mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
main
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9249e6f219 |
feat(agente): os três papéis — Conversador, Operador e Segurança (vazamento 30% → 10%) (#181)
* docs(agente): a spec dos tres papeis, e a terceira porta que a doutrina nao previa A doutrina FALAR/OPERAR ja existia e o passo 1 dela ja estava fechado com numero (30% de vazamento com prompt de operador, 0% com prompt de atendimento, 18 turnos, gate desarmado). O que faltava era a spec que rege o resto. Fecha as sete decisoes de produto. As tres que mudam o desenho: uma unidade com tres papeis (nao tres agentes), o Operador roda DEPOIS do envio (o lead nunca espera o CRM), e desligar o Operador NAO desliga o registro — a declaracao do Conversador ja e linguagem de negocio e vira estado por codigo, sem LLM. Registra a TERCEIRA porta de vazamento, que a doutrina subestimava: o vazamento medido `unsafe_url:https_required` nao veio da description nem do name da tool, veio do DADO que ela devolveu. Esconder a ferramenta nao fecha essa porta — exige camada de projecao propria. Na mesma resposta chegaram UUIDs crus de usuario a tela do cliente, que o detector nao pega por desenho. Registra tambem que o botao Testar da tela nao exercita guardrail nenhum: /versions/[vid]/test chama runAgent do runtime deprecated, que nao importa runBeforeSend. O self-hoster testa, ve limpo, publica, e producao diverge. Nao muda comportamento — tres documentos. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * feat(agente): a declaracao do turno — a fronteira FALAR/OPERAR vira contrato Passo 2 da spec 16. O checkpoint ja era a metade de um contrato entre quem fala e quem opera; faltava o campo que diz o QUE ACONTECEU no turno, e faltava declara-lo como fronteira em vez de subproduto. A declaracao viaja na chamada de FECHAMENTO que ja existe, nao numa tool. O motivo e o que este arquivo ja escrevia sobre o checkpoint: uma tool dependeria de o modelo lembrar de chama-la, e o turno em que ele esquecesse seria um lead parado em silencio. Custo adicional zero — e a mesma chamada de modelo. AUSENTE NAO E VAZIA, e o desenho inteiro gira nisso. `declaracao: undefined` (coluna NULL) = o modelo nao declarou; `{nada_a_declarar: true}` = ele avaliou e nao havia nada. Um `.default({})` no Zod, ou um `not null default` na coluna, colapsaria os dois e faria "esqueceu" parecer "nao havia nada" — escondendo o esquecimento exatamente onde o invariante 4 do sistema vivo manda mostra-lo. LeadCheckpointRow faz Omit e redeclara o campo porque as duas pontas representam o "nao sei" diferente: o modelo omite, o Postgres guarda null. A declaracao ja NASCE com leitor, senao seria evento sem consumer: buildHandoffSummary passa a entregar ao humano o que a pessoa quer e o que foi prometido a ela, antes de compromissos e objecoes — e o prazo vai colado na promessa, porque quem assume precisa do "ate quando" na mesma linha. Um artefato, dois leitores (invariante 2). O texto que o Conversador le nao carrega vocabulario de sistema, e ha teste que reprova se carregar — e o defeito de 30% que a spec existe para matar. Schema em tripla: migration 0110 + apendice idempotente no baseline + MANIFEST. Evidencia observada: 16 testes novos; suite 2603 passed (270 arquivos); typecheck limpo; lint 0 erros e 189 warnings — o MESMO numero medido na main com as mudancas em stash, entao nenhum warning novo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * feat(agente): a projecao — fecha a terceira porta, a que a doutrina subestimava Passo 3 da spec 16. As duas primeiras portas de vazamento (nome e descricao da ferramenta) se fecham nao mostrando a ferramenta. A terceira nao: e o DADO que ela devolve, e foi medida em turno real — "deram erro na acao de webhook: unsafe_url:https_required", copiado do resultado para a tela do cliente. ALLOWLIST, nunca denylist: campo novo no contexto nao chega ao cliente por omissao. A linha e identificador do SISTEMA sai (lead_id, conversation_id, media_storage_path), dado da PESSOA fica (nome, telefone, e-mail) — confundir as duas produz um agente que nao sabe com quem fala. O interruptor se justifica sozinho: a projecao arma quando NENHUMA ferramenta de catalogo entrou no turno, porque e exatamente ai que os ids nao tem uso (as MCP os recebem como argumento, as nativas resolvem pelo closure). Liga-la sempre quebraria o agente que opera; por env seria mais um botao para errar. No passo 6, quando o Conversador perder as ferramentas de escrita, ela passa a valer sozinha. ACHADO NAO PREVISTO, corrigido aqui: search_knowledge devolvia chunk_id e knowledge_source_id CRUS ao modelo, dois UUIDs por resultado, em toda busca com RAG — sem uso nenhum do lado dele, ja que as citacoes sao montadas pelo codigo. Era fonte silenciosa do UUID cru que a medicao viu chegar na tela do cliente. Fecha tambem a releitura: sem isso a projecao seria decorativa, bastava o modelo chamar get_lead_context para receber tudo cru de volta. E o cabecalho do bloco de estado deixa de imprimir o nome da tabela (lead_state). UM BUG QUE QUASE ENTROU, registrado porque o teste dele fica: eu aplicava a traducao de erro ao CORPO das mensagens. "Meu site tem um webhook quebrado" — frase legitima de quem vende integracao — viraria "nao consegui concluir essa verificacao", e o agente responderia a uma pergunta que ninguem fez. Os testes medem contra os turnos REAIS de gpt-5.6-terra versionados em evidence/, com controle positivo em cada caso e falha declarada se a evidencia sumir. Fixture inventada mede a imaginacao de quem a escreveu, que e justamente a que ja falhou — senao o defeito nao teria acontecido. `projeta` ficou OPCIONAL no tipo de buildOpening porque tests/invariants/** e congelado por hook: torna-lo obrigatorio forcaria editar um invariante existente, que a catraca proibe com razao. Custo registrado no handoff. Evidencia observada: 18 testes novos; suite 2621 passed (271 arquivos); test:db verde (68 arquivos, 462 passed, 1 skipped); typecheck limpo; lint 0 erros. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * fix(projecao): a traducao de erro pisava no conteudo do RAG do tenant Bug MEU, introduzido no commit anterior e pego pela sabotagem — nao por leitura. `traduzirErroCru` era aplicada a TODA string do retorno de ferramenta. O detector de vazamento considera "webhook" vazamento (com razao, na SAIDA), e a base de conhecimento de qualquer empresa que venda integracao fala webhook. O `content` do search_knowledge viraria "nao consegui concluir essa verificacao" linha apos linha: a projecao destruiria o RAG do tenant para proteger contra um vazamento que o gate de saida ja cobre. E a MESMA falha que a de reescrever a fala do cliente, corrigida horas antes, numa irma que nao se parece por fora: as duas nascem de aplicar num texto de TERCEIROS um filtro desenhado para texto do SISTEMA. Procurei a classe depois de pegar a primeira instancia e nao achei esta — ela so apareceu quando a sabotagem mostrou que o teste passava pelo mecanismo errado. Agora a traducao so alcanca valor sob chave de erro (lista fechada e curta: erro/error/message/code/reason/detail e os equivalentes em pt). Dois testes novos: o conteudo do RAG sobrevive intacto falando "webhook", e o campo de erro continua traduzido — a restricao nao desarmou a defesa. COMO O TESTE ANTERIOR PASSAVA SEM MEDIR: o detector pega UUID (categoria erro_cru), entao os casos de evidencia passavam pela TRADUCAO mesmo com a remocao de chave desligada — dois mecanismos redundantes, e eu atribuia o resultado ao errado. A sabotagem S2 derrubava 1 caso de 3; era o sinal. Evidencia observada: 20 testes; typecheck limpo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * docs(handoff): o bug do RAG e a sabotagem que o pegou Registra no handoff vivo o defeito que entrei e corrigi (traducao pisando no conteudo da base de conhecimento), COMO o teste passava sem medir (dois mecanismos redundantes, credito atribuido ao errado), e o sinal que denunciou — S2 derrubando 1 caso de 3, quando devia derrubar os 3. Depois da correcao: S2 derruba 3 de 3. Oito sabotagens, oito reprovacoes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * feat(agente): nasce o OPERADOR — disparo imposto pelo runtime, nunca pelo modelo Passo 4 da spec 16. O papel que mexe no sistema e nunca fala com o lead: ele nao tem send_message no toolset. A separacao e por AUSENCIA, a unica forma que nao depende de o modelo obedecer. O DISPARO E DO RUNTIME. O job `operator_turn` e enfileirado no fim do turno, logo apos o checkpoint existir, com sourceEventId = job do Conversador (retry da fila nao gera um segundo Operador). Se o Conversador o "chamasse", devolveria o problema inteiro: voltaria a depender de o modelo lembrar, e o turno em que ele nao achasse necessario seria um lead parado no funil, em silencio. Depois do checkpoint porque a declaracao E o insumo — antes, ele leria o turno anterior. O CURTO-CIRCUITO E ONDE O PASSO 2 SE PAGA. `nada_a_declarar: true` => nao chama modelo (quem avaliou estava la, com todo o contexto). Declaracao AUSENTE => roda, porque ausente significa que NINGUEM avaliou, e e ai que promessa fica orfa. Os dois estados que o passo 2 se recusou a colapsar decidem, aqui, se a chave do self-hoster e gasta. Ha teste afirmando que eles levam a decisoes OPOSTAS. `operator_enabled` nasce false: migration nao liga sozinha um papel que gasta chamada de modelo por turno. `operator_model` null = herda o do Conversador. O loader le `?? false` para o clone que ainda nao aplicou a 0111 — schema velho desliga o papel, nunca gasta modelo por engano. Promessa declarada e nao cumprida vira `promise_unfulfilled` na Central, com copy que diz o que o CLIENTE espera, nao o que o sistema deixou de gravar — do lado de la existe uma pessoa que ouviu um compromisso. DOIS ERROS MEUS QUE AS CATRACAS DO REPO PEGARAM, nao eu: - criei um SEGUNDO bloco de `job_queue_kind_check` no baseline quando ja existe um. Isso quebra o update.sh de todo clone com vocabulario posterior. Eu tinha aplicado essa licao corretamente ao agent_inbox_items minutos antes e a irma passou batido — o padrao pego numa ocorrencia da alibi as outras. Consolidado no bloco unico (tests/unit/baseline-constraint-reconstruida.test.ts); - o kind novo divergia entre banco e TypeScript (tests/invariants/vocabulario-banco-x-typescript.test.ts). Evidencia observada: 7 testes de decisao + 5 invariantes de schema; suite 2630 passed (272 arquivos); test:db verde (69 arquivos, 467 passed); typecheck limpo; lint 0 erros / 189 warnings (mesmo numero da main). Schema em tripla: migration 0111 + apendice no baseline + MANIFEST. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * docs(handoff): passo 4 registrado, com as duas catracas que me pegaram Registra o que o passo 4 entregou e o que NAO entregou (o Operador tem canal, ainda nao tem mao), a sabotagem com contagem prevista, e os dois erros meus que foram pegos por catraca do repo e nao por leitura minha — porque a saida limpa sem essa nota creditaria ao rigor o que foi merito do mecanismo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * feat(agente): o Operador ganha MAO — ferramentas proprias, e nenhuma delas fala Fatia 1 do passo 5. O papel deixa de so registrar promessa e passa a operar: le `operator_tool_ids`, monta o toolset pela ponte MCP e roda um turno de modelo com briefing proprio e atribuicao de custo propria (purpose 'operator_turn' — sem isso o gasto dele entraria como se fosse conversa, e "quanto custa ligar o papel" nao teria resposta). COLUNA PROPRIA, nao reuso de tool_ids, por tres razoes que se somam: a tela nao pode mentir (lista compartilhada faria a secao Operador configurar o que o Conversador executa); o teto de 20 se resolve por DIVISAO em vez de aumento (hoje sao ate 32 tools num prompt so, e o e2e capacidades-do-agente esta fora do CI por causa disso); e o passo 6 vira migration de DADOS, mover ids entre colunas. SEM CANAL, e isso e estrutural: send_message e tool NATIVA do engine e nao existe no catalogo, entao nao ha id que a ligue; e crm_send_whatsapp_message esta em BLOCKED_TOOL_IDS, que agora e exportado para ser aussertivel — garantia que nenhum teste consegue ler e garantia que ninguem percebe quando some. O system do papel fala de CRM com todas as letras, e PODE: esse texto nao alcanca cliente. E a assimetria exata da spec — no Conversador o mesmo vocabulario e o defeito de 30% medido. Sem ferramenta configurada ele nao chama modelo: descobrir que nao tem mao nao vale a chave do self-hoster. Herdar as do Conversador daria 20 capacidades a quem nao escolheu nenhuma, entao o default e vazio. TERCEIRA CATRACA DO REPO A ME PEGAR: VERSION_COLUMNS esta copiado em 6 arquivos e eu atualizei 1. O proprio teste conta que `cases_enabled` fez isso (entrou em 2 de 7) e o sintoma nao e typecheck nem teste — e um campo que "se desmarca sozinho" na tela depois do refresh, e o save seguinte grava por cima. Repeti o padrao que o comentario descrevia. Evidencia observada: 8 testes novos; suite unit 1459 passed (154 arquivos); test:db verde (69 arquivos, 467 passed); typecheck limpo. Schema em tripla: migration 0112 + apendice no baseline + MANIFEST. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * feat(ui): a tela ganha os PAPEIS — e diz a consequencia, nao o nome da feature Fatia 2 do passo 5. A configuracao do agente deixa de ser uma lista unica e passa a ter navegacao por papel: "Conversa com o cliente" e "Organiza o sistema". Os rotulos dizem o que cada papel FAZ — Conversador/Operador e o nosso vocabulario interno; quem configura pensa em "quem fala com meu cliente" e "quem organiza minha casa". SECOES, nao abas com save proprio: o rascunho e um so. Duas telas com dois saves fariam o usuario publicar metade da configuracao e criariam dois caminhos para o mesmo ai_agent_versions. A REGUA DA DISCIPLINA DE INFORMACAO e dizer a CONSEQUENCIA. Com o papel desligado, a tela explica o que CONTINUA acontecendo (o basico e registrado sozinho — decisao da spec 16 §2.1) e o que PARA (decidir sobre a operacao). Sem a primeira frase o usuario conclui que desligar deixa o sistema cego e liga por medo, nao por escolha. Ha teste exigindo as duas. Papel desligado NAO mostra a configuracao dele: convidar alguem a configurar o que nao vai rodar produz a conclusao de que o produto quebrou. O modelo herdado tem NOME na tela ("A mesma que conversa"), em vez de aparecer como pendencia — e ganhou o caminho de VOLTA: um Select nao oferece "nenhum" como item, entao sem o botao a escolha seria de mao unica e o usuario nao teria como saber por que. QUARTA CATRACA a me pegar nesta sessao: VERSION_COLUMNS em 6 arquivos, eu atualizei 1 (corrigido na fatia anterior). Ha um teste novo que varre a tela inteira atras do nosso vocabulario (MCP, tool_ids, operator_, job, payload). Evidencia observada: 8 testes de componente; suite unit 1459 passed antes deste arquivo; typecheck limpo; lint 0 erros. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * docs(handoff): passo 5 parcial, e o bloqueio que impediu a prova de tela Registra as duas fatias entregues (mao do Operador + tela dos papeis) e o motivo de a prova de jornada NAO ter sido feita: .env.local aponta para a nuvem, e o playwright sobe o app com next start, que carrega .env.local. Rodar e2e nesta maquina hoje cria agentes e conversas de teste no banco de PRODUCAO. Nao e especifico deste epico — vale para qualquer sessao que rode e2e aqui agora. Fica declarado que o que esta provado da tela e comportamento de componente, nao jornada de usuario. Sao coisas diferentes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * fix(e2e): a suite escrevia no banco de PRODUCAO — tres camadas de conserto Medido em 2026-08-06. `.env.local` de um checkout de trabalho aponta para a nuvem, e o playwright sobe o app com `next start`, que o carrega. A suite criava organizacoes, usuarios e agentes de teste no banco real, passando verde. A org `e2e-test-org` esta na producao deste projeto desde 2026-04-29. Ela nao chegou la por acidente de uma sessao: chegou porque esse era o comportamento normal da suite. ISOLAR O webServer NAO BASTAVA, e essa foi a descoberta. Os scripts de seed leem `.env.local` DIRETO DO DISCO, ignorando process.env — entao o env injetado no servidor nunca os alcancava, e as specs chamam esses seeds sozinhas no meio do teste. O sintoma que denunciou: o factor TOTP em `.e2e-creds.json` nao existia no banco local, porque tinha sido criado na nuvem. 93 arquivos tem esse padrao. As tres camadas: 1. `.env.e2e` (gerado por `pnpm e2e:env`, nao versionado) + injecao explicita no webServer, com guard que RECUSA rodar se o arquivo faltar — cair no .env.local em silencio e o modo de falha caro; 2. `pnpm e2e:build`, porque as tres NEXT_PUBLIC_* sao embutidas no BUNDLE: buildar com o env errado e trocar so no start deixaria a URL de producao dentro do JavaScript do browser. `node --env-file` nao serve aqui — o next build cria Workers e o Node recusa propagar a flag (ERR_WORKER_INVALID_EXEC_ARGV). O script prova as DUAS direcoes: host de producao ausente do bundle E host local presente (sem o controle positivo, "nao achei producao" pode ser um grep que nao acha nada); 3. `scripts/lib/env-de-teste.ts` — process.env VENCE o arquivo, e todo seed ANUNCIA o destino ("escrevendo em LOCAL" / "⚠️ REMOTO"). Um seed que escreve em producao acha os mesmos dados de teste de sempre e termina com "✅ Seed completo": sem a linha impressa, nada distingue os dois casos. `seed-e2e-credentials.ts` migrado (e o que a suite chama sempre). Os outros ~14 seeds e as ~78 sondas ficam declarados como divida no handoff. GANHO ESTRUTURAL do worktree dedicado: aqui nao existe `.env.local`, entao um script que o leia do disco falha ALTO (ENOENT) em vez de escrever na nuvem. O isolamento deixa de depender de disciplina. Evidencia observada: spec agente-novo-e-uso 5/5 passed contra o banco LOCAL (era 0/5 antes, por MFA — o factor do banco nao batia com o do arquivo); suite unit 1467 passed (155 arquivos); typecheck limpo; guard do playwright recusa sem .env.e2e (verificado). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * docs(handoff): o worktree dedicado, e as tres camadas do isolamento do e2e Registra o achado que destravou a prova de tela e que vale para o repo inteiro: a suite escrevia em producao, e isolar so o webServer nao bastava porque os seeds leem .env.local do disco. Registra tambem o ganho estrutural do worktree (sem .env.local, script que o leia falha alto em vez de escrever na nuvem) e as tres dividas que sobraram: os ~92 scripts nao migrados, o spec da aba nova, e a decisao sobre as fixtures que ja estao na producao. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * fix(e2e): os 16 seeds param de ler .env.local do disco, e um gate congela isso Completa o conserto de |
||
|
|
0c79915811 |
fix(crm-vivo): a limpeza da sonda sai do caminho feliz — e o processo para de pendurar
DESCOBERTO POR ACIDENTE, e o acidente e a parte que importa: ao re-tirar a evidencia da rede, a screenshot mostrou CINCO cards "rede dossie" no board do CRM Vivo. Eu nao estava procurando isso; eles apareceram na foto de uma prova que eu tirava para outra coisa. A limpeza existia e estava no FIM DO CAMINHO FELIZ. Sonda de UI morre no meio o tempo todo — locator que nao resolve, timeout, pagina que nao hidrata — e cada morte dessas deixava um lead no board, contando nas colunas como negocio de verdade. Limpeza no caminho feliz roda exatamente quando nao fez falta e falta exatamente quando teria feito. Agora ela mora num `.finally()` no topo, alimentado por `leadCriado`. E O SEGUNDO DEFEITO SO APARECEU PORQUE EU TESTEI O PRIMEIRO: sabotei a sonda com um throw depois da criacao do lead para ver se a limpeza aguentava. Ela aguentou — e o PROCESSO NAO ENCERROU. Ficou 7 minutos ate um timeout externo mata-lo, porque `browser.close()` tambem estava no caminho feliz e o Playwright segura o event loop. Uma sonda que pendura em CI e pior que uma que suja o banco. PROVA (sabotagem deliberada, aplicada e revertida): antes do conserto travou 7min no timeout externo · lead ficou no banco depois do conserto EXIT=1 · encerrou em 10s · 0 leads deixados E o caminho feliz segue: as duas sondas EXIT=0, cura e denuncia (0 -> 1). O `catch` na limpeza existe so para nao substituir a causa original no log: se a sonda ja esta morrendo, a falha da limpeza nao pode ser o que aparece. typecheck 0 · eslint 0 nos dois arquivos. |
||
|
|
4dc9e9c4eb |
test(crm-vivo): evidencia da rede RE-TIRADA contra o HEAD, e o log para de mentir sobre a condicao
A evidencia anterior era de ANTES de |
||
|
|
f320d1e84e |
feat(realtime): a rede de segurança também no dossiê — e o aparato certo
O dossiê é o caso mais grave dos três: a timeline PROMETE contar a vida do
negócio, e uma timeline congelada não parece congelada — parece um negócio sem
novidade. O usuário não tem como distinguir, e por isso não desconfia.
O PAR COMPLETO, agora nos dois lugares:
canal trouxe apareceu divergências
board · morta não sim 0 → 1
board · viva sim sim 0 → 0
dossiê · morta não sim 0 → 1
dossiê · viva sim sim 0 → 0
═══ TRÊS APARATOS DESCARTADOS ANTES DE UM FUNCIONAR ═══
Matar a entrega parecia trivial e não era. Cada tentativa media outra coisa:
page.route intercepta HTTP e NÃO WebSocket — o canal entregava, e
o "curou: sim" era o realtime funcionando;
throw no construtor derrubava a página com Runtime Error do Next: APP
QUEBRADO, que é outra condição. Só descobri porque o
dossiê achou DOIS `role=dialog` na tela;
stub artesanal o canal seguia `subscribed` — nem estava em uso, e eu
media um canal vivo achando que estava morto;
page.routeWebSocket o canal fica em `connecting` e nunca entrega. O real.
⚠️ A PROVA DO BOARD DE
|
||
|
|
25fb56d40e |
feat(realtime): a rede de segurança — cura a perda E denuncia a falha
Uma peça com três papéis, e ela responde ao único achado do dia que atravessou
três retratações intacto: QUANDO A ENTREGA MORRE, NENHUMA TELA AVISA.
1. CURA. Hoje, se o canal para de entregar, o board NUNCA se recupera — nem ao
voltar para a aba. A tela fica congelada num passado que parece presente, e
só um F5 conserta.
2. DENUNCIA. Se o refetch traz estado diferente e o canal não entregou nada no
intervalo, alguma coisa se perdeu. É a única forma de SABER que morreu.
3. DESTRAVA a cerca. Sem o sinal "houve entrega recente?", divergência é
indistinguível de "nada aconteceu", e a verificação só consegue REPROVAR.
⚠️ NÃO É "refetch periódico com outro nome". A diferença é o COMPARADOR: um
refetch periódico ESCONDE a perda (o dado volta e ninguém soube que faltou);
este a denuncia. Curar sem detectar seria pior que o estado atual — a falha
viraria invisível em vez de visível-tarde, e ninguém consertaria a causa.
E ele tem uma vantagem que o realtime não tem: NÃO PODE nascer anônimo nem
morrer calado. É requisição com resposta. O canal responde SUBSCRIBED e pode
nunca entregar nada, em silêncio — foi exatamente isso que custou o dia.
PROVA — O PAR, matando a entrega de verdade:
canal trouxe card apareceu divergências
entrega MORTA não SIM 0 → 1
entrega VIVA sim sim 0 → 0
Só o lado morto mostraria a cura sem distinguir "a rede funcionou" de "o canal
funcionou". Só o lado vivo não mostraria nada. Juntos: cura nos dois casos,
denuncia só quando há o que denunciar — um detector que gritasse sempre seria
desligado na primeira semana.
MATAR A ENTREGA EXIGIU O APARATO CERTO: a primeira versão usou `page.route`, que
intercepta HTTP e NÃO WebSocket. O canal entregou normalmente e o "curou: sim"
era o realtime funcionando. Aparato que não produz a condição mede outra coisa e
devolve verde que parece prova. Agora é `addInitScript` substituindo o construtor
de `WebSocket` só para URLs de realtime.
E O COMPARADOR TAMBÉM NASCEU ERRADO: comparava com um `entregaAntes!` que, sendo
`null`, virava `>= 0` — sempre verdadeiro. O detector acusaria divergência em
toda mudança legítima. Agora o critério são duas perguntas explícitas: a tela
mudou? o canal trouxe algo desde a última vez?
As divergências ficam OBSERVÁVEIS na tela (`data-refetch-divergencias`), pelo
mesmo motivo do status do canal: "a entrega morreu" e "nada aconteceu" têm a
mesma aparência, que é silêncio.
typecheck 0 · lint 0 erros · 416 testes em 69 arquivos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dxBNZMjUzBx8xWs8DvGvP
|