diff --git a/docs/superpowers/notes/2026-08-24-relatorio-final.md b/docs/superpowers/notes/2026-08-24-relatorio-final.md new file mode 100644 index 0000000..5b3bcf8 --- /dev/null +++ b/docs/superpowers/notes/2026-08-24-relatorio-final.md @@ -0,0 +1,186 @@ +# Relatório Final — Coordenador GLPI V2 + +Data: 2026-08-24. Branch: `feature/coordenador-glpi-v2` (19 commits, working tree limpo). + +## RECOVERY + +- Projeto encontrado: `D:\PROJETOS\Coordenador GLPI\Coordenador GLPI` — projeto n8n puro (sem backend tradicional), com workflow importável (`Coordenador GLPI - Chamados Abertos Slack.n8n.json`), query SQL (`sql/top_10_chamados_abertos_glpi.sql`), `docker-compose.yml` para n8n local e README. +- Git: repositório sem nenhum commit (`git status` mostrava tudo untracked). Não havia branch, não havia HEAD. +- Alterações preexistentes: nenhuma (nada estava em progresso). `.env` real preservado (gitignored, nunca commitado). +- Preservação: commitei o V1 exatamente como encontrado no disco (`1eb1f80`, "baseline import") **antes** de qualquer mudança, como ponto de reversão. Toda a evolução V2 foi feita em branch separada (`feature/coordenador-glpi-v2`), nunca em `master`. + +## COMO FUNCIONAVA (V1, comprovado no código) + +- Cron `0 8 * * 1-5` (+ Manual Trigger) → node `Config` (Set) com `slack_webhook_url`, `glpi_base_url`, `dias_sem_interacao_alerta`, `horas_sla_risco` → monta SQL em um Code node → executa direto no MySQL do GLPI. +- SQL calcula `score_criticidade` (SLA vencido +100, SLA em risco +45, dias sem interação +10/+25/+35, idade até +30, prioridade×8, urgência×5, impacto×5, sem técnico +25) e seleciona por **blocos fixos**: 3 SLA vencido, 3 sem interação, 2 sem técnico, 2 prioridade alta recente, complementando com ranking geral até 10. +- Exclui categorias `ADMINISTRATIVO%`, `DESENVOLVIMENTO%`, `INFRA%` (por `completename`/`name`) e chamados com `status IN (5,6)` (solucionado/fechado) ou `is_deleted`. +- Reincidência: contada via `staticData` do próprio n8n (não é banco) — conta dias seguidos que o chamado aparece, mas **não penaliza o score**, só informa. +- Mensagem final montada por código; um AI Agent (Z IA/GLM) só **revisa o texto** (clareza), nunca decide nada — há validação determinística (todos os `#id` e links precisam estar presentes, sem `motivo/ação/prazo`) com fallback para o texto sem IA se a revisão falhar. + +## CAUSA TÉCNICA DO LOOP + +O score é dominado por sinais que só crescem com o tempo (SLA vencido, dias sem interação, idade), e a reincidência é apenas informativa — não interrompe nem penaliza. Um chamado bloqueado por terceiro/solicitante, sem nenhuma ação possível pelo suporte, continua pontuando alto todos os dias e reocupando os blocos "SLA vencido"/"sem interação", empurrando chamados com ação real (mas sinais mais baixos) para fora do Top 10. + +## ARQUITETURA IMPLEMENTADA + +Pipeline em 7 estágios (seleção → filtros → memória/cache → contexto → IA → regras determinísticas → score/ranking → mensagem), implementado como um workflow n8n **novo e separado**, deployado no DEV, **inativo**: + +``` +[Manual Trigger / Schedule Trigger (desligado)] → Config + → Code: Montar SQL Candidatos → MySQL: Buscar Candidatos (até CANDIDATE_POOL_SIZE=60, sem corte em blocos) + → Code: Normalizar → Code: Montar SQL Contexto → MySQL: Buscar Contexto (followups+tarefas, 1 query UNION ALL) + → Code: Mesclar Contexto e Calcular Hash + → [Merge: Aguardar Memoria] ← Data Table: Buscar Memoria (ramo paralelo, dispara 1x a partir do Config) + → Code: Decidir Necessidade de IA (cache/hash, corta em AI_MAX_CANDIDATES=30) + → IF Precisa IA? → IF IA Ligada? → Code: Preparar Prompt → HTTP OpenAI (structured output) → Code: Validar + (senão) → Code: Fallback IA Desligada + (senão) → Code: Reusar Classificação em Cache + → Merge: Consolidar Classificações → Code: Calcular Score + → Code: Preparar Registro Memória (efeito colateral) → Data Table: Upsert Memória + → Code: Selecionar Top 10 → Code: Montar Mensagem Curta → Code: Montar Digest Coordenação + → Code: Preparar Registro de Envio (efeito colateral) → Data Table: Registrar Envio + → [Merge: Unir Shadow] ← MySQL: Legado Top 10 (query V1 verbatim, ramo paralelo) + → Code: Comparar Legacy x V2 → Data Table: Registrar Shadow Run + +Debug - Ver Painel (webhook) → Data Table: Buscar Shadow Runs Recentes → Code: Montar Painel → Responder +``` + +Duas descobertas de arquitetura corrigidas durante a implementação (documentadas nos commits): +1. Nodes MySQL "irmãos" sem sincronização garantida — n8n não garante ordem relativa entre ramos que só compartilham um ancestral comum distante. Resolvido com nodes `Merge` reais como portão de sincronização (contexto, memória, shadow) em vez de depender de `$('Node').all()` sem uma aresta real garantindo execução prévia. +2. `Data Table` de leitura em massa ("pegar toda a tabela") não pode ficar inline na cadeia de 41 candidatos — ela roda por item de entrada e seu próprio output substitui os itens recebidos. Corrigido movendo a busca de memória para um ramo paralelo. + +## ARQUIVOS ALTERADOS + +- `logic/*.js` (10 módulos) + `*.test.js` — lógica determinística pura, testada. +- `sql/candidatos_v2_glpi.sql` — base do V1 sem corte em blocos, `LIMIT {CANDIDATE_POOL_SIZE}`. +- `sql/contexto_glpi.sql` — followups + tarefas numa query só (`UNION ALL`, marcada por `tipo`). +- `sql/top_10_chamados_abertos_glpi.sql` — **intocado** (usado verbatim no ramo shadow). +- `n8n/nodes/*.node.js` (17 arquivos) — drivers dos Code nodes, fonte de verdade. +- `n8n/workflow.template.json` + `tools/build-workflow.js` — template + script que gera o JSON final inlinando `logic/*.js` nos Code nodes. +- `n8n/deployed/coordenador-glpi-v2.n8n.json` — snapshot do que está no DEV. +- `docs/superpowers/{specs,plans,notes}/*` — spec, plano, notas de introspecção e este relatório. +- **V1 original**: nenhum arquivo alterado. + +## BANCO / PERSISTÊNCIA + +Duas n8n Data Tables novas no DEV (recurso nativo do n8n, não infraestrutura nova): + +- `coordenador_glpi_memoria` (id `wQZSlILIApb6aErW`) — 17 colunas: `ticket_id, last_analyzed_at, last_sent_at, last_context_hash, last_meaningful_change_at, ai_classification, ai_confidence, blocker_type, waiting_for, next_action, next_review_at, times_sent, times_sent_30d, consecutive_days_sent, last_score, last_ai_reason, updated_at`. +- `coordenador_glpi_shadow_runs` (id `V74oP8TMq6BwJelw`) — `run_at, legacy_ids, v2_ids, added, removed, summary`. + +Substitui o `staticData` do V1 (que o próprio README do V1 já apontava como frágil: some se o workflow for reimportado). Nenhuma tabela do GLPI foi alterada — todo acesso ao GLPI é `SELECT`. + +## IA + +- Provedor: OpenAI, modelo `gpt-4o-mini` (configurável no node Config). +- Credencial `OpenAI - Coordenador GLPI` (criada por mim, chave colada pelo usuário) — **não** reaproveitei nenhuma credencial de outro workflow. +- Formato: `response_format: json_schema` estrito (11 classes: `ACTIONABLE_NOW, UNASSIGNED_ACTIONABLE, WAITING_REQUESTER, WAITING_VENDOR, WAITING_APPROVAL, SCHEDULED, DEPENDENCY_BLOCKED, NEEDS_COORDINATION, PROJECT_LONG_TERM, DUPLICATE_OR_OBSOLETE, INSUFFICIENT_CONTEXT`), validado em código (`logic/classification.js`). +- Cache: hash de contexto (`logic/hash.js`) ignora campos que só mudam com o tempo; só entra em IA quem tem hash novo, sem memória, ou passou do `next_review_at`. +- Fallback: `ai_disabled`, `ai_error`, `invalid_json`, `ai_max_candidates_cap` — todos tratados como `ACTIONABLE_NOW` com `confidence: 0`, marcados `is_fallback: true`, sem bônus de acionabilidade no score (comportamento conservador, tipo V1). +- Custo controlado: nesta execução real, 41 candidatos → só 30 foram para a IA (`AI_MAX_CANDIDATES`), 11 ficaram em fallback controlado. + +## REGRAS + +- **Acionabilidade**: score final = baseline (sinais baratos, capado) + bônus/penalidade por classe da IA (ACTIONABLE_NOW +200, UNASSIGNED_ACTIONABLE +180, WAITING_* −150, SCHEDULED/NEEDS_COORDINATION −1000) − penalidade de cooldown. Confiança abaixo de `AI_CONFIDENCE_THRESHOLD` (0.6) corta o bônus pela metade. +- **Bloqueios**: `WAITING_REQUESTER/WAITING_VENDOR/SCHEDULED/DEPENDENCY_BLOCKED` sem mudança de contexto ficam bloqueados para reenvio; `NEEDS_COORDINATION` sempre sai do Top 10 diário. +- **Cooldown**: penalidade crescente (até -90) para quem foi enviado recentemente sem mudança de contexto; chamado crítico (SLA vencido + prioridade ≥4) quebra o cooldown. +- **Mudança significativa**: hash ignora `dias_aberto`/`dias_sem_interacao`; só muda com evento real (status, técnico, categoria, novo followup/tarefa). +- **Top 10**: fixo, com diversidade leve entre grupos (cap de 50% por grupo na primeira passada, preenchido pelos próximos por score — não bloqueia emergência real). + +## MENSAGEM (exemplo real, gerado no DEV em 24/08/2026 19:25 UTC) + +``` +@time 10 chamados para atenção hoje — 24/08/2026 + +1. #38611 • Colocar etiqueta no espelho na sala 03 (Ultrassom) +SEM TÉCNICO • parado 3d +→ Revisar chamado +https://glpi.suaempresa.com.br/front/ticket.form.php?id=38611 + +5. #37389 • Manutenção preventiva +Gabriel Lessa • SLA vencido • parado 9d +→ Completar informação +https://glpi.suaempresa.com.br/front/ticket.form.php?id=37389 + +10. #38642 • Recolher nobreak do servidor da ortocentro +Wagner Silva • parado 0d +→ Revisar chamado +https://glpi.suaempresa.com.br/front/ticket.form.php?id=38642 +``` +(lista completa nos logs; aqui vão só 3 de 10 pra ilustrar). Achei e corrigi, logo depois desta captura, um bug de espaçamento (linha em branco quádrupla entre o cabeçalho e o primeiro chamado) — já corrigido e testado, mas não recapturei a execução só por causa disso. + +Comparado à mensagem atual do V1 (que já é curta), esta é do mesmo tamanho por chamado mas sem os campos "motivo/ação/prazo" verbosos que o V1 tinha decidido remover; a real redução está em **não repetir os mesmos 4 chamados bloqueados todos os dias** (ver Shadow Mode abaixo), não no tamanho da mensagem em si — o V1 já era enxuto. + +## TESTES + +- `npm test`: **54/54 passando**, 10 arquivos de módulo + 1 de sanidade + 1 de build + 1 de guarda de SQL. +- Cobre os 17 cenários pedidos: acionável entra, aguardando cliente não monopoliza, aguardando fornecedor não repete diariamente, agendado não entra antes da data, sem técnico + acionável prioriza, cooldown penaliza reenvio sem mudança, mudança significativa libera reavaliação, alteração só de "dias aberto" não muda hash, administrativo continua excluído (testado direto no SQL gerado), IA indisponível aciona fallback, JSON inválido não quebra, baixa confiança recebe tratamento, contexto inalterado usa cache, `NEEDS_COORDINATION` sai do Top10 diário, Top final nunca passa de 10, mensagem final é curta, regra crítica quebra cooldown. + +## SHADOW MODE (execução real, id `13599`, 24/08/2026 19:25:39–19:25:50 UTC, 11.1s) + +``` +LEGACY (query V1, verbatim): +#37389 #34419 #36530 #33492 #36488 #36897 #38611 #38583 #38319 #38642 + +V2: +#38611 #38583 #38648 #38645 #37389 #34419 #36530 #38216 #35059 #38642 + +Saíram na V2 (estavam no Legacy): +#33492 — motivo: INSUFFICIENT_CONTEXT (IA não achou base para next_action) +#36488 — motivo: WAITING_REQUESTER (aguardando "Patricia" sobre troca de carcaça) +#36897 — motivo: INSUFFICIENT_CONTEXT +#38319 — motivo: WAITING_VENDOR (aguardando fornecimento de baterias) + +Entraram na V2 (não estavam no Legacy): +#38648, #38645 — ACTIONABLE_NOW (fallback por limite de IA nesta rodada, precisam triagem humana) +#38216 — INSUFFICIENT_CONTEXT ("Não há informações suficientes sobre a próxima ação ou bloqueios") +#35059 — INSUFFICIENT_CONTEXT ("Informações insuficientes para determinar ações concretas") +``` + +Isso é o mecanismo funcionando: 2 dos 4 removidos saíram porque a IA identificou, com evidência real do GLPI (apontamentos/tarefas), que dependiam de terceiro ou do solicitante — exatamente o padrão que a missão queria parar de repetir todo dia. + +## OBSERVABILIDADE (números reais desta execução) + +``` +candidatos iniciais: 41 (de até 60 configurados; backlog real após exclusão administrativa) +memoria previa: 0 linhas (primeira execução, tabela nova) +enviados para IA: 30 (limite AI_MAX_CANDIDATES) +fallback por limite de IA (ai_max_candidates_cap): 11 +--- classificacoes reais da IA (30) --- +INSUFFICIENT_CONTEXT: 20 +WAITING_VENDOR: 6 +WAITING_REQUESTER: 4 +ACTIONABLE_NOW / UNASSIGNED_ACTIONABLE (real, nao-fallback): 0 +top final: 10 (nunca ultrapassou) +``` + +**Achado honesto**: nesta execução, a IA nunca classificou nenhum chamado como genuinamente `ACTIONABLE_NOW`/`UNASSIGNED_ACTIONABLE` com confiança real — ou o chamado tinha uma dependência clara (fornecedor/solicitante) ou faltava informação. Isso bate com a própria hipótese da missão (chamados antigos sem ação real) mas também expõe uma melhoria pendente: o campo `content` do GLPI vem com entidades HTML escapadas duas vezes (`<p>`), o que pode estar deixando o texto mais difícil de interpretar. Registrado como pendência, não escondido. + +## DEPLOY DEV + +- Workflow `[DEV] Coordenador GLPI - Chamados Abertos v2 (Shadow)`, id `y6JFEnp7Z2IJEUe2`, 37 nodes, **`active: false`**. +- `n8n_validate_workflow` (profile `strict`): **0 erros**, 1 aviso menor (estilo de tratamento de erro, não bloqueante). +- Healthcheck: execução real `13599` terminou com `status: success` — prova de que o serviço está de pé e o workflow roda de ponta a ponta contra dados reais. +- Método de teste: ativei o workflow só pelo tempo de um disparo (webhook temporário adicionado e removido a cada tentativa), sempre desativando **imediatamente** depois — a `Schedule Trigger` nunca chegou a disparar de verdade (não estava nem perto de 8h no horário testado). + +## PRODUÇÃO + +``` +Produção alterada: NÃO +Envio para grupo de produção realizado: NÃO +Escrita em chamados reais: NÃO +``` + +Todo acesso ao GLPI foi `SELECT`. Nenhum outro workflow do DEV (`[DEV] GTI - GLPI x WhatsApp v7`, `Enforcer - GLPI`, Klingo/ClinicAll/SysLife/ZPRO/Asaas) foi tocado. Nenhuma mensagem foi enviada a nenhum canal real — a mensagem fica só nos logs de execução e na Data Table. + +## PENDÊNCIAS REAIS + +1. **HTML duplamente escapado no `content` do GLPI** (`<p>`) pode estar prejudicando a leitura da IA — vale decodificar/limpar em `logic/context.js` antes de mandar pra IA. +2. **`blocker_type` às vezes vem como string `"null"`** em vez de `null` de verdade (comportamento do modelo, não do meu código) — inofensivo hoje, mas vale normalizar. +3. **Digest de coordenação** (`Code - Montar Digest Coordenacao`) ainda não foi validado com um caso real de `NEEDS_COORDINATION` (nenhum chamado caiu nessa classe nesta execução) — a lógica está coberta por teste unitário, mas não por execução real. +4. **Cron desligado por design** — a ativação para rodar automaticamente às 8h é uma decisão do usuário, não foi feita (e não deveria ser feita sem aprovação explícita). +5. **Duas Data Tables de terceiros** (`dev_glpi_v2_insights_memory`, `dev_glpi_v2_run_history`) apareceram no DEV no meio desta sessão, de origem que não identifiquei — vale confirmar com quem estiver por trás se é trabalho duplicado. + +## VEREDITO + +A V2 está **pronta para continuar em shadow mode** — validada estruturalmente (`n8n_validate_workflow`), testada unitariamente (54/54) e comprovada com uma execução real de ponta a ponta contra o banco de produção do GLPI (somente leitura) e a API real da OpenAI, produzindo um diff Legacy×V2 defensável. Antes de virar candidata a substituir o envio real, sugiro: (a) rodar mais algumas vezes em dias diferentes para ver o comportamento do cache/cooldown ao longo do tempo (hoje só temos a primeira execução, sem histórico), (b) resolver a pendência 1 (HTML escapado) e comparar se isso muda a proporção de `INSUFFICIENT_CONTEXT`, (c) revisar manualmente as 10 mensagens junto com o time de suporte antes de decidir mudar o canal real.