feat(v2): complete n8n workflow template (36 nodes) covering full V2 pipeline + shadow branch

- 14 new Code node drivers implementing the pipeline stages: normalize
  candidates, build context SQL, merge context + hash, decide AI need,
  prepare AI prompt, validate AI response, AI-disabled fallback, reuse
  cached classification, compute score, select top 10, prepare memory/
  envio records, build message/digest, compare legacy x V2, debug panel.
- Consolidated the two context queries (followups/tasks) into a single
  UNION ALL query tagged by tipo, and added a "Merge - Unir Shadow"
  synchronization node before comparing legacy x V2: n8n does not
  guarantee relative execution order between sibling branches that only
  share an early common ancestor, so cross-branch data was only safe to
  read by name once a real Merge node forced both to complete first.
- Reordered score calculation before the memory upsert (score is needed
  in the persisted row) and turned both Data Table writes into
  side-effect branches that fan out from the main path instead of
  gating it, so the Data Table node's own output shape never needs to
  carry the full pipeline state forward.
- Fixed a duplicate-identifier bug in the build script: score.js's
  require('./cooldown') survived inlining and collided with
  cooldown.js's own computeCooldown function in the same Code node
  scope. build-workflow.js now strips inter-module requires when
  inlining, and a new test syntax-checks every generated jsCode string
  plus asserts no leftover require() calls.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Gustavo
2026-08-24 15:31:44 -03:00
co-authored by Claude Sonnet 5
parent ec596a29a8
commit a99a25cb1a
24 changed files with 1818 additions and 76 deletions
@@ -1248,8 +1248,7 @@ git commit -m "feat(v2): add short message and coordination digest builders"
**Files:**
- Create: `sql/candidatos_v2_glpi.sql`
- Create: `sql/contexto_followups_glpi.sql`
- Create: `sql/contexto_tarefas_glpi.sql`
- Create: `sql/contexto_glpi.sql`
- Create: `n8n/nodes/build-sql-candidatos.node.js`
- Create: `n8n/nodes/build-sql-legado.node.js`
- Test: `n8n/nodes/build-sql-candidatos.node.test.js`
@@ -1344,26 +1343,24 @@ ORDER BY score_criticidade DESC, sla_vencido DESC, dias_sem_interacao DESC, dias
LIMIT {CANDIDATE_POOL_SIZE};
```
- [ ] **Step 2: Criar `sql/contexto_followups_glpi.sql` e `sql/contexto_tarefas_glpi.sql`**
`sql/contexto_followups_glpi.sql`:
- [ ] **Step 2: Criar `sql/contexto_glpi.sql`** — followups e tarefas numa unica query (`UNION ALL`, marcada por `tipo`), para que o node MySQL de contexto seja um unico ancestral direto de `Code - Mesclar Contexto e Calcular Hash` (evita depender da ordem relativa de dois nodes MySQL irmaos sem sincronizacao garantida pelo n8n).
```sql
-- Followups publicos dos candidatos (IDs interpolados pelo node, validados como inteiros).
SELECT items_id AS tickets_id, id, date, content AS texto
-- Contexto (followups publicos + tarefas) dos candidatos, numa unica query
-- para garantir que o node de merge so roda depois que os dois tipos ja chegaram.
-- Coluna de status de glpi_tickettasks (state) a confirmar via DESCRIBE quando a
-- credencial MySQL estiver disponivel (Task 14) -- ver nota no plano.
SELECT 'followup' AS tipo, items_id AS tickets_id, id, date, content AS texto, NULL AS status
FROM glpi_itilfollowups
WHERE itemtype = 'Ticket' AND is_private = 0 AND items_id IN ({IDS})
ORDER BY items_id, date ASC;
```
`sql/contexto_tarefas_glpi.sql`:
UNION ALL
```sql
-- Tarefas dos candidatos. Coluna de status a confirmar via DESCRIBE glpi_tickettasks
-- quando a credencial MySQL estiver disponivel (Task 14) -- ver nota no plano.
SELECT tickets_id, id, date, state AS status, content AS texto
SELECT 'task' AS tipo, tickets_id, id, date, content AS texto, state AS status
FROM glpi_tickettasks
WHERE tickets_id IN ({IDS})
ORDER BY tickets_id, date ASC;
```
@@ -1439,7 +1436,7 @@ Expected: PASS
- [ ] **Step 6: Commit**
```bash
git add sql/candidatos_v2_glpi.sql sql/contexto_followups_glpi.sql sql/contexto_tarefas_glpi.sql n8n/nodes/build-sql-candidatos.node.js n8n/nodes/build-sql-legado.node.js n8n/nodes/build-sql-candidatos.node.test.js
git add sql/candidatos_v2_glpi.sql sql/contexto_glpi.sql n8n/nodes/build-sql-candidatos.node.js n8n/nodes/build-sql-legado.node.js n8n/nodes/build-sql-candidatos.node.test.js
git commit -m "feat(v2): candidate pool SQL (admin exclusion preserved, no hard LIMIT 10) + verbatim legacy SQL for shadow mode"
```
@@ -1536,8 +1533,7 @@ function buildDriverCode(driverRelativePath) {
driver = driver.replace('/* __SQL_CANDIDATOS_V2__ */', sqlAsJsTemplateLiteral('sql/candidatos_v2_glpi.sql'));
driver = driver.replace('/* __SQL_LEGADO_V1_VERBATIM__ */', sqlAsJsTemplateLiteral('sql/top_10_chamados_abertos_glpi.sql'));
driver = driver.replace('/* __SQL_CONTEXTO_FOLLOWUPS__ */', sqlAsJsTemplateLiteral('sql/contexto_followups_glpi.sql'));
driver = driver.replace('/* __SQL_CONTEXTO_TAREFAS__ */', sqlAsJsTemplateLiteral('sql/contexto_tarefas_glpi.sql'));
driver = driver.replace('/* __SQL_CONTEXTO__ */', sqlAsJsTemplateLiteral('sql/contexto_glpi.sql'));
const logicBlock = requiredLogicFiles.length ? `${inlinedLogic(requiredLogicFiles)}\n\n` : '';
return `${logicBlock}${driver.trim()}`;
@@ -1600,6 +1596,8 @@ git commit -m "feat(v2): add build script that inlines tested logic/*.js into n8
- Create: `n8n/nodes/build-coordination-digest.node.js`
- Create: `n8n/nodes/compare-legacy-v2.node.js`
- Create: `n8n/nodes/debug-panel.node.js`
- Create: `n8n/nodes/prepare-memoria-analise-record.node.js` (prepara a linha para `Data Table - Upsert Memoria Analise`, ramo efeito-colateral)
- Create: `n8n/nodes/prepare-envio-record.node.js` (prepara a linha para `Data Table - Registrar Envio`, filtra `_grupo==='selected'`, ramo efeito-colateral)
**Interfaces:**
- Cada driver assume que as funções de `logic/*.js` listadas em `// @requires` já estão no escopo (inlined pelo build script da Task 12). Cada um consome `$input.all()`/`$input.first()` (shape n8n padrão) e os nodes anteriores via `$('Nome do Node')`, seguindo exatamente o padrão já usado no V1 e no workflow `[DEV] GTI - GLPI x WhatsApp v7`.
@@ -1612,8 +1610,7 @@ Manual Trigger
-> Code - Montar SQL Candidatos -> MySQL - Buscar Candidatos
-> Code - Normalizar Candidatos Base
-> Code - Montar SQL Contexto
-> MySQL - Buscar Followups
-> MySQL - Buscar Tarefas
-> MySQL - Buscar Contexto (followups + tarefas numa unica query UNION ALL, marcada por `tipo` -- evita depender da ordem relativa de dois nodes MySQL irmaos)
-> Code - Mesclar Contexto e Calcular Hash
-> Data Table - Buscar Memoria (get, returnAll)
-> Code - Decidir Necessidade de IA
@@ -1622,16 +1619,20 @@ Manual Trigger
[true] -> Code - Preparar Prompt IA -> HTTP - OpenAI Classificar Chamado -> Code - Validar Classificacao IA -> Merge - Consolidar Classificacoes (input 0)
[false] -> Code - Fallback IA Desligada -> Merge - Consolidar Classificacoes (input 1)
[false] -> Code - Reusar Classificacao em Cache -> Merge - Consolidar Classificacoes (input 2)
Merge - Consolidar Classificacoes
-> Data Table - Upsert Memoria Analise
-> Code - Calcular Score
-> Code - Selecionar Top 10 (Run Once for All Items)
-> Data Table - Registrar Envio
-> Code - Montar Mensagem Curta
-> Code - Montar Digest Coordenacao
-> Code - Comparar Legacy x V2
-> Data Table - Registrar Shadow Run
-> Code - Montar SQL Legado -> MySQL - Legado Top 10 (Shadow) [alimenta Code - Comparar Legacy x V2 via $('MySQL - Legado Top 10 (Shadow)').all(), mesmo padrao de leitura cruzada ja usado no V1/WhatsApp workflow]
Merge - Consolidar Classificacoes (mode: append)
-> Code - Calcular Score
-> Code - Selecionar Top 10 (Run Once for All Items) [caminho principal]
-> Code - Montar Mensagem Curta
-> Code - Montar Digest Coordenacao
-> Merge - Unir Shadow (append, input 0)
-> Code - Preparar Registro Memoria Analise -> Data Table - Upsert Memoria Analise [ramo efeito-colateral, sem continuidade]
(a partir de Code - Selecionar Top 10, tambem:)
-> Code - Preparar Registro de Envio (filtra _grupo==='selected') -> Data Table - Registrar Envio [ramo efeito-colateral]
-> Code - Montar SQL Legado -> MySQL - Legado Top 10 (Shadow) -> Merge - Unir Shadow (append, input 1)
Merge - Unir Shadow (append)
-> Code - Comparar Legacy x V2 [le os dados por nome -- $('Code - Montar Digest Coordenacao').first() e $('MySQL - Legado Top 10 (Shadow)').all() -- o Merge so existe para GARANTIR que os dois ja executaram antes deste node rodar; ler pelo item mesclado seria fragil]
-> Data Table - Registrar Shadow Run
Debug - Ver Painel (Webhook GET, path "coordenador-glpi-v2-debug")
-> Data Table - Buscar Shadow Runs Recentes (get, returnAll, sortBy run_at:desc, limit implicito pelos ultimos N)
@@ -1641,8 +1642,8 @@ Debug - Ver Painel (Webhook GET, path "coordenador-glpi-v2-debug")
- [ ] **Step 1: Para cada node de Code listado acima, criar o arquivo driver em `n8n/nodes/` seguindo o padrão dos já criados na Task 11** (comentário `// @requires <logic files>` no topo quando aplicável; corpo usa `$input`, `$('Nome do Node')`, `$env`, retorna `return [{ json: {...} }];` ou, para o de seleção final, roda uma vez para todos os itens: usar `$input.all()` e retornar múltiplos itens). Cada driver deve:
- `Code - Normalizar Candidatos Base`: `@requires context.js` — mapeia `$input.all()` com `normalizeTicket(item.json, { followups: [], tasks: [] })`.
- `Code - Montar SQL Contexto`: monta as duas queries de `sql/contexto_followups_glpi.sql`/`contexto_tarefas_glpi.sql` substituindo `{IDS}` pelos ids normalizados (`Number(id)`, `join(',')`, nunca concatenando string crua do GLPI — todos os ids já vêm como inteiro da query anterior).
- `Code - Mesclar Contexto e Calcular Hash`: `@requires hash.js` — junta `$('Code - Normalizar Candidatos Base').all()` com `$('MySQL - Buscar Followups').all()` e `$input.all()` (tarefas), agrupa por `tickets_id`, chama `buildContextFingerprint`.
- `Code - Montar SQL Contexto`: monta a query única de `sql/contexto_glpi.sql` substituindo `{IDS}` pelos ids normalizados (`Number(id)`, `join(',')`, nunca concatenando string crua do GLPI — todos os ids já vêm como inteiro da query anterior).
- `Code - Mesclar Contexto e Calcular Hash`: `@requires context.js hash.js` — junta `$('Code - Normalizar Candidatos Base').all()` com `$input.all()` (linhas de `MySQL - Buscar Contexto`, separadas por `tipo==='followup'|'task'`), agrupa por `tickets_id`, chama `buildContextFingerprint`.
- `Code - Decidir Necessidade de IA`: `@requires cache.js` — para cada candidato, busca a linha de memória correspondente (de `$('Data Table - Buscar Memoria').all()`) e chama `needsReanalysis`; ordena por `score_criticidade` e limita a `config.aiMaxCandidates` os que vão para IA; os excedentes recebem `precisa_ia:false` com motivo `ai_max_candidates_cap` e caem no branch de fallback determinístico (não ficam sem classificação).
- `Code - Preparar Prompt IA`: `@requires context.js` — monta `system` (regras da seção 16 da missão: não inventar fatos, distinguir evidência de inferência, JSON estrito, SLA vencido sozinho não é evidência de acionabilidade, idade sozinha não é acionabilidade) e `user` = `buildAiContextText(ticket, config.aiMaxContextChars)`.
- `Code - Validar Classificacao IA`: `@requires classification.js, fallback.js` — tenta `JSON.parse` da resposta; se falhar ou `validateAiClassification` retornar `valid:false`, usa `buildFallbackClassification('invalid_json')`; se o node HTTP anterior tiver `error` (timeout/HTTP≠2xx, capturado via `continueOnFail`), usa `buildFallbackClassification('ai_error')`.
@@ -1656,7 +1657,7 @@ Debug - Ver Painel (Webhook GET, path "coordenador-glpi-v2-debug")
- `Code - Montar Painel Debug`: monta um HTML/JSON simples com a última linha de `coordenador_glpi_shadow_runs` + contagem de memória (mesmo padrão do node `Debug - Montar Painel` do workflow `[DEV] GTI - GLPI x WhatsApp v7`).
- [ ] **Step 2: Adicionar ao `n8n/workflow.template.json` todos os nodes não-Code com seus `parameters` finais**:
- `MySQL - Buscar Candidatos`, `MySQL - Buscar Followups`, `MySQL - Buscar Tarefas`, `MySQL - Legado Top 10 (Shadow)`: `type: n8n-nodes-base.mySql`, `parameters: { operation: 'executeQuery', query: "={{ $json.query }}" }`, `credentials: { mySql: { name: 'MySQL GLPI - Coordenador' } }` (sem `id` — resolvido na Task 14 quando o id real existir).
- `MySQL - Buscar Candidatos`, `MySQL - Buscar Contexto`, `MySQL - Legado Top 10 (Shadow)`: `type: n8n-nodes-base.mySql`, `parameters: { operation: 'executeQuery', query: "={{ $json.query }}" }`, `credentials: { mySql: { name: 'MySQL GLPI - Coordenador' } }` (sem `id` — resolvido na Task 14 quando o id real existir).
- `Data Table - Buscar Memoria`: `type: n8n-nodes-base.dataTable`, `parameters: { resource: 'row', operation: 'get', dataTableId: { mode: 'name', value: 'coordenador_glpi_memoria' }, returnAll: true }`.
- `Data Table - Upsert Memoria Analise` e `Data Table - Registrar Envio`: `operation: 'upsert'` no mesmo `dataTableId`, campos mapeados via `columns` (resourceMapper — ver Task 14 para o shape exato, resolvido com `n8n_validate_workflow`/`n8n_autofix_workflow`).
- `Data Table - Registrar Shadow Run` e `Data Table - Buscar Shadow Runs Recentes`: mesma lógica sobre `coordenador_glpi_shadow_runs`.
@@ -1692,7 +1693,7 @@ git commit -m "feat(v2): complete n8n workflow template (25+ nodes) covering ful
- [ ] **Step 1: Confirmar a credencial MySQL** — chamar `n8n_manage_credentials` (`action: list`) e localizar `MySQL GLPI - Coordenador`. Se ainda não existir, parar aqui e avisar o usuário (não simular/hardcodar). Se existir, anotar o `id`.
- [ ] **Step 2: Introspecção obrigatória do schema real (nunca assumir)** — com a credencial MySQL viva, rodar via node `mysql` temporário (`n8n_test_workflow` num workflow mínimo de 1 node, ou reaproveitando `MySQL - Buscar Candidatos` já criado) as queries:
- `SHOW COLUMNS FROM glpi_tickettasks;` — confirmar se o campo de status é `state` (assumido no Task 11) ou outro nome; ajustar `sql/contexto_tarefas_glpi.sql` se necessário.
- `SHOW COLUMNS FROM glpi_tickettasks;` — confirmar se o campo de status é `state` (assumido no Task 11) ou outro nome; ajustar `sql/contexto_glpi.sql` se necessário.
- `SHOW COLUMNS FROM glpi_itilfollowups;` — confirmar `content`/`date`/`is_private` existem como assumido.
- Rodar `sql/candidatos_v2_glpi.sql` (com `{CANDIDATE_POOL_SIZE}`→`5` para teste rápido) e conferir que retorna linhas plausíveis.
- Registrar tudo (colunas confirmadas, ajustes feitos) em `docs/superpowers/notes/2026-08-24-schema-introspection.md`.