mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
main
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
02d9aceacf |
refactor(mcp): remove a description morta do catálogo — 51 cópias que ninguém lia
`lib/mcp/tools/catalogo/*.ts` declarava `description` em 51 capacidades; o tipo a documentava como "Texto tecnico entregue ao MODELO" e sete cabeçalhos repetiam "`description` fala com o modelo". Nenhum consumidor lia esse campo: a ponte do turno (`lib/ai/runtime/tools.ts`) monta `def.description`, do HANDLER, e a rota `/api/v1/mcp/tools` também. As duas fontes divergiam em 48 das 51. ## Por que remover, e não sincronizar Fonte única mudaria o que 48 tools dizem ao modelo — risco alto, ganho zero. Um gate de paridade obrigaria manter dois textos sincronizados para sempre, com a duplicata seguindo lá para ser editada por engano. Remover mata a armadilha na raiz: não dá para editar o lugar errado se o lugar não existe. E a remoção SE AUTO-VERIFICA. Tirei o campo do tipo e o `tsc` apontou cada leitura — prova mais forte que grep. ## O que o typecheck achou: a dívida não era teórica Um leitor, e o pior possível: `evidence/ia-360-w4/medicao-vazamento/remedir-com-operador.ts`, o script que mede vazamento de vocabulário do agente. A função se chama `descreverFerramentas` e o comentário diz "A ferramenta como o modelo a vê: nome + descrição, que é o que pode vazar" — e lia `TOOL_CATALOG.description`, exatamente o texto que o modelo NÃO vê. NÃO MEDIDO: se isso muda o resultado daquela medição. O `name` é idêntico nas duas fontes e é o vetor principal de vazamento, então o efeito pode ser nulo, mas não rodei. Corrigi a fonte para `allTools` e deixei a ressalva no script. Não reabri a medição arquivada de outra branch. ## O buraco que a remoção expôs `tests/unit/catalogo-servido.test.ts` testava a junção com FIXTURES (`description: "faz algo"`), nunca com o catálogo real — nenhum gate garantia que uma capacidade servida tem descrição. Esvaziar a de um handler passaria calado: a tela sem explicação e o modelo com uma ferramenta sem contrato. Caso novo: toda capacidade servida tem descrição não-vazia E ela é IDÊNTICA à do handler. A segunda metade é a que importa — se reaparecer uma cópia no catálogo e a junção preferi-la, reprova. Sabotagens: `description` de um handler vira "" → 1 → 1 (a primeira tentativa não sabotou nada: escrevi `description: "" ||`, e `"" || "texto"` devolve o texto — instrumento quebrado, não gate fraco); junção servindo outro texto → 1 → 2. ## Correção de um número que publiquei A mensagem do commit |
||
|
|
20d699affd |
feat(radar): a IA enxerga as demandas sem próximo passo (passo 4 do cap. 5)
O Radar deu a lista ao humano; faltava o outro lado do invariante 2. Quem pode
agendar o retorno que falta às três da manhã é a IA.
`crm_list_at_risk_leads` já devolvia `sem_proximo_passo` no payload desde a
migração do Radar — e não servia para nada, porque o modelo só usa o que a
`description` promete. Dado que chega sem ser declarado é, para o agente, o
equivalente a um campo que a tela recebe e não pinta. A descrição agora declara
o campo, os subcampos e as duas saídas (`crm_schedule_followup` por contact_id,
`crm_close_demand`) — sem nomear a saída, o modelo vê o problema e não sabe o
que fazer com ele.
## O achado que valia mais que a tarefa
`carregaRadarDeRisco` faz 7 leituras tenant-aware com SERVICE ROLE, que bypassa
RLS: o `.eq("organization_id", …)` é a única defesa. Medido:
Sabotagem: remover o filtro de org da leitura de `demandas`
Previsão: 0 reprovações Resultado: 0
O teste existente se chamava "toda leitura filtra a org do contexto" e
exercitava UMA leitura, com o resolver devolvendo [] — o que fazia a função
retornar cedo e as outras seis nem acontecerem. A leitura que eu mesmo adicionei
no commit anterior entrou sob esse álibi (anti-pattern #10 da doutrina).
Duas camadas, porque uma não alcança a outra:
* unit — que o filtro é EMITIDO, nas 7 leituras;
* `tests/sonda-radar-isolamento-orgs.ts` — que ele SEPARA, com 2 inquilinos
reais. Não virou invariante de `test:db` por um motivo medido: aquele runner
sobe só Postgres, sem PostgREST, e supabase-js não roda lá; um teste em SQL
só reescreveria o predicado, que é testar o teste.
## Sabotagens (previsão antes de rodar)
filtro de org fora (unit) 1 → 1
ignorar o array do join do PostgREST 1 → 1
description promete campo inexistente 1 → 1
a ponte monta a description do CATÁLOGO 1 → 1
filtro de org fora (sonda, 2 orgs) 5 → 7
A última divergiu porque previ contra um banco imaginado com só os meus 4
registros. E a divergência expôs asserção fraca minha: "A1 é o contato certo"
passava SOB vazamento, porque `has()` num conjunto que vazou o mundo inteiro é
sempre verdadeiro. Trocada por igualdade de conjunto; refeito 7 → 7.
## Dívida declarada
`lib/mcp/tools/catalogo/*.ts` declara `description` em 51 capacidades e o
cabeçalho diz "description fala com o modelo". Ninguém lê esse campo — a ponte e
o catálogo servido usam a do handler. 48 das 51 divergem, então um gate de
paridade nasceria vermelho. Fechei o call site do meu próprio trabalho com um
teste que exige que a ponte monte a descrição do handler; a decisão sobre as 48
fica declarada no handoff.
Provado na tela: /app/ai/agents/<mcp_agent> com login real e Supabase local
(HTML de /login com 2 ocorrências de 127.0.0.1:54321 e 0 de *.supabase.co).
6/6, sem jargão, sem scroll horizontal, zero erros de console.
typecheck 0 · lint 0 errors · 1493 unitários (1490 + 3)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkS3mzwtXughmjVC5FCoNo
|