ModularReports
Companheiro do DRS
ModularReports: Roadmap de Implementação
| Campo | Valor |
|---|---|
| Produto | ModularReports |
| Documento | Roadmap de Implementação (companheiro do DRS) |
| Data de revisão | 2026-08-09 |
Documentos da família
- DRS (modularreports), o que o sistema faz.
- Inventário de Artefatos (modularreports-inventario), tudo que precisa existir e como cada item deve ser.
- Jornadas dos Atores (modularreports-jornadas), como cada papel opera o sistema.
- Plano de Testes (modularreports-testes), o que validar antes de entregar.
- Questionário de Elicitação (modularreports-questionario), decisões de produto em aberto que não travam a construção.
Este é o roteiro de construção do sistema, em ordem, com verificação primeiro: para cada tarefa, o observável que prova que ela está pronta é declarado antes do como construir. A ordem vem das ondas de entrega do DRS (bloco 9, J.1 e J.2) e de suas dependências. Cada tarefa cobre um ou mais requisitos funcionais (RF) do DRS.
Como usar
Cada tarefa é um cartão com um código curto e estável (ex.: F1-03), o RF que cobre, o que implementar, como verificar (o teste, declarado primeiro) e o critério de aceite H1. Marque a tarefa como feita quando o teste passar. As fases são sequenciais; dentro de uma fase, siga a ordem dos cartões. Ao fim de cada fase há uma Definição de Pronto: a fase só fecha quando todos os seus critérios estão verdes.
Mapa de cobertura
| Fase | Onda do DRS | Módulo do DRS | RFs cobertos |
|---|---|---|---|
| Planejamento | Preparação | Elicitação, especificação, companheiros, validação, aprovação | Pré-concluída na aprovação do DRS |
| M, Mockup Navegável | Preparação | Referência visual do sistema completo (bloco 6, F.3) | Concluída: mockup de 17 telas publicado e aprovado |
| F1, Protótipo do pipeline IA | Fatia 1 | reports-compose (CMP), reports-boundary (BND), reports-ingest (ING) | RF-BND-001, RF-CMP-005/006/008 a 016, RF-ING-003, RNF-SEG-002/003/004, RN-012, RF-CMP-018 (mecanismo determinístico) |
| F1b, Schema de dados | Fatia 1b | Modelo de dados (E.1, E.2) | Cria as tabelas do schema reports (as 7 do E.2 mais report_goal), antes das fatias que as consomem; RN-009 (trigger report.company_id = company_id do Account) |
| F2, Contrato de bloco e snapshot | Fatia 2 | reports-render (REN), reports-publish (PUB), reports-ingest (ING) | RF-REN-001/002/003, RF-PUB-001, RF-ING-005, RN-001 a RN-006 |
| F3, Package renderizador | Fatia 3 | reports-render (REN) | RF-REN-004/005/007, RNF-MANUT-001, RNF-A11Y-001 |
| F4, Ingestão, quota e storage | Fatia 4 | reports-ingest (ING), reports-boundary (BND) | RF-ING-001/002/003/004/006, RF-BND-002, RNF-LGPD-001 |
| F5, Leitor público | Fatia 5 | reports-render (REN) | RF-REN-006/010, RNF-SEG-001 (caminho do leitor: role reports_reader, GRANT das 3 tabelas e policy PERMISSIVE publicado+não-expirado), RNF-SEG-005, RNF-PERF-001, RNF-DISP-001, RN-011 |
| F6, Role, entitlement e RBAC no Hub | Fatia 6 | reports-admin (ADM) | RF-ADM-001/002/012, RNF-SEG-001 (caminho do operador: RESTRICTIVE por company_id), RN-007 (log append-only) |
| F7, Chrome e shell do operador | Fatia 7 | reports-admin (ADM) | RF-ADM-003/004/014, RNF-MANUT-002 |
| F8, Wizard e compositor | Fatia 8 | reports-compose (CMP), reports-curate (CUR) | RF-CMP-001 a 004, RF-CUR-001 a 008, RNF-PERF-002, RF-CMP-017, RF-CMP-019, RF-CMP-020 |
| F9, Publicação e link | Fatia 9 | reports-publish (PUB), reports-compose (CMP), reports-comment (COM) | RF-CMP-007, RF-PUB-002 a 010, RF-COM-004, RN-010 (imutabilidade do snapshot imposta pela função SECURITY DEFINER de publish), RF-CMP-018 |
| F10, Templates | Fatia 10 | reports-admin (ADM) | RF-ADM-009/010/016/017, RN-013 (escopo Global e individual) |
| F11, Importação de metas | Fatia 11 | reports-ingest (ING), reports-boundary (BND) | RF-BND-003, RF-ING-007, RN-008 (snapshot congela as metas vigentes no publish) |
| F12, Comentários | Fatia 12 | reports-comment (COM) | RF-COM-001 a 006 |
| F13, Analytics | Fatia 13 | reports-analytics (ANL), reports-admin (ADM) | RF-ANL-001 a 006, RF-ADM-015, RNF-LGPD-003 |
| F14, Domínio custom e white-label | Fatia 14 | reports-render (REN) | RNF-SEG-006 |
| F15, PDF condicional | Fatia 15 | reports-render (REN) | RF-REN-008/009 |
| F16, Painel do cliente | Fatia 16 | reports-admin (ADM), reports-render (REN) | RF-ADM-005/006/007/008/011/013, RF-REN-011 (portal logado do cliente) |
| F17, Blocos reais, cadência e biblioteca de modelos | Fatia 17 | reports-render (REN), reports-compose (CMP), reports-admin (ADM) | RF-REN-012 (cobertura nominal dos 16 tipos do Ap.3), RF-REN-013, RF-CMP-001 (cadência no passo 1), RF-CMP-021, RF-CMP-022, RF-CMP-023, RF-ADM-017, RN-014, RN-013 item 4, ADR-016, ADR-017. Vem na fila antes de F13 a F16, ver a nota de prioridade da fase |
| F12, Comentários | Fatia 12 | reports-comment (COM) | RF-COM-001 a RF-COM-006, mais RNF-SEG-007 (guarda de script inline, {F12-07}), nascida da verificação real desta fatia |
| F19, Geração agendada | Fatia 19 | reports-admin (ADM), reports-compose (CMP) | RF-ADM-019 a RF-ADM-022, RF-CMP-027, RF-CMP-028, RF-CMP-029, RN-017, ADR-021, E.2 (report_schedule, schedule_cycle, report_job). Nasceu da resposta do dono a {Q4b}. Depende de {F17-05} (cadência) e de {F18-05} (anti-fadiga por quadrantes) |
| F20, Modelo de venda e camada de resultado | Fatia 20 | reports-admin (ADM), reports-render (REN), reports-compose (CMP), reports-ingest (ING) | RF-ADM-023, RF-REN-016, RF-REN-017, RF-CMP-030, RF-CMP-031, RF-ING-008, RN-018, ADR-022, ADR-023, Ap.10. Nasceu da planilha de gestão apontada pelo dono e fecha o item de investigação do Ap.9. Depende de {F11-01} e {F11-02} (importação) |
| F18, Honestidade do número e calibração da régua | Fatia 18 | reports-compose (CMP), reports-render (REN), reports-admin (ADM), reports-curate (CUR) | RF-CMP-016 (reescrito), RF-CMP-024, RF-CMP-025, RF-CMP-026, RF-REN-014, RF-REN-015, RF-ADM-018, RF-CUR-009, RNF-MANUT-003, RNF-MANUT-004 (rota provada pelo HTTP servido, {F18-18}), RNF-SEG-008 (varredura de topologia no publish), RNF-PERF-002 (teto escalado), RNF-MANUT-005 (número que vem de cima também se mede), RN-015, RN-016, ADR-018, ADR-019, ADR-020, Ap.4 (segundo critério de coerência, E1 a E3). Depende de F17 |
Estado global
Recontagem em 2026-08-18, após o fechamento do {F8-58} e o nascimento do {F8-62}, e ela é a base vigente: qualquer número mais antigo neste documento está morto. Mesmo método das recontagens anteriores, expressão regular sobre toda linha de cartão de checklist do documento, aceitando letra minúscula no fim do código, conferido sem duplicidade (269 linhas de cartão para 269 códigos únicos). Placar: 269 cartões no documento, sendo 29 de Planejamento (100%) e 2 de Mockup (100%), e 238 de Construção, dos quais 226 fechados e 12 abertos. Contando a Fase M dentro da barra de Construção, como o renderizador faz: 228 de 240 = 95,0%. Os 12 abertos: {DEF-01}, {DEF-02}, {F8-20}, {F8-37}, {F8-38}, {F8-41}, {F12-07}, {F18-11}, {F18-16}, {F18-19}, {F20-10} e {F8-62} (nascido nesta rodada). O Plano de Testes não foi remedido nesta rodada (carregado o último valor conferido no próprio documento: 165 itens, 90 fechados, 136 Alfa e 29 Beta, todos os fechamentos na seção Alfa = 54,5%). Pelos pesos 40/40/20 das três seções, o geral fica em 88,9%.
O que mudou nesta rodada, em uma frase: o {F8-58} fechou e nasceu o {F8-62} no lugar dele, denominador líquido +1, sem nenhum outro cartão mudando de estado. A guarda de endereço de aviso que o {F8-58} media saiu do módulo de relatórios e passou a valer para o Hub inteiro (publishNotification, apps/api/src/lib/notifications.ts, 20 importadores, fail closed), com 7 isenções nomeadas por prefixo só para o módulo control e 3 destinos mortos corrigidos com o destino medido no front. A medição de fechamento (feita por conta própria, antes de declarar pronto) DIVERGE da tabela que o próprio cartão trazia desde a rodada de 2026-08-18 anterior: a tabela contava 584 casam/8.395 não casam, a remedição achou 395 casam/8.584 não casam, e as duas ficam escritas no cartão, sem ajustar uma pela outra. O achado que sustenta o {F8-62}: no control o campo link é ao mesmo tempo chave de deduplicação e destino de navegação, e a segunda função nunca foi honrada, cerca de 8,2 mil avisos levam ao painel inicial em silêncio; mexer nesses avisos vivos é decisão do dono, não de quem implementa, e por isso virou cartão em vez de conserto em massa.
Recontagem em 2026-08-09, feita depois da reconciliação de documento da fatia {F8-08} A, e ela é a base vigente: qualquer número mais antigo neste documento está morto. Universo e critério, declarados porque número sem critério não se audita: o universo é toda linha de cartão de checklist DESTE documento, e o critério é a marca no próprio texto (a declaração de quem produz, não o clique de quem lê), conferida contra o estado efetivo que o sistema serve, que devolveu exatamente os mesmos 218 itens com 201 fechados. Placar: 218 cartões no documento, sendo 29 de Planejamento e 2 de Mockup, todos fechados desde a aprovação, e 187 de Construção com 170 fechados e 17 abertos. Contando a Fase M dentro da barra de Construção, como o renderizador faz, são 189 com 172. O Plano de Testes foi recontado no próprio documento na mesma data: 165 itens com 90 fechados, sendo 136 Alfa e 29 Beta. Pelos pesos 40/40/20 das três seções, o geral é 87,3%.
O denominador subiu de 183 para 187 e o percentual de Construção caiu de 92,9% para 90,9%, e isso é o processo funcionando. Nenhum cartão fechado reabriu. Os quatro que nasceram vieram todos de defeito medido durante a obra desta fatia: {F8-21} (a barra de marca da agência na página pública nunca foi construída, e é ela que bloqueia a metade do logo), {F8-22} (as páginas de link revogado e expirado resolvem identidade por uma segunda precedência), {F8-23} (a varredura de número reprova casos que o próprio requisito isenta nominalmente) e {F8-24} (a procedência da identidade é gravada e nenhuma tela a lê). O Plano de Testes subiu de 163 para 165 pelos dois requisitos novos do DRS.
A ORDEM FOI INVERTIDA na fatia {F8-08} A, e o registro fica aqui porque a lição é de processo, não de cartão. Neste projeto o DRS é lei e mudança de escopo se reconcilia no documento ANTES de codar; o plano da fatia declarava isso como passo 1, com todas as letras. A construção foi despachada sem o passo 1: o código ficou pronto e em gates, e o DRS e o Inventário publicados eram anteriores aos commits e não mencionavam nada da sobreposição de identidade por relatório. Quem mediu e apontou a divergência foi a revisão de código, não o processo. A reconciliação veio depois, em 2026-08-09 (RF-CUR-004 endurecido, RF-CUR-012, RF-CUR-013, RN-020, a coluna nova no Inventário, o artefato {ART-SCR-03}, os quatro cartões filhos e a cobertura Alfa dos requisitos novos), e nada foi revertido. O que muda daqui para frente: despachar construção sem despachar o passo zero deixa o bloqueio (aqui, {F8-21}) existindo só na cabeça de quem planejou, e bloqueio que não virou cartão não bloqueia ninguém.
Um achado desta reconciliação vale por si, porque é um número herdado que estava errado. O plano da fatia registrou que o perímetro da disciplina de número derivava de uma segunda lista curada, deixando a abertura do relatório de fora da varredura. Medido no código vivo em 2026-08-09, por controle negativo comportamental e não por busca textual: a premissa é FALSA, já tinha sido corrigida por {F18-20}, e a abertura reprova corretamente um número digitado à mão. A mesma medição, porém, encontrou o defeito oposto e ele é real, e virou {F8-23}. Evidência anterior a um conserto não prova defeito vivo, e por isso número que vem pronto se remede antes de virar cartão.
NOTA DE PRODUÇÃO, 2026-08-05: as Fatias 11 a 20 saíram da branch e foram para produção. Os números abaixo, medidos em 2026-08-03, descreviam código construído e testado numa linha de trabalho separada da que serve o produto; não descreviam o que estava no ar. Nesta data as dez fatias foram mergeadas na linha principal e colocadas em produção, com QA e revisão de código independentes rodadas pela primeira vez sobre o pacote acumulado (a suíte completa, banco real: mais de mil testes verdes; revisão de código: zero achado crítico, zero regressão fora do escopo). Um defeito de infraestrutura foi achado e corrigido no mesmo movimento: o servidor que atende o link público não encaminhava as rotas novas de comentário (sessão, comentário, pedido de acesso, asset por identificador) para o leitor, então quem clicasse em "Comentar" no relatório publicado caía em erro; corrigido e provado com o fluxo completo (convite por link mágico até o comentário persistido). Os cartões marcados fechados abaixo agora descrevem produto no ar, não apenas branch testada, e é essa distinção que esta nota registra.
Um cartão foi reaberto e re-fechado no mesmo dia, e a volta fica registrada porque a lição vale. Uma medição contra a infraestrutura em produção achou o {F15-02} (assets do leitor e do PDF) afirmando um mecanismo de armazenamento que parecia não existir, e o cartão foi reaberto. A medição tinha consultado o serviço de armazenamento errado: a plataforma-mãe nunca usou aquele serviço, de nenhum módulo, então a consulta sempre daria zero. O mecanismo real é armazenamento em sistema de arquivos sobre volume dedicado, já usado por outras áreas do Hub, com arquivo real do próprio ModularReports gravado nele; medido de novo no mecanismo certo, o cartão fechou de volta. Nenhum outro cartão fechado precisou reabrir: a verificação direta em produção (tabelas do banco, rotas da API, containers ativos, disparadores) confirmou o que os demais cartões já diziam. O {ART-INFRA-01} do Inventário teve a mesma volta, com a ficha reescrita para descrever o mecanismo real.
Recontagem em 2026-08-08, pelo endpoint de estado do roadmap sobre o documento publicado. Esta é a base única; qualquer número mais antigo neste documento está morto, incluindo o "159 de 174" que outra frente publicou nesta mesma data. Aquele número e os anteriores vinham de uma contagem que perdia três cartões cujo código termina em letra minúscula ({F1b-01}, {F8-02b}, {F8-02c}), fechados desde as fases iniciais; o endpoint os enxerga porque não filtra por formato de código. Contagem exaustiva por código, sem duplicidade (cada código aparece como cartão de checklist exatamente uma vez, conferido): 209 cartões no total, sendo 29 de Planejamento (100% fechados), 2 de Mockup (100% fechados) e 178 de Construção, dos quais 164 fechados e 14 abertos. Os 14 abertos, cada um com o motivo no próprio cartão: {F8-07}, {F8-08}, {F8-11}, {F8-12}, {F8-15}, {F8-16}, {F8-17}, {F8-18}, {F12-07}, {F13-03}, {F18-11}, {F18-16}, {F18-19} e {F20-10}. {F4-07} e {F5-06}, que apareciam abertos na última recontagem, fecharam entre uma medição e outra e saem da lista.
Recontagem de 2026-08-08 ao fim do dia, e ela SOBE o denominador de propósito. Universo e critério, declarados porque número sem critério não se audita: o universo é toda linha de cartão de checklist DESTE documento e o critério é a marca no próprio texto (- [x] contra - [ ]), ou seja, a declaração de quem produz, não o clique de quem lê. Placar: 211 cartões no total, sendo 29 de Planejamento (fechados), 2 de Mockup (fechados) e 180 de Construção, dos quais 165 fechados e 15 abertos; 165 mais 15 fecham em 180, e 29 mais 2 mais 180 fecham em 211. Os 15 abertos: {F8-07}, {F8-08}, {F8-12}, {F8-15}, {F8-16}, {F8-17}, {F8-18}, {F8-19}, {F8-20}, {F12-07}, {F13-03}, {F18-11}, {F18-16}, {F18-19} e {F20-10}. O que mudou em relação ao parágrafo acima, item a item: {F8-11} fechou e sai da lista de abertos; {F8-19} e {F8-20} são cartões novos, nascidos do planejamento do {F8-07} (imagem vetorial servida embutida na página pública, e a eliminação de dados pessoais que não alcança arquivo nenhum). Denominador de 178 para 180 sem nenhum cartão fechado reabrir: os dois novos vieram de defeito medido durante a obra, que é o processo funcionando e não o escopo escorrendo.
Duas bases circulam e as duas são verdadeiras; ao comparar números, diga qual está usando. Excluindo Planejamento e Mockup, a Construção tem 178 cartões com 164 fechados. Contando a Fase M dentro da barra de Construção, como o renderizador faz, são 180 com 166. A diferença é exatamente os 2 cartões de Mockup, que nasceram fechados.
O denominador subiu com o nascimento de {F12-08} em 2026-08-08, e é o mesmo desenho de sempre: a obra encontrou defeito olhando o produto. {F12-08} (o botão de comentar sobrepondo o título do bloco em tela de celular) nasceu e fechou no mesmo dia, visto na verificação real de {F5-06}, mergeado e deployado, provado em produção por geometria real. Nenhum cartão fechado reabriu. Bases que não valem mais: qualquer contagem de Construção que não inclua {F1b-01}, {F8-02b} e {F8-02c}, e o "159 de 174" publicado por outra frente nesta mesma data.
Recontagem em 2026-08-08, à noite, publicando o que fechou depois da recontagem do fim de tarde: dois cartões fecharam. {F8-12} fechou por verificação (composição síncrona provada em produção, sem código novo). {F13-03} fechou mergeado e deployado (fonte única de contagem de visualização, prova real: o mesmo relatório passou a mostrar 147 nos dois lugares que exibem visualização). {F8-11}, já fechado no parágrafo anterior, permanece sem mudança. Nenhum outro cartão mudou de estado; {F8-07} e {F8-08} ganharam achados novos no planejamento (os dois cartões que nasceram deles, {F8-19} e {F8-20}, já estão contados acima), mas os dois seguem abertos, por falta de construção e não de exercício. Placar: 211 cartões no total, 29 de Planejamento (fechados), 2 de Mockup (fechados) e 180 de Construção, dos quais 167 fechados e 13 abertos. Os 13 abertos: {F8-07}, {F8-08}, {F8-15}, {F8-16}, {F8-17}, {F8-18}, {F8-19}, {F8-20}, {F12-07}, {F18-11}, {F18-16}, {F18-19} e {F20-10}. Contando a Fase M dentro da barra de Construção, como o renderizador faz: 169 de 182 = 92,9%. Testes não foram remedidos nesta rodada (carregado o último valor publicado, 91 de 161 = 56,5%). Pelo peso 40/40/20 do progresso geral, o geral fica em 88,4%.
Recontagem em 2026-08-09, do zero, pelo mesmo método declarado acima, sem herdar nenhum número anterior por adoção. Método: expressão regular sobre toda linha de cartão de checklist do documento (- [ ] {CODIGO} ou - [x] {CODIGO}), aceitando letra minúscula no fim do código, o mesmo formato que a contagem de 2026-08-08 tinha perdido em três cartões ({F1b-01}, {F8-02b}, {F8-02c}); conferido que cada código aparece exatamente uma vez, sem duplicidade, 213 linhas de cartão para 213 códigos únicos. Placar: 213 cartões no total, sendo 29 de Planejamento (fechados), 2 de Mockup (fechados) e 182 de Construção, dos quais 169 fechados e 13 abertos. Os 13 abertos: {F8-08}, {F8-15}, {F8-16}, {F8-17}, {F8-18}, {F8-19}, {F8-20}, {F12-07}, {F18-11}, {F18-16}, {F18-19}, {F18-21} e {F20-10}. O que mudou em relação ao parágrafo acima, item a item: {F18-20} é cartão novo e já nasce fechado (mergeado e deployado em 2026-08-09, a disciplina de número passou a cobrir os 16 tipos de bloco por exceção nomeada em vez de lista curada declarada tipo a tipo); {F18-21} é cartão novo e nasce aberto (o limite que sobrou da correção acima, sigla de custo seguida de quatro dígitos inteiros ainda escapa da guarda); e {F8-07} sai da lista de abertos por correção de prosa, não por trabalho novo desta rodada, o cartão já estava [x] desde 2026-08-08 (fechado, mergeado e deployado, ver o próprio card) e o parágrafo anterior não tinha acompanhado o texto. Contando a Fase M dentro da barra de Construção, como o renderizador faz: 171 de 184 = 92,9%. Testes não foram remedidos nesta rodada (carregado o último valor publicado, 91 de 161 = 56,5%). Pelo peso 40/40/20 do progresso geral, o geral fica em 88,5%.
Reconciliação em 2026-08-09, mesmo dia, dois cartões mudam de estado. {F8-15} fechou: mergeado e deployado, provado em produção (o próprio cartão descreve a prova). No mesmo commit nasceu {F8-25}, aberto, achado fora do diff da verificação de {F8-15}: a terceira ocorrência do padrão ler, chamar a rede e gravar cego, ainda sem conserto em duas rotas da pesquisa de mercado, por isso pede varredura do padrão e não conserto caso a caso. Recontagem pelo mesmo método do parágrafo anterior (expressão regular sobre toda linha de cartão, aceitando letra minúscula no fim do código, sem herdar nenhum número por adoção): conferido que cada código aparece exatamente uma vez, sem duplicidade, 214 linhas de cartão para 214 códigos únicos. Placar: 214 cartões no total, sendo 29 de Planejamento (fechados), 2 de Mockup (fechados) e 183 de Construção, dos quais 170 fechados e 13 abertos. Os 13 abertos: {F8-08}, {F8-16}, {F8-17}, {F8-18}, {F8-19}, {F8-20}, {F8-25}, {F12-07}, {F18-11}, {F18-16}, {F18-19}, {F18-21} e {F20-10}. O que mudou em relação ao parágrafo anterior, item a item: {F8-15} sai da lista de abertos (fechou); {F8-25} entra (nasceu aberto); nenhum outro cartão mudou de estado. {F8-08} segue aberto sem alteração nesta reconciliação, mesmo com outra frente de trabalho em curso nele: ele só fecha quando o próprio cartão declarar a prova. Contando a Fase M dentro da barra de Construção, como o renderizador faz: 172 de 185 = 93,0%. Testes não foram remedidos nesta rodada (carregado o último valor publicado, 91 de 161 = 56,5%). Pelo peso 40/40/20 do progresso geral, o geral fica em 88,5%.
O total de Construção SOBE quando a obra encontra defeito, e isso é o desenho funcionando, não o placar piorando. Entre 04/08 e 05/08 ele foi de 167 para 171: fecharam {F4-05}, {F7-04} e {F10-07}, e nasceram cinco cartões, todos de defeito MEDIDO durante a construção, nenhum de escopo novo inventado. {F4-07} (o Inventário afirma vinte parâmetros editáveis por painel e a lista de fontes é constante congelada em código), {F5-06} (a página pública não declara viewport, então todo o desenho de tela estreita fica adormecido em celular real), {F8-15} (salvar uma parte do briefing apaga as outras), {F18-19} (andaime interno da IA vazando no texto que o cliente lê) e o próprio {F4-07} vieram de olhar, não de planejar. Bases antigas que circularam aqui e não valem mais: "167 de Construção com 12 abertos", "191 cartões com 134 fechados" e "16 cartões abertos".
O ModularReports está com o DRS aprovado e o mockup navegável (17 telas) publicado e aprovado: Planejamento 29 de 29 cartões = 100%, Fase M 2 de 2 = 100%, Construção 170 de 187 cartões = 90,9%, Testes 90 de 165 itens = 54,5% (as duas contagens foram remedidas em 2026-08-09, cada uma no seu próprio documento, e conferidas contra o estado efetivo que o sistema serve; {F5-09} não tem TA associado no Plano de Testes). Pelo peso 40/40/20 do progresso geral, e contando a Fase M dentro da barra de Construção como o renderizador faz (172 de 189 = 91,0%), o geral fica em 87,3%. Este parágrafo é o estado presente e foi reescrito junto da recontagem do topo; os parágrafos datados acima são história e descrevem o momento deles, não o de agora.
O percentual CAIU de 92,4 para 90,8 e isso é o processo funcionando. Nenhum cartão fechado reabriu: três nasceram, todos de defeito medido durante a obra. Enquanto a construção continuar olhando o produto de verdade, o denominador vai crescer, e um placar que só sobe seria sinal de que ninguém está olhando.
O denominador caiu de percentual porque subiu de tamanho, e isso é o esperado. O geral recuou de 72,9% para 69,6% sem que uma linha de código fosse desfeita: 16 cartões novos entraram na fila e 18 itens novos entraram no Plano de Testes. Progresso que sobe quando o escopo cresce seria o sinal errado.
A família "peça pronta que ninguém aciona" fechou INTEIRA em 2026-08-04, com {F10-05}, {F10-06}, {F11-03} e {F20-08}. Metade dela era trabalho já feito e inalcançável, não trabalho pendente. O {F20-08} foi destravado por decisão de MECANISMO e não de produto: reusar a localização de dado em artefato que já existe, apontada para as chaves canônicas do funil, sem inventar um terceiro caminho. Essa decisão não subiu ao dono porque as chaves já tinham sido decididas por ele na planilha de referência, e o que restava era mecanismo. Três propriedades ficaram provadas ao vivo: a camada de resultado não ramifica por modelo de venda, etapa não encontrada sai como ausente e nunca como zero, e a distinção dos dois nulos vale no caminho real (sem modelo declarado a composição fica idêntica à anterior, e artefato ilegível recusa com motivo em vez de fabricar bloco).
Fatia entregue em 2026-08-03: dois cartões fechados e um defeito real de publicação corrigido. O {F20-09} fechou com o aviso de conflito de eixo nomeando as séries em texto, na mesma caixa vermelha dos demais erros, e o achado é o de sempre: a detecção já existia no backend e a API já devolvia o aviso, faltava o painel consumir. O {F18-06} fechou com identidade persistente da recomendação, critério declarado (dono e horizonte como portas, similaridade dentro delas) e cinco controles negativos de isolamento, com o limite escrito em código: o limiar é calibrado por sonda sintética, não por dado real, e calibrá-lo virou o {F20-10}. Junto vieram dois defeitos que ninguém tinha pedido para procurar: o gate de proveniência não enxergava o plano de ação (número cru publicava sem verificação, provado ANTES de corrigir e agora ligado ao caminho real, {F20-11}), e o validador de tamanho e horizonte do item existia, era testado e nenhum gate o chamava ({F20-12}), sétima ocorrência da mesma classe nesta sessão.
A interface do operador ganhou suíte de teste de componente em 2026-08-03, e ela fechou 4 itens Alfa e deixou 3 explicitamente parciais. A tela não tinha nenhum teste, então toda cláusula que dependia do comportamento dela só era verificável a olho. Estado medido: 2 arquivos de teste de componente com 11 casos, cada um nascido com controle negativo provado individualmente. Fecharam por cobertura INTEIRA do critério: {TA-28}, {TA-46}, {TA-54} e {TA-82}. Seguem abertos por cobertura PARCIAL, com a cláusula que falta nomeada em cada um: {TA-26} (falta o vínculo com o template), {TA-42} (falta esconder bloco) e {TA-15} (o teste prova a EXIBIÇÃO do histórico, e o critério é sobre a GRAVAÇÃO do log). Nasceu também o {F20-09}, achado de produto: o aviso de conflito de eixo existe, funciona e é apenas um contorno tracejado, sendo a exceção silenciosa num produto que avisa todo o resto em caixa vermelha com texto.
A auditoria do Plano de Testes fechou 28 itens Alfa em 2026-08-03, e REABRIU um. O reaberto é o mais importante: o {TA-66} estava aprovado validando a geração de PDF, e a fase que a implementa vive numa branch órfã, medida como não ancestral da linha principal nem de nenhuma das cinco outras em curso. Era um cartão de teste aprovado sobre funcionalidade que não existiria no produto entregue. A auditoria também levantou uma divergência entre documento e código no custo de julgar, que a medição reconciliou a favor do documento: o padrão é três passadas por critério, e o que mudou foi a concorrência delas, não a contagem.
A Fase F18 chegou a 15 de 18 em 2026-08-03, com as seis guardas centrais LIGADAS no publish real. Elas estavam escritas e testadas por unidade, e o publish não as chamava. Cada ligação foi provada individualmente, comentando a própria linha e conferindo que só o teste dela caía, uma a uma nas seis, e as sete suítes irmãs que também publicam seguiram verdes, o que prova a outra metade: nenhum relatório legítimo passou a reprovar. O achado mais fino da fase veio junto: a saída legal do {F18-07} era ela própria sempre reprovada pelo gate de proveniência estrutural, ou seja, a escotilha existia no texto e era inalcançável no código, que é o defeito exato contra o qual a regra do {F18-10} foi escrita. Sobram três cartões, cada um por motivo nomeado: o {F18-06} é lacuna de MODELO DE DADOS e não de ligação (escalar recomendação repetida exige persistir a identidade da recomendação entre edições, e não há onde guardar), e o {F18-11} e o {F18-16} têm evidência de entrega medida no repositório mas sem confirmação do H1.
A Fase F20 FECHOU em 2026-08-03, e a F18 avançou sem fechar. A F20 (modelo de venda e camada de resultado) fechou com a prova que sustenta a camada inteira: as 11 métricas com fórmula pendente foram exercitadas com todos os operandos presentes, e nenhuma emitiu, o que prova que a trava é a regra e não a falta de insumo; o prazo de retorno do CAC segue bloqueado mesmo com fórmula literal, porque um operando dele não existe no catálogo. As contagens do catálogo são função calculada e não constante escrita à mão, que é o que impede o placar de mentir depois. O funil canônico ignora o rótulo digitado (provado corrompendo o rótulo e conferindo que ele não chega ao HTML) e o funil livre roda inalterado, com regressão zero. A F18 avançou 6 cartões (chegando a 8 de 18, com 10 ainda abertos: {F18-01} a {F18-08}, {F18-11} e {F18-16}), e o {F18-15} derrubou uma premissa do próprio documento: a composição encadeia dois passos e não de cinco a sete, e as três passadas da régua eram independentes e agora rodam em paralelo. Nasceu um cartão aberto, o {F20-08}: os construtores do funil e do resultado existem e nada os aciona, porque falta neste documento o mapeamento entre o dado do artefato e as chaves canônicas de operando, e inventá-lo seria repetir o erro que as fórmulas pendentes evitam.
A Fase F19 (geração agendada) FECHOU em 2026-08-03, entregue e provada nos dois repositórios, com a tela passando por smoke real. A trava que o dono impôs como condição está provada por dois meios independentes: nenhuma referência a publicação no disparador nem no módulo de agenda, e o publish seguindo como endpoint único sem parâmetro de origem. A guarda não é uma condição dentro do publish, é a ausência do caminho. Três decisões de escopo do dono ficaram escritas como escopo declarado e não como pendência: a agenda automatiza a composição e não a coleta (ela adota um relatório já preparado, e sem ele o ciclo fecha com desfecho nomeado); o gate de honestidade roda no publish e não dentro da composição automática, com a condição registrada de que isso muda se a publicação virar automática; e o próximo vencimento aparece por extenso na tela para não duplicar a matemática de calendário em duas linguagens.
A Fase F17 FECHOU em 2026-08-03, com dois cartões acrescentados durante a entrega. Os dez originais ({F17-01} a {F17-10}) entregaram os cinco tipos de bloco novos, a cadência na instância, a guarda de largura de janela, a métrica não somável e a biblioteca semeada. O {F17-11} nasceu porque quatro dessas peças estavam construídas, testadas e NÃO ligadas ao caminho real, e cada ligação nasceu com um teste que prova que falha sem ela (procedimento executado, não presumido: comentar a chamada, rodar, ver vermelho, restaurar). O {F17-12} nasceu da causa por trás disso, que é o achado mais estrutural do dia: as peças nunca estiveram no índice público do pacote, e por isso eram indefinidas para o consumidor externo, enquanto os testes de unidade passavam por importarem o módulo de origem direto. A varredura mediu 63 nomes fora do índice, 62 internos de propósito e 1 defeito vivo, e a causa foi fechada com um teste de contrato por máquina (RNF-MANUT-006). O smoke visual da fase achou três defeitos que 32 testes verdes não pegaram, todos invisíveis para asserção e óbvios para o olho.
Mais três cartões fecharam no mesmo dia, e o mais velho deles com conclusão e não com "resolvido". {F1-17} era o cartão mais antigo em aberto do projeto: fechou com a régua recalibrada contra um relatório real de 25 seções e a conclusão medida de que comprimento não explica a reprovação dos dois documentos legados, ou seja, régua exigente contra prática antiga e não régua descalibrada. {F18-12} fechou entregando esse documento ao conjunto de referência. {F18-13} fechou REESCRITO: a premissa dele (rubrica julga bloco a bloco) foi medida no código e é falsa, o que também matou o risco de custo de 75 chamadas por relatório, que era 3 o tempo todo. Nasceu daí a RNF-MANUT-005: número que vem de cima também se mede.
Quatro números escritos estavam divergentes do que a medição mostra, e os quatro foram acertados contra a fonte, nunca um contra o outro. (1) O parágrafo abaixo dizia que a fila tinha ido a 143 cartões, quando o conjunto medido tinha 146 fora do Planejamento antes desta sessão. (2) O Inventário declarava 31 artefatos enquanto a própria tabela de tipos dele já somava 33. (3) O placar das fórmulas do Ap.10 chegou como oito pendentes e a recontagem por classe deu onze. (4) O DRS registrava um risco de custo de 75 chamadas de modelo por relatório para julgar qualidade, e a leitura do código mostrou 3, porque a rubrica nunca julgou bloco a bloco. Nenhum dos quatro mudou o trabalho feito: os dois primeiros eram aritmética acumulada na prosa, e os dois últimos eram afirmação recebida pronta que ninguém tinha medido. É deste dia que nasce a RNF-MANUT-005.
De 115 para 123 (primeiro movimento). A auditoria dos relatórios que a operação já entregou abriu a Fase F17 com 8 cartões. Três achados a sustentam. O primeiro é um gap de bloco medido: o registro do renderizador tem 11 tipos, e os que faltam estão em uso em 4 dos 6 relatórios reais no formato v2, o que significa que hoje o produto não reproduz o relatório que a operação entrega, e isso torna o critério de adoção (A.7) inalcançável por construção. O segundo é a decisão de cadência: periodicidade é atributo da instância e não tipo de modelo, o type fica fechado em três propósitos, e a cadência escolhe contra o que o relatório compara (RN-014, ADR-016, {F17-05}). O terceiro é a guarda de comparação, que fecha um defeito com caso real registrado, comparar janela parcial com janela cheia, mais a regra de métrica não somável entre janelas ({F17-06}, {F17-07}).
De 123 para 143 (segundo movimento). O council de 2026-08-02 sobre essa mesma auditoria acrescentou 2 cartões à F17 e abriu a Fase F18 com 16; somam-se a eles o {F12-07}, nascido da verificação real da Fatia 12, e o {F18-17}, nascido da varredura de topologia da própria documentação. Na F17, o tipo de abertura herdado do sistema irmão foi DECOMPOSTO em dois neste contrato: o divisor puro section_intro ({F17-01}, reescrito) e a régua de confiabilidade reliability ({F17-09}, novo), que é DERIVADA por código e não escrita à mão, mais o hero singular ({F17-10}, novo). Com isso o catálogo do Ap.3 vai a dezesseis tipos. O {F17-08} passou a semear dois modelos Globais confirmados mais um terceiro individual da agência de origem, porque uma amostra só, em formato antigo, não sustenta apresentar uma hipótese a todas as agências como se fosse o jeito certo de fazer. A F18 tem 17 cartões e fecha classes de mentira que ainda passam (base da comparação declarada dentro da frase, tupla de identidade completa do número exibido, causalidade degradada para coocorrência sem evidência, anti-fadiga medindo variação do dado contra variação do texto, histórico mínimo por seção, vocabulário único de não medido, sazonalidade no cadastro do cliente) e conserta a régua de qualidade que travava o {F1-17} (conjunto de referência recalibrado para o tamanho real do produto, segundo critério de coerência de documento, teto de tempo por bloco que escala). Ela também escreve o limite de usabilidade das travas ({F18-10}, recusa entrega a saída legal e existe escotilha registrada) e a regra de que toda trava nasce com fixture de controle negativo ({F18-11}).
De 146 para 162 (terceiro movimento). O dono respondeu em 2026-08-03 as duas perguntas que travavam o formato do produto, e cada resposta virou uma fase. {Q4b} trouxe a geração agendada para dentro do escopo, com as quatro palavras da resposta (configurável, editável, ativável e pausável) tratadas como requisitos e não como adjetivos: nasceu a Fase F19 com 9 cartões, e a tabela de estado da composição deixou de estar escrita como evolução futura e passou a ser parte do produto (ADR-021). Junto vieram as duas consequências que já se sabia onde doem: a geração automática não publica sozinha e não ganha atalho pelas guardas de honestidade ({F19-07}), e a correção do anti-fadiga ({F18-05}) virou PRÉ-REQUISITO da agenda, porque cadência curta e automática pressiona muito mais uma guarda que hoje mede texto contra texto. {Q5b} fechou a convivência com o outro sistema de páginas de resultados: não substitui nada por enquanto, e conviver não é espelhar (RN-019, ADR-024). E a fonte que o dono apontou para o modelo de venda (a planilha de gestão que a operação usa de verdade) abriu a Fase F20 com 7 cartões e fechou o item de investigação que o Ap.9 mantinha aberto, contra a parte cara da hipótese: o corte entre geração de leads e comércio eletrônico é estrutural, mas produz UMA estrutura com vocabulário trocável e não duas, então nenhum tipo de relatório novo nasce por vertical de negócio (ADR-022) e nenhum tipo de bloco novo nasce por causa disso (ADR-023). O primeiro cartão da F20 é a conferência das fórmulas contra a planilha, porque o que se transcreveu com certeza foi a lista das métricas e a estrutura do funil, não as fórmulas campo a campo, e uma fórmula errada aqui não quebra nada: produz um número verdadeiro na forma e falso no valor. Parte dessa conferência já foi feita em 2026-08-03: a exportação da planilha não expõe fórmula de célula, mas traz notas do autor, e delas saíram quatro formas transcritas (Lucro Bruto, ROMI, CPR e CAC Payback) mais a definição de unidade do LTV. As notas também revelaram uma métrica que não estava na lista inicial, o CAC Payback, então o catálogo do Ap.10 foi de 19 para 20 entradas, e restam 11 fórmulas a conferir.
A Fatia 12 (comentários) fechou quase inteira, e a lição dela vale para o projeto. A verificação de ponta a ponta REPROVOU o que os testes aprovavam: a política de segurança de conteúdo do leitor bloqueava os três scripts inline da fatia, então o magic link nunca criava sessão e dois botões eram código morto, com a tela parecendo correta porque tudo vinha do servidor renderizado. Corrigido por autorização de hash, sem afrouxar segurança e com unsafe-inline recusado nominalmente. Um segundo defeito da mesma verificação: todo comentário assinava como "Cliente" genérico porque a consulta do leitor não selecionava a coluna de nome do autor. Fechados por prova real com lado negativo: {F12-01}, {F12-03}, {F12-04}, {F12-05} e {F12-06}. {F12-02} segue ABERTO de propósito, e não por falta de construção: a primeira metade do H1 está provada (escrita por endpoint separado autenticado, anônimo recebe 401), e a segunda metade não foi exercida (falta assumir a role do leitor público contra o banco e confirmar que INSERT, UPDATE e DELETE em comentário são recusados). Fechar sem esse controle negativo seria arredondamento: isolamento de leitura não é ausência de permissão de escrita. Nasceu {F12-07}, aberto, com a guarda genérica que impede a repetição do defeito (RNF-SEG-007): o teste que existe hoje enumera os três scripts conhecidos e por isso não reprova o quarto.
Plano de Testes, recontado em 2026-08-08 no próprio documento, e o parágrafo abaixo estava errado em quase tudo. Universo e critério: toda linha de cartão de checklist do documento de testes, pelo código entre chaves, e a marca no texto do documento (não o clique de quem lê). Placar: 163 cartões, sendo 134 Alfa (124 por requisito, numerados sem buraco de {TA-01} a {TA-124}, mais 10 gerais nomeados: visual, KPI, segurança, performance, domínio, mobile, regressão, e o trio de sanity de uso, navegação e autonomia) e 29 Beta (26 por jornada, sem buraco de {TB-01} a {TB-26}, mais 3 nomeados: navegação, feedback e ajuda); 134 mais 29 fecham em 163. Feitos: 90, todos na seção Alfa; a seção Beta está em zero, o que é o esperado, porque Beta só começa depois de Alfa fechar. Os 2 cartões Alfa novos desta rodada são {TA-123} e {TA-124}, um para cada regra nova do DRS (RF-CUR-010 e RF-CUR-011); e {TA-23} voltou de feito para aberto, porque estava fechado sobre um gesto que não existe no sistema (o motivo está no próprio cartão). O parágrafo seguinte é o texto anterior, preservado por honestidade e MORTO como fonte de número: ele afirmava 136 itens e ao mesmo tempo 113 mais 24 fechando em 137, ou seja, não fechava nem consigo mesmo, e é exatamente o tipo de número que envelhece sem ninguém perceber porque ninguém recontava.
Plano de Testes: 136 itens, contados no próprio documento. Universo e critério: cartões de checklist do documento de testes, contados pelo código entre chaves na linha do cartão, não por estimativa. Placar: 113 Alfa (103 por requisito, numerados sem buraco de {TA-01} a {TA-103}, mais 10 gerais: visual, KPI, segurança, performance, domínio, mobile, regressão, e o trio de sanity de uso, navegação e autonomia) mais 24 Beta, e 113 mais 24 fecham em 137. Eram 123 antes desta rodada e 120 na versão publicada. Os 12 cartões Alfa novos são {TA-92} a {TA-103} (um por requisito novo, mais um pela regra de fixture de controle negativo, um pela guarda de script inline e um pela varredura de topologia do publish) e os 2 Beta novos são {TB-20} e {TB-21}, um por jornada nova. Feitos: 29. Cinco são os testes Alfa de comentário provados na verificação real da Fatia 12 ({TA-69}, {TA-71}, {TA-72}, {TA-73}, {TA-74}); o {TA-70} fechou junto com {F12-02} pela conferência de privilégio da Fatia 13; seis vêm da Fatia 13 ({TA-75}, que cobre o pipeline e a conferência de privilégio do ingestor, mais {TA-76}, {TA-78}, {TA-79}, {TA-80} e {TA-18}); o {TA-DOM} fechou com a Fatia 14; {TA-66} e {TA-67} fecharam com a Fatia 15, os dois por inspeção visual do arquivo; e sete vêm da Fatia 16 ({TA-08}, {TA-09}, {TA-10}, {TA-11}, {TA-14}, {TA-16} e {TA-81}), com a nota de procedência de segunda mão registrada no topo da seção Alfa. Segue aberto o {TA-77}, pelo mesmo motivo nomeado de {F13-03}.
Fatias 1, 1b, 2, 3, 4, 5, 6, 7, 8, 9 e a maior parte da 10 fechadas; a Fatia 11 fechou o backend ({F11-01}, {F11-02}) com prova real (testes de integração 9/9 verdes e publish real), o que também move os itens de teste Alfa {TA-03} e {TA-25} para feito, e um achado de segurança corrigido no mesmo commit (a RLS de report_goal só conferia company_id, nunca que account_id pertencia àquela company; fechado estendendo a RN-009 via trigger trg_report_goal_rn009_company_matches_account, com reteste confirmando a escrita cruzada recusada). Falta a interface: nenhuma tela do painel permite ao operador importar ou ver as metas hoje, então o cartão {F11-03} segue aberto. Na Fatia 10, {F10-00} a {F10-04} estão fechados e verificados; {F10-05}, {F10-06} e {F10-07} seguem abertos, cada um com o motivo no próprio card. Seguem abertos, cada um por motivo registrado no próprio card: {F1-17} (gate D, a régua reprova 2 documentos legados do conjunto de referência, e a causa medida está endereçada por {F18-12} e {F18-13}), {F4-05} (pesquisa de mercado, falta buscador externo), {F7-04} (marca no link público, metade), {F8-07} (criativos; o bloqueio de storage CAIU com a {F15-02}, e o motivo real, medido em 2026-08-08, é que não existe onde clicar: nenhuma rota cria bloco e nenhum controle da tela envia imagem para o rascunho, ou seja, é falta de construção e não de exercício), {F8-08} (cor por relatório, falta coluna), {F8-11}, {F8-12} e {F11-03}. A Fase 9 (publicação e link) fechou inteira, mais o card {F9-13}, cuja guarda de coerência de rótulo o {F18-03} agora generaliza para a tupla de identidade completa. Da Fatia 12 segue aberto o {F12-07}. As Fatias 9, 12 (menos um cartão), 13 (menos um cartão), 14, 15 e 16 estão fechadas.
Onde o projeto está, em uma frase: as 16 fatias do plano original do bloco 9 do DRS estão entregues, e os 41 cartões abertos são de outra natureza: 13 são pontas com motivo nomeado dentro das fatias fechadas, e 28 são as Fases F17 e F18, que não existiam no plano e nasceram da auditoria dos relatórios que a operação já entrega e do council de 2026-08-02. Ou seja, o que falta não é o que foi planejado e não saiu; é o que a realidade acrescentou depois.
A Fatia 15 (PDF) fechou, e a lição dela é sobre o que a verificação de estrutura NÃO vê. O arquivo foi conferido com os olhos, página a página, convertendo as páginas em imagem: documento real de 22 blocos e 11 tipos, tema claro, cor da agência aplicada em rótulo, valor e barras do funil, e ausência de marca da agência desenvolvedora confirmada NO VISUAL e não só por busca de texto (busca de texto não pega marca em imagem, que é o modo de vazamento que importa no white-label). Nenhum bloco cortado, nenhuma página começando por número solto, sem token cru, sem objeto cru, sem NaN.
A paginação passou por três estados, e os dois primeiros eram defeito: sem regra nenhuma, o gerador cortava bloco no meio e a página começava por um número sem rótulo (defeito de rótulo produzido pela diagramação); com a regra aplicada de forma uniforme, todo bloco inteiro era empurrado para a página seguinte, a primeira página ficava com cerca de 60 por cento em branco e o documento inchava para 11 páginas. O segundo defeito só apareceu porque alguém ABRIU o arquivo, não estava em relatório nenhum de execução. A regra final é seletiva por natureza do bloco (pequeno e indivisível não quebra; longo e divisível quebra entre linhas com o cabeçalho repetindo) e o resultado medido é 8 páginas densas. Virou RNF-REN-001 no DRS e {TA-105} no Plano de Testes, escrito com o histórico das duas tentativas, porque regra por natureza é o tipo de coisa que alguém "simplifica" para uniforme sem saber que o uniforme já falhou nas duas pontas.
Duas notas de método que a fatia rendeu. A {F15-02} foi construída seguindo a Especificação do bucket que já existia no Inventário ({ART-INFRA-01}), sem invenção de escopo na hora, que é exatamente o que a REGRA-INV-1 existe para garantir. E o defeito da moldura de mídia foi corrigido nas DUAS superfícies, leitor e PDF, porque era o mesmo CSS compartilhado: não era defeito do PDF, era defeito do renderizador aparecendo em dois lugares, e corrigir de um lado só criaria a divergência entre saídas que RF-REN-005 existe para impedir.
A Fatia 16 (painel do cliente) fechou, e com ela as 16 fatias do plano original do bloco 9 do DRS estão entregues. Provado com navegador real, login real e troca de empresa pelo switcher de verdade, desktop e 390 pixels: lista de clientes; ficha vazia visualmente distinta de ficha com erro de carregamento (a com erro traz aviso e ação de tentar de novo, e nunca se confunde com "nenhum relatório", que é a RN-015 aplicada à listagem e exatamente a confusão que deixou o defeito de rota sombreada passar duas vezes); ficha com dois relatórios reais, um publicado e um revogado, com selos distintos; e o portal do cliente mostrando o publicado com link e escondendo o revogado com a mensagem certa no MESMO request. Mais 22 asserções de integração real, incluindo os negativos de isolamento que mais importam: um contato não vê relatório de outra conta da MESMA agência, e a leitura direta por identificador sob a regra de acesso de outra agência devolve nada.
NOTA DE PROCEDÊNCIA, escrita porque a diferença importa: ao contrário das Fatias 13 e 15, os screenshots desta fatia não passaram pelo crivo visual do dono do projeto. A prova é de primeira mão de quem construiu, com método correto e detalhes que indicam observação real, mas é de segunda mão do ponto de vista da aprovação. O motivo está registrado: a sessão chegou perto do limite de contexto e a escolha foi declarar isso em vez de fingir uma verificação que não houve. Se aparecer defeito visual nesta fatia, é este parágrafo que explica, e não vale ler o cartão fechado como "foi visto e aprovado".
Mudança de comportamento do produto, decidida como rollout: o gate de publicação passou a EXIGIR tom de voz. Como a tabela de configuração de identidade da agência é nova, nenhuma agência tinha tom declarado e o gate travaria todas no primeiro dia. A saída não foi afrouxar o requisito: foi semear um tom neutro por agência, editável na tela, e apagar o default volta a travar, que é a prova de que a exigência continua viva. A semente não é exceção ao H1 do RF-ADM-006, é a segunda alternativa que ele já previa, e o relatório registra que a origem do tom foi o padrão da AGÊNCIA e não a ficha do CLIENTE, distinção que é a razão de o estado de identidade existir.
Bug real corrigido de passagem, e pré-existente: a cor da agência era lida no formato errado, então NENHUM relatório com ficha preenchida mostrava a cor certa. Não foi introduzido nesta fatia, foi descoberto nela, e seguiria invisível porque cor errada não quebra nada, apenas mente sobre a marca do cliente.
A Fatia 13 (analytics) fechou 6 de 8, e o critério para fechar foi o lado negativo exercitado, nunca o positivo observado. Fechados: {F13-01} (com o ingestor LENTO a resposta continua rápida, com ele FORA DO AR a página não cai, que são dois modos de falha distintos e os dois foram provocados de propósito), {F13-02} (3 acessos reais gerados no link público, refletidos no painel, sem semear linha no banco), {F13-05} (sem referrer sai "Origem desconhecida" e nenhum canal é fabricado, visto na tela), {F13-06} (prova visual em pixel, desktop e 390 pixels, com o isolamento ATACADO de propósito: relatório de outra agência responde como inexistente e não como permissão negada, que é a resposta certa porque negar permissão já confirma que o recurso existe), {F13-07} (o negativo prova que republicar não altera a duração já registrada, sem o que a métrica viraria ruído no critério de adoção que ela alimenta) e {F13-08} (privilégio conferido NO BANCO: a role do ingestor tem zero permissão de tabela e só execução da função). O {F13-06} também provou zero distinguível de não medido na superfície de analytics, que é a RN-015 aplicada, e que papel sem a capability é recusado na URL direta e não apenas escondido do menu.
O {F13-04} fechou: a segunda cláusula foi exercitada como o cartão exigia, com varredura do HTML BRUTO de 2 relatórios publicados reais, buscados anonimamente, e zero email de verdade (o único casamento foi um campo de formulário vazio). As duas cláusulas eram fatos diferentes e foram provadas separadamente, que era exatamente o motivo de o cartão ter ficado aberto uma rodada.
O {F13-03} segue aberto, e por decisão, não por esquecimento. Veio uma ANÁLISE DE CÓDIGO argumentando que o modo apresentar é renderização no próprio navegador do que já estava carregado, e portanto nunca chama a rota pública nem qualquer contagem. O argumento é estrutural e provavelmente certo. Mesmo assim não fecha: quem analisou registrou, honestamente, que não conseguiu completar o clique real porque um guard de contexto de empresa barrou. O critério desta fase é que caminho distinto que falharia CALADO fecha por exercício e não por leitura de código, e aplicá-lo só quando é confortável seria não ter critério nenhum.
A Fatia 14 (domínio custom e white-label) fechou inteira. O teste central não foi o caminho feliz, foi o negativo do isolamento, em HTTP real: uma requisição chegando pelo domínio ativo da agência A carregando token de relatório da agência B é recusada como inexistente. Domínio custom sem isolamento é vazamento entre agências com a marca do cliente por cima, então esse é o caso que precisava ser exercitado. A verificação de posse roda contra registro DNS REAL, sem simulação, e a tela foi provada ao vivo por navegador (criar, verificar, remover). Limitação DECLARADA (ADR-020), registrada como tal e não como pendência esquecida: ativar o domínio com certificado exige hoje um passo manual no servidor, porque a infra não tem emissão sob demanda; está documentado no código e num script de provisionamento, e escrito no card, no DRS e no topo da fase para que quem for ativar saiba de antemão.
Padrão que virou requisito, porque aconteceu DUAS vezes no mesmo dia: uma rota de caminho fixo registrada depois de uma rota dinâmica do mesmo prefixo é engolida por ela e passa a responder erro para todo chamador, com a interface mascarando como "não tem nada" (o front trata erro como lista vazia). Ocorreu no painel de analytics e no domínio custom, por caminhos diferentes. A suíte não pega porque chama o handler importado direto, pulando o roteador, ou seja, o caminho testado nunca é o caminho servido, que é a mesma família do bloqueio de scripts inline do leitor ({F12-07}). Virou RNF-MANUT-004 no DRS e o cartão {F18-18}, que exige a conferência de ordem de registro, o teste batendo no HTTP real, e a varredura retroativa das rotas existentes, já que as duas ocorrências conhecidas foram achadas por acaso e não por busca.
O {F12-02} fechou de carona. Ele estava aberto exatamente por faltar o controle negativo sobre a role do leitor público, e a conferência de privilégios feita em {F13-08} confirmou que essa role segue sem permissão de escrita. É o valor de ter deixado aberto em vez de arredondar: o cartão foi fechado por um fato, e não por uma suposição que ninguém teria revisitado.
Comportamento que parecia defeito e está documentado agora: o Super Admin precisa de contexto de empresa ATIVO para ver o painel de analytics de uma agência; em modo global ele vem vazio. Isso é correto, porque a regra de acesso ao relatório é por agência e não abre exceção para o papel que enxerga todas as empresas, e uma exceção assim seria uma segunda regra de acesso ao mesmo dado (é assim que isolamento entre agências morre). Registrado no DRS (RF-ANL-006, Notas), no topo da Fase F13 e na jornada "Auditar uso" do Super Admin, porque custou tempo de investigação e voltaria a custar.
As decisões técnicas de construção foram resolvidas no DRS (bloco 9, J.4) e a descoberta de integração da plataforma-mãe já fixou os internos que o Reports reusa (Fase 0, {P0-29}). As duas decisões que travavam o formato do produto foram RESPONDIDAS em 2026-08-03 e viraram fase. {Q4b}: a geração agendada entra no escopo, configurável, editável, ativável e pausável, e por isso nasceu a Fase F19 e a report_job deixou de ser evolução (ADR-021). {Q5b}: o ModularReports não substitui nada por enquanto, os dois sistemas convivem, e a convivência não é espelhamento (RN-019, ADR-024). {Q4} ficou respondida por consequência de {Q4b}: o relatório compara períodos, e a edição agendada compara com a anterior automaticamente. Junto veio a resposta sobre o modelo de venda, que fechou o item de investigação do Ap.9 e abriu a Fase F20. Quatro decisões de produto seguem em aberto no Questionário, e são do dono: {Q1} (como uma agência parceira nova começa), {Q2} (quem libera o acesso do cliente para comentar), {Q5c} (já que os dois sistemas convivem, por qual critério cada relatório vai para um ou para o outro, e quem decide na hora) e {Q8} (as métricas comerciais da agência aparecem na página do cliente, ou são só de gestão interna). Nenhuma das quatro impede os cartões da fila atual: {Q8} tem default seguro escrito no Ap.10 e imposto por RF-REN-017. {Q3}, {Q4}, {Q4b}, {Q5}, {Q5b}, {Q6} e {Q7} já foram respondidas.
Planejamento
Concluída na aprovação do DRS. Vale 40% do projeto (seção Planejamento). Estas etapas cobrem tudo que aconteceu antes de começar a construir o sistema.
0.1 Entendimento e Elicitação
0.2 Especificação (o DRS)
0.3 Documentos companheiros
0.4 Validação e Qualidade
0.5 Publicação e Aprovação
Fase M, Mockup Navegável (pós-Planejamento, gate de aprovação)
Dependência: DRS aprovado (Planejamento fechado). A construção (F1 em diante) não começa com esta fase aberta. Concluída: mockup completo publicado em modulareasy.com/mockups/modulareasy/modularreports e aprovado.
DRS (bloco 6, F.3) e Ap.6. As 17 telas do sistema (login, lista de relatórios, clientes, pasta do cliente, wizard de 4 passos, direção e prompt, compositor com ajuste fino, templates, página pública, modal de publicação, modo apresentar, analytics, identidade da agência, usuários, painel do Super Admin, painel do cliente) com o design system da plataforma-mãe no chrome do app e a linguagem visual das páginas de resultados no leitor público, base compartilhada e Mapa do mockup flutuante.
Como verificar (teste primeiro): todas as páginas respondem 200 na URL publicada; cada tela reflete a superfície correspondente do bloco 7 (F.1); smoke visual página a página em desktop e mobile.
Aceite (H1): Dado o mockup publicado, Quando o cliente navega pelo Mapa do mockup, Então alcança toda tela do sistema, E cada tela aparece com a identidade oficial da plataforma-mãe.
Gate. Apresentado e aprovado.
Como verificar (teste primeiro): aprovação registrada; DRS declara o mockup (bloco 6, F.3) REFERÊNCIA VISUAL APROVADA e GUIA DO DESENVOLVIMENTO.
Aceite (H1): Dado o mockup aprovado, Quando a construção de uma tela começa, Então a página correspondente do mockup é o guia, E divergência visual se resolve consultando o mockup.
Definição de Pronto M: mockup completo no ar, aprovado, e vinculado no DRS como guia do desenvolvimento. CUMPRIDA.
Fase F1, Protótipo do pipeline IA (Fatia 1, gates HARD de I.1)
Dependência: nenhuma além do mockup aprovado (Fase M). Fatia cravada como risco número 1 do produto (I.1): entrega o package e as interfaces identity, storage e render, o sandbox de parse isolado e a composição in-process, agnóstico à decisão de módulo, mais os gates de qualidade e segurança que reprovam o protótipo se falharem.
DRS I.1, Ap.8; artefato ART-MIG-02 do Inventário. Montar o conjunto de referência da Fatia 1: converter relatórios reais do formato v2 para o contrato v3 (blocks[] com id, data_points com dp: e source), a partir dos relatórios reais já entregues e dos artefatos crus quando existirem; onde só há o valor formatado, marcar provenance: "legacy". Incluir os casos: os relatórios reais completos, o export adversarial com número inflado (fail-loud), e casos fora do nicho.
Como verificar (teste primeiro): o script de conversão v2 para v3 roda sobre os relatórios reais e produz documentos v3 válidos com id por bloco e dp: por número; o caso adversarial fica marcado como tal; o conjunto tem os relatórios reais, o adversarial e ao menos dois fora do nicho.
Aceite (H1): Dado o acervo de relatórios reais em v2, Quando o script de conversão roda, Então cada relatório vira um documento v3 com bloco id estável e todo número com dp: (ou provenance: legacy quando só há o formatado), E o golden set inclui os reais, o caso adversarial de dado inflado e casos fora do nicho.
RF-BND-001. Na Fatia 1 a fronteira é declaração de princípio no código do package e da composição: nenhuma função escreve no domínio de metas contínuas do módulo results; o Reports não modela input semanal nem série temporal viva de KPI. A varredura de rotas que prova a fronteira na aplicação real roda em F6/F7, quando as rotas do Reports existem.
Como verificar (teste primeiro): o código do package e da composição não importa nem escreve nenhum domínio de metas contínuas do results; nenhuma estrutura de dado do protótipo modela input semanal.
Aceite (H1): Dado um pedido para acompanhar uma métrica ao longo do tempo com metas semanais, Quando o operador está no ModularReports, Então o sistema não oferece input semanal nem série temporal viva de KPI, E nenhuma tela do Reports grava ou atualiza registro no domínio de metas contínuas do módulo results.
RF-ING-003, RF-CMP-009 (parte do parse). Container efêmero sem rede de saída que recebe os bytes do artefato e devolve JSON estruturado saneado; defesas de arquivo malicioso (magic bytes, XXE desligado, limite de razão de descompressão, SVG sanitizado, xlsm rejeitado); a falha é POR ARQUIVO (fail-closed por arquivo): o arquivo falho é rejeitado com o motivo apontado ao operador e vira lacuna declarada, sem derrubar o parse dos demais do lote, e nunca degrada aberto. É o único processo de fato isolado do pipeline.
Como verificar (teste primeiro): o container de parse não resolve DNS externo; um xlsm é rejeitado, um SVG com script é sanitizado, um zip bomb acima da razão limite é recusado; num lote de vários arquivos com um falho, o falho é excluído com motivo e vira lacuna, e os demais seguem.
Aceite (H1): Dado um arquivo submetido ao parse, Quando o parser roda, Então ele executa sem acesso de rede de saída e devolve JSON saneado, E um xlsm é rejeitado, um SVG com script é sanitizado, um zip bomb acima do limite é recusado, E a falha de um arquivo do lote o exclui com motivo e vira lacuna sem derrubar os demais (fail-closed por arquivo).
RF-CMP-005. Extrator por código lê célula/campo e injeta o valor; a IA escreve só narrativa.
Como verificar (teste primeiro): para um artefato com valores numéricos, todo número exibido é rastreável ao extrator, nenhum vem do texto gerado pelo modelo.
Aceite (H1): Dado um artefato com valores numéricos, Quando o pipeline compõe, Então todo número exibido no relatório provém do extrator determinístico ligado a uma célula ou campo, E nenhum número exibido é gerado pelo texto do modelo de linguagem.
RF-CMP-006. Cada número referencia dp:<id> com origem (artefato mais localizador, ou fonte externa da allowlist); número sem dp: é sinalizado como pendência.
Como verificar (teste primeiro): todo número colocado num bloco carrega dp:<id> resolvível; um número sem dp: aparece na lista de pendências.
Aceite (H1): Dado um relatório em composição, Quando um número é colocado num bloco, Então ele carrega um dp:<id> cuja origem aponta para um artefato e localizador ou uma fonte da allowlist, E um número sem dp: é sinalizado como pendência.
RF-CMP-008. Check determinístico confere a direção afirmada (melhorou/piorou) contra a comparação numérica de origem.
Como verificar (teste primeiro): um caso com narrativa invertida (diz "melhorou" com número que piorou) é sinalizado pelo check.
Aceite (H1): Dado um número cuja narrativa afirma uma direção (subiu, caiu, melhorou, piorou), Quando o check de polaridade roda, Então a direção afirmada é conferida contra a comparação numérica de origem, E uma inversão é sinalizada.
RN-012, RF-CMP-018; golden set de {F1-00}. Mecanismo por código, não por IA: não supressão do fato adverso aritmético (gasto maior que zero e conversões igual a zero, meta de CPA excedida, receita igual a zero com gasto maior que zero); selo calculado por código na métrica âncora derivada do campo objetivo_campanha do briefing (enum fechado do Ap.1) pelo mapa objetivo para métrica primária (parâmetro do ART-CFG-01, seed default por objetivo), não editável, não removível pela narrativa; pureza de canal (o label do dp: entra sanitizado por metric_id, nunca concatenado no corpo da instrução). Provar contra o golden set de {F1-00}, incluindo o caso adversarial de tentativa de supressão via evitar da direção; no compositor, o bloco do fato adverso aparece protegido e esconder é recusado na hora com o motivo (RF-CMP-018).
Como verificar (teste primeiro): um caso golden com gasto maior que zero e conversas igual a zero forçando supressão via evitar é bloqueado no publish; a métrica âncora sem selo é bloqueada; um label malicioso de dp: não chega ao corpo da instrução (só o metric_id sanitizado chega).
Aceite (H1): Dado um relatório em que um fato adverso aritmético de RN-012 está presente nos dados mas ausente da composição, ou cuja métrica âncora publica sem o selo íntegro, ou com um badge de valência oposta ao selo, Quando o operador tenta publicar, Então a publicação é recusada, E o sistema aponta exatamente o fato suprimido, o selo ausente ou o badge contradito.
RF-CMP-010. Wizard é UI sobre a API; a separação compor versus publicar é por capability (reports.compose versus reports.publish), não por escopo de token; nenhum ator de serviço carrega reports.publish. A composição interna roda com claims de sistema, com reports.compose e jamais reports.publish.
Como verificar (teste primeiro): um ator com reports.compose mas sem reports.publish que tenta publicar é recusado; publicar só funciona numa sessão humana com reports.publish.
Aceite (H1): Dado um ator com a capability reports.compose mas sem reports.publish, Quando ele tenta publicar um relatório, Então a publicação é recusada, E publicar só conclui a partir de uma sessão humana com reports.publish.
RF-CMP-011. A composição reusa o gateway de IA da plataforma-mãe (proibido client de IA próprio) com a cadeia gratuita (dois provedores gratuitos encadeados, primário e fallback); sem upgrade automático para modelo pago.
Como verificar (teste primeiro): inspeção do código mostra a composição chamando o gateway da plataforma-mãe, sem client de IA próprio; a configuração usa só a cadeia gratuita, sem fallback automático para modelo pago.
Aceite (H1): Dado o pipeline de composição, Quando ele roda em produção, Então usa apenas o gateway de IA da plataforma-mãe com a cadeia gratuita configurada, E nenhum upgrade para modelo pago acontece automaticamente sem decisão registrada.
RF-CMP-012. Cada composição registra tokens e tempo de processamento.
Como verificar (teste primeiro): ao final de uma composição, o registro de tokens e tempo existe e está disponível para acompanhamento operacional.
Aceite (H1): Dado uma composição concluída, Quando ela termina, Então o sistema registra tokens consumidos e tempo de processamento daquele job, E esses valores ficam disponíveis para o acompanhamento operacional.
RF-CMP-013. Tom de voz da ficha de identidade do cliente é injetado como parâmetro direto da narrativa.
Como verificar (teste primeiro): dois clientes com tons de voz diferentes ("técnico e sóbrio" vs "leve e próximo") geram narrativas com registro distinto.
Aceite (H1): Dado um cliente com tom de voz "técnico e sóbrio" e outro com "leve e próximo", Quando a IA compõe para cada um, Então a narrativa reflete o tom declarado do respectivo cliente, E o tom nunca é inferido em silêncio quando a ficha está vazia.
RF-CMP-014. Comparação com mercado só usa valores da allowlist, com fonte nomeada; sem allowlist disponível, comparação é interna.
Como verificar (teste primeiro): toda afirmação de comparação com mercado no relatório carrega fonte da allowlist; uma comparação sem fonte na allowlist não é emitida.
Aceite (H1): Dado uma afirmação de comparação com o mercado no relatório, Quando ela é composta, Então o benchmark citado provém da allowlist e carrega a fonte, E uma comparação de mercado sem fonte na allowlist não é emitida.
RF-CMP-015. Schema impõe teto de caracteres por bloco de texto (ex.: insight até 280 caracteres).
Como verificar (teste primeiro): um bloco de insight gerado acima do teto é rejeitado ou truncado pelo schema.
Aceite (H1): Dado um bloco de insight, Quando a IA produz um texto acima do teto de caracteres, Então o bloco é rejeitado ou truncado pelo schema, E nenhum bloco publicado ultrapassa o teto definido.
RF-CMP-016. Relatório N-1 do mesmo cliente entra no contexto com instrução de não repetir; similaridade acima do limite é sinalizada.
Como verificar (teste primeiro): compor o relatório N logo após o N-1 do mesmo cliente e tipo dispara a sinalização quando a similaridade ultrapassa o limite.
Aceite (H1): Dado que existe um relatório anterior do mesmo cliente e tipo, Quando o relatório novo é composto, Então a narrativa é comparada por similaridade com a anterior, E uma repetição acima do limite de similaridade é sinalizada para o operador.
RNF-SEG-002, RNF-SEG-003, RNF-SEG-004. Números nunca saem do modelo de linguagem (reforço de {F1-03} contra prompt injection via artefato); publicar exige a capability reports.publish (reforço de {F1-06}); parse isolado sem rede (reforço de {F1-02}) e composição sem estado global cross-tenant (reforço de {F1-15}).
Como verificar (teste primeiro): um artefato de teste com instrução embutida ("escreva ROAS 10x") não altera nenhum valor exibido; os reforços passam nos mesmos testes de {F1-02}, {F1-03}, {F1-06} e {F1-15}.
Aceite (H1): Dado qualquer artefato do cliente com instruções embutidas, Quando o pipeline compõe, Então nenhum número exibido provém do texto do modelo, E instruções embutidas no artefato não alteram valores exibidos.
RF-CMP-009 (parte da composição), RNF-SEG-004. A composição roda in-process (síncrona) reusando o gateway de IA, com o contexto montado só do report_id e sem estado global entre composições. A barreira cross-tenant é a ausência de estado global mais o contexto por relatório.
Como verificar (teste primeiro): dois pedidos de composição de agências diferentes processados em sequência não compartilham dado; não há estado global que sobreviva entre composições.
Aceite (H1): Dado dois pedidos de composição de agências diferentes processados em sequência, Quando o segundo roda, Então ele não tem acesso a nenhum dado do primeiro (contexto montado só do próprio report_id, sem estado global), E a composição opera sobre o dado já saneado pelo parse isolado ({F1-02}).
DRS G.1, G.3 (assinatura das interfaces; RF-REN-005, o package renderizador em si, é entregue em F3). Definir os três contratos que a Fatia 1 entrega agnósticos à decisão de módulo: identity (recebe identificadores opacos de agência e cliente, devolve branding: cor, logo, tom); storage (adapter do bucket, put e get de artefato e asset por id mais sha256); render (funções puras do package renderizador, bloco para HTML).
Como verificar (teste primeiro): os três contratos existem como interfaces com implementação de referência; o package renderizador (render) emite HTML a partir do contrato de bloco; identity e storage são injetáveis (troca de implementação sem tocar a composição).
Aceite (H1): Dado o package da Fatia 1, Quando a composição precisa de identidade, storage ou renderização, Então ela usa as interfaces identity, storage e render, E nenhuma delas acopla o protótipo à decisão de módulo (chrome, banco, storage concretos entram por injeção).
FECHADO em 2026-08-03, com conclusão MEDIDA, e não com "resolvido". Era o cartão mais antigo em aberto do projeto. A régua foi recalibrada com um relatório REAL de 25 seções, entregue a cliente pagante e anonimizado antes do parse (cliente, sub-marcas, cidades, concorrentes, domínio e identificadores de conta trocados; números e estrutura preservados). O resultado fecha a dúvida que travava o gate D: os dois documentos legados continuam reprovando, pela dimensão de honestidade comparativa, e passam limpos no critério novo. Ou seja, comprimento não explica a reprovação. A conclusão que fica registrada é: régua exigente contra a prática antiga da casa, e NÃO régua calibrada contra a escala errada. Isso confirma a decisão do dono em {Q6} de manter a régua exigente, e é por isso que o cartão fecha com essa frase e não com "resolvido". Veredito cru do relatório real, para constar: nota máxima nas cinco dimensões do critério antigo e média 3,33 no critério novo, que apontou eco entre seções distantes. Um documento que a operação entregou e o cliente pagou tira nota perfeita no critério que já existia e perde ponto no que acabou de nascer, o que é a melhor evidência possível de que o segundo critério faz falta. As duas pendências que o cartão esperava foram respondidas: o papel do golden legado como âncora de nota 4 caiu com {Q6} (o acervo antigo é material de referência de conversão, não padrão de qualidade), e a capacidade de IA gratuita para medir foi endereçada por {Q5} (stack de provedores gratuitos) e deixou de ser aperto quando se mediu que julgar custa 3 chamadas por documento, não 75.
Motivo de abertura original, MEDIDO em 2026-07-28 e preservado aqui como histórico. A rubrica está construída e validada: o juiz separa golden de degradado com folga (2,80 a 3,80 contra 1,20 a 1,80). Os gates A (zero alucinação numérica), B (tríade de segurança) e C (isolamento cross-tenant) estão VERDES. O gate D (qualidade da narrativa) segue aberto por dois fatos medidos: (1) dois dos documentos do golden set, que são conversões de relatórios LEGADOS entregues antes das regras do Ap.5, pontuam 2,80 com a dimensão de honestidade comparativa em 2 (comparação vaga sem dono nomeado), ou seja a régua nova reprova o padrão antigo da casa, e a premissa do card de que o golden equivale a nota 4 foi refutada pela medição; (2) a nota da composição PRODUZIDA pelo pipeline (que é o que o gate deveria medir) não pôde ser obtida porque a cota diária do provedor gratuito primário esgotou (100 mil tokens por dia, consumidos pelo próprio protótipo) com o segundo provedor gratuito fora do ar. Pendências que este card espera: decisão de produto sobre o papel do golden legado como âncora de nota 4, e capacidade de IA gratuita suficiente para medir (ver o Questionário).
DRS I.1, Ap.4; golden set de {F1-00}. Juiz por modelo de linguagem em temperatura 0, 3 passadas e mediana, calibrado com golden (nota 4) versus degradado (nota 1 ou 2), nas 5 dimensões (D1 a D5). Usa o golden set montado em {F1-00}.
Como verificar (teste primeiro): rodar o golden set de {F1-00} e o caso adversarial contra a rubrica; confirmar que o juiz separa golden (nota 4) de degradado (nota 1 ou 2); nenhuma composição publicável tem número sem proveniência ({F1-04}), isolamento cross-tenant quebrado ({F1-15}) ou publicação fora de sessão humana com reports.publish ({F1-06}); uma narrativa genérica sem âncora (sem ponto de dado, sem destaque do briefing, sem comparação nomeada) é reprovada pela rubrica (Ap.5 item 11); o A/B da taxa de composição sem edição (braço com a direção vazia versus braço com a direção preenchida) roda com a guarda: subir a taxa "sem edição" no braço preenchido não pode baixar D2 nem D3 da rubrica 5D em relação ao braço vazio.
Aceite (H1): Dado o golden set e o caso adversarial, Quando a rubrica roda, Então a média é maior ou igual a 3,0 com nenhuma dimensão igual a 1 e D2/D3 maiores ou iguais a 3, E os quatro gates HARD (zero alucinação numérica, tríade de segurança, isolamento cross-tenant, qualidade da narrativa) passam. Os indicadores de custo, latência e taxa de composição sem edição são termômetro NÃO bloqueante enquanto o conjunto de referência for pequeno (DRS J.4); os números de referência da Fatia 1 (não são gate) são: custo por página abaixo de US$ 0,50 em tokens, latência na faixa de 10 a 30 segundos (RNF-PERF-002) e ao menos metade dos blocos aceitos sem reescrita manual. Ficar muito fora desses números é sinal de revisar o pipeline, não de reprovar a fatia.
Definição de Pronto F1: os 4 gates HARD de I.1 verdes (zero alucinação numérica, tríade de segurança, isolamento cross-tenant, qualidade da narrativa pela rubrica 5D); package com as interfaces identity, storage e render, o sandbox de parse isolado e a composição in-process prontos para as fatias seguintes.
Fase F1b, Schema de dados (Fatia 1b)
Dependência: nenhuma (a migration não depende do chrome do Hub). Cria as tabelas do schema reports ANTES das fatias que as consomem: F2 mexe em snapshot e report_draft, F4 grava artifact, F5 lê snapshot. A migration de RLS, entitlement, RBAC e auditoria (que dependem do catálogo do Hub) fica em F6.
DRS E.1, E.2; artefatos ART-ENT-01 a 08 do Inventário. Migration que cria o schema reports com as 7 tabelas do E.2 (report, report_draft, snapshot, artifact, comment, link_event, template) mais report_goal (metas por cliente). Só a criação das tabelas; sem RLS, entitlement nem RBAC (esses vêm em F6). A report_job (evolução) e a agency_domain (Fatia 14) nascem nas suas fatias.
A ordem de criação respeita as FKs: template primeiro (referenciado por report.template_id, nullable), depois report, e por fim as que apontam para report (report_draft, snapshot, artifact, comment, link_event); report_goal referencia hub.accounts/hub.companies e não depende das outras. A migration cria também o trigger de integridade RN-009: um BEFORE INSERT/UPDATE em report que confere report.company_id contra o company_id do Account referenciado (hub.accounts.company_id) e rejeita gravar um relatório que cruze a agência de uma Company com o Account de outra.
Como verificar (teste primeiro): migration aplicada cria as 8 tabelas com as colunas do E.2; rollback remove tudo sem resíduo; rodar de novo é idempotente; inserir um report com company_id diferente do company_id do Account referenciado é rejeitado pelo trigger RN-009 (o par que bate é aceito).
Aceite (H1): Dado o banco do Hub sem o schema reports, Quando a migration de dados roda, Então as 8 tabelas (as 7 do E.2 mais report_goal) existem com as colunas, chaves e índices do bloco E.2 e das fichas do Inventário, E a migration é idempotente (rodar de novo não falha nem duplica).
Definição de Pronto F1b: as tabelas de dados do schema reports existem e as fatias F2 a F5 têm onde ler e escrever, sem esperar o chrome do Hub (F6/F7).
Fase F2, Contrato de bloco e snapshot (Fatia 2)
Dependência: {F1b-01} (tabelas do schema) e {F1-15} (a composição in-process, que produz o formato de blocos e pontos de dado). Esta fatia entrega o coração de dados que serve IA, curadoria, leitor e analytics.
RF-REN-001, Ap.2. blocks[], cada bloco com id estável, type, hidden?, tags?, props. id nunca regenerado (âncora de comentário e analytics). Publicar remove blocos ocultos do snapshot.
Como verificar (teste primeiro): republicar um relatório mantém o mesmo id de cada bloco não removido; um bloco marcado oculto some do snapshot.
Aceite (H1): Dado um documento de relatório, Quando ele é serializado, Então cada bloco tem um id estável que não muda entre republicações, E um bloco marcado oculto não aparece no snapshot publicado.
RF-REN-002. Tipos de bloco num registro declarativo (componente, schema, defaults); leitor ignora tipo desconhecido em silêncio, compositor sinaliza.
Como verificar (teste primeiro): um snapshot com um tipo de bloco fora do registro renderiza os demais blocos sem erro no leitor, e aparece sinalizado no compositor.
Aceite (H1): Dado um snapshot com um tipo de bloco que o leitor não conhece, Quando o cliente abre o link, Então a página renderiza os demais blocos sem erro e ignora o desconhecido, E no compositor o mesmo tipo desconhecido é sinalizado ao operador.
RF-REN-003. Payload carrega schema_version; leitor converte versões antigas por funções puras, sem reescrever o armazenado.
Como verificar (teste primeiro): um snapshot fixture em versão antiga renderiza correto após conversão em memória; o registro armazenado permanece na versão original após a leitura.
Aceite (H1): Dado um snapshot em versão de schema antiga, Quando o leitor o renderiza, Então ele o converte em memória para a versão corrente e exibe corretamente, E o registro armazenado permanece na versão original.
RF-PUB-001, RN-001, RN-002, RN-010. Publicar congela snapshot (blocos, metas vigentes, branding); editar depois não muda o já servido. A imutabilidade é imposta no schema (RN-010): a role de aplicação não recebe UPDATE direto em snapshot; a escrita acontece apenas pela função SECURITY DEFINER de publish/republish.
Como verificar (teste primeiro): alterar a identidade visual ou as metas do cliente após um publish não muda o relatório já publicado; editar o rascunho não altera o snapshot até novo publish; um UPDATE direto em snapshot pela role de aplicação é recusado (só a função de publish escreve).
Aceite (H1): Dado um relatório publicado, Quando a identidade visual do cliente ou as metas são alteradas depois, Então o relatório publicado continua exibindo os valores copiados no momento do publish, E editar o rascunho não muda o snapshot já servido até um novo publish, E um UPDATE direto em snapshot fora da função de publish é recusado (RN-010).
RF-ING-005, RN-006. Criativos e imagens referenciados por asset_id + sha256, nunca URL crua ou base64 no payload.
Como verificar (teste primeiro): inspecionar o payload de um bloco de mídia; confirmar ausência de URL crua e de base64 embutido.
Aceite (H1): Dado um criativo inserido num relatório, Quando o payload do bloco é gravado, Então o bloco referencia asset_id e sha256 do asset, E o payload não contém a URL crua nem o conteúdo base64 da imagem.
RN-001 a RN-006 (DRS E.4). Integridade cruzada: public_token vive no report (RN-001); snapshot.payload copia metas/branding (RN-002); todo número referencia dp: (RN-003); artifact respeita purge_after sem quebrar o publicado (RN-004); comment.block_id vira orphaned quando o bloco some (RN-005); asset por asset_id+sha256 (RN-006, reforço de {F2-05}).
Como verificar (teste primeiro): suíte de integridade cobre as 6 regras com um caso positivo e um caso de violação para cada.
Aceite (H1): Dado o schema reports povoado, Quando a suíte de integridade roda, Então as 6 regras de negócio de dados passam, E nenhuma violação passa despercebida.
Definição de Pronto F2: contrato de bloco v3 estável, snapshot imutável provado, e as 6 regras de negócio de dados verdes. Serve de base para F3 (renderizador), F8 (compositor) e F13 (analytics).
Fase F3, Package renderizador (Fatia 3)
Dependência: {F2-01} (contrato de bloco). Package de funções puras que serve leitor, preview e PDF a partir do mesmo contrato.
RF-REN-004. Markdown sanitizado com allowlist em todos os consumidores (leitor, preview, PDF).
Como verificar (teste primeiro): um bloco com script embutido no corpo não executa em nenhum dos três consumidores.
Aceite (H1): Dado um bloco cujo corpo contém um script embutido, Quando qualquer consumidor renderiza o bloco, Então o script é removido pela sanitização, E nenhum HTML executável do corpo chega ao navegador do cliente.
RF-REN-005, ADR-008, RNF-MANUT-001. Funções puras que emitem HTML, versionado como package, sem segunda implementação de blocos no React ou na composição.
Como verificar (teste primeiro): um mesmo documento renderizado pelo leitor, pelo preview e pelo PDF usa as mesmas funções (mesma saída de HTML para os mesmos dados); busca no código não encontra uma segunda implementação de bloco.
Aceite (H1): Dado um mesmo documento de relatório, Quando ele é renderizado pelo leitor, pelo preview e pelo gerador de PDF, Então os três usam as mesmas funções do package renderizador, E não existe uma segunda implementação dos blocos fora do package.
RF-REN-007. Conjunto de tokens --r-* por cliente, aplicando as cores da ficha de identidade.
Como verificar (teste primeiro): dois clientes com paletas diferentes renderizam com os tokens do respectivo cliente; trocar a paleta de um não afeta a página já publicada do outro.
Aceite (H1): Dado dois clientes com paletas diferentes, Quando seus relatórios são renderizados, Então cada página aplica os tokens --r-* do respectivo cliente, E trocar a paleta de um cliente não afeta a página já publicada do outro.
RNF-A11Y-001. As duas superfícies de leitura atendem contraste AA nos tokens claro e escuro.
Como verificar (teste primeiro): auditoria de contraste automatizada (axe ou equivalente) nas duas superfícies com os dois temas de tokens.
Aceite (H1): Dado o leitor e o PDF renderizados com os tokens do cliente, Quando a auditoria de contraste roda, Então ambos atendem AA nos dois conjuntos de tokens (claro e escuro).
RNF-MANUT-001. O renderizador vive como package próprio com versão, não como código espalhado pelos consumidores.
Como verificar (teste primeiro): o package tem número de versão próprio e é importado (não copiado) pelos três consumidores.
Aceite (H1): Dado o package renderizador, Quando um consumidor precisa dele, Então ele importa a dependência versionada, E nenhum consumidor mantém cópia própria do código de renderização.
Definição de Pronto F3: os três consumidores (leitor, preview, PDF) renderizam do mesmo package, sanitizados, com tokens por cliente e contraste AA.
Fase F4, Ingestão, quota e storage (Fatia 4)
Dependência: {F1-02} (sandbox de parse isolado), {F1-16} (interface de storage), {F1b-01} (tabela artifact). Entrada de dados do sistema.
RF-ING-001. Aceita planilha, CSV, PDF, Markdown, JSON, imagem; recusa tipos fora da lista.
Como verificar (teste primeiro): subir um arquivo de cada tipo suportado é aceito; um tipo fora da lista é recusado com mensagem clara.
Aceite (H1): Dado o passo de dados do wizard, Quando o operador sobe um arquivo de tipo planilha, CSV, PDF, Markdown, JSON ou imagem dentro da quota, Então o arquivo é aceito e associado à composição, E um tipo fora dessa lista é recusado com mensagem clara.
RF-ING-002. Quota (nº de artefatos, MB, páginas de PDF) validada antes de tokenizar; limites admin-editáveis.
Como verificar (teste primeiro): subir um conjunto acima da quota é recusado antes de qualquer conteúdo chegar ao parse ou à composição; a mensagem aponta o limite ultrapassado.
Aceite (H1): Dado a quota configurada em N artefatos e M megabytes, Quando o operador sobe um conjunto que ultrapassa N ou M, Então o sistema recusa antes de enviar qualquer conteúdo à composição de IA, E exibe qual limite foi ultrapassado.
RF-ING-003. Ligar o upload ao sandbox de parse já construído em {F1-02} (container sem rede de saída, magic bytes, XXE desligado, limite de razão de descompressão, SVG sanitizado, xlsm rejeitado, fail-closed); o resultado saneado alimenta a composição.
Como verificar (teste primeiro): um arquivo subido pela ingestão passa pelo sandbox de {F1-02}; o container de parse não resolve DNS externo; um xlsm é rejeitado, um SVG com script é sanitizado, um zip bomb acima da razão limite é recusado.
Aceite (H1): Dado um arquivo submetido ao parse, Quando o parser roda, Então ele executa sem acesso de rede de saída, E um xlsm é rejeitado, um SVG com script é sanitizado, e um arquivo compactado com razão de descompressão acima do limite é recusado.
RF-ING-004, RNF-LGPD-001. Artefato apagado do storage após TTL curto; relatório publicado permanece íntegro.
Como verificar (teste primeiro): simular expiração do TTL de um artefato de um relatório já publicado; confirmar remoção do storage e integridade do relatório.
Aceite (H1): Dado um relatório publicado, Quando o TTL do artefato cru expira, Então o artefato cru é apagado do storage, E o relatório publicado continua íntegro sem depender do artefato.
FECHADO em 2026-08-05, com o aceite INTEIRO coberto. A última cláusula (ver o achado, com fonte e endereço, na página pública que o cliente abre) foi fechada com artefato real de campanha de cliente, não com amostra sintética: relatório composto e publicado de verdade, e a página anônima aberta e OLHADA em tela grande e em celular, mostrando "Fontes externas consultadas" com o nome da fonte e o endereço clicável dentro do bloco de confiabilidade. Conferido por conta própria e não pelo relato: a publicação existe no banco com a fonte externa dentro do retrato imutável, e o link foi revogado no fim (responde como encerrado), então nenhuma página de teste ficou viva. Duas tentativas anteriores, com amostra inventada, foram recusadas pelos gates de proveniência do número e de âncora de honestidade; os gates estavam certos e a saída foi satisfazê-los com dado real, nunca contorná-los.
CONSTRUÇÃO (contexto), deployada em produção em 2026-08-05. O motivo antigo caiu: o buscador que faltava já era infraestrutura da casa e só não estava ligado (o metabuscador próprio do vendor, com resposta em formato de dados habilitada, já estava anexado à rede interna do Hub, então o serviço da API fala com ele por nome, sem porta pública, sem serviço pago e sem container novo). Entregue e provado: filtro por domínio à prova de subdomínio, sósia e credencial embutida na URL, com a fonte alegada tendo de casar com o endereço do achado; sumarização por IA cujo prompt, por construção, não tem parâmetro que aceite artefato, ponto de dado ou bloco do cliente, com o conteúdo da web entrando pelo canal de dado não confiável e a guarda de separação reprovando ANTES do disparo; curadoria real do operador no passo de dados do assistente, com a etapa nascendo desligada, três estados de falha nomeados e distintos, e falha nenhuma reprovando o relatório; e o achado curado virando ponto de dado com marca de proveniência própria, que chega ao bloco de confiabilidade com fonte e endereço. Ao ligar isto, dois requisitos que estavam inalcançáveis desde a primeira fatia passaram a ser chamados pelo caminho real de composição: a instrução de fontes de benchmark e a separação entre canal de instrução e canal de dado.
A cláusula que falta: ver o achado, com fonte e endereço, na página pública real que o cliente abre. Foi verificado o texto renderizado pelo mesmo registro de blocos que o leitor usa, e o leitor foi exercitado à parte, mas a publicação que provaria a última milha não existe como artefato: nenhuma das quatro publicações vivas no banco contém fonte externa. Duas tentativas de publicar um relatório de teste com achado foram recusadas por gates de OUTROS requisitos (proveniência do número e âncora de honestidade), corretamente, por a amostra ser sintética. Fecha quando um relatório com dado real e achado curado for publicado e a página anônima for olhada.
RF-ING-006. Etapa opcional, roda sem os artefatos do cliente no mesmo contexto, só fontes da allowlist, achados com fonte e URL nomeadas.
Como verificar (teste primeiro): ligar a pesquisa de mercado num relatório de teste; confirmar isolamento de contexto e que cada achado usado carrega fonte e URL.
Aceite (H1): Dado que o operador liga a pesquisa de mercado para um relatório, Quando a etapa roda, Então ela executa isolada dos artefatos do cliente e consulta apenas fontes da allowlist, E cada achado usado no relatório carrega a fonte e a URL de origem nomeadas.
RF-BND-002. Sem gestão, CRUD, biblioteca persistente nem histórico de artefatos crus; nenhuma tela de acervo.
Como verificar (teste primeiro): varredura de rotas confirma ausência de tela de listagem/edição/reabertura de artefatos crus como acervo.
Aceite (H1): Dado um artefato subido para compor um relatório, Quando a composição termina e o relatório é publicado, Então o artefato cru é descartado após o TTL configurado, E não existe nenhuma tela no sistema que liste, edite ou reabra artefatos crus como acervo.
FECHADO em 2026-08-08, com as cinco fatias mergeadas e deployadas. A contagem que o cartão
exigia como primeiro passo derrubou o número do documento, e as fatias A a E deram caminho de
edição a todos os parâmetros que tinham ponto de chamada vivo, mais a tela dos atributos do
cliente. A última fatia fechou com a prova certa: baixar o limite pelo painel e a rajada seguinte
ser recusada, sem reiniciar nada.
Duas coisas ficam declaradas como NÃO feitas, e não escondidas: três parâmetros seguem sem
tela de propósito, porque não são chamados por caminho nenhum (dar configuração a guarda que
ninguém aciona entrega tela que grava e não muda nada), e eles dependem dos cartões {F8-16} e do
irmão de verbosidade; e a restrição de banco que espelha a regra da sazonalidade não entrou,
porque exigiria migration no outro repositório.
O achado mais caro da série não foi nenhum parâmetro, foi o padrão: um parâmetro que parecia
ter um ponto de bloqueio tinha cinco, três deles o mesmo limite copiado em rotas irmãs e um em
outro processo; e o gate que reprova publicar número sem procedência era chamado sem a
configuração, julgando sempre pela regra antiga. Em ambos, a tela gravava e o sistema ignorava.
Antes de dar qualquer parâmetro por configurável: contar os pontos de bloqueio dele, inclusive
fora do processo principal.
E a configurabilidade cria risco que o valor fixo não tinha: enquanto o número era constante,
ele era o teto. Ver {F8-18}.
CONTAGEM FEITA em 2026-08-05: 4 editáveis, 16 não, e a aritmética fecha em 20. O cartão nasceu
porque um parâmetro tinha sido medido como falso; a contagem exaustiva dos vinte, por leitura do
código real e dos três níveis (coluna no banco, rota com guarda de permissão, chamada de fato pela
tela), mostrou que o falso era a regra e não a exceção. Só têm caminho de edição por uma pessoa:
retenção de retratos e medição, identidade padrão da agência, tipos de relatório, e a promoção de
um tipo a global. Os outros dezesseis são constante congelada, com todos os pontos de chamada
usando o valor padrão.
A causa é sistemática e está escrita no código: o pipeline foi desenhado com o valor injetável
por parâmetro, como preparo para configuração futura, e vários arquivos trazem o comentário "até o
painel existir". O Inventário documentou essa INTENÇÃO como fato entregue. É a família de defeito
mais cara deste projeto outra vez: peça pronta e inalcançável.
RF-CMP-014, RF-ING-006, e os demais requisitos citados no {ART-CFG-01}.
RECONTAGEM do planejamento, e ela mudou o corte: dos treze que pareciam precisar de tela,
três não são chamados em lugar nenhum do caminho real. Dois já são o cartão {F8-16}; o terceiro
é o teto de verbosidade por bloco, que estava no balde errado. Ele não é "falta painel": é
guarda que ninguém aciona, e por isso o requisito que promete que nenhum bloco publicado ultrapassa
o teto não tem hoje quem o faça valer. Os três ficam DIFERIDOS até serem ligados, porque
configurar antes de ligar entrega uma tela que grava e não muda nada, que é o defeito exato que
este cartão conserta. Sobram nove com ponto de chamada vivo. A aritmética fecha: nove com tela,
um só-tela, um em fatia própria, três diferidos, duas correções de ficha, mais os quatro que já
funcionam, igual a vinte.
**O escopo real, depois de separar o que é de categoria diferente (tratar tudo junto seria errar de
novo): os parâmetros acima precisam de origem em configuração e tela; um** (a base sazonal) já
tem coluna e rota prontas e precisa só da tela; um (a lista de guardas que admitem exceção)
não deve ganhar painel, porque o próprio requisito que a ficha cita declara que ela é escrita em
código e não julgada na hora, então o defeito ali é a linha do Inventário e se corrige apagando a
promessa; e um (o limiar de alerta de excesso de ressalvas) não existe no produto, é feature
ausente e não parâmetro sem tela, então sai deste cartão e vira requisito próprio quando alguém
decidir construí-lo.
O que implementar: para os treze, a origem em configuração por agência e a tela que a edita,
respeitando o escopo declarado (global do Super Admin, por agência do Admin), e o ponto de chamada
passando a LER a configuração em vez do valor padrão, que é onde a metade do trabalho de verdade
mora: sem isso, a tela grava e a composição continua ignorando. Para a base sazonal, só a tela. Para
a lista de guardas, corrigir a ficha do Inventário.
Como verificar (teste primeiro): para cada parâmetro que ganhar tela, um admin de agência (ou o
Super Admin, conforme o escopo) altera o valor e a composição seguinte se comporta diferente,
sem migration e sem deploy; e a contagem publicada bate com o número de parâmetros com caminho de
edição provado.
FATIA A ENTREGUE e DEPLOYADA em 2026-08-05: quota de entrada e TTL do artefato cru. Com ela
nasceu o mecanismo que as próximas fatias reusam: um catálogo declarativo de onde a tela é gerada,
o formato da rota é derivado e o teste de exaustividade é percorrido, para não existir uma segunda
lista que alguém esqueça de atualizar; os valores padrão reexportados das constantes que já
existiam, nunca redigitados; e a resolução acontecendo uma vez na entrada de cada caminho real,
nunca ponto a ponto. Valor ausente cai no padrão de antes, valor inválido cai no padrão sem passar
calado (nomeia o campo e registra auditoria), e salvar uma parte não apaga as irmãs. Provado com o
admin mudando o valor pela tela e o sistema recusando na hora, sem migration e sem reinício.
A lição que esta fatia pagou, e que as próximas oito herdam: a quota não tinha um ponto de
bloqueio, tinha cinco, e quatro foram descobertos tarde. Três eram o mesmo limite fixo
duplicado em rotas irmãs; o quinto vivia em outro processo e por isso escapou até de quem
recontou dentro do Hub. Ligar um ponto e deixar os irmãos fixos é pior que não ligar nenhum, porque
o placar fica verde e a configuração passa a mentir. Antes de dar qualquer parâmetro por
configurável: contar os pontos de bloqueio dele, inclusive fora do processo principal.
A configurabilidade também criou risco novo, registrado no cartão {F8-18}: enquanto o valor era
fixo, ele era o teto.
FATIA B ENTREGUE e DEPLOYADA em 2026-08-08: os cinco parâmetros da comparação (tolerância de
largura de janela, desfecho da comparação parcial, métricas não somáveis entre janelas, limiar de
materialidade e histórico mínimo por tipo de bloco), que foram juntos por dividirem o mesmo
caminho. O mecanismo da Fatia A passou no seu primeiro teste real: cada parâmetro custou uma
entrada no catálogo mais a fiação inevitável no ponto de chamada, sem nenhuma segunda lista, e a
contagem de pontos de chamada bateu com o previsto, sem irmão escondido. Um dos pontos religados
vive no disparador do ciclo agendado, que roda sozinho e ninguém olha.
**A varredura de apoio dava OK falso de duas formas, e as duas foram fechadas com o caso que ela
DEVE reprovar:** ela só enxergava referência literal à constante, e deixava passar quem chamasse a
função sem informar o parâmetro; e, rodada do jeito natural, lia o catálogo já publicado e
ignorava os parâmetros da fatia em construção, passando verde por omissão. Agora ela resolve o
catálogo certo sozinha ou falha fechada, e um chamador adversarial de verdade prova que reprova.
Corrigido junto: a descrição de um parâmetro enganava o admin, sugerindo que tirar uma métrica
da lista faria o sistema somar entre janelas, quando o efeito real é a agregação deixar de rodar.
Quem edita precisa ler o que o sistema faz, não o que o nome sugere.
**FATIA C ENTREGUE e DEPLOYADA em 2026-08-08: a allowlist de fontes e o mapa de objetivo para
métrica âncora**, deixados por último de propósito por serem os únicos dois cuja configuração
errada degrada a veracidade do relatório e não a conveniência. Com eles os nove parâmetros com
ponto de chamada vivo ficam completos, e o patamar global do Super Admin foi exercitado pela
primeira vez de verdade (precedência padrão, global, agência, com um gestor de agência recebendo
recusa ao tentar escrever no global).
O defeito que esta fatia achou é o mais grave da família até aqui: o gate de honestidade, que é
o que reprova publicar número sem procedência, era chamado sem o mapa configurado e portanto
julgava sempre pela regra antiga, mesmo com outra configuração gravada. A agência configurava, a
tela gravava, e a publicação continuava decidindo pelo valor velho. Provado corrigido por um teste
que muda o veredito do mesmo relatório conforme a configuração.
Decisões declaradas, porque as duas leituras eram defensáveis: allowlist vazia da agência
significa "não acrescento nada", e a base sobrevive, de modo que a guarda nunca esvazia; allowlist
vazia no escopo global é escolha do Super Admin e vale como está; e o mapa da âncora só é aceito
inteiro, com o parcial recusado na escrita, para ninguém descobrir a configuração quebrada só na
hora de publicar. A validação de domínio passou a recusar sufixo público isolado, com a cobertura
declarada como parcial em vez de prometida como total.
**FATIA D ENTREGUE e DEPLOYADA em 2026-08-08: a ficha do cliente ganhou a sazonalidade e o modelo
de venda. Esta foi a única fatia que não construiu configuração nenhuma**: o backend das duas
já existia, correto e testado, e o que faltava era caminho para uma pessoa. É a família de
defeito mais cara deste projeto na forma mais pura, porque passa por qualquer suíte verde. Os dois
entraram juntos por medição: o modelo de venda estava na condição idêntica, e o cartão que o
entregou estava marcado como feito. O efeito a jusante de ambos foi provado no comportamento, não
no código: ligar a sazonalidade muda a base de comparação da publicação, e o modelo de venda decide
se os slots de resultado nascem.
Um defeito grave apareceu no review e vale como regra: a leitura do perfil falhava em silêncio
e o salvar mandava sempre o objeto inteiro. Juntos, faziam a rede cair, o formulário abrir com
valores padrão e o admin apagar os dados reais achando que salvou, com aviso de sucesso
confirmando a perda. Guarda que falha aberta em silêncio não guarda, e é a mesma família do cartão
{F8-15}.
E uma decisão tomada pela spec, não pela conveniência: o mês de pico era gravado, exibido e
nunca consultado pela composição. O DRS fecha a base sazonal com o mesmo período do ano
anterior, sem exigir o mês, então o campo não foi ligado à toa: passou a ser apresentado como
informativo. Controle que promete efeito e não tem é pior que controle ausente.
Fica nomeado como não feito: a mesma regra que impede sazonalidade ligada sem mês existe na
rota, mas não no banco, pois pôr a restrição lá exigiria migration no outro repositório e não
entrou nesta fatia.
Aceite (H1): Dado o conjunto de parâmetros declarado no Inventário, Quando cada um é
percorrido, Então cada parâmetro tem origem em configuração e tela de edição no escopo declarado,
E o número publicado no cartão do Inventário é igual ao número de parâmetros com caminho de edição
provado, E nenhum parâmetro declarado editável permanece como constante no código.
Definição de Pronto F4: upload, quota, parse isolado e TTL funcionando ponta a ponta com um conjunto de artefatos real; nenhuma tela de acervo de artefatos.
Fase F5, Leitor público (Fatia 5)
Dependência: {F2-04} (snapshot imutável), {F3-02} (package renderizador). Serviço isolado somente leitura que serve o snapshot ao cliente final.
RF-REN-006, ADR-003, RNF-SEG-001 (caminho do leitor), RN-011. Cria a role reports_reader com GRANT de SELECT apenas em report, snapshot e comment (não no schema inteiro, que exporia rascunho, artefato e metas), sem alcance a outra tabela do schema nem a outro schema, sem query pesada. Habilita ROW LEVEL SECURITY nessas três tabelas e cria a policy PERMISSIVE do reports_reader que libera SÓ linhas de relatório com status publicado e não expirado (e o snapshot e os comment ligados a elas), resolvidas pelo public_token; essa PERMISSIVE do leitor convive com a RESTRICTIVE por company_id do operador ({F6-04}), porque cada policy é escopada pela sua role.
Como verificar (teste primeiro): a role reports_reader lê report, snapshot e comment de um relatório PUBLICADO e não expirado (permitido); tentar ler um relatório em rascunho, um não publicado, um expirado e um de outra agência retorna ZERO linhas (a PERMISSIVE só libera publicado+vigente); tentar um INSERT/UPDATE (recusado), consultar outro schema (recusado) e ler uma quarta tabela do próprio schema como report_draft, artifact ou report_goal (recusado).
Aceite (H1): Dado o serviço do leitor público, Quando ele resolve um link, Então usa a role reports_reader com SELECT apenas em report, snapshot e comment e uma policy PERMISSIVE que só devolve linhas de relatório publicado e não expirado, E não devolve rascunho, relatório não publicado, expirado ou de outra agência, E não tem permissão para escrever nem para ler report_draft/artifact/report_goal ou qualquer outro schema do Hub.
RF-REN-010. Link revogado ou expirado mostra página brandada com a marca da agência, sem conteúdo do relatório.
Como verificar (teste primeiro): abrir um link revogado e um expirado; confirmar página brandada e ausência total do conteúdo do relatório.
Aceite (H1): Dado um link revogado ou expirado, Quando o cliente o abre, Então vê uma página brandada com a identidade da agência e a mensagem para pedir novo link, E nenhum conteúdo do relatório é exibido.
RNF-SEG-005. Referrer-Policy: no-referrer, assets same-origin, token mascarado em log, resolução token para snapshot no-store.
Como verificar (teste primeiro): inspecionar os headers de resposta do leitor; conferir ausência do token cru em log de acesso.
Aceite (H1): Dado o leitor público, Quando ele serve uma página, Então envia Referrer-Policy: no-referrer e não registra o token cru em log, E os assets são servidos same-origin.
RNF-PERF-001. Página servida do cache; resolução token para snapshot sem varredura custosa.
Como verificar (teste primeiro): medir tempo de resposta de um link já publicado com cache quente; confirmar ausência de query pesada no caminho.
Aceite (H1): Dado um link já publicado, Quando o cliente o abre, Então a página responde a partir do cache sem query pesada, E a resolução token para snapshot não faz varredura custosa.
RNF-DISP-001. Leitor público e container de parse isolado têm deploy independente da fila de release do Hub; o chrome e a composição in-process acompanham a fila.
Como verificar (teste primeiro): publicar uma correção no leitor ou no container de parse sem passar pelo pipeline de release do chrome do Hub.
Aceite (H1): Dado uma janela em que a fila de release do Hub está congelada, Quando é preciso subir uma correção no leitor público ou no container de parse, Então ela sobe sem depender do deploy do Hub. O leitor tem disponibilidade maior que o painel e não há SLA contratual por ser ferramenta interna (DRS J.4).
FECHADO em 2026-08-08, mergeado e deployado. A declaração entrou na camada que já decide título
e metadados da página pública, sem tocar o renderizador compartilhado, que serve também o preview
do compositor e o PDF. Provado em produção, não só no código: relatórios publicados reais servindo
a declaração, e o limite de taxa do leitor intacto depois do rebuild.
A cobertura foi construída, não herdada: os relatórios publicados reais exercitam só 6 dos 16
tipos de bloco, então a verificação montou uma amostra com os 16 e mediu transbordo horizontal em
cada um. Testar só o que por acaso existe já deixou defeito passar neste projeto.
Acordar as regras revelou defeito novo, como previsto: o botão de comentar sobrepõe o título
em três tipos de bloco quando não há sobretítulo e o texto é longo. Estava invisível porque o
celular nunca renderizava em largura real. Fica em cartão próprio, por viver em outro arquivo.
E confirmou CSS morto: cinco classes de tela estreita que nenhum caminho emite, das quais
quatro sem nenhum aviso no código. Regra escrita e nunca executada é hipótese, não comportamento.
Achado de infraestrutura corrigido no mesmo deploy: o segredo da rota interna de configuração
tinha sumido do arquivo de ambiente em disco, sobrevivendo apenas injetado no container antigo,
o rebuild seguinte o teria perdido. Restaurado do template versionado e conferido contra a outra
ponta antes de gravar.
Cartão novo, defeito MEDIDO em 2026-08-05 durante a correção do {F7-04}. O documento servido pelo leitor não emite a declaração de viewport, então o navegador de celular assume uma largura fictícia de cerca de 980 pixels e todas as regras de estilo escritas para telas estreitas ficam adormecidas. Ou seja: o cuidado de desenho para celular existe no código e nunca chegou a nenhum cliente. Só apareceu porque uma verificação foi olhar a página com largura real de telefone; a suíte nunca pegaria, porque teste sem navegador não tem viewport.
RF-REN-006, RNF-USAB (leitura em celular). A página pública é o que o cliente da agência abre, e boa parte abre no telefone.
O que implementar: o documento passa a declarar o viewport como o padrão da web manda; e, com as regras de tela estreita finalmente ativas, revisar cada uma delas contra um telefone de verdade, porque nenhuma nunca foi exercitada de fato (uma regra escrita e nunca executada é uma hipótese, não um comportamento).
Como verificar (teste primeiro): abrir um relatório publicado num telefone de verdade, ou num navegador com largura de telefone e viewport respeitado, e confirmar que o desenho estreito acontece; conferir bloco a bloco que nenhum deles quebra, transborda na horizontal ou fica ilegível.
Aceite (H1): Dado um relatório publicado aberto em tela de celular, Quando a página carrega, Então o desenho para tela estreita é aplicado de fato, E nenhum bloco transborda na horizontal nem fica ilegível.
FECHADO em 2026-08-08, mergeado e deployado. Oito classes removidas, uma mantida com dono. A
prova é a inversa do normal: a página publicada não pode mudar, e o corpo do HTML servido saiu
byte a byte idêntico nos dois relatórios reais, com a única diferença ficando dentro do bloco de
estilo.
A medição respondeu as duas perguntas, não só a primeira. Além de "ninguém emite", cada classe
foi conferida contra o DRS para saber se era promessa de requisito pela metade (nesse caso o
defeito seria a peça faltando, não o estilo sobrando, e remover seria desfazer trabalho). Das
cinco nomeadas, todas eram código morto; a varredura achou mais três (do bloco de
confiabilidade, cujo emissor foi retirado de propósito e o estilo ficou esquecido) e **uma que não
era morta**, mantida com exceção que agora tem cartão próprio, o {F5-09}.
O artefato durável desta fatia não é a remoção, é a varredura, que reprova quando aparece
classe sem emissor. Ela nasceu com o mesmo defeito que esta série vem catando, dentro dela mesma:
não descia em subpastas, então um emissor futuro produziria um "sem órfãs" falso. Corrigido antes
do merge, com o caso que prova.
Achado que virou cartão próprio: uma das classes removidas vinha do mockup aprovado, que
desenha e-mail cru do visitante na página pública, contra requisito escrito. Ver {F5-08}.
Cartão novo, medido em 2026-08-08 durante a verificação do {F5-06}. O estilo do leitor tem
cinco classes que nenhum caminho do produto emite: a grade de criativos, e as quatro do rodapé
de assinatura (assinatura, dados da assinatura, marca d'água e nota privada). Só uma delas traz
aviso no código; as outras quatro parecem comportamento vigente para quem lê. Regra escrita e
nunca executada é hipótese, não comportamento, e ela custa caro de duas formas: dá impressão falsa
de cobertura a quem vai mexer no desenho, e some no meio das regras que valem de verdade.
RNF-MANUT-002, RNF-MANUT-003.
MEDIR ANTES DE REMOVER: para cada uma, confirme que nenhum caminho a emite hoje e decida se
ela era promessa de requisito que ficou pela metade. Se for promessa pendente, o defeito é a peça
faltando, não o estilo sobrando, e aí o cartão vira outro. Remover regra que alguém ia usar amanhã
é desfazer trabalho.
Como verificar (teste primeiro): varredura que confronte as classes declaradas no estilo com as
efetivamente emitidas por algum caminho, e que reprove quando aparecer classe nova sem emissor;
e a página publicada continuando idêntica, bloco a bloco, depois da remoção.
Aceite (H1): Dado o estilo do leitor, Quando a varredura roda, Então toda classe sem emissor é
listada, E as cinco já conhecidas aparecem nominalmente, E nenhuma foi removida sem antes se
confirmar que não era requisito pendente, E a página publicada não muda.
FECHADO em 2026-08-08, nas três cópias (a fonte no workspace e as duas servidas em produção),
com backup antes e o site conferido no ar servindo já o texto corrigido. Sem deploy: as cópias
servidas foram corrigidas direto, e a fonte junto, para o próximo deploy não trazer o defeito de
volta.
A decisão veio do próprio requisito, e é a parte que vale guardar: o RF-ANL-004 **não proíbe a
marca d'água**, proíbe o e-mail cru, e já declara o que deve aparecer no lugar (identificador
opaco: nome quando a pessoa está logada, "visitante" quando não). Ou seja, o mockup contradizia
uma spec que tinha a resposta escrita. Não houve escolha de produto a fazer nem escalada a fazer;
houve requisito a ler. Trocar o conteúdo foi o certo, remover o bloco teria descartado
comportamento que a spec pede.
A varredura das outras dezesseis telas achou e-mail em quatro, e nenhuma viola: as telas do
operador e da agência são autenticadas, não são HTML público; o campo do formulário é a própria
pessoa digitando o endereço dela; e a menção dentro de um comentário é conteúdo escrito por
usuário, não o sistema expondo terceiro. Fica registrado, de baixa prioridade e sem cartão, que
menção por e-mail dentro de comentário pode merecer tratamento um dia.
Cartão novo, confirmado em 2026-08-08 na verificação do {F5-07}. O mockup da página pública
ainda mostra "acessado por" seguido do endereço de e-mail do visitante, em duas cópias do arquivo.
RF-ANL-004 proíbe isso explicitamente no HTML público. O produto está certo (nunca foi
construído assim), mas o mockup é o guia visual de toda tela deste projeto: enquanto ele
desenhar aquilo, alguém vai reconstruir um dia, de boa-fé, achando que está seguindo o desenho
aprovado.
RF-ANL-004, RNF-LGPD-001.
O que implementar: corrigir o mockup nas duas cópias para não exibir identificação pessoal do
visitante, e conferir se outras telas do mockup trazem o mesmo tipo de promessa que a spec
nega. Achado assim raramente vem sozinho.
Como verificar (teste primeiro): varrer o mockup por endereço de e-mail e outros dados
pessoais desenhados, e confirmar que nenhuma tela promete o que um requisito proíbe.
Aceite (H1): Dado o conjunto de telas do mockup aprovado, Quando ele é percorrido, Então
nenhuma exibe dado pessoal de visitante que RF-ANL-004 proíba, E nenhuma promete comportamento que
um requisito do DRS negue.
Cartão novo, nascido de uma exceção nomeada em 2026-08-08. A limpeza do {F5-07} manteve, com
razão, a classe do desenho de iniciais do logo: ela não é código morto, é promessa adiada de
propósito desde o {F7-04}, porque o contrato de identidade não carrega o nome da agência para
derivar a inicial. Só que uma exceção sem cartão vira permissão permanente, e esta já sobreviveu a
duas fatias como comentário no código.
RF-ADM-004.
O que implementar: ou o contrato de identidade passa a carregar o nome da agência e o desenho
de iniciais entra como alternativa para quem não tem logo, ou se decide que ele não vai existir e
a classe sai junto com a decisão registrada. O que não pode é seguir como intenção sem dono.
Como verificar (teste primeiro): uma agência sem logo cadastrado abre a página pública e o
topo mostra o desenho de iniciais com a inicial certa; ou, na outra decisão, a varredura de estilo
sem emissor deixa de precisar de exceção.
Aceite (H1): Dado uma agência sem logo cadastrado, Quando a página pública é aberta, Então o
topo apresenta a alternativa decidida, E a varredura de estilo não carrega mais exceção sem dono.
**FECHADO em 2026-08-08 pela decisão DESCARTAR, medida contra o próprio RF-ADM-004, não escolhida
por gosto. O H1 do requisito (bloco 6, RF-ADM-004) exige exibir logo e cor da agência quando a
agência TEM identidade definida, e que nenhuma marca de outra agência apareça**, e não exige
nenhum desenho alternativo para quando a agência não tem logo. A própria nota de violação do
requisito é explícita: "viola-se este requisito se a página pública de um cliente da agência A
exibir marca de OUTRA agência ou marca genérica QUANDO A TEM IDENTIDADE DEFINIDA". Sem logo não é
esse caso. E a cláusula "nenhuma marca aparece" já foi VERIFICADA passando sem chip nenhum no
{F7-04}: "Sem logo, nada quebra; logo inválido é descartado", com layout idêntico a antes, sem buraco
e sem placeholder vazio, decisão deliberada registrada no próprio BLOCK_REGISTRY.hero. Busca no
DRS inteiro (RF, RN, Ap, jornadas) por "iniciais"/"chip"/"initial" não achou nenhuma outra menção,
a promessa nasceu só do comentário do código, nunca da spec.
O que saiu: a regra .r-hero-logo-chip de src/render-css.mjs (nunca teve emissor em nenhum
.mjs do pacote, em nenhuma das 3 superfícies que compartilham o mesmo render.mjs/
render-blocks.mjs), o comentário do BLOCK_REGISTRY.hero que prometia o fallback, e a entrada
r-hero-logo-chip do PENDING_REQUIREMENT_ALLOWLIST em test/render-css-coverage.test.mjs, porque a
allowlist volta a ficar vazia, mecanismo genérico preservado pra uma futura promessa real.
Prova de que a página não muda: HTML emitido por renderForReader/renderForPreview/
renderForPdf para o mesmo documento, com e sem logo, é byte-idêntico antes/depois fora do
<style> inline (só a regra CSS órfã sai do bloco de estilo; nunca apareceu como atributo
class= em nenhuma das duas versões). Suíte 1294/1299 verdes, as 3 falhas são de ambiente
(ERR_MODULE_NOT_FOUND em ingest-server/reader-server, pré-existentes, idênticas ao baseline
antes da mudança). Branch feat/f5-09-iniciais-logo (modularreports-dev, commit e4c5542),
pushada, sem merge ainda.
Definição de Pronto F5: leitor público no ar, isolado, rápido, com estados brandados e deploy próprio.
Fase F6, Role, entitlement e RBAC no Hub (Fatia 6)
Dependência: {F1b-01} (tabelas de dados já criadas). Provisiona o registro do módulo, o entitlement, o RBAC, a RLS e a auditoria que as fatias de chrome (F7, F8) e publicação (F9) vão usar. As tabelas de dados nasceram em F1b; aqui entram as camadas que dependem do catálogo do Hub.
ENTREGUE em 2026-07-29, com deploy e validação em produção. O dono decidiu fazer a integração agora ({Q7} do Questionário). Migration aditiva no ModularHub ampliou a lista de módulos permitidos de 11 para 12 valores, semeou o módulo reports nas 4 agências existentes sempre DESLIGADO, e o gatilho de agência nova passa a semear desligado também. O catálogo espelho em código do Hub recebeu o módulo e os 8 slugs de permissão reports.*. Achado que salvou o card: a rota que liga e desliga módulo faz atualização, não criação, então sem o preenchimento prévio o botão do super admin falharia em toda agência já existente. Provado em produção pelo caminho real: o módulo aparece na lista, liga e desliga por HTTP autenticado, e os 11 módulos antigos ficaram com exatamente os mesmos estados.
RF-ADM-001; artefato ART-MIG-01. Cunhar o module_key novo reports: migration que adiciona o literal reports à restrição CHECK da tabela de entitlements de módulo do Hub, mais a entrada reports (rótulo "Relatórios", não core) no catálogo de módulos em código. Não reusa o módulo results. As tabelas de dados já existem (F1b).
Como verificar (teste primeiro): após a migration, uma Company pode receber o entitlement reports; nenhuma Company existente recebe o módulo ligado automaticamente; o catálogo de módulos em código lista reports.
Aceite (H1): Dado o Hub sem o módulo reports, Quando a migration de registro roda, Então o literal reports está na CHECK de módulos válidos e no catálogo de módulos em código, E nenhuma Company recebe o entitlement ligado por default.
RF-ADM-001. Entitlement reports per-Company, desligado por default, ligado pelo Super Admin.
Como verificar (teste primeiro): criar uma Company nova e confirmar que nenhuma rota/menu do Reports aparece; ligar o entitlement pelo painel do Super Admin e confirmar que a entrada aparece sem novo deploy.
Aceite (H1): Dado uma Company recém-criada, Quando ninguém ligou o entitlement reports, Então nenhuma rota, menu ou atalho do ModularReports aparece para os usuários dessa Company, E ligar o entitlement no painel do Super Admin passa a exibir a entrada do Reports sem novo deploy.
RF-ADM-002; artefato ART-ACS-01. Sem papel novo; slugs de permissão reports.* registrados no catálogo de permissões em código do Hub (defaults por papel mais metadados de capability), não numa tabela nova; override por usuário reusa a concessão de capability existente.
Como verificar (teste primeiro): o catálogo em código lista todas as permissões do Reports com prefixo reports.; nenhum papel novo aparece na lista de papéis do sistema; nenhuma tabela nova de permissões foi criada.
Aceite (H1): Dado o catálogo de permissões do Hub, Quando o módulo reports é instalado, Então todas as permissões do Reports têm slug com prefixo reports. registrado no catálogo, E não existe nenhum papel novo criado exclusivamente para o Reports.
RNF-SEG-001 (caminho do operador). Todas as tabelas do schema com RLS RESTRICTIVE por company_id para os usuários de agência autenticados, REVOKE ALL, helpers SECURITY DEFINER com search_path fixado. Esta RESTRICTIVE por company_id é escopada às roles dos usuários de agência e NÃO afeta a policy PERMISSIVE do reports_reader criada em {F5-01} (o leitor público não tem company_id de sessão e é liberado por token, não por company): as duas convivem, cada uma na sua role.
Como verificar (teste primeiro): um usuário da agência A consulta as tabelas do Reports e só recebe linhas de A; tentar contornar por FK cross-schema falha; confirmar que habilitar a RESTRICTIVE do operador não zera a leitura do reports_reader de um relatório publicado (a PERMISSIVE do leitor continua valendo).
Aceite (H1): Dado um usuário da agência A, Quando ele consulta qualquer dado do Reports, Então só recebe registros com company_id de A, E nenhuma FK cross-schema contorna esse escopo, E a leitura do reports_reader de um relatório publicado segue funcionando após a RESTRICTIVE do operador entrar.
RF-ADM-012; artefato ART-AUD-01. Toda ação relevante grava evento no log de auditoria do Hub com event_type prefixado reports., por um ponto único (o helper de auditoria do Hub), gravando valor anterior e valor novo quando altera estado; append-only por RLS; o log de republicação (RF-PUB-010) deriva desses eventos.
Como verificar (teste primeiro): publicar, republicar, revogar e encerrar cliente geram evento no log com antes e depois quando aplicável; buscar no código confirma um único ponto de escrita de auditoria; nenhum evento contém dado pessoal cru.
Aceite (H1): Dado qualquer ação de publicar, republicar, revogar ou encerrar cliente, Quando ela é executada, Então um evento com event_type iniciado por reports. é gravado no log do Hub com valor anterior e novo quando aplicável, E não há caminho de escrita de auditoria fora do ponto único.
Definição de Pronto F6: schema provisionado, entitlement desligado por default, RBAC sem papel novo, RLS provado com dois tenants, auditoria por ponto único.
Fase F7, Chrome e shell do operador (Fatia 7)
Dependência: {F6-02} (entitlement), {F6-03} (RBAC). Grupo de rotas isolado que o operador usa.
RF-ADM-014, ADR-002. Menu e chrome próprios, não usa o menu lateral do Hub; entrada por item de menu do Hub, atalhos e endereço direto; mesma sessão.
Como verificar (teste primeiro): logado no Hub com o módulo ligado, clicar na entrada do Reports abre chrome próprio sem novo login; acessar o endereço direto também resolve na mesma sessão.
Aceite (H1): Dado um usuário logado no Hub com o módulo reports ligado, Quando ele clica na entrada do Reports, Então abre o chrome próprio do Reports (menu próprio, não o menu do Hub) na mesma sessão sem novo login, E o endereço direto do Reports também resolve na mesma sessão.
RNF-MANUT-002. Chrome do Reports carrega sob demanda; o white-label runtime do Hub não pinta o chrome do Reports.
Como verificar (teste primeiro): medir o bundle do Hub sem o Reports carregado; confirmar que o chrome do Reports só entra na rede ao navegar para ele.
Aceite (H1): Dado o bundle do Hub, Quando o usuário não navega para o Reports, Então o código do chrome do Reports não é carregado, E navegar para o Reports carrega o grupo de rotas sob demanda.
RF-ADM-003. Admin da agência gerencia (criar, editar, desativar) só os usuários da própria agência.
Como verificar (teste primeiro): um admin da agência A cria, edita e desativa usuários de A; tentar listar ou alterar usuário de outra agência é recusado.
Aceite (H1): Dado um admin da agência A, Quando ele gerencia usuários, Então consegue criar, editar e desativar usuários apenas da agência A, E não enxerga nem altera usuários de qualquer outra agência.
FECHADO em 2026-08-05, mergeado e DEPLOYADO em produção nas três superfícies (cabeçalho do módulo, leitor público e PDF). A cor da agência já passava nas duas pontas; o logo não aparecia em nenhuma, e essa era a metade que o cartão dava como pronta. Três causas, todas medidas antes de qualquer conserto: o cabeçalho do módulo não usava o componente de logo do Hub, apesar de o comentário do próprio arquivo alegar que reusava; o serviço do leitor entregava ao renderizador só a cor; e o retrato imutável da publicação nunca copiava o logo da agência, então o leitor não teria de onde tirá-lo mesmo se pedisse. Colateral corrigido junto: o renderizador nunca repassava a identidade a bloco nenhum, e a folha de estilo já previa o espaço do logo havia tempo, sem nunca ter sido usada. Verificado com olhos nas duas pontas, na mesma agência real: cabeçalho e gaveta mobile com a marca certa, e a página pública anônima em tela grande e estreita, publicada pelo caminho real de publicação e com o retrato escrito pela função de verdade. A cláusula "nenhuma outra marca aparece" passa. Sem logo, nada quebra; logo inválido é descartado. Retratos publicados antes da mudança seguem intocados, como manda a imutabilidade.
Dois defeitos que só apareceram porque alguém foi olhar, e que teriam passado por suíte verde: o logo sumia por completo na página pública abaixo de 1100px, escondido por uma regra antiga (reprovado pelo QA e corrigido: agora empilha abaixo do texto); e uma regra genérica de imagem vencia a do logo por especificidade, de modo que o teto de tamanho nunca funcionou, hoje o tamanho é medido na tela, não declarado no CSS. O PDF, que também nasceu sem o logo, entrou na mesma fatia (RF-REN-009, imagem embutida, sem busca de rede) e foi conferido página a página, com e sem logo.
Não exercitado, e nomeado: o teto de tamanho menor abaixo de 600px não pôde ser disparado porque o documento do leitor não declara viewport, o que mantém adormecidas todas as regras de tela estreita. Isso é o cartão {F5-06}, não desta fatia.
Contexto da medição anterior. O motivo antigo (a página pública só nasceria na Fatia 9) caiu: a Fatia 9 entregou e um relatório real foi publicado e aberto sem sessão. Com as duas pontas finalmente mensuráveis na MESMA agência (uma agência real, com logo e cor já cadastrados), o resultado é: a cor da agência passa nas duas pontas; o logo não aparece em nenhuma. No chrome do operador, o cabeçalho do próprio módulo mostra um ícone genérico e o nome do produto, enquanto o painel principal do Hub troca o ícone corretamente na mesma sessão e com a mesma empresa ativa; o dado do logo chega certo pela sessão, então é fiação faltando no cabeçalho do módulo, não dado ausente. Na página pública, o documento servido não tem imagem nenhuma, nem o desenho de iniciais que a folha de estilo já prevê: o serviço do leitor entrega ao renderizador só a cor da identidade, nunca o logo. A cláusula "nenhuma outra marca aparece" passa nas duas pontas. Fecha quando o logo da agência aparecer no cabeçalho do módulo e na página pública, com as mesmas duas pontas conferidas por olhos na mesma agência.
RF-ADM-004. Logo e cor da ficha da Company brandizam chrome do operador e link público.
Como verificar (teste primeiro): agência A com logo e cor definidos; abrir o chrome e publicar um link mostram a marca de A; nenhuma outra marca aparece.
Aceite (H1): Dado a agência A com logo e cor definidos na ficha da Company, Quando um operador de A usa o Reports e publica um link, Então o chrome do operador e a página pública exibem o logo e a cor de A, E nenhuma marca de outra agência aparece.
Definição de Pronto F7: chrome isolado no ar, lazy, com self-service de agência e identidade da agência aplicada.
Fase F8, Wizard e compositor (Fatia 8)
Dependência: {F1} (pipeline IA), {F2} (contrato de bloco), {F3} (package renderizador, o preview do compositor usa as mesmas funções puras), {F6}/{F7} (chrome, RBAC). Onde o operador compõe e cura.
RF-CMP-001. Coleta cliente, período e tipo (template); não avança sem os dois primeiros.
Como verificar (teste primeiro): selecionar cliente, período e tipo e avançar cria o rascunho vinculado; tentar avançar sem cliente ou período é bloqueado.
Aceite (H1): Dado o wizard aberto, Quando o operador seleciona cliente, período e tipo e avança, Então o rascunho é criado vinculado a esse cliente e período com o template escolhido, E não é possível avançar sem cliente e período preenchidos.
RF-CMP-002, Ap.1. Coleta KPI principal, a direção (objeto do Ap.1: objetivo_campanha no enum fechado, objetivo/tese, público, decisão alvo, tom, enfatizar, evitar, blocos preferidos, lacunas conhecidas), destaques, identidade; KPI, objetivo_campanha, tese e decisão alvo obrigatórios.
Como verificar (teste primeiro): preencher os campos e avançar grava a direção no rascunho; tentar avançar sem KPI principal, objetivo_campanha, tese ou decisão alvo é bloqueado; um campo da direção não grava nem altera nenhum dp: em data_points; a direção não introduz um tipo de bloco fora do Ap.3.
Aceite (H1): Dado o passo de briefing, Quando o operador preenche KPI principal, a direção (com o objetivo da campanha no enum, a tese e a decisão alvo) e a identidade e avança, Então o objeto de direção fica gravado no rascunho e disponível ao pipeline como instrução tipada, E o KPI principal, o objetivo_campanha, o objetivo (tese) e a decisão alvo são obrigatórios para avançar, E nenhum campo da direção grava ou altera um dp:.
RF-CMP-017. Monta e mostra ao operador o prompt de instrução que será enviado à IA (a direção compilada em texto), com o catálogo de pontos de dado somente leitura como contexto (referenciável só por metric_id, nunca digitável nem colável); o operador revisa, edita e adiciona detalhe, e só confirma quando pronto. Dependência: {F8-02}.
Como verificar (teste primeiro): preencher a direção e chegar ao prompt mostra o texto de instrução montado e editável e o catálogo de pontos de dado em modo somente leitura; tentar digitar ou colar no catálogo é bloqueado; editar o prompt não cria nem altera nenhum dp: em data_points; tentar compor sem confirmar o prompt é bloqueado.
Aceite (H1): Dado a direção pronta, Quando o operador chega ao prompt, Então o sistema mostra o prompt de instrução montado e editável e o catálogo de pontos de dado em modo somente leitura, E a composição só dispara após o operador confirmar, E nenhuma edição do prompt cria um dp: nem torna o catálogo de pontos de dado editável.
RF-CMP-019. Uma chamada única à cadeia gratuita (RF-CMP-011) transforma nota solta do operador mais o catálogo de dp:/blocos disponíveis num objeto de direção editável; opcional por uso (a direção manual basta), disponível desde a Fatia 1, fora do teto de 60s. Dependência: {F8-02}, {F1-07} (gateway de IA reusado).
Como verificar (teste primeiro): acionar o compilador com uma nota solta devolve um objeto de direção (nunca narrativa nem número) editável pelo operador; pular o compilador e preencher a direção manualmente em {F8-02} basta para avançar.
Aceite (H1): Dado uma nota solta do operador e o catálogo de dp: e blocos, Quando o operador aciona o compilador, Então uma chamada à cadeia gratuita devolve um objeto de direção editável (nunca narrativa nem número), E o operador revisa e confirma antes de compor, E o passo é opcional (a direção manual do {F8-02} basta sem ele).
RF-CMP-003. Preview do que a IA entendeu mais "o que não achei" em destaque.
Como verificar (teste primeiro): artefato com um dado esperado pela intenção mas ausente faz esse dado aparecer na lista "o que não achei", não some.
Aceite (H1): Dado artefatos processados no passo de dados, Quando a leitura da IA termina, Então o operador vê um preview do que a IA entendeu e uma lista explícita "o que não achei", E um dado esperado pela intenção mas ausente nos artefatos aparece nessa lista, não some em silêncio.
RF-CMP-004. Composição proposta bloco a bloco com origem de cada número; confirmar abre no compositor.
Como verificar (teste primeiro): cada bloco proposto exibe a origem de seus números; confirmar abre o rascunho no compositor com esses blocos.
Aceite (H1): Dado a leitura concluída, Quando o wizard propõe a composição, Então cada bloco proposto exibe a origem de cada número (artefato e célula ou campo), E ao confirmar, o rascunho abre no compositor com esses blocos.
RF-CUR-001. Editar texto, reordenar e esconder blocos; bloco escondido não vai ao snapshot.
Como verificar (teste primeiro): editar, reordenar e esconder um bloco persiste; o bloco escondido não aparece no preview nem no snapshot publicado depois.
Aceite (H1): Dado um rascunho aberto no compositor, Quando o operador edita o texto de um bloco, reordena e esconde outro, Então as mudanças persistem no rascunho, E um bloco escondido não aparece no preview nem irá para o snapshot.
RF-CUR-002. Regenerar um bloco com instrução nova mantém proveniência e não altera os demais.
Como verificar (teste primeiro): pedir regenerar um bloco com instrução ("foco no público 25 a 34") recompõe só aquele bloco mantendo dp: dos números; os demais blocos permanecem intactos.
Aceite (H1): Dado um bloco no compositor, Quando o operador pede regenerar com uma instrução, Então só aquele bloco é recomposto pela IA mantendo a proveniência dos números, E os demais blocos permanecem intactos. Regenerar um bloco é resposta imediata, síncrona (DRS J.4).
FECHADO em 2026-08-08, mergeado e deployado, provado em produção: criativo inserido pelo
compositor, relatório publicado, e a imagem aparecendo no link público real que o cliente abre.
Link revogado e tudo limpo depois.
A fatia não tinha um defeito, tinha cinco, e nenhum era dela. Ela foi a primeira coisa a gerar
um criativo em produção, e por isso exercitou pela primeira vez tudo que dependia disso: a ação não
existia em camada nenhuma (o cartão dizia que faltava só exercitar); a rota do preview **não tinha
como servir a imagem**, o que faria a segunda metade do aceite falhar mesmo com rota e botão
prontos; a guarda que protege a referência do arquivo estava escrita, exportada e **ausente da
lista de gates; a gravação da composição lia o rascunho, chamava a IA e gravava sem reler nem
travar**, perdendo em silêncio o bloco que o operador acabara de inserir; e o segredo que liga o
proxy de imagem do leitor nunca tinha sido provisionado desde a fatia que o criou. Todos
invisíveis porque nunca houve um criativo em produção.
Duas regras nasceram no DRS antes do código, porque a fatia as exigia: o bloco inserido pelo
operador sobrevive a qualquer reescrita do conjunto (gravar composição deixou de ser "substitua
tudo" e virou junção com trava), e a legenda entra na disciplina de número, com o perímetro
derivado da ficha de campos editáveis e não de lista à mão, foi lista à mão que deixou a
legenda passar até aqui.
E um bloqueio de entrega que três frentes não viram: a branch não buildava. A checagem de
tipos passava, a construção da imagem de produção falhava, e a diferença entre as duas só apareceu
quando alguém precisou do artefato de verdade. Ver a lição registrada em memória.
Deixado fora de propósito, com cartão próprio: imagem vetorial é recusada pela rota nova
({F8-19}) e a eliminação de dados pessoais ainda não alcança arquivo ({F8-20}). Os dois tinham
superfície zero até esta fatia, e é ela que acaba com o zero.
RF-CUR-003. Inserir imagem de anúncio como bloco de mídia, referenciando o asset por asset_id+sha256.
O MOTIVO ANTERIOR ESTAVA ERRADO, medido em 2026-08-08. O cartão dizia que faltava só alguém
exercitar o aceite. Não falta: não existe onde clicar. Uma verificação percorreu o compositor
em produção, inventariou todos os controles da tela e não achou nenhum de inserir mídia; varreu o
código do painel e não achou nenhuma chamada de envio de arquivo fora da ingestão de artefatos; e
varreu as rotas e descobriu que não existe nenhuma que CRIE bloco. As rotas de bloco todas
operam sobre um bloco que já precisa existir, e quem cria bloco é só a composição de IA.
A rota de envio do arquivo existe e funciona (provada ao vivo: devolve identificador e resumo
criptográfico corretos, o resumo confere, e nunca devolve endereço nem a imagem embutida). O que
não existe é o caminho entre ela e o rascunho. **É a sexta ocorrência da família "peça pronta,
nunca ligada" neste projeto**, e a mais nítida: o único lugar do repositório que aciona o
armazenamento é um teste, chamando a função direto, sem passar por rota nem por tela.
O que falta construir: uma rota que crie o bloco de mídia a partir do identificador e do
resumo, e o controle no compositor que envia o arquivo e chama essa rota. Atenção ao que isso
significa no desenho: hoje todo bloco nasce da composição de IA, com proveniência. Um bloco
inserido à mão é o primeiro que nasce sem esse caminho, e precisa passar pelos mesmos gates de
publicação que os demais.
Nota de 2026-08-02, CONFIRMADA em 2026-08-05 após uma volta em falso. O bloqueio de storage caiu de verdade com a {F15-02}: uma medição no meio do caminho desconfiou disso por ter consultado o serviço de armazenamento errado (a plataforma-mãe nunca usou aquele serviço, de nenhum módulo), e uma segunda medição no mecanismo real (sistema de arquivos sobre volume dedicado, já com arquivo real do módulo gravado nele) confirmou que o storage funciona. O que essa nota concluía sobre o motivo do cartão ("segue aberto por UM motivo só: ninguém exercitou o H1") está morto desde 2026-08-08, pela medição do parágrafo de cima: não é falta de exercício, é falta de construção. A parte da nota que continua de pé é só a do storage.
O DRS ganhou duas regras por causa deste cartão, publicadas em 2026-08-08 ANTES de qualquer código (Regra Zero). O planejamento desta fatia esbarrou em duas perguntas que o documento não respondia, e nenhuma das duas era detalhe de implementação. RF-CUR-010: hoje todo bloco nasce da composição e o caminho que grava a composição sobrescreve o conjunto inteiro; a partir do momento em que existe bloco inserido à mão, é preciso dizer o que acontece com ele quando o conjunto é gravado de novo. A resposta escrita é que o bloco do operador SOBREVIVE, com o preço declarado no próprio requisito (a gravação deixa de ser "substitua tudo" e vira junção) e com ordem e colisão já resolvidas ali, para não virarem decisão na hora. RF-CUR-011: a definição de "número exibido" do Ap.8 nomeava só o body_md, então uma legenda de criativo com número dentro publicaria sem origem sem violar requisito nenhum; o perímetro passou a ser derivado da ficha do Ap.3 (todo campo de texto editável pelo operador) em vez de uma lista mantida à mão. Atenção a uma armadilha do requisito novo: o critério de métrica é o MESMO do body_md, então "Criativo 3" (ordinal) NÃO reprova; endurecer só a legenda é violar RF-CUR-011, não cumpri-la. Provas em {TA-123} e {TA-124}.
Como verificar (teste primeiro): inserir um criativo adiciona um bloco de mídia ao rascunho e aparece no preview; o payload não guarda URL crua nem base64. As três cláusulas irmãs, que este cartão fecha junto e não depois: o payload referencia por identificador e hash ({TA-23}, reaberto em 2026-08-08 porque estava fechado sobre um gesto que não existe), o bloco do operador sobrevive a uma segunda gravação do conjunto ({TA-123}) e a legenda não publica métrica sem origem ({TA-124}).
Aceite (H1): Dado o compositor, Quando o operador insere um criativo, Então um bloco de mídia referenciando o asset por identificador e hash é adicionado ao rascunho, E o criativo aparece no preview.
FECHADO POR INTEIRO em 2026-08-10. A Fatia B (logo do cliente) foi mergeada nos dois repos
(5a89341 no pacote, ab86f02 no Hub) e deployada nas três superfícies: front, API e leitor. QA
PASS e review APPROVED.
A prova que fecha o cartão, e ela é a mais forte do dia: o QA alterou o rascunho **direto no
banco, contornando até a guarda da própria aplicação, e a página publicada continuou servindo o
logo antigo**. O retrato congelado é congelado de verdade, não por convenção de código. Junto disso:
duas marcas na página e exatamente duas imagens, sem sobreposição de caixas; proporção conferida
contra o arquivo (300 por 100 renderizando 3 para 1 em tela grande e em celular); retrato publicado
em 30/07, sem chave nenhuma de identidade, abrindo em 200; e o fluxo humano inteiro exercitado com
arquivo real, da prévia à publicação.
O que a Fatia B fechou além do próprio escopo: a rota do logo entrou sem a rejeição de
imagem vetorial que a rota de bloco de mídia aplica de propósito, não por decisão, mas porque a
guarda vive em quem chama e não havia como lembrar. Corrigido por reuso da mesma constante, com o
par de casos provado por HTTP real nas duas rotas. O review confirmou que **o terceiro caminho ainda
nasceria furado**, o que é a justificativa medida do {F8-19}.
Verificação de integração feita antes de publicar, e ela não era óbvia: a branch estava cinco
commits atrás da main, porque o {F8-25} mergeou no meio, e os dois tocaram o mesmo arquivo de rotas.
Depois do merge, a guarda de corrida do {F8-25} foi rodada contra a main mesclada: 13 sítios,
zero violações. Sem isso, uma escrita nova entrando sem trava passaria com os dois gates verdes,
porque cada um viu só o seu lado.
**METADE ENTREGUE em 2026-08-09 (Fatia A: cor e título), MERGEADA e DEPLOYADA. A metade do LOGO
dependia de {F8-21}**, hoje fechado. O cartão fechou PARCIAL de propósito, com a cláusula que
falta nomeada, porque placar menor e verdadeiro vale mais que placar redondo.
O defeito central era anterior ao controle novo, e é o que a fatia de fato consertou: o preview
não recebia identidade nenhuma e desenhava com um valor fixo. O operador curava vendo uma cor e
o cliente recebia outra. Depois do deploy, o preview passou a mostrar a cor real da agência **mesmo
sem nenhuma sobreposição configurada**.
Três defeitos foram revelados e consertados no caminho, nenhum previsto no cartão: uma segunda
consulta de leitura sem a coluna nova fazia a tela esquecer o que o banco tinha gravado; a
restrição do banco tinha buraco de lógica de três valores igual ao da validação no código, ou
seja, duas camadas que falham do mesmo jeito não são duas camadas; e a cor não aparecia no
compositor porque a montagem do preview descartava o invólucro que carrega a variável, embora a
página pública usasse o mesmo mecanismo com ele presente.
A ORDEM FOI INVERTIDA nesta fatia, e fica registrado. O plano desta fatia declarava, como passo
1, publicar no DRS e no Inventário as regras que ela exigia antes de qualquer código. A
construção foi despachada sem esse passo: o código ficou pronto e em gates, e o documento não sabia
dele. A revisão de código mediu a divergência e apontou. **A obra precedeu o documento aqui, e a
reconciliação veio depois, em 2026-08-09** (RF-CUR-004 endurecido, RF-CUR-012, RF-CUR-013, RN-020,
a coluna nova no Inventário e este cartão). Nada foi revertido; o que se corrigiu foi a ordem, e a
lição é que despachar construção sem despachar o passo zero deixa o bloqueio vivendo só na cabeça
de quem planejou.
O que a Fatia A entregou: sobreposição de identidade por relatório com conjunto de chaves
fechado em duas e restrição própria no banco (nunca misturada na do ajuste fino, RF-CUR-008
intocado); a precedência extraída para um ponto único chamado tanto pela publicação quanto pelo
preview; limpar a sobreposição remove a chave e nunca grava vazio; a seção de identidade na
coluna direita do compositor, com atalho de cores lido do catálogo e entrada livre de cor, nunca
lista fixa na tela; e o título do relatório editável em linha, que era peça pronta e
inalcançável fora do assistente.
O defeito maior que ela consertou não era o pedido do cartão: o preview **nunca resolvia
identidade nenhuma** e mostrava a cor padrão do renderizador, ou seja, o operador curava numa cor e
só descobria a cor real do cliente depois de publicar. Isso valia para todo relatório, com zero
sobreposição.
O que ela NÃO entregou, e por quê: o logo do cliente. Existe um portador de marca na página
publicada e ele carrega a marca da agência; pôr o logo do cliente ali apagaria a marca da agência,
que é o que {F7-04} construiu de propósito e o que RF-ADM-004 exige. O portador do cliente e o da
agência são separados desde o protótipo aprovado, e o da agência nunca foi construído: é
{F8-21}. Enquanto ele não existir, a Fatia B não começa.
RF-CUR-004, RF-CUR-012, RF-CUR-013, RN-020. Sobreposição por relatório, campo a campo, resolvida
por ponto único, congelada no retrato e rerresolvida na republicação.
Como verificar (teste primeiro): definir a cor pelo atalho e pela entrada livre reflete no
preview na mesma interação; limpar devolve a cor da ficha do cliente; publicar copia a cor para o
retrato e marca a procedência; editar a sobreposição depois de publicado não muda a página
publicada, e republicar muda mantendo o mesmo link; renomear o relatório pelo compositor persiste e
é recusado depois de publicado; preview e publicação devolvem a MESMA cor para o mesmo estado. Com
a Fatia B: a página publicada exibe o logo do cliente no portador do cliente e a marca da
agência no portador da agência, as duas ao mesmo tempo.
Aceite (H1): Dado um rascunho no compositor, Quando o operador define a cor de acento do
relatório, Então o preview passa a exibir essa cor na mesma interação e sem publicar, E publicar
copia essa cor para o snapshot, E limpar a sobreposição remove a chave em vez de gravar vazio, E
definir ou limpar um dos dois campos não altera o outro, E nenhuma chave fora do par fechado é
aceita em camada nenhuma, E quando o operador sobrepõe o logo do cliente a página publicada exibe
esse logo no portador do cliente e continua exibindo a marca da agência no portador da agência.
RF-CUR-005. Cada tipo de bloco declara no schema os campos editáveis; compositor só expõe esses.
Como verificar (teste primeiro): abrir a edição de um bloco só mostra os campos da whitelist; um valor numérico com proveniência não aparece como editável.
Aceite (H1): Dado um tipo de bloco com whitelist de campos editáveis, Quando o operador abre a edição desse bloco, Então só os campos da whitelist são editáveis, E campos fora da whitelist (por exemplo, o valor numérico com proveniência) não são editáveis à mão.
RF-CUR-006. Modo apresentação (tela cheia, navegação por passos), rótulo "rascunho", sem pins de comentário.
Como verificar (teste primeiro): entrar no modo apresentar abre tela cheia com o rótulo "rascunho"; sair volta ao compositor sem ter publicado nada.
Aceite (H1): Dado um rascunho no compositor, Quando o operador entra no modo apresentar, Então a apresentação abre em tela cheia com o rótulo "rascunho" e sem os pins de comentário, E sair do modo apresentar volta ao compositor sem ter publicado nada.
FECHADO em 2026-08-08 POR VERIFICAÇÃO, sem código novo. O comportamento já existia; o que
faltava era a prova da metade que o requisito realmente protege. Publicar por sessão humana já
tinha evidência de sobra nas fatias anteriores, e ninguém nunca tinha exercitado a recusa.
A metade decisiva, provada no caminho real: a identidade que o **próprio sistema usa
internamente** foi testada em três formas diferentes, e em nenhuma a publicação concluiu, com o
banco confirmando zero linha tocada. Não foi leitura de código: foi chamada de verdade contra
produção.
Um terceiro caso, que o cartão não pedia e mudou a leitura: por padrão os papéis operacionais
já vêm com a permissão de publicar, então "humano sem a permissão" só existe por revogação
explícita. A verificação criou um usuário real, revogou a permissão pelo mecanismo de administração
de verdade, teve a publicação recusada, e ainda conferiu que a sessão continuava funcionando, para
não confundir recusa de permissão com sessão quebrada. Usuário removido no fim.
Aspereza registrada, sem reprovar: um ator de serviço com identidade vazia derruba a consulta
de permissões com erro genérico em vez de uma recusa limpa. Nada publica, então não é furo; é
tratamento de erro a melhorar.
RF-CUR-007. Publicar exige sessão humana com a capability reports.publish; ator de serviço é recusado (reforça {F1-06}).
Como verificar (teste primeiro): tentar publicar via chamada de API com um ator sem reports.publish é recusado; publicar via sessão humana com reports.publish conclui.
Aceite (H1): Dado um rascunho pronto, Quando a publicação é acionada, Então ela só conclui a partir de uma sessão humana com a capability reports.publish, E qualquer tentativa de publicação por ator de serviço é recusada.
FECHADO em 2026-08-08 POR VERIFICAÇÃO, sem código novo. A composição responde na mesma chamada,
medido em produção: uma requisição, resultado completo no corpo, e o rascunho gravado dentro da
própria chamada. A tabela da fila assíncrona existe e está vazia, o que confirma o requisito
pelo lado de fora: ela segue reservada para evolução, não em uso.
A metade que costuma faltar foi provada de verdade, e não por leitura: um container descartável
com as chaves de IA quebradas fez os quatro provedores falharem de fato, e o operador recebeu
mensagem em linguagem humana, sem jargão, sem código de erro cru, dizendo que os números continuam
válidos e o que fazer em seguida. Falha explícita, nunca silêncio.
O que NÃO foi provado, e fica nomeado: o pixel do indicador de espera na tela. O caminho está
confirmado no código, e a caixa de falha usa exatamente os campos que a prova acima exercitou, mas
o ambiente de automação não conseguiu alcançar o último passo do assistente depois de oito
tentativas por caminhos independentes. É limitação de ferramenta, declarada em vez de maquiada.
RNF-PERF-002. A composição é síncrona in-process (10 a 30 s); o wizard mostra o wait com fail-loud enquanto processa. Uma fila assíncrona com estado persistido (report_job) é evolução, não V1.
Como verificar (teste primeiro): disparar uma composição mostra o wait no wizard e conclui de forma síncrona; uma falha da composição aparece explícita (fail-loud), não em silêncio.
Aceite (H1): Dado uma composição disparada no wizard, Quando ela roda, Então o wizard mostra o wait e devolve o resultado de forma síncrona, E uma falha aparece explícita ao operador. A fila assíncrona (report_job) fica reservada para evolução (DRS RNF-PERF-002).
RF-CUR-008. Dependência: {F8-01} (compositor). O operador ajusta os 4 steppers de token (paddingStep, gapStep, fontSizeStep, buttonAccentStep) por bloco (style_overrides) e por relatório (theme_overrides), sempre por índice de escala fechada com clamp aos limites; nunca CSS, HTML ou valor livre; o título nunca desce abaixo do corpo na escala de fonte; o acento nunca passa de hex, só degrau da rampa já validada AA.
Como verificar (teste primeiro): ajustar um dos 4 steppers muda o valor em passos dentro da escala com clamp; enviar uma chave fora do conjunto fechado é rejeitado pelo schema (additionalProperties:false); forçar buttonAccentStep num degrau abaixo de AA reprova o publish ou é ajustado por clamp de contraste; forçar um override que estoura o layout no render real mobile ou PDF reprova o publish.
Aceite (H1): Dado um bloco ou o relatório, Quando o operador ajusta uma das 4 propriedades, Então o valor muda em passos dentro da escala de tokens com clamp (nunca fora), E o publish é recusado se o buttonAccentStep resultar em contraste abaixo de AA ou se o override estourar o layout no render real mobile ou PDF, E nenhuma chave fora do conjunto fechado é aceita.
RF-CMP-020. Dependência: {F1-07}/{F1-08} (gateway de IA, cadeia gratuita). Decisão do dono ({Q5}, 2026-07-29): operação só com IA gratuita, mas com comunicação explícita ao operador e escalonamento ao admin. Quando composeWithFreeChain esgota a cadeia inteira (sinaliza explicitamente que todos os provedores falharam), nenhuma narrativa é entregue, o operador vê mensagem em linguagem humana (sem jargão nem código de erro cru) dizendo que os números continuam válidos e que dá para escrever à mão ou tentar depois, e um payload pronto para a Central de Notificações do Hub (messages.publish_notification) carrega o relatório, o horário e o motivo real por provedor. Entregue nesta fatia: a base determinística (o gateway captura o motivo real por tentativa em vez de descartá-lo; a camada de falha de composição classifica, monta a mensagem humana e o payload do admin, e dispara notifyComposeFailureAdmins contra o schema real; composeReport expõe tudo isso em narrativeFailure). Fora do escopo desta fatia (fica para quando o RBAC do Hub estiver montado, {F6}/{F7}): a tela do botão de um clique e a resolução de QUEM é o admin da agência (RF-ADM-002/RF-ADM-003); o ponto de ligação já está pronto e documentado no próprio módulo de falha de composição.
Como verificar (teste primeiro): simular a queda dos 4 provedores gratuitos confirma que nenhuma narrativa sai (narrativeText: null, aiOk: false) e que dataPoints/gaps continuam intactos; a mensagem ao operador não contém nenhum dos termos técnicos (código de falha interno, status HTTP cru, nome de variável de ambiente); o payload do admin contém o reportId, o horário e o motivo real de cada um dos 4 provedores; notifyComposeFailureAdmins contra o schema real do Hub (teste de integração, com credenciais lidas do ambiente e nunca persistidas) grava uma linha em messages.notifications por admin alvo, lida de volta com type='system_alert', origin_module='reports' e o link relativo certo.
Aceite (H1): Dado que a cadeia gratuita de IA falhou por completo durante uma composição, Quando o resultado chega ao operador, Então nenhuma narrativa é entregue (nem local nem de stub), E o operador vê uma mensagem em linguagem humana explicando o que aconteceu e o que fazer, sem jargão técnico nem código de erro cru, E os números do relatório continuam íntegros e disponíveis, E uma ação de um clique dispara um aviso ao admin da agência pela Central de Notificações do Hub com o relatório, o horário e os provedores que falharam e o motivo de cada um.
FECHADO em 2026-08-09, mergeado e deployado, provado em produção. O conserto é de uma linha: a
gravação parcial deixou de substituir a coluna inteira e passou a mesclar por chave.
A medição do conjunto mudou a leitura do defeito: cinco pontos gravam aquela coluna, e
apenas um estava quebrado. Os outros quatro já mesclavam corretamente. O defeito era o ímpar,
não a regra, e é por isso que ele sobreviveu tanto tempo: pela ordem que o assistente usa, nunca se
manifestava.
A pergunta que virou resposta, e não ficou como suposição: cruzando o registro de auditoria
contra o estado atual, dois rascunhos de fato perderam dado. Os dois eram fixtures de teste já
revogadas, e um terceiro candidato foi investigado e descartado por estar íntegro. **Nenhum
rascunho de cliente real foi atingido**, e isso é afirmação medida duas vezes, por frentes
diferentes, não estimativa.
A prova do aceite foi o gesto, não o teste: a verificação percorreu o assistente de verdade,
avançou uma etapa, disparou a pesquisa de mercado, voltou pela etapa anterior e salvou de novo,
que é exatamente o movimento que dispara o defeito. Sem o conserto, sete testes reprovam.
Limitação registrada, sem promessa falsa: só uma das três partes tem caminho de remoção
nominal. As outras duas se sobrescrevem, mas não se apagam. Nenhum requisito pede isso hoje, e o
comentário do código merece redação mais estreita para não sugerir cobertura que não existe.
Achado fora do diff, que virou o cartão {F8-25}: a mesma corrida de ler, chamar a rede e gravar
cego existe em outras duas rotas, ainda sem conserto. É a terceira ocorrência do padrão.
Cartão novo, defeito MEDIDO em 2026-08-05 durante a verificação da {F4-05}. A rota que salva o briefing do rascunho substitui a coluna inteira em vez de mesclar, então chamá-la depois da leitura dos artefatos, da pesquisa de mercado ou do prompt apaga silenciosamente o que aquelas etapas já tinham gravado. O defeito só apareceu porque uma verificação ponta a ponta chamou os passos fora da ordem feliz e perdeu o trabalho duas vezes; pela ordem que o assistente usa hoje, ele não se manifesta, o que é exatamente o que o torna perigoso (a próxima tela, o próximo salvamento automático ou o retorno a um passo anterior o acordam). É a assinatura de regressão silenciosa da casa: quem grava não declara o que remove.
RF-CMP-002, RF-ING-005, RF-ING-006.
O que implementar: a gravação parcial do briefing passa a mesclar por chave, preservando as chaves que não vieram na requisição; e quem precisar de fato apagar uma parte apaga aquela parte nominalmente, nunca por substituição total implícita.
Como verificar (teste primeiro): gravar a leitura dos artefatos, depois a pesquisa de mercado, depois o prompt, em qualquer ordem e em chamadas separadas, e confirmar que as três continuam no rascunho ao reler do banco; e confirmar que apagar uma parte nominalmente não toca as outras.
Aceite (H1): Dado um rascunho com leitura de artefatos, pesquisa de mercado e prompt já gravados, Quando qualquer uma dessas partes é salva de novo isoladamente, Então as outras permanecem íntegras no rascunho, E nenhuma chave do briefing é removida sem ter sido nomeada na requisição.
FECHADO em 2026-08-18, conferido NA TELA - o card nasceu com olhos e foi verificado com olhos.
As 10 faixas da tela de branding foram lidas em produção: nenhuma carrega número em ordem de byte
cru. A quota passou a dizer "no máximo 200 artefatos, 256 MB no total e 5000 páginas de PDF".
Corrigido na FONTE, não na tela: o texto sai do catálogo (que é o que a tela exibe) e é
derivado do próprio teto, então mudar o teto muda o texto. O valor da chave continua em bytes
- é o contrato da API, e há teste provando que não mudou.
A guarda por construção achou o irmão do defeito no mesmo commit: a faixa do limite de
requisições do leitor dizia "entre 1000 e 3600000 ms" (1 segundo e 1 hora, que ninguém lê assim).
Corrigida junto. A guarda percorre o catálogo INTEIRO procurando número de 7+ dígitos em qualquer
faixa - parâmetro novo com unidade ilegível nasce reprovado, sem ninguém lembrar de vir aqui.
Suíte 1631 verde; merge em main, deployado.
Cartão novo, visto na tela durante o deploy do {F8-18} em 2026-08-13, com olhos, não em teste.
O que acontece: a faixa recém-criada exibe o número em bytes (nove dígitos) imediatamente
acima de um campo cuja unidade declarada é megabyte. Quem opera lê dois números que parecem
incomparáveis e precisa fazer a conta de cabeça para saber se o valor que quer cabe.
Por que não foi consertado no deploy, e a decisão está certa: não era escopo daquela fatia, e
mexer em tela durante deploy de infraestrutura é como se estraga deploy. Virou cartão porque
cobrança sem dono some na próxima leitura. Foi assim que o {F8-50} nasceu.
É pequeno e é real: o {F8-18} existiu para que o limite parasse de ser peça que só aparece na
recusa. Um número que o operador não consegue comparar com o campo ao lado ainda é isso.
Como verificar (teste primeiro): a faixa é exibida na mesma unidade do campo que ela limita.
Aceite (H1): Dado um parâmetro com faixa, Quando o operador olha a tela, Então faixa e campo
falam a mesma unidade, E ele não precisa converter nada para saber se o valor cabe.
FECHADO em 2026-08-13, mergeado e em produção: pacote 1f044c1 antes do Hub 2aa4561, nessa
ordem por exigência do gate: o Hub lê o catálogo em tempo de execução por montagem do outro repo, e
a ordem inversa faria a tela dizer que os parâmetros não são graváveis **enquanto as escritas
funcionavam**.
O cartão errou num dos três fatos que afirmava, e o erro foi para melhor. Ele supunha que o corpo
era lido inteiro antes da decisão. Não era: a recusa já acontecia antes. O defeito real era
outro e mais perigoso: a guarda falhava aberta. Cabeçalho de tamanho ausente virava zero e
aprovava; ilegível virava valor não-numérico e, pela forma da condição, **pulava a verificação
inteira. A validação mediu que sete** formas de valor passavam pela guarda antiga e são recusadas
pela nova.
A faixa é por construção, e isso foi testado, não afirmado: a validação acrescentou uma entrada
nova ao catálogo real sem teto e a escrita foi recusada. Parâmetro novo nasce recusado, não
ignorado. A lista curada não teve como nascer aqui.
O limite de memória virou real neste deploy, que é por que o cartão não podia fechar antes: o
processo que atende todas as agências rodava sem teto nenhum e agora tem 2 GB. A validação rodou
o controle que a obra não tinha rodado (os mesmos 3 GB sem o limite passam), provando que quem
mata é a flag, e não o acaso.
Provado em produção, com olhos e com número: 10 de 10 entradas exibem a faixa na tela; escrita
acima do teto recusada nomeando o limite; escrita dentro dele aceita; e o valor de produção
restaurado byte a byte ao retrato inicial.
O que continua estimado, e está declarado no doc de deploy: o fator de multiplicação do pico de
memória por requisição veio de leitura de código, não de medição sob carga. Por isso a conta de
quantas requisições simultâneas cabem no teto (cerca de duas e meia) está escrita lá, junto do
aviso de que quem segura isso hoje é o corte da camada da frente. Ver {F8-54}.
Três cartões nasceram desta obra: {F8-54} (o limite anunciado que a frente corta),
{F8-55} (a mesma guarda fail-open em outras duas rotas do Hub) e {F8-56} (o placar que reprova
por acaso). Mais o {F8-57} acima, visto na tela.
Cartão novo, achado no review da Fatia A da {F4-07} em 2026-08-05, e medido, não suposto. O
limite de tamanho de envio passou a ser configurável pelo admin da agência, e **o catálogo não tem
faixa máxima**. O serviço de parse está protegido por um teto de infraestrutura próprio, mas o
processo da API do Hub lê o corpo inteiro da requisição em memória, e o container roda **sem
limite de memória** (medido: limite zero). Ou seja, um número grande digitado por um admin de uma
agência pode derrubar o processo que atende todas as agências. O risco nasceu junto com a
configurabilidade: antes, o valor fixo era o teto.
RNF-PERF-002, RNF-DISP-001, e o próprio {ART-CFG-01}.
O que implementar: faixa máxima no catálogo de parâmetros, recusada na escrita (fail-closed, é
onde a guarda mora) e não só na leitura; limite de memória no container da API, coerente com a
faixa; e leitura do corpo que não dependa de buferizar tudo antes de decidir, ou pelo menos a
recusa acontecendo antes de o corpo inteiro entrar na memória.
Como verificar (teste primeiro): tentar gravar um valor acima da faixa é recusado com o limite
nomeado; enviar um corpo grande é recusado sem que o processo cresça em memória proporcionalmente;
e o container tem limite de memória declarado.
Aceite (H1): Dado um admin de agência configurando o limite de tamanho de envio, Quando ele
informa um valor acima da faixa permitida, Então a escrita é recusada nomeando o limite, E nenhum
valor gravável permite que uma requisição de uma agência esgote a memória do processo que atende
as demais.
Cartão novo, achado durante o {F8-48} em 2026-08-12, e é o caso concreto que dá nome ao problema
que aquele cartão descreve em geral.
O que foi medido: um merge que entrou na main hoje traz no próprio assunto do commit a
afirmação "qa PASS", e o arquivo de gate de QA correspondente tem zero bytes. O gate de
review da mesma fatia tem conteúdo real, ou seja, não é falha de ambiente que atingiu os dois: **um
foi escrito e o outro não.**
O que isso significa, sem dramatizar: ninguém sabe se aquela verificação aconteceu. Pode ter
acontecido e o registro ter se perdido; pode não ter acontecido. **A ausência de registro torna as
duas hipóteses indistinguíveis**, e é exatamente isso que um gate existe para impedir.
Por que virou cartão em vez de conserto imediato, e a decisão está certa: a obra do {F8-48}
recusou-se a escrever um veredito que ninguém mediu. Seria cometer, dentro do commit que mata o
defeito, exatamente o defeito. E recusou-se a apagar o arquivo, porque apagar destrói a evidência de
que a fatia mergeou alegando gate. Registrou como pendência viva.
O que faltava, e este cartão resolve: a pendência existia só como prosa no cabeçalho de um
registro. Cobrança sem dono não é cobrança: some na próxima leitura.
O que implementar: rodar a verificação daquela fatia de verdade e gravar o gate com o que for
medido. Se ela reprovar, é achado, não fracasso: significa que algo entrou em produção sem
passar, e aí o conserto é trabalho seguinte, com número na mão.
Cuidado: aquela fatia não é do módulo de relatórios. Antes de medir, confirme o que ela mudou e
se o comportamento ainda está em produção. Reverificar código que já foi substituído é gastar
fôlego à toa.
Como verificar (teste primeiro): o gate daquela fatia deixa de estar vazio e contém veredito
medido; e a varredura do {F8-48} para de listá-lo como pendência.
Aceite (H1): Dada a fatia que mergeou alegando verificação aprovada, Quando alguém consulta o
gate dela, Então encontra um veredito que corresponde a uma medição real, E a pendência sai do
registro.
FECHADO em 2026-08-13. A verificação foi feita de verdade e o gate passou de **0 para 10.851
bytes, gravado nas duas coleções. Veredito: aprovado com ressalva**, e a ressalva é o produto.
A fatia está viva e valia medir: 23 commits entraram depois sem reverter nada, e o pacote servido
em produção é posterior ao merge. Não era arqueologia.
O achado que fecha o círculo, e é mais forte do que este cartão previa: aquele merge alegou
duas coisas no próprio assunto. A primeira, "qa PASS", tinha o gate vazio. A segunda, "review
APPROVED", tem gate com conteúdo, dizendo NEEDS CHANGES. As duas afirmações contrariadas pelo
registro que elas invocam.
E aqui está a parte que importa não confundir: o CÓDIGO está certo. A verificação exercitou o
guarda com quatro controles negativos plantados, confirmou o contrato de ponta a ponta contra a
produção real, e achou as correções que o review pedia já aplicadas na main. **O que era falso
era o registro, não a entrega.** É por isso que o defeito é insidioso: ninguém mentiu sobre o
produto. O carimbo é que ficou oco, e carimbo oco não distingue "verifiquei" de "não verifiquei".
A verificação foi refeita contra a LEI, e ficou melhor. A lição de método é essa. A primeira
medição confrontou a fatia com as próprias alegações dela (assunto do merge, comentário do
guarda, gate de review). Depois o verificador foi ler os requisitos publicados e reenquadrou o
que tinha achado. Medir a obra contra o que ela diz de si mesma é medir o eco.
O veredito da fatia ficou MAIS firme, não mais frouxo: as três cláusulas do requisito que é dela
passam, inclusive a mais difícil, que passa por construção. Uma fatia irmã ficou **parcial
declarada**: a faixa existe, mas gravação, número do caso e aparecimento na central exigem sessão
logada e não foram medidos.
Três reprovações nomeadas, todas do módulo de erros e não deste roadmap:
1. A alegação de "exaustivo por construção" do guarda não se sustenta. Uma das formas de escrever
o erro cru é acusada; três outras passam, e a que escapa é justamente a forma histórica que o
próprio gate de review daquela fatia descreve como a mais comum no código antigo.
2. Não existe verificação de cobertura, e é isso que reprova, não o número. O requisito não
exige cobrir todos os códigos de uma vez; exige que o quanto falta seja **sempre conhecido e
decrescente. Medido à mão: a interface emite 559 códigos e o catálogo tem 301**. Mas não
há série anterior contra a qual provar que diminui, porque nada mede isso. A nota do próprio
requisito é explícita: viola-se quando a verificação não sabe dizer quantos faltam.
3. 276 das 301 entradas do catálogo não têm ação sugerida (91,7%), contra um requisito que exige
mensagem humana e ação, e que manda rejeitar a gravação de entrada sem uma das duas. Não há
rejeição: o campo é opcional e nenhum guarda o cobra. É desvio entre a lei publicada e a
implementação, e pertence ao registro de erros, entregue antes desta fatia.
O que NÃO foi medido, declarado: nada de navegador logado, nenhuma prova visual renderizada. O
lado da tela foi provado por estrutura e por contrato de rede.
FECHADO em 2026-08-18. O critério do cartão era um só - *duas execuções simultâneas não podem
se ver* - e foi atendido por inteiro: um banco por execução (mhub_test_<pid>_<epoch>),
clonado de um template com schema, GRANTs e RLS de produção. **Medido: o clone leva ~840 ms e
carrega as 165 policies**, ou seja, isolar sai mais barato que a etapa de investigação que cada
frente gastava provando que a reprova não era dela.
A raiz era uma linha: scripts/test-all.sh passava o --env-file de produção ao container de
teste. O -e DATABASE_URL agora entra depois dele (o -e vence) e o banco é dropado no trap
de saída.
O template recusa a si mesmo se estiver errado: scripts/test-db-base.sh compara as policies
com produção e falha se divergirem - sem isso, um teste de isolamento multi-tenant passaria no
template e falharia no ar, que é pior que não ter template.
Contenção não cai em silêncio para produção: não conseguindo criar o banco, a suíte diz que
rodou sob contenção, que é o mínimo que o próprio cartão aceita.
A guarda permanente mede o efeito, não a configuração: lê o DATABASE_URL que o processo
recebeu de fato e reprova se apontar para produção ou fugir do padrão com marca de execução.
Duas guardas do próprio repo pegaram trabalho meu no caminho, e as duas estavam certas:
{ERR-06} mostrou que a home de {OP-01} montava faixa de erro à mão em vez de usar o
InlineErrorBanner (que deriva mensagem e contexto de relato do valor CRU do catch - guardar
a string já traduzida faz o "Avisar o suporte" chegar vazio), e {DS-01} apontou que a dropzone
de {OP-04} mudou de className e saiu do baseline nominal. Corrigidos os dois no mesmo commit.
Suíte do pacote 1631 verde; merges 3b1e920 (pacote) e 05a38fe (Hub), front deployado.
Cartão novo, medido em 2026-08-13 por três frentes independentes na mesma tarde, cada uma
chegando à mesma conclusão sem falar com as outras.
O que acontece: a suíte da API roda contra o banco vivo compartilhado. Com várias frentes
trabalhando ao mesmo tempo, ela acusa arquivos que ninguém tocou (com assinatura de **morte do
processo do banco, não de asserção) e a rodada leva três vezes** o tempo histórico. A mesma
rodada chega a marcar um arquivo como reprovado e outro como resolvido, em sentidos opostos.
O DANO É MAIOR QUE O PLACAR, e isto foi medido em 2026-08-13, não suposto: as suítes rodando de
worktrees não mergeados escrevem no banco de PRODUÇÃO. Foi assim que nasceram os 265 avisos com
endereço morto na conta real do dono, atribuídos por três frentes ao agendador de produção **até
alguém pegar uma suíte no ato**. Ou seja: a falta de banco próprio não deixa só o placar mentiroso,
ela suja o produto e ainda fabrica diagnóstico errado sobre a origem da sujeira. **Vinte e três
worktrees** ainda rodam código antigo contra esse banco.
O custo real, e é o motivo do cartão: cada frente agora gasta uma etapa inteira **provando que a
reprova não é dela**: roda isolado, compara com o checkout de produção, conta suítes concorrentes,
argumenta que o diff não tem uma linha de banco. Três fizeram isso hoje. É trabalho de investigação
que não produz nada, repetido por todo mundo, toda vez.
O dano pior é o que ainda não aconteceu: quando reprovar é normal, a reprova verdadeira passa.
Já existe o hábito de olhar um vermelho e presumir ambiente, e o hábito está certo hoje, o que é
exatamente o que o torna perigoso. Um gate que reprova por acaso ensina a ignorar gate.
O que implementar: a suíte precisa de banco próprio por execução, não do compartilhado. Como
isso se faz aqui é decisão de quem implementar, mas o critério é um só: **duas execuções simultâneas
não podem se ver. Se isolamento completo não couber, o mínimo aceitável é a suíte falhar
dizendo** que o ambiente estava sob contenção, em vez de acusar um arquivo inocente.
PARTE DISTO JÁ FOI CONSERTADO em 2026-08-13, e o conserto vale como precedente. Dois dos arquivos
que reprovavam não eram contenção: eram consequência real de um merge do mesmo dia. Um deles
porque um comentário contendo chave truncou o padrão de uma varredura, que parou de olhar e seguiu
verde, e ainda passou a afirmar que uma peça tinha consumidor quando tinha zero. Consertado na
raiz, com guarda que exige que os dois lados da conta saiam da mesma fonte, mais a guarda
equivalente do outro lado, e o agregador deixou de anunciar estouro de tempo como "resolvido".
Mergeado e verde em produção. A lição que fica: três frentes independentes chamaram isso de
"contenção do banco" antes de alguém medir o mecanismo. "É ambiente" fecha a investigação cedo.
E o próprio instrumento já dá conselho errado, medido em 2026-08-13: o agregador anunciou
"resolvido, considere remover do baseline" para um arquivo que naquela mesma rodada **estourou o
tempo. Quem seguir o conselho apaga do baseline uma falha conhecida**, e a próxima rodada passa
a acusá-la como se fosse nova, ou pior, deixa de acusá-la. É guarda falhando aberta dentro da
ferramenta que existe para nos dizer a verdade sobre o placar. Conserte isso junto: estouro de
tempo não é aprovação, e o agregador não pode tratar "não reprovou" como "passou".
Não trate como flake a ser tolerado. Marcar como instável e seguir é a resposta que transforma o
problema em permanente. O baseline conhecido já cresceu assim.
E há um custo de segunda ordem, medido no mesmo dia: ao alegar que uma reprova é do ambiente, as
frentes começaram a montar prova por comparação de tempo ("esta rodada levou o triplo do
histórico"). Uma revisão foi conferir e o número invertia: a rodada verde tinha levado mais
que a vermelha. A conclusão continuava certa. O que de fato fecha o caso é a **assinatura de morte
do processo do banco**, aparecendo em fatias sem relação entre si, cada uma reprovando coisa
diferente. Conclusão certa por prova errada continua sendo prova errada, e alegação de
alheiedade sem artefato não vale nada. Enquanto este cartão não fechar, quem alegar ambiente deve
citar o mecanismo e anexar o artefato, nunca comparar duração.
Como verificar (teste primeiro): duas execuções simultâneas da suíte terminam com o mesmo
placar que cada uma teria sozinha.
Aceite (H1): Dada a suíte rodando com outras execuções simultâneas, Quando ela termina, Então o
placar é igual ao da execução isolada, E nenhuma reprova é causada por trabalho de outra frente.
FECHADO em 2026-08-18 pelo Cenário 1, que é a recomendação do próprio cartão. Medido: fábrica
anunciava 100 MB, teto configurável 256 MB, e a frente corta em 64 MB - um envio de
80 MB morria antes de chegar, com o operador tendo respeitado o número da tela.
Fábrica desceu para 60 MB (4 MB de folga para o envelope multipart) e o teto para 64 MB:
adiantaria pouco alinhar o default e deixar o admin configurar um número que cai no mesmo buraco.
Por que não o Cenário 2: o corte da frente é a proteção de memória do processo que atende
todas as agências. O runbook do {F8-18} já tinha feito essa conta e previsto quem executaria
este cartão - e o efeito colateral bom ficou registrado lá: **com o teto gravável dentro do que a
frente aceita, o cenário de OOM por configuração deixa de ser alcançável.**
FRONT_MAX_BODY_BYTES declara o número da frente em vez de deixá-lo implícito - sem constante
nomeada a relação vira coincidência, alguém sobe o default e nada acusa. Mesmo par de {OP-08}.
Efeito colateral legítimo corrigido junto: o teto de corpo do parse-server é derivado do default,
e o teste dele redigitava "180MB" ao lado da derivação - o número fixo virou mentira quando o
default caiu, então ficou a propriedade (maior que o default), que é o que ele guardava.
Suíte 1635 verde; merges no pacote e no Hub, runbook atualizado.
Cartão novo, achado pela obra do {F8-18} em 2026-08-13, medindo a camada que o cartão dela nem
mencionava.
O que foi medido: o limite de envio que o produto traz de fábrica é maior do que o que o
servidor da frente aceita. Um envio legítimo de tamanho intermediário (acima do que a frente passa,
abaixo do que o produto diz aceitar) morre antes de chegar. O operador leu o número na tela,
respeitou o número, e o envio falhou mesmo assim.
Por que é defeito e não configuração: o produto anuncia um limite que ele não consegue
honrar. É a família de sempre (peça declarada e inalcançável), só que virada para o usuário: aqui
quem confia no número é quem opera.
Não conserte sozinho o que é decisão de produto: ampliar o que a frente aceita muda o que entra em
produção, e isso não é chamada de quem implementa. Cenário 1: baixar o padrão de fábrica para
caber no que a frente aceita (barato, imediato, e o número na tela passa a ser verdade); o preço é
que o teto do produto encolhe. Cenário 2: subir o que a frente aceita até o padrão de fábrica
(mantém o teto anunciado); o preço é mais memória em jogo e a faixa máxima recém-criada precisa ser
reexaminada contra ele. Recomendação: Cenário 1. O menor número já é o real hoje; alinhar o
anúncio à realidade não pede nada de ninguém, e subir o teto é decisão que se toma quando alguém
precisar, com o caso na mão.
PERIGO, e é o motivo de este cartão não ser trivial, achado no review do {F8-18} em 2026-08-13:
a camada da frente não é só um estorvo, é a proteção que está de pé hoje. O limite de memória do
processo foi dimensionado contra uma requisição no teto; com várias ao mesmo tempo, cabem cerca de
duas e meia antes de o processo que atende todas as agências morrer. Quem segura isso hoje é
justamente o corte da frente. **Escolher o Cenário 2 sem refazer a conta de memória remove a
proteção**, e remove exatamente o risco que o {F8-18} existiu para fechar, com todos os gates
verdes. Se subir o corte da frente, recalcule e declare quantas requisições simultâneas cabem.
Enquanto estiver nisto, meça a cadeia inteira: onde mais o produto anuncia um limite que outra
camada corta antes? Um número só é verdade se toda a cadeia o honrar.
Como verificar (teste primeiro): um envio do exato tamanho anunciado como aceito **chega e é
processado**; se não chegar, reprova nomeando a camada que cortou.
Aceite (H1): Dado o limite de envio que o produto anuncia, Quando alguém envia um arquivo desse
tamanho, Então ele é aceito pela cadeia inteira, E nenhuma camada intermediária corta abaixo do que
a tela promete.
FECHADO em 2026-08-18. As duas rotas eram tasks.ts (anexo de tarefa) e chat.ts (anexo de
conversa), as duas com parseInt(header ?? "0") - que falha aberta duas vezes: ausente vira 0
e aprova; ilegível vira NaN e, como Number.isFinite(NaN) é falso, a condição inteira é pulada e
a guarda não roda. As duas passaram a usar o lugar único, não uma cópia corrigida.
O cuidado que o cartão pede mudou o código: a folga padrão da guarda é 1 MB e essas rotas
sempre usaram 8 KB, então a folga vai explícita - adotar o padrão as faria aceitar ~1 MB a
mais, que é afrouxar, o inverso da intenção. O teste entra com o par: o ilegível que passou a ser
recusado (411) e o envio legítimo no teto que continua passando.
DIVERGÊNCIA RECONCILIADA, e a implementação viva venceu: o aceite escrito neste cartão pedia
recusar também o cabeçalho ausente. A guarda única do {F8-18} decidiu o contrário, com o
motivo no próprio arquivo - corpo sem content-length é transfer-encoding: chunked legítimo, e
o client_max_body_size do nginx já o recusa acima do teto; recusar aqui quebraria cliente
válido. Este cartão foi escrito antes dessa decisão existir. **O aceite vigente é: ausente segue,
ilegível e excedente são recusados.**
E este cartão consertou o {F8-56}, que eu tinha fechado sem prova. Ao exercitar o caminho real descobri que aquele merge chamava criar_banco_de_teste - uma função que nunca foi gravada no arquivo (o patch que a inseria falhou numa asserção e não escreveu; o seguinte inseriu só o chamador). O script caía sempre no ramo de contenção: por acaso o comportamento era o seguro (a suíte não rodava contra produção), mas a feature não existia. Por que passou: a imagem mhub-api-test-runner:f8-33 não existia neste host, então o caminho docker - o único onde o banco isolado entra - nunca tinha sido percorrido. Imagem construída pelo Dockerfile do test-all.README.md, e agora provado ponta a ponta: banco próprio criado (mhub_test_1042565_1787057915), 52 arquivos de teste rodando contra ele, a guarda confirmando de dentro do container que não é produção, e zero bancos deixados para trás. É a mesma lição que a onda OP escreveu dois dias antes, e desta vez fui eu: card só fecha depois de exercitar o caminho real, não o que o código diz que faz. Cartão novo, achado pela obra do {F8-18} em 2026-08-13. Aquela fatia consertou uma guarda de tamanho de corpo que falhava aberta: cabeçalho ausente virava zero e aprovava; cabeçalho ilegível virava valor não-numérico e pulava a condição inteira, então a guarda simplesmente não rodava. Fail-open silencioso, a assinatura que este projeto já nomeou. O que foi medido: a mesma condição, copiada, vive em outras duas rotas do Hub, fora do módulo de relatórios. Elas não foram tocadas porque estavam fora do escopo, e a decisão de não consertar por fora está certa. Por que vira cartão em vez de nota: cobrança sem dono some na próxima leitura. Foi exatamente assim que o {F8-50} nasceu. O conserto do módulo de relatórios extraiu a guarda para um lugar único; o trabalho aqui é as outras duas passarem a usar esse mesmo lugar, não ganharem cada uma a sua cópia corrigida. Copiar o conserto reproduz o problema com a idade zerada. Cuidado, e é o que decide o tamanho do cartão: essas rotas não são do módulo de relatórios. Meça o que elas aceitam hoje antes de apertar. Uma guarda que passa a recusar o que sempre passou é regressão, mesmo sendo mais correta. Entre com o par: o caso que prova que ela pega, e o que prova que ela não ficou severa demais. Como verificar (teste primeiro): cabeçalho ausente e cabeçalho ilegível são recusados nas duas rotas; o envio legítimo que passa hoje continua passando. Aceite (H1): Dada uma rota do Hub que limita tamanho de corpo, Quando o cabeçalho de tamanho vem ausente ou ilegível, Então a requisição é recusada, E nenhuma dessas rotas mantém cópia própria da guarda.
Cartão novo, achado em 2026-08-13 pela obra do {F8-53}, que foi contar os avisos do próprio módulo
e mediu o Hub inteiro de passagem.
O número, e ele é grande demais para ser aceito sem reconferência: de 7256 avisos publicados no
Hub, 6903 carregam endereço que não casa com nenhuma rota declarada no front. O módulo de
relatórios responde por uma fração pequena disso; a maior parte vem de outro módulo.
ANTES DE QUALQUER CONSERTO, MEÇA O NÚMERO DE NOVO, e por conta própria. Ele veio de uma
comparação contra a tabela de rotas que a fatia do {F8-53} montou para o seu escopo. É
inteiramente possível que outros módulos usem convenção de endereço diferente e que boa parte dos
6903 esteja certa pelo padrão deles. **Um número que diz "95% do produto está quebrado" quase sempre
quer dizer que a régua está errada**, e agir sobre ele sem reconferir seria o erro mais caro
disponível aqui.
Se o número se confirmar, ainda assim não é um cartão de conserto em massa: é um cartão de
guarda. A única conferência que existe hoje sobre esses endereços checa se o texto começa com
barra. O {F8-53} construiu, para o módulo de relatórios, um funil que confere o endereço contra as
rotas no momento da criação, fail-closed, com motivo escrito obrigatório quando o aviso
legitimamente não tem link. O trabalho aqui é levar esse mesmo funil para os outros produtores,
não sair corrigindo endereço a endereço.
Cuidado com o que já está no banco: apagar aviso publicado é destrutivo e é decisão do dono, não
de quem implementa. Meça, relate, não apague.
---
### MEDIÇÃO REFEITA POR CONTA PRÓPRIA (2026-08-18) - o número se confirma, a leitura muda
Contado direto no banco, com a régua remontada do zero a partir dos path="/…" do App.tsx (não reaproveitei a tabela do {F8-53}, que é o que o cartão manda evitar):
| Módulo | Avisos com link | Casam com rota | Não casam |
|---|---|---|---|
| control | 8.181 | 57 | 8.124 |
| reports | 310 | 42 | 268 |
| social | 3 | 0 | 3 |
| drx | 257 | 257 | 0 |
| chat | 135 | 135 | 0 |
| tasks | 84 | 84 | 0 |
| opadmin | 8 | 8 | 0 |
| financeiro | 1 | 1 | 0 |
| Total | 8.979 | 584 | 8.395 |
A suspeita do cartão - "outros módulos usam convenção diferente e a régua está errada" - NÃO se confirmou. Cinco módulos estão 100% corretos pela mesma régua. O problema está concentrado em dois prefixos: /controle (8.124) e /reports (268, o resíduo do próprio módulo, cuja rota real é /relatorios). Confirmado por busca no front inteiro: /controle não existe em lugar nenhum - nem no router, nem em qualquer arquivo.
O ACHADO QUE MUDA O DESENHO DO CONSERTO, e não estava no cartão: no módulo control o campo link não é um endereço de navegação - é a chave de deduplicação. O próprio código diz ("Link canônico de dedup: /controle/alertas/task_overdue:<task_id>. Usado como discriminador único") e a dedup consulta por LIKE '/controle/alertas/task_overdue:%', em 15 pontos. E o mesmo campo é o destino do clique: NotificationBell.tsx:114 faz navigate(n.link) para qualquer valor que comece com /. Como rota desconhecida cai no path="*" → <Navigate to="/">, clicar em qualquer um dos 8.124 leva ao painel inicial, em silêncio. Não é endereço errado por descuido: é um campo com duas responsabilidades, e a segunda nunca foi honrada.
Consequência para o conserto: levar o funil do {F8-53} para o control como está o faria reprovar as 15 chamadas de dedup - que não são erro de endereço. O caminho que resolve os dois sem DDL é o control passar a discriminar por um link que existe (/alertas?..., e /alertas é rota real), mantendo a dedup pela mesma técnica com a string nova; a transição precisa cobrir os dois formatos por uma janela, senão os 8.124 antigos deixam de casar e republicam. Fica explicitamente fora do que executo sozinho: mexer na dedup de 8 mil avisos vivos do produto do dono, e a alternativa mais limpa (coluna própria de chave de dedup) é DDL em messages, fora do schema reports. Medido, relatado, nada apagado - como o cartão pede. Como verificar (teste primeiro): um aviso criado com endereço que não casa com rota declarada é recusado, em qualquer módulo; e um aviso legítimo (inclusive com trecho variável, parâmetro de busca ou fragmento) continua sendo criado. Aceite (H1): Dado qualquer produtor de aviso do Hub, Quando ele publica um aviso com link, Então o endereço é conferido contra as rotas declaradas na criação, E nenhum produtor consegue publicar endereço que não existe.
---
### FECHADO em 2026-08-18, guarda Hub inteiro ligada, e SEGUNDA MEDIÇÃO diverge da tabela acima
A guarda saiu do módulo de relatórios e passou a valer para o Hub inteiro, em publishNotification (apps/api/src/lib/notifications.ts): 20 importadores, zero insert direto em produção, fail closed. O espelho de rotas mudou de casa para apps/api/src/lib/web-routes.ts (sem re-export); o casador matchesDeclaredRoute continua único, em notif-link-scan.ts. 7 isenções NOMEADAS por prefixo, só para o módulo control, nunca por módulo inteiro, cada uma com motivo escrito, dono e cartão de saída {F8-62}; publicar sob isenção emite [notif-link][isento]; isenção MORTA reprova na guarda estática. 3 destinos mortos corrigidos com o destino medido no front: /canais-email/threads?conv=<id> (a página passou a ler ?conv), /empresas?company=<id>&suspension=<t> (com o LIKE do dedup cobrindo os DOIS formatos por uma janela) e /agencia.
A remedição de fechamento (por conta própria, antes de fechar o cartão) DIVERGE da tabela acima, e as duas ficam registradas, sem ajustar nenhuma: a tabela publicada nesta MEDIÇÃO REFEITA (2026-08-18) contava 584 casam / 8.395 não casam; a segunda contagem, feita para fechar o cartão, mediu 395 casam / 8.584 não casam. As duas linhas que a tabela dava como 100% corretas e não são: chat (135, rota real /canais-email/threads/<id> tem 2 segmentos, o link tinha 3) e control (+54, formato /admin/companies/<id>?suspension=, que não existe nem como rota de API). O cartão fecha com AS DUAS contagens escritas, não com uma corrigindo a outra.
Provas da guarda ligada: dry-run com o balde "lançaria" VAZIO antes de ligar a guarda; guarda estática 90/0; par de casos em execução 26/0 (6 endereços mortos lançam SEM gravar; 27 legítimos seguem publicando); reprovação de isenção morta provada por QUEBRA. Duas reprovas custaram a fatia inteira e as duas eram do INSTRUMENTO de teste, não da guarda: o plantio em disco usava nome fixo dentro do repo e, como sessão concorrente é requisito deste workspace, uma rodada lia o resíduo de outra como produtor real (o teto da isenção acusava "2ª publicação" num arquivo que tem uma). Isolar a escrita por PID não bastou, a varredura lê a árvore inteira; corrigido ignorando plantio alheio na varredura e limpando só o de processo morto. Provado com 3, 5 e 8 processos simultâneos.
NADA foi apagado no banco: os avisos com endereço morto seguem medidos e relatados, como o cartão sempre pediu. Sucessor nomeado: {F8-62}, o achado de produto que a medição revelou (no control o campo link tem duas responsabilidades, chave de dedup e destino de navegação, e a segunda nunca foi honrada) fica fora do que se corrige em massa aqui, por ser decisão do dono sobre dado vivo.
ENTREGUE em 2026-09-21 pelo Cenario 2, merge 087e847 no ModularHub (o codigo e todo de la;
aqui entra so a documentacao). Gates plan APPROVED, qa PASS e review APPROVED, os tres viajando
dentro do merge. Prod: /api/health 200, verificador do {P0-A2} 14/14, 0 avisos no formato
velho.
Numeros: nenhum dos que o cartao trazia fechava. A recontagem deu 8 construtores (7 no
escopo), 8 leituras de dedup, 10 predicados LIKE (8 no escopo) e 7 prefixos isentos,
contra os "6 construtores e 7 consultas" de um paragrafo e as "15 chamadas" de outro. E a
divida era 15.747, nao 8,2 mil: os 540 de /admin/% que o cartao dava como rota real eram
537 quebrados (/admin/companies nao existe) e so 3 funcionavam.
Escopo ampliado por decisao, e esta escrito no merge: absorveu o {F8-58}/{F8-59}, matando o
LIKE velho de control-jobs.ts:892. Motivo: mesmo defeito, mesma correcao, mesmo arquivo.
O achado que governou a implementacao inteira: a janela dupla do {F8-58} nunca funcionou
(o formato novo tinha 0 linhas no banco e o velho seguiu sendo gravado ate 13/09, 26 dias
depois do commit). Por isso a expiracao desta virou teste que FALHA na data, nunca cartao
datado, e o qa verificou injetando o bug "esqueceu de remover": o teste fica vermelho de
verdade.
Defeito de execucao que fica registrado: a entrega foi **deployada antes dos gates e do
merge**, o que inverte a ordem que protege (o gate deixa de poder dizer "nao entra" e so pode
dizer "reverta"). E, nos ~10 minutos entre o backfill e o deploy, o codigo ainda velho
gravou 44 avisos no formato morto, justamente quando a isencao daquele prefixo ja tinha sido
removida. Foram corrigidos pelo mesmo UPDATE idempotente; eram 4 eventos x 11 destinatarios,
todos da "Agencia Teste", entao nenhum cliente esteve exposto.
Pendencias nomeadas, que NAO reprovam o H1: o cron real nao foi disparado depois do deploy
(o executor publicou direto, para nao atingir cliente real da ADS4PROS); e a prova por clique
saiu numa empresa com o modulo Controle desligado, entao o aviso chega a parede de entitlement,
que satisfaz o H1 literal ("tela real, nunca o painel em silencio") mas nao mostrou a Central
renderizada.
O que PERDEU: a maior familia, wf_inactive (6.736 avisos, 44% do modulo), passa a cair
numa tela valida que nao lista esse tipo, porque a Central monta 3 tipos. Virou o {F8-63}.
ENTREGUE em 2026-09-21, merge 9a6ea3c no ModularHub. Gates plan APPROVED, qa PASS e
review APPROVED, os tres viajando no merge. **Provado por CLIQUE real, que e o unico jeito de
fechar este cartao:** o aviso "Workforce sem atividade registrada: Deact WF" leva a Central com
a linha destacada e o nome legivel, e o controle negativo provou o metodo (bloqueando a
pessoa pela propria interface, o item saiu do destaque e do contador; depois restaurado).
7 prints em tmp/qa-screenshots/f8-63-clique-2026-09-21/.
A alternativa obvia foi DERRUBADA por medicao, e vale registrar porque era a minha aposta:
mandar o link direto para a ficha da pessoa (/pessoas/<id>) parecia mais simples e barato.
Mas nao consertaria os 6.391 avisos ja entregues, que estao gravados com o formato antigo, e
reprovaria em 387 deles, porque 6 de 18 wf_ids medidos nao resolvem mais e a ficha
devolve 404, que e literalmente a clausula de reprovacao deste cartao. A saida escolhida conserta
os 6.391 sem tocar um byte de dado, porque o discriminador do link ja e byte a byte o
alert_key que a Central casa desde o {F8-62}.
control-notif-links.ts ficou intocado de proposito: mexer nele criaria um terceiro formato
dentro da janela dupla vigente e o primeiro ciclo pos-deploy reenviaria ate 221 avisos duplicados.
A ordem foi respeitada desta vez: migration antes da imagem (senao o primeiro "descartar"
tomaria 500 de constraint), e gates antes do merge e do deploy, que e o que o {F8-62} inverteu.
O que PERDEU: a Central deixou de ser curta e passa a expor nome de colaborador marcado como
parado numa tela que o TM abre na frente de terceiros (mitigado por filtro e dispensa no proprio
cartao); e by_type/total mudaram de escala, entao leitura historica de "total de alertas"
nao e comparavel atraves deste merge.
**ENTREGUE em 2026-09-21, merge 4e469a4, deployado pelo caminho unico (14/14) e com o codigo
novo confirmado dentro da imagem que roda.** Gates plan APPROVED, qa PASS e review APPROVED,
os tres viajando no merge.
Nao era defeito de texto, e essa foi a descoberta que mudou o tamanho do cartao: o mesmo
zero alimentava QUATRO leituras, e tres nao sao texto. A severidade (sevByThreshold), a regra
configuravel do admin (_mag >= rule.gte), a ORDENACAO (b._mag - a._mag) e o rotulo.
Consertar so a exibicao deixaria essas pessoas no fim da fila e fora de qualquer limiar, com o
texto certo. Por isso a perna que o cartao exigia era a certa.
Nao exigiu migration, ao contrario do que o cartao supunha: control-alerts.ts ja tipava
dias_inativo como number | null. A representacao propria ja existia no dado e chegava
intacta ate a leitura; era ali que morria.
Provado com BASELINE, que e o padrao que este projeto deveria repetir: revertendo o arquivo
para o commit-pai numa instancia efemera, o defeito reproduz (NULL vira "0 dias",
severidade baixa, posicoes 12 a 16 de 17); com o conserto, as 5 pessoas sobem para as posicoes
0 a 4 com "nenhuma atividade registrada". Controle negativo: uma fixture com 4 dias reais
de atividade permanece na base, severidade baixa, o que prova que o conserto nao atropelou a
lista inteira para o topo.
O que PERDEU: a lista muda de ordem para quem ja se acostumou com ela, e uma regra de admin
criada esperando "so casos de N dias" passa a incluir tambem os "nunca houve", que sao mais
graves porem nao tem numero. Isso e intencional e esta dito aqui para nao virar surpresa.
Achado do qa na prova por clique do {F8-63}, confirmado ao vivo. Em control.ts:787, uma
pessoa que nunca registrou atividade nenhuma aparece como "0 dias sem atividade" e
severidade Baixa. Medido na conta de teste: a pessoa "Deact WF" esta ha 111 dias sem
nenhuma tarefa e e exibida como o caso mais brando da lista.
A causa e de familia conhecida: ausencia de registro esta sendo tratada como zero, e zero
aqui significa o oposto do que o dado diz. O mesmo erro de ler sentinela como valor.
Observavel: quem nunca teve atividade aparece no topo da severidade, nao na base, e o texto
diz o que realmente se sabe ("nenhuma atividade registrada"), nunca "0 dias". Reprova continuar
ordenando essas pessoas como se fossem as mais saudaveis da lista.
A perna que impede a reincidencia, e sem ela o conserto do calculo nao basta: o estado
"NUNCA houve atividade" precisa de representacao propria no dado, distinta de "houve ha
pouco". Enquanto os dois compartilharem o mesmo zero, qualquer leitura futura (ordenacao,
filtro, relatorio, media) vai confundir os dois de novo, cada uma do seu jeito, e o conserto
desta tela nao protege as proximas. **O painel nao esta so deixando de avisar: ele esta
afirmando ativamente o contrario do que acontece**, que e pior que o silencio do {F8-65}.
Reprova fechar corrigindo so a exibicao da Central.
Nasceu do {F8-62}, e e o preco declarado dele. Com o link consertado, o aviso de
wf_inactive chega a /alertas, que e tela real, mas a Central monta apenas 3 tipos e esse nao
e um deles. O usuario sai do painel-em-silencio e cai numa tela que nao fala do item que ele
clicou, o que e melhor, e ainda assim nao e o que o aviso promete.
Observavel: clicar num aviso de wf_inactive leva a uma tela que mostra AQUELE item.
Reprova se a tela abrir sem o item, ou se a solucao for esconder o aviso em vez de exibi-lo.
ENTREGUE em 2026-09-22, merge 9e41c25 no ModularHub (aqui entra so documentacao). Gates
plan APPROVED, qa PASS e review APPROVED, os tres viajando no merge.
O observavel fechou: universo de leituras de dedup sobre link foi de 9 para 0 (8 em
control-jobs.ts, 1 em error-report-notice.ts).
A janela dupla do {F8-62} MORREU aqui, nao empilhou. Sairam JANELA_DUPLA_ATE, OLD_PREFIX,
prefixosDeDedup, linksExatosDeDedup, discriminadorDeLink e o bloco datado do teste; entrou
guarda estatica sem data. O motivo e simples: quando a dedup nao le o link, o formato do link
sai da pergunta. Restam so tres comentarios citando o nome, registrando que morreu.
A ordem INVERTEU em relacao ao {F8-62}, e de proposito: la a escrita mudava de formato e por
isso havia um vao; aqui a escrita e aditiva, entao foi DDL, depois escritor em dual-write,
depois backfill, depois leitor. Com o escritor ja no ar, nenhuma linha nova nasce sem chave e o
conjunto a corrigir fecha em vez de crescer. O vao deixou de existir, nao foi mitigado.
Executado em producao nesta ordem, com medicao entre cada passo: DDL aplicada com o codigo
VELHO seguindo saudavel (prova de que e aditiva mesmo); escritor deployado 14/14; backfill com
16.372 linhas ganhando chave e pos-condicao validada dentro da propria transacao (0 linhas
de control/errors sem chave); leitor deployado 14/14, producao 200.
A pos-condicao usa contagem RELATIVA, nao total absoluto, e isso importa: a tabela cresceu
44 linhas entre duas medicoes do mesmo dia, e uma assercao com numero fixo abortaria uma
migration correta.
Achado alem do inventario do plano: 4 pontos de chamada (/admin/jobs x2 e alert-actions x2)
quebrariam em silencio dentro de catch mudo por falta da chave. Corrigidos.
Provado com controle negativo, que e o que separa "nao duplicou" de "esta desacoplado": em
banco restaurado de producao (17.798 linhas), com dedupe_key o link mutado nao duplica (1
aviso) e, lendo link, duplica (2). Rollback exercitado nos dois sentidos com totais identicos.
Divida NOMEADA, que fica aberta: o commit do leitor, se for ao ar ANTES do backfill, duplica
avisos ate o backfill terminar, e nao ha guarda em codigo, so a ordem do deploy. Nesta
entrega a ordem foi garantida por execucao medida passo a passo, mas "disciplina de deploy" e
exatamente o que o {P0-A2} desmontou: documento que descreve o certo nao impede a mao que faz
diferente. O {P0-A7} e o cartao que trata essa familia.
O que PERDEU: quem depurava lendo identidade no link perde o atalho; o digest passa a
contar dedupe_key distinto (mesmo numero hoje, numero diferente no dia em que dois enderecos
forem o mesmo evento, e isso aparece em texto que o TM le); os 9 modulos que nao dedupam ganham
coluna nula em todo select *; e produtor novo de control/errors perde a liberdade de
publicar sem chave, atrito deliberado.
E o Cenario 1 do {F8-62}, que o dono mandou NAO sumir da fila, porque e ele que impede o
defeito de renascer no proximo aviso que alguem escrever.
O que implementar: coluna propria de chave de dedup em messages.notifications (schema do
ModularHub), com backfill dos avisos existentes ANTES de trocar as consultas, e o link passa a
ser so endereco de navegacao.
Por que continua valendo mesmo com o {F8-62} entregue: o Cenario 2 consertou os enderecos,
mas o campo continua com duas responsabilidades. A proxima mudanca de rota reabre exatamente
este defeito, e a janela dupla que o {F8-62} abriu e divida com prazo.
Observavel: a dedup do control nao le mais o campo link, e mudar o formato de um link
deixa de poder quebrar a deduplicacao. Reprova fechar deixando qualquer LIKE de dedup sobre
link.
Nasceu da medição do {F8-58}. No módulo control o campo link tem duas responsabilidades: é a
chave de deduplicação (control-jobs.ts:120 diz isso por escrito, "Link canônico de dedup") e é o
destino do clique (NotificationBell.tsx:114 faz navigate(n.link)). A segunda responsabilidade nunca
foi honrada: os cerca de 8,2 mil avisos do control levam ao painel inicial em silêncio (rota
desconhecida cai em path="*" e vira <Navigate to="/">). São 6 construtores e 7 consultas LIKE
vivas sobre esse campo.
O que implementar: uma das duas saídas medidas, decisão do dono, não de quem implementa. (a) coluna
própria de chave de dedup, DDL em messages (fora do schema reports), o link passa a ser só
endereço de navegação. (b) migrar o FORMATO do link para um endereço que existe (/alertas?...,
/alertas é rota real), mantendo a dedup pela mesma técnica de LIKE com a string nova, com **janela
dupla de 7 dias** cobrindo os dois formatos (o digest lê /controle/alertas/% numa janela de 7 dias;
sem a janela dupla os avisos antigos deixam de casar e republicam).
Consulta que mede a dívida a qualquer momento, sem DDL: `select count(*) from
messages.notifications where origin_module = 'control' and link like '/controle/%'`.
Como verificar (teste primeiro): um clique num aviso do control criado ANTES da mudança continua
levando a uma tela válida, nunca ao painel inicial em silêncio; um aviso criado DEPOIS da mudança tem
link que casa com rota declarada; as 15 chamadas LIKE de dedup do control continuam recusando
republicação do mesmo evento.
Aceite (H1): Dado um aviso publicado pelo control, Quando o destinatário clica nele, Então ele
chega a uma tela real do Hub, nunca ao painel inicial em silêncio, E a deduplicação do control
continua recusando republicação do mesmo evento, E a transição cobre os avisos antigos e os novos pela
janela dupla.
PERGUNTA REGISTRADA PARA O DONO, e o loop seguiu (2026-09-21). O proprio cartao diz que a escolha e decisao do dono, nao de quem implementa, entao ela nao foi tomada aqui. O que foi feito foi MEDIR, para a decisao nao sair no escuro:
- A divida quase dobrou: sao 15.210 avisos com
linkem/controle/%, nao os cerca de
8,2 mil que o cartao anotou. Mais 540 em /admin/%, que caem em rotas reais.
- O silencio esta confirmado nos dois lados:
/alertasexiste como rota declarada;
nenhuma rota comeca com /controle; e App.tsx:701 tem path="*" levando a <Navigate to="/" replace />. Ou seja, os 15.210 cliques vao para o painel inicial sem erro.
- Formato real gravado:
/controle/alertas/<tipo>:<uuid>, em que o sufixo depois dos dois
pontos e justamente a chave de dedup. E por isso que o campo acumula as duas responsabilidades.
Cenario 1, coluna propria de dedup. Nasce dedup_key em messages.notifications; o link passa a ser so endereco. Ganha: a dedup deixa de depender do formato de uma URL, entao mudanca de rota nunca mais reabre este defeito, e os 6 construtores passam a dizer o que fazem. Perde: exige DDL numa tabela quente e que pertence ao Hub (messages), fora do schema reports; e preciso backfill dos 15.210 antes de trocar as consultas, senao a dedup para de casar e o sistema republica tudo. Cenario 2, migrar o formato do link. O link passa a ser /alertas?..., que e rota real, e a dedup continua por LIKE com a string nova, com janela dupla de 7 dias cobrindo os dois formatos. Ganha: conserta os 15.210 cliques sem DDL e sem tocar tabela de outro schema; e o caminho mais curto ate o usuario parar de cair no painel. Perde: o campo continua com duas responsabilidades, entao a proxima mudanca de rota reabre exatamente este defeito, e a janela dupla vira divida com prazo (quem esquecer de remove-la depois deixa LIKE duplicado vivo).
Recomendacao de quem mediu, para o dono confirmar ou descartar: Cenario 2 agora, porque o dano e de navegacao e esta acontecendo em 15.210 avisos, e Cenario 1 como cartao proprio logo em seguida, porque e ele que impede a reincidencia. Fazer so o 2 conserta a tela e mantem a armadilha; fazer so o 1 demora mais para o usuario sentir.
FECHADO em 2026-08-13, mergeado na ordem vinculante (pacote 57b6a4b, depois Hub 88c03a9) e
deployado. Eram quatro avisos, não três, e três apontavam para endereços inexistentes.
Provado por CLIQUE, que é a prova que faltava desde o começo: os três levam agora às telas certas
(agendas, portal do cliente, compositor), e o controle negativo mostra os endereços antigos
redirecionando ao painel em silêncio, sem erro. É por isso que o defeito viveu semanas com gates
verdes: todo mundo verificou que o aviso era criado, entregue, contado e lido, e ninguém clicou.
A causa raiz do erro original: um dos endereços existe, como rota de API. Foi colado como se
fosse endereço de tela.
Divergência do cartão feita de propósito, e correta: o cartão mandava apontar tudo para o
compositor. O aviso de comentário resolvido vai para o cliente, e o compositor é tela de operador:
seria trocar destino inexistente por destino proibido. Foi medido até o destinatário antes de
decidir.
CORREÇÃO DA MINHA PREMISSA, medida no deploy: eu escrevi que o aviso morto estava sendo publicado
pelo agendador de produção. Falso. Os avisos mortos são 100% da empresa de teste, e o produtor
são as suítes rodando a partir de worktrees não mergeados contra o banco de produção - uma foi
pega no ato. Produção nunca mais autora endereço morto, mas **a contagem só para quando cada frente
tomar a linha principal**. Ver {F8-56}.
O que a fatia deixa além dos três consertos: guarda que confere todo aviso do módulo contra a
tabela de rotas no momento da criação, fail-closed, com isenção por publicação (não por
arquivo), prova conferida por posição e teto de contagem. Custou quatro rodadas, e cada uma caiu
pelo mesmo motivo: marcação declarativa mais larga que a justificativa.
As cinco proteções foram provadas por QUEBRA, não por leitura: o revisor fechou cada buraco e
exigiu que o caso reprovasse. É o teste que, na fatia irmã do mesmo dia, revelou que dois casos
equivalentes eram decoração.
Cegueiras que restam estão declaradas e presas em casos, cada uma com a severidade medida: as duas
formas de apelido furam a varredura mas não a conferência em execução.
Nasceu daqui: {F8-58}, a mesma classe no Hub inteiro, com a trava de reconferir o número antes
de agir.
O enunciado original do cartão, preservado abaixo: Cartão novo, achado em 2026-08-13 pela obra do {F8-47}, que foi construir o aviso vizinho e esbarrou neste. O que foi medido: a notificação de "comentário resolvido" (viva desde a Fatia 12, ou seja há quase duas semanas em produção) carrega um link para um endereço que não existe no front. Quem clica cai no painel inicial, sem nenhum aviso de que o destino não existe, porque a regra de rota desconhecida redireciona em silêncio. Por que ninguém viu: o mecanismo funciona. O aviso é criado, entregue, contado e some quando lido. Todo o caminho que alguém verificaria está correto. O que ninguém exercitou foi o clique, que é a única parte que o destinatário usa. É a família que este projeto já nomeou: peça pronta e inalcançável, com o agravante de ter passado por gate verde. Redirecionamento silencioso é o que torna isto invisível: um destino inexistente que respondesse "não encontrado" teria aparecido no primeiro clique. Cair no painel inicial parece navegação normal. O que implementar: apontar o aviso para um endereço que existe e que leva a ver o comentário, não a uma tela genérica. A fatia irmã já usa o endereço do compositor, onde o acesso ao comentário está sempre visível. Use o mesmo, não invente um terceiro. ESTE NÃO É RISCO HIPOTÉTICO: está acontecendo agora. Medido durante o deploy de 2026-08-13: o aviso do disparador de agendas está sendo publicado em produção a cada poucos minutos, para o dono, com link para endereço que não existe. Não é "alguém pode clicar um dia": é aviso morto acumulando no sino de quem usa o sistema todo dia. Comece por esse, não pelo do comentário resolvido. Já medido, e é pior do que um: a validação foi olhar os outros e achou mais um apontando para endereço inexistente, no disparador de ciclo das agendas. Dois dos três avisos do módulo levam para o vazio. O único que funciona é o que nasceu ontem, com este defeito já conhecido. Não é descuido isolado: é o que acontece quando o endereço do aviso é escrito à mão e nada confere. Por isso a guarda importa mais que os dois consertos: trocar as duas cadeias resolve hoje e o terceiro aviso nasce errado amanhã. Como verificar (teste primeiro): o endereço que o aviso carrega casa com uma rota declarada, e um endereço que não casa reprova. Guarda por conferência de tabela, não por leitura humana, senão o próximo aviso nasce com o mesmo furo. Aceite (H1): Dado um aviso do módulo entregue a alguém, Quando essa pessoa clica nele, Então ela chega à tela que trata do assunto do aviso, E nenhum aviso pode ser criado apontando para endereço que não existe.
FECHADO em 2026-08-13, mergeado (085ba86) e no ar, com o bundle publicado e prova na tela.
As 15 divergências vivas fechadas. Placar do módulo: de 36 divergências e 15 vivas para **18 e
ZERO vivas, com o universo varrido idêntico** - foi essa igualdade que provou que a queda é
conserto e não amputação.
A causa não era cada tela: era não existir a quem perguntar. A tabela do contrato do servidor
vivia dentro dos testes, então cada tela escolhia sozinha qual permissão consultar. Ela virou
código de produção e a pergunta virou uma só, com as mesmas entradas do guarda.
Provado na tela, ator a ator: o papel de execução de fábrica, sem revogação nenhuma, deixou de
ver os controles que o servidor recusaria e continua usando o que pode - inclusive abrindo o
assistente. Publicar permaneceu, corretamente, porque aquele papel tem a permissão. **Zero recusa ao
abrir qualquer tela.**
O smoke achou o que nenhum teste pegava, e virou regra: uma tela buscava dado incondicionalmente
numa rota que exige permissão, e a pessoa levava erro só de abrir. A suíte mede o controle;
aquilo era carga. Virou invariante: toda chamada disparada na abertura é guardada pelo que exige
ou dispensada com motivo, e o invariante obrigou 13 chamadas em 7 telas a perguntar.
Cinco instâncias dessa classe apareceram, cada uma por um meio diferente - navegador, leitura,
invariante, mutação e execução instrumentada. Nenhum meio sozinho teria bastado, e o censo final
por análise sintática bateu chave a chave, provando que não há sexta.
O defeito de método que custou as rodadas: marcação declarativa que remove algo da vista da
guarda, sete vezes com nome novo. O desfecho foi eliminar o campo, não torná-lo honesto - e a
prova é adversarial: reintroduzir a declaração com outro nome não engana o invariante, porque o
que reprova é a consequência.
Fica declarado: 18 divergências latentes, o operador nunca alcança o controle porque a mesma
permissão guarda o carregamento da tela - protegidas por encanamento, não por guarda. E nove
formas que a varredura não vê, cada uma com o efeito nomeado.
Sucessor: {F8-52} prova que o servidor recusa; esta fatia prova que o produto não oferece.
Cartão novo, achado pela matriz de permissão do {F8-49} em 2026-08-13, e é o defeito do {F8-46}
letra por letra, numa tela maior, a mais usada do módulo.
O que acontece: os controles de publicar, republicar e salvar como template são
desenhados sem condição nenhuma de permissão. Quem tem acesso a compor mas não a publicar
carrega o rascunho inteiro (a rota de leitura exige só compor), vê os botões, clica e leva
recusa do servidor.
Por que é pior que o caso anterior: lá o operador perdia um clique numa tela de análise. Aqui ele
monta o relatório inteiro e descobre no fim que não pode publicá-lo. O trabalho não se perde,
mas a expectativa sim, e o produto não avisou em momento nenhum.
Uma tela irmã tem a versão fraca do mesmo: o assistente de criação é oferecido a todos, e a
recusa só aparece na hora de criar.
O que implementar: os controles só aparecem para quem tem a permissão que o servidor exige, e
reusando a mesma verificação que já virou fonte única no {F8-46}, não uma segunda condição.
Decidir explicitamente o que o operador sem permissão vê no lugar: nada, ou o controle desabilitado
com o motivo.
A matriz do {F8-49} já prende este defeito: os casos afirmam o comportamento errado **de
propósito, para que ao consertar eles reprovem** e obriguem a virar a afirmação. Não os apague:
inverta-os.
Mais duas ocorrências, achadas no review em 2026-08-13, e a segunda é pior que todas:
- "Novo cliente" é guardado pela permissão de compor, mas a rota que ele chama exige a de
criar conta. O papel de execução tem a primeira de fábrica e não tem a segunda: ele vê o
botão e leva recusa sem que ninguém tenha revogado nada. Não é caso de borda: é o padrão.
- O abridor de comentários da ficha do cliente não tem guarda nenhuma e chama rota que exige
compor, numa tela alcançável só com a permissão de identidade. Passou despercebido porque a
massa de teste devolvia lista vazia: com zero comentários o controle nem aparece, e o caso
ficou verde sem exercitar nada.
**Quinta divergência, achada em 2026-08-13 pela própria forma da matriz, sem ninguém suspeitar
dela: o painel de usuários** é liberado pela permissão de gerenciar usuários da agência, e o
servidor exige também a de gerenciar a equipe. Não saiu de inspeção humana: saiu de a
verificação passar a comparar tela contra rota. É a prova de que o conserto foi de forma, não de
caso.
Achado que vale mais que as cinco divergências: o painel de comentários parece protegido nos
dois pais que não têm guarda, e está, por dependência de dado, não por guarda. O botão só existe
quando a lista tem item, e a lista vem de rota guardada pela mesma permissão. Servindo a lista a quem
não tem a permissão, o botão aparece. Ou seja: hoje não vaza por coincidência de encanamento. **O
dia em que a origem do dado mudar, vaza sem que nada acuse**, e ninguém vai ligar a mudança da
origem a um furo de permissão. Ponha guarda de verdade nos dois pais.
Mais três, achadas pela validação em 2026-08-13, todas na ficha do cliente: importar metas exige
a permissão de compor; editar a identidade visual e encerrar o cliente exigem a de atualizar conta,
e a tela guarda as três pela permissão de identidade. Ou seja, a ficha do cliente sozinha tem
mais divergências do que o resto do módulo somado.
O que essas três provam sobre o placar, e vale mais que elas: o conjunto de divergências
conhecidas não é o total do módulo: a lista de controles verificados era montada à mão, então o
número media a lista, não o produto.
**O CONJUNTO REAL, medido em 2026-08-13 ao longo de cinco rodadas de correção do próprio instrumento,
e o número CAIU na última: 88 chamadas, 83 guardadas, 55 ações, cerca de 36 divergências: 15
vivas e 21 latentes. Não as oito que a lista curada conhecia: ela media um quinto do problema.**
Atenção ao histórico deste número, porque ele ensina mais que o valor: 5 → 32 → 34 → 39 → 36.
As quatro primeiras subidas vieram de o instrumento parar de esconder; a última descida veio
de ele parar de inventar. Ou seja, o mesmo instrumento errou nos dois sentidos, sempre por
premissa não medida. Não trate nenhum destes valores como definitivo sem o gate verde.
Guarda por PAPEL é a categoria que faltava, e foi ela que causou a sobrecontagem. Rotas exigem,
no servidor, que a pessoa seja gestora da própria agência antes de olhar a permissão. O
instrumento passou a enxergá-las (acerto), mas partindo da premissa de que **nenhuma tela consulta
papel. Cinco consultam, e em três delas tela e servidor concordam**: eram falso positivo.
A premissa era suposição, não medição, o mesmo erro que causou a subcontagem, com o sinal
trocado.
Ao consertar, lembre de duas coisas: esconder o controle por permissão não resolve divergência
por papel; e antes de tratar uma divergência por papel como real, **confira se aquela tela já consulta
papel**.
O número tem "pelo menos" na frente de propósito, e isto é parte do achado. A primeira derivação
publicou 32 e 12. A validação mediu que o instrumento **não enxergava chamada escrita em linhas
quebradas (5 invisíveis) e que três linhas da sua tabela declaravam lista vazia, e lista vazia
remove a chamada do placar por construção, escondendo duas divergências vivas**. Ou seja: as duas
formas de o número encolher em silêncio já aconteceram, as duas dentro do instrumento de medida.
Trate 34 como piso e não como conjunto até a verificação fechar com gate verde.
Divergência VIVA e LATENTE não são a mesma coisa, e esta distinção decide a ordem do trabalho.
Das 32, 12 são vivas: a pessoa clica e leva recusa. As outras 20 são latentes: o ator divergente
nunca chega ao controle, porque a mesma permissão guarda o carregamento da tela. **Latente não é
resolvida: é protegida por acidente de encanamento**, igual ao painel de comentários. Mudou a origem
do dado, vira viva, e nada acusa.
Comece pelas 12 vivas: são as que machucam alguém hoje. Mas não feche o cartão nas 12: as 20
latentes precisam ganhar guarda de verdade ou motivo escrito de por que não precisam.
Mais uma, que não estava na lista de ninguém: ativar o acesso de um contato, na ficha do cliente,
não tem guarda de permissão alguma e o servidor exige atualizar contato. Diferente de "Novo
cliente", esta exige revogação para aparecer, não nasce pronta.
A regra que o review destravou, e vale para toda guarda de permissão daqui em diante: cruzar o
controle com a permissão que a tela consulta não prova nada: ela prova que a tela é coerente
consigo mesma. A pergunta é qual permissão o SERVIDOR exige naquela rota, e o defeito vive
exatamente onde as duas divergem. Uma varredura que compara a tela com a tela é cega **por
construção** para a família inteira deste cartão.
Achado colateral, medido: existe uma permissão de criar cliente declarada nos papéis e com **zero
referências** no servidor: a tela usa outra no lugar dela. Peça declarada e nunca acionada, a
família de sempre.
Como verificar (teste primeiro): um ator com compor e sem publicar não vê os controles de
publicar e de salvar como template; um com a permissão vê e usa.
Aceite (H1): Dado um operador sem a permissão de publicar, Quando ele abre um relatório no
compositor, Então os controles de publicar e de salvar como template não lhe são oferecidos, E ele
não descobre a recusa depois de montar o documento.
FECHADO em 2026-08-18, começando pela pergunta que o cartão manda - *quais decisões ainda vêm
de campo escrito e quantas removem algo da conta* - em vez de recomeçar pelo último buraco.
Medido: 84 entradas, 28 saem da conta por tipo, e ehDivergente começa literalmente com
if (cls.tipo !== "acao") return false.
A regra que entrou é ASSIMÉTRICA, e a assimetria é o ponto: declarar "leitura" para um verbo
de escrita é proibido - é a única direção que esconde; declarar "acao" para um GET continua
permitido, com motivo escrito e entrada nomeada, porque essa direção infla o placar em vez de
encolher, e um instrumento que proíbe ser conservador empurra para o lado errado.
A fonte do verbo é o AST do cliente de API, não texto. Duas réguas por expressão regular
falharam antes, e ficam registradas porque o erro é instrutivo: casar por nome final deu **34
ambíguos** (list/get existem em dezenas de namespaces) e navegar texto contando chaves deu
62 indeterminados. Régua ruim produz número errado - o defeito que este cartão existe para
tirar daqui. Pelo AST: 84 de 84 resolvidos, zero erro, e o declarado bate em 82; as 2
exceções são a mesma entrada conservadora (comments.list).
H1 executado: falsifiquei api.companies.patch (PATCH) para "leitura" e a guarda reprovou
nomeando entrada e verbo. Restaurado.
Dívida minha que a fatia revelou, e quem pegou foi a guarda do {F8-49}: o
api.reports.agencyReports, criado por mim em {OP-01}, entrou no módulo **sem contrato de
permissão declarado e sem a home perguntar por ele** - ela ainda usava a forma anterior ao
{F8-51}. Agora a home pergunta pelo método, não dispara chamada que o servidor recusaria, e o
método tem sua linha no contrato com a mesma capability do handler. Os três placares literais
subiram com motivo no diff (96→98 varridas, 82/83→83/84 guardadas) e o CompanyScopeSwitcher
entrou na fronteira declarada, porque {OP-01} o trouxe para o chrome. Vivas continuam 0.
201 testes do módulo verdes, typecheck limpo, front deployado e conferido na tela.
Cartão novo, nascido em 2026-08-13 de sete rodadas de gates sobre o {F8-49}, e ele existe porque a
busca por consertos pontuais foi encerrada de propósito.
O que aconteceu, e o histórico é o achado: a verificação de permissão precisa marcar coisas para
decidir o que conta como divergência. Cada marcação que remove algo da conta virou um jeito de
apagar defeito real com a suíte verde. A sequência medida foi **lista vazia, depois "sem guarda",
depois "papel da tela", depois o recorte de ação e leitura, e cada nome nasceu dentro do conserto
do anterior. Seis vezes. Ao fim, era possível apagar 12 das 15 divergências vivas** trocando uma
palavra num campo.
O que NÃO está errado, e é importante não confundir: o número em si. As 36 divergências e as 15
vivas foram recomputadas por três medições independentes, com os invariantes fechando, e não
dependem dessas guardas. O que se pode falsificar é a **proteção contra alguém adulterar os campos
depois**.
Por que parou de valer a pena consertar caso a caso: fechar um caminho criava o seguinte. O
problema deixou de ser o campo e passou a ser o desenho: um instrumento que decide a partir de
campo escrito à mão sempre poderá ser convencido por quem escreve o campo.
O que a fatia entregou no lugar, e foi a decisão certa: os três caminhos conhecidos ficaram
presos por casos que afirmam a existência do buraco. Fechar qualquer um deles quebra o caso e
obriga a reescrever a declaração. Ninguém é enganado, porque os furos estão nomeados e presos por
máquina.
O que implementar aqui: tirar a decisão do campo escrito. O caminho provável é derivar a
classificação da mesma forma que a lista de chamadas já é derivada (do próprio código), de modo que
marcar não seja uma opção. Enquanto isso não existir, o número vale enquanto alguém revisar os campos, e
isso está declarado no topo do arquivo.
Cuidado ao começar: não recomece pelo último buraco. **Comece perguntando quais decisões daquele
instrumento ainda vêm de campo escrito** e quantas delas removem algo da conta. Foi essa pergunta,
feita tarde, que revelou o padrão.
Como verificar (teste primeiro): falsificar qualquer campo de classificação não consegue
reduzir a contagem de divergências vivas sem que algum caso reprove.
Aceite (H1): Dada a verificação de permissão, Quando alguém altera uma marcação de classificação
para esconder uma divergência, Então algum caso reprova, E a contagem de vivas não pode ser reduzida
por edição de campo.
FECHADO em 2026-08-18, 8 PASS / 0 FAIL contra o banco e as rotas reais. Cada capability do
módulo tem um caso que chama a rota de verdade (reportsRoutes.request, passando pelo roteador
do Hono) com ator que não a tem, e afirma 403.
O ator é derivado em dois níveis, nunca escrito à mão (é o que o cartão manda reusar): o papel
sai de ROLE_DEFAULTS - o primeiro cujos padrões não incluem a capability -, com afirmação de que
existe um papel que tem (sem esse par, verde poderia significar só que ninguém a tem); e o
usuário sai do banco, por consulta, sem criar nem apagar nada. O conjunto de capabilities
também é derivado, e um caso exige que toda capability tenha rota: capability nova sem cobertura
reprova nomeada.
Três coisas que a construção ensinou, e as três viraram código:
(1) 401 e 403 são respostas diferentes e o teste as separa - a primeira rodada deu 401
(AUTH_ACCESS_REVOKED, o middleware exige sub existente e ativo), e sem essa distinção eu teria
contado a recusa do gate de sessão como se fosse a de permissão.
(2) a ordem dos gates é entitlement → capability: a segunda rodada deu 404
(MODULE_NOT_ENABLED) porque o ator era de agência sem o módulo; passar do primeiro é condição
para medir o segundo.
(3) quando o teste acusou GET /settings/identity respondendo 200 para workforce, **o
defeito era do mapa do próprio teste**, não do servidor - o contrato já declara aquela leitura
como aberta ao tenant, e quem exige a capability é o PUT. Um instrumento que só sabe acusar o
servidor nunca teria revelado isso.
Papel sem ator ativo em agência com o módulo ligado faz o caso ser pulado dizendo por quê -
nunca contado como aprovado. Merge 291e7cd.
Cartão novo, do limite que o {F8-49} declarou ao entregar (2026-08-13). Aquela fatia provou a
metade da tela: quem não tem a permissão não vê o controle. A metade do servidor, que ele
de fato recusa, foi estabelecida lendo o guarda, não exercitando a rota com um ator revogado.
O que foi medido e é o vazio: as suítes das rotas do módulo tocam só o caminho positivo.
Nenhuma prova que a resposta é recusa quando quem chama não tem a permissão.
Por que importa mesmo com a tela consertada: esconder o botão é conveniência, não segurança. A
rota continua exposta a quem a chame direto, e é o servidor que precisa recusar. Se algum dia a
guarda do servidor cair, nada acusa: a tela seguiria escondendo o botão e o produto pareceria
correto.
É a mesma família que este projeto já nomeou: caminho negativo nunca exercitado é
indistinguível de caminho negativo quebrado.
O que implementar: para cada rota do módulo que exige permissão, um caso que a chame com ator
sem ela e afirme a recusa. A armadilha já está resolvida no {F8-49}: o ator sem permissão se
deriva dos padrões do papel, com afirmação de que ele realmente a tinha; reuse essa forma, não
escreva ator à mão.
Como verificar (teste primeiro): remover a guarda de uma rota faz o caso reprovar; com a guarda,
passa.
Aceite (H1): Dada uma rota do módulo que exige permissão, Quando ela é chamada por ator sem essa
permissão, Então a resposta é recusa, E existe caso que reprova se a guarda for removida.
FECHADO em 2026-08-13, mergeado (1cc0e0b). Fatia só de teste: nada de produção mudou, e o valor
dela é a medição que o {F8-51} agora usa.
O placar, sobrevivente de cinco recomputações independentes: 88 chamadas varridas, 83 guardadas,
55 ações, 36 divergências, 15 vivas, 21 latentes.
Custou dez rodadas, e o histórico do número é a lição: 5, 32, 34, 39, 36. As subidas vieram de o
instrumento parar de esconder; a descida, de ele parar de inventar. Errou nos dois sentidos,
sempre pela mesma causa: premissa lida e não medida.
O defeito que reapareceu seis vezes com nome novo: toda marcação que remove algo da conta
virou um jeito de apagar defeito real com a suíte verde. Lista vazia, depois "sem guarda", depois
"papel da tela", depois o recorte de ação e leitura, e cada nome nasceu **dentro do conserto do
anterior. Em certo ponto era possível apagar 12 das 15 vivas** trocando uma palavra.
Onde a busca parou, e por quê: fechar um caminho criava o seguinte, então o problema deixou de ser
o campo e passou a ser o desenho. Os caminhos restantes ficaram **presos por casos que afirmam o
buraco: fechar qualquer um quebra** o caso e obriga a reescrever a declaração. O conserto
estrutural é o {F8-61}.
A armadilha mais fina, e ela quase passou: a primeira tentativa de prender os buracos exercitava
cópias do critério. Fechando os buracos de verdade, a suíte seguia verde, e a nota que
prometia proteção virou mentira. O teste que pegou isso: feche o buraco e exija que o caso quebre.
E o fecho não foi por código, foi por HONESTIDADE: a última reprova foram duas frases que
anunciavam cobertura inexistente, dentro de testes verdes, que é onde ninguém reabre para conferir.
O ganho concreto além do número: a delegação virou classe, derrubando o vão de 57 para **7 rotas
em 713**, e as 7 restantes são leitura pública ou interna.
O enunciado original do cartão, preservado abaixo: Cartão novo, escrito em 2026-08-12 a partir de uma admissão do próprio QA do {F8-46}, e a lição é de método, não de código. O que aconteceu: o QA exercitou o fluxo inteiro como super administrador, o perfil que tem todas as permissões e, por definição, nunca é recusado. Passou em tudo. O review então achou um defeito que vivia exatamente naquele buraco: uma tela oferecia ação a quem o servidor recusaria, e só aparece para quem não tem a permissão. A regra da casa que se aplica, já registrada em outro contexto: caminho negativo verde mascara o positivo morto. Aqui é o inverso e igualmente cego: o sujeito que sempre passa não prova nada sobre quem não passa. Por que vai repetir: a conta de serviço é a mais cômoda de usar, está documentada, e faz o smoke andar rápido. Ninguém erra por preguiça: erra porque o caminho fácil é o caminho errado, e nada no processo avisa. Um agravante medido na mesma fatia: naquela tela, captura de imagem não distingue o botão clicável do texto informativo: mesma cor, mesmo peso, mesmo tamanho. Ou seja, mesmo quem tentasse validar o caso negativo por imagem obteria uma prova vazia. Tem que ser pela estrutura da página ou pela resposta real do servidor. O que implementar: toda verificação de tela que dependa de permissão passa a exercitar os dois sujeitos, um com e um sem, e a prova do caso negativo não pode ser imagem. Onde isso vive é decisão da fatia: um teste em formato de matriz por permissão é o caminho mais barato e já existe precedente no produto. Cuidado ao construir: criar ator sem permissão exige registro explícito de revogação: um papel que já tem a permissão por padrão continua tendo se ninguém a revogar de fato. Foi assim que uma prova anterior se desfez ao ser medida: o usuário de teste nunca esteve sem a permissão. Como verificar (teste primeiro): um ator sem a permissão não vê a ação e recebe recusa do servidor; um ator com ela vê e usa. Os dois no mesmo teste, e o primeiro precisa falhar se a guarda for removida. Aceite (H1): Dada uma tela cuja ação depende de permissão, Quando a verificação roda, Então ela exercita um sujeito com e um sem a permissão, E a prova do caso negativo vem da estrutura ou da resposta do servidor, nunca de imagem.
Cartão novo, medido em 2026-08-12 durante a reverificação do {F8-46}. O gate de QA daquela fatia
estava com zero bytes, e o relato do próprio QA afirmava "gravado e relido do disco". A releitura
não aconteceu, ou não verificou nada.
NÚMERO CORRIGIDO, e o erro foi meu, do mesmo tipo que este projeto já pagou hoje. A primeira
redação dizia três arquivos vazios: eu contei o que apareceu no fim de uma listagem, e li a
amostra como se fosse o universo. Medido depois, sem limite: **994 gates no total, 121 com zero
bytes, e (o número que importa) 76 deles em qa/ e review/, que são exatamente os que
autorizam merge**. A obra achou 123 pelo critério mais forte (pega também arquivos de 2 e 5 bytes,
com só quebra de linha ou marca de codificação), mais 21 no checkout de produção.
Ou seja: quase quarenta por cento dos gates de QA e de review deste projeto não registram nada,
e cada um deles carimbou uma entrega.
Por que isto é pior que gate ausente: gate ausente bloqueia o merge, porque falta. Gate vazio
passa: o arquivo existe, o nome bate, e ninguém abre. Ele autoriza a entrega **sem deixar
registro de o que foi verificado**, e o histórico do projeto fica com um carimbo que não carimba
nada.
A causa é a mesma que este projeto já nomeou várias vezes: a verificação é **declaração de quem
escreve** ("reli o arquivo"), não medição. Todo pedido de gate desta sessão passou a incluir "releia
depois de escrever", e mesmo assim um saiu vazio, porque a instrução depende de disciplina, não de
construção.
O que implementar: o gate passa a ser verificado por construção: arquivo com zero bytes, ou sem
veredito legível, não conta como gate. Onde essa verificação vive é decisão da fatia: no fluxo
que consome os gates antes de mergear, ou numa varredura que roda no comando único da raiz e acusa
qualquer gate vazio no diretório.
Prefira a segunda ou as duas: a primeira protege daqui para frente; a segunda **acha os que já
estão lá**, que é como este defeito foi descoberto das duas vezes.
Junto, e é trabalho de arqueologia curto: os três arquivos vazios de hoje e de maio precisam ser
reconstituídos ou removidos com registro do motivo. Gate vazio no histórico é pior que ausência,
porque parece prova.
Como verificar (teste primeiro): um gate com zero bytes é acusado; um gate com veredito passa; e
a varredura encontra os que já existem no diretório hoje.
Aceite (H1): Dado um arquivo de gate vazio ou sem veredito, Quando a verificação roda, Então ele
é acusado e não conta como aprovação, E os gates vazios já existentes no repositório são listados.
FECHADO em 2026-08-13, mergeado (fd26508). QA PASS e review APPROVED, ambos em duas rodadas.
A verificação roda no comando único, aborta antes do resto, e o critério é mais forte que "zero
bytes": pega arquivo só com espaço, só com quebra de linha, só com marca de codificação, e texto
sem veredito legível. Zero falso positivo medido em 922 gates legítimos, por três partes.
O furo que o QA achou, e é o cartão em miniatura: um gate dizendo **"em andamento, ainda não
verifiquei nada" passava, porque o rótulo de estado valia como veredito. A guarda que exige
veredito aceitava a declaração explícita de que nada foi verificado.** Corrigido de forma estreita
(os rótulos deixaram de valer sozinhos, nenhuma palavra de desfecho foi tocada), com custo medido em
zero antes do commit.
O Major do review, e o diagnóstico honesto: a varredura não alcançava o workspace, que é
justamente onde os gates deste loop são escritos. A obra mediu primeiro e achou que o workspace
não existe na produção: sem montagem, sem cópia. Ou seja, "descobrir melhor" era impossível.
A solução foi declarar o endereço por máquina e anunciar a cobertura.
E aí veio o menor mais irônico da fatia: o anúncio **se desligava quando alguém declarava uma
raiz**: a guarda que torna a rodada honesta se calava exatamente na configuração corrigida. Virou
tabela de contrato: uma linha por coleção, sempre, com estado varrida ou não alcançada, e um
aviso de que o verde só vale para o que foi varrido. Declarar um caminho qualquer **não compra
cobertura que não existe**.
O merge conflitou com DUAS fatias de outras sessões, que tocaram os mesmos arquivos e numeraram
o mesmo passo. O método da resolução é a lição: resolver marcador a marcador teria perdido
trabalho alheio, porque os blocos são parecidos e foram entrelaçados. Partiu-se da main inteira
e inseriu-se o bloco novo: assim nenhuma linha alheia pode sair, por construção. Medido depois:
54 adições, zero remoções. Hoje três guardas convivem no comando único.
FECHADO em 2026-08-13, mergeado (56570e2) e em produção. Decisão tomada sem devolver a escolha
ao dono: o cartão já recomendava avisar dentro do produto, sem canal externo, e foi isso,
reusando o mesmo canal que já avisava o autor na resolução. Zero tabela, zero serviço, zero e-mail.
O defeito que a obra quase cometeu, e é o motivo de terem sido duas rodadas: o critério de quem
recebe reusava a função de permissões que as rotas usam, mas reimplementava as entradas dela:
filtrava os grants por agência (quando a revogação é arquivada por usuário) e lia o papel da coluna
do perfil (quando o guarda lê o papel do vínculo, com o modo multi-agência ligado em produção).
Resultado medido: o aviso incluía quem o servidor recusa, alcançável por 9 usuários com vínculo
fora da própria agência. Era exatamente o defeito do {F8-46}, cometido dentro do conserto dele.
E o controle negativo não podia ter pegado: arquivava a revogação sob a agência **do próprio
relatório, que é justamente o caso que o filtro defeituoso enxerga. Ele nunca poderia falhar.**
A rodada 2 refez o par num ramo que não tinha um único caso, e incluiu o gêmeo sem a revogação:
sem ele, o caso mediria o vínculo em vez da revogação. A validação removeu o gêmeo para conferir: a
asserção fica verde pelo motivo errado. Peça que sustenta peso.
Provado na tela, não só no banco: comentário real de um contato real gerou 5 avisos, um por
operador e nenhum para o autor; o sino subiu, o aviso abriu, o link levou ao compositor com o
comentário à vista, e o contador caiu só para quem leu: os outros quatro seguiram não lidos.
Todo o dado de teste apagado nominalmente, com conferência de órfãos depois.
O que fica declarado como decisão, não como achado: super administrador sem vínculo com a
agência não recebe, para não receber comentário de todos os inquilinos. E o tipo de aviso é
obrigatório: o operador não consegue silenciar comentários.
Dois cartões nasceram daqui: {F8-53} (avisos apontando para rota inexistente) e a confirmação
medida de que o vermelho da suíte tinha causa real, não só contenção: ver {F8-56}.
O enunciado original do cartão, preservado abaixo: Cartão novo, do julgamento do QA no {F8-46} (2026-08-12), e ele é a metade que aquela fatia deliberadamente não cobriu. O que ficou resolvido: no compositor, o acesso aos comentários está sempre visível: quem abre o relatório para trabalhar encontra, sem depender de notar número nenhum. O que sobrou: nas outras duas telas, o número deixou de ser mudo e passou a levar a algum lugar, mas descobrir que existe comentário novo ainda depende de o operador visitar aquela tela e reparar. É o risco que o cartão anterior nomeou com todas as letras: contador que ninguém olha é o mesmo problema com outra roupa. Por que a fatia anterior parou aí, e a decisão foi correta: nenhum requisito do documento pede aviso ao operador: o único que trata de notificação manda avisar o autor quando o comentário é resolvido, e isso já existia. Construir canal novo teria sido escopo inventado. Então este cartão é decisão de produto antes de ser trabalho de código, e ela tem dois desfechos legítimos: Cenário 1, o operador vai atrás. Fica como está. O comentário espera na tela até alguém abrir o relatório. Serve quando a agência tem ritmo de revisão previsível: todo relatório é reaberto de qualquer jeito. O preço: um comentário urgente pode dormir dias, e quem escreveu não tem como saber disso. Cenário 2, o sistema avisa. O operador recebe sinal quando chega comentário novo. Elimina a espera invisível. O preço: mais um canal para manter, e a pergunta imediata de onde avisar (dentro do produto, por e-mail, ou num resumo periódico), porque aviso que vira ruído é ignorado, e aí volta ao Cenário 1 com custo extra. Recomendação: Cenário 2, dentro do produto, sem canal externo. O cliente escreveu esperando resposta; a assimetria de ele não saber se foi lido é o que o {F8-46} começou a corrigir. Aviso dentro do produto não cria canal novo de infraestrutura e some quando lido. Como verificar (teste primeiro): um comentário deixado por um cliente chega ao operador sem que ele precise abrir aquele relatório; e um comentário já lido não continua sinalizando. Aceite (H1): Dado um comentário novo deixado por um cliente, Quando o operador usa o produto, Então ele fica sabendo sem depender de abrir o relatório específico, E o sinal desaparece depois de lido.
Cartão novo, confirmado em 2026-08-11 pelas duas partes que validaram o {F8-17}, de forma
independente: as rotas que listam e que marcam como resolvido um comentário de cliente **não
têm chamador nenhum**: nem na tela do operador, nem em serviço nenhum.
O que isso significa no produto, e não é detalhe técnico: o cliente abre o relatório, escreve um
comentário, e ele chega ao banco. O operador vê apenas um contador agregado na tela de
análise. Não há onde ler o que foi dito, nem como responder ou dar por resolvido.
É a pior forma da família "peça pronta e inalcançável", porque desta vez quem fica pendurado é o
cliente final: ele escreve achando que alguém vai ler, e do outro lado não existe leitura
possível. O sistema promete uma conversa que não pode acontecer.
A metade que existe é a mais difícil: receber, guardar, controlar acesso e contar já está pronto
e em produção. Falta a tela.
Ao construir, decidir explicitamente: o operador é avisado de comentário novo, ou precisa
entrar para descobrir? Contador que ninguém olha é o mesmo problema com outra roupa.
Como verificar (teste primeiro): um comentário deixado por um cliente aparece para o operador com
o texto, o autor e a data; ele consegue marcar como resolvido; e o que foi resolvido deixa de pesar
como pendência.
Aceite (H1): Dado um comentário deixado por um cliente no relatório publicado, Quando o operador
abre o produto, Então ele lê o comentário e sabe quem escreveu e quando, E consegue marcá-lo como
resolvido, E não depende de contador agregado para descobrir que existe.
FECHADO em 2026-08-12, mergeado (d843dc3) e deployado. QA PASS e review APPROVED, este na
terceira rodada. Um componente só, reusado em três telas, consumindo as rotas que **já existiam e
estavam sem chamador**: nada novo no servidor.
O crítico que o review pegou, e é a família que esta sessão já pagou: a tela de análise exigia
permissão mais fraca que as rotas de comentário, então quem tinha acesso à análise **via o botão
e levava recusa silenciosa**. Oferecer ação que o servidor vai negar gasta o tempo da pessoa e falha
depois.
A rodada mais instrutiva foi a terceira, e ela não era sobre o código: era sobre a PROVA. O
conserto estava certo desde a segunda rodada, confirmado por construção, comparando a condição da
tela com o que o servidor exige nos dois métodos. O que não se sustentou foi a evidência. O
review mediu e desmontou: o usuário de teste era de um papel que **já tinha a permissão por
padrão**, não havia registro de revogação no dia, e a metade positiva não tinha artefato.
E o ponto mais fino veio do próprio autor, ao admitir: aquela tela **não pode ser provada por
imagem, e não por limitação de ferramenta, mas por decisão de produto**. O texto que aparece
para quem não tem a permissão é deliberadamente idêntico ao botão em cor, peso e tamanho, porque o
número não deve sumir. Duas capturas dos dois casos saem iguais.
O que substituiu a prova: uma matriz de quatro atores contra o elemento produzido, com
controle negativo nos dois sentidos: o defeito original (sempre oferece) e o inverso (nunca
oferece, nem a quem pode) reprovam três casos cada. O review recontou linha a linha e confirmou que
o sentido "guarda severa demais" está coberto, que é o que costuma escapar.
Ganho colateral: a expressão de permissão estava copiada em cinco telas. Virou fonte única,
com zero cópias residuais confirmadas por varredura, e o escopo declarado no próprio arquivo.
Dois cartões nasceram de falhas do PROCESSO de verificação, não do produto: {F8-48} (o gate de QA
saiu com zero bytes enquanto o relato afirmava tê-lo relido, e há outros dois assim, de maio,
nunca notados) e {F8-49} (o QA exercitou tudo como super administrador, **o perfil que nunca é
recusado**, e por isso não podia achar este defeito). O terceiro é {F8-47}, a metade que a fatia
deliberadamente não cobriu: o operador ainda depende de visitar a tela para descobrir que há
comentário novo.
**Cartão novo, e ele não nasce de um caso: nasce de a mesma coisa ter aparecido cinco vezes em duas
varreduras (2026-08-05). Backend pronto, testado, mergeado, com o cartão fechado, e nada no
painel o chama**. Já apareceram assim: a base sazonal do cliente (coluna e rota prontas, zero
referência na tela), o modelo de venda do cliente (idêntico, e o cartão que o entregou está [x]),
a visibilidade de métrica por cliente (três rotas prontas, zero referência na tela), o teto de
verbosidade por bloco, e os dois tetos de tempo da composição do {F8-16}. Consertar os cinco um a
um é o erro conhecido da casa: **guarda que percorre lista curada só protege o que alguém lembrou
de listar**, e a sexta ocorrência entra em silêncio com o placar verde.
RNF-MANUT-003, e o critério de aceite de todo cartão deste roadmap (artefato só fecha se existe
E é alcançável).
O que implementar: uma varredura exaustiva por construção, que enumere toda rota do módulo
e toda função exportada do pacote e confronte com quem de fato as aciona no caminho que uma pessoa
percorre, produzindo a lista das órfãs. Ela reprova quando aparece órfã nova, e o legítimo se anota
com o motivo, para a rodada seguinte mostrar só o que é novo, detector barulhento vira
catraca, não gate. Depois, ligar as órfãs achadas ou reabrir os cartões que as declararam
entregues; e cada cartão reaberto diz qual metade existia.
Como verificar (teste primeiro): acrescentar uma rota que ninguém chama e confirmar que a
varredura REPROVA; anotar uma órfã legítima com motivo e confirmar que a rodada seguinte não a
mostra mais; e conferir que as cinco já conhecidas aparecem na primeira execução.
Aceite (H1): Dado o conjunto de rotas e funções exportadas do módulo, Quando a varredura roda,
Então toda peça sem quem a acione em produção é listada, E a lista das cinco já conhecidas aparece
nominalmente, E uma peça órfã nova reprova a integração contínua, E órfã anotada com motivo não
reaparece como ruído.
FECHADO em 2026-08-12, mergeado (4428ef6). QA PASS e review APPROVED, ambos na segunda rodada.
Este cartão é o melhor argumento a favor dos gates que esta sessão produziu. A primeira entrega
afirmou 179 peças órfãs. Os dois gates reprovaram de forma independente, cada um amostrando e
verificando item a item: o review achou 5 falso-positivos em 7 rotas, o QA achou 7 em 13. E o
pior: uma rota ativa em produção, documentada no manual do projeto como servindo tráfego real,
entrou na lista com o motivo escrito "nenhum chamador real foi encontrado". A lista não era só
ruidosa: afirmava falsidade com aparência de fato apurado, e quem a seguisse teria quebrado a
página de análise.
Sete causas de extração de texto, não três. O review nomeou três, o QA achou uma quarta
(prefixo de host), e o autor achou mais três ao consertar, incluindo o fallback **emprestando o
chamador de um método para outro no mesmo caminho**.
A lição que fica, e ela estava escrita no próprio arquivo: o autor já havia caçado essa exata
classe de problema para um delimitador, com varredura por estado, e **não aplicou a mesma disciplina
ao outro**. A rede de segurança anunciada não funcionava. Análise por expressão regular sobre código
real quebra nos casos que importam. O conserto foi trocar por varredura de estado, **na raiz, sem
exceções pontuais**.
Placar final: 24 rotas órfãs viraram 9, e o QA verificou todas as nove, não uma amostra,
trazendo a main (35 commits à frente) para dentro antes de medir. A metade dos exports do pacote foi
validada como sã pelos dois gates desde a primeira rodada.
O achado de produto que a varredura revelou virou {F8-46}: o cliente comenta no relatório
publicado, o comentário chega ao banco, e o operador não tem tela para ler nem responder: só um
contador agregado.
Nota de honestidade registrada pelo QA: uma rota citada no review antigo como exemplo de
falso-positivo é, hoje, organicamente órfã: três medições independentes concordam. Fica escrito
para quem ler o histórico não repetir a alegação antiga sem reconferir.
Cartão novo, achado de lado durante a contagem da {F4-07} em 2026-08-05. O teto de tempo por
bloco e o piso e teto de tempo por documento existem como valores no pacote, **e as funções que os
aplicam não são chamadas em lugar nenhum fora do próprio módulo**. O sintoma esperado disso é que a
composição de IA roda hoje sem limite de tempo: um provedor lento não é cortado, o operador fica
esperando, e o requisito de desempenho que promete uma janela de dez a trinta segundos não tem
quem o faça valer.
RNF-PERF-002. Este cartão é risco operacional, não configuração, por isso saiu de dentro da
{F4-07} em vez de virar mais uma linha de parâmetro sem painel.
MEDIR ANTES DE CONSTRUIR: a primeira tarefa é confirmar o sintoma no caminho REAL de
composição, não no pacote isolado. Pode existir um teto em outro lugar (no cliente de IA, no
servidor de aplicação, no proxy) que ninguém lembrou de contar, e nesse caso o defeito é a peça
duplicada, não a ausência. Confirme com uma composição real que demore.
O que implementar (se o sintoma se confirmar): o caminho real de composição passa a aplicar o
teto por bloco e o teto por documento, com o comportamento de estouro sendo o mesmo fail-loud que
a cadeia de IA já usa quando falha: nada de narrativa pela metade publicada como se fosse inteira.
MEDIÇÃO FEITA EM 2026-08-10, e ela reescreve o que construir. O cartão mandava medir antes de
construir, prevendo que "pode existir um teto em outro lugar que ninguém lembrou de contar". **Havia,
e é pior do que não haver.**
1. As peças de teto não têm chamador nenhum. Nem no pacote fora do próprio arquivo, nem no
programa do leitor, nem na API. Só o barril as exporta, e **o comentário do barril afirma que a
API as usa**. Mais uma afirmação de código que descreve intenção como se fosse fato.
2. O cliente de IA não tem teto próprio. Nenhum sinal de aborto, nenhum tempo limite.
3. O proxy corta em 60 segundos. A composição é síncrona sobre uma rota servida atrás do
proxy, cuja leitura está limitada a 60 segundos, enquanto o bloco vizinho, de outro serviço, usa
900. Ou seja, o teto existe, mas na camada errada.
Por que o teto do proxy é pior que teto nenhum, e este é o achado que importa:
- Ele derruba a conexão, não o trabalho. O operador recebe um erro de gateway, e a composição
continua rodando no servidor e gravando. É exatamente o desfecho que o aceite proíbe: narrativa
pela metade tratada como inteira, agora sem ninguém olhando.
- Ele não é uma falha nomeada. O operador não vê "estourou o tempo", vê o erro genérico do
proxy, indistinguível de o serviço estar fora do ar.
- Ele torna o orçamento desenhado inalcançável. O piso por documento é de 60 segundos e a
referência é de 6 por bloco, então qualquer documento acima do piso seria cortado pelo proxy
antes de o teto do próprio produto valer. O orçamento que o pacote calcula nunca chega a decidir
nada.
Consequência para o que implementar: não basta ligar as peças ao caminho real. É preciso que o
teto da aplicação seja menor que o do proxy, ou que a composição deixe de depender de uma
conexão aberta. Ligar o orçamento sem tratar a camada de baixo entrega um teto que nunca dispara,
com o placar verde.
Como verificar (teste primeiro): uma composição forçada a demorar além do teto é interrompida
e o operador vê a falha nomeada, em vez de esperar indefinidamente; e uma composição normal não é
afetada. Além disso, e por causa da medição acima: provar que o corte veio da aplicação e não
do proxy, e que nada continuou gravando depois do corte.
FECHADO em 2026-08-10, mergeado (2cb3a94 no pacote, 4e4982a no Hub) e deployado. QA PASS e
review APPROVED. O teto passou a ser da aplicação, fixado abaixo do teto do proxy, com falha nomeada
no canal de falha que já existia, nenhum canal novo. O disparador agendado passa pelo mesmo caminho,
então não sobrou metade sem teto.
A prova mais difícil do aceite foi feita rodando, não afirmando: forçaram a chamada a demorar 53
segundos, mediram o retorno em ~50, esperaram a resposta tardia chegar de verdade e releram o
banco antes e depois: bytes idênticos. Nada gravou depois do corte, que era o desfecho proibido.
A pergunta que decidia se isto podia ir ao ar foi respondida com número. Eu tinha levantado o
risco de o teto impedir documento real de existir, com base na fórmula de orçamento (108 a 150
segundos para 18 a 25 blocos). O QA mediu o tempo real: 1,2 a 2,4 segundos, oito execuções.
A fórmula não era medição: a composição é uma chamada de IA só, e o número de blocos quase não
move o relógio. Ver {F8-37}, onde a correção está registrada junto do que sobrou de risco de verdade.
Dívida declarada, não escondida: o teto da aplicação só funciona porque é menor que o do proxy, e
esse vínculo é prosa nos dois lados, não verificação. Quem baixar o número do proxy reabre o
defeito sem que teste nenhum acuse. O review propôs tornar o valor operável sem reconstruir a
imagem; ficou para depois de propósito, porque a resposta do QA podia mudar o desenho inteiro, e
trabalho feito antes da resposta é trabalho jogado fora.
Aceite (H1): Dado uma composição que ultrapassa o teto de tempo declarado, Quando ela roda,
Então é interrompida no teto, E o operador recebe a falha explícita, E nenhuma narrativa parcial é
entregue como se estivesse completa.
Cartão novo, achado no planejamento do {F8-07} em 2026-08-08. O tipo de imagem vetorial está na
lista de tipos aceitos pelo envio de arquivo, e o leitor serve esse conteúdo embutido na página
pública. Imagem vetorial não é só imagem: ela carrega script. Um arquivo desses, enviado por quem
tem acesso ao painel, vira script armazenado executando na página que o cliente final abre, num
serviço que hoje ainda não tem criativo nenhum circulando.
RNF-SEG-001, RF-ING-001, RN-006.
Por que agora e não depois: hoje não há nenhum bloco de mídia em nenhum rascunho ou retrato de
produção, ou seja, a superfície é zero. No dia em que o {F8-07} ligar a inserção de criativos,
ela deixa de ser zero. Fechar isto antes é barato; depois, é incidente.
O que implementar: tirar o tipo vetorial da lista de aceitos, ou, se houver motivo real para
aceitá-lo, servi-lo de forma que não execute (nunca embutido) e sanitizá-lo na entrada com o mesmo
rigor do parse isolado. A primeira saída é a que o {F8-07} não precisa; escolha-a a menos que
alguém demonstre a necessidade.
Como verificar (teste primeiro): enviar um arquivo vetorial com script embutido e confirmar que
ele é recusado na entrada; e, se a decisão for aceitar, que a página pública o serve sem executar
nada.
Aceite (H1): Dado um arquivo de imagem vetorial contendo script, Quando alguém o envia, Então
ele é recusado na entrada, E nenhuma página pública serve conteúdo capaz de executar script vindo
de arquivo enviado.
MEDIÇÃO EM 2026-08-10, e ela DERRUBA a parte mais alarmante deste cartão. A redação acima afirma
que o arquivo "vira script armazenado executando na página que o cliente final abre". **Isso não é
verdade hoje**, e foi medido, não deduzido: o leitor aplica a política de segurança de conteúdo em
toda resposta, aplicada no início do tratador de requisição e portanto também na resposta do
proxy de imagem, com script-src sem unsafe-inline mais nosniff. Script dentro de arquivo
vetorial não executa na página pública, nem embutido nem aberto direto. O cartão nasceu de
leitura de código e superestimou a exposição.
O que a medição NÃO derruba, e é o que sustenta o cartão de pé:
1. A camada de armazenamento continua aceitando o tipo vetorial. Quem barra é uma rejeição
aplicada em um único ponto de chamada (a rota de bloco de mídia), com a decisão declarada no
próprio comentário como local e contida até este cartão fechar.
2. Guarda por ponto de chamada nomeado faz todo caminho novo nascer furado, em silêncio. Provado
no mesmo dia: a rota de logo do cliente ({F8-08} Fatia B) entrou como segundo caminho para o
mesmo armazenamento sem a rejeição, e ninguém decidiu isso: simplesmente não havia como
lembrar. Fechado ali por reuso da mesma constante, mas o terceiro caminho vai repetir.
3. A ponta do compositor não foi medida. A defesa que salva é do leitor; o mesmo arquivo é
desenhado também no preview do operador, que é outra origem, com outra política. Medir essa
ponta faz parte deste cartão, e é onde o risco pode estar de verdade.
Consequência para a prioridade: deixa de ser urgência de incidente e passa a ser dívida
estrutural. A saída continua sendo a mesma que o cartão já indicava (tirar o tipo da lista de
aceitos no armazenamento, e não em cada rota), porque é ela que troca inclusão nomeada por
construção.
FECHADO em 2026-08-18, e no ar. A recusa do tipo vetorial saiu das rotas e virou propriedade do armazenamento: image/svg+xml nao existe mais na tabela que define o que se aceita (lib/reports-asset-storage.ts), entao isAllowedAssetMime ja o recusa e todo caminho de escrita futuro herda a recusa sem que seu autor precise saber que ela existe, que era exatamente o furo do item 2 acima. A constante por chamador nomeado (ASSET_REJECTED_MIME) foi removida junto com os tres comentarios que ainda a descreviam; as duas rotas seguem respondendo o mesmo 415 REPORT_ASSET_MIME_NOT_ALLOWED (regressao provada: 40 PASS / 0 FAIL no teste do {F8-08b}). O tipo saiu tambem do mapa de extensoes, entao nem a leitura reconstroi o tipo ativo a partir de um arquivo herdado: arquivo com aquela extensao sairia como application/octet-stream, que com nosniff nao executa. Medido antes de remover: zero arquivo vetorial no volume de producao, entao nada existente deixou de ser servido. A ponta do compositor, o item 3 que o cartao dizia nao ter sido medido, foi medida, e ela desmente a suposicao confortavel: o painel nao manda politica de seguranca de conteudo nenhuma (hub.modulareasy.com responde so nosniff), ao contrario do leitor publico. A defesa que salvava a pagina do cliente nunca existiu do lado do operador, que e justamente a origem que carrega a sessao dele. Por isso as duas rotas que servem o byte (a do painel e a interna) passaram a manda-lo inerte por si: default-src 'none'; sandbox mais nosniff. E defesa em profundidade: o armazenamento ja nao aceita tipo ativo, e isto cobre o dia em que alguem aceitar um de novo. O caso que reprova sem o fix nao passa por nenhuma das duas rotas: e um chamador que grava direto no armazenamento, o formato exato do terceiro caminho que nascia furado. Verificado devolvendo o tipo a tabela: 3 casos reprovam, e o (3) grava de verdade o documento com script. Com a guarda, 5 PASS / 0 FAIL. Ha controle positivo (os 4 tipos raster seguem aceitos) e controle negativo (o mesmo chamador direto grava um PNG), entao verde nao pode significar "recusa tudo" nem "o ambiente nao deixou gravar". Provado em producao, nao no teste: imagem reconstruida, mhub-api recriado (62 conteineres antes e depois), e uma requisicao real a rota interna devolveu 200 com content-security-policy: default-src 'none'; sandbox e x-content-type-options: nosniff. Fixture nominal criada so para essa medicao e removida na sequencia (zero arquivo restante). Merge 657d8d0.
Cartão novo, achado em 2026-08-13 pela obra do {F8-20}, que foi contar arquivos em produção para
dimensionar outro problema e esbarrou neste.
CUIDADO AO REMEDIR, e isto é parte do cartão: a contagem de linhas da tabela é agregado móvel:
a revisão mediu 0, 2, 4 e 0 no intervalo de um minuto, porque há ingestão e expurgo vivos. **Quem
remedir no minuto errado fecha este cartão por engano. A prova que vale é por identificador**: os
8 arquivos do disco não têm linha correspondente, e foi assim que a revisão confirmou.
O que foi medido, em produção: 8 arquivos de artefato no disco sem nenhuma linha correspondente
na tabela que os indexa. A rotina que expira artefatos por tempo de vida **registrou os identificadores
como expurgados** e os bytes ficaram. O registro diz que o trabalho foi feito; o disco diz que não.
É a mesma família do {F8-20}, e é por isso que ele nasceu aqui: processo que **relata sucesso sem
ter feito o trabalho**. A diferença é que lá o alcance nunca existiu, e aqui ele existiu pela metade,
o que é pior, porque a auditoria registra conclusão.
Consequência que já está de pé: qualquer varredura guiada pela tabela não acha nenhum dos 8.
A eliminação de dados do {F8-20} só os alcança porque foi construída para varrer o diretório, e
não a tabela, decisão que nasceu justamente desta medição.
O que implementar: a expiração apaga os bytes, e relata nominalmente o que apagou, para
"não havia nada" continuar distinguível de "não olhei". Os 8 já existentes precisam de destino
decidido, e apagar arquivo de produção é decisão do dono, não de quem implementa: meça, liste, e
traga.
Meça a causa antes de consertar: o expurgo falha e mesmo assim carimba, ou nunca tentou tocar o
disco? São consertos diferentes, e o segundo tem irmãos em outras rotinas.
Como verificar (teste primeiro): um artefato que passa do tempo de vida some do disco, e o
relatório da expiração o nomeia; e um artefato dentro do prazo continua intacto.
Aceite (H1): Dado um artefato que passou do tempo de vida, Quando a expiração roda, Então o
arquivo é removido do armazenamento, E o resultado declara o que foi removido, E nenhuma linha é
carimbada como expurgada sem que os bytes tenham saído.
FECHADO em 2026-08-18, e no ar. O cartao mandava medir a causa antes de consertar, e a medicao derrubou a hipotese que ele mesmo carregava. Ele supunha "o expurgo falha e mesmo assim carimba". Nao e isso: purgeExpiredArtifacts empurrava para a lista de purgados tudo que nao lancasse, inclusive o {deleted:false} com que o proprio adaptador diz "nao havia byte nenhum aqui". Um desfecho que significa nao fiz nada era contado igual a um que significa removi. Nao havia falha para relatar: havia uma ausencia sendo lida como sucesso. O conserto: tres desfechos, contados separados. removed (o byte saiu do armazenamento nesta passagem, o unico que significa trabalho), absent (nao havia byte: id ja purgado antes, ou artefato cujo parse falhou e por isso nunca chegou a ser escrito) e failed (a tentativa lancou, e a linha nao sai, para a proxima passagem tentar de novo). A linha do absent continua saindo de proposito: reter faria a varredura tentar para sempre sobre um byte que nao existe. O que o cartao exige nao e reter a linha, e o relato parar de mentir. A auditoria por agencia passou a nomear os dois desfechos (removed_artifact_ids e absent_artifact_ids), mantendo artifact_ids para nao quebrar leitor antigo. Era exatamente essa indistinguibilidade que deixou 8 arquivos orfaos com a auditoria afirmando trabalho feito. Provado em producao, com a varredura real: fixture nominal de dois artefatos vencidos, um com byte no disco e um sem. O relato saiu 2 candidato(s), 1 removido(s) do disco, 1 sem byte no disco, 0 falha(s), com o identificador do ausente nomeado no aviso. Antes, essa mesma passagem teria dito "2 purgados". O byte do primeiro saiu do disco, as duas linhas sairam da tabela, e o registro de auditoria gravou removed_count: 1 e absent_count: 1 com os identificadores. Fixture removida por completo (zero arquivo, zero linha restante). 6 casos no pacote, e o que reprova sem o fix e o par assimetrico. Ha os dois controles espelhados (todos com byte, nenhum com byte), entao verde nao pode significar "classifica tudo como ausente" nem "classifica tudo como removido". Verificado devolvendo o arquivo ao estado anterior: 0 PASS / 6 FAIL. Regressao medida contra main com a suite inteira: 5 falhas identicas nos dois lados (pre-existentes, de ambiente) e uma instavel sob carga que, isolada, passa nos dois lados em 3 rodadas. Zero regressao introduzida. ACHADO NOVO, e o cartao nao o previa: ha um SEGUNDO caminho que produz orfao. A chave estrangeira de reports.artifact e ON DELETE CASCADE sobre reports.report. Apagar um relatorio leva as linhas de artefato sem tocar em byte nenhum, e nenhuma correcao na rotina de expiracao alcanca isso, porque a rotina nunca chega a ver essas linhas. Medido por identificador nos 8 orfaos: 6 vieram do expurgo (relatorio vivo, linha apagada, que e o defeito corrigido aqui) e 2 vieram do cascade (relatorio ja apagado). E a razao de o {F8-20} ter nascido varrendo diretorio e nao tabela. DECISAO DO DONO, como o cartao determina: os 8 arquivos seguem no disco, somando 52 KB, todos de 05 a 09/08. Apagar arquivo de producao nao e decisao de quem implementa. Lista nominal: 5ed3f61e 8 KB, 614eeda4 8 KB, c3b763fb 8 KB, c9d92a51 8 KB, c68b3f9a 4 KB, fe92b019 4 KB (expurgo); 2695d227 8 KB, 1f16cabf 4 KB (cascade). Aritmetica que fecha: 24 arquivos no disco menos 16 linhas vivas igual a 8 orfaos. Merges ad22a52 (pacote) e 2e24f86 (Hub).
Cartão novo, achado pela revisão do {F8-20} em 2026-08-13, ao procurar o quinto caso perigoso da
eliminação de dados.
O cenário: um arquivo da agência reusado por vários clientes, cujo veículo natural é o modelo de
relatório. Se a eliminação de um cliente alcançasse esse arquivo, ela apagaria o byte de todos, e
um relatório novo de terceiro passaria a exibir "removida a pedido de eliminação de dados" no
lugar de uma imagem que ninguém pediu para remover. Dano silencioso, cruzando clientes, e
irreversível.
CORREÇÃO IMPORTANTE, de 2026-08-13: isto NÃO é risco futuro, ACONTECE. A primeira versão deste
cartão dizia que duas guardas fora da fatia fechavam o caso. Estava errado, e o erro é instrutivo:
a revisão chegou a essa conclusão lendo as duas guardas; a validação executou e mediu o
arquivo do modelo morrendo ao eliminar uma conta. Quem rodou vence quem leu, e esta é a segunda
vez na mesma obra que essa regra decide.
E o dano medido é maior do que o previsto: a lápide que substitui a imagem é por agência,
então todo relatório futuro de outros clientes passa a exibir o aviso de eliminação de dados
por causa de um pedido que não era deles. O dano cruza clientes e alcança quem ainda nem existia
quando o pedido foi feito.
Por isso este cartão passa a ser bloqueante da fatia de eliminação, não sucessor dela.
O que implementar: tornar a proteção deliberada. Ou a eliminação passa a reconhecer arquivo
compartilhado entre clientes e recusa apagá-lo (relatando, nunca em silêncio), ou o caminho que
poderia criar esse compartilhamento ganha guarda própria com o motivo escrito. **O que não vale é
seguir confiando nas duas coincidências.**
Como verificar (teste primeiro): montar deliberadamente o compartilhamento (habilitando o tipo
de bloco no modelo) e confirmar que a eliminação de um cliente não apaga o byte dos outros. Sem
esse teste, a próxima pessoa que mexer nas duas guardas não terá como saber o que quebrou.
Aceite (H1): Dado um arquivo referenciado por mais de um cliente, Quando a eliminação de dados de
um deles roda, Então o arquivo não é removido enquanto houver outro cliente referenciando-o, E a
decisão é relatada, E existe teste que reprova se a proteção sair.
FECHADO em 2026-08-18, e no ar. O cartao dava duas saidas, e a implementada e a segunda: o caminho que CRIA o compartilhamento ganhou guarda propria, com o motivo escrito. E ela que nao depende da fatia de eliminacao (que segue em branch de outra frente, nao mergeada): fecha a porta antes que o dano tenha por onde acontecer, que e o que "bloqueante da fatia" significa. Dois achados de medicao mudaram a implementacao. Primeiro: media nem esta em BLOCK_TYPES, entao barrar "o bloco de midia" nao protegeria nada. O vetor real e props, um mapa livre em qualquer tipo de bloco, e um hero com props.asset_id cria exatamente o mesmo compartilhamento. Por isso a guarda e por forma, nao por tipo de bloco. Segundo: dois modelos em producao ja carregam referencia a arquivo hoje, ou seja, o caminho nunca foi teorico. A guarda vive no funil unico (toTemplateEnvelope), nao em cada rota. Os quatro caminhos de escrita de modelo passam por ele (criar, editar, duplicar, criar-a-partir-de-relatorio), e dois deles nao passam pelo schema de entrada, porque copiam blocos que ja estao no banco. Uma guarda no schema teria deixado duplicar e criar-a-partir-de furados em silencio, que e o mesmo modo de falha que o {F8-19} corrigiu noutro lugar no mesmo dia. Provado por medicao e nao por leitura: os tres TEMPLATE__FAILED que seguem devolvendo 500 (apagar, promover, despromover) foram conferidos um a um e nenhum grava default_blocks. O criterio e nominal por sufixo (asset_id, sha256) e desce a arvore inteira, para que campo novo de referencia (cover_asset_id, logo_asset_id) ja nasca barrado sem ninguem lembrar de atualizar a guarda, e para que referencia aninhada em props.items[0].foto.asset_id seja pega igual a uma no topo. Campo vazio (asset_id: null) nao acusa: e campo vazio, nao referencia, e barra-lo faria a guarda reprovar o caso normal. A recusa e relatada, como o cartao exige: 422 TEMPLATE_BLOCK_ASSET_REF, com o campo exato que a causou e o motivo em linguagem humana. Se um caminho futuro esquecer de traduzir, o pior caso e um 500 no lugar de um 422: a gravacao continua barrada, porque quem barra e o funil. A guarda falha fechada. 12 casos, 0 falhas. O que reprova sem a guarda inclui duplicar um modelo legado, o caminho que nao passa por schema. Verificado removendo a guarda do funil: os tres caminhos gravam de verdade, e o duplicar cria uma segunda copia com a mesma referencia, que e o dano do cartao acontecendo na medicao. Ha controles negativos (modelo normal, campo vazio, palavra "asset" solta) para que verde nao possa significar "acusa tudo". Typecheck limpo, regressao 40/40 em reports-identity, e a guarda conferida dentro da imagem que serve producao apos o deploy (62 conteineres antes e depois, hub 200). DECISAO DO DONO: os 2 modelos legados com referencia a arquivo seguem no banco, ambos com 0 relatorios usando: bb2f87e5 "F820 Modelo da Agencia" (na agencia V4 Percus, cliente real) e 88574064 "F820 Modelo GLOBAL" (sem agencia). Sao fixtures da frente do {F8-20}, e entram na mesma decisao de limpeza do {F8-41}. A guarda nova impede que nascam novos e impede que estes sejam duplicados, mas nao apaga o que ja existe: apagar dado de producao nao e decisao de quem implementa. Merge 3807379.
ENTREGUE em 2026-09-22, merge dd6422e no ModularHub, migration 0233 aplicada antes da
imagem e deploy 14/14. Gates plan APPROVED, qa PASS e review APPROVED, os tres no merge.
O comportamento em producao, medido logo apos o deploy, e exatamente o desenhado: o
catch-up rodou no boot, achou a ocorrencia perdida de 21/09 e gravou skipped_expired,
sem emitir o resumo velho (zero notificacoes de digest criadas). "Nao saiu" virou fato
consultavel na tabela em vez de silencio, que era o buraco de origem. E
/api/health/scheduler responde {"ok":true,"atrasados":[]}.
O detector NASCEU CEGO e o qa pegou antes do merge, e esta e a parte que mais importa: ele
media agora - ultima ocorrencia perdida, valor que para job periodico **nunca passa de um
periodo. Com folga de 90 min sobre um job de 60 min, o auto_escalation podia ficar 30
dias parado aparecendo em dia. Era o proprio defeito deste cartao reaparecendo dentro do
detector. Corrigido na causa: ancora na primeira** ocorrencia perdida, mais folga abaixo do
periodo. Provado nas duas pontas (2h e 30 dias) e sem falso positivo em operacao normal.
A armadilha de calibragem que gerou isso fica escrita, porque ela e sutil: a folga tinha
acabado de ser corrigida por um bom motivo (a versao anterior, de "um periodo", cegaria o job
SEMANAL no caso real de 21/09, perdido por 2h44). Acertou para o job longo e abriu buraco no
curto, porque so o caso que motivou a mudanca foi testado.
Decisoes que o cartao carrega: auto_suspension fica sem catch-up de proposito
(recuperar suspensao atrasada suspenderia contas em lote numa hora sem ninguem olhando); o
catch-up e coalescente (buraco de 6 dias num job horario gera 1 execucao, nao 144); e o
detector vive fora do processo, porque dentro dele seria cego exatamente quando o agendador
morre.
Divida NOMEADA, declarada e nao escondida: faltam o selo "atrasado" no /admin/jobs e o
PATCH de catchup_mode/folgas na interface, entao hoje esses valores so mudam por SQL direto, o
que arranha admin-editability-first. E nao existe verificacao de sanidade impedindo um
admin de configurar folga maior que o periodo e reintroduzir o defeito, o que o proprio BUG-01
prova ser possivel.
**Nasceu do plano do {F8-63}, que foi olhar o digest e achou algo maior que o destino do
link.** Medido em 2026-09-21: o ultimo aviso de digest em messages.notifications e de
2026-08-31, e sao ZERO nos ultimos 21 dias, contra 148 no total historico. O
weekly_digest parou de emitir ha tres semanas e o sistema seguiu verde.
Por que isso passou: ninguem monitora AUSENCIA. Aviso que nao chega nao gera erro, nao
aparece em log de falha e nao quebra teste; some em silencio, igualzinho a API que ficou 7 dias
fora com o site respondendo 200. E a mesma familia do {P0-A3}.
O que implementar: primeiro descobrir POR QUE parou (cron, agendamento, excecao engolida ou
mudanca de dependencia), consertar, e so entao decidir o destino do link do digest, que
era a pergunta original. O /alertas e destino defensavel para ele, porque o corpo do resumo
("N alertas pendentes") sai de fetchPendingAlertSources, a mesma funcao que monta a Central;
o que falta e recorte temporal, e esse e o unico ponto que depende do dono.
Observavel: existe um resumo semanal novo depois da correcao, e existe verificacao que
reprova a AUSENCIA dele por mais de uma semana. Reprova consertar a emissao sem deixar o
detector de ausencia, porque foi justamente a falta dele que deixou o defeito viver 3 semanas.
DIAGNOSTICO de 2026-09-22, e ele MUDA o que este cartao e. O resumo semanal nao quebrou: ele nao rodou porque o agendador inteiro estava fora do ar em TODAS as tres segundas.
- O job esta ATIVO e sempre esteve (
control.cron_jobs,weekly_digest,is_active=t,
schedule 23 8 1), e o ultimo run consta como success em 31/08. Nao ha UMA execucao com falha registrada. O defeito nao deixou rastro porque nao houve tentativa.
- As execucoes eram perfeitamente semanais ate parar: 27/07, 03/08, 10/08, 17/08, 24/08,
31/08. As tres segundas seguintes (07/09, 14/09 e 21/09) nao tem registro nenhum.
- **Varrendo
cron_job_executionsdesde 25/06 (sem expurgo na tabela, 2.312 execucoes), ha
TRES buracos em que NENHUM job rodou, de nenhum tipo: 01 a 10/07 (10 dias), 07 e 08/09 (2 dias) e 14 a 20/09 (7 dias). Sao 19 dias sem agendador em tres meses. CORRECAO de 2026-09-22: a primeira leitura deste cartao disse 6 dias e 18 no total; 14/09 tambem tem zero execucoes, entao o terceiro buraco comeca nele. E a volta foi as 11:07 de 21/09, depois do horario do digest (08:23): a quarta semana se perdeu por 2h44**, e uma recuperacao com 3h de folga a teria salvado.
- So o terceiro buraco era conhecido (a parada da API de 14 a 21/09, achada pelo fiscal em
21/09). Os outros dois ninguem sabia, e o de julho durou dez dias.
- Nos dias dos buracos faltam ate os jobs de hora em hora (
auto_escalation, 24 execucoes
por dia), o que descarta "tick perdido" e prova parada do processo.
O que isso ensina sobre o mecanismo, e vale para os 6 jobs, nao so para o digest: o agendamento e setInterval de 60s dentro do processo da API, com matcher de minuto exato e sem recuperacao. Job de hora em hora perde um tique e o proximo cobre; job semanal perde o tique e perde SETE DIAS. Somando a isso que o processo pode simplesmente nao existir naquele minuto, o resumo semanal virou o unico sintoma visivel de uma queda, e um sintoma que so aparece uma vez por semana.
O cartao passa a exigir DUAS coisas, e a primeira nao e o digest:
- Recuperacao (catch-up): ao subir, o agendador compara o que deveria ter rodado com o que
rodou e executa o que ficou para tras, em vez de esperar a proxima janela. Sem isso, toda reinicializacao em cima de um horario agendado custa um ciclo inteiro daquele job.
- Detector de AUSENCIA, que e o que o cartao ja pedia: reprova quando um job nao roda ha
mais de um periodo dele. Os 19 dias existiram porque ninguem monitora o que nao aconteceu. CORRECAO de 2026-09-22 sobre o last_run_status, e ela piora o quadro: ele nao guarda o ultimo sucesso, como este cartao afirmou antes. Ele grava o resultado da ultima tentativa (running -> success/error, control-jobs.ts:1512 e :1547). O problema nao e a semantica do campo: e que nao existe campo nenhum para a tentativa que DEVERIA ter havido. Por isso o par (last_run_at, last_run_status) nunca consegue dizer "atrasado", nem se alguem corrigir o sentido dele. Fonte de verdade para ausencia e a ocorrencia esperada e nao ocorrida, calculada da propria cron, nunca o ultimo registro bom. E o detector NAO pode morar dentro do processo da API: seria cego exatamente quando o agendador morre, que e o unico momento em que ele importa. Reprova fechar so religando o digest, ou so olhando o digest: o defeito e do agendador.
A CAUSA das tres quedas, medida pela sessao fiscal em 2026-09-22, e nao e infra: fomos nos. No host nao houve reinicio do daemon do Docker desde 25/06, ha zero eventos de falta de memoria, e a maquina esta de pe sem reiniciar desde 30/04. Nada externo derrubou o container. Somado ao fato de que o mhub-api era o unico container mhub-* com politica de reinicio no, o que sobra e o simples: alguma sessao parou o container (deploy, rebuild, teste) e nao o subiu de volta, e a politica no transformou cada parada pontual em parada indefinida. Indicio, declarado como indicio e nao como fato: o buraco de 01 a 10/07 contem a data de um wipe de ambiente (ha um _BACKUP-PRE-WIPE-2026-07-05 no repositorio). Os logs daquele container nao existem mais para fechar a prova. Consequencia para a fila: esta porta ja foi fechada pelo {P0-A2}, que criou o caminho unico de recriacao com a politica junto e o verificador que reprova container sem ela. As tres paradas so foram possiveis porque qualquer mao podia parar e esquecer, e nada perguntava depois. Este cartao trata do que acontece quando uma parada ocorre, nao de evita-la.
FECHADO em 2026-10-15, com prova. Os 2 modelos F820 sairam em 26/09 ({REC-F-14}, GO do dono). SELECT
em producao (reports.template): total=3, individual sem agencia=0, nome F820=0 (evidencia
/opt/modularhub-evidencias/f8-41/f8-69-select.txt). A guarda contra voltar (CHECK no banco) e do {F8-41}.
Origem: smoke visual do fiscal em producao, 2026-09-24 (P1). Em /relatorios/templates
aparecem "F820 Modelo da Agencia" (ADS 4 PROS) e "F820 Modelo GLOBAL", este Individual SEM
agencia de origem, o que contradiz a regra da propria tela. **Procedencia medida pelo loop (so
leitura):** reports.template 88574064... (company_id nulo, is_global=false) e bb2f87e5...
(ADS 4 PROS), criados no MESMO instante, 2026-08-13 23:43:46 UTC; o nome "F820 Modelo" existiu
no codigo de teste do {F8-20} (commits cb47b1b e b7baa30, 13/08) e depois saiu; o commit
8e17ab8 (14/08 01:12) diz "o ator 14 para de escrever solto em producao". Ou seja: sobra de
uma rodada de teste do {F8-20} que gravou em producao antes da trava. Nenhum relatorio usa os
dois (reports.report com esses template_id = 0). Os default_blocks apontam para assets
(aecb5600..., 096ef77c...) a conferir no storage.
Exclusao em producao passa pelo dono (regra do fiscal): o cartao entrega a medicao completa
(templates, assets e objetos de storage ligados, prova de que nenhum dado real depende deles) e
o pedido de exclusao nomeado; apagar so depois do GO.
GO relatado pelo fiscal (2026-09-24, noite), escopo fechado: so os 2 ids acima, mais os
assets e objetos de storage EXCLUSIVAMENTE deles pela medicao do cartao; dump das linhas em
arquivo de backup antes; prova depois: 0 linhas para os 2 ids, /relatorios/templates sem os
dois, contagem de Individuais = 1. O loop confirma o GO direto com o dono na hora de apagar.
Aceite (H1): Dado a lista de modelos em producao, Quando o Super Admin abre
/relatorios/templates, Entao nenhum modelo de teste aparece, E nenhum modelo Individual existe
sem agencia de origem, provado por consulta e pela tela.
ENTREGUE em 2026-09-27 pelo {SMK-05} do roadmap do ModularHub, merge 02ac516. Mesmo
defeito, mesma fonte: o mapa unico blockTypeLabel (canonical-enums), alinhado aos 16 tipos do
BLOCK_REGISTRY do renderizador, serve a lista de modelos, o Compositor e a criacao de
relatorio; teste de paridade reprova tipo novo sem nome. Os 5 que vazavam agora tem nome.
Origem: smoke visual do fiscal em producao, 2026-09-24 (P2). Em /relatorios/templates a
lista de blocos mistura nomes traduzidos com identificadores internos (section_intro,
reliability, line_chart, donut, action_plan). Print em
00-operacao/projetos-internos/ophub-v2-modularerp/tmp/fiscal-smoke-2026-09-24/ERR-14-15-templates-rotulo-tecnico-viewport.png.
Medir ao pegar: de onde vem o rotulo (mapa de tipos de bloco), quantos tipos estao sem nome
humano, e se o mesmo mapa serve o compositor e o relatorio publicado.
Aceite (H1): Dado a lista de blocos do editor de modelos, Quando o usuario a abre, Entao todo
bloco tem nome humano em portugues e nenhum identificador interno aparece, E um tipo de bloco
novo sem nome reprova um teste.
FECHADO POR RECONCILIAÇÃO em 2026-10-16 (plano plan/f8-68.gate, sem código): o {P0-A4a} cobre os 5
caminhos. Purga e eliminação liberam a posse; apagar conta é barrado pela FK (409) e a eliminação da conta
libera inclusive posse sem byte (I8, depois a conta apaga com 204); não há rota de apagar agência nem
relatório (só SQL, e aí a posse fica de propósito, migration 0258). Observável corrigido: "zero donos sem
relatório" contradiz o desenho (deixaria arquivo vivo sem dono, impossível de eliminar); o certo é "toda
conta tem saída pela eliminação". Produção hoje: 0 donos, 0 relatórios, 0 lápides. Achado sem card: o
texto do 409 diz que há arquivos mesmo quando já sumiram (a saída funciona, a frase é imprecisa).
**Achado durante a limpeza do {F8-41} em 2026-09-22, e e um residuo que nasce de uma guarda
BEM-INTENCIONADA. reports.asset_owner tinha 6 registros, e os 6 eram orfaos**: o relatorio
deles ja nao existia. Todos com sha256, size_bytes e created_by nulos, e
storage.objects vazio, ou seja, eram ponteiros para arquivo que nunca existiu.
Por que acontece: a FK asset_owner_conta_com_arquivo_vivo e ON DELETE RESTRICT e protege
a CONTA de ser apagada enquanto houver dono de arquivo. Mas **nada protege o asset_owner de
perder o relatorio: quando o relatorio cascateia, o registro de dono fica. Entao a propria
limpeza produz residuo**, e produziria de novo a cada apagamento de relatorio, com ou sem
intervencao nossa.
O efeito pratico ja foi sentido: esses orfaos travaram a remocao de uma conta de fixture na
agencia parceira, com a mensagem de que havia "arquivo vivo" quando nao havia arquivo nenhum.
Uma guarda que mente sobre o motivo e pior que uma guarda ausente, porque manda investigar o
lugar errado.
O que implementar: ou o asset_owner cascateia junto com o relatorio, ou existe varredura
que recolhe dono sem relatorio. Decidir qual, e declarar o que acontece com asset_owner cujo
arquivo EXISTE de verdade (sha256 preenchido) quando o relatorio sai, porque ai apagar o
ponteiro sem apagar o arquivo deixa lixo no storage.
Observavel: apagar um relatorio nao deixa registro de dono para tras, e uma consulta por
dono sem relatorio devolve zero. Reprova fechar so limpando os orfaos de hoje: eles ja foram
removidos e voltariam no proximo apagamento.
ENTREGUE em 2026-09-22, merge 2d4efed, migration 0234 antes da imagem, deploy 14/14.
Gates plan APPROVED, qa PASS e review APPROVED, os tres no merge.
Em producao, a cadencia confirma o desenho: em 60 segundos o relay de 15s registrou 4
tiques, o flusher de 30s registrou 2, e os de 60s registraram 1 cada. Os de periodo longo
(imap 5min e os dois diarios) ainda nao bateram e nao aparecem como atrasados, que era o
falso positivo a evitar. /api/health/scheduler responde
{"ok":true,"atrasados":[],"desligados":[]}.
Desenho: control.scheduler_heartbeat, UPDATE-only, 9 linhas para sempre, e foi por isso
que reusar cron_job_executions foi reprovado, ja que so o relay daria 5.760 linhas por dia
numa tabela que ja tem 2.312 sem expurgo. Folga derivada do periodo, clamp(120s, 2h), para
cobrir um espectro de 15 segundos a 24 horas sem cegar o curto nem atrasar o longo.
NAO tem catch-up, e isso foi medido, nao suposto: os oito varrem estado ou fila e se
recuperam sozinhos ao voltar; o reports-schedule ja tinha catch-up coalescente proprio,
escrito antes do {F8-65}. Catch-up aqui teria sido trabalho errado.
Os gates acharam DOIS defeitos, e os dois sao da familia do instrumento que parece medir:
1. O botao "Desligar" da tela nao desligava o job. So mudava a coluna de expectativa,
enquanto o interruptor real vivia em outro lugar: o Super Admin clicava, a tela respondia, e
o agendador continuava rodando. Agora opera o interruptor de verdade e, onde nao pode,
recusa com 409 dizendo onde desligar, em vez de fingir.
2. Em 5 dos 9, um catch interno engolia o erro ANTES do heartbeat. O job podia falhar em
todo tique com o painel verde. Provado ao vivo revogando permissao: erro no log 5 vezes e
last_ok_at subindo, fail_count em zero. Corrigido em duas camadas, **preservando o
isolamento por item: a falha agrega e lanca no fim do tique, e o agendador continua
tentando**. Provado quebrando uma de tres faixas do expurgo: as outras duas commitaram
trabalho real (6 linhas) e so a quebrada contou como erro.
O que PERDEU: cerca de 6.900 atualizacoes por dia numa API ociosa; passa a existir falha do
vigia que nao e falha do job; a superficie publica cresce; e **ninguem ganha historico por
execucao**, entao nao sera possivel datar um buraco depois do fato, como se fez com os 19 dias.
Nasceu do {F8-65}, e e o cartao irmao dele. O agendador do Control nao morreu sozinho: ele e
mais seis vivem no MESMO processo da apps/api e ficaram fora nos mesmos 19 dias
(01-10/07, 07-08/09, 14-20/09): imap-sync, opadmin-export, notif-retention,
event-outbox-relay, reports-schedule, reports-artifact-purge e error-events-purge.
CORRECAO de 2026-09-22, em duas rodadas: sao OITO alem do control, nao seis. Faltavam no
inventario o error-events-purge e o error-events-flusher (lib/error-events.ts, 30s).
Com o control sao NOVE no mesmo processo. Periodos: relay 15s, flusher 30s, control 60s,
opadmin-export 60s, imap 5min, reports-schedule 15min, artifact-purge 1h, notif-retention e
error-events-purge 24h.
E este cartao NAO e sobre catch-up, para nenhum dos oito. Todos varrem estado ou fila
(pendente, expurgo vencido, idade, saida nao entregue) e se recuperam por construcao ao
voltar. O reports-schedule, que parecia o mais critico por disparar relatorio de cliente, **ja
tem catch-up coalescente proprio** (reports-cycle-trigger.ts, janelas nao fechadas mais corte
em um), escrito ANTES do {F8-65} e com o mesmo desenho. O control precisou de catch-up por ser
gatilho temporal puro, cuja ocorrencia perdida nao existe em lugar nenhum depois.
Unica perda sem conserto possivel: o flusher acumula em memoria, e o que esta no buffer
morre com o processo.
O ACHADO QUE REDEFINE ESTE CARTAO, medido no codigo: nenhum dos sete registra execucao.
O control so pode ser auditado porque tem control.cron_job_executions , e foi de la que
sairam os 19 dias. Os outros sete tem apenas setInterval (60s na maioria, 15s no outbox
relay) e nao gravam nada que diga "rodei"; o unico insert deles e o do outbox relay em
hub.webhook_deliveries, que e o trabalho dele e nao registro de execucao.
Logo, isto nao e "estender o detector": e criar a testemunha que nao existe. Sem registro,
nao ha com o que comparar, e a ausencia continua invisivel por construcao.
Cuidado que o cartao carrega para quem implementar: o outbox relay roda a cada 15 segundos,
entao gravar uma linha por tique dariam cerca de 5.700 linhas por dia so dele. E
cron_job_executions ja tem 2.312 linhas sem nenhum expurgo. Registrar execucao aqui nao
pode nascer sendo a proxima tabela que ninguem limpa.
O que NAO aconteceu, medido antes de virar alarme: o reports-schedule e o envio agendado
de relatorio para cliente, e a primeira leitura disto soou como "cliente deixou de receber".
Nao deixou. Medido em 2026-09-22 no mhub-db: reports.report_schedule vazia,
reports.report_job com 0 linhas e reports.schedule_cycle com 0 linhas, nenhum
registro em nenhuma delas, nunca. Nao havia agendamento para executar, entao nao houve perda.
Isso muda o TEMPO VERBAL do cartao, nao a prioridade dele: o risco e futuro. No dia em que
existir o primeiro agendamento de cliente, o mesmo processo pode morrer e ninguem saber, e ai e
entrega a cliente. Cartao que afirma perda que nao houve tem o mesmo defeito de numero que este
projeto passou o dia caindo.
O que implementar: estender aos seis o detector de ausencia que o {F8-65} constroi, que por
desenho vive FORA do processo (dentro dele seria cego exatamente quando o processo morre).
Observavel: cada um dos seis tem periodo declarado e ha verificacao, fora do processo, que
reprova quando qualquer um deles nao roda ha mais de um periodo. Reprova cobrir so o Control, e
reprova detector que viva dentro da apps/api.
FECHADO em 2026-10-16 pelo {P0-A4a} do Hub (merge Hub 7539832, Reports f1c7da5; RF-ADM-024; em
produção). Dry-run, execução idêntica ao dry-run, sentinelas preservadas, trilha sem dado pessoal, falha
fechada com storage fora, lápide 410 e página de conteúdo eliminado no link do retrato.
Cartão novo, achado no planejamento do {F8-07} em 2026-08-08. A rotina que elimina dados
pessoais a pedido percorre as tabelas do módulo, e não existe tabela de arquivos: os arquivos
enviados vivem no sistema de arquivos, referenciados por identificador dentro do bloco. Resultado:
a eliminação roda, relata sucesso, e os arquivos ficam.
RNF-LGPD-001, RF-ING-004, RN-006.
**Enquanto não há criativo nenhum em produção isto é teórico, e é exatamente por isso que dá para
resolver com calma**, mas deixa de ser teórico no dia em que o {F8-07} ligar a inserção. Um
processo que relata sucesso sem ter feito o trabalho é pior que um que falha alto.
O que implementar: a eliminação passa a alcançar os arquivos referenciados pelo que está sendo
eliminado, e relata o que apagou, para a diferença entre "não havia nada" e "não olhei" ficar
visível. Decidir também o que acontece com arquivo referenciado por retrato imutável já publicado,
que é o caso difícil: o retrato não muda, mas o requisito de eliminação existe.
Como verificar (teste primeiro): enviar um arquivo, referenciá-lo, rodar a eliminação e
confirmar que o arquivo saiu do disco e que o relatório da eliminação o menciona; e exercitar o
caso do arquivo referenciado por publicação já feita.
Aceite (H1): Dado um arquivo enviado e referenciado, Quando a eliminação de dados pessoais roda,
Então o arquivo é removido do armazenamento, E o resultado da eliminação declara o que foi removido,
E o caso do arquivo preso a retrato já publicado tem tratamento decidido e registrado.
Rodada 17 (2026-08-14): o conserto porta a porta acabou, porque virou propriedade. Dezesseis
rodadas fecharam uma junção de cada vez, e cada uma nasceu de alguém atravessar a seguinte. O
levantamento por construção mostra por que isso não ia terminar sozinho: das 46 colunas de
identificador das 22 tabelas do módulo, 21 são âncora (a agência da própria linha, ou o pai de
quem a linha se pendura) e 2 não apontam para agência nenhuma; sobram 23 junções que podem
discordar - e 14 delas não tinham guarda de espécie alguma. Medido por efeito, sob o papel que
a produção realmente usa para gravar, tudo em transação revertida: antes, 13 das 14 aceitas
(a décima quarta foi recusada pela chave primária, não por guarda de inquilino); **depois, 0
aceitas e 14 recusadas, com os 6 caminhos legítimos vivos nos dois lados** - inclusive o que
esta fatia inteira existe para proteger, a lápide do relatório já apagado. Entre as 14 estavam a
quinta porta no nível da tabela (override da própria agência apontando relatório alheio, linha
que a vítima não enxerga numa tabela que fala sobre ela, fechada pela política no padrão que a casa
já tinha) e a do nível da função (as duas rotinas de recomendação gravavam o identificador de
relatório sem conferir nada). **A afirmação deixou de ser "as quinze estão conferidas" e passou a
ser "toda junção com inquilino é conferida"** - e ela se monta do catálogo, então coluna nova
aparece sozinha. Provou-se pelo avesso: derrubada a guarda de uma tabela em produção, os dois
casos reprovaram nomeando as duas colunas exatas. O que fica declarado, e é o limite honesto:
o arquivo não tem linha própria - a agência dele é o diretório, e o banco não enxerga disco.
A guarda cobre o que o banco sabe (posse viva ou lápide já existente); arquivo sem nenhum dos dois
ainda é reivindicável pela agência errada. Medido: 8 arquivos, 6 com dono, 2 nessa condição.
Fechar isso pede conferir o caminho no disco, e é cartão próprio.
Retomado em 2026-10-15 pelo {P0-A4a} do ModularHub, fatia F1: o requisito nasceu. O trabalho
das 17 rodadas não entrou em nenhuma main e volta por branch nova, em fatias (F0 leitura do que
já existe em produção, F1 requisito, F2 migrations, F3 posse na escrita, F4 eliminação de arquivo).
A F1 escreveu no DRS o RF-ADM-024 (a eliminação alcança os arquivos e relata o que apagou):
posse registrada na gravação, ensaio que não apaga nada, execução que apaga exatamente o que o
ensaio listou, preservação com motivo do que não é do cliente, falha fechada sem varredura,
um evento de auditoria por execução e lápide (410) no lugar do "não encontrado". O RF-ADM-011
passou a apontar para ele, o RNF-LGPD-002 ganhou corpo e H1, e o modelo de dados ganhou as tabelas
de posse e de lápide.
Decisão do retrato já publicado, pela Regra Zero (uma resposta certa, sem cenário): o retrato
do cliente eliminado não é editado nem mantido com buracos, é eliminado inteiro. O RF-ADM-011 já
manda apagar os snapshots e revogar os links; a imutabilidade do RF-PUB-001 protege contra edição
do rascunho, não contra a eliminação; e a lei não dá base para guardar uma página de resultados
depois do fim do tratamento. O link passa a mostrar a página brandada de conteúdo eliminado (410),
distinta da de link revogado, e os arquivos que ele citava saem do armazenamento. Cópia que já
saiu do sistema (PDF baixado pelo cliente final) não é alcançável, e o requisito declara isso.
Aceite (H1) passa a ser o do RF-ADM-024, que contém e endurece o H1 acima. O Plano de Testes
ganha o item Alfa do RF-ADM-024 quando a F4 fechar. Próximo: F0, depois do {INFRA-21}.
Cartão novo, e ele não nasce de um caso: nasce da TERCEIRA ocorrência do mesmo padrão (2026-08-09).
Uma rota lê o estado do rascunho, faz uma chamada de rede que leva segundos, e grava **substituindo
o conjunto inteiro** a partir do que leu antes. Qualquer escrita que aconteça nessa janela é
sobrescrita em silêncio.
Já custou um Critical, na fatia dos criativos, onde a composição apagava o bloco que o operador
acabara de inserir. Foi corrigido lá, relendo com trava na mesma transação que grava. **As duas
rotas da pesquisa de mercado têm o mesmo desenho e continuam sem conserto**, substituindo o conjunto
de pontos de dado a partir de um retrato lido antes da chamada externa.
RF-CMP-002, RF-ING-006, RN-004.
O que implementar, e o ponto é não consertar caso a caso: as rotas afetadas passam a reler com
trava na transação que grava, como já foi feito na composição. E, porque esta é a terceira vez,
vale uma varredura que encontre o padrão: gravação que substitui coleção inteira derivada de
leitura anterior a uma chamada externa. Consertar as duas conhecidas deixa a quarta nascer igual.
Como verificar (teste primeiro): um teste que reproduza a corrida em cada rota afetada e
reprove sem o conserto (o defeito anterior sobreviveu a testes verdes porque eles exercitavam a
lógica contra dado estático, nunca o fluxo de duas transações); e a varredura acusando um caso
plantado.
Aceite (H1): Dada uma escrita concorrente durante a janela de uma chamada externa, Quando a rota
grava, Então nenhum dado gravado nesse intervalo é perdido, E o padrão de gravação cega é acusado
por varredura quando reaparecer.
FECHADO em 2026-08-10, mergeado (60696d3) e deployado. QA PASS e review APPROVED.
A varredura entregou mais do que o cartão pedia, e é esse o ponto de ter pedido varredura. 13
sítios classificados; 6 com o padrão, dos quais 4 vulneráveis e corrigidos, e **dois deles não
eram nomeados pelo cartão**: criar artefato e regenerar bloco. Conserto caso a caso teria deixado os
dois de pé, com o placar verde.
Prova reconferida sem confiar em relato: o QA reverteu o código e mediu, rota a rota, que os
testes de corrida falham sem o conserto e passam com ele (2, 1 e 2 falhas respectivamente). E
plantou ele mesmo uma rota vulnerável nova, que a varredura acusou.
A guarda mede mecanismo, não etiqueta, e isso veio de uma segunda rodada. Ela nasceu exigindo só
a presença de um marcador de texto. O review apontou que marcador colado sem trava passaria, que é
exatamente o que acontece quando alguém copia o bloco vizinho "para seguir o padrão". Reforçada para
exigir a trava real em código, não em comentário, com o par de casos provado: com trava não acusa,
sem trava acusa.
Um desvio de rota que quase virou dívida, registrado porque a lição é de método. Ao medir o
encanamento, o pacote da API apareceu sem comando de teste, e a leitura apressada foi "os 30
arquivos falham". Estava errado: eles seguem outra convenção, rodam um a um e passam (o
arquivo conferido devolveu 7 de 7). O engano nasceu de apontar o executor errado, e a instrução que
pediu o placar já sugeria a conclusão, o que enviesou quem mediu. Um comando de teste chegou a ser
adicionado e foi desfeito antes do merge: ele faria o projeto reportar 30 falhas inexistentes, e
comando que mostra vermelho falso é pior que comando nenhum, porque ensina a ignorar vermelho. O
problema real virou {F8-33}.
Achados encaminhados como cartão próprio, não como recomendação dentro do gate: {F8-30} (sete
escritas sem trava, sem rede na janela, incluindo a de publicar, número corrigido de seis para sete
pela recontagem do review), {F8-31} (a conexão do banco fica presa durante a IA em três rotas, e
a rota de regenerar já mostra o desenho certo) e {F8-32} (a função de leitura muta a estrutura que
recebe, obrigando todo chamador a uma cópia defensiva que ninguém é lembrado de fazer).
**Cartão novo, nascido do planejamento de {F8-08} em 2026-08-08 e escrito no roadmap em 2026-08-09,
junto da reconciliação do documento. É ele que bloqueia a Fatia B de {F8-08}** (o logo do
cliente por relatório), e enquanto não existir como cartão o bloqueio vive só na cabeça de quem
planejou.
O conflito, medido: o protótipo aprovado desenhou dois portadores de marca na página
pública, com donos distintos: a abertura do relatório é do CLIENTE, e a barra superior é da
AGÊNCIA, com a linha de crédito de quem preparou o relatório. A barra nunca foi construída, e
por isso a marca da agência foi parar no único portador que existe, o do cliente. Consequência: uma
sobreposição de logo do cliente naquele slot apagaria a marca da agência da página inteira,
desfazendo o que {F7-04} entregou de propósito e tornando falso o aceite de RF-ADM-004 para aquele
relatório. É a regressão silenciosa clássica: passaria por plano, teste, revisão e verificação
visual, porque os quatro olham o comportamento NOVO e nenhum pergunta o que sumiu.
A alternativa foi avaliada e descartada, e fica registrada para não voltar: fazer o portador do
cliente preferir o cliente e deixar a agência como reserva no mesmo slot não custa barra nova, mas
numa página com logo de cliente a marca da agência desaparece do produto inteiro. Trocar uma
promessa cumprida por outra não é entregar.
RF-ADM-004, RF-CUR-004, RN-020 item 4; artefato {ART-SCR-03} do Inventário.
O que implementar: a barra superior no leitor público, alimentada exclusivamente pela
identidade congelada no retrato publicado (nunca leitura ao vivo do cadastro, que contradiria
RN-020 item 3), com logo na proporção natural, estado sem logo, contraste no mesmo mínimo do resto
da página e recolhimento em tela estreita. Ficha completa em {ART-SCR-03}.
Como verificar (teste primeiro): um relatório publicado real exibe a marca da agência na barra
superior; a proporção renderizada do logo é conferida contra a proporção natural do arquivo, não a
olho; um relatório publicado ANTES desta entrega continua abrindo sem erro; e o caso de agência sem
logo cai no nome em texto em vez de espaço vazio.
Aceite (H1): Dado um relatório publicado, Quando o cliente final abre o link, Então a barra
superior exibe a marca da agência que preparou o relatório, E a marca vem da identidade congelada
no retrato e não do cadastro ao vivo, E o logo é exibido na proporção natural, E a abertura do
relatório permanece disponível como portador do cliente.
FECHADO em 2026-08-10, mergeado (fa00cf0) e deployado. Migration 034 congela também o nome da
agência no retrato, ao lado do logo que já era congelado. QA PASS e review APPROVED, os dois com o
Check 0 explícito: a marca da agência mudou de lugar sem sumir e sem duplicar. Contagem de
imagens na página inteira igual a uma no caso com logo e zero nos demais, nenhuma órfã no lugar
antigo. A abertura ficou vaga de propósito, que é o que este cartão existia para entregar: a
Fatia B de {F8-08} está destravada.
O defeito que só apareceu porque alguém olhou: a barra nasceu como irmã do documento, **fora do
container onde as variáveis de cor existem**, e ficou sem cor nenhuma (fundo transparente, texto
preto). O próprio autor achou no smoke e corrigiu movendo-a para dentro. Medido depois do conserto:
contraste 15,97:1 no nome e 6,91:1 na linha de crédito, logo quadrado de 900 por 900 renderizado
como quadrado, sem sobreposição com o título nem em tela grande nem em celular. O review deixou o
risco nomeado: nada automatizado impede uma fatia futura de pôr transformação no container e
quebrar a fixação da barra.
Divergência de julgamento entre os dois gates, registrada porque ela é a parte interessante. O
preview do compositor e o PDF continuam mostrando o logo da agência na abertura. O review chamou de
dívida nova, da mesma família que a fatia da cor pagou; o QA respondeu que não viola requisito
nenhum, porque RN-020 item 4 e a ficha {ART-SCR-03} escopam a regra à página pública. **Os dois
estão certos no que afirmam**: não há violação de spec, e ainda assim o operador aprova uma abertura
e o cliente recebe outra. Virou {F8-28}, atrás de {F8-08} na fila, e lá está registrado o achado
que nenhum dos dois gates viu: o PDF não tem barra superior, então quando o logo do cliente
ocupar a abertura a marca da agência some do PDF por completo se ninguém decidir onde ela vai.
Achado de infra durante o deploy, corrigido no mesmo ato: o leitor subia avisando que
READER_ASSET_SECRET não estava configurada e gerava um segredo por processo. As URLs de imagem
são assinadas a cada requisição, então nenhum retrato guardado quebrou; o que quebra é quem está com
a página aberta no instante do restart, e (o caso caro) qualquer futuro segundo leitor, porque
duas réplicas assinariam com segredos diferentes e as imagens falhariam de forma intermitente, do
jeito mais difícil de diagnosticar. O segredo era previsto no desenho e nunca tinha sido
provisionado. Provisionado e o container recriado; o aviso saiu do log.
Cartão novo, achado no planejamento de {F8-08} em 2026-08-08, escrito em 2026-08-09. As páginas
que respondem por um link revogado ou expirado resolvem a identidade ao vivo, lendo o cadastro,
em vez de usar a identidade congelada no retrato. E a precedência que elas aplicam **já hoje
diverge** da que a publicação aplica: elas não têm o degrau do padrão da agência e caem direto na
cor padrão do renderizador.
**Com a sobreposição por relatório ({F8-08} Fatia A), isso passa a ser a terceira precedência viva
no sistema**, exatamente contra RF-CUR-012, que existe para haver uma só.
RF-CUR-012, RN-020, RF-REN-006.
Por que ficou fora de {F8-08}: são páginas que, por definição, não estão mostrando relatório
nenhum. Não é desculpa para deixar sem dono, que é o que {F5-09} acabou de cobrar.
O que implementar: essas páginas passam a resolver identidade pelo mesmo ponto único das
demais, ou declaram, no próprio código e neste cartão, por que a identidade delas é deliberadamente
outra. Uma das duas, nunca o silêncio atual.
Como verificar (teste primeiro): revogar um link de um relatório cuja cor veio de sobreposição e
conferir que a página de revogado usa a mesma cor que a página do relatório usava, ou a cor
declarada como deliberada; e o mesmo para um link expirado.
Aceite (H1): Dado um link revogado ou expirado de um relatório com identidade resolvida, Quando
alguém o abre, Então a identidade exibida vem do mesmo ponto único de resolução do restante do
sistema, E não existe uma segunda precedência de identidade viva no produto.
FECHADO em 2026-08-10, mergeado (ef77eb9) e deployado. QA PASS e review APPROVED. Saída
escolhida: a primeira, ponto único, lendo a identidade já congelada no retrato.
A pergunta que parecia estrutural se dissolveu ao ser medida. O cartão tratava como ponto difícil
"de onde vem a identidade quando não há relatório para mostrar". A resposta: **esse caso não
existe**. Revogar exige que o relatório esteja publicado, e expirar só é avaliado depois disso.
Então sempre há retrato, com a identidade já resolvida. O único estado sem identidade é o de link
que nunca existiu, e ele segue como estava, por desenho.
Prova direta de que a leitura ao vivo morreu: o QA revogou e expirou relatórios reais e, **depois
disso**, alterou o cadastro da agência e do cliente no banco. As páginas continuaram mostrando a cor
e o nome congelados. É o teste que separa "não lê mais ao vivo" de "por acaso coincide".
O defeito que ninguém tinha pedido para procurar, e é o mais grave desta fatia: essas páginas
nasceram sem cor nenhuma, desde a Fatia 5. O elemento principal nunca carregou a classe a que as
variáveis de cor são escopadas, então a identidade era declarada e não alcançava nada. Meses no ar
assim. Foi achado de raspão, por quem tinha ido mexer em identidade por outro motivo, e
confirmado por medição do estilo aplicado, não pela folha declarada.
É a terceira ocorrência da mesma armadilha em dois dias ({F8-21}, esta, e a dormente em {F8-35}).
Por isso a quarta não vai ser consertada uma a uma: virou {F8-36}, que varre o conjunto.
Achado de precedência que o review mediu e o cartão não sabia: essas páginas tinham **dois dos
quatro degraus** de resolução de identidade. Ao unificar, elas ganharam o degrau do padrão da agência
que faltava: mudança de comportamento visível, intencional e coberta por teste.
FECHADO em 2026-08-18. Placar REMEDIDO com o critério comportamental do próprio cartão
(chamar a varredura e ler o veredito, nunca busca textual): das 7 formas, **5 já passavam e 2
reprovavam** - o cartão registrava 4 e 3, e a diferença é a data por extenso ("Junho de 2026"),
fechada horas antes na mesma sessão pela onda OP abaixo. As duas que faltavam são as formas de
ordinal escrito SEM o indicador sobrescrito: "Criativo 3" (o exemplo literal da nota do
requisito) e "3o criativo".
A isenção não nasceu de lista nova, que seria a inclusão nomeada que este projeto já pagou
caro: o nome da peça vem de ENTITY_PHRASES_FOR_ORDINAL, achatado do MESMO ENTITY_VOCABULARY
que classifica a entidade da tupla de identidade - entidade nova entra nos dois usos de uma vez,
e o teste percorre o vocabulário inteiro em vez de citar quatro nomes.
Controle negativo por forma nova, como o cartão exige: o número só é isento COLADO ao
substantivo. "Criativo 3 trouxe 340 leads" passou a acusar SÓ o 340 (antes acusava "3" e "340"),
e "O criativo campeão do mês trouxe 3 vendas" continua acusando o 3.
Suíte 1613 verde; merge 403899f em main, deployado.
**Cartão novo, e ele nasceu ao contrário do que o planejamento previa: a premissa do plano estava
MORTA e a medição achou o defeito oposto.** O plano de {F8-08} registrou a suspeita de que o
perímetro da disciplina de número derivava de uma segunda lista curada, deixando a abertura do
relatório de fora da varredura. **Medido em 2026-08-09 no código vivo: essa suspeita é falsa e já
tinha sido corrigida por {F18-20}** (o perímetro deriva dos campos editáveis, e a abertura devolve
os quatro campos de texto dela). Controle negativo executado: uma abertura com um número de métrica
digitado à mão reprova, como deve.
O que a mesma medição encontrou, e é defeito vivo: a varredura reprova casos que o próprio
RF-CUR-011 isenta com todas as letras. O requisito diz, no aceite, que "uma data, um ordinal ou uma
faixa de segmentação no mesmo campo não reprovam", e a nota dele cita nominalmente um exemplo que
hoje reprova.
Contagem. Universo: as três classes que o aceite de RF-CUR-011 isenta nominalmente (data,
ordinal, faixa de segmentação), exercitadas por forma de escrita. Critério: COMPORTAMENTAL,
chamando a varredura de proveniência com a legenda como único conteúdo e lendo o veredito dela,
nunca busca textual no código. Placar: **7 formas exercitadas, 4 passam e 3 reprovam, e 4 mais 3
fecham em 7.** Passam (lado negativo, lido por inteiro): data com barras, data no formato de ano
com traços, ordinal com indicador ordinal sobrescrito, e faixa de idade com hífen. Reprovam: um
ordinal escrito só com o dígito ("Criativo 3", que é o exemplo literal da nota do requisito), o
mesmo ordinal com a letra em vez do indicador sobrescrito, e uma data por extenso cujo ano é lido
como métrica ("Junho de 2026"). Evidência de 2026-08-09, medida contra a linha principal do
repositório do renderizador na mesma data, portanto posterior a {F18-20} e não herdada dele.
O requisito que REPROVA este defeito, com o texto literal: RF-CUR-011, aceite, cláusula "E uma
data, um ordinal ou uma faixa de segmentação no mesmo campo não reprovam".
O que implementar: a isenção passa a cobrir as três classes pela FORMA como as pessoas
realmente escrevem, e não só pelos formatos que o conjunto de padrões livres já reconhece. O
cuidado é o de sempre e está no próprio requisito: alargar a isenção não pode virar "qualquer
dígito passa", senão o defeito central volta pela porta da frente. Cada forma nova aceita nasce com
o par de controle: uma que passa e uma métrica vizinha que continua reprovando.
Como verificar (teste primeiro): as 7 formas do placar acima viram casos de teste, as 3 que
hoje reprovam passam a passar, as 4 que passam continuam passando, e um controle negativo por
forma nova prova que a métrica equivalente continua sendo reprovada.
Aceite (H1): Dada uma legenda contendo uma data, um ordinal ou uma faixa de segmentação em
qualquer das formas exercitadas, Quando a varredura de proveniência roda, Então nenhuma delas
reprova, E uma métrica escrita à mão no mesmo campo continua reprovando, E cada forma isenta tem o
seu controle negativo no conjunto de testes.
Cartão novo, achado no planejamento de {F8-08} em 2026-08-08, escrito em 2026-08-09. O retrato
publicado grava, honestamente, de onde veio a cor do relatório (sobreposição, ficha do cliente,
padrão da agência ou lacuna). Nenhuma tela mostra isso. O aviso que existe de verdade é o de
tom de voz; o de cor, que RF-ADM-006 exige em palavras ("usando identidade padrão da agência"),
não tem superfície.
É a família "peça pronta que ninguém aciona", que este projeto já pagou várias vezes. O campo é
gravado, a distinção entre os valores foi desenhada de propósito (é ela que separa "herdou porque a
ficha está vazia" de "o operador escolheu"), e o produto não expõe a diferença que justifica o
campo existir.
RF-ADM-006, RF-CUR-012, RN-020 item 1.
O que implementar: o compositor e a página de publicação exibem a procedência da cor em
linguagem humana, e o aviso de identidade padrão da agência aparece quando é esse o caso, do mesmo
jeito que o de tom de voz já aparece. Sem inventar canal novo: é a mesma superfície de aviso que já
existe.
Como verificar (teste primeiro): um relatório de cliente com ficha vazia mostra o aviso de
identidade padrão da agência; um com ficha preenchida não mostra; e um com sobreposição de
relatório mostra que a cor foi escolhida naquele relatório, nunca confundido com herança.
Aceite (H1): Dado um relatório cuja cor veio do padrão da agência, Quando o operador o abre no
compositor ou vai publicar, Então o aviso de identidade padrão da agência aparece, E um relatório
com cor declarada pela ficha do cliente não exibe esse aviso, E um relatório com cor escolhida nele
mesmo é apresentado como escolha e não como herança.
FECHADO em 2026-08-18, e no ar. GET /:id/preview ja CHAMAVA fn_resolve_identity, a mesma funcao que o publish usa, e jogava a marcacao fora: o campo era resolvido e descartado uma requisicao antes de chegar em quem compoe. O trecho quebrado era o entre a funcao e a tela, nao a funcao (essa tem cobertura desde {F8-08a}). No servidor, a correcao e um campo. Na tela, o compositor passou a dizer de onde veio a cor, em linguagem humana, na secao Identidade que ja existe, sem canal novo. Os quatro estados sao ditos de forma diferente: report_override como escolha ("Cor escolhida neste relatorio" - chamar isto de heranca seria dizer o contrario do que aconteceu), declared como cor da ficha do cliente, e agency_default e gap em tom de aviso, com o que fazer para sair do estado. 4 casos, 0 falhas, exercitando a rota real com os tres cenarios do aceite. O quarto exige que os tres sejam distinguiveis entre si - sem ele, os tres primeiros passariam com a rota devolvendo sempre o mesmo valor. Achado ao montar a fixture: a ficha de identidade visual do cliente nao e coluna de hub.accounts, e tabela propria (hub.account_identidade_visual), medido no schema depois de a primeira versao do teste falhar assumindo o contrario. O SMOKE VISUAL ACHOU TRES DEFEITOS QUE O TESTE VERDE NAO PEGAVA, e e a licao do cartao: (1) o aviso nascia dentro da secao Identidade, que e colapsavel e comeca fechada - o aviso que RF-ADM-006 exige "em palavras" so aparecia para quem ja tivesse ido procurar. Aparecer sob clique nao e aparecer. Os dois casos de advertencia passaram a ficar sempre visiveis, fora do acordeao; os informativos seguem dentro, onde sao contexto de quem vai mexer na cor. (2) no tema escuro o corpo do aviso saiu em rgb(28,19,3), quase preto, sobre fundo escuro: usei text-warning-foreground (cor para fundo solido) sobre fundo translucido. A varredura mostrou que esse token tinha uma unica ocorrencia no repo inteiro, a minha. A correcao nao foi trocar a classe: foi parar de montar a caixa a mao e usar o primitivo Callout ({DS-01}), criado justamente para fechar essa familia - deixar uma caixa artesanal ali seria abrir a 42a variacao do que aquele cartao fechou. (3) Callout nao repassa props extras, entao os marcadores de teste passados a ele seriam descartados em silencio, e o typecheck nao acusa. Sem o wrapper, o proprio smoke que achou os dois primeiros defeitos deixaria de encontrar o elemento, e eu leria "sumiu" como "corrigi". Medido na producao, na tela, com a secao ainda fechada (aria-expanded=false): aviso visivel com altura real, procedencia agency_default, texto completo, icone presente, e contraste do corpo de 9,75:1 no tema escuro e 4,81:1 no claro (WCAG AA pede 4,5). Captura de imagem nao foi possivel nesta sessao porque o painel do navegador nao compoe quadros; a verificacao foi por medicao do renderizado (posicao, cor computada, contraste, visibilidade), e isso esta dito aqui em vez de declarado como se eu tivesse olhado a imagem. Merges 90b8923 (funcionalidade) mais os dois de correcao vindos do smoke. RF-ADM-006, RF-CUR-012, RN-020 item 1.
**Cartão novo, escrito em 2026-08-10, fechando a decisão de produto {Q9} do Questionário de
Elicitação.** O dono decidiu em uma frase: "o nome do relatório ser o título do relatório". Não
existem dois nomes a unificar, e essa é a descoberta que reescreve o cartão: **o banco tem um único
campo de título (reports.report.title), o operador o escreve, e ele não alcança a página
pública por caminho nenhum**. O retrato imutável do publish não o copia, e o que o cliente final lê
no topo do documento vem do card de compartilhamento ou de um texto genérico quando esse card não
foi preenchido. Renomear um relatório é, hoje, gesto de organização interna que o cliente nunca vê.
A pergunta nunca teve duas respostas legítimas, e ficou meses no Questionário como se tivesse.
O documento chama-se relatório, o operador dá nome a ele, e o cliente abre a página: o nome é o
título. Registrar isso como decisão pendente foi transformar trabalho meu em pergunta do dono.
RF-CUR-011, e o próprio DRS já escreveu a consequência (§ RF-ADM sobre renomear): *"se o título
passar a chegar à página pública, ele entra no perímetro no mesmo ato"*. Ou seja, este cartão não é
só encanamento. Ele coloca o título dentro da disciplina de número, ao lado dos campos
editáveis dos blocos. Título com número cru passa a reprovar a publicação como qualquer outro
texto editável, e essa metade é a que não pode ser esquecida: ligar o caminho sem ligar a guarda
abriria um buraco na única trava que separa número com dono de número inventado.
Cuidado com o retrato imutável: relatório já publicado não muda de título retroativamente. O
título vai para o retrato no ato do publish, como o logo e a cor já vão; publicação antiga, sem o
campo no retrato, continua renderizando pelo caminho de hoje, sem quebrar.
O que implementar: o título passa a ser copiado para o retrato no publish; a página pública o
usa como título do documento, com o texto genérico permanecendo só como reserva de retrato antigo;
e a varredura de proveniência passa a cobrir o título junto dos demais campos editáveis.
Como verificar (teste primeiro): publicar um relatório com título escrito pelo operador e abrir
a página pública anônima, com olhos, em tela grande e celular; publicar um com número cru no título
é recusado, nomeando o título como origem da recusa; um retrato publicado antes desta mudança
continua abrindo sem quebrar; e renomear um relatório já publicado não altera a página no ar até
que ele seja publicado de novo.
Aceite (H1): Dado um relatório cujo operador escreveu o título, Quando o cliente final abre o
link público, Então o título que ele lê é o que o operador escreveu, E um título com número sem
origem declarada recusa a publicação apontando o título, E um relatório publicado antes desta
mudança continua abrindo, E renomear depois de publicado só alcança o cliente na publicação
seguinte.
FECHADO em 2026-08-10, mergeado (b97c4cc no pacote, b7c3fb2 no Hub) e deployado. QA PASS e
review APPROVED. **A decisão de produto que estava parada há meses no Questionário virou produto no
mesmo dia em que foi respondida.**
As duas metades entregues juntas, que era a exigência: o título é congelado no retrato no ato do
publish e usado como título do documento que o cliente abre; e o título **entrou na disciplina de
número**: título com número sem origem recusa a publicação. Ligar o caminho sem ligar a guarda
abriria um buraco na única trava que separa número com dono de número inventado.
Prova de que a guarda reprova de verdade: o QA reverteu a chamada do gate no worktree dele e
o teste caiu: publicou com 200 e gravou o número cru no retrato. Restaurou e voltou a reprovar.
Sem essa reversão, o teste provaria apenas que o caminho feliz funciona.
A decisão delicada, medida e aceita: o título não pertence a bloco nenhum, então o mecanismo do
{F18-21} (cruzar contra os dados do bloco) não tem contra o que cruzar. Escolha: recusar, o mesmo
tratamento que o contexto isolado da pesquisa de mercado já usa. Consequência real, medida pelo QA:
um código de peça que passa livre dentro de um bloco é recusado no título. Julgado aceitável
porque nada é cortado em silêncio: a publicação para, a violação é nomeada, e o operador ajusta
antes de publicar. É diferente do caso da pesquisa de mercado, onde o texto some sem aviso.
Retrato imutável provado nos dois sentidos: dois relatórios publicados antes desta fatia,
sem o campo no retrato, abrem em 200 e caem no texto genérico; e renomear depois de publicado só
alcança o cliente na publicação seguinte. O texto do card de compartilhamento continua vencendo
onde existe, verificado ao vivo, não por leitura de código.
Premissa minha corrigida pelo review: eu havia pedido para conferir a coerência de quatro
migrations sobre a função de publicação. São duas. As outras mexem na resolução de identidade.
O review mediu antes de responder em vez de aceitar o enunciado.
Achados encaminhados como cartão próprio: {F8-39} (o servidor nomeia onde está o número recusado
e a tela não mostra, e esta fatia acrescentou o título como mais uma origem no mesmo balde
indiferenciado) e {F8-38} (o resolvedor do card de compartilhamento, pronto, testado, sem chamador,
e com texto padrão que diverge do que produção usa).
**Cartão novo, medido em 2026-08-10 durante a decisão do {F8-26}, e ele é um achado colateral: o
defeito não pertence ao cartão que o encontrou.** A lista de relatórios vive dentro da ficha do
cliente, agrupada por ano, e cada linha diz mês, visualizações e comentários. **O mês exibido é o
da publicação** (published_at), e o agrupamento por ano também. A tabela guarda o período coberto
em campo próprio (reports.report.period), e nenhuma das duas informações da tela vem dele.
Por que isso mente, e não é só imprecisão: um relatório de dezembro publicado em janeiro
aparece listado sob 2027, escrito "janeiro". Quem procura o fechamento do ano passado não o
encontra onde ele deveria estar. Quanto mais tarde a agência publica, mais a lista erra, ou seja,
o erro cresce justamente quando o operador mais precisa se achar.
É irmão do {F13-03} (duas contagens divergentes de visualização): dado existe, tela desenha
outro, e ninguém percebe porque os dois coincidem no caso comum, a publicação em dia.
O que implementar: a lista passa a exibir e agrupar pelo período coberto, com a data de
publicação preservada onde ela é a informação certa (o histórico de publicação), nunca substituindo
uma pela outra em silêncio.
Como verificar (teste primeiro): um relatório cujo período coberto e mês de publicação caem em
anos diferentes é listado sob o ano do período coberto; um relatório ainda não publicado continua
aparecendo na lista, sem sumir por não ter data de publicação.
Aceite (H1): Dado um relatório que cobre dezembro e foi publicado em janeiro do ano seguinte,
Quando o operador abre a ficha do cliente, Então ele aparece agrupado no ano do período coberto e
rotulado com esse período, E um relatório sem publicação continua visível na lista.
ESCOPO AMPLIADO em 2026-08-10, durante a construção, e a ampliação é deliberada. A obra achou o
mesmo defeito no portal do cliente (ReportsPortalPage.tsx): o mesmo par exato, lê
period_start e formata published_at. Duas superfícies, uma raiz. O cartão passa a cobrir as
duas.
Por que não virou cartão separado: o portal é a tela que o cliente final abre. Fechar a
lista da agência e deixar o portal para depois inverte a prioridade (quem paga continuaria vendo
dezembro listado sob 2027) e cai na regra que este projeto já pagou várias vezes: **guarda que
cobre só parte do conjunto não guarda**, porque a metade descoberta herda o placar verde da metade
coberta. Junto veio a exigência de uma grafia só de período no produto inteiro: duas cópias da
mesma formatação divergem em poucos meses.
Aceite adicional (H1): Dado o mesmo relatório de dezembro publicado em janeiro, Quando o
cliente final abre o portal dele, Então o período que ele lê é o coberto e não o de publicação, E a
grafia do período é a mesma nas duas telas.
Achado colateral registrado e NÃO consertado aqui, de propósito: a página da ficha do cliente
transborda horizontalmente em celular, e a obra provou que é anterior e independente desta
mudança (um cartão que ela nunca tocou transborda igual). Merece cartão próprio, não carona numa
branch de outro assunto.
FECHADO em 2026-08-10, mergeado (9601b49) e deployado. QA PASS e review APPROVED, nas duas
superfícies. A varredura fechou em 2 ocorrências, medidas por três partes independentes com o
mesmo número.
O defeito que quase escapou, e foi o review quem viu: a primeira entrega passou a agrupar
pelo período e continuou ordenando pela data de publicação: ano certo, ordem errada dentro
dele. Seria o mesmo defeito do cartão deslocado do agrupamento para a ordem, e fecharia a fatia com
o sintoma vivo. Corrigido no banco (a ordenação é do banco, a da tela é redundância) e provado pelo
QA por três caminhos: consulta direta, requisição real e a tela.
A prova de mecanismo foi feita duas vezes, revertendo: o QA trocou o código pelo da main e viu
os quatro testes caírem mostrando o mês errado, e reproduziu a ordenação antiga no banco para ver a
inversão acontecer. Nenhum dos dois lados foi aceito por relato.
Achado colateral que reescreveu outro cartão: o QA observou que a formatação de período usada no
processamento também alcança a abertura do relatório publicado. Fui medir: **oito retratos
publicados guardam o período como 2026-06-01 a 2026-06-30, e o renderizador pinta** isso na
página que o cliente final abre. O {F8-34}, que tinha nascido como detalhe de tela interna, passou a
ser cartão de página pública. **Um comentário mergeado nesta própria fatia afirma que aquela
formatação "nunca é renderizada para o usuário", e é falso**. Corrigi-lo faz parte do {F8-34}.
Cartão novo, escrito em 2026-08-10, nascido do review do {F8-21}, que perguntou explicitamente
se a divergência era decisão coerente ou dívida nova, e concluiu dívida. Registrado aqui porque
nota em mensagem de commit não é fila de trabalho: some.
O que diverge. Depois do {F8-21}, a marca da agência vive na barra superior da **página
pública e a abertura fica livre para o logo do cliente. Mas o preview do compositor** e o
PDF continuam desenhando o logo da agência na abertura, pelo caminho antigo. São três
superfícies do mesmo relatório mostrando duas coisas diferentes.
Por que isso é caro, e não é preciosismo. O preview existe para o operador aprovar o que o
cliente vai receber. Se ele aprova uma abertura e o cliente abre outra, o preview deixou de ser
prova e virou ilustração. **É a mesma família de defeito que a fatia da cor já pagou neste
projeto** ({F8-08a}): a prévia desenhava com cor fixa enquanto a página publicava com a cor real,
e ninguém viu por semanas porque as duas coincidiam no caso comum.
O PDF é o caso mais grave dos dois, porque ele não tem barra superior nenhuma: quando o logo do
cliente ocupar a abertura, a marca da agência desaparece do PDF por completo se ninguém decidir
onde ela vai. Fatia que trate só o preview e esqueça o PDF resolve metade e cria um sumiço.
Depende de {F8-08} (a metade do logo do cliente): as duas superfícies só têm o que desenhar na
abertura depois que aquele cartão fechar. Enquanto ele não fechar, este fica atrás dele na fila.
RF-ADM-006, RN-020.
O que implementar: preview e PDF passam a mostrar a mesma abertura que a página pública, e a
marca da agência ganha lugar declarado no PDF, já que ali não existe barra superior. Onde exatamente
é decisão de desenho a tomar contra o mockup aprovado, não a inventar no código.
Como verificar (teste primeiro): o mesmo relatório publicado, aberto nas três superfícies com
olhos, mostra a mesma abertura; e o PDF exibe a marca da agência em lugar declarado, nunca sem ela.
Aceite (H1): Dado um relatório publicado com logo de cliente e logo de agência, Quando ele é
aberto no preview do compositor, na página pública e no PDF, Então a abertura é a mesma nas três,
E a marca da agência aparece em todas as três, E em nenhuma delas as duas marcas ocupam o mesmo
espaço.
FECHADO em 2026-08-10, mergeado (8f78726 no pacote, a136f78 no Hub) e deployado. QA PASS e
review APPROVED, os dois na segunda rodada.
A solução foi um par, não uma cópia: a barra fixa continua sendo do leitor (não faz sentido num
documento paginado), e nasceu um selo estático para o preview e o PDF. **No PDF a marca da
agência foi para o topo da primeira página**, dentro do documento. Era o risco central do cartão,
porque ali não existe barra superior e a marca sumiria por completo quando a abertura passasse a ser
do cliente.
O QA olhou as quatro combinações (agência com e sem logo, cliente com e sem logo) **nas três
superfícies**, com fixtures reais e um PDF gerado de verdade, página por página. Confirmou o selo só
na primeira página, sem duplicar, e retrato antigo abrindo nas três.
Três correções vieram do review, e a terceira é a mais instrutiva:
1. As duas marcas tinham o mesmo texto em duas implementações, sem nada garantindo paridade.
Virou fonte única para a regra, com cada uma mantendo só a própria apresentação.
2. O recorte do selo na tela falharia calado se a marcação mudasse. Agora o servidor informa se
injetou, a tela compara, e diverge alto. O review conferiu que as duas pontas são **de fato
independentes**: uma mede antes da injeção, a outra reparseia o entregue.
3. O módulo novo nascia INVISÍVEL à varredura de cor construída horas antes ({F8-36}). Ela
descobria módulos por qual função chamavam, e o selo não a chamava. **A varredura foi ampliada
para descobrir pelo que o módulo PRODUZ**, com o alcance medido: 4 módulos, sem ruído.
É o mecanismo se corrigindo no mesmo dia em que nasceu, e por um caso real em vez de hipótese:
o primeiro módulo criado depois dele já caiu no furo que o review daquela fatia havia declarado como
limite aceito.
Erro meu na validação do deploy, registrado porque quase virou alarme falso: meu comando conferia
o pacote servido contra o de disco pegando o primeiro em ordem alfabética, e o build produz dois
arquivos com o mesmo prefixo. Medi de novo: os dois são do mesmo build, o menor é fragmento
compartilhado referenciado pelo principal, e o documento aponta para o certo. **O comando de
verificação estava errado, não o deploy.**
Cartão novo, escrito em 2026-08-10 a partir do número medido pela varredura do {F8-25}, e ele
nasce por um motivo de método: o achado já estava anotado no código como defeito conhecido, e
comentário em arquivo não é fila de trabalho. Ninguém abre o repositório procurando dívida; abre o
roadmap. Enquanto ele viveu só como comentário, foi redescoberto por três obras diferentes.
O conjunto, medido, e o número foi corrigido antes de este cartão nascer: dos sítios que
substituem o rascunho por inteiro, 6 têm o padrão do {F8-25} (rede na janela) e 7 não têm:
reordenar blocos, editar propriedades, alternar visibilidade, estilo, tema, identidade e
publicar. Esses sete leem, modificam em memória e gravam de volta sem trava nenhuma, só que
a janela entre ler e gravar é curta porque não há chamada de rede dentro dela.
O sétimo apareceu só na recontagem do review, e ele é o mais sensível da lista: publicar é o ato
que congela o retrato. A obra tinha contado seis; o review recontou e achou sete. Registrado aqui
como sete porque foi o número medido por último, e a obra foi avisada para conferir e reportar
divergência em vez de adotar.
Janela curta não é janela fechada, e é isso que separa este cartão de "não é problema". Duas
requisições simultâneas do mesmo operador em duas abas, ou dois operadores da mesma agência
editando o mesmo relatório, e uma das duas edições some sem erro. O produto é multiusuário por
desenho: agência tem equipe.
Por que fica atrás do {F8-25} e não junto: aquele cartão tinha sintoma medido e prova de corrida
reproduzível; este tem risco estrutural com probabilidade menor. Misturar os dois teria inchado uma
fatia que já entregou quatro consertos, dois deles fora do que o cartão nomeava.
O que implementar: a mesma forma de trava já usada nos outros seis lugares: releitura sob trava
dentro da transação que grava. Uma forma só: se ao chegar aqui houver dialetos diferentes no
repositório, unificar faz parte da fatia, senão o próximo sítio copia o dialeto errado.
Como verificar (teste primeiro): para cada uma das seis rotas, duas escritas concorrentes de
campos distintos preservam as duas; o teste falha com o código de hoje e passa com o conserto.
Aceite (H1): Dado dois operadores da mesma agência editando o mesmo relatório ao mesmo tempo,
Quando cada um altera uma coisa diferente, Então as duas alterações sobrevivem, E nenhuma
desaparece sem erro.
FECHADO em 2026-08-10, mergeado (a8ec37b) e deployado. QA PASS e review APPROVED, os dois na
segunda rodada. Sete rotas travadas; a varredura agora conta 13 escritas sob trava e nenhuma sem.
A primeira entrega estava errada no lugar que mais importava, e os dois gates acharam sozinhos.
A rota de publicar relia sob trava apenas o card de compartilhamento. Blocos, números, tema e
identidade (o retrato inteiro) continuavam vindo sem trava, alimentando as quinze verificações
e a gravação final. Consequência dupla: edição concorrente sumia do publicado em silêncio, **e o
produto publicava um retrato que nenhuma verificação tinha examinado**, no ato que congela o
documento para o cliente.
A prova do QA é a melhor evidência produzida nesta sessão, e não é leitura de código: ele abriu
uma transação que gravava o tema e a segurou; disparou o publish; confirmou a transação no meio da
janela. Resultado antes do conserto: o rascunho tinha o tema novo, e **o retrato publicado saiu sem
ele, sem erro nenhum**. Depois do conserto, a edição sobrevive nos três campos.
**Travar a porta menor e deixar a maior aberta é o padrão que quase fechou este cartão com o placar
verde**: o card de compartilhamento estava protegido, a suíte passava, e o defeito continuava vivo
no mesmo endpoint.
Segundo defeito, que só um teste bem construído pega: a rota de estilo devolvia o bloco pelo
índice anterior à trava. O teste original não o encontrava porque o escritor concorrente **não
reordenava** a lista: os dois índices coincidiam justamente no caso testado. Reescrito para
reordenar e conferir o corpo da resposta, ele falha sem o conserto.
O custo foi declarado e verificado, não alegado: a trava passou a ser segurada durante toda a
fila de verificações. Isso só é aceitável porque publicar não faz chamada de rede nem de IA.
Review e QA confirmaram varrendo o corpo das verificações e dos auxiliares, sem encontrar nenhuma. Se
algum dia entrar uma, esta decisão vira o risco de conexão presa que outra fatia de hoje levantou.
Achados encaminhados como cartão próprio: {F8-42} (a trava de título mergeada hoje reprova uma
fixture antiga, e nasceu invisível) e o placar real da suíte da API, registrado em {F8-33}.
Cartão novo, nascido pequeno e MEDIDO grande no mesmo dia (2026-08-10). Ele começou como achado
menor do review do {F8-27}: o compositor mostra as datas no formato do banco. O QA da mesma fatia
observou que a formatação suspeita também alimenta a abertura do relatório, e fui medir.
O que a medição achou, e muda a natureza do cartão: a abertura do relatório publicado exibe
o período assim: 2026-06-01 a 2026-06-30. Confirmado nos dois extremos, não por leitura de código:
os retratos guardam essa forma, e o renderizador a pinta como texto visível na faixa de
informação da abertura (render-blocks.mjs, o trecho que emite o período do bloco de abertura). Ou
seja, não é uma tela interna do operador: é a página que o cliente final abre.
NÚMERO CORRIGIDO, e o erro foi meu, do tipo mais bobo e mais fácil de repetir. A primeira redação
dizia "oito retratos publicados". Oito era o LIMITE da minha consulta, não a contagem. Li o
tamanho da amostra como se fosse o universo. O QA foi conferir e devolveu o número certo. Medido de
novo, sem limite: **12 retratos no total, 9 com período gravado, e 2 relatórios em estado publicado
hoje.** Os três números respondem a perguntas diferentes, e misturá-los é o que produziu o erro.
Por que ninguém viu: a data está correta. É o período certo, do relatório certo. Só está
escrita para uma máquina ler, na peça mais visível do documento, e nenhuma trava reprova texto
certo em formato errado.
Uma afirmação FALSA foi mergeada hoje junto do {F8-27}, e ela é o motivo de este cartão quase ter
nascido pequeno: o comentário da nova fonte única de grafia diz que aquela formatação "nunca é
renderizada para o usuário". É renderizada, na página pública. **Corrigir esse comentário faz parte
desta fatia**: comentário que afirma fato errado desinforma pior que comentário nenhum, e este
projeto já foi mordido por isso mais de uma vez hoje.
O conjunto, para não consertar de novo pela metade: são três superfícies com o período em
formato de máquina: a abertura do relatório publicado (a que importa), o PDF, que compartilha o
renderizador, e a tela do compositor. O {F8-27} criou uma fonte única de grafia; toda superfície
fora dela é uma segunda grafia esperando divergir.
O que implementar: o período chega às superfícies já na grafia humana, vindo da fonte única, sem
que cada tela formate por conta própria. Onde o dado é congelado no retrato, decidir explicitamente
se a grafia entra no retrato ou é aplicada na exibição. Retrato antigo, com a forma de máquina,
não pode quebrar.
Como verificar (teste primeiro): publicar um relatório e abrir a página pública com olhos,
confirmando que a abertura traz o período em linguagem humana; o mesmo no PDF e no compositor; e um
retrato publicado antes desta mudança continua abrindo.
Aceite (H1): Dado um relatório publicado com período preenchido, Quando o cliente final abre o
link, Então a abertura mostra o período em linguagem humana, E o PDF e o compositor mostram a mesma
grafia, E um retrato publicado antes desta mudança continua abrindo sem quebrar.
FECHADO em 2026-08-10, mergeado (4437c64) e deployado. QA PASS e review APPROVED. O QA fez o
que o construtor não conseguiu: subiu tudo descartável e olhou (página pública anônima em tela
grande e celular, PDF real gerado, e o compositor). Sem isso, este cartão teria fechado no papel,
porque o defeito dele só existe para quem olha.
Terceira superfície achada durante a obra: a barra do compositor lia as datas cruas direto na
tela. Como ela lê ao vivo, e não do retrato, passou a mostrar a grafia humana **até em relatórios
antigos**, conserto que alcança o passado sem republicar.
Decisão declarada e julgada: a grafia humana entra no retrato a partir de agora. Relatório
publicado antes continua com a forma de máquina até ser republicado. Aceito porque é o **mesmo
padrão** de congelamento que o título e a identidade já usam neste produto. Divergir aqui criaria
uma segunda regra de congelamento. Republicar já é o mecanismo documentado.
O texto enviado à IA mudou junto, de propósito, e o QA conferiu que a composição não degradou.
A LACUNA DE DEPLOY que este cartão revelou, e ela quase entregou o site sem a mudança: a fatia
criou um pacote novo de workspace, e o build do front falhou na main por não achá-lo: o
elo do pacote não existia no checkout de produção. O pnpm install o criou e o build passou. **Só
não virou entrega silenciosa pela metade porque o build é feito antes de publicar**: um fluxo que
publicasse o diretório de saída sem conferir o build teria copiado o pacote anterior, e o site
ficaria no ar sem a mudança, com o merge verde e o cartão fechado. Pacote de workspace novo exige
encadeamento no checkout antes de construir.
Número meu corrigido pelo QA: a redação inicial dizia "oito retratos publicados", e **oito era o
limite da minha consulta**. Medido sem limite: 12 retratos, 9 com período, 2 relatórios publicados.
Achado encaminhado: {F8-40} (o período congela na composição e o título na publicação, dois
momentos diferentes para dois campos vizinhos da mesma abertura).
**Cartão novo, medido em 2026-08-10 antes de mergear o {F8-25}, e ele é o mais estrutural deste
dia. O pacote apps/api do Hub tem 30 arquivos de teste e zero script que execute o
conjunto**: não há test no pacote, não há tarefa equivalente na raiz, não há CI no repositório.
CORREÇÃO IMPORTANTE, e ela foi feita medindo, não relendo: a primeira redação deste cartão dizia
que os arquivos "não rodam por comando nenhum" e que 30 deles falhavam. **As duas coisas estavam
erradas, e o erro nasceu de apontar o executor errado para eles.** Esses arquivos seguem outra
convenção: são programas autônomos, executados um a um, e passam. O arquivo exercitado para
conferir devolveu 7 de 7, incluindo casos adversariais escritos por revisões anteriores. O que não
existe é o comando que roda todos.
Por que o erro merece ficar escrito: um relato de subagent trouxe "30 falharam" e a redação
anterior o adotou. O número era verdadeiro para o executor que ele usou e falso sobre o produto. É
a mesma armadilha que este projeto já nomeou em outro contexto (número que vem pronto também se
mede) e ela é mais perigosa aqui porque o número confirmava a hipótese de quem o recebeu.
O contraste dentro da mesma casa mede o tamanho do buraco: o pacote do módulo
(modularreports-dev) tem cerca de sessenta comandos de teste, incluindo um que roda tudo e outro
que roda a suíte contra banco real. É por isso que todo relato de fatia traz números do módulo
("1408 testes, 143 de integração") e nenhum traz número da API do Hub. Ninguém escondeu nada:
não havia o que contar.
Por que isto invalida promessas já dadas como cumpridas. A guarda entregue pelo {F8-25} promete
que a quarta ocorrência da corrida "não passa em silêncio". Ela mora aqui. Como nada a executa, a
promessa é falsa como entregue, e o mesmo vale para qualquer trava que qualquer fatia anterior
tenha deixado neste pacote. **São duas regras cravadas deste projeto se encontrando no mesmo
ponto:** "nunca falhou" e "nunca rodou" são indistinguíveis, e *guarda que falha aberta em
silêncio não guarda*.
O que implementar: um comando que execute o conjunto na convenção que os arquivos já usam,
agregando os resultados num placar único. **NÃO trocar a convenção de 30 arquivos para acomodar o
comando**: isso é reescrever o corpo de testes inteiro por causa do encanamento, e nenhum deles
pediu para ser reescrito.
A armadilha concreta, e ela quase entrou: um executor de suítes apontado para esses arquivos
reporta 30 falhas que não existem. Um comando test nesse formato é pior que nenhum comando,
porque treina o time a ignorar vermelho, e vermelho ignorado é como uma falha de verdade passa.
**DOIS FATOS MEDIDOS EM 2026-08-10 que quem construir esta fatia vai encontrar, e é melhor saber
antes:**
1. Parte dos arquivos só roda com ambiente e dentro da rede certa. Rodá-los do jeito ingênuo
produz erro de validação de variáveis ausentes, e mesmo com as variáveis carregadas eles falham
ao resolver nomes de container que só existem dentro da rede do Docker. **Isso não é teste podre
nem produto quebrado**: é infraestrutura de execução que nunca foi montada, e é metade do motivo
de não existir comando.
2. E este é o pior: um arquivo imprime "erro fatal" e termina com código de sucesso. Medido:
placar "0 PASS, 0 FAIL, mais erro fatal" e código de saída zero. **Qualquer executor o contaria
como aprovado.** Um comando construído sem tratar isso nasce mentindo: verde por fora, sem ter
executado nada por dentro. Vale conferir quantos arquivos têm esse comportamento antes de confiar
no primeiro placar.
O PLACAR REAL, medido em 2026-08-10 pelo QA do {F8-30}, que rodou os 33 arquivos um a um:
cerca de 67 casos reprovando, concentrados em sete arquivos, **idênticos com e sem a fatia que
ele estava validando**, ou seja, já estavam assim na main. Três outros arquivos nem chegam a
rodar, por ambiente puro (falta de uma ferramenta na imagem de teste, e dois que importam caminhos
de worktree que não existem mais). Um oscila entre execuções, por depender de rede real.
Isto deixa de ser hipótese e vira o argumento do cartão. Ninguém escondeu nada e ninguém foi
desleixado: cada fatia mediu o que tocou e reportou verde honestamente. Como nada executa o
conjunto, as reprovações se acumularam sem dono, e a primeira vez que alguém somou tudo foi por
acaso, comparando placares de outra coisa. Uma delas ({F8-42}) é regressão de uma fatia mergeada
hoje, que nasceu invisível pelo mesmo motivo.
Consequência para o desenho da fatia: o primeiro placar vai vir muito vermelho, e isso é o
produto, não o fracasso. **Separar "reprova por defeito" de "não roda por ambiente" faz parte do
trabalho**: misturar os dois produz um comando que ninguém confia, e comando que ninguém confia é
igual a comando nenhum.
Como verificar (teste primeiro): um único comando executa os 33 arquivos e imprime um placar
que bate com a execução individual; um teste plantado que deve falhar aparece vermelho nesse placar;
o comando reprova quando qualquer arquivo reprova, nunca só o último; **e um arquivo que morre com
erro fatal aparece como reprovado, nunca como aprovado por código de saída zero.**
Aceite (H1): Dado o pacote da API do Hub, Quando alguém roda o comando de teste do projeto,
Então os 33 arquivos são executados e o placar reflete a execução individual de cada um, E um teste
que deve reprovar aparece como reprovado, E nenhum arquivo é pulado em silêncio.
FECHADO em 2026-08-10, mergeado (c601104). QA PASS e review APPROVED. Entrega aditiva: um
script que agrega as execuções individuais, sem trocar a convenção de nenhum arquivo, mais a
documentação de como rodar (incluindo a exigência de ambiente e de estar dentro da rede do Docker,
que é metade do motivo de não existir comando antes).
A prova é ter tentado enganá-lo. O QA plantou três arquivos: um que **imprime reprovação e sai
com código de sucesso (o golpe que existe de verdade no repositório), um mudo**, e um que reprova
honestamente. Os três foram classificados certo: o mudo vira "não rodou", nunca verde. E um arquivo
novo entrou na varredura sem ninguém registrá-lo.
**O PLACAR REAL, medido por três partes com o mesmo número: 35 arquivos, 24 aprovados, 10 reprovando
(73 casos) e 1 que não roda.** O número de arquivos também foi corrigido: eu vinha escrevendo 30 e
depois 33.
O comando se pagou na primeira execução, e o achado é sobre esta sessão: **34 das 73 reprovações,
em 6 arquivos, são a mesma regressão: a trava de título do {F8-26}, que eu mergeei e deployei
hoje, recusando fixtures antigas cujo nome contém número. Eu havia registrado isso como um**
arquivo, porque era o que estava medido quando outro QA tropeçou nele. Enquanto se media arquivo a
arquivo, a regressão parecia caso isolado.
É a demonstração do próprio cartão: não faltou disciplina a ninguém. Cada fatia mediu o que
tocou e reportou verde com honestidade. O que faltava era alguém somar, e ninguém somava porque
não havia comando. Os 6 arquivos restantes agora têm número, lista e assinatura, trabalho seguinte,
com o número na mão.
FECHADO em 2026-10-16, merge Hub d15b8c6 (carimbo 7abf24e; review APPROVED em 4 rodadas, qa PASS
na rodada 2). As 3 guardas em produção: (1) direct-connection-guard no test-all (teste não abre conexão
fora do db.ts); (2) migration 0272 aplicada em produção, CHECK (is_global OR company_id IS NOT NULL)
em reports.template, validada (0 violadores; backup em /root/f8-41-bkp/); (3) `GET
/api/health/dado-de-teste` no ar (200, tudo zero), cadastro como alvo do Vigia em curso. API 15/15.
**Remocao do dado FEITA em 2026-10-15; o cartao segue aberto ate as 3 guardas do plano entrarem
(meta-guarda de Pool nos testes, CHECK em reports.template, sentinela no Vigia).** Apagado em
producao: relatorio 037b52fe (QA temporario do {F8-21}, 10/08, Agencia Teste) com cascata de 1
snapshot e 3 eventos de link (1+1+3), contagens antes/depois conferidas na mesma transacao; as demais
tabelas de reports e hub.accounts/companies iguais. 21 pastas vazias de relatorio removidas do disco
(0 arquivos, 0 pastas restantes). Backup das linhas exatas em /root/f8-41-bkp/ (modo 700/600, sha256).
/api/health 200. Evidencia em /opt/modularhub-evidencias/f8-41/.
Guardas em implementacao (branch feat/f8-41, 2026-10-15): (1) verificador de conexao direta em
teste (scanner que respeita string e regex literal); (2) migration 0272, `CHECK (is_global OR
company_id IS NOT NULL) em reports.template; (3) rota publica GET /health/dado-de-teste` (so
contagens, cache 60 s) para o Vigia. Limite declarado do detector de nome: casa o cartao no inicio
do nome como a suite cria (F8-40, F4-07c, F847, F6-OKR-03, SMK-nn, Cliente Teste/X/Y/Z, prefixo
FIXTURE ); falso positivo aceito: cliente cujo nome comece por F + 3 ou 4 digitos ou por "FIXTURE ";
falso negativo aceito: dado de teste com nome livre, e as empresas descartaveis em si (so as contas e
relatorios delas sao contados). Por isso todo teste novo usa escopoDeFixture / fix.nome.
Cartão novo, medido em 2026-08-10 durante a verificação de resíduo do {F8-39}. Fui conferir se
aquela obra tinha limpado o que criou (tinha) e encontrei o acúmulo de todas as anteriores.
Os números, medidos: 23 relatórios no banco de produção, e todos são fixture de teste. Nenhum
dado de cliente real. Distribuição: 11 rascunhos, 10 revogados e 2 publicados com link vivo; 11
deles têm link que nunca expira. Há títulos explicitamente marcados como temporários, com a
própria instrução de revogar depois do smoke, ainda de pé.
O que torna isto mais que sujeira: cinco relatórios estão sob a conta de uma **agência parceira
real**, não sob a nossa. Quem entrar por lá vê um cliente chamado "Cliente Smoke" na própria lista
de clientes. É o nosso andaime aparecendo na casa de quem confia na plataforma.
A causa não é descuido de uma sessão, é do processo: cada obra declarou "fixtures limpas no fim",
e cada uma estava dizendo a verdade sobre o que ela mesma criou. Ninguém é dono do conjunto. É a
mesma forma de defeito que este projeto já nomeou em código (guarda por ponto de chamada nomeado
deixa o conjunto descoberto), só que aplicada a dados.
O que implementar, e a ordem importa:
1. Inventário nominal dos 23, classificando cada um: descartável, ou algo que alguém ainda usa.
Apagar dado de produção é irreversível e exige aprovação explícita do dono. O inventário é o
que torna essa aprovação possível de dar em um minuto.
2. Revogar antes de apagar os links vivos, que é reversível e tira a exposição imediata.
3. Fechar a torneira: obra de verificação passa a criar fixture sob uma conta reconhecível e
isolada, nunca sob agência parceira, e com expiração curta por construção, não por disciplina de
quem lembra.
Como verificar (teste primeiro): a lista de clientes da agência parceira não mostra nenhuma conta
de teste; e uma obra nova que crie fixture não consegue criá-la fora da conta isolada.
Aceite (H1): Dado o banco de produção, Quando alguém de uma agência parceira abre a lista de
clientes dela, Então nenhuma conta de teste nossa aparece, E nenhum link de teste segue vivo sem
expiração, E fixture nova nasce isolada por construção.
INVENTARIO NOMINAL (passo 1) FEITO em 2026-09-21, e ele MUDA o cartao.
- O acervo nao e 23, sao 262, medidos em
reports.report: 167 rascunhos e 95 revogados,
zero publicados. O cartao foi escrito em 10/08 e o numero cresceu mais de dez vezes desde entao, o que confirma a tese dele: a torneira segue aberta e ninguem e dono do conjunto.
- Por agencia:
F4-05 Market Research Smoke B164 (141 com link vivo sem expiracao),
Agencia Teste 59, Modulareasy 32, ADS 4 PROS [ADOS] 6, F14 WL Agencia A 1. As quatro primeiras sao contas nossas; a exposicao real e a quinta.
- Os 6 da parceira, nominalmente: cinco estao sob um cliente chamado *Cliente Smoke
ModularReports verificacao branding* (fixture nossa, 05 a 10/08, um deles com a propria instrucao "temp, revogar apos smoke" no titulo) e um esta sob AutoWorx [AURX], que e CLIENTE REAL da parceira (conta criada em 01/07), com titulo "Teste", criado em 14/08.
- PASSO 2 EXECUTADO, e so nele: o unico rascunho com link vivo sob o Cliente Smoke
(2a0c84b6) foi revogado, com retrato antes e depois. Revogar e reversivel e tira a exposicao imediata, que e o que o proprio cartao manda fazer antes de apagar.
- PERGUNTA REGISTRADA, e o loop seguiu: o relatorio "Teste" sob
AutoWorx**nao foi
tocado. Ele e posterior a medicao original do cartao, esta dentro de um cliente real, e nao ha trilha que diga quem o criou (nao ha registro de autoria na tabela). Pode ser fixture nossa colocada no lugar errado ou pode ser o proprio parceiro experimentando a plataforma. Revogar trabalho do parceiro por deducao seria pior que a exposicao que se quer remover.** Quem pode responder em um minuto e o dono, perguntando a ADS 4 PROS.
- O que este inventario ensina sobre o escopo: o passo 3 (fechar a torneira) deixou de ser o
menos urgente. Enquanto ele nao existir, cada obra nova soma fixture, e o proximo inventario achara um numero maior que 262. A ordem util passou a ser 3, depois 1 e 2 de novo, porque inventariar um acervo que cresce e enxugar gelo.
PASSO 2 CONCLUIDO em 2026-09-21, com GO do dono para os da parceira. Palavras dele sobre o relatorio que eu havia segurado: "A conta de cliente e real mas as acoes na ferramenta foram testes. Os relatorios nao sao reais ainda." Com isso:
- ADS 4 PROS: 6 de 6 revogados, incluindo o que estava sob o cliente real
AutoWorx.
Links vivos sem expiracao na parceira: 0. Retrato tirado antes e depois.
F4-05 Market Research Smoke B: 141 revogados de uma vez. E conta de smoke pelo proprio
nome, e os titulos confirmam (Wire A a Wire G, da fatia F4-05). Reversivel.
- Sobraram 24 links vivos, todos em contas NOSSAS e nenhum em parceira:
Modulareasy22,
F14 WL Agencia A 1, Agencia Teste 1.
- Os 22 da
ModulareasyNAO foram tocados de proposito, e e triagem nominal pendente: ali
fixture e trabalho real convivem. QA sem logo agencia branding real, teste e Smoke direct fetch sao nossos de teste, mas Fechamento Julho 2026, Diagnostico do semestre e Lancamento Inverno tem cara de relatorio real de dogfooding. Revogar em bloco aqui seria o mesmo erro que segurar o AutoWorx evitou, so que na nossa casa. PASSO 3 EXECUTADO em 2026-09-22, com GO do dono e conferencia cruzada do fiscal a cada lote. O acervo foi de 262 relatorios para 2, e as duas primeiras pernas do H1 fecham: zero contas de teste visiveis para a parceira e zero links vivos sem expiracao.
| inicio | fim | |
|---|---|---|
| empresas | 93 | 4 |
| contas | 182 | 89 |
| relatorios | 262 | 2 |
| pessoas | 75 | 75 |
| projetos DR-X | 9 | 9 |
O que protegeu a execucao, e vale mais que o resultado:
- Identificar por NOME apagaria 57 empresas de uma vez. As 93 empresas tinham apenas **11
nomes distintos** (uma fixture repetida 57 vezes, outra 24). Toda exclusao foi por id.
- 155 de 159 chaves estrangeiras para
hub.companiessaoCASCADE, alcancando 163 tabelas:
errar a identificacao nao erraria uma linha, erraria um banco.
- Trava por lote exigindo o numero exato, com
DR-Xeusers_profileconferidos **dentro da
mesma transacao**; se qualquer um mexesse, abortava. Nenhum dos dois mudou em momento algum.
- Backup aberto antes de usar (
pg_restore --list, 230 tabelas com dados), nao so criado.
A "Agencia Teste" foi preservada e a medicao provou que estava certo: e a maior base do ambiente (44.899 linhas em 67 tabelas) e guarda os 9 projetos DR-X. Sairam dela apenas as 60 contas de smoke criadas em lote, nunca a empresa. A TERCEIRA PERNA CONTINUA ABERTA, e e a que importa para nao repetir: a torneira. Acervo zerado hoje nao impede o proximo smoke de plantar 57 empresas amanha, e foi exatamente esse padrao que produziu o acervo. Enquanto obra de verificacao puder criar fixture fora de uma conta isolada e sem expiracao por construcao, este cartao volta.
ATUALIZADO em 2026-08-18: passos 2 e 3 FEITOS e no ar. So o passo 1 (apagar) segue aberto, porque e irreversivel. O PLACAR MULTIPLICOU POR 11 EM 8 DIAS, e recontar mudou a natureza do problema: o cartao registrou 23 relatorios em 10/08. Hoje sao 262, com 84 PUBLICADOS (o cartao dizia 2), todos sem expiracao e todos sem senha. Numero velho de cartao subestima; a diferenca entre "sujeira acumulada" e "84 URLs publicas abertas" estava em medir de novo antes de agir. Passo 2 (revogar) FEITO. Os 84 foram revogados sob uma guarda que aborta se algum publicado estiver sob conta que nao case o padrao de teste; a consulta de controle devolveu zero, entao nenhum dado de cliente foi tocado. Provado ao vivo: um token que respondia 200 agora responde 410. Restam 0 publicados. Isso e reversivel e era o que tirava a exposicao imediata. Passo 3 (torneira) FEITO. A causa nunca foi descuido: cada obra limpou o que ela mesma criou e estava dizendo a verdade. Faltava escopo. O padrao da casa manda derivar o ator do banco em vez de escreve-lo a mao, e isso esta certo, mas a derivacao pega a primeira agencia que casa o criterio, e essa pode ser a de um cliente real. Foi assim que a conta "Cliente Smoke" foi parar na agencia parceira: ninguem decidiu, o teste perguntou "me da um team_manager" e o banco respondeu com um de verdade. fixtures/escopo-de-fixture.ts troca "a primeira que aparecer" por "a de fixtures, ou falha" (fail-closed), e a guarda e por excecao nomeada: os 31 legados ficam declarados e congelados, e arquivo novo ja nasce coberto. A guarda me pegou enquanto eu a escrevia: montei a lista de legados a mao a partir de um head -20 truncado, e ela acusou os 11 que faltavam. Regenerei a lista pela varredura. E a tese do proprio cartao acontecendo com quem o estava fechando. Tambem limpo nesta sessao: os 8 arquivos de artefato orfaos do {F8-59} (todos sob conta de smoke da casa; o disco agora tem 16 arquivos para 16 linhas, aritmetica fechada) e uma conta de fixture que eu deixei na V4 Percus nesta mesma sessao, por ordem errada no meu proprio codigo de teste (registro de limpeza depois do passo que podia falhar, corrigido em 1bc977a). O QUE FALTA, E E A UNICA COISA QUE DEPENDE DO DONO: apagar as contas e relatorios de fixture. Nada disso e dado de cliente (medido: nenhum publicado estava fora de conta de teste). A exposicao ja esta neutralizada pela revogacao, entao nao ha urgencia; o que resta e higiene. Distribuicao das contas de teste por agencia: Agencia Teste 67, F4-05 Market Research Smoke B 24, Modulareasy 3, F8-25 Race Smoke Artifacts B 2, F4-07a Smoke B 1, e V4 Percus 1 (esta ultima e a que aparece para uma agencia parceira: 3ef6481f, "Cliente Smoke ModularReports verificacao branding", com 5 relatorios, todos ja revogados). Tres agencias inteiras (F4-05 Market Research Smoke B, F8-25 Race Smoke Artifacts B, F4-07a Smoke B) sao fixture e podem ir junto. Merge d690a4e.
ESCOPO CORRIGIDO no mesmo dia, e o número saltou de 1 para 7. A primeira redação registrava um
arquivo, que era o único conhecido quando o QA do {F8-30} tropeçou nele. Assim que o comando de
teste do {F8-33} rodou o conjunto pela primeira vez, apareceram **mais seis arquivos com a assinatura
exata: número sem origem, apontando o título. Cerca de 34 casos** ao todo.
A lição do salto é a do próprio cartão: o alcance só apareceu quando alguém executou o conjunto.
Enquanto se media arquivo a arquivo, a regressão parecia um caso isolado, e eu a registrei assim,
com confiança, porque era o que estava medido.
Cartão novo, medido em 2026-08-10 pelo QA do {F8-30}, e é uma regressão que ESTA sessão publicou.
O {F8-26} pôs o título dentro da disciplina de número e foi mergeado e deployado hoje. Um arquivo
de teste pré-existente usa um título de fixture que contém um agrupamento de três dígitos, e a trava
nova recusa a publicação por causa dele. Seis testes daquele arquivo caem, em cascata.
O produto está certo; o teste é que ficou inválido. A trava faz exatamente o que foi desenhada
para fazer, e a decisão de recusar código no título está registrada e aceita. O que quebrou foi uma
fixture escrita quando essa regra não existia.
O que torna isto grave não é o defeito, é o modo como escapou. O QA do {F8-26} reportou 40 de
40 verdes. E aquele número não cobria este arquivo. Como nada no projeto executa a suíte da API
por comando ({F8-33}), a reprovação nasceu invisível: só apareceu porque outra fatia, dois cartões
depois, resolveu rodar os 33 arquivos um a um para comparar placares.
É a forma de defeito que este projeto mais paga, agora aplicada a testes: cada gate mediu o que
ele mesmo tocou, ninguém mediu o conjunto, e o placar verde de cada parte somou um todo vermelho.
Cuidado ao consertar: trocar o título da fixture é o conserto óbvio, mas confira antes **o que
aquele arquivo testa**. Se ele publica de propósito com título arbitrário, trocar o texto pode
esconder um segundo problema em vez de resolver o primeiro.
Como verificar (teste primeiro): o arquivo passa por inteiro; e um título com número cru continua
sendo recusado pela trava, provado no mesmo arquivo.
Aceite (H1): Dado o arquivo de teste de identidade, Quando ele é executado, Então todos os casos
passam, E a trava de título continua recusando número sem origem.
PARCIALMENTE FECHADO em 2026-08-10, mergeado (c601104): 1 dos 7 arquivos. O arquivo de
identidade voltou a 40 de 40, e a trava continua recusando número cru, confirmado pelo teste
dedicado dela, intocado: trocar a fixture não afrouxou nada, e o arquivo não testava o texto do
título, só o usava como rótulo.
FECHADO POR INTEIRO em 2026-08-10, mergeado (20e9a4a). Os 6 arquivos restantes entraram numa
segunda fatia, com QA PASS e review APPROVED. A suíte da API foi de 23 para 30 arquivos verdes, e
a assinatura da regressão desapareceu do log: zero ocorrências, medido por três partes.
O risco desta fatia não era o conserto, era ele esconder outra coisa. Trocar o texto do título é
o conserto óbvio, e se em algum arquivo o título importasse para o que estava sendo testado, a troca
teria mascarado o caso. Os dois gates conferiram arquivo a arquivo: o título só entra na criação
do relatório e nunca é lido de volta nem comparado em asserção nenhuma. O diff toca só arquivos
de teste; nenhuma linha de produção, nenhuma verificação afrouxada.
O review foi além e mediu o mecanismo: para o título, o cruzamento contra dados do bloco não
existe, então qualquer dígito reprova, não só código com sigla, como se poderia supor lendo o
cartão. Conferiu que os 32 títulos novos não contêm um único dígito, ou seja, não plantam a próxima
reprovação.
A trava seguiu intocada e exercitada, com o par de casos ao vivo: título limpo publica, título
com número cru recusa apontando o título.
O que sobrou reprovando na suíte não é desta família: quatro arquivos, um sensível ao caminho da
pasta e três de tempo em testes de corrida. Nomeados, para não serem confundidos com esta regressão.
Cartão novo, do achado menor do review do {F8-31} em 2026-08-10. Enquanto aquela fatia tirava a
chamada de IA de dentro da transação, ficou registrado que **o serviço interno que analisa o arquivo
enviado continua dentro dela, e sem limite de tempo declarado**.
É o mesmo defeito com outro nome. O {F8-31} nasceu porque conexão presa durante trabalho longo
esgota o conjunto e para o Hub inteiro, não só o módulo de relatórios. Aquele cartão mirou a IA
porque era o caso medido; a análise de arquivo prende a conexão pela mesma razão, e sem teto pode
prender por tempo indefinido.
Por que ficou de fora, e a decisão foi certa: o {F8-31} tinha caso medido, prova de esgotamento e
desenho pronto para copiar. Misturar teria inchado a fatia e adiado a correção que já estava provada.
Cuidado ao construir: esse serviço roda em processo separado, por desenho de segurança. Ele é o
único com acesso ao motor de contêineres, justamente para a API pública não ter. **Não desfaça esse
isolamento** para resolver o tempo; a saída é a mesma do {F8-31} (ficar fora da transação) mais um
teto próprio.
Como verificar (teste primeiro): com o conjunto de conexões reduzido, uma análise demorada não
bloqueia requisição comum de outro módulo; e uma análise que passe do teto é interrompida com erro
nomeado, sem deixar registro pela metade.
Aceite (H1): Dada uma análise de arquivo demorada em curso, Quando chega uma requisição comum de
outra parte do Hub, Então ela é atendida normalmente, E a análise que ultrapassa o teto é
interrompida com erro nomeado.
FECHADO em 2026-08-10, mergeado (4d9bb08) e deployado. QA PASS e review APPROVED.
O alcance era TRÊS rotas, não uma. O cartão previa só a de criar artefato; a obra mediu e achou
também as duas de importação, que chamam o mesmo serviço. Consertar a nomeada e deixar as duas
teria fechado o cartão com o defeito vivo em dois terços dos casos.
A lição da fatia anterior foi carregada para dentro desta, e funcionou. O {F8-31} perdeu
atomicidade justamente ao aplicar o desenho de duas etapas numa rota cuja primeira etapa escrevia.
A instrução desta fatia trouxe esse alerta, e o review confirmou: nas três rotas a primeira etapa é
leitura pura. O defeito não se repetiu.
O QA testou o que o autor não testou, e é o motivo de o gate existir: forçou falha real de banco
no meio do lote das duas rotas de importação. Resultado: zero linhas sobreviventes (inclusive as
válidas gravadas antes, desfeitas por rollback) e nova tentativa produzindo exatamente três. Nenhum
teste do autor cobria esse caminho.
O número do teto veio de medição, e foi medido duas vezes. O autor: lote de 20 arquivos do
conjunto real, pior caso 1,8 segundo. O QA foi além: gerou 96 MB de arquivos reais, perto do teto da
quota, e mediu 7,8 segundos, ainda cerca de oito vezes de folga sobre os 60 segundos. No caminho
achou um limite por arquivo que não estava documentado.
LIMITE DECLARADO, e ele foi MEDIDO em vez de suposto: o teto fecha a conexão do nosso lado;
não cancela o contêiner que já está trabalhando. Medido com eventos do motor de contêineres: a
nossa ponta desistiu aos 180 milissegundos e o contêiner só terminou aos 792, concluindo
normalmente, sem receber sinal nenhum. Isso não invalida a fatia: o alvo era soltar a conexão do
banco, e isso acontece. Virou {F8-44}, com o conserto no serviço de análise, no outro repositório.
Placar honesto, com divergência reportada em vez de ajustada: o autor mediu 34 aprovados e 5
reprovando; o QA mediu 33 e 6. A diferença é um arquivo com instabilidade de IA real, já documentada,
que oscilou entre as duas rodadas. O conjunto de reprovações é idêntico ao da main: zero falha
nova.
Produção não mudou: a variável do teto não está definida lá, e o padrão é o valor medido.
Cartão novo, escrito em 2026-08-10 a partir de MEDIÇÃO feita durante o {F8-43}, não de suposição.
O que foi medido: com o teto do cliente forçado a 150 milissegundos, a nossa ponta desistiu em
180. O contêiner de análise tinha acabado de arrancar aos 185 e só terminou aos 792: trabalho
normal, concluído com sucesso, nunca interrompido. São cerca de **610 milissegundos de trabalho
remoto depois de ninguém mais estar esperando**, e o número escala com o tamanho do arquivo.
Confirmado no código: o serviço de análise nunca liga a queda da conexão de entrada a um
encerramento do processo que ele disparou.
Por que isto NÃO invalida o {F8-43}: o alvo daquela fatia era soltar a conexão do banco, e
isso acontece de qualquer jeito. O que sobra é desperdício de recurso do lado do serviço, não
travamento do Hub.
Por que ainda assim é dívida: sob carga, cada análise abandonada continua ocupando o serviço de
análise por todo o seu tempo natural. Quem desiste não libera nada lá; e como o serviço é o único
com acesso ao motor de contêineres, é ele o gargalo que sobra.
Onde mora o conserto: no serviço de análise, no outro repositório, fora do alcance da fatia que
o encontrou. Ele precisa observar a queda da conexão de entrada e encerrar o processo que disparou.
Cuidado que não pode ser esquecido: encerrar processo de contêiner é operação privilegiada, e
esse serviço é isolado por desenho justamente por isso. **A correção não pode alargar quem tem esse
acesso**: ela acontece dentro de quem já o tem.
Como verificar (teste primeiro): com o teto do cliente reduzido, o contêiner disparado é
encerrado logo após a desistência, e não segue até o fim natural. O teste precisa falhar hoje.
Aceite (H1): Dada uma análise em curso e um cliente que desiste por tempo, Quando a conexão cai,
Então o processo de análise é encerrado, E nenhum contêiner segue trabalhando sem alguém esperando.
FECHADO em 2026-08-10, mergeado (db1101d) e deployado. QA PASS e review APPROVED.
Este cartão INTRODUZIU um poder destrutivo, e por isso foi o mais escrutinado do dia: encerrar
processos de contêiner, dentro do único serviço com acesso ao motor. As duas perguntas que decidiam
se podia ir ao ar foram respondidas com medição:
1. O alvo pode ser influenciado de fora? Não. O nome vem sempre da própria execução, os três
chamadores de produção nunca o fornecem (só o teste injeta), e não há shell no caminho.
2. Pode matar a análise errada? O QA forçou o caso que a suíte não cobria: **três análises
simultâneas, cancelando duas quase ao mesmo tempo**. Só as duas morreram; a terceira concluiu
intocada. Trocar desperdício por corrupção era o risco real desta fatia.
Prova por eventos do motor de contêineres, com controle negativo: encerramento forçado quando há
cancelamento, término normal quando não há. Nenhum resíduo em disco nem contêiner órfão.
Duas rodadas de ajuste no registro de falha, e a segunda corrige a primeira: o tratamento
original engolia qualquer erro do encerramento, escondendo até uma falha real de acesso ao motor,
e o sintoma visível seria apenas o desperdício voltando, com esta fatia mergeada e verde. Ao
corrigir, porém, o caso benigno (cancelar contêiner que já saiu sozinho) passou a gerar erro em
toda desistência tardia. Detector ruidoso vira catraca: em duas semanas ninguém leria o log, e a
falha real se perderia no ruído. Os dois casos foram separados, com o par provado contra motor real,
inclusive apontando para um destino inalcançável para gerar a falha verdadeira.
Efeito colateral pego pela própria suíte: o auxiliar novo quebrou a verificação de contrato do
barril, que exige todo export público declarado. Classificado como interno.
Nota de processo, declarada: os gates validaram o commit anterior às duas rodadas de log; o delta
posterior é registro de falha, sem alterar a decisão de encerrar nem o desfecho da chamada, com o par
provado e a suíte inteira verde.
Isolamento confirmado no deploy: o acesso ao motor de contêineres segue exclusivo deste serviço.
O leitor e a API continuam sem ele.
**Cartão novo, do achado menor do review do {F8-34} em 2026-08-10, e ele só ficou visível porque as
duas fatias entraram no mesmo dia.**
O desencontro: o título é congelado no ato da publicação ({F8-26}); o período é
congelado no ato da composição ({F8-34}). São dois campos vizinhos na mesma abertura, com dois
momentos de congelamento diferentes.
O que isso produz na prática: entre compor e publicar existe curadoria, e o operador pode
editar. Se ele mudar as datas do relatório depois de compor e publicar sem recompor, a abertura
mostra o período antigo enquanto o resto do documento e o cadastro dizem outro. Ninguém recebe erro:
os dois valores são "certos", cada um do seu momento.
Por que isso é da família mais perigosa deste projeto: é divergência entre camadas em que **as
duas pontas parecem corretas**. Não há teste que reprove, porque nenhum lado está errado
isoladamente, só o par está.
Anterior às duas fatias, e revelado por elas: o congelamento na composição já existia para
outros valores da abertura. O {F8-34} não criou o desencontro, tornou-o visível ao pôr ali um campo
que o operador edita com frequência.
O que implementar: um momento de congelamento só para o que a abertura mostra, ou uma
reconciliação explícita no publish que atualize o que ficou para trás. **Escolher qual é parte da
fatia**, e a escolha precisa olhar o que mais é congelado na composição, não só estes dois campos.
Como verificar (teste primeiro): compor, alterar as datas do relatório, publicar sem recompor, e
conferir que a abertura publicada não contradiz o período do relatório.
Aceite (H1): Dado um relatório composto e depois editado nas datas, Quando o operador publica
sem recompor, Então a abertura publicada não contradiz o período do relatório, E nenhum campo da
abertura mostra valor de um momento diferente dos vizinhos.
FECHADO em 2026-08-10, mergeado (83c0e69 no pacote, 32fcac3 no Hub) e deployado. QA PASS e
review APPROVED, os dois na segunda rodada. Caminho escolhido: reconciliar no publish a partir do
estado atual, seguindo o padrão que o produto já usa para identidade e para o bloco de confiabilidade,
e aplicado também ao preview, para as duas superfícies não poderem divergir.
O achado que REABRE parcialmente uma entrega do mesmo dia, e foi VISTO, não deduzido: o {F8-26}
levou o título ao cliente pela aba do navegador, **mas o texto visível no corpo da abertura continuou
congelado no valor da composição**. O QA publicou um relatório real, renomeou sem recompor, publicou
e olhou: aba com o título novo, corpo com o velho, com captura de tela. **O review havia concluído
o contrário lendo o código**, e eu repassei essa conclusão ao autor, errado nas duas pontas.
Regra de despacho que sai daqui, porque aconteceu duas vezes hoje: quando review e QA divergem
sobre fato observável, quem publicou e olhou tem precedência sobre quem leu o arquivo. O review
aceitou a correção e a registrou no próprio gate.
A regressão que a fatia INTRODUZIU, e ela era sistêmica: ao reconciliar, o título e o período
passaram a ser injetados sempre nas propriedades da abertura, e essas propriedades alimentam a
extração de texto da trava de anti-fadiga. Como título e período sempre diferem entre
relatórios consecutivos do mesmo cliente, a medida de similaridade se diluía e **a trava parava de
disparar. Ou seja: a fatia consertava uma divergência visual e desligava uma trava de honestidade
já em produção**, em silêncio, para todos os relatórios. Achada pelo QA, reproduzida de forma
determinística, invisível ao review.
O conserto foi mais preciso do que a sugestão, e o cuidado merece registro: o período foi para a
lista global de exclusão, porque só existe na abertura; o outro campo NÃO, porque é narrativa
legítima em outros sete tipos de bloco: a exclusão dele foi escopada ao tipo. Uma exclusão
global teria trocado uma regressão por outra maior, enfraquecendo a trava naqueles sete. Os dois
gates confirmaram que não vaza.
O crítico do review, que era o pior desfecho possível: o campo continuava sendo oferecido como
editável. O operador editava, recebia confirmação de sucesso, e a reconciliação **substituía em
silêncio**. Fechado tirando-o dos campos editáveis, recusando a alteração de forma nomeada, e
mostrando-o na tela como informativo com o ponteiro para onde ele de fato muda. A cobertura da
disciplina de número sobrevive pela trava de título, provada por requisição real.
Placar honesto e auditado: o QA mediu 35 aprovados, 5 reprovando, e explicou **as duas que saíram
desde a rodada anterior**: uma por conserto, outra por instabilidade, provada rodando de novo em vez
de embolsar o número melhor.
Cartão novo, do achado do review do {F8-26} em 2026-08-10. O gate de proveniência **já nomeia a
origem de cada número sem dono: o servidor devolve qual bloco (ou o título) causou a recusa. A
tela do compositor não mostra isso**: o operador vê o número cru e a razão, e não sabe onde
procurar.
Por que isto importa mais do que parece: a disciplina de número é a trava que sustenta a página
inteira, e ela bloqueia a publicação. Uma trava que reprova sem dizer onde vira catraca. O
operador que não acha o número aprende a contornar, e é assim que uma guarda boa morre.
A informação já existe e é jogada fora na última camada. Não é preciso construir nada no
servidor: é preciso a tela exibir o que já recebe. Custo baixo, e o defeito é de mostrar, não de
medir.
Anterior ao {F8-26}, e agravado por ele: aquele cartão acrescentou o título como mais uma
origem possível no mesmo balde indiferenciado. Antes o operador tinha que procurar entre os blocos;
agora tem que procurar entre os blocos e o título, com a mesma ausência de pista.
O que implementar: a tela mostra a origem que o servidor já nomeia, junto do número recusado, de
um jeito que leve o operador ao lugar, não apenas o rótulo técnico.
Como verificar (teste primeiro): tentar publicar com número cru no título aponta o título;
com número cru num bloco aponta aquele bloco; e os dois ao mesmo tempo apontam os dois.
Aceite (H1): Dado um relatório recusado na publicação por número sem origem, Quando o operador
vê a recusa, Então ela indica onde está cada número recusado, E o operador chega ao lugar sem
precisar procurar no documento inteiro.
FECHADO em 2026-08-10, mergeado (f91255e) e deployado. QA PASS e review APPROVED. A recusa
passou a dizer o lugar e a levar até ele: o título abre a edição em linha, o bloco abre a edição
daquele bloco, provado com dois blocos do mesmo tipo, para garantir que abre aquele e não o
primeiro. Nada foi construído no servidor: a informação já chegava e era descartada na última
camada.
O defeito que a primeira entrega trouxe, e ele destruía a própria promessa: clicar na violação do
título abria a edição mesmo com o relatório já publicado, e o servidor recusa alterar título
fora de rascunho. O operador clicaria, digitaria e descobriria a recusa ao sair do campo. O caso não
é raro: a mesma rota serve publicar e republicar, então é justamente aí que a recusa aparece com
o relatório publicado. Levar a um campo que não aceita edição é pior que não levar. Corrigido
reusando a mesma condição do botão de renomear, com teste do caso publicado.
Review e QA acharam o mesmo defeito de forma independente, o QA sem conhecer o achado do review.
Redundância entre gates provando que serve para alguma coisa.
Episódio de sessões concorrentes, resolvido sem perda: o QA largou arquivos de rascunho no
worktree compartilhado e quebrou o typecheck da sessão que construía. O construtor removeu o
arquivo, o QA percebeu, limpou e passou a usar cópias descartáveis. Os dois se corrigiram sozinhos.
É o cenário de sessões concorrentes funcionando como deve: **atrito visível em vez de perda
silenciosa**: o arquivo estranho quebrou a compilação em vez de sobrescrever trabalho alheio.
ENTREGUE em 2026-09-21, merge 2dc7730. A saida foi REMOVER, e a medicao e que decidiu.
Gates qa PASS e review APPROVED, os dois viajando dentro do proprio merge.
**O cartao dizia "o texto padrao dela diverge do que esta em producao". Medida, a divergencia e
pior do que isso:** producao (bin/reader-server.mjs:438) usa titulo "Relatorio de resultados"
e descricao vazia; a funcao morta usava "Relatorio" e "Resultados de ${accountName}.".
Liga-la passaria a estampar o nome da conta do cliente no og:description, que e o texto do
preview de link no WhatsApp e no LinkedIn, hoje em branco. Nao era so "muda o que o cliente le":
era expor o nome do cliente em metadado publico. O vazamento nunca chegou a producao, porque
a peca nunca foi acionada; existia so na funcao morta, esperando quem a ligasse.
Recorte que o cartao nao tinha: o arquivo NAO era morto. buildShareCardSvg tem chamador
real (bin/reader-server.mjs) e sanitizeShareCard e usada pelo Hub (routes/reports.ts:4423).
So o resolvedor era inalcancavel, e so ele saiu; as outras duas ficaram intactas, provado por
controle positivo (importar o pacote e conferir que as duas existem e a terceira e undefined).
Ficou escrito no topo do arquivo onde a resolucao real vive e por que duplica-la e perigoso,
para quem for reintroduzir a funcao esbarrar no motivo.
Nao deployado, e nao precisa ser com urgencia: o container do reader roda ha 5 semanas e a
funcao removida era inalcancavel, entao a remocao nao muda comportamento nenhum ate o proximo
deploy do pacote. Isso esta dito para ninguem confundir merge com deploy.
Cartão novo, achado durante a construção do {F8-26} em 2026-08-10. A função que resolve o card de
compartilhamento (src/share-card.mjs) existe, tem testes e nenhum chamador. Pior que inerte:
o texto padrão dela diverge do que está em produção, então quem a ligar acreditando estar
ativando uma peça pronta muda o que o cliente lê, sem perceber.
É a família de defeito mais cara deste projeto, e a contagem já passa de dez ocorrências: peça
construída, testada, aprovada e inalcançável. Ela passa por todos os gates porque cada um olha o
que mudou, e nenhum pergunta "quem, em produção, aciona isto?".
Por que este caso é pior que os anteriores: os outros eram peças inertes. Esta é uma peça inerte
que discorda da realidade. Ligá-la sem comparar os dois textos é trocar o que o cliente vê por
uma versão antiga que ninguém revisou.
O que implementar: decidir entre ligar (e então reconciliar o texto padrão com o que
produção usa hoje, declarando a diferença) ou remover. Não deixar como está: peça pronta e
inalcançável é armadilha para a próxima pessoa.
Conversa com {F8-17}, que varre o CONJUNTO das peças inalcançáveis. Se aquele cartão for feito
antes, este entra nele; se este for feito antes, ele é mais um caso para a varredura provar que
pega.
Como verificar (teste primeiro): ou existe caminho real que a aciona e o texto que o cliente vê
não muda, ou ela não existe mais no código.
Aceite (H1): Dado o resolvedor do card de compartilhamento, Quando o produto roda em produção,
Então ou ele é acionado por um caminho real sem alterar o que o cliente lê, ou ele foi removido, E
não permanece pronto e inalcançável.
FECHADO em 2026-10-16 (pacote a864243 + fix d36a29b, Hub 0bbe7ea; review APPROVED em 2 rodadas,
qa PASS; API 15/15). O orçamento de tempo vive dentro da cadeia: cada provedor recebe min(teto por
provedor ai.fallback.timeout_ms, o que sobra), o último fica com todo o restante, nenhum começa com
menos de 5 s, a chamada cortada é cancelada de verdade; DOCUMENT_TIMEOUT só quando o orçamento cortou
algum elo. O audit da composição grava provider_used, model_used, fallback_depth e latency_ms (a
medida do uso do reserva). Trava contra provedor real estendida ao transporte do pacote em processo de
teste. Os 7 chamadores sem teto não mudam.
**Cartão novo de 2026-08-10, que NASCEU ERRADO e foi corrigido pela medição no mesmo dia. A correção
fica registrada porque a lição vale mais que o cartão.**
A versão errada, e o erro foi meu: escrevi que a composição síncrona, cortada em 50 segundos,
tornava impossível compor um documento real, porque a fórmula de orçamento do produto pede 108 a
150 segundos para 18 a 25 blocos. Tratei a fórmula como se fosse medição. O QA foi medir o tempo
real contra o provedor real: 1,2 a 2,4 segundos, em oito execuções. Folga de vinte a quarenta
vezes. O motivo estava no código o tempo todo: a composição é uma chamada de IA só, não uma por
bloco, então a quantidade de blocos quase não move o relógio.
Achado de lado, e é a família mais cara deste projeto: a fórmula que "escala o teto com o tamanho
do documento" é código morto. O piso dela (60s) é maior que o teto da aplicação (50s), então ela
devolve sempre exatamente o teto, para qualquer quantidade de blocos, provado executando de 1 a
100. A peça existe, é testada, está ligada, e não decide nada.
O que sobra de verdade, e é o motivo de este cartão continuar aberto: quando a cadeia cai no
provedor reserva (cenário real, já ocorrido em produção por esgotamento de cota do principal), o
tempo salta. Medido: 11,7s, 20,7s, 31,4s, **49,85s (cento e quarenta e sete milissegundos de
folga)**, e uma execução acima de 120 segundos. Ou seja, no dia em que o principal falhar, a
composição passa a estourar o teto de forma intermitente, e o operador recebe falha por demora
justamente quando o produto já está degradado.
Não é regressão do {F8-16}: o teto antigo, do proxy, era igualmente apertado, só cortava de
forma desonesta, deixando o trabalho rodando. A fatia trocou o disfarce pela verdade, e a verdade
mostrou que a reserva não cabe no orçamento.
O que implementar, e a escolha é parte da fatia: ou o caminho reserva passa a caber no teto, ou a
composição deixa de depender de uma conexão aberta (e o teto deixa de ser refém do proxy), ou o
produto assume que degradar significa demorar mais e trata isso na tela em vez de falhar. **Meça
antes o quanto a reserva é acionada**: a resposta muda a escolha.
Conversa com {F8-31} (a conexão do banco presa durante a IA): as duas são faces do mesmo desenho
síncrono.
Como verificar (teste primeiro): com o provedor principal indisponível, uma composição de
documento real termina com sucesso de forma repetível, sem depender de sorte no tempo de resposta.
Aceite (H1): Dado o provedor principal indisponível, Quando o operador compõe um relatório de
tamanho real, Então a composição termina com sucesso de forma repetível, E o operador não recebe
falha por demora num momento em que o produto já está degradado.
Cartão novo, escrito em 2026-08-10 a partir do achado I1 do review do {F8-25}, e é anterior
àquela fatia: o review conferiu que o mesmo desenho já estava em main. Fica registrado como
cartão porque recomendação dentro de um gate de review não é fila: o gate fecha, o cartão fica.
O que acontece: em três rotas (criar artefato, e as duas de pesquisa de mercado) a transação e a
conexão do pool são abertas antes da chamada de IA, porque já há escrita e leitura antes dela. A
trava de linha que o {F8-25} acrescentou está no fim e está certa. **O problema não é a trava, é a
conexão**, que fica ocupada durante toda a IA.
Por que isso é caro, e por que não aparece hoje: o pool tem tamanho fixo. Enquanto o produto tem
poucos operadores simultâneos, sobra conexão e nada acontece. Com uma agência de equipe grande, ou
várias agências gerando ao mesmo tempo, as conexões acabam presas esperando IA e **o Hub inteiro
para**, não só o módulo de relatórios, inclusive quem nem está usando relatório. É a classe de
defeito que só se manifesta quando o produto dá certo.
O desenho correto já existe dentro da própria casa: a rota de regeneração de bloco mantém a IA
fora de qualquer transação, em duas transações separadas, e foi apontada pelo review como a mais
segura das quatro. Não é preciso inventar padrão: é preciso as outras três seguirem esta.
O que implementar: as três rotas passam a chamar a IA fora de transação, abrindo a transação de
escrita só depois da resposta, preservando a releitura sob trava que o {F8-25} entregou.
Como verificar (teste primeiro): com o pool reduzido a poucas conexões, várias gerações
simultâneas não esgotam o pool nem bloqueiam uma requisição simples e alheia ao módulo; o teste
falha com o código de hoje.
Aceite (H1): Dado o pool de conexões próximo do limite e várias gerações de IA em curso, Quando
chega uma requisição comum de outra parte do Hub, Então ela é atendida normalmente, E nenhuma
conexão fica ocupada esperando a IA responder.
FECHADO em 2026-08-10, mergeado (51d2b5b) e deployado. QA PASS e review APPROVED, ambos na
segunda rodada.
A prova mais contundente da sessão, e ela mostra que o travamento do Hub era real, não teoria:
com o conjunto de conexões reduzido a duas e duas gerações em curso, uma requisição comum **de outro
módulo do Hub (a listagem de empresas) nunca respondeu** no código antigo: travamento medido,
zero conexão livre. Com o conserto, respondeu em dezenas de milissegundos. Medido dos dois lados,
por duas partes independentes.
A premissa do cartão foi corrigida por medição: ele dizia três rotas. São duas: uma das de
pesquisa de mercado não chama IA nem rede nenhuma, só cura dado já gravado. Confirmado por leitura do
corpo dela pelas duas partes.
O defeito que a fatia INTRODUZIU, e é a lição principal: ao adotar o desenho de duas etapas nas
rotas, a de criar artefato perdeu atomicidade que existia antes: passou a confirmar o registro e
o arquivo em disco antes da IA, deixando órfãos e permitindo duplicação numa nova tentativa. A
causa é sutil e vale escrever: nas outras rotas a primeira etapa só lê; nesta, ela escreve.
Copiar a forma sem olhar essa diferença trocou uma corrida por um vazamento, e tudo continuaria
verde no caminho feliz. É a regra da casa aplicada de novo: toda mudança em código que funciona
declara o que PERDE, não só o que ganha.
A prova do conserto foi feita contra o commit reprovado, pelas duas partes: ele de fato deixa um
registro e um arquivo órfãos; o corrigido não deixa nenhum, falha nomeado, e uma nova tentativa
produz exatamente um artefato.
Terceiro achado, e ele fecha um furo aberto pela própria fatia da manhã: o comando da suíte,
entregue horas antes, conferia o código de saída numa direção só. Reprovação impressa com saída
de sucesso virava reprovação; mas **saída de erro sem linha de reprovação impressa era classificada
como aprovada**. O primeiro arquivo de teste criado depois daquele comando caiu exatamente nesse
furo. A metade simétrica foi acrescentada e a classificação virou peça própria e testável.
O placar caiu de 32 para 31 aprovados, com 6 reprovando, e a queda É o produto. Nenhuma
regressão: um dos dois é o arquivo que agora reprova honestamente em vez de mentir, o outro é
instabilidade de IA real já documentada.
Configuração nova: o tamanho do conjunto de conexões virou ajustável, com piso e documentação, e
produção não mudou: a variável não está definida lá, e o padrão é o valor fixo anterior.
Achado encaminhado: {F8-43}. O serviço de análise de arquivo continua dentro da transação e sem
limite de tempo, pelo mesmo mecanismo, com outro nome.
Cartão novo, do achado I3 do review do {F8-25}, escrito em 2026-08-10. A função que propõe a
leitura dos números altera a estrutura recebida no próprio lugar, por contrato documentado. Por
causa disso, a rota de criar artefato precisou guardar uma cópia da base antes de chamá-la, só para
conseguir calcular o que mudou depois.
O conserto que entrou no {F8-25} trata o sintoma, e isso está declarado. A causa vive no outro
repositório (o pacote do módulo), fora do alcance daquela fatia. Enquanto ela existir, **todo novo
chamador precisa lembrar de tirar a cópia antes**, e quem esquecer não vai receber erro nenhum: vai
receber um delta vazio e concluir que nada mudou.
É a mesma família do que este projeto já chamou de guarda por inclusão nomeada: funciona por
disciplina de quem escreve, não por construção. Some no primeiro chamador distraído.
O que implementar: a função passa a ser pura, devolvendo estrutura nova em vez de alterar a
recebida; os chamadores param de precisar da cópia defensiva, que sai junto.
Como verificar (teste primeiro): chamar a função e conferir que a estrutura passada permanece
idêntica; e um chamador que não faça cópia defensiva ainda assim calcula o delta certo.
Aceite (H1): Dado um chamador que passa uma estrutura de números, Quando a função de leitura é
executada, Então a estrutura original permanece inalterada, E o chamador obtém o resultado sem
precisar de cópia defensiva.
FECHADO em 2026-08-10, mergeado (4610c81 no pacote, f041dee no Hub) e deployado. QA PASS e
review APPROVED, sem nada bloqueante em nenhum dos dois.
A impureza não mudou de dono, ficou contida, que era a pergunta que decidia a fatia. A função
interna continua alterando o que recebe, mas agora o que ela recebe é sempre a cópia local, e o
único outro chamador dela já trabalhava sobre objeto próprio. Os quatro caminhos de retorno devolvem
a cópia; nenhum toca o original.
O detalhe que salvou a cobertura, e quase ninguém teria visto: o apoio de teste também mutava.
Consertar a rota sem consertá-lo teria contaminado o estado "antes" e **escondido a corrida que outro
teste prova**: cobertura falsa, com tudo verde. Foi ajustado junto, com o motivo escrito.
Custo medido contra produção, não estimado: 21 rascunhos reais, média de menos de 3 chaves e
859 bytes, máximo pouco acima de 7 KB. A cópia é irrelevante, e o total de cópias no caminho **caiu
de duas para uma**: a fatia removeu mais trabalho do que acrescentou.
O QA se corrigiu sozinho num falso positivo: a primeira medição dele acusou uma reprovação a mais,
causada pelo nome do próprio worktree que ele criou. Reconheceu como ruído do ambiente e não da
branch, em vez de reportar o número inflado.
Cartão novo, do levantamento pedido no review do {F8-29} em 2026-08-10. Número medido: **6 telas,
7 ocorrências** do mesmo padrão de grid que causou o transbordo daquela fatia: colunas em fração
sem piso declarado, o que faz a trilha ser resolvida pelo conteúdo mais largo.
Hoje é risco latente, não defeito ativo: nenhuma delas tem, no momento, texto com corte reticente,
que é o ingrediente que transforma o padrão em transbordo. **O dia em que alguém acrescentar um, a
tela quebra**, e quem acrescentar não terá como saber.
O agravante, e é o que torna este cartão necessário: existe uma regra global que recorta o
excedente. Ela é legítima e precisa ficar (sustenta o cabeçalho fixo no produto inteiro), mas o
efeito colateral é que transbordo não vira rolagem visível: vira conteúdo cortado e inalcançável.
Foi exatamente por isso que o defeito do {F8-29} viveu escondido, e é por isso que os próximos vão
se esconder do mesmo jeito.
**Ou seja: o produto tem sete lugares onde o defeito pode nascer e um mecanismo que garante que ele
nasça invisível.**
O que implementar, e a segunda metade é a que importa: declarar o piso nas sete ocorrências; e
instrumentar para que o próximo transbordo apareça em vez de ser engolido (por uma verificação que
reprove grid em fração sem piso, ou por um sinal visível em desenvolvimento). Consertar as sete sem isso
deixa o mecanismo de ocultação intacto para a oitava.
Como verificar (teste primeiro): um grid novo com fração sem piso é acusado; e uma tela com texto
de corte reticente numa dessas colunas não transborda em tela estreita.
Aceite (H1): Dada qualquer tela do produto com grid de colunas em fração, Quando conteúdo largo é
colocado nela, Então a página não transborda, E um grid novo sem piso é acusado antes de chegar à
produção.
FECHADO em 2026-08-11, mergeado (be3503d) e deployado. QA PASS e review APPROVED, os dois na
terceira rodada. As sete ocorrências corrigidas sem alterar nenhuma proporção, e uma **varredura por
construção que acusa grid novo sem piso, provada acusando um defeito reintroduzido de verdade**,
em arquivo diferente do que o autor usou, na forma montada dinamicamente.
A guarda nasceu no mesmo buraco que ela existe para fechar, e foi preciso reprovar duas vezes.
Primeira reprovação: a varredura não rodava em comando nenhum. É literalmente o cartão {F8-33},
fechado horas antes: dezenas de testes sem comando que os executasse. Corrigido criando **um comando
na raiz** que agrega o teste do front e a suíte da API, sem reescrever nenhum dos dois.
Segunda reprovação, e ela veio de o review ATACAR a própria solução: o comando distingue "quebrei
agora" de "já estava assim" por um arquivo de referência versionado. O review **acrescentou uma linha
com dado real** e o placar ficou verde: sem revisão, sem motivo obrigatório, sem prazo. A mitigação
era só processo. Corrigido imprimindo sempre quais linhas entraram e saíram na referência, mesmo
em rodada verde, com o ataque simulado e listado.
A trava que fecha o abuso pior foi demonstrada ao vivo, por acidente: rodei o comando num
worktree sem dependências e ele bloqueou, porque sem placar impresso não há o que comparar, sem
exceção de referência nenhuma. Não dá para silenciar suíte que não rodou.
Divergência de medição entre mim e o review, resolvida sem inventar cartão: um arquivo apareceu
como não executado só na rodada dele. Somando as execuções das três partes, ficou **onze verdes
contra uma vermelha**: intermitente, não determinístico, compatível com contenção de recurso num
workspace que roda sessões concorrentes. O autor não acrescentou à referência e não abriu cartão,
porque sem defeito reproduzível não há o que nomear, e acrescentar teria sido exatamente o atalho
que o review acabara de provar que o arquivo permite.
Limites declarados, não descobertos depois: a varredura tem falso negativo quando o valor do grid
vem de variável, e a comparação da referência olha o commit anterior: uma adição enterrada sob
commits seguintes escaparia. Os dois estão escritos.
Achado de ambiente, resolvido no caminho: rodar os testes do front dentro da imagem da API
quebrava por incompatibilidade da imagem, não do código. Cada lado passou a rodar no seu ambiente.
**Cartão novo, medido em 2026-08-10 durante a construção do {F8-27}, e registrado como cartão
próprio porque é anterior e independente daquela obra**, provado assim: um cartão da mesma página
que a obra nunca tocou transborda de forma idêntica.
O que acontece: a ficha do cliente empurra a largura da página para além da tela em celular, e
o corpo do documento passa a rolar na horizontal. É o operador da agência que sofre, não o cliente.
Por que só apareceu agora: este projeto declarou viewport na página pública há poucos dias
({F5-06}); antes disso o navegador de celular desenhava tudo como se fosse tela grande e **todo o
desenho estreito estava adormecido**. Defeito de largura em tela estreita é a classe que aquele
cartão acordou, e é esperado que apareça mais de um.
O que implementar: conteúdo largo rola dentro do próprio recipiente, nunca a página; a regra
vale para o container, não para um seletor pontual, senão o próximo cartão largo reintroduz o
transbordo.
Como verificar (teste primeiro): a ficha de um cliente com nome longo, muitos contatos e metas
preenchidas, aberta em 375px, não rola na horizontal em ponto nenhum da página.
Aceite (H1): Dado um cliente com ficha cheia, Quando o operador a abre em celular, Então a
página não rola horizontalmente, E o conteúdo largo que precisar rolar o faz dentro do próprio
recipiente.
FECHADO em 2026-08-10, mergeado (670ac58) e deployado. QA PASS e review APPROVED. **O diff tem
uma linha de conteúdo; o valor está no diagnóstico.**
A causa é estrutural, e explica por que o sintoma era confuso: o grid principal não declarava
piso para as colunas, então a trilha era resolvida pelo conteúdo mais largo, e texto com corte
reticente tem largura natural igual à frase inteira. O piso foi declarado no container, então
qualquer cartão futuro colocado ali nasce contido.
O que fazia o defeito parecer inexistente: existe uma regra global que recorta o excedente,
herdada de outra correção. Com ela, transbordo **não vira rolagem visível: vira conteúdo cortado e
inalcançável**. Por isso o relato dizia "rola de lado" e a primeira medição bruta dizia "zero
transbordo": as duas estavam certas sobre coisas diferentes.
A regra fica, e a decisão está registrada: ela sustenta o cabeçalho fixo no produto inteiro, e
removê-la teria alcance muito maior que o ganho. Mas **ela é o mecanismo que garante que o próximo
transbordo nasça invisível, daí o {F8-45}, com o número medido: 6 telas, 7 ocorrências** do
mesmo padrão, hoje latentes porque nenhuma tem texto de corte reticente. O dia em que alguém
acrescentar um, quebra sem aviso.
Conserto anterior que veio junto, confirmado como anterior: a proporção entre as colunas em tela
grande já estava errada antes desta fatia, pelo mesmo motivo. Corrigida no mesmo ato, com a proporção
pretendida provada por medição.
O QA resolveu a limitação que travou o autor, em vez de aceitá-la: o ambiente não compunha
imagem, e ele gerou capturas reais usando o próprio motor de renderização do navegador. **Olhou
quatro imagens de verdade**, em duas resoluções, com a ficha cheia, em vez de tratar medição
geométrica como equivalente a ver.
E qualificou a promessa em vez de repeti-la: "nasce contido" vale para o dimensionamento da
coluna; um filho sem corte próprio ainda vaza, invisível por causa da mesma regra de recorte.
Nenhum cartão real da tela tem esse formato hoje: limite declarado, não descoberto depois.
**Cartão novo, escrito em 2026-08-10 depois da TERCEIRA ocorrência da mesma armadilha em dois
dias.** Ele não nasce de um caso: nasce da contagem.
A armadilha, sempre igual: a página declara as variáveis de cor num container, o elemento nasce
fora dele, e a cor simplesmente não alcança. Nada quebra, nada avisa, a suíte fica verde: o
elemento aparece com a cor padrão do navegador, e só quem olha percebe.
As três ocorrências, e o padrão de descoberta é o que mais assusta:
1. {F8-21}, a barra da agência: achada pelo próprio autor no smoke, minutos depois de nascer.
2. {F8-22}, a página de link revogado e expirado: nascida assim desde a Fatia 5, meses no ar
sem cor nenhuma, achada de raspão por quem foi mexer em identidade por outro motivo.
3. {F8-35}, a página de senha: dormente, porque aquela tela ainda não recebe identidade real.
Vai acordar no dia em que passar a receber, e ninguém vai ligar uma coisa à outra.
Ou seja: das três, só uma foi achada por quem a criou. As outras duas dependeram de alguém
passar por perto. Isso não escala, e é exatamente o que a regra da casa chama de exaustivo por
construção contra exaustivo por disciplina.
Por que consertar uma a uma é o caminho errado: as duas primeiras já foram consertadas assim, e
a terceira apareceu mesmo assim. O conserto é de uma linha; o problema é descobrir, e descobrir
é o que nenhum dos consertos anteriores endereçou.
O que implementar: varrer toda página do leitor que declara identidade e conferir, **por
construção**, que a cor declarada alcança o elemento visível. Prefira uma verificação que reprove
no conjunto a uma lista curada de páginas: lista curada faz a página nova nascer furada, que é a
mesma família de defeito por outro eixo. Os consertos de {F8-35} e de qualquer ocorrência que a
varredura achar entram nesta fatia.
Como verificar (teste primeiro): plantar uma página nova com o elemento fora do container e ver
a varredura acusar; e uma com o elemento dentro passar. O primeiro é o que prova.
Aceite (H1): Dada uma página do leitor que declara identidade, Quando a verificação roda, Então
toda página cuja cor declarada não alcança o elemento visível é acusada, E uma página nova com o
mesmo defeito é acusada sem que ninguém precise adicioná-la a uma lista.
FECHADO em 2026-08-10, mergeado (9f7d364) e DEPLOYADO. QA PASS e review APPROVED.
A prova que fecha o cartão é do QA, e é a única que valia: ele plantou um módulo novo com o
elemento fora do container; a varredura o descobriu sozinha e reprovou; ele corrigiu e passou.
Isso é o que separa descoberta por construção de lista curada com outro nome.
A varredura se pagou ANTES de ser aprovada: ela achou uma **quarta ocorrência que ninguém tinha
reportado**: a seção de comentários da página pública, no ar desde 31/07, com texto preto em vez da
cor do tema, medido pelo estilo aplicado num navegador real. Nenhum dos dois consertos pontuais
anteriores a teria encontrado, e ela passou por todos os gates desde então.
Placar final da família: quatro ocorrências, todas corrigidas: duas em fatias anteriores, a
página de senha ({F8-35}) e a seção de comentários, estas duas aqui.
O risco desta fatia era a extração, porque o conserto da seção de comentários mexeu na montagem
da página pública, o caminho mais quente do produto. O review conferiu que a função extraída
reproduz exatamente as inserções anteriores, e que a única mudança é o conserto, não efeito
colateral. Ela é pura, sem acesso a banco, e o teste chama a mesma função de produção em vez de
duplicar a lógica.
Limite declarado, não descoberto depois: a descoberta encontra módulos pela chamada direta à
função de tokens. Um módulo que declare identidade por caminho indireto nasce invisível. Está escrito
no próprio arquivo, e há verificação de sanidade que acusa o mecanismo quebrando por completo.
Detalhes técnicos da entrega. src/identity-scope-guard.mjs
(findUnreachedIdentityConsumers) mede, para qualquer HTML, se toda variável --r-* CONSUMIDA
(em regra CSS, inclusive dentro de @media, ou em style= inline) está de fato aninhada dentro do
elemento que a DECLARA -- por construção, sem jsdom: tokenizador de profundidade de chaves pro CSS
+ parser mínimo de pilha de tags pro HTML, replicando exatamente a cascata de custom property que o
navegador aplica. test/identity-scope-guard.test.mjs prova o par exigido (elemento plantado fora
do container é ACUSADO; o mesmo elemento dentro PASSA), mais 6 casos de borda (@media aninhado,
style= inline, elemento void, classe declarada e nunca usada).
A descoberta É por construção, não lista curada: test/identity-scope-sweep.test.mjs varre
src/*.mjs por grep (quem CHAMA tokensToCssBlock(, nunca quem a define) e exercita toda função
render* exportada por cada arquivo achado, via 2 convenções de chamada genéricas já existentes no
código (objeto único vs document+ctx) -- nenhum nome de página vive no teste. Achou os 3 módulos
reais (render.mjs, reader-invalid-page.mjs, reader-password-page.mjs); um arquivo novo que
chamar tokensToCssBlock( entra sozinho na próxima rodada.
QUARTA ocorrência, achada pela própria varredura e nunca reportada antes (a contagem que o
cartão previu: "melhor ainda: diga quantas achou"): bin/reader-server.mjs colava a seção de
comentários + gate/composer ({F12-03}/{F12-04}/{F12-06}) com .replace("</body>", ...) -- ou seja,
como IRMÃ de <article class="report">, fora do escopo onde --r-* é declarado. Vivo desde
2026-07-31, nunca coberto por teste sem banco (a colagem só existia dentro de
handleReaderRequest, que exige DATABASE_URL). Medido ao vivo com getComputedStyle num
documento reconstruído com as MESMAS funções de produção: .r-comments-title/.r-comment/
.r-comment-gate resolviam color:rgb(0,0,0), nunca a cor do tema. Corrigido extraindo a colagem
como função pura assembleReaderPageHtml (bin/reader-server.mjs, sem banco) e trocando
.replace("</body>", ...) por .replace("</article>", ...) -- test/reader-server-assembly.test.mjs
roda essa função com dados sintéticos e sem DB, com uma regressão nomeada que planta a colagem
ANTIGA e confirma que ela é acusada. Reconfirmado ao vivo contra mhub-db real:
test/reader-server.test.mjs (HTTP real, inclui o bloco {F12} de comentários) 40/40, sem
regressão nas outras 3 fatias que esse arquivo cobre ({F5}, rate-limit, {F13} analytics,
{F8-21}, {F8-08b}).
Placar da varredura, nominal: 4 páginas/pontos de colagem varridos, 4 tinham o defeito antes
desta fatia ({F8-21} e {F8-22} já corrigidos em cartões anteriores; {F8-35}, corrigido junto; a
seção de comentários, achada e corrigida NESTA fatia), 0 ficaram sem conserto. Suíte completa
(npm test) 1470 testes, 0 falhas, 2 skipped (DB-gated, esperado sem DATABASE_URL). Imagem
modularreports-reader builda limpa (reader.Dockerfile, tag de teste f8-36-test, removida
depois).
**Cartão novo, achado ao vivo em 2026-08-10 durante a verificação de {F8-22}, fora do pedido daquele
cartão mas na mesma classe de defeito, a terceira ocorrência dela neste projeto.** tokensToCssBlock
(render-tokens.mjs) escopa as custom properties --r-* ao seletor .report. reader-invalid-page.mjs
tinha o mesmo bug e foi corrigido dentro de {F8-22}; src/reader-password-page.mjs
(renderPasswordGatePage, RF-PUB-003) tem o mesmo desenho e o mesmo defeito, ainda sem conserto:
<main class="r-pwgate"> nunca ganhou a classe report, então a cor resolvida
(resolveAccentColor(branding) || DEFAULT_ACCENT) nunca alcança nenhum elemento da página.
Por que ainda não apareceu em produção: bin/reader-server.mjs sempre chama
renderPasswordGatePage com companyName: null, branding: null -- a tela de senha hoje não recebe
identidade nenhuma, então o sintoma visível seria idêntico com ou sem o bug (a página já mostra o
DEFAULT_ACCENT genérico de qualquer jeito). Isso não torna o cartão dispensável: a função já foi
escrita para receber e aplicar branding real (importa resolveAccentColor, tem a classe
.r-pwgate__agency pronta) -- o dia em que alguém decidir ligar a identidade da agência nessa tela
(mesma decisão que {F8-21} tomou pra revogado/expirado), o defeito reaparece em silêncio, do jeito
mais difícil de notar: código parece certo, cor nunca aplica.
Fora do escopo deste cartão: decidir SE a página de senha deveria mostrar a identidade da agência
hoje (isso é RF-PUB-003 não pedir nem proibir, decisão de produto em aberto) -- este cartão só fecha o
defeito estrutural de escopo CSS, pra quando essa decisão vier o caminho já estar limpo.
RF-PUB-003.
O que implementar: mesmo fix de {F8-22} em reader-invalid-page.mjs -- dar ao <main> a classe
report (<main class="r-pwgate report">), seguro pelo mesmo motivo (documento autocontido, não
importa render-css.mjs/LAYOUT_CSS).
Como verificar (teste primeiro): chamar renderPasswordGatePage com uma cor de teste e confirmar
por getComputedStyle (ou pela mesma prova estrutural de test/reader-invalid-page.test.mjs) que a
cor alcança o elemento, não só a variável declarada no <style>.
NÃO CONSERTAR ISOLADAMENTE: entra junto do {F8-36}, que varre o conjunto. Consertar a terceira
ocorrência uma a uma é o que já se fez com as duas primeiras, e a quarta vai aparecer igual.
Aceite (H1): Dado renderPasswordGatePage chamada com uma identidade resolvida, Quando a página
renderiza, Então a cor resolvida se aplica de fato ao elemento visível, não só é declarada no bloco de
estilo.
FECHADO em 2026-08-10, junto do {F8-36}, mergeado (9f7d364) e DEPLOYADO. O conserto é a **mesma
forma** do da página de link inválido, comparada byte a byte pelo review, não é uma quarta variação
para o mesmo problema. Provado ao vivo com relatório protegido por senha real, medindo o estilo
aplicado em tela grande e celular. <main class="r-pwgate"> virou
<main class="r-pwgate report">. Confirmado por test/identity-scope-sweep.test.mjs
(reader-password-page.mjs, varredura por construção) sem violação.
Definição de Pronto F8: wizard completo dos 4 passos e compositor com curadoria, regenerar, criativos, whitelist e apresentar, todos com proveniência preservada.
Fase F9, Publicação e link (Fatia 9, o gate de adoção mede-se aqui)
Dependência: {F2-04} (snapshot), {F8} (compositor). Onde o link nasce.
RF-CMP-007. Publicação bloqueada se qualquer número não tiver dp: (gate HARD, reforça {F1-04}).
Como verificar (teste primeiro): tentar publicar um relatório com um número sem dp: é recusado, apontando exatamente qual número está sem origem.
Aceite (H1): Dado um relatório com ao menos um número sem dp: de origem, Quando o operador tenta publicar, Então a publicação é recusada, E o sistema aponta exatamente quais números estão sem origem.
RF-PUB-002. Token de no mínimo 128 bits, noindex, rate-limit.
Como verificar (teste primeiro): medir o tamanho do token gerado; confirmar header noindex; disparar requisições acima do limite é barrado.
Aceite (H1): Dado um relatório publicado, Quando o link é gerado, Então o token tem no mínimo 128 bits, a página responde com noindex, E requisições acima do rate-limit ao caminho do link são barradas.
RF-PUB-003. Senha de 4 palavras opcional; default sem senha.
Como verificar (teste primeiro): um link com senha pede a senha antes de exibir conteúdo; um link sem senha abre direto.
Aceite (H1): Dado um link com senha definida, Quando alguém abre o link, Então a página pede a senha antes de exibir o conteúdo, E um link sem senha definida abre direto.
RF-PUB-004. Data de expiração opcional; default permanente.
Como verificar (teste primeiro): um link com expiração no passado responde como expirado ({F5-02}); um link sem expiração continua servindo.
Aceite (H1): Dado um link com data de expiração no passado, Quando alguém o abre, Então a página responde como expirada com o estado brandado, E um link sem expiração continua servindo indefinidamente.
RF-PUB-005. Modal de publicação gera QR code do link.
Como verificar (teste primeiro): ler o QR code exibido leva à mesma página do link publicado.
Aceite (H1): Dado um link publicado, Quando o operador abre o modal de publicação, Então um QR code que resolve para o link é exibido e copiável, E ler o QR leva à mesma página do link.
RF-PUB-006, Ap.7. Editar og:title, og:description e capa, com preview de como fica no WhatsApp.
Como verificar (teste primeiro): editar os 3 campos e publicar; compartilhar o link mostra exatamente o card revisado.
Aceite (H1): Dado o modal de publicação, Quando o operador edita og:title, og:description e a capa e publica, Então o link compartilhado exibe exatamente esses metadados no card, E o operador viu o preview do card antes de publicar.
RF-PUB-007. Sistema não envia o link automaticamente; oferece copiar link e QR.
Como verificar (teste primeiro): publicar um relatório não dispara nenhuma mensagem automática ao cliente; o link e o QR ficam disponíveis para copiar.
Aceite (H1): Dado um relatório recém-publicado, Quando a publicação conclui, Então o sistema oferece copiar o link e o QR mas não dispara nenhuma mensagem ao cliente automaticamente, E o envio depende de uma ação manual do operador.
RF-PUB-008. Revogar purga o cache e passa a responder 410 com página brandada.
Como verificar (teste primeiro): revogar um link em cache; confirmar 410 e ausência do conteúdo antigo em qualquer camada de cache.
Aceite (H1): Dado um link ativo em cache, Quando o operador o revoga, Então o cache é purgado e o link passa a responder 410 com a página brandada de link revogado, E o conteúdo anterior não é mais servido de nenhum cache.
RF-PUB-009. Republicar sobrescreve o snapshot; sem galeria de versões.
Como verificar (teste primeiro): editar o rascunho e republicar mantém o mesmo link, servindo o snapshot novo; nenhuma segunda URL ou galeria surge.
Aceite (H1): Dado um relatório já publicado, Quando o operador republica após editar o rascunho, Então o mesmo link passa a servir o snapshot novo, E não é criada nenhuma versão navegável separada da página.
RF-PUB-010. Republicar grava log humano (quem, o quê, onde, quando), visível só para a agência.
Como verificar (teste primeiro): republicar gera entrada no histórico de alterações visível na agência; nenhuma superfície do leitor público expõe esse histórico.
Aceite (H1): Dado um relatório republicado, Quando alguém da agência abre o histórico de alterações, Então vê a lista de alterações em linguagem humana, E nenhuma superfície do leitor público expõe esse histórico ao cliente.
RF-COM-004, RN-005. No fluxo de republicação, varrer os comentários ancorados: quando o bloco ancorado sumiu do novo snapshot, marcar o comentário orphaned e preservar o excerpt, sem apagar nem versionar a página. É o ponto onde o orphaned é setado (a dependência F12 para F9).
Como verificar (teste primeiro): republicar um relatório em que um bloco com comentário foi removido marca aquele comentário como orphaned com o excerpt preservado; comentários de blocos que sobreviveram continuam ancorados.
Aceite (H1): Dado um relatório com comentários ancorados, Quando ele é republicado sem um dos blocos comentados, Então o comentário daquele bloco fica orphaned com o excerpt preservado, E os comentários dos blocos que permaneceram seguem ancorados ao id estável.
RF-CMP-018, RN-012. Gate HARD de honestidade, irmão do gate de proveniência ({F9-01}): bloqueia o publish se um fato adverso obrigatório de RN-012 foi suprimido, se a métrica âncora não carrega o selo íntegro, ou se existe um badge do produto com valência oposta ao selo. Selo íntegro tem 2 formatos: com sinal (base de comparação existe) ou marcador explícito "sem base" (nenhuma das 2 fontes resolveu); o segundo formato LIBERA o publish, não reprova.
Como verificar (teste primeiro): tentar publicar um relatório com fato adverso suprimido, ou com métrica âncora sem selo em NENHUM dos 2 formatos íntegros, ou com badge de valência oposta ao selo, é recusado, apontando exatamente o que falhou; publicar um relatório cuja âncora não tem base de comparação alguma é ACEITO, com o selo no formato "sem base" (RF-CMP-018 H2).
Aceite (H1): Dado um relatório em que um fato adverso aritmético de RN-012 está presente nos dados mas ausente da composição, ou cuja métrica âncora publica sem o selo em nenhum dos 2 formatos íntegros, ou com um badge de valência oposta ao selo, Quando o operador tenta publicar, Então a publicação é recusada, E o sistema aponta exatamente o fato suprimido, o selo ausente/corrompido ou o badge contradito.
Aceite (H2): Dado um relatório cuja métrica âncora não tem base de comparação válida (nem no artefato, nem em relatório anterior do mesmo cliente e tipo), Quando o operador publica, Então a publicação é ACEITA com o selo "sem base" (número sem seta), porque ausência de base não é desonestidade.
FIX 2026-07-30 (achado crítico da Fatia 9, reconciliado): o cálculo do selo (resolveAndSealAnchor) existia desde {F1-18} mas nenhum caminho real de composição ou publicação o chamava, ou seja, todo relatório do wizard reprovava o gate. Ligado ao caminho real de publicação com a precedência de 2 fontes descrita em RN-012 item 2 do DRS (artefato, depois relatório anterior publicado do mesmo cliente e tipo, por consulta a reports.report e reports.snapshot injetada no pacote puro, mesmo padrão de transporte usado pelo gateway de IA). Provado de ponta a ponta em produção: 3 relatórios publicados pela API real (1 pelo wizard completo com IA real, 2 com extrator real e dado real de um cliente do conjunto de referência em 2 períodos), com os links públicos anônimos respondendo 200 nos dois cenários (sem base = selo sem seta; com base = selo com sinal). 552 de 552 testes verdes (539 anteriores mais 13 novos desta correção).
RF-CMP-021, RN-012 item 4. Gate HARD de coerência de rótulo, irmão do gate de proveniência ({F9-01}) e do de honestidade ({F9-12}). Os dois provam, respectivamente, que o número TEM origem e que nenhum fato adverso sumiu; nenhum dos dois lê a etiqueta que a prosa põe no número. A IA não inventa o número, mas escolhe qual número entra em qual frase, então pode mentir com número verdadeiro, e o resultado parece auditado. Mecanismo determinístico por léxico fechado em português (mesmo padrão da verificação de polaridade, RF-CMP-008, nunca a IA se auto-avaliando): a família de métrica citada na oração ao redor de cada token de número tem que bater com a família do rótulo real daquele dado (o mesmo rótulo que o card mostra ao lado do número). Conservador por decisão: rótulo fora do vocabulário, ou oração sem métrica reconhecida, não são julgados.
Como verificar (teste primeiro): publicar uma composição cuja narrativa cita o dado de impressões numa oração que fala de "cliques no link" é recusado, apontando o token, o rótulo real e a família que a frase atribuiu; a mesma narrativa com o rótulo correto publica normalmente.
Aceite (H1): Dado um relatório cuja narrativa cita um token de número numa oração que nomeia uma família de métrica diferente da família do rótulo real daquele dado, Quando o operador tenta publicar, Então a publicação é recusada com 409, E a mensagem aponta o token, o rótulo real e a métrica que a frase atribuiu a ele.
Achado real 2026-07-30 (smoke visual na página publicada, não em teste): a narrativa escreveu "a campanha alcançou 167.364 cliques no link" ligando o dado de IMPRESSÕES (o de cliques valia 0 no mesmo relatório), "o custo por clique foi de R$ 2.320,19" ligando o dado de CUSTO POR RESULTADO, e chamou de "leads" o dado de CONVERSAS INICIADAS. Quatro frases falsas com quatro números verdadeiros, todas aprovadas pelos gates que existiam. A guarda roda no publish real e devolve 409; provada contra o rascunho gravado desse mesmo relatório, onde acha as quatro.
Definição de Pronto F9: publicar, revogar, republicar e o modal completo (senha, expiração, QR, card) funcionando ponta a ponta, com a republicação setando orphaned nos comentários dos blocos removidos; este é o marco que o critério de adoção (A.7) mede.
Fase F10, Templates (Fatia 10)
Dependência: {F1b-01} (tabelatemplate), {F6} (RBAC ehub.is_super_admin()), {F8} (wizard usa o template no passo 1). Regra que rege a fase inteira: RN-013. Todo template nasce individual (da agência de quem criou) e só o Super Admin o marca como Global. A ordem dos cartões é a da dependência: primeiro o escopo existe no schema ({F10-00}), depois o CRUD ({F10-01}, {F10-02}), depois os dois gestos que o escopo habilita ({F10-03}, {F10-04}).
RN-013, RF-ADM-016, RF-ADM-017; artefato {ART-ENT-07}. Migração de schema, primeiro cartão da fatia. A tabela reports.template existe desde {F1b-01}, mas com a coluna is_factory e com a visibilidade ampla amarrada a company_id nulo, forma que não expressa a regra: apagar a agência de origem na promoção inviabiliza a despromoção, e "de fábrica" não é o mesmo conceito que "Global" (um Global pode ter nascido individual numa agência). A migração renomeia is_factory para is_global, mantém company_id preenchido também nos Globais promovidos (nulo só nos semeados pela operação da plataforma), cria índice em is_global e reescreve as regras de escopo: leitura quando is_global é verdadeiro OU company_id é a Company da sessão; escrita da agência só quando company_id é a Company da sessão E is_global é falso; leitura e escrita irrestritas para o Super Admin (hub.is_super_admin()). A tabela está vazia hoje (medido em 2026-07-30: 0 linhas), então a renomeação não tem migração de dado.
Como verificar (teste primeiro): com a sessão de uma agência A, ler um template Global originado na agência B devolve a linha e tentar atualizá-la é recusado; ler um individual de B não devolve nada; com a sessão do Super Admin, ler devolve os individuais de A e de B e atualizar um Global é aceito; \d reports.template não tem mais a coluna is_factory e tem is_global.
Aceite (H1): Dado um template Global originado na agência A e um template individual da agência B, Quando a sessão da agência A consulta a tabela, Então o Global aparece com o company_id da agência de origem preservado e o individual de B não aparece, E a agência A não consegue gravar em nenhum template com is_global verdadeiro, E a sessão do Super Admin lê os individuais de A e de B e grava no Global.
Estado (2026-07-30): aplicado e verificado no banco: is_global booleano não nulo com falso por default, company_id de origem preservado, e quatro regras de escopo restritivas, uma por comando, com leitura de Global ou próprio ou Super Admin e escrita exigindo próprio E não Global, sendo que a condição sobre a linha antiga fecha a autopromoção.
RF-ADM-009, RN-013. CRUD completo pelo painel; blocos padrão editáveis sem tocar em código. Todo template criado nasce com escopo individual da agência do criador. O template é gabarito consumido no nascimento do relatório: os blocos padrão são copiados para o rascunho naquele instante e o relatório não fica atrelado ao template depois.
Como verificar (teste primeiro): criar um template pelo painel e conferir que ele nasce individual da agência; iniciar um relatório com ele gera o rascunho com exatamente os blocos padrão na ordem definida; editar, duplicar e excluir o template funciona pelo painel; depois de editar e depois de excluir o template, reabrir o relatório já criado e conferir que os blocos dele não mudaram.
Aceite (H1): Dado um template "resultado de campanha" com blocos padrão definidos, Quando o operador inicia um relatório escolhendo esse template, Então o rascunho nasce com exatamente os blocos padrão do template na ordem definida, E o template criado tem escopo individual da agência do criador, E editar, duplicar ou excluir o template é possível pelo painel sem tocar em código, E editar ou excluir o template depois não altera os blocos de nenhum relatório já criado.
Estado (2026-07-30): fechado. Painel provado ponta a ponta em produção, com prova visual em pixel nos dois papéis (Super Admin e agência, via impersonation de um Team Manager da agencia-teste): CRUD completo pelo painel, escopo individual preservado, contadores corretos, mobile 390px sem perda de ação. Divergência registrada em relação ao mockup aprovado: a prévia em miniatura de cada template virou a lista real dos tipos de bloco na ordem (ex. "Hero, KPIs, Funil, Tabela"), em vez dos KPIs decorativos do mockup, decisão de implementação. A contagem "usado em N relatórios" do mockup ficou de fora desta fatia, ver {F10-05}.
RF-ADM-010, RN-013. Salvar um relatório curado como template novo, sem copiar dados do cliente; guarda também o perfil de direção (objetivo, decisão alvo, tom, blocos preferidos, pesos, sem dado do cliente) para pré-carregar o próximo relatório que usar o template (alimenta o anti-fadiga, RF-CMP-016). O template gerado nasce individual da agência do operador, mesmo quando o relatório de origem foi montado a partir de um Global.
Como verificar (teste primeiro): salvar como template a partir de um relatório curado cria um template na biblioteca com a estrutura de blocos, com escopo individual da agência, sem números nem textos específicos do cliente original; o perfil de direção salvo (sem dado do cliente) aparece pré-carregado no próximo relatório que usa esse template; repetir a partir de um relatório que nasceu de um Global e conferir que o template gerado também nasce individual.
Aceite (H1): Dado um relatório curado, Quando o operador escolhe "salvar como template" e dá um nome, Então um template novo com a estrutura de blocos daquele relatório passa a aparecer na biblioteca de templates da agência com escopo individual, E o conteúdo específico do cliente (números, textos) não é copiado para o template, E o perfil de direção (sem números nem textos do cliente) é salvo no template e pré-carregado no próximo relatório que o usar.
Estado (2026-07-30): fechado. Painel provado ponta a ponta em produção junto com {F10-01}, mesmo smoke visual. CRUD completo da agência confirmado por impersonation (Team Manager da agencia-teste com "salvar como template" disponível). Mesmas duas divergências registradas: prévia em lista real de blocos (ver {F10-01}) e contagem de uso ausente (ver {F10-05}).
RF-ADM-016, RN-013; gate de papel Super Admin, sem permissão de promoção no catálogo ({ART-ACS-01}). Promover e despromover não têm slug de permissão: o sistema confere o papel Super Admin direto, porque a concessão individual de permissão do Hub alcança qualquer permissão e qualquer usuário, e uma promoção modelada como permissão seria delegável a quem não é Super Admin, furando a exclusividade que RN-013 exige sem exceção. A escrita da tabela é a segunda camada: a linha só é aceita como Global quando quem grava é o Super Admin. Gesto exclusivo do Super Admin, na visão dele da biblioteca: marcar um template como Global e desmarcar. A promoção marca o próprio registro (mesmo identificador, company_id de origem preservado), nunca cria cópia, e os relatórios que já apontam para o template seguem apontando. Enquanto Global, o template só é editado, despromovido ou excluído pelo Super Admin, inclusive contra a agência que o criou. Despromover devolve ao escopo individual da agência de origem; um Global sem agência de origem (semeado pela operação da plataforma) não tem despromoção e a retirada dele é a exclusão. Promoção e despromoção gravam evento no log de auditoria pelo ponto único (RF-ADM-012).
Como verificar (teste primeiro): promover um template individual da agência A e conferir que o identificador não mudou, que nenhuma linha nova nasceu na tabela, que a agência de origem continua registrada e que ele aparece na biblioteca da agência B; tentar editar ou excluir esse template como admin e como operador de A é recusado; despromover devolve o template à biblioteca individual de A; tentar promover ou despromover com qualquer papel fora do Super Admin é recusado, inclusive depois de conceder individualmente a esse usuário todas as permissões do módulo; conferir os dois eventos de auditoria com valor anterior e novo.
Aceite (H1): Dado um template individual da agência A, Quando o Super Admin o marca como Global, Então o registro segue sendo o mesmo (mesmo identificador, contagem de linhas da tabela inalterada, agência de origem preservada) e passa a aparecer na biblioteca de todas as agências, E o admin e o operador de A deixam de conseguir editar, despromover ou excluir esse template, E despromover devolve o template ao escopo individual da agência A, E a tentativa de promover ou despromover feita por qualquer papel fora do Super Admin é recusada, inclusive quando esse usuário recebeu concessão individual de qualquer permissão do módulo, E cada uma das duas ações gravou evento de auditoria com valor anterior e valor novo.
Estado (2026-07-30): fechado. Interface provada ponta a ponta em produção, com prova visual em pixel: tabela do Super Admin (desktop e 390px) com coluna de agência de origem, Global com origem mostrando "Despromover", Global sem agência de origem (semeado) mostrando só editar e excluir sem despromoção, individual mostrando "Promover a Global", contadores corretos, mobile em cards sem perder ação. Divergência registrada em relação ao mockup aprovado: os eventos de promoção e despromoção SÃO gravados (ponto único RF-ADM-012), mas a seção "Log recente" da tela não lê nem mostra esse log nesta fatia, ver {F10-06}.
RF-ADM-017, RN-013. A biblioteca de uma agência lista os Globais mais os individuais dela, e nada mais, com o escopo visível em cada item; a visão do Super Admin lista também os individuais de todas as agências, que é o que torna a promoção possível. Botão "duplicar" em um Global cria um template individual da própria agência, editável, sem vínculo com o original; é o caminho para variar um Global sem pedir nada ao Super Admin.
Como verificar (teste primeiro): com um Global, um individual da agência A e um individual da agência B no banco, abrir a biblioteca como operador de A e conferir que aparecem só os dois primeiros, cada um com o escopo indicado; duplicar o Global e conferir que a cópia nasce individual de A e editável; alterar os blocos padrão do Global e conferir que a cópia não mudou; alterar a cópia e conferir que o Global não mudou; abrir a visão do Super Admin e conferir que os individuais de A e de B aparecem.
Aceite (H1): Dado um template Global, um individual da agência A e um individual da agência B, Quando um operador de A abre a biblioteca, Então ele vê o Global e o individual de A e não vê o individual de B, E ao duplicar o Global a cópia nasce individual de A e editável por ele, E alterar os blocos padrão do Global depois não altera a cópia, E alterar a cópia não altera o Global, E o Super Admin, na visão dele, enxerga os individuais de A e de B.
Estado (2026-07-30): fechado. Prova visual em pixel confirma os dois lados: agência (impersonation) vendo o Global só com usar e duplicar, com a nota "Somente o Super Admin edita", e o individual com CRUD completo; marca trocando de cor por agência (white-label ativo). Duas divergências registradas em relação ao mockup, ambas viraram cartão novo: contagem "usado em N relatórios" ausente ({F10-05}) e log de auditoria não lido na tela ({F10-06}). Defeito achado no mesmo smoke: com zero templates e nenhum filtro, o texto do estado vazio do Super Admin culpa um filtro inexistente, ver {F10-07}.
Cartão novo, nascido do fechamento de {F10-01} a {F10-04}: divergência em relação ao mockup aprovado, registrada e não escondida. O mockup aprovado (templates.html, superadmin-templates.html) mostra em cada template quantos relatórios o usam; a Fatia 10 fechou sem essa contagem porque não existe endpoint que a calcule, e a escolha foi omitir o número em vez de fabricar um. RF-ADM-009, RF-ADM-017.
O que implementar: contagem de relatórios por template de origem (preservada mesmo que o template seja depois editado ou excluído, já que o relatório não fica atrelado ao template após o nascimento, RF-ADM-009), exposta como campo na listagem de templates da agência e na biblioteca do Super Admin.
Como verificar (teste primeiro): um template usado como origem de 3 relatórios mostra "Usado em 3 relatórios" na biblioteca da agência e na visão do Super Admin; um template nunca usado mostra "Usado em 0 relatórios", a linha não some.
Aceite (H1): Dado um template usado como origem de N relatórios, Quando a biblioteca de templates é aberta (agência ou Super Admin), Então cada template mostra o número real de relatórios que o usaram como origem, E o número não depende do template ainda existir com os mesmos blocos.
Cartão novo, nascido do fechamento de {F10-03}: divergência em relação ao mockup aprovado, registrada e não escondida. Os eventos de promoção e despromoção SÃO gravados pelo ponto único de auditoria (RF-ADM-012), mas não existe rota de leitura exposta nesta fatia, então a seção "Log recente" da tela do Super Admin (superadmin-templates.html) não mostra o log. RF-ADM-012, RF-ADM-016.
O que implementar: endpoint de leitura do log de auditoria filtrado pelos eventos de template, consumido pela seção "Log recente" da tela de biblioteca do Super Admin.
Como verificar (teste primeiro): promover e depois despromover um template gera duas linhas no log; a tela do Super Admin lista as duas com autor, data, valor anterior e valor novo, sem exigir consulta direta ao banco.
Aceite (H1): Dado um template promovido e depois despromovido, Quando o Super Admin abre o log recente na tela de templates, Então as duas ações aparecem com autor, data, valor anterior e valor novo, E nenhuma leitura desse log exige acesso direto ao banco.
FECHADO em 2026-08-05, mergeado em main (319a532). O defeito foi confirmado no código antes de corrigir (a frase fixa vivia dentro do bloco do Super Admin e nunca consultava o filtro). A visão da agência já distinguia os dois casos, mas pelo tamanho da lista, mecanismo que não serve ao Super Admin porque só ele tem filtro combinável de três dimensões (escopo, agência e busca); por isso o derivado é próprio, e não uma segunda cópia do mecanismo da agência. Verificado ao vivo em sessão logada de Super Admin real, com a lista de templates zerada por resposta simulada não destrutiva (nenhum dado de produção alterado): sem filtro aparece a frase que não menciona filtro; cada uma das três dimensões isolada, e combinadas, aparece a frase do filtro com o botão de limpar, e o botão zera as três juntas. A visão da agência ficou intocada (o diff inteiro vive dentro do bloco do Super Admin). Achado colateral pré-existente, não deste cartão: o seletor de agência do Super Admin só lista agências que já possuem template, então com zero templates no sistema não há agência para escolher pela tela.
Contexto original. Defeito confirmado por olhos humanos no smoke visual, em produção. Na visão do Super Admin, com zero templates no sistema e NENHUM filtro aplicado, a tela mostra "Nenhum template encontrado com este filtro", culpando um filtro que não existe. A visão da agência distingue os dois casos; a do Super Admin não. RF-ADM-016, RF-ADM-017.
O que implementar: o estado vazio da biblioteca do Super Admin passa a checar se há filtro ativo antes de escolher o texto: sem filtro e sem templates, o texto diz que não há templates no sistema; com filtro ativo e zero resultados, o texto diz que o filtro não encontrou nada e oferece limpar o filtro.
Como verificar (teste primeiro): com zero templates no sistema, abrir a biblioteca do Super Admin sem aplicar filtro mostra o texto de "nenhum template no sistema"; aplicar um filtro que não bate com nada mostra o texto de "filtro sem resultado" com o botão de limpar.
Aceite (H1): Dado zero templates no sistema, Quando o Super Admin abre a biblioteca sem filtro ativo, Então o texto exibido não menciona filtro, E quando um filtro ativo não encontra nada o texto exibido menciona o filtro e oferece limpar, E o texto reprova por fato (a frase muda conforme haja ou não filtro ativo, nunca a mesma frase para os dois casos).
Definição de Pronto F10: escopo Global e individual valendo no schema e na tela, biblioteca com CRUD completo, "salvar como template" funcionando, promoção e despromoção exclusivas do Super Admin com auditoria, duplicação de um Global gerando cópia individual desatrelada, mais os três achados do fechamento (contagem de uso, leitura do log de auditoria na tela e o texto do estado vazio do Super Admin) resolvidos.
Fase F11, Importação de metas (Fatia 11)
Dependência: {F2-04} (snapshot copia metas), {F9} (publish). A Fatia 9 já nasce com o recorte mínimo de leitura de metas para o bloco planejado versus realizado; esta fatia é a importação estruturada completa (DRS J.4).
RF-BND-003. Metas aparecem só como comparação planejado vs realizado dentro de um relatório; sem alerta, recálculo ou notificação fora dele.
Como verificar (teste primeiro): importar uma meta e compor um relatório mostra a comparação nos blocos; nenhum cron ou notificação dispara a partir dessa meta fora do relatório.
Aceite (H1): Dado um conjunto de metas importado para um cliente, Quando o operador compõe um relatório, Então as metas aparecem apenas como comparação planejado versus realizado nos blocos daquele relatório, E o sistema não dispara alerta, recalculo periódico nem notificação baseada nessas metas fora do relatório.
RF-ING-007; artefato ART-ENT-08 (report_goal). Operador importa metas, público-alvo e planejamento (JSON, Markdown ou planilha) por cliente, gravando em report_goal (criada em F1b); o snapshot copia as metas vigentes no publish (RN-008).
Como verificar (teste primeiro): importar um arquivo de metas para um cliente disponibiliza os valores planejados nos blocos de comparação do próximo relatório composto.
Aceite (H1): Dado um arquivo de metas e planejamento importado para um cliente, Quando o operador compõe um relatório, Então os valores planejados ficam disponíveis para os blocos de comparação planejado versus realizado, E a importação não cria monitoramento contínuo.
RF-ING-007; ART-ENT-08 (report_goal). O backend da importação já existe ({F11-02}, rotas POST /reports/accounts/:accountId/goals/import e GET .../goals); falta a interface no painel do operador. Hoje a única forma de usar RF-ING-007 é chamando a API diretamente, sem tela.
O que implementar: tela no chrome do operador (Fase F7) para escolher o cliente, enviar o arquivo de metas (JSON, Markdown ou planilha) pelas rotas já existentes, e ver a lista das metas importadas daquele cliente (métrica, valor alvo, unidade, período), sem sair do painel.
Como verificar (teste primeiro): o operador abre a tela, escolhe um cliente, envia um arquivo real de metas, e vê as metas recém-importadas listadas na mesma tela, sem chamar a API por fora do painel.
Aceite (H1): Dado o operador autenticado no painel, Quando ele conclui a importação de um arquivo de metas para um cliente, Então vê as metas listadas na mesma tela sem sair do painel, E a lista reflete exatamente o que a rota GET .../goals retorna para aquele cliente.
Definição de Pronto F11: importar metas e ver a comparação planejado vs realizado num relatório publicado, sem qualquer tracking contínuo criado, e o operador consegue concluir a importação e ver as metas listadas pelo próprio painel, sem chamar a API por fora.
Fase F12, Comentários (Fatia 12)
Dependência: {F5} (leitor público), {F2-01} (id estável do bloco), {F9-11} (quem setaorphanedna republicação é o fluxo de F9). Comentário logado ancorado. Lição desta fatia, registrada porque vale para o projeto inteiro: a verificação de ponta a ponta reprovou o que os testes aprovavam. A própria política de segurança de conteúdo do leitor (semscript-srcdeclarado, caindo nodefault-src 'self') bloqueava os três scripts inline que a fatia introduziu. Efeito: o magic link nunca criava sessão, e os botões "Comentar" e "Ver trecho no relatório" eram código morto. A tela PARECIA certa porque tudo vinha renderizado do servidor, e a suíte passou porque teste não roda navegador com política aplicada. Corrigido sem afrouxar segurança, por autorização de hash, com os hashes computados a partir das MESMAS constantes que montam as tags, então tag e hash não têm como divergir;unsafe-inlinefoi recusado nominalmente, porque o leitor é internet-facing e renderiza narrativa de inteligência artificial e texto de Contact (verificado: zerounsafe-inlineemscript-src). Segundo defeito da mesma verificação: todo comentário aparecia assinado como "Cliente" genérico, porque a coluna de nome do autor existia no banco desde a migração de 2026-07-31 e a consulta do leitor simplesmente não a selecionava. A guarda genérica que impede a repetição disso nasceu como {F12-07}.
RF-COM-001. Comentar exige login (Contact do Hub); nenhum comentário anônimo é gravado.
Como verificar (teste primeiro): um visitante não autenticado que tenta comentar é levado ao atalho de acesso do Hub; nenhum comentário sem autor autenticado aparece no banco.
Aceite (H1): Dado um visitante não autenticado numa página pública, Quando ele tenta comentar, Então o sistema pede login (via atalho de acesso do Hub) antes de aceitar o comentário, E nenhum comentário anônimo é gravado.
FECHADO em 2026-08-02 por verificação real, com lado negativo: anônimo recebe 401 na escrita e a tela mostra o convite de acesso em vez de erro cru; nenhum comentário sem autor autenticado no banco.
RF-COM-002. Escrita por endpoint separado autenticado; role do leitor segue sem permissão de escrita.
Como verificar (teste primeiro): enviar um comentário passa por um endpoint distinto do leitor; a role do leitor público ({F5-01}) continua sem qualquer INSERT/UPDATE possível.
Aceite (H1): Dado o serviço do leitor público, Quando um comentário é enviado, Então a escrita acontece por um endpoint separado autenticado, E a role do leitor público segue sem qualquer permissão de escrita.
FECHADO em 2026-08-02, pela verificação de privilégio da Fatia 13 ({F13-08}). A primeira metade já estava provada nesta fatia (a escrita passa por endpoint separado e autenticado, anônimo recebe 401). A segunda metade, que ficou aberta de propósito por não ter prova (o 401 do endpoint prova isolamento de leitura, não ausência de permissão de escrita da role), foi fechada pela conferência de privilégios no banco feita na Fatia 13: a role do leitor público segue sem permissão de escrita.
RF-COM-003. Cada comentário carrega o id do bloco e o trecho citado (excerpt obrigatório).
Como verificar (teste primeiro): criar um comentário sobre um bloco grava o id do bloco e o excerpt; o comentário aparece ancorado ao bloco correto na leitura.
Aceite (H1): Dado um comentário criado sobre um bloco, Quando ele é gravado, Então carrega o id do bloco e o trecho citado, E o comentário aparece ancorado ao bloco correto na leitura.
FECHADO em 2026-08-02 por verificação real: comentário aparece ancorado no bloco certo, legível em desktop e em 390 pixels de largura; nenhum comentário vaza entre relatórios. Defeito corrigido no caminho: a assinatura vinha como "Cliente" genérico porque a consulta do leitor não selecionava a coluna de nome do autor, que existia no banco desde a migração de 2026-07-31.
RF-COM-004. O orphaned é setado no fluxo de republicação ({F9-11}); aqui a leitura exibe o comentário órfão com o excerpt e a marca "referente a versão anterior", sem versionar a página.
Como verificar (teste primeiro): comentar um bloco, remover esse bloco numa republicação ({F9-11}); o comentário permanece visível na leitura com a marca de versão anterior, sem versionar a página.
Aceite (H1): Dado um comentário ancorado num bloco, Quando esse bloco é removido numa republicação, Então o comentário permanece visível com o trecho citado e a marca "versão anterior", E o sistema não cria uma versão navegável da página para preservá-lo.
FECHADO em 2026-08-02 por verificação real: o comentário órfão aparece marcado como referente à versão anterior, com o trecho preservado, e SEM o link de âncora, que é o comportamento correto porque o bloco não existe mais.
RF-COM-005. Resolver fecha o loop: notifica o autor e marca "resolvido".
Como verificar (teste primeiro): marcar um comentário como resolvido dispara notificação ao autor e muda o estado exibido.
Aceite (H1): Dado um comentário aberto, Quando o operador o marca como resolvido, Então o autor do comentário é notificado, E o comentário passa a exibir estado "resolvido".
FECHADO em 2026-08-02: auditado e provado em integração real, 15 de 15 verdes.
RF-COM-006. Cliente sem sessão pede acesso e recebe magic link do Hub; comentário habilita após autenticar.
Como verificar (teste primeiro): um Contact provisionado sem sessão pede acesso pela página, recebe o magic link, autentica, e o campo de comentário fica habilitado.
Aceite (H1): Dado um cliente com Contact provisionado mas sem sessão, Quando ele pede acesso pela página, Então recebe um magic link do Hub que abre a sessão, E o campo de comentário fica habilitado após a autenticação.
FECHADO em 2026-08-02 por verificação real: o magic link cria a sessão sozinho e o campo de comentar aparece em seguida. Este era justamente o caminho que a política de segurança de conteúdo bloqueava antes do fix (ver a lição no topo da fase).
FECHADO em 2026-10-16: pacote main 9f7bd4a (merge b939361), Hub main 5248b6e (merge
af9ef07, carimbo aa05a99; pacote 2016/2016), API 15/15 e web publicado. Review APPROVED na rodada 5:
as rodadas 1 a 4 acharam fail-open no extrator por regex (src= dentro de atributo, > dentro de
valor, comentário curto, <!-- em atributo/RCDATA/script e teto de profundidade que descartava em
silêncio). A saída foi trocar o extrator caseiro pelo parse5 (WHATWG, devDependency, só em teste) e
fazer o teto estourado virar NAO MEDI. O hash vem do texto servido: o parse5 normaliza CR LF como o
navegador. qa PASS em Chromium: zero violação com os 3 scripts rodando de verdade, 1 violação ao
injetar um script extra, e o par sem CSP executa. No Hub, guarda dos 2 emissores com exceção nominal
que expira; evaluateInlineScriptCsp está no registro do {F18-11}. Pôr CSP nas 2 páginas do Hub ficou
fora do card (a de retorno precisa de nonce).
RNF-SEG-007; nasce da lição desta fatia. Já existe teste cobrindo os três scripts inline atuais, e é exatamente por isso que este cartão é necessário: um teste que enumera três scripts conhecidos não reprova o QUARTO. Qualquer script inline futuro morre em silêncio do mesmo jeito, com a tela parecendo certa porque o servidor renderiza o conteúdo, e a suíte passando porque teste não roda navegador com política aplicada.
O que implementar: conferência automática que extrai os scripts inline efetivamente servidos pelo leitor e os compara com os hashes declarados na política, reprovando divergência nos DOIS sentidos (script servido sem hash correspondente, e hash declarado sem script correspondente); asserção nominal de que script-src não contém unsafe-inline; derivação dos hashes a partir das mesmas constantes que montam as tags, nunca de uma cópia colada do script.
Como verificar (teste primeiro): acrescentar um script inline novo sem regerar o hash e confirmar que a conferência REPROVA; alterar um caractere do conteúdo de um script existente sem regerar o hash e confirmar que reprova; acrescentar unsafe-inline a script-src e confirmar que reprova; remover um script deixando o hash órfão e confirmar que reprova; conferir que a página real carrega os scripts sem erro de política no console do navegador (não apenas em teste sem navegador). Fixture de controle negativo (RNF-MANUT-003): o script inline sem hash, que a suíte DEVE reprovar.
Aceite (H1): Dado o leitor público servindo uma página com scripts inline, Quando a conferência roda, Então todo script inline servido tem hash correspondente declarado na política, E script-src não contém unsafe-inline, E acrescentar um script inline novo sem o hash faz a conferência reprovar, E alterar o conteúdo de um script sem regerar o hash também faz a conferência reprovar.
FECHADO em 2026-08-08, mergeado e deployado. A correção reserva espaço à direita do título em
tela estreita, sem mover nem encolher o botão, que é a única forma de alcançar o comentário.
Provado em produção por geometria real: a sobreposição medida entre o botão e a primeira linha do
título dá falso, e o limite de requisições do leitor não regrediu no rebuild.
O número que o smoke original trouxe estava certo, e era pequeno demais: o relato dizia três
tipos de bloco colidindo, e a medição contra o código anterior achou 12 de 14 com título. Os
dois que escapavam era por espaçamento próprio, não por proteção estrutural. Corrigir só os
três reportados teria deixado nove defeitos vivos, por isso a correção vale para as classes de
título que todo tipo reusa, e não por tipo.
O teste precisou de duas correções para virar prova: ele mede interseção com a **linha
renderizada** do título, não com a caixa do elemento (que cobre a largura toda e daria falso
positivo); e pulava em silêncio quando a biblioteca de navegador não estava instalada, ou
seja, passaria vazio em ambiente novo sem avisar. Agora falha duro. Guarda que falha aberta não
guarda, e esta era a única rede protegendo o tamanho reservado contra o botão crescer.
Cartão novo, VISTO em 2026-08-08 na verificação do {F5-06}, em tela de 375 pixels. O botão de
comentar se sobrepõe ao título do bloco quando ele não tem sobretítulo e o texto é longo,
reproduzido em três dos dezesseis tipos de bloco (comparação, gráfico de linha e rosca). O cliente
final abre a página no telefone, então isso é defeito visível para quem paga o relatório.
Por que só apareceu agora, e a lição: o defeito existia desde que o botão nasceu, e ficava
invisível porque a página pública não declarava viewport, então nenhum celular renderizava em
largura real. Consertar aquilo acordou este. **Quando uma camada adormecida acorda, o que ela
revela não é regressão: é dívida que estava escondida**, e vale procurar irmãos antes de fechar.
RF-COM-002, RF-REN-006.
O que implementar: o botão deixa de disputar espaço com o título em tela estreita, sem sumir
(ele é o que torna o comentário alcançável) e sem quebrar o desenho de tela grande, que funciona.
Como verificar (teste primeiro): percorrer os dezesseis tipos de bloco em largura de celular,
com título longo e sem sobretítulo, e confirmar que nenhum tem sobreposição e que o botão continua
acionável por toque; e que a tela grande não mudou.
Aceite (H1): Dado um relatório publicado aberto em tela de celular, Quando um bloco tem título
longo e nenhum sobretítulo, Então o botão de comentar não se sobrepõe ao título em nenhum dos tipos
de bloco, E continua acionável, E o desenho em tela grande permanece como está.
Definição de Pronto F12: comentário logado, ancorado, sobrevivente a republicação e com resolução notificando o autor, funcionando ponta a ponta, PROVADO em navegador real com a política de segurança aplicada (e não só em suíte de teste), com a guarda genérica de script inline no lugar. Estado em 2026-08-02: {F12-01}, {F12-03}, {F12-04}, {F12-05} e {F12-06} fechados por verificação real; {F12-02} aberto por metade de H1 sem prova (falta o controle negativo sobre a role do leitor); {F12-07} aberto.
Fase F13, Analytics (Fatia 13)
Dependência: {F5} (leitor público emite os acessos), {F9} (link publicado). Medição de audiência. Contexto de empresa ativo é obrigatório para o Super Admin ver este painel, e isso NÃO é defeito. Em modo global (sem empresa ativa) o painel vem vazio, porque a regra de acesso ao relatório é por agência e não abre exceção para quem enxerga todas as empresas. O comportamento é coerente com o resto do produto (Relatórios é módulo por agência, RF-ADM-001) e é o correto: uma exceção para o papel global significaria uma segunda regra de acesso ao mesmo dado, e regra de acesso duplicada é como isolamento entre agências morre. Está escrito aqui e no DRS (RF-ANL-006) porque custou tempo de investigação para quem chegou sem saber, e vai custar de novo.
RF-ANL-001. Evento de visualização ingerido por pipeline separado do render; não bloqueia o caminho somente-leitura.
Como verificar (teste primeiro): abrir uma página pública gera evento sem que o tempo de resposta do leitor dependa da escrita do evento.
Aceite (H1): Dado a leitura de uma página pública, Quando um evento de visualização é registrado, Então ele é ingerido por um pipeline separado do render, E a coleta de evento não bloqueia nem depende do caminho somente-leitura do leitor.
FECHADO em 2026-08-02, com os dois lados negativos exercitados: com o ingestor artificialmente LENTO a resposta do leitor continua rápida (prova o "não bloqueia"), e com o ingestor FORA DO AR a página não cai (prova o "não depende"). São dois modos de falha distintos e os dois foram provocados de propósito, não observados por sorte.
RF-ANL-002. Analytics começa com número de visualizações e último acesso por link.
Como verificar (teste primeiro): acessar um link várias vezes e conferir no painel do relatório o número de visualizações e a data/hora do último acesso.
Aceite (H1): Dado um link acessado várias vezes, Quando o operador abre o analytics do relatório, Então vê o número de visualizações e a data e hora do último acesso, E esses valores refletem os acessos reais registrados.
FECHADO em 2026-08-02: 3 acessos reais gerados no link público apareceram no painel, com a contagem e a data e hora do último acesso batendo com os acessos gerados. O critério exige que os valores REFLITAM acessos reais, e foi assim que se testou, com acesso real e não com linha semeada no banco.
FECHADO em 2026-08-08, mergeado e deployado. A prova é o número: o mesmo relatório passou a
mostrar 147 nos dois lugares que exibem visualização, consultados em produção depois do deploy.
Antes um dizia 147 e o outro 152, e o que mostrava a mais era justamente a tela onde o operador
acompanha o cliente.
O conserto não foi copiar o filtro: os dois pontos calculavam a contagem por conta própria, e
copiar criaria um terceiro caminho igualmente frágil. Agora existe uma fonte só, e uma varredura
que reprova quando alguém escrever contagem fora dela, provada nos dois sentidos, inclusive
encontrando as quatro ocorrências no código anterior.
O cuidado que evitou um defeito silencioso: a fonte única foi criada com avaliação de permissão
pelo chamador, de propósito, porque a identidade do banco tem privilégio de contornar o
isolamento entre agências. Sem essa opção, ela teria desligado a separação em silêncio. Provado com
duas empresas reais: cada uma enxerga só a própria linha, e ambas aparecem apenas sob a identidade
privilegiada, o que demonstra que o isolamento vem da escolha e não de acaso. O leitor público
continua sem alcance a ela.
Uma ponta mais ampla que o requisito, e consciente: a marca de sessão vale por domínio, então
operador de outra agência também fica de fora. Nunca infla o número, só exclui a mais. **RESOLVIDO
em 2026-08-08, alinhando o TEXTO ao comportamento**, porque nenhuma superfície deste sistema consome
a distinção "de outra agência" e o RF-ANL-006 proíbe criá-la, ler relatório alheio exige o token
secreto (RNF-SEG-001) e portanto não é fluxo legítimo, e a alternativa levaria identificador de
tenant para um cookie lido por serviço anônimo. O requisito saiu MAIS exigente, não menos: a medição
mostrou que faltava alguém reprovar o caso em que essa amplitude morde o próprio cliente, então o H1
ganhou a cláusula de que a leitura do cliente CONTA.
**VERIFICADO em 2026-08-08, e o defeito é outro: o mecanismo FUNCIONA, e existem DOIS contadores que
discordam.** A coleta distingue corretamente acesso de dentro e de fora, provado com acessos reais
ao link público e conferência no banco antes e depois: cada um caiu no balde certo. O painel de
medição filtra as sessões da agência como deve. Mas a ficha do cliente conta sem filtro nenhum,
e é ela que mostra o número na tela onde o operador acompanha o cliente. Os dois números divergem
ao vivo, no mesmo relatório, no mesmo instante.
Por que isso é grave apesar de a produção ainda não estar inflada: nenhum cliente real tem
relatório publicado hoje, então o número errado ainda não chegou a ninguém. Ele infla no primeiro.
E o defeito é da família mais cara deste produto: número verdadeiro na frase errada, a
contagem existe, está certa na origem, e o lugar onde a pessoa a lê mostra outra.
O que implementar: o número passa a ter uma fonte só. Não basta copiar o filtro para o
segundo ponto: dois lugares calculando a mesma coisa independentemente foi o que criou a
divergência, e um terceiro nasceria igual. E a varredura precisa saber reprovar quando aparecer
contagem de visualização sem o filtro.
Uma ponta mais ampla que o requisito, e é consciente: a marca de sessão vale por domínio, não
por agência, então um operador de outra agência também é excluído. É decisão registrada no
código, mas o requisito fala em "a própria agência", vale alinhar o texto ao comportamento, ou o
contrário.
Como verificar (teste primeiro): um acesso externo e um interno ao mesmo relatório, e todos
os lugares que mostram visualização exibindo o mesmo número; e a varredura reprovando uma contagem
nova escrita sem o filtro.
RF-ANL-003. Acessos vindos de navegador com sessão da plataforma, de qualquer agência, e o modo apresentar não entram na contagem; a leitura do cliente entra.
Como verificar (teste primeiro): um acesso com sessão da plataforma não sobe a contagem do cliente; o mesmo link aberto sem sessão sobe; a leitura do cliente pela sessão de comentário sobe; apresentar em reunião não sobe.
Aceite (H1): Dado um acesso ao link público a partir de navegador com sessão ativa da plataforma, Quando as métricas são contadas, Então esse acesso não entra na contagem de visualizações do cliente; E dado o mesmo link aberto sem sessão da plataforma, Quando as métricas são contadas, Então esse acesso ENTRA na contagem; E dado o cliente que abriu sessão de comentário pelo próprio link de acesso, Quando ele lê o relatório, Então a leitura dele ENTRA na contagem; E o modo apresentação não emite evento de visualização.
SEGUE ABERTO em 2026-08-02, por um caminho do H1 não exercitado, e não por falta de construção. Provado com lado negativo: sem o cookie de sinal o acesso CONTA, com ele não conta, que é exatamente a forma certa de provar uma exclusão. O que falta: o H1 nomeia DOIS caminhos de entrada, o operador que abre o link e o modo apresentar, e só o primeiro foi exercitado. O modo apresentar é um fluxo próprio (Fatia 8) e precisa emitir o mesmo sinal; se ele não emitir, apresentações em reunião inflam a contagem do cliente em silêncio, que é a falha que este requisito existe para impedir. Provar o mecanismo do cookie não prova a cobertura dos dois caminhos. Fecha exercitando o modo apresentar e conferindo que a contagem não sobe. Atualização de 2026-08-02: veio uma ANÁLISE DE CÓDIGO argumentando que o modo apresentar é renderização no próprio navegador do que já estava carregado, e portanto nunca chama a rota pública nem qualquer contagem. O argumento é estrutural e provavelmente certo, e mesmo assim NÃO fecha o cartão: quem analisou foi honesto em registrar que não conseguiu completar o clique real, porque um guard de contexto de empresa barrou. Pelo critério desta fase, caminho distinto que falharia calado não fecha por leitura de código, fecha por exercício. Fica aberto.
RF-ANL-004. Visitante deslogado aparece agregado como "visitante" com identificador opaco; sem email cru no HTML.
Como verificar (teste primeiro): inspecionar o HTML público e o payload de analytics; confirmar ausência de email cru e presença de identificador opaco.
Aceite (H1): Dado um visitante deslogado, Quando ele é contabilizado, Então aparece agregado como "visitante" com identificador opaco, E nenhum email cru é exposto no HTML público.
FECHADO em 2026-08-02, as duas cláusulas provadas separadamente porque são fatos diferentes. Primeira: o identificador é derivado por hash com sal que MUDA TODO DIA, não reversível nem correlacionável entre dias, e o lado negativo confirmou que ele nunca carrega o endereço de origem. Segunda: varredura do HTML BRUTO de 2 relatórios publicados reais, buscados anonimamente, com zero email de verdade (o único casamento foi um campo de formulário vazio). A segunda ficou aberta por uma rodada de propósito, porque tratava de outra superfície e não se deduzia da primeira.
RF-ANL-005. Origem inferida pelo referrer; ausência de referrer vira "origem desconhecida".
Como verificar (teste primeiro): acessos com e sem referrer disponível são registrados corretamente, sem inventar canal quando ausente.
Aceite (H1): Dado um acesso com referrer disponível, Quando a origem é registrada, Então ela reflete o referrer, E a ausência de referrer é registrada como origem desconhecida, sem inventar um canal.
FECHADO em 2026-08-02, com o lado perigoso visto na tela: sem referrer a origem sai como "Origem desconhecida", e nenhum canal é fabricado. Este é o modo de falha que importa, porque canal inventado é mentira silenciosa que o leitor acredita; origem errada com referrer presente seria erro visível. As duas cláusulas saem do mesmo resolvedor de origem, e foi a saída dele que se olhou.
RF-ANL-006. Admin vê agregados (visualizações, último acesso, comentários não lidos, duração de produção) só da própria agência.
Como verificar (teste primeiro): admin da agência A abre o painel e vê só os agregados de A; nenhum dado de outra agência aparece.
Aceite (H1): Dado um admin da agência A, Quando ele abre o painel de analytics, Então vê os agregados apenas dos relatórios de A, E não enxerga dados de nenhuma outra agência.
FECHADO em 2026-08-02 por verificação visual em pixel (desktop e 390 pixels), com o isolamento atacado de propósito. O isolamento não foi observado, foi TESTADO: a tentativa ativa de abrir relatório de outra agência responde como inexistente, e não como permissão negada, que é a resposta certa (negar permissão confirma a existência do recurso e já é vazamento). Também provado: números reais refletidos; zero distinguível de não medido (um relatório sem acesso mostra visualizações 0, traço no último acesso e a frase "Sem acessos ainda", ao lado de outro com 3 visualizações e data real), que é a distinção da RN-015 aplicada à superfície de analytics; as métricas que o DRS difere nesta fatia estão ausentes sem deixar buraco visual; e papel sem a capability não vê o item de menu E é recusado na URL direta, ou seja, a guarda não é só de menu.
Nota de comportamento, para não parecer defeito: o Super Admin precisa de contexto de empresa ativo para ver os dados de uma agência. Em modo global o painel vem vazio, e isso é correto (ver a nota no topo desta fase e RF-ANL-006 no DRS).
RF-ADM-015. Duração decorrida entre início da composição e publicação, disponível no painel de analytics.
Como verificar (teste primeiro): iniciar e publicar um relatório e conferir a duração registrada no painel (alimenta o critério de adoção A.7).
Aceite (H1): Dado um relatório iniciado no wizard, Quando ele é publicado, Então o sistema registra a duração decorrida entre o início da composição e a publicação, E essa duração fica disponível no painel de analytics da agência.
FECHADO em 2026-08-02, com lado negativo: o positivo mede o tempo decorrido e o exibe no painel; o negativo prova que REPUBLICAR não altera o valor já registrado, que é o que garante que a métrica meça produção e não a última vez que alguém mexeu. Sem esse negativo, a duração viraria ruído justamente no critério de adoção (A.7) que ela alimenta.
RF-ANL-001; artefato ART-INFRA-04. Container próprio que ingere os eventos de audiência e escreve em link_event, separado do leitor público (que é só-SELECT, RF-REN-006). O leitor emite o evento, o ingestor grava; a coleta não bloqueia o caminho somente-leitura.
Como verificar (teste primeiro): o ingestor é um processo distinto do leitor; a role do leitor segue sem INSERT; um pico de eventos não degrada o tempo de resposta do leitor.
Aceite (H1): Dado a leitura de uma página pública, Quando um evento é registrado, Então ele é gravado em link_event pelo ingestor separado, E a role do leitor público não tem permissão de escrita e o caminho de leitura não depende da escrita do evento.
FECHADO em 2026-08-02, com privilégio conferido no banco e não deduzido do código: a role do ingestor tem ZERO permissão de tabela e só execução da função, que é o menor privilégio possível para quem só precisa gravar evento; e a role do leitor público segue sem permissão de escrita. O "caminho de leitura não depende da escrita" está provado pelo mesmo controle negativo de {F13-01} (ingestor fora do ar, página de pé). Esta mesma conferência fechou a metade que faltava em {F12-02}, que estava aberto exatamente por não ter o controle negativo sobre a role do leitor.
Definição de Pronto F13: painel de analytics por agência com visualizações, último acesso, referrer, exclusão de sessão interna e duração de produção, isolado por tenant, alimentado por um ingestor de eventos separado do leitor. Estado em 2026-08-02: {F13-01}, {F13-02}, {F13-05}, {F13-06}, {F13-07} e {F13-08} fechados com lado negativo exercitado; {F13-03} aberto (falta exercitar o modo apresentar, que é o segundo caminho de entrada nomeado pelo H1) e {F13-04} aberto (falta varrer o HTML público em busca de email cru). Os dois eram verificações baratas e nenhum dos dois era falta de construção. Atualização de 2026-08-08: os dois fecharam. {F13-04} fechou em 2026-08-02 (varredura do HTML público confirmou zero email cru, ver o cartão). {F13-03} fechou em 2026-08-08, mergeado e deployado (fonte única de contagem de visualização, ver o cartão). Fase F13 FECHADA, 8 de 8.
Fase F14, Domínio custom e white-label (Fatia 14)
Dependência: {F7} (chrome), {F9} (link). Última camada de branding por agência. Limitação DECLARADA desta fase (ADR-020), não pendência esquecida: ativar o domínio com certificado exige hoje um passo MANUAL no servidor, porque a infra não tem emissão de certificado sob demanda. O ciclo do domínio e a validação de posse funcionam sozinhos; só a emissão do certificado tem esse passo, documentado no código e num script de provisionamento. Está escrito aqui para que quem for ativar um domínio saiba de antemão, em vez de descobrir na hora e tratar como defeito.
RNF-SEG-006, ADR-015; artefato ART-ENT-10 (agency_domain). Criar a entidade agency_domain (company_id, domain, txt_token, status) e a validação: o vínculo de domínio custom por agência só ativa após validação de posse por registro TXT; desvínculo remove o CNAME. O proxy e o certificado on-demand são trabalho novo se a plataforma-mãe não tiver mecanismo próprio (confirmar antes desta fatia, ADR-015).
Como verificar (teste primeiro): a tabela agency_domain existe; tentar ativar um domínio custom sem o registro TXT correto falha; ativar com o TXT correto liga o CNAME e emite o certificado; desvincular remove o CNAME.
Aceite (H1): Dado uma agência que quer um domínio custom, Quando ela não tem o registro TXT de posse configurado, Então o vínculo não ativa, E configurar o TXT correto ativa o CNAME com certificado, E desvincular remove o CNAME.
FECHADO em 2026-08-02, com o isolamento white-label provado pelo NEGATIVO e por HTTP real: uma requisição chegando pelo domínio ativo da agência A carregando token de relatório da agência B é recusada como INEXISTENTE. Esse é o coração da feature (domínio custom sem isolamento é vazamento entre agências com marca do cliente em cima) e foi exercitado, não deduzido. A tabela de domínio por agência tem o ciclo pendente, verificado e ativo, e a verificação de posse roda contra registro DNS REAL, sem simulação. Limitação declarada (ADR-020): a emissão do certificado na ativação tem um passo manual no servidor, porque a infra não tem emissão sob demanda; documentado no código e no script de provisionamento.
RNF-SEG-006. A gestão de domínio custom já é uma seção da tela de identidade da agência no mockup (agencia-branding.html): o admin cadastra o domínio, vê o token TXT a configurar e acompanha o status (pendente, verificado). O que falta no mockup é o estado "pendente de validação TXT" antes do "propagado", a incluir numa iteração; a construção segue essa seção como referência.
Como verificar (teste primeiro): o admin cadastra um domínio, vê o token TXT, e a tela mostra o status mudar de pendente para verificado após a posse; a tela é escopada por agência.
Aceite (H1): Dado um admin de agência na tela de domínio custom, Quando ele cadastra um domínio, Então vê o token TXT a configurar e o status pendente, E após a validação de posse o status vira verificado, E a tela só mostra os domínios da própria agência.
FECHADO em 2026-08-02, provado ao vivo por navegador: criar o domínio, verificar a posse contra DNS real e remover, os três no fluxo da tela. Defeito achado e corrigido no caminho: a rota estática desta tela nasceu SOMBREADA por uma rota dinâmica do mesmo prefixo e respondia erro para todo chamador, com a interface mascarando como "não tem nada" (segunda ocorrência do mesmo padrão no dia; virou RNF-MANUT-004 e o cartão {F18-18}).
Definição de Pronto F14: domínio custom por agência funcionando com validação de posse obrigatória, entidade agency_domain e tela de gestão; a tela nova entra numa iteração do mockup. Estado em 2026-08-02: FASE FECHADA, com o isolamento white-label provado pelo negativo em HTTP real e a emissão de certificado registrada como limitação declarada (ADR-020), não como pendência.
Fase F15, PDF condicional (Fatia 15)
Dependência: {F3-02} (package renderizador). Reuso do gerador da plataforma-mãe. Lição desta fase, e o motivo de a regra de paginação estar no DRS (RNF-REN-001): o uniforme já foi tentado e falhou nas DUAS pontas. A primeira versão não tinha regra nenhuma e cortava bloco no meio da página, deixando a página começar por um número solto sem rótulo, que é defeito de rótulo produzido pela diagramação. A primeira correção resolveu o corte, mas aplicou a regra de forma CEGA, empurrando cada bloco inteiro para a página seguinte: a primeira página ficou com cerca de 60 por cento em branco e o documento inchou para 11 páginas. Esse segundo defeito só apareceu porque alguém ABRIU o arquivo; não estava em nenhum relatório de execução. A correção final é seletiva por natureza do bloco (pequeno e indivisível não quebra; longo e divisível quebra entre linhas com o cabeçalho repetindo), e o resultado é 8 páginas densas, sem corte e sem vazio. Quem for simplificar isso depois precisa saber que o simples já foi tentado.
ALERTA de 2026-08-03 RESOLVIDO em 2026-08-05: a reintegração terminou e o código chegou à linha principal e à produção. A fase do PDF (junto das Fatias 11 a 20 inteiras) foi mergeada na linha principal e colocada em produção em 2026-08-05, com QA e revisão de código independentes rodadas pela primeira vez sobre o pacote acumulado (zero bug bloqueante, zero regressão fora do escopo). A rota de PDF está confirmada respondendo em produção. A prova antiga (visual, página a página) segue válida porque foi feita sobre o MESMO código que chegou à produção, sem alteração no caminho. Lição de processo preservada, porque continua valendo para qualquer fase futura: encadear branch por fase cria dependência invisível, e uma fase que nasce da branch errada desaparece sem aviso, sem que teste, gate ou suíte apontem, porque todos rodam dentro da branch onde o código obviamente existe.
RF-REN-008. PDF gerado pelo package renderizador, tema claro com contraste AA; se o gerador não estiver disponível, a opção não é oferecida.
Como verificar (teste primeiro): com o gerador disponível, pedir o PDF produz um arquivo pelo mesmo package do leitor; com o gerador indisponível, a opção de PDF não aparece.
Aceite (H1): Dado um relatório publicado e o gerador de PDF disponível, Quando o operador pede o PDF, Então o PDF é gerado pelo package renderizador em tema claro com contraste AA e assets embutidos, E se o gerador não estiver disponível, a opção de PDF não é oferecida.
FECHADO em 2026-08-02 por inspeção visual do arquivo, página a página, e não por conferência de estrutura. Documento real de 22 blocos e 11 tipos, tema claro, cor da agência aplicada em rótulo, valor e barras do funil, e nenhuma marca da Modulareasy confirmada NO VISUAL, não apenas por busca de texto (busca de texto não pega marca em imagem, que é justamente o modo de vazamento que importa no white-label). Nenhum bloco cortado: cada página nova começa pelo título do bloco, nunca por um número solto sem rótulo. Sem token cru, sem objeto cru, sem NaN.
Paginação por natureza do bloco (RNF-REN-001), com a história do defeito registrada acima: a regra uniforme foi tentada e falhou nas duas pontas (sem regra corta o bloco; com "nunca quebrar" a primeira página fica 60 por cento vazia e o documento vai a 11 páginas). A regra final é seletiva e o resultado medido é 8 páginas densas.
RF-REN-009. Leitor usa URL assinada de curta duração; PDF recebe assets em base64 pré-buscado, sem fetch de rede na geração.
Como verificar (teste primeiro): inspecionar as imagens do leitor (URL assinada com expiração curta) e do PDF gerado (base64 embutido, sem chamada de rede durante a geração).
Aceite (H1): Dado um relatório com criativos, Quando o leitor o exibe, Então as imagens carregam por URL assinada de curta duração, E o PDF do mesmo relatório embute os assets em base64 pré-buscado, sem buscar em rede durante a geração.
REABERTO e RE-FECHADO no mesmo dia (2026-08-05), com a lição registrada. Reabri este cartão por engano: consultei o serviço de armazenamento por objeto da plataforma-mãe, achei zero registro e concluí que o bucket nunca existiu. O erro não foi medir, foi medir o mecanismo errado. Uma segunda frente apontou, e conferi por conta própria antes de aceitar: a plataforma-mãe nunca usou aquele serviço, de nenhum módulo, então a consulta sempre daria zero, com ou sem este cartão. O mecanismo real é armazenamento em sistema de arquivos sobre volume dedicado, o mesmo que outras áreas do Hub já usam para anexo, e a medição direta nele confirmou os DOIS lados do H1: o leitor serve a imagem por URL assinada por HMAC com expiração curta (verificada antes de qualquer busca ao arquivo), e a rota de PDF monta o mapa de asset para data-URI em base64 pré-buscado antes de chamar o gerador, sem fetch de rede durante a geração, exatamente como o H1 exige. Achei também arquivo real de artefato do módulo já gravado nesse mecanismo, prova de uso real e não só de código lido. Fecha por mecanismo confirmado, não pela crença de 2026-08-02.
Consequência na fila, corrigida de novo: o {F8-07} (inserir criativos no relatório) NÃO está mais bloqueado por storage (o mecanismo real sustenta o que o cartão promete). A segunda metade desta frase envelheceu e está corrigida em 2026-08-08: ela dizia que o cartão seguia aberto só por falta de exercitar o H1, e a medição do planejamento mostrou que o motivo é outro e maior, falta de construção (nenhuma rota cria bloco, nenhum controle da tela envia imagem para o rascunho). O motivo vigente vive no próprio {F8-07}.
Defeito corrigido nas DUAS superfícies: a moldura de mídia estava errada no leitor e no PDF, porque era o mesmo CSS compartilhado. Não era defeito do PDF, era defeito do renderizador aparecendo em dois lugares; corrigir só no PDF teria deixado o leitor errado e criado divergência entre as duas saídas que o RF-REN-005 existe para impedir.
Definição de Pronto F15: PDF gerado pelo mesmo package do leitor, condicional à disponibilidade do gerador, sem fetch de rede na geração, com paginação por natureza do bloco (RNF-REN-001) e verificação visual do arquivo página a página, porque as duas falhas conhecidas de paginação não aparecem em teste de estrutura. Estado em 2026-08-02: FASE FECHADA.
Fase F16, Painel do cliente (Fatia 16, última fatia)
Dependência: {F1b-01} (as tabelas de dados já nascem no schema do dia 1, incluindo o modelo de identidade do cliente). Fecha o ciclo de administração do cliente final. NOTA DE PROCEDÊNCIA DA PROVA, e ela vale para os 7 cartões desta fase. Diferente das Fatias 13 e 15, cujas evidências visuais foram abertas e conferidas pelo dono do projeto, os screenshots desta fatia NÃO passaram pelo crivo visual dele. A prova é de primeira mão de quem construiu, com método correto (navegador real, login real, troca de empresa pelo switcher de verdade, desktop e 390 pixels) e com detalhes específicos que indicam observação real, mas é verificação de SEGUNDA mão do ponto de vista da aprovação. O motivo está registrado e é honesto: a sessão chegou perto do limite de contexto e a escolha foi fechar declarando isso, em vez de fingir uma verificação que não houve. Se aparecer defeito visual nesta fatia, este parágrafo é a explicação: não é caso de "foi visto e aprovado", é caso de "foi provado por quem construiu e aceito assim, com a diferença escrita". Comportamento de plataforma que vale referenciar aqui (documentado em RF-ANL-006 do DRS e no topo da Fase F13): o Super Admin em modo global, sem contexto de empresa ativo, sempre vê a pasta de relatórios VAZIA, porque a regra de acesso ao relatório não abre exceção para quem enxerga todas as empresas. É coerente com o produto (Reports é módulo por agência) e não foi introduzido nesta fatia. Está repetido aqui porque quem chegar pelo painel do cliente tropeça na mesma coisa e conclui que é defeito.
RF-ADM-005, Ap.6. Reports lê e popula a ficha existente account_identidade_visual do Hub; tom de voz alimenta a narrativa (consumido desde {F1-09}).
Como verificar (teste primeiro): preencher a ficha de um cliente (cores, tom de voz) e confirmar que a composição e o publish seguinte usam esses valores; nenhuma segunda tabela de identidade é criada.
Aceite (H1): Dado um cliente com ficha de identidade visual preenchida (cores válidas e tom de voz), Quando um relatório é composto e publicado para ele, Então os tokens visuais da página usam as cores da ficha, E o tom de voz da ficha é injetado como parâmetro da narrativa da IA.
FECHADO em 2026-08-02 (ver a nota de procedência da prova no topo da fase). Bug real corrigido de passagem, e PRÉ-EXISTENTE a esta fatia: a cor da agência era lida no formato errado, então NENHUM relatório com ficha preenchida mostrava a cor certa. O defeito não foi introduzido aqui, foi descoberto aqui; sem esta fatia ele seguiria invisível, porque a página renderizava normalmente, só que com a cor errada, e cor errada não quebra nada, apenas mente sobre a marca do cliente.
RF-ADM-006. Ficha vazia bloqueia a publicação ou usa default declarado da agência com aviso explícito.
Como verificar (teste primeiro): tentar publicar para um cliente sem ficha preenchida mostra o bloqueio ou o aviso "usando identidade padrão da agência"; nenhum caso publica com identidade inferida em silêncio.
Aceite (H1): Dado um cliente sem ficha de identidade preenchida, Quando o operador tenta publicar, Então o sistema bloqueia e pede o preenchimento, ou aplica o default declarado da agência exibindo o aviso "usando identidade padrão da agência", E em nenhum caso o sistema inventa cor ou tom sem aviso.
FECHADO em 2026-08-02, com uma mudança de comportamento do produto declarada (decisão de rollout do dono). O gate de publicação passou a EXIGIR tom de voz. Como a tabela de configuração de identidade da agência é nova, nenhuma agência tinha tom declarado, e o gate travaria todas no primeiro dia. A saída não foi afrouxar o requisito: foi semear um tom NEUTRO por agência, editável na tela. Apagar o default volta a travar a publicação, e é esse fato que prova que a exigência continua viva em vez de ter virado letra morta.
A semente não é exceção ao H1, é a segunda alternativa que ele já previa: o valor semeado é um default DECLARADO da agência, e o relatório registra que a origem do tom foi o padrão da agência e não a ficha do cliente. Essa distinção de procedência é a razão de o estado de identidade existir; sem ela, "tem tom" e "herdou tom" viram a mesma coisa e o aviso perde sentido.
RF-ADM-007. Operador cria Account do Hub sem sair do chrome do Reports.
Como verificar (teste primeiro): criar um cliente novo pelo Reports grava um Account vinculado à Company da agência; o operador continua no chrome do Reports.
Aceite (H1): Dado um operador no ModularReports, Quando ele cria um cliente novo, Então o registro é gravado como Account do Hub vinculado à Company da agência, E o operador continua no chrome do Reports sem ser jogado para o chrome do Hub.
RF-ADM-008. Atalho "ativar acesso" aciona a função base do Hub; sem convite/senha próprios.
Como verificar (teste primeiro): clicar em "ativar acesso" na ficha de um Contact sem acesso aciona a ativação nativa do Hub; nenhum fluxo de convite próprio do Reports existe.
Aceite (H1): Dado um Contact sem acesso ativado, Quando o operador clica em "ativar acesso" na ficha do cliente, Então o sistema aciona a ativação de acesso do Hub para aquele Contact, E o Reports não cria fluxo de convite ou senha próprio.
RF-ADM-011, RNF-LGPD-002. Encerrar cliente revoga todos os links, comentários e analytics; retenção admin-editável; processo de eliminação explícito.
Como verificar (teste primeiro): encerrar um cliente com links publicados faz todos responderem como revogados ({F9-08}); o processo de eliminação apaga snapshots e anonimiza a origem (comentários, watermark, Contact) conforme a retenção, e as referências do log deixam de resolver sem o log append-only ser mutado (RN-007).
Aceite (H1): Dado um cliente com links publicados, Quando o admin encerra esse cliente, Então todos os links do cliente passam a responder como revogados, E o processo de eliminação apaga os snapshots e anonimiza os comentários na origem conforme a retenção, E a pseudonimização na origem faz as referências do log de auditoria daquele cliente deixarem de resolver sem mutar o log append-only (RN-007).
RF-ADM-013. Ficha do cliente mostra relatórios organizados por ano, sem KPI de mídia contínuo.
Como verificar (teste primeiro): abrir a ficha de um cliente com relatórios em anos diferentes mostra o agrupamento por ano; nenhum dashboard de métrica contínua aparece na ficha.
Aceite (H1): Dado um cliente com relatórios publicados em anos diferentes, Quando o operador abre a ficha do cliente, Então os relatórios aparecem agrupados por ano, E a ficha não exibe nenhum KPI de mídia contínuo.
FECHADO em 2026-08-02 (ver a nota de procedência da prova no topo da fase). Provado com dois relatórios reais, um publicado e um revogado, com selos distintos. E, o que é mais difícil e foi coberto: ficha VAZIA é visualmente distinta de ficha com ERRO de carregamento (a com erro traz aviso e ação de tentar de novo, e nunca se confunde com "nenhum relatório"). É a RN-015 aplicada à superfície de listagem, e é justamente a confusão que fez o defeito de rota sombreada (RNF-MANUT-004) passar despercebido duas vezes: erro tratado como lista vazia.
RF-REN-011. Portal de leitura para o Contact autenticado pela sessão do Hub (tela cliente-painel do mockup): lista os relatórios publicados dos Accounts a que ele pertence, com atalho para abrir cada um e ver comentários recentes. Só leitura, reusando o login base do Hub; sem composição, publicação ou administração.
Como verificar (teste primeiro): um Contact autenticado abre o portal e vê só os relatórios dos seus Accounts; um Contact de outro cliente não vê esses relatórios; nenhuma ação de compor/publicar/administrar aparece para o Contact.
Aceite (H1): Dado um Contact autenticado pela sessão do Hub, Quando ele abre o portal do cliente, Então vê a lista dos relatórios publicados para os seus Accounts com atalho para abrir cada um, E não vê relatório de cliente a que não pertence, E nenhuma ação de composição, publicação ou administração é oferecida.
FECHADO em 2026-08-02, com os negativos de isolamento exercitados em integração real (22 asserções). Os dois que importam: um contato NÃO vê relatório de outra conta da MESMA agência (isolamento dentro do tenant, que é o caso mais fácil de errar porque a agência é a mesma), e a leitura direta por identificador sob a regra de acesso de outra agência devolve nada. Provado também no mesmo request: o portal mostra o relatório publicado com link e esconde o revogado com a mensagem certa, ou seja, os dois estados coexistem e são tratados na mesma resposta, sem depender de recarregar a página.
Definição de Pronto F16: painel do cliente completo (identidade, criação, ativação de acesso, offboarding LGPD, pasta por ano) e o portal logado de leitura do cliente final. Estado em 2026-08-02: FASE FECHADA, com as 16 fatias do bloco 9 do DRS entregues. A prova desta fase é de segunda mão do ponto de vista da aprovação (ver a nota de procedência no topo). O que segue depois daqui não é o plano original: são as Fases F17 e F18, nascidas da auditoria dos relatórios reais e do council de 2026-08-02, mais os cartões abertos por motivo nomeado nas fatias anteriores.
Fase F17, Blocos reais, cadência e biblioteca Global (Fatia 17)
Prioridade: esta fase nasceu em 2026-08-02 da auditoria dos relatórios que a operação já entregou, e vem na fila antes das fases F13 a F16, embora a numeração a coloque depois delas. O motivo é o critério de adoção (DRS A.7): enquanto os quatro tipos de bloco abaixo faltarem no registro, o sistema não reproduz o relatório que a operação entrega hoje, e um sistema que não reproduz o próprio trabalho da casa não é adotado. A posição no documento é de numeração, não de fila. Contagem que sustenta a fase inteira (medida em 2026-08-02, critério comportamental). Universo: as 8 linhas da tabela de relatórios do acervo. Critério: o valor detypeefetivamente presente na lista de seções de cada relatório publicado, lido do conteúdo do próprio relatório, nunca busca textual em código. Placar: 6 relatórios estão no formato v2 com lista de seções e 2 estão em formato anterior sem lista de seções (um fechamento periódico multi-vertical e uma auditoria de marca, que não é relatório de mídia), e 6 mais 2 fecham em 8. Dos 6 v2: 4 diagnósticos de herança mais 2 fechamentos de campanha, e 4 mais 2 fecham em 6. O universo cobre 5 clientes distintos de uma mesma operação, não é um cliente só, e também não é "todos os relatórios já feitos": é o acervo dessa base. O lado negativo foi lido por inteiro, não amostrado: os 2 fechamentos de campanha têm a sequência completafunnel_simple,stat_cards,video_retention,rankings,data_table,insights,gap, e nenhum dos dois contém bloco de abertura em prosa, régua de confiabilidade,line_chart,donutouaction_plan. Dependência: {F2} (contrato de bloco v3) e {F3} (package renderizador) para os cartões de bloco; {F1b-01} e {F10-00} para a coluna de cadência e para os modelos semeados.
RF-REN-012, RF-REN-002, RF-REN-013; Ap.3 (ficha de props do section_intro); artefato {ART-BLK-01}. Contagem: 4 de 6 relatórios v2 abrem com prosa de contexto, e são os 4 diagnósticos de herança, cada um com dois blocos de abertura em sequência; os 2 fechamentos de campanha, lidos por inteiro, não usam nenhum. O registro do renderizador tem 11 tipos e este não está entre eles. O tipo único do sistema irmão foi DECOMPOSTO em dois neste contrato: o divisor puro (este cartão) e a régua derivada ({F17-09}), porque um é texto do redator e o outro é fato calculado, e juntá-los num só tipo obrigaria o campo derivado a ser editável.
O que implementar: componente section_intro mais entrada no registro declarativo, com os campos do Ap.3 (title, eyebrow, body_md sanitizado com allowlist, cap de 3 parágrafos). É repetível e NÃO tem period nem source, que pertencem ao hero singular (RF-REN-013). O campo body_html do sistema irmão NÃO entra: HTML cru anula o saneamento em todas as superfícies de uma vez.
Como verificar (teste primeiro): um documento com três blocos section_intro renderiza os três pelo componente próprio, nenhum cai no caminho de tipo desconhecido; um body_md com marcação fora da allowlist é sanitizado; enviar body_html pela API é recusado pelo schema; enviar period ou source neste bloco é recusado.
Aceite (H1): Dado um documento com três blocos section_intro, Quando o renderizador o processa, Então os três resolvem para o componente próprio e nenhum cai no caminho de tipo desconhecido, E os três são aceitos sem restrição de quantidade, E um payload que traga body_html, period ou source neste bloco é recusado pelo schema.
RF-REN-012, RF-REN-002; Ap.3 (ficha de props do line_chart); artefato {ART-BLK-01}. Contagem: 4 de 6 relatórios v2 usam line_chart, os mesmos 4 diagnósticos de herança (três deles com dois line_chart, um com um); os 2 fechamentos de campanha, lidos por inteiro, não usam nenhum. Ausente do registro de 11 tipos.
O que implementar: componente line_chart mais entrada no registro, com os campos do Ap.3 (x_labels[], series[].label, series[].points[] com dp por ponto, series[].axis no enum fechado left ou right). Séries de grandezas diferentes não dividem o mesmo eixo.
Como verificar (teste primeiro): um documento com duas séries de grandezas diferentes e axis distintos renderiza os dois eixos; as duas séries com o mesmo axis e grandezas diferentes é erro de composição sinalizado; todo ponto numérico resolve por dp e nenhum aparece como dígito cru.
Aceite (H1): Dado um bloco line_chart com duas séries de grandezas diferentes, Quando o renderizador o processa, Então ele resolve para o componente line_chart, E cada série é desenhada no eixo declarado em axis, E duas séries de grandezas diferentes declaradas no mesmo eixo são sinalizadas como erro de composição, E nenhum ponto da série é gravado como dígito fora de um dp.
RF-REN-012, RF-REN-002; Ap.3 (ficha de props do donut); artefato {ART-BLK-01}. Contagem: 4 de 6 relatórios v2 usam donut, os mesmos 4 diagnósticos de herança (três deles com dois donut, um com um); os 2 fechamentos de campanha, lidos por inteiro, não usam nenhum. Ausente do registro de 11 tipos.
O que implementar: componente donut mais entrada no registro, com os campos do Ap.3 (slices[].label, slices[].value com dp, center_label, center_value com dp).
Como verificar (teste primeiro): um documento com um bloco donut de quatro fatias renderiza pelo componente próprio; cada valor de fatia resolve por dp; o valor central resolve por dp e o rótulo central é editável.
Aceite (H1): Dado um bloco donut com quatro fatias e valor central, Quando o renderizador o processa, Então ele resolve para o componente donut e não para o caminho de tipo desconhecido, E o valor de cada fatia e o valor central vêm de um dp com proveniência, E nenhum deles é editável à mão no compositor.
RF-REN-012, RF-REN-002; Ap.3 (ficha de props do action_plan); artefato {ART-BLK-01}. Contagem: 4 de 6 relatórios v2 usam action_plan, os mesmos 4 diagnósticos de herança, e nos 4 ele é o penúltimo bloco, imediatamente antes do gap; os 2 fechamentos de campanha, lidos por inteiro, não usam nenhum. Ausente do registro de 11 tipos.
O que implementar: componente action_plan mais entrada no registro, com os campos do Ap.3 (items[].title, items[].body com cap de 280 caracteres, items[].horizon no enum fechado imediato, curto ou medio, items[].owner).
Como verificar (teste primeiro): um documento com um action_plan de três itens de horizontes diferentes renderiza pelo componente próprio; um body acima de 280 caracteres é rejeitado ou truncado pelo schema; um horizon fora do enum é recusado.
Aceite (H1): Dado um bloco action_plan com três itens, Quando o renderizador o processa, Então ele resolve para o componente action_plan, E um item com body acima do cap de 280 caracteres não é publicado, E um item com horizon fora do enum fechado é recusado.
RF-CMP-001, RN-014, ADR-016; artefatos {ART-ENT-01} (coluna cadence) e {ART-CFG-01} (base sazonal opcional da cadência mensal). A cadência é atributo da instância, nunca tipo de modelo: fragmentar type por cadência partiria o acervo do cliente que mudasse de cadência e devolveria ao estado "sem base" o anti-fadiga (RF-CMP-016) e a base do selo (RN-012 item 2), que usam a mesma chave "cliente mais tipo". A cadência é declarada, não inferida da largura do período, porque é a divergência entre as duas que revela o período parcial ({F17-06}).
O que implementar: coluna cadence em reports.report com valor fechado de seis (quinzenal, mensal, trimestral, semestral, anual, pontual) imposto por restrição no schema; campo obrigatório no passo 1 do wizard; mapa cadência para base de comparação da tabela de RN-014, consumido pela fonte (ii) de RN-012 item 2; parâmetro de agência para a base sazonal opcional da cadência mensal.
Como verificar (teste primeiro): gravar uma cadência fora dos seis valores é recusado pelo banco; o wizard não avança sem cadência; um relatório trimestral resolve como base o trimestre anterior e o mesmo trimestre do ano anterior, e um quinzenal resolve a quinzena anterior; nenhum ponto do código deduz a cadência da largura de period.
Aceite (H1): Dado um relatório com cadência declarada trimestral, Quando a composição resolve a base de comparação, Então a base sai do mapa de RN-014 e não da largura do período, E gravar um valor fora dos seis é recusado pelo schema, E o wizard recusa avançar sem cadência.
RF-CMP-021, RN-014 item 5, RN-012 item 2; artefato {ART-CFG-01} (tolerância e desfecho). Cartão que fecha um defeito com caso real registrado: um trimestre com apenas um mês de dados foi comparado com o trimestre cheio do ano anterior e o texto afirmou queda de 71 por cento, número que a diferença de largura das janelas produziu sozinha. Hoje isso depende de disciplina de quem redige; este cartão move a guarda para o código.
O que implementar: cálculo da largura em dias das duas janelas em toda comparação emitida (selo da âncora, bloco comparison, dp derivado de variação entre períodos, base escolhida pela cadência); tolerância como parâmetro de agência com seed default de 10 por cento; desfecho configurável entre recusar a variação e emiti-la com a marca explícita de janela parcial declarando as duas larguras reais; a marca gravada no dp derivado e copiada para o snapshot, fora do alcance da narrativa e da curadoria.
Como verificar (teste primeiro): um relatório de janela de 31 dias comparado a uma base de 92 dias não produz variação limpa em nenhum dos dois modos; no modo de marca, o número publicado carrega as duas larguras em dias; a marca presente no rascunho continua presente no snapshot; a curadoria não consegue removê-la; duas janelas dentro da tolerância produzem variação normal, sem marca.
Aceite (H1): Dado um relatório cuja janela tem 31 dias e cuja base de comparação tem 92 dias, Quando a composição emite a variação, Então ou ela não é emitida, ou ela sai com a marca de janela parcial declarando 31 e 92 dias, E nenhuma variação sem marca é gravada no dp derivado, E a marca aparece no snapshot publicado, E uma comparação entre janelas dentro da tolerância continua saindo sem marca.
RF-CMP-022; artefato {ART-CFG-01} (conjunto de métricas não somáveis). Alcance somado entre janelas conta a mesma pessoa duas vezes e infla o número sem que nada no sistema reclame.
O que implementar: conjunto nominal de métricas não somáveis como parâmetro de agência, com seed default de três (alcance, usuários únicos, visitantes únicos); no agregado de mais de uma janela, o valor de uma métrica desse conjunto é o pico das janelas, e a formula do dp derivado declara que é pico e sobre quantas janelas; métrica fora do conjunto continua somando.
Como verificar (teste primeiro): um relatório que agrega três janelas mensais com alcance em cada uma grava o maior dos três e não a soma; a formula do dp distingue pico de soma; uma métrica fora do conjunto agregada no mesmo relatório continua somando; adicionar uma métrica ao conjunto pelo painel muda o comportamento sem deploy.
Aceite (H1): Dado um relatório que agrega três janelas mensais com alcance em cada uma, Quando o valor agregado é calculado, Então o dp gravado é o maior dos três e não a soma, E a formula declara que o valor é o pico e sobre quantas janelas, E uma métrica fora do conjunto agregada no mesmo relatório continua sendo somada.
RF-ADM-017, RN-013 item 4, DRS Ap.9; artefatos {ART-TPL-01}, {ART-TPL-02}, {ART-TPL-03}. A biblioteca Global nasce hoje vazia. Contagem que decide o escopo de cada um: Diagnóstico de Herança tem N igual a 4 e Fechamento de Campanha tem N igual a 2, os dois confirmados por relatórios reais no formato v2, e vão para Global. Fechamento Periódico Multi-Vertical tem N igual a 1, e essa única amostra está em formato anterior ao v3, sem lista de seções tipadas: ele nasce individual da agência de origem, porque semear no Global uma sequência inferida de uma amostra só apresenta uma hipótese a todas as agências como se fosse o jeito certo de fazer. Promover é gesto do Super Admin sob demanda, quando aparecer um segundo caso real. Depende de {F17-01} a {F17-04} e de {F17-09}, porque os modelos usam tipos de bloco que ainda não existem no registro.
O que implementar: duas linhas semeadas em reports.template com company_id nulo e is_global verdadeiro (pesquisa e campanha), e uma terceira linha com is_global falso e company_id da agência de origem (fechamento), todas com default_blocks no envelope de duas chaves e a sequência de blocos do Ap.9; a linha individual carrega, na descrição exibida ao operador, a marca de hipótese com a amostra que a sustenta.
Como verificar (teste primeiro): as duas linhas Globais aparecem na biblioteca de toda agência com o módulo ligado e não oferecem despromoção (Global sem agência de origem, RN-013 item 4); a terceira NÃO aparece para nenhuma agência além da de origem; iniciar um relatório com o modelo Diagnóstico de Herança gera o rascunho com a sequência do Ap.9 na ordem; a descrição do terceiro modelo diz que é hipótese e cita a amostra; o Super Admin consegue promover o terceiro a Global e a promoção preserva a agência de origem.
Aceite (H1): Dados os dois modelos Globais semeados e o terceiro individual, Quando um operador de uma agência que não é a de origem abre a biblioteca, Então ele vê os dois Globais sem opção de despromoção e NÃO vê o terceiro, E iniciar um relatório com o modelo Diagnóstico de Herança copia para o rascunho exatamente a sequência de blocos do Ap.9 na ordem, E a descrição do terceiro modelo apresentada ao operador da agência de origem declara que ele é hipótese sustentada por uma amostra, E o Super Admin consegue promovê-lo a Global preservando a agência de origem.
RF-CMP-023, RF-REN-012, RF-REN-002; Ap.3 (ficha de props do reliability); artefato {ART-BLK-01}. Contagem: 4 de 6 relatórios v2 trazem a régua de confiabilidade, os 4 diagnósticos de herança, e nos 4 ela foi DIGITADA À MÃO pelo redator; os 2 fechamentos de campanha, lidos por inteiro, não a trazem. É o achado mais forte da auditoria: o sistema já calcula os três fatos que essa régua afirma, e hoje um humano os redigita, que é o padrão "correto enquanto alguém lembrar". Este cartão o transforma em garantia de máquina.
O que implementar: componente reliability mais entrada no registro; montagem por código a partir de três fontes que já existem no pipeline (os artefatos que parsearam com sucesso, RF-ING-003; as lacunas do bloco gap, RF-CMP-003; a base da âncora ou o estado "sem base", RN-012 item 2 e RN-015); corpo gerado por template determinístico, ZERO chamada de IA; os três campos derivados não editáveis em nenhuma superfície; campo note opcional do operador ao lado, nunca por cima.
Como verificar (teste primeiro): compor um relatório com dois artefatos, uma lacuna e a âncora sem base, e conferir que o bloco nasce sem nenhuma chamada ao gateway de IA (contador de chamadas em zero para este bloco); tentar editar cada um dos três campos derivados pelo compositor, pela API e pela curadoria, e confirmar recusa nos três caminhos; tentar excluir o bloco do documento e confirmar recusa; acrescentar a note e conferir que nenhum dos três campos muda; conferir que o que a régua afirma bate com o bloco gap e com o selo da âncora da MESMA página.
Aceite (H1): Dado um relatório composto a partir de dois artefatos, com um bloco esperado virado lacuna e com a âncora no estado "sem base", Quando a página é montada, Então o bloco reliability existe sem nenhuma chamada de IA, E ele nomeia as duas fontes, a lacuna com o motivo e o estado "sem base" da âncora, E nenhuma superfície permite editar ou remover esses três campos, E acrescentar a note do operador não altera nenhum dos três.
RF-REN-013; Ap.3 (nota de singularidade do hero); artefato {ART-BLK-01}. O hero carrega period e source, que declaram o que o documento inteiro cobre. Dois hero significariam dois períodos e o documento se contradizendo sobre a própria janela, que é a mesma família de defeito de rótulo que {F9-13} e {F18-03} fecham. Quem quiser dividir o documento usa section_intro ({F17-01}), repetível de propósito.
O que implementar: restrição de cardinalidade no schema do documento (no máximo um bloco hero); o compositor não emite o segundo; a curadoria não oferece duplicar um hero; o gate de publicação recusa documento com dois.
Como verificar (teste primeiro): enviar pela API um documento com dois hero e confirmar recusa pelo schema; pedir à composição um segundo hero e confirmar que ela não o emite; tentar duplicar o hero no compositor e confirmar que a ação não existe; forçar dois hero no rascunho por caminho de banco e confirmar que o publish reprova; conferir que dois ou mais section_intro continuam passando sem restrição.
Aceite (H1): Dado um documento que já tem um bloco hero, Quando a composição, a curadoria ou a API tentam acrescentar um segundo, Então a operação é recusada nas três superfícies, E o gate de publicação recusa um documento que contenha dois blocos hero, E acrescentar um segundo section_intro continua sendo permitido sem restrição de quantidade.
FECHADO em 2026-08-03, e é este cartão que impede a F17 de ser código morto. As quatro peças centrais da fase (a derivação da régua de confiabilidade, a imposição de abertura singular, a guarda de largura de janela e a métrica não somável entre janelas) estavam construídas e testadas por unidade, e mesmo assim nenhuma delas era chamada no caminho real. Este cartão as ligou na composição e na publicação de verdade.
O que foi feito: a régua de confiabilidade passa a nascer sozinha em TODA composição, e é RE-DERIVADA no publish, quando a métrica âncora já tem o selo calculado (é o único momento em que a régua consegue afirmar a base de comparação com o número final na mão). As outras três entraram no mesmo caminho.
Como foi verificado, e o método é o ponto: cada ligação nasceu com um teste que prova que falha SEM a ligação. O procedimento foi executado e não presumido: comentar a chamada, rodar a suíte, ver o teste ficar vermelho, restaurar. Um teste que passa com e sem a linha que ele deveria proteger não é teste, é decoração, e essa é exatamente a classe de defeito que esta fase produziu.
Aceite (H1): Dada uma composição qualquer, Quando ela roda pelo caminho real, Então o bloco de confiabilidade existe no rascunho sem ninguém o ter pedido, E ele é re-derivado no publish já com o selo da âncora, E comentar a chamada de qualquer uma das quatro peças faz ao menos um teste reprovar.
FECHADO em 2026-08-03. É o achado mais estrutural do dia, e a causa por trás do {F17-11}. As quatro peças não estavam desligadas por esquecimento de chamada: elas nunca foram expostas no índice do pacote, então eram um valor indefinido para qualquer consumidor externo. Todo chamador de fora enxerga o que o índice decide, nunca o módulo de origem, e por isso nenhum teste de unidade do pacote pegava o defeito: os testes importam o módulo fonte direto, que é o único caminho em que a função existe.
Contagem da varredura que se seguiu (medida em 2026-08-03; critério: comparar os nomes realmente exportados por cada módulo do pacote contra os nomes presentes no índice, ambos obtidos por leitura do próprio código em tempo de execução, nunca por busca textual nem por lista escrita à mão). Universo: 55 módulos e 279 exports. Placar: 63 nomes fora do índice, dos quais 62 internos de propósito e 1 defeito vivo, e 62 mais 1 fecham em 63. Depois da correção do único defeito, a medição repetida dá 62 fora do índice, todos classificados, e 62 mais 1 corrigido fecha nos 63 originais.
O único defeito vivo era o pior possível pela ironia: o aviso de falha da composição, que existe justamente para tornar a falha da inteligência artificial compreensível ao operador. Sem ele, o operador recebia um erro técnico opaco em vez da mensagem que separa a narrativa que falhou dos números que continuam íntegros.
A causa foi fechada, não só o sintoma (RNF-MANUT-006): nasceu um teste de contrato que varre os módulos do pacote, obtém os exports reais em tempo de execução, remove os classificados como internos e afirma que todo nome restante está no índice. A classificação do que é interno foi feita ANTES, export a export, com o consumidor real conferido um a um, para o teste não nascer quebrado nem ser afrouxado depois para passar. Provado por controle negativo direto sobre o defeito desta sessão.
Aceite (H1): Dado um nome exportado por um módulo do pacote e destinado a consumidor externo, Quando o teste de contrato roda, Então esse nome está no índice público, E removê-lo do índice faz o teste reprovar nomeando o módulo e o nome, E um nome classificado como interno não reprova, E a classificação declara o critério e o consumidor real de cada nome.
Definição de Pronto F17: ATENDIDA, fase FECHADA em 2026-08-03. Os dezesseis tipos do Ap.3 no registro, com os cinco novos provados por documento que os percorre; a régua de confiabilidade nascendo derivada e não editável; o hero singular imposto nas três superfícies; cadência declarada na instância com valor fechado imposto pelo schema e mapa de base de comparação valendo; a guarda de largura de janela recusando ou marcando toda comparação entre janelas diferentes, com a marca sobrevivendo ao publish; alcance e as demais métricas não somáveis usando o pico entre janelas; a biblioteca semeada com os dois modelos Globais confirmados mais o terceiro como individual da agência de origem; e, acrescentados durante a entrega, as quatro peças LIGADAS ao caminho real ({F17-11}) e o contrato público do pacote verificado por máquina ({F17-12}).
O smoke visual achou três defeitos que 32 testes verdes não pegaram, e é esta a evidência de por que a regra de olhar existe. Todos os três eram invisíveis para asserção e óbvios para o olho: no gráfico de linha, uma escala única esmagava a série menor até a linha reta; no donut, o texto do centro ficava sobreposto; e o bloco de confiabilidade repetia o mesmo fato três vezes. Nenhum quebrava build, nenhum derrubava teste, e os três teriam chegado ao cliente. A suíte prova que o dado está certo, não que a página está legível, e essas são duas perguntas diferentes.
Fase F18, Honestidade do número e calibração da régua
Onda nascida do council de 2026-08-02 sobre a auditoria dos relatórios reais. Ela não acrescenta funcionalidade nova de produto: fecha classes de mentira que o sistema ainda deixa passar, e consertou a régua de qualidade que travava o cartão {F1-17}, hoje fechado. Depende de {F17} (os tipos de bloco e a cadência precisam existir antes de as guardas os cobrirem). Princípio de dono desta fase (ADR-019): quando uma regra de comparação nasce, pergunte se o NÚMERO é falso, ou se o número é verdadeiro e a FRASE é falsa. Número falso, o dono é o extrator, que se recusa a produzi-lo. Número verdadeiro e frase falsa, o dono é o selo mais o compositor, proibido de emitir a afirmação quando o selo diz não comparável. Regra de nascença de toda guarda desta fase (RNF-MANUT-003): cada cartão entrega, junto com a trava, a fixture de controle negativo que ela DEVE reprovar, na suíte da integração contínua. Cartão sem fixture não fecha.
As seis guardas de {F18-01} a {F18-04}, {F18-07} e {F18-08} foram LIGADAS no publish real em 2026-08-03, e é isso que as tira de decorativas. Estavam escritas e testadas por unidade, e o publish não as chamava.
O método da prova, e é ele que deve se repetir. Cada guarda foi verificada INDIVIDUALMENTE: comentar a própria linha de ligação, rodar, e conferir que só o teste dela caía. Uma a uma, nas seis. Não é controle negativo genérico que derruba tudo e prova pouco (esse passa mesmo quando cinco das seis estão mortas); é a prova de que cada ligação específica está viva no caminho real. E as sete suítes irmãs que também publicam seguiram verdes, ou seja, nenhum relatório legítimo passou a reprovar, que é a outra metade da prova: uma guarda que reprova tudo também "funciona" e é inútil.
Achado extra, e é a lição mais fina desta fase: a saída legal que o {F18-07} documenta era ela própria sempre reprovada pelo gate de proveniência estrutural. A escotilha existia no texto e era inalcançável no código. É exatamente o defeito contra o qual a regra "toda recusa entrega uma saída legal" ({F18-10}) foi escrita, encontrado dentro da nossa própria implementação dela. Uma saída legal que o sistema recusa não é saída, é a mesma parede com outro nome. Corrigido isentando o histórico mínimo do gate estrutural.
RF-CMP-024, RN-016; artefato {ART-BLK-01}. Percentual pelado ("mais 18 por cento") é o veículo desta classe de mentira. A forma exigida é "mais 18 por cento ante dezembro de 2025 (mesmo mês do ano anterior)": a base vai DENTRO da frase, nunca só em rodapé nem só em metadado.
O que implementar: campo base obrigatório, valor fechado de dois (meta, periodo_anterior), derivado da origem do dado e não escolhido pelo operador, no bloco comparison; a base colada ao número no componente não editável do selo da âncora e em toda variação derived de dois períodos; a base copiada para o snapshot.
Como verificar (teste primeiro): um bloco cuja origem é a meta e outro cuja origem é o período anterior exibem bases diferentes por extenso, no cabeçalho e em cada linha; varrer a página publicada e confirmar que nenhum número comparativo aparece sem base na própria frase; tentar trocar a base pela curadoria e confirmar recusa; publicar e confirmar que a base sobreviveu no snapshot. Fixture de controle negativo: uma página com um percentual comparativo sem base, que a varredura DEVE reprovar.
Aceite (H1): Dado um bloco comparison cuja origem é a meta importada e outro cuja origem é o período anterior, Quando a página é renderizada, Então cada um exibe a sua base por extenso no cabeçalho e em cada linha, E nenhum número comparativo da página aparece sem a base na própria frase, E a base não é editável pelo operador, E ela sobrevive no snapshot publicado.
RF-CMP-024, ADR-018; Ap.3 (ficha do comparison). Contagem: 0 de 6 relatórios v2 usam comparison, e nenhum contorno foi observado no acervo (diferente do media, cuja falta apareceu como tabela de texto no lugar do criativo). Ele fica porque planejado versus realizado é decisão de produto, mas fica DEPOIS dos tipos com uso comprovado e com data de liquidação da aposta.
O que implementar: base obrigatório no schema do bloco; min_periods com default 2 quando a base é periodo_anterior; a condição de morte registrada no Ap.3 e no acompanhamento de adoção (se em 2 ciclos reais depois da Fatia 11 nenhum relatório usar comparison, o tipo é cortado do registro e do contrato).
Como verificar (teste primeiro): um comparison sem base é recusado pelo schema; um comparison com base de período anterior e só 1 período disponível se oculta por {F18-07}; o acompanhamento de adoção registra o uso do tipo por relatório real, de modo que a condição de morte seja verificável por contagem e não por impressão.
Aceite (H1): Dado um bloco comparison enviado sem o campo base, Quando o schema o valida, Então ele é recusado, E o acompanhamento de adoção registra, por relatório real publicado, se o tipo foi usado, E a contagem acumulada permite decidir a condição de morte por fato e não por opinião.
RF-CMP-025; evolui a guarda de {F9-13}. A guarda atual fechou só um espaço da frase, o nome da métrica. Passa hoje: "o criativo em vídeo alcançou 167.364 impressões em julho", onde o número é o total da CAMPANHA e o criativo respondeu por 41 por cento dele. A métrica citada está certa e a frase é falsa mesmo assim.
O que implementar: cinco campos nominais obrigatórios em todo ponto de dado extraído (métrica, entidade, período, segmento, agregação), preenchidos pelo extrator e gravados no snapshot; o gate de coerência compara os CINCO com o que a frase afirma, nomeando o campo divergente na mensagem de reprovação.
Como verificar (teste primeiro): uma frase que atribui a um criativo um número cuja tupla diz entidade igual a campanha é reprovada com o campo divergente nomeado; a mesma frase corrigida passa; repetir a prova para período, segmento e agregação, um por vez; conferir que nenhum ponto de dado chega ao snapshot com qualquer um dos cinco campos vazio. Fixture de controle negativo: exatamente a frase do criativo com o número da campanha, que a suíte DEVE reprovar.
Aceite (H1): Dado um ponto de dado cuja tupla declara entidade igual a campanha e uma frase que atribui esse número a um criativo, Quando o gate de coerência roda, Então a frase é reprovada com o campo divergente nomeado, E a mesma frase com a entidade correta passa, E a divergência é detectada também em período, segmento e agregação, E nenhum número exibido chega ao snapshot com campo vazio na tupla.
RF-CMP-026. "O aumento veio da campanha do Meta" afirma causa; os números mostram coocorrência. A degradação preserva a informação verdadeira em vez de apagar a frase.
O que implementar: conjunto NOMINAL de conectivos causais (veio de, foi causado por, resultou em, gerou, por causa de, graças a, devido a, o que levou a, o que explica); exigência de evidência designada no artefato (recorte que isole a variável, teste declarado na fonte, ou atribuição emitida pela própria plataforma de origem); reescrita automática para a forma de coocorrência quando a evidência não existe, antes de o texto chegar ao operador.
Como verificar (teste primeiro): uma frase causal sem evidência designada chega ao operador já na forma de coocorrência; a mesma frase com evidência designada é mantida causal e referencia a evidência; nenhum conectivo do conjunto chega ao snapshot sem evidência; conferir que a frase não é apagada, apenas degradada. Fixture de controle negativo: a frase causal sem evidência, que a suíte DEVE flagrar.
Aceite (H1): Dado uma frase que usa um conectivo causal do conjunto nominado e um artefato sem evidência designada, Quando a composição termina, Então a frase chega ao operador na forma de coocorrência, E a mesma frase com evidência designada é mantida causal com a evidência referenciada, E nenhum conectivo do conjunto chega ao snapshot sem evidência designada.
FECHADO em 2026-08-03, por medição e não por relato. Este cartão não estava na lista de fechamento recebida, e a contagem o revelou: fechar os seis pedidos deixaria quatro abertos, e a lista nomeava três. A conferência do estado real encontrou a suíte do quadrante verde (zero falhas), o módulo do anti-fadiga presente com teste próprio, e a Fase F19 já fechada tendo este cartão como pré-requisito duro declarado (RN-017 item 9), o que seria contraditório com ele aberto. Três evidências convergentes, e nenhuma delas é a mensagem de commit sozinha.
RF-CMP-016 (requisito reescrito). A guarda atual compara texto com texto, e uma trava que pune repetição empurra a IA a inventar variação: o modelo aprende que reescrever a mesma leitura com palavras novas passa, o que produz novidade fabricada sobre dado parado.
O que implementar: medição da variação do dado sobre os pontos de dado da âncora e das métricas destacadas na direção, com limiar de materialidade como parâmetro de agência; classificação nos quatro quadrantes (dado igual e leitura igual é LEGÍTIMO e a página declara "sem mudança material desde a edição anterior"; dado mudou e leitura igual REPROVA; dado igual e leitura nova SINALIZA; dado mudou e leitura nova passa); o acervo de narrativa anterior continua SEM filtro de cadência, ao contrário da base do selo, e essa diferença é deliberada.
Como verificar (teste primeiro): montar os quatro casos e conferir os quatro desfechos, um a um; conferir que o caso "dado igual e leitura igual" publica com a declaração de estabilidade e não é tratado como defeito; conferir que o acervo consultado não filtra por cadência. Fixture de controle negativo: a composição com dado mudado e leitura idêntica, que a suíte DEVE reprovar.
Aceite (H1): Dado um cliente com relatório anterior, Quando o relatório novo é composto, Então o sistema o classifica num dos quatro quadrantes, E "dado mudou e leitura igual" reprova, E "dado igual e leitura nova" é sinalizado sem reprovar, E "dado igual e leitura igual" passa e a página declara que não houve mudança material desde a edição anterior.
**RECONCILIAÇÃO em 2026-10-16 (plano do {F20-13}): o "fechado" vale para o modelo e as funções, NÃO para
o produto rodando.** Nenhuma rota do Hub chamava as funções que gravam a recomendação nem o casamento
entre edições. Ou seja, nenhuma publicação real gerava identidade nem ocorrência, e a escalada nunca
aconteceu fora de teste. Quem liga isso é o {F20-13}. Mostrar a escalada na página ("terceira edição
seguida") continua sem chamador, e foi para o {F20-14}.
FECHADO em 2026-08-03, com a lacuna de modelo de dados preenchida. A recomendação ganhou identidade persistente entre edições, com critério declarado: dono e horizonte funcionam como portas, e a similaridade de texto só decide DENTRO delas. O que esse critério deixa de fora está documentado junto, que é o que o torna auditável em vez de mágico. Isolamento entre agências provado por cinco controles negativos, incluindo a recusa entre contas da MESMA agência, que é o caso mais fácil de errar porque o tenant é o mesmo.
LIMITE DECLARADO EM CÓDIGO, e ele não é detalhe: o limiar de similaridade está calibrado por sonda sintética, não por dado real. Ele funciona contra o que foi construído para testá-lo, e ninguém ainda o viu decidir sobre um plano de ação de verdade. Calibrar exige conjunto real com esse bloco, que hoje não existe ({F20-10}).
Motivo de abertura original, preservado como histórico. Não era trabalho de ligação, era LACUNA DE MODELO DE DADOS. A distinção importa e por isso está escrita: nos outros seis cartões desta rodada a peça existia e ninguém a chamava; aqui não há onde guardar. Escalar uma recomendação repetida (dizer "terceira quinzena consecutiva com a mesma recomendação pendente") exige persistir a IDENTIDADE da recomendação entre edições diferentes do mesmo cliente, e hoje não existe coluna nem tabela que faça isso: cada edição nasce com as suas recomendações e nada as liga às da edição anterior.
Solução provável, apontada e NÃO decidida: dar identidade estável à recomendação, que é o mesmo princípio do identificador estável de bloco que o contrato v3 já usa (Ap.2, id nunca regenerado, âncora de comentário e de analytics). Se a recomendação ganhar um identificador que sobreviva entre edições, a contagem de repetições vira consulta e o escalonamento vira regra. A forma exata (identificador por texto normalizado, por métrica de origem, ou por chave declarada no bloco) é decisão de quem implementar, e entra neste roadmap antes de virar código.
RF-CMP-016. Quando a mesma recomendação aparece em edições consecutivas sem ter sido executada, a versão antiga da guarda mandava reescrevê-la, o que destruía o achado. "Terceira quinzena consecutiva com a mesma recomendação pendente" costuma ser a informação mais valiosa daquele relatório.
O que implementar: identidade estável da recomendação entre edições do mesmo cliente; contagem de edições consecutivas em que ela seguiu pendente; promoção do item na página com a contagem visível; a recomendação repetida deixa de contar como fadiga de texto.
Como verificar (teste primeiro): três edições consecutivas com a mesma recomendação não executada produzem, na terceira, o item promovido com a contagem "terceira edição consecutiva"; executar a recomendação zera a contagem; a repetição não dispara a sinalização de fadiga de {F18-05}.
Aceite (H1): Dadas três edições consecutivas de um cliente com a mesma recomendação pendente, Quando a terceira é composta, Então a recomendação aparece promovida com a contagem de edições consecutivas em que está pendente, E ela não é reescrita com outras palavras, E ela não é contabilizada como fadiga de texto.
RF-REN-014. Seção sem massa de dados não fica curta, fica falsa: um gráfico de linha com dois pontos desenha uma tendência que o dado não sustenta, e um ranking sobre 15 dias ordena ruído.
O que implementar: campo min_periods por bloco com defaults por tipo (line_chart 3, comparison com base de período anterior 2, rankings por parâmetro de agência), admin editáveis; ocultação automática abaixo do mínimo; entrada obrigatória no bloco gap com o motivo e os dois números (exigido contra disponível).
Como verificar (teste primeiro): um line_chart com min_periods 3 e 2 períodos disponíveis não aparece na página e aparece no gap com os dois números; o mesmo bloco com 3 períodos renderiza; ocultar sem deixar a lacuna reprova. Fixture de controle negativo: o gráfico de dois pontos renderizado, que a suíte DEVE reprovar.
Aceite (H1): Dado um bloco line_chart com min_periods igual a 3 e um relatório com 2 períodos disponíveis, Quando o relatório é composto, Então o bloco não aparece na página, E a lacuna aparece no bloco gap declarando o histórico exigido e o disponível, E o mesmo bloco com 3 períodos renderiza normalmente.
RF-REN-015, RN-015. Hoje três blocos expressam "não medido" de três jeitos (funnel_simple.steps[].dashed, o bloco gap, e o ponto nulo de line_chart), e três jeitos para o mesmo conceito é como o leitor aprende a ignorar um deles.
O que implementar: valor NULO como única representação no dado (nunca zero, nunca string vazia); render interrompido nas três superfícies (traço na série, etapa tracejada no funil, célula vazada na tabela) e em nenhum caso queda desenhada até zero; motivo declarado uma vez no bloco gap; a distinção de RN-015 escrita no código que decide reprovar (nulo de cobertura) ou passar (nulo de comparação).
Como verificar (teste primeiro): um relatório com etapa de funil, ponto de série e célula de tabela todos não medidos mostra os três como interrupção, nenhum como zero, com os três motivos no gap; conferir que nenhum dp gravado para eles carrega zero; conferir que o nulo de comparação ("sem base") continua publicando normalmente. Fixture de controle negativo: o não medido gravado como zero, que a suíte DEVE reprovar.
Aceite (H1): Dado um relatório em que uma etapa do funil, um ponto de série e uma célula de tabela são todos não medidos, Quando a página é renderizada, Então os três aparecem como interrupção e nenhum como zero, E os três motivos aparecem no bloco gap, E nenhum ponto de dado gravado para eles carrega o valor zero.
RF-ADM-018, RN-016; artefato {ART-FLD-01}. Escolher a base de comparação relatório a relatório é como o viés entra na entrega, e viés não aparece dentro de um relatório: aparece na comparação entre relatórios, que é justamente quando ninguém está olhando.
O que implementar: campo booleano de sazonalidade mais mês de pico opcional no cadastro do cliente, editável pelo admin da agência; leitura pelo compositor no ato da composição; para cliente sazonal, base primária igual ao mesmo período do ano anterior; base copiada para o snapshot no publish; nenhuma tela oferece a escolha da base por relatório.
Como verificar (teste primeiro): um cliente sazonal compara contra o mesmo período do ano anterior e um não sazonal contra o período imediatamente anterior; a base sai por extenso dentro da frase ({F18-01}); trocar o atributo depois não altera relatório já publicado; varrer as telas do wizard e do compositor e confirmar que a escolha de base não é oferecida.
Aceite (H1): Dado um cliente marcado como sazonal, Quando um relatório dele é composto, Então a base primária é o mesmo período do ano anterior, E a base aparece por extenso dentro da frase de cada número comparativo, E o mesmo relatório para um cliente não sazonal usa o período imediatamente anterior, E trocar o atributo depois não altera nenhum relatório já publicado.
RF-CUR-009. Sem escotilha o operador constrói o caminho sombra (monta o relatório fora do sistema e cola o resultado), e aí você perde TODAS as guardas de uma vez. Uma trava que só diz não constrói um produto insuportável.
O que implementar: cada guarda não estrutural passa a nomear, na própria mensagem, a saída legal daquele caso (média diária, recorte equivalente, pico com janela nomeada, quantos períodos faltam); override com justificativa escrita OBRIGATÓRIA, gravada no log de auditoria com autor, relatório e guarda, e marcada na página publicada; a lista NOMINAL das guardas estruturais sem override (número sem proveniência, publicar sem sessão humana) escrita no código, não julgada na hora; as guardas disparam na extração e na composição, com o gate de publicação como última rede.
Como verificar (teste primeiro): cada guarda não estrutural, uma a uma, exibe a saída legal na mensagem; o override sem justificativa é recusado; a justificativa aparece no log com os três campos; a página publicada mostra a marca de liberação humana; tentar override numa guarda estrutural é recusado; conferir que nenhuma das guardas dispara pela PRIMEIRA vez só no publish.
Aceite (H1): Dado um operador que esbarra numa guarda não estrutural, Quando a guarda dispara, Então a mensagem nomeia a saída legal disponível, E o operador consegue seguir escrevendo uma justificativa obrigatória, E a justificativa fica gravada no log com autor, relatório e guarda, E a página publicada carrega a marca de liberação humana, E a mesma tentativa contra uma guarda estrutural é recusada sem oferecer override.
FECHADO em 2026-10-16: pacote main 80d587f (merge 012da5c), mergecheck com test-all do Hub
apontando para o pacote mergeado: VERDE, pacote 1924/1924. Plan, review APPROVED e qa PASS. Descoberta
automática de guarda (assert|enforce|check|validate|evaluate em src/*.mjs): 48 achadas, 48 no
registro (37 novas), 2 fora com motivo e card em fixtures/negative-fixture-fora.txt. A mutação desliga
cada guarda e exige vermelho (as 48 ficam vermelhas), e o QA mutou 5 à mão. Achado no caminho:
assertAntiFatigueQuadrant estava verde mesmo desligada, porque a fixture testava outra função;
corrigido. As guardas novas do {F12-07} e do {F18-19} entram no registro quando forem mergeadas
(a suíte reprova até isso). O Hub ficou no {INFRA-24}.
Estado MEDIDO em 2026-08-03, e o cartão segue aberto de propósito. Existe commit declarando o registro de fixtures de controle negativo e existe artefato correspondente no repositório, mas mensagem de commit não é prova de H1, e o aceite deste cartão é forte: cada guarda tem a SUA fixture, e a ausência de qualquer uma reprova a integração contínua. Não confirmei a cobertura guarda a guarda, então não fecho. Para fechar, falta conferir que o conjunto de fixtures cobre nominalmente cada guarda existente e que remover uma delas faz a suíte reprovar.
RNF-MANUT-003. Trava sem caso que ela DEVE reprovar não é trava, é comentário: passa verde para sempre, inclusive no dia em que parar de funcionar.
O que implementar: uma fixture por guarda, cuja expectativa é a REPROVAÇÃO, na suíte da integração contínua; a suíte falha se qualquer uma dessas fixtures passar; conferência que impede o merge de guarda nova sem a sua fixture. As fixtures nominais iniciais: o trimestre de um mês contra o trimestre cheio (RF-CMP-021), alcance somado entre três janelas (RF-CMP-022), a frase do criativo com o número da campanha (RF-CMP-025), o conectivo causal sem evidência (RF-CMP-026), o gráfico de dois pontos (RF-REN-014), o não medido gravado como zero (RF-REN-015), o segundo hero (RF-REN-013), e a composição com dado mudado e leitura idêntica (RF-CMP-016).
Como verificar (teste primeiro): desligar cada guarda, uma por vez, e confirmar que a suíte FICA VERMELHA por causa da fixture correspondente (é essa a prova de que a fixture serve); acrescentar uma guarda nova sem fixture e confirmar que a conferência bloqueia o merge.
Aceite (H1): Dado qualquer guarda deste documento, Quando a suíte da integração contínua roda, Então existe ao menos uma fixture cuja expectativa é a reprovação por aquela guarda, E desligar a guarda deixa a suíte vermelha por causa dessa fixture, E uma guarda nova sem fixture de controle negativo é bloqueada no merge.
FECHADO em 2026-08-03. Entrou no conjunto de referência um relatório REAL de 25 seções, entregue a cliente pagante, anonimizado antes do parse (cliente, sub-marcas, cidades, concorrentes, domínio e identificadores de conta trocados; números e estrutura preservados, porque o que se quer medir é a escala e a estrutura, não a identidade). A conclusão da recalibração está no {F1-17} e no Ap.4: os dois documentos legados continuam reprovando por honestidade comparativa, então comprimento não explica a reprovação e a régua é exigente contra a prática antiga, não descalibrada.
Motivo de abertura original, preservado como histórico. O maior documento do conjunto de referência tinha 7 seções e cerca de 8,5 KB; os relatórios reais têm de 18 a 25 seções. A régua que aprova qualidade nunca tinha visto um exemplar do tamanho em que o produto opera.
O que implementar: acrescentar ao conjunto de referência um diagnóstico real de 20 ou mais seções, com os artefatos crus correspondentes; reajustar os limiares das 5 dimensões contra ele; registrar, por documento do conjunto, quantas seções ele tem, para que o viés de tamanho fique visível e não volte a passar despercebido.
Como verificar (teste primeiro): o conjunto passa a conter ao menos um documento de 20 ou mais seções; rodar a rubrica sobre ele e sobre um degradado do mesmo tamanho e confirmar que a régua os separa; confirmar que os documentos legados convertidos deixam de puxar o limiar para baixo por serem pequenos e antigos, e não por serem ruins.
Aceite (H1): Dado o conjunto de referência recalibrado, Quando a rubrica roda sobre um documento real de 20 ou mais seções e sobre um degradado do mesmo tamanho, Então ela os separa com folga, E o número de seções de cada documento do conjunto está registrado, E o limiar aprovado não depende exclusivamente de documentos de menos de 10 seções.
FECHADO em 2026-08-03, e REESCRITO antes de fechar, porque a premissa dele era falsa. Este cartão nascia dizendo que a rubrica julgava bloco a bloco e que por isso era cega para defeitos de documento inteiro, e propunha uma rubrica em dois níveis (N1 por bloco amostrado mais N2 de documento). A premissa foi medida no código e é falsa: o juiz extrai as unidades narrativas de todos os blocos, junta num texto único e avalia o documento INTEIRO numa chamada, repetindo 3 vezes para a mediana. Nunca houve fragmentação, e portanto não havia nível 1 a criar: o nível 1 já era de documento. A cegueira é real, mas vem do ENUNCIADO da avaliação, não da granularidade: as cinco dimensões D1 a D5 não perguntam sobre a relação entre seções distantes, e o juiz não reprova o que ninguém lhe pediu.
O que foi entregue no lugar: um SEGUNDO CRITÉRIO, E1 a E3, que pergunta o que o primeiro não perguntava. E1 eco entre seções (a mesma leitura reaparece longe com outras palavras, sem acrescentar nada), E2 contradição entre seções (duas seções afirmam o oposto sobre a mesma métrica no mesmo período) e E3 tese sustentada (a tese aberta no hero é retomada e fechada, não abandonada no último terço). Roda na mesma forma do primeiro: uma chamada sobre o documento inteiro, 3 passadas e mediana. Ficha no Ap.4 do DRS.
Consequência de custo, também corrigida: o risco registrado no DRS de que julgar custaria 75 chamadas por relatório (25 blocos vezes 3 passadas) não existia, porque nascia da mesma premissa falsa. Julgar custa 3 chamadas por documento com um critério e 6 com os dois, independente do número de blocos. O parágrafo de risco foi reescrito no bloco 12 do DRS com o número certo em vez de removido, para que a correção fique legível.
Prova (H1 atendido): Dado o relatório real de 25 seções que entrou no conjunto de referência, Quando os dois critérios rodam sobre ele, Então o critério antigo dá nota máxima nas cinco dimensões, E o critério novo dá média 3,33 apontando eco entre seções distantes, E o custo medido é de 3 chamadas por critério e não cresce com o número de blocos. O documento perfeito no critério que existia e imperfeito no que nasceu é a evidência de que o segundo critério faz falta.
Fica aberto o controle negativo, e por isso o Alfa correspondente NÃO fecha junto: o critério foi exercido sobre um documento real, mas ainda não sobre fixtures construídas de propósito (um documento com duas seções se contradizendo, outro com a tese abandonada). Essa prova é o {TA-120}, que nasce aberto. Fechar o cartão de construção não fecha o cartão de teste, e é assim mesmo.
RNF-PERF-002. O teto único de 60 segundos por documento está calibrado errado e pune exatamente o relatório grande, que é o relatório real: um de 7 seções cabe folgado e um de 25 não cabe, então o teto reprova por tamanho e não por lentidão.
O que implementar: teto por bloco (referência de 6 segundos); teto de documento igual ao número de blocos multiplicado pelo teto por bloco, com piso de 60 segundos e limite superior configurável por agência; o bloco que estoura o seu teto é interrompido sem derrubar a composição inteira.
Como verificar (teste primeiro): um documento de 25 blocos recebe 150 segundos e um de 7 recebe o piso de 60; um bloco travado é cortado em 6 segundos e os demais seguem; nenhum documento é reprovado por tamanho enquanto cada bloco respeitar o seu teto; o custo e o tempo já gastos continuam registrados no estouro.
Aceite (H1): Dado um documento de 25 blocos e um de 7 blocos, Quando ambos são compostos, Então o teto de cada um é o número de blocos multiplicado pelo teto por bloco com piso de 60 segundos, E um bloco isolado que ultrapasse o teto por bloco é interrompido sem derrubar a composição, E nenhum documento é reprovado por tamanho enquanto cada bloco respeitar o seu teto.
FECHADO em 2026-08-03, e DERRUBOU a premissa que estava escrita no documento. A composição não encadeia de cinco a sete leituras: encadeia DUAS. E as 3 passadas da régua de qualidade, que rodavam em série, são independentes entre si e passaram a rodar em paralelo. A previsão de gargalo que o DRS fazia com base no número antigo caiu junto, e o texto foi corrigido no Ap.4 e no bloco 7 (G.1), que é onde restrição arquitetural pertence. É a TERCEIRA afirmação recebida pronta que a medição derrubou nesta sessão (as outras: o placar das fórmulas do Ap.10 e a arquitetura da rubrica), e as três já viviam no documento como se fossem fato verificado. Foi o que produziu a RNF-MANUT-005 e o que define o gatilho dela: não é desconfiar da fonte, é medir no ato de escrever, inclusive o que já estava lá.
Premissa original, preservada como histórico. A composição encadearia de 5 a 7 leituras. Se cada uma precisa apenas do catálogo de pontos de dado e da direção do briefing, elas são independentes e paralelizam; se alguma depende mesmo da saída da anterior, isso é uma restrição arquitetural que hoje não está declarada em lugar nenhum, e restrição arquitetural não declarada pertence ao DRS, não ao código.
O que implementar: instrumentar cada uma das leituras declarando explicitamente as suas entradas; medir quais consomem saída de outra; paralelizar as independentes; e escrever o resultado, qualquer que seja, no bloco 7 do DRS.
Como verificar (teste primeiro): o mapa de dependências entre as leituras existe e é derivado da instrumentação, não da leitura do código a olho; as independentes rodam em paralelo e o tempo total cai de forma mensurável; se houver dependência real, ela está escrita no bloco 7 do DRS com o motivo.
Aceite (H1): Dada a composição instrumentada, Quando o mapa de dependências entre as leituras é levantado, Então cada leitura declara as suas entradas por medição, E as leituras independentes passam a rodar em paralelo com queda mensurável do tempo total, E toda dependência real encontrada está declarada no bloco 7 do DRS.
FECHADO em 2026-08-03, e a QUARTA ocorrência é a evidência A FAVOR que fecha o argumento. A rota de visibilidade de métrica ({F20-07}) nasceu registrada depois da rota dinâmica, mesma causa das três anteriores. A diferença decisiva: esta foi PEGA pelo verificador, porque ele foi executado. Ou seja, a ferramenta funciona; o que falhava era ela ser opcional. Isso não enfraquece a lição da terceira ocorrência, confirma: a terceira passou porque ninguém rodou, a quarta foi barrada porque alguém rodou, e a diferença entre as duas não foi a qualidade da ferramenta, foi um ato de lembrança. Uma guarda cujo funcionamento depende de alguém lembrar de acioná-la tem a taxa de acerto da memória humana, não a do código. Por isso a entrega deste cartão é a trava obrigatória na integração contínua, e não o verificador executável, que já existia e já funcionava.
TERCEIRA ocorrência, em código escrito DEPOIS de o verificador já existir. A rota da tela de agendas ({F19-09}) nasceu registrada no fim do arquivo e foi engolida pela rota dinâmica do mesmo prefixo, exatamente como as duas de 2026-08-02, com a ferramenta de checagem já disponível e o requisito já escrito. A causalidade que este cartão passa a carregar: ferramenta que existe mas não é trava não impede nada. Enquanto a verificação for algo que alguém PODE rodar, ela depende de lembrança e falha junto com a memória de quem escreve; ela só protege quando REPROVA sozinha, no mesmo lugar em que o código não passa. É por isso que a entrega deste cartão não é um verificador executável, e sim o teste que faz a requisição HTTP real contra o roteador montado e que reprova quando a ordem é invertida. Um verificador que ninguém é obrigado a rodar é documentação, não guarda.
RNF-MANUT-004; nasceu de duas ocorrências no mesmo dia e ganhou a terceira no dia seguinte. O padrão apareceu no painel de analytics e no domínio custom, escrito por caminhos diferentes: uma rota de caminho fixo registrada DEPOIS de uma rota dinâmica do mesmo prefixo é engolida por ela, passa a responder erro para todo chamador, e a interface mascara como "não tem nada" porque o front trata erro como lista vazia. Duas ocorrências independentes da mesma causa em um dia é padrão, não coincidência.
Por que a suíte não pega: os testes chamam o handler importado direto, pulando o roteador, que é justamente a camada onde o defeito mora. O caminho testado nunca é o caminho servido. Mesma família do bloqueio de scripts inline do leitor ({F12-07}) e da guarda que enumerava três scripts e não reprovava o quarto.
O que implementar: conferência automática que monta o roteador e falha quando uma rota de caminho fixo está registrada depois de uma rota dinâmica do mesmo prefixo; e a regra de que toda rota nova entra com um teste de regressão que faz a requisição HTTP REAL contra o roteador montado, nunca contra o handler importado. Aplicar a conferência retroativamente às rotas que já existem, porque as duas ocorrências conhecidas foram achadas por acaso e não por varredura.
Como verificar (teste primeiro): inverter a ordem de registro de um par conhecido e confirmar que a conferência REPROVA; escrever uma rota fixa nova sombreada de propósito e confirmar que o teste de HTTP real a pega, enquanto um teste que chama o handler direto passaria (é essa diferença que justifica a regra); rodar a conferência sobre todas as rotas atuais e obter zero sombreamento. Fixture de controle negativo (RNF-MANUT-003): o par sombreado, que a suíte DEVE reprovar.
Aceite (H1): Dado uma rota de caminho fixo que compartilha prefixo com uma rota dinâmica, Quando a conferência roda sobre o roteador montado, Então a fixa está registrada antes da dinâmica, E existe um teste que faz a requisição HTTP real e recebe a resposta da rota fixa, E inverter a ordem de registro faz esse teste falhar, E a varredura sobre as rotas existentes retorna zero sombreamento.
RNF-SEG-008. Os documentos deste projeto são servidos sem login, então tudo neles é público. A varredura de 2026-08-02 achou 26 menções de topologia acumuladas por sessões diferentes (8 no DRS, 13 no Roadmap, 1 no Inventário, 4 no Plano de Testes), mais um nome de cliente. Nenhuma foi escrita com má intenção: cada sessão registrou onde tinha acabado de mexer. Por isso a verificação pertence ao PUBLISH e não à redação, porque o autor de cada frase não enxerga o acúmulo.
O que implementar: passo de varredura no procedimento de publicação dos documentos, cobrindo as classes nominais: nome de repositório, nome de branch, hostname interno, endereço IP, caminho de arquivo de código, caminho de container ou de servidor, porta, arquivo de ambiente, e nome de pessoa. O passo REPROVA o publish com a lista do que achou e em qual documento, em vez de avisar. Exceções nominais que NÃO contam (para a varredura não virar ruído que se aprende a ignorar): as URLs públicas do mockup e do leitor, as rotas da própria API do produto, e os identificadores do modelo de dados especificado no DRS.
Como verificar (teste primeiro): injetar, num documento de teste, uma menção de cada classe, uma por vez, e confirmar que a varredura REPROVA cada uma nomeando documento e trecho; confirmar que uma afirmação de prova ("provado em produção", "552 de 552 testes verdes") NÃO é reprovada, que é o controle que impede a varredura de comer a substância junto com o endereço; confirmar que as exceções nominais passam; rodar contra os 6 documentos atuais e obter zero.
Aceite (H1): Dado um documento com uma menção de qualquer das classes nominadas, Quando a varredura do publish roda, Então o publish é reprovado com o documento e o trecho nomeados, E uma afirmação de prova sem endereço passa sem ser reprovada, E a varredura contra os 6 documentos publicados retorna zero ocorrência.
FECHADO em 2026-10-16. Pacote: main e666cd8. Hub: main 30e4975, com carimbo fbe106b e pacote
2057/2057. A rodada 1 do mergecheck reprovou no teste do preflight de conexões, que contava um clone
criado por agent concorrente; sozinho ele passou 4/4.
Deploy: migration 0273 aplicada em produção com backup antes (/root/f18-16-bkp/), e as 3
constraints nasceram validadas: template_blocos_contrato_v3, report_draft_blocos_contrato_v3 e
snapshot_payload_contrato_v3. API 15/15 e web publicado.
O que mudou: y_format, body_html e cor livre em fatia do donut são recusados em qualquer tipo
de bloco e em qualquer profundidade. No pacote, assertBlockPayloadContractV3 deixou de ser órfã. No
Hub, POST/PATCH de modelo e PATCH de bloco respondem 400 REPORT_BLOCK_CONTRACT_V3, com mensagem que
nomeia o substituto. No banco, um CHECK nas 3 colunas cobre os 13 pontos de gravação e os futuros.
accent_index fora de 0..5 agora é recusado; antes era corrigido em silêncio.
Testes: review APPROVED. QA PASS: os 3 campos em 13 tipos, 27/27 UPDATEs recusados no banco e
baseline com 132 FAIL sem o fix.
Limite declarado: a rota de publicar não chama a barreira. Ela é coberta pelo CHECK do banco, mas
sem a mensagem amigável.
Estado MEDIDO em 2026-08-03, e o cartão segue aberto de propósito. Existe commit citando este cartão junto de {F18-07} e {F18-08}, e existem suítes de contrato de bloco e de versão de schema no repositório. Mensagem de commit não é prova de H1: o aceite exige que os TRÊS campos nominados do Ap.3 (formato de número no bloco, cor livre na fatia do donut e corpo em HTML cru) sejam recusados PELO SCHEMA, cada um com o seu controle negativo. Não confirmei os três, então não fecho. Para fechar, falta exercitar os três campos e ver o schema recusar cada um.
Ap.3 (tabela de campos excluídos), Ap.2. Três campos existem no renderizador irmão por acidente de construção. Sem uma barreira de schema, eles voltam na primeira vez que alguém copiar um payload de lá para cá, e a exclusão vira lenda oral.
O que implementar: rejeição por schema (additionalProperties:false nos blocos afetados) de line_chart.y_format (o formato vem só do dp), de cor hexadecimal em donut.slices[] (existe accent_index sobre a rampa da identidade do cliente) e de body_html em qualquer bloco narrativo (existe body_md com allowlist); mensagem de erro que nomeia o campo substituto, para o desenvolvedor não tentar contornar.
Como verificar (teste primeiro): enviar cada um dos três campos pela API, um por vez, e confirmar recusa com a mensagem nomeando o substituto; confirmar que accent_index fora da rampa também é recusado; confirmar que um body_md com marcação fora da allowlist é sanitizado e não recusado. Fixture de controle negativo: um payload com os três campos, que a suíte DEVE reprovar.
Aceite (H1): Dado um payload de bloco contendo y_format, cor hexadecimal em fatia de donut ou body_html, Quando o schema o valida, Então cada um dos três é recusado com a mensagem nomeando o campo substituto, E um accent_index fora da rampa da identidade do cliente também é recusado, E o payload equivalente usando apenas os campos do contrato v3 é aceito.
Estado da F18 em 2026-08-03: 15 cartões de 18 fechados, 3 ABERTOS. Fechados: {F18-01} a {F18-05}, {F18-07} a {F18-10} e {F18-12} a {F18-18}. Abertos: {F18-06} (lacuna de modelo de dados, não de ligação), {F18-11} e {F18-16} (com evidência de entrega medida no repositório, mas sem confirmação do H1, e por isso não fechados). 15 mais 3 fecham em 18.
Duas correções de contagem nesta fase, e as duas foram pegas contando em vez de adotando. A primeira foi minha: cheguei a escrever que a fase estava fechada tratando um conjunto de seis cartões como "os seis restantes" sem contar a fase inteira, quando dez estavam abertos. A segunda veio na rodada seguinte: a lista de fechamento recebida previa quatro cartões restantes e nomeava apenas três, e a recontagem revelou que o quarto era o {F18-05}, que a medição mostrou já entregue. Nos dois casos o erro era de aritmética sobre um conjunto que ninguém tinha percorrido inteiro, e nos dois casos a recontagem pelo estado efetivo o desfez. É o que a RNF-MANUT-005 exige, e vale inclusive quando o número é meu.
FECHADO em 2026-10-16: pacote main 9f7bd4a (merge 9f7bd4a), Hub main 5248b6e (merge
5248b6e, carimbo aa05a99), API 15/15 e web publicado. A guarda fica na rota de publicar, ponto
único de todo retrato público: roda dentro da transação e antes de senha, expiração, cartão e
fn_publish_report, e recusa com 422 SCAFFOLDING_LEAK sem gravar nada (banco idêntico antes e
depois). Varre todo texto-folha sem lista de campos e deriva os padrões das próprias constantes que
injetam o andaime; não aceita override. Review APPROVED; qa PASS em SA e TM, 375 e 1440, claro e
escuro. Baseline em d15b8c6: o mesmo vazamento publicava com 200 e o leitor exibia o marcador.
Limite declarado: raciocínio livre da IA em prosa não é detectável. A tela ainda mostra o marcador
literal ao operador; isso foi para o {F18-19b}. O mergecheck revelou o {INFRA-25} do Hub.
Cartão novo, defeito VISTO em relatório publicado de verdade em 2026-08-05, durante a verificação do {F7-04}. No bloco de leitura da página publicada apareceram, no texto que o cliente lê, o marcador interno que separa o canal de dado não confiável do canal de instrução e campos de preenchimento que a IA não substituiu (do tipo "inserir valor aqui"). Não é defeito da fatia que o encontrou: é do caminho de narrativa, e vinha passando por todos os gates atuais, porque nenhum deles pergunta se o texto final contém andaime do próprio sistema. É a assinatura conhecida da casa: guarda que percorre lista curada só protege o que alguém lembrou de listar, e ninguém listou "o texto não pode conter as marcas que eu mesmo injeto".
RF-CMP-003, RF-CMP-017, RNF-SEG-003, Ap.2. Este é o pior tipo de vazamento do produto, porque é visível para o cliente final da agência parceira e denuncia o mecanismo interno.
O que implementar: uma guarda de publicação que reprova quando o texto de qualquer bloco narrativo contém marca de andaime do sistema (os delimitadores de canal, os rótulos de seção do prompt e qualquer campo de preenchimento não substituído), derivando os padrões das mesmas constantes que os injetam, nunca de uma cópia colada, senão a guarda envelhece na primeira vez que o delimitador mudar. E a fixture de controle negativo correspondente ({F18-11}): o texto com andaime, que a suíte DEVE reprovar.
Como verificar (teste primeiro): compor um relatório cujo texto contenha o delimitador de canal e outro cujo texto contenha um campo de preenchimento não substituído, e confirmar que a publicação é recusada nos dois casos, nomeando o trecho ofensor; mudar o valor do delimitador na constante de origem e confirmar que a guarda continua pegando, sem edição.
Aceite (H1): Dado um relatório cujo texto narrativo contém marca de andaime interno ou campo de preenchimento não substituído, Quando a publicação é acionada, Então ela é recusada com o trecho ofensor nomeado, E nenhuma página publicada exibe delimitador de canal, rótulo de prompt ou campo de preenchimento ao cliente.
FECHADO em 2026-10-16.
- Merges: pacote main b74bfa0, Hub main a03cb25 (merge a0d55b0, carimbo 749d731).
- Deploy: API 15/15, web publicado.
- Gates: review APPROVED na rodada 3 (a 1 pediu tirar SCAFFOLDING_MASK do barril; a ERRATA corrigiu a
afirmação de que nenhum teste do Hub conferia o trecho). qa PASS em SA e TM, 375 e 1440, claro e escuro.
- O que mudou: o scanner mascara o texto inteiro ANTES de recortar os 80 caracteres. O marcador de
canal (etiqueta de abertura com artifact_id inteira), os rótulos e os moldes de prompt viram
"[marcação interna do sistema]" no 422, no painel e no toast. [object Object] e [inserir ...]
continuam literais. Baseline em e666cd8: o marcador aparecia literal.
- Ressalvas do qa: o canvas atrás do modal mostra o próprio rascunho (correto); o recorte pode cortar a
própria máscara no fim.
- No caminho: o mergecheck revelou o {INFRA-25b} e o {INFRA-25c}.
Nasceu do qa do {F18-19} em 2026-10-16. Quando a publicação é recusada por vazamento de canal, o
painel do compositor cita o trecho ofensor literal, e esse trecho É o marcador interno
(<<<DADO_NAO_CONFIAVEL_DO_CLIENTE artifact_id=...>>>). Só o operador da agência vê, nunca o cliente
final, mas é nome interno na tela, e o RNF-SEG-003 pede que o mecanismo não seja denunciado.
Decisão (resposta única, não é pergunta): mascarar. O painel diz "marcação interna do sistema" no
lugar do marcador e mantém o resto do trecho, para o operador achar o bloco.
Aceite (H1): Dado um rascunho com marcador de canal no texto, Quando a publicação é recusada, Então
o painel aponta o bloco e o trecho, E nenhum marcador, nome de constante ou id interno aparece na tela,
E a recusa e o "nada gravado" do {F18-19} continuam iguais.
FECHADO em 2026-08-09, mergeado e deployado, com a prova que fecha o círculo: o mesmo título de
bloco de topo que ontem publicou com número cru hoje é recusado em produção, e um código de
peça legítimo continua publicando. O buraco fechou sem virar catraca.
A correção inverteu a lógica, e é isso que impede a recaída. Antes, um tipo entrava na disciplina
declarando que entrava, e por isso o mesmo defeito nasceu três vezes (introdução de seção,
legenda de mídia, e o topo). Agora a cobertura é derivada do conjunto real de campos editáveis, com
exceção nomeada: os dezesseis nascem cobertos, e um tipo novo nasce coberto sem declarar nada,
provado registrando um tipo inventado e vendo a guarda pegá-lo.
O caminho até aqui custou três rodadas, e o motivo vale mais que o resultado: cada conserto
abriu um buraco pela ponta oposta. Ampliar a cobertura criou recusa indevida de código de peça;
consertar isso soltou número colado a palavra comum, que era regressão; e o mesmo afrouxamento
soltou as siglas de métrica coladas ao número, que são o vocabulário nativo de um relatório de
mídia. Em todas as três, o teste que faltava era o do caso inverso, e foi por isso que cada
correção passou verde carregando o defeito seguinte. Fechou com um critério único de forma e
tamanho, sem lista de siglas, com baseline provado em cada caso.
O limite que sobra está nomeado, medido e com cartão: sigla de custo seguida de quatro dígitos
inteiros ainda escapa. O ataque à calibração discriminou em vez de generalizar (só duas das siglas
testadas são plausíveis nessa faixa, conferido contra o conjunto real de dados). Ver {F18-21}.
Cartão novo, e o defeito foi PROVADO no caminho real em 2026-08-08, não deduzido. Um relatório
com o título do bloco de topo escrito como "Geramos 340 leads em julho", sem nenhuma origem,
publicou com sucesso e o link público anônimo serviu o texto cru a qualquer visitante. O gate
de proveniência não recusou.
Contagem que fecha, porque este cartão afirma quantidade. Universo: os 16 tipos de bloco do
registro. Critério: o tipo declara quais campos de texto entram na varredura de número sem origem.
Declaram: 4 (leitura, mídia, introdução de seção e plano de ação). Não declaram: 12.
4 + 12 = 16. E mesmo entre os 4, só o subcampo declarado é varrido: os dois primeiros cobrem o
corpo dos itens e não o título do próprio bloco.
Os 12 de fora, nominalmente, com os campos de texto editáveis expostos: topo (sobretítulo,
título, subtítulo, descrição); indicadores (rótulo e rodapé dos itens); funil (título, sobretítulo,
descrição, rótulo e sublinha das etapas, nota do resumo); cartões de estatística (título, descrição,
rótulo dos cartões); retenção de vídeo (título, descrição, nota, rótulo das linhas); rankings
(título, sobretítulo, descrição, rótulo dos cartões); tabela (título, sobretítulo, descrição, nota,
rótulo das colunas); lacuna (título, sobretítulo, descrição); comparação (título, descrição, métrica
das linhas); gráfico de linha (título, descrição, nota, rótulo das séries); rosca (título, descrição,
nota, rótulo central); confiabilidade (nota).
Por que este é o pior lugar possível para o buraco: o bloco de topo é singular, é o primeiro que
o cliente lê, e o título é justamente onde alguém escreveria o número que quer destacar. A promessa
central deste produto é que número exibido tem dono nomeado; aqui ela não vale.
É o mesmo padrão já corrigido duas vezes (introdução de seção e legenda de mídia), e nunca
aplicado ao topo. Guarda que percorre lista curada só protege o que alguém lembrou de listar,
e a lista aqui tem 4 de 16.
O outro lado, medido junto e igualmente real: a regra isenta apenas data, faixa e ordinal, então
um código de peça como uma identificação de criativo reprova indevidamente a publicação. Já
aconteceu duas vezes com quem estava operando. Consertar só um dos lados troca um defeito por outro.
O que implementar: a cobertura deixa de depender de cada tipo lembrar de se declarar. Derive-a
do conjunto real de campos de texto editáveis, com exceção nomeada para o que não deve entrar,
em vez de inclusão nomeada para o que deve. E trate o falso positivo do código alfanumérico.
Como verificar (teste primeiro): para cada um dos 16 tipos, um número cru em cada campo de
texto editável é recusado na publicação; um código de peça alfanumérico não é; e acrescentar um
tipo novo sem declarar nada entra na disciplina por padrão, em vez de escapar.
Aceite (H1): Dado um relatório com número sem origem em qualquer campo de texto editável de
qualquer tipo de bloco, Quando a publicação é acionada, Então ela é recusada nomeando o trecho, E um
código alfanumérico de peça não provoca recusa, E um tipo de bloco novo nasce coberto sem precisar
declarar nada.
Cartão novo, medido em 2026-08-09 durante o fechamento do {F18-20}. A disciplina de número passou
a distinguir código de peça de métrica crua por forma e tamanho do agrupamento de dígitos. É a
melhor heurística disponível sem lista curada, e ela tem um limite declarado e agora medido:
uma sigla seguida de quatro dígitos inteiros escapa.
A calibração foi atacada e o resultado discrimina, em vez de generalizar: das siglas de métrica
testadas, só duas são plausíveis nessa faixa, porque custo por aquisição e custo por lead não
têm teto (cliente de ticket alto), enquanto as demais ficam presas a valores pequenos, conferido
contra o conjunto real de dados. Ou seja, o escape existe e é estreito, não é hipótese de gabinete.
RF-CMP-007, RN-012, Ap.8.
O que implementar, e o caminho importa: cruzar o número encontrado contra os **dados do próprio
bloco**, em vez de julgar pelo texto. Se o valor existe entre os dados com origem, é métrica e
precisa de marcador; se não existe em lugar nenhum, é candidato a código. **Baixar o limiar de
dígitos quebraria códigos legítimos, e manter lista de siglas seria voltar à lista curada** que o
{F18-20} acabou de eliminar. Os dois caminhos fáceis são os errados.
Como verificar (teste primeiro): uma sigla de custo seguida de quatro dígitos que corresponda a
um dado do bloco é recusada; um código de peça com o mesmo formato, que não corresponde a dado
nenhum, passa; e os casos já cobertos pelo {F18-20} continuam como estão.
Aceite (H1): Dado um agrupamento de dígitos colado a letras num campo de texto, Quando a
publicação é acionada, Então ele é tratado como métrica se corresponder a um dado do bloco e como
código se não corresponder, E nenhuma lista de siglas é consultada para decidir.
FECHADO em 2026-08-10, mergeado (dcb475d) e deployado. QA PASS e review APPROVED. O
cruzamento passou a usar os valores já vinculados ao bloco, cobrindo propriedades e narrativa
pela mesma fonte única que já existia, montado uma vez por bloco. Nenhuma lista de siglas foi
criada, que era o atalho proibido.
O QA provou o mecanismo revertendo o código, não lendo o relato: com a versão anterior, três
testes caem: o par de aceite, a normalização e o caso de colisão. Trava cujos dois ramos aceitam
tudo já foi encontrada neste projeto, e é por isso que essa prova é obrigatória.
A decisão mais dura da fatia, e ela CUSTA algo: no bloco de pesquisa de mercado o cruzamento é
estruturalmente impossível: aquele contexto é isolado por segurança e nunca recebe dado do
cliente, por desenho. A escolha foi recusar, e o QA mediu o efeito real fora do teste do autor:
um achado que cite um código legítimo de quatro dígitos passa a ser descartado em silêncio. O
julgamento, que eu subscrevo: passar os dados àquele contexto para permitir o cruzamento quebraria
a fronteira de isolamento, o que é pior; e a perda é de um statement, não do relatório.
Dois gates discordaram, e a divergência foi resolvida medindo, não escolhendo lado. O review
apontou como furo o fato de a extração de números do texto nunca capturar sinal, deixando um dado
negativo sem colidir. Eu mandei consertar por valor absoluto, e estava errado. O QA havia
exercitado o caso e julgado correto o comportamento atual: para uma métrica negativa "se disfarçar
de código" ela precisaria estar escrita com prefixo de letras, o que é contorcido, enquanto comparar
por módulo rejeitaria código legítimo sempre que o bloco tivesse um dado de mesmo valor com sinal
trocado. Furo teórico trocado por rejeição real é a direção errada numa trava que já mutila texto na
pesquisa de mercado. O conserto foi cancelado e o limite ficou declarado no código, junto da
função de cruzamento.
Risco de colisão medido pelos dois, independentemente: zero ocorrências do formato no conjunto
real de dados. O caso do teste é demonstração construída, e está dito que é.
Definição de Pronto F18 (ainda NÃO atendida): toda comparação da página declara a sua base dentro da frase; a tupla de identidade completa cobre todo número exibido e o gate nomeia o campo divergente; conectivo causal sem evidência sai como coocorrência; o anti-fadiga classifica pelos quatro quadrantes de dado contra texto e a recomendação repetida escala em vez de ser reescrita; seção sem histórico se oculta e vira lacuna; não medido tem uma representação única e nunca é zero; sazonalidade vive no cadastro do cliente; toda guarda não estrutural oferece saída legal e escotilha registrada; cada guarda tem a sua fixture de controle negativo na integração contínua; a régua de qualidade foi recalibrada contra um documento do tamanho do produto e ganhou o nível de documento; o teto de tempo escala com o tamanho; e os três campos excluídos são barrados por schema.
Fase F19, Geração agendada (Fatia 19)
Origem: resposta do dono a {Q4b} em 2026-08-03. A geração agendada deixou de ser evolução e virou escopo, e com ela a report_job (ADR-021, DRS E.2). As quatro palavras da resposta são requisitos e não adjetivos: configurável ({F19-02}), editável ({F19-03}), ativável e pausável ({F19-04} e {F19-05}).
FASE FECHADA em 2026-08-03, entregue e provada nos dois repositórios, incluindo a tela com smoke real (criar, listar, pausar, retomar, editar com prefill e histórico de ciclos).
A trava principal está PROVADA, e era a condição imposta pelo dono: não existe caminho que publique sozinho. Provado por dois meios independentes. Busca dirigida: nenhuma referência a publicação no disparador de ciclo nem no módulo de agenda, nos dois repositórios, medida arquivo a arquivo. E por teste: o publish segue sendo o único endpoint, sem parâmetro de origem, de modo que não existe nem sequer a forma de dizer a ele "esta edição veio da agenda". A guarda não é uma condição escrita dentro do publish, que alguém poderia inverter; é a ausência do caminho, que ninguém pode chamar.
ESCOPO DECLARADO, e não pendência: a agenda automatiza a COMPOSIÇÃO, não a COLETA (decisão do dono, 2026-08-03, RN-017 item 7). Este documento não define upload automático de artefato e não existe conector de ingestão. O disparador adota um relatório que o operador já deixou preparado (criado, com artefato subido e direção confirmada, sem ter clicado em compor) cuja conta, propósito e período batem com a janela do ciclo. Sem ele, o ciclo fecha com o desfecho no_artifact. O que a cadência tira do operador é montar e escrever, não juntar os dados, e dizer "geração automática" sem essa frase seria prometer mais do que se entrega.
ESCOPO DECLARADO: o gate de honestidade não roda dentro da composição automática, e está certo assim (RN-017 item 8). Ele roda no publish, que continua manual e obrigatório. O rascunho automático é matéria-prima, e a honestidade se verifica quando alguém decide publicar; o desfecho de falha da composição automática cobre falha estrutural e exceção, nunca honestidade. Condição registrada: se um dia a publicação virar automática, esse gate precisa entrar ANTES, dentro do caminho automático, como pré-requisito e não como consequência.
Dependências, e uma delas é dura. {F17-05} (cadência declarada na instância e mapa de base de comparação) precisa existir antes, porque a agenda carrega a cadência. E {F18-05} (anti-fadiga comparando variação do DADO contra variação do TEXTO) é PRÉ-REQUISITO, não melhoria paralela (RN-017 item 6): a cadência curta e automática faz as edições nascerem no ritmo do calendário e não no ritmo do operador, e punir repetição de texto num acervo denso, com o anti-fadiga ainda medindo texto contra texto, empurra a composição a inventar variação onde o dado não variou. Construir a agenda antes disso é ligar o volume numa guarda que mede a coisa errada.
DRS E.2, RN-017. Criar report_schedule (com unicidade sobre o par cliente mais propósito), schedule_cycle (com o desfecho em valor fechado de seis) e promover report_job de evolução a tabela real, com schedule_id opcional. Estender RN-009 (guarda de coerência entre tenant e conta) às três, como {F11-01} teve de fazer para as metas.
Como verificar (teste primeiro): o INSERT de uma segunda agenda para o mesmo par cliente mais propósito é rejeitado pelo banco; um outcome fora dos seis é rejeitado; um INSERT com company_id de uma agência e account_id de conta de outra é rejeitado; a leitura sob a regra de acesso de outra agência devolve nada.
Aceite (H1): Dado o schema aplicado, Quando uma segunda agenda do mesmo par é inserida, Então o banco a recusa, E um desfecho fora dos seis nominais é recusado, E o cruzamento de tenant é recusado com o mesmo código de RN-009.
RF-ADM-019, agenda de geração por cliente e por tipo.
O que implementar: criação da agenda com cadência, âncora de calendário, modelo, responsável e estado ativa; recusa da segunda agenda no mesmo par com motivo em linguagem humana; a composição avulsa segue existindo em paralelo.
Como verificar (teste primeiro): criar agenda mensal de fechamento para um cliente; tentar a segunda no mesmo par e ver a recusa com motivo; criar agenda de outro propósito do mesmo cliente e ver aceita; compor um relatório avulso desse cliente e ver que a agenda não impede.
Aceite (H1): Dado um cliente sem agenda para o propósito fechamento, Quando o operador cria uma agenda mensal para esse par, Então a agenda é gravada com cadência, âncora de calendário, modelo, responsável e estado ativa, E uma segunda tentativa no MESMO par é recusada com o motivo, E uma agenda de outro propósito do mesmo cliente é aceita, E o operador continua conseguindo compor um relatório avulso desse cliente fora da agenda.
RF-ADM-020, a identidade da agenda sobrevive à edição.
O que implementar: edição de cadência, âncora, modelo e responsável sobre a MESMA linha; a mudança vale do próximo ciclo em diante; registro no log de alterações em linguagem humana (RF-ADM-012) com valor anterior e novo.
Como verificar (teste primeiro): com três edições já geradas, mudar a cadência de mensal para trimestral e conferir que o identificador da agenda não mudou, que as três edições seguem ligadas a ela, que nenhuma delas foi alterada, e que o log traz o antes e o depois.
Aceite (H1): Dada uma agenda mensal com três edições já geradas, Quando o operador a muda para trimestral, Então a agenda continua sendo a mesma e as três edições anteriores seguem ligadas a ela, E a mudança vale do próximo ciclo em diante, E nenhuma edição já publicada é alterada, E o log registra a cadência anterior e a nova com quem mudou e quando.
RF-ADM-021, pausar é estado, não exclusão com outro nome.
O que implementar: transição para paused; o disparador do ciclo passa a fechar o ciclo com desfecho skipped_paused em vez de gerar; tudo o mais permanece intacto.
Como verificar (teste primeiro): o controle NEGATIVO é o teste inteiro aqui, item por item, porque é onde a implementação erra. Com uma agenda de quatro edições, uma delas em rascunho: pausar, vencer o ciclo, e conferir nominalmente que a agenda existe, que os quatro campos de configuração seguem com os mesmos valores, que as quatro edições seguem alcançáveis, que os links publicados seguem respondendo 200 e que o rascunho segue editável e publicável.
Aceite (H1): Dada uma agenda mensal ativa com quatro edições geradas, uma delas em rascunho, Quando o operador a pausa e o ciclo seguinte vence, Então nenhuma edição nova nasce, E a agenda continua existindo com a mesma cadência, âncora, modelo e responsável, E as quatro edições continuam alcançáveis, E os links já publicados continuam respondendo, E o rascunho em curso continua editável e publicável.
RF-ADM-022, retomar não repõe o atraso.
O que implementar: transição para active sem qualquer geração no ato; os ciclos vencidos durante a pausa ficam gravados como skipped_paused com as janelas deles e são exibidos na agenda; recuperar um deles é composição manual do operador.
Como verificar (teste primeiro): pausar por três ciclos, retomar, e conferir que nenhum rascunho nasceu no ato; avançar até o próximo vencimento e conferir que nasceu exatamente UM; conferir que a agenda lista os três ciclos vencidos com as janelas.
Aceite (H1): Dada uma agenda mensal pausada por três ciclos, Quando o operador a retoma, Então nenhuma edição nasce no ato de retomar, E a próxima edição nasce só quando o próximo ciclo vencer, E a agenda mostra os três ciclos vencidos durante a pausa com as janelas deles, E cada um deles só vira relatório se o operador o compuser à mão.
RF-CMP-027, é a comparação que dá sentido à cadência.
O que implementar: o disparador do ciclo compõe a edição pelo MESMO caminho de composição da criação manual (ADR-013 intacto, muda quem dispara), resolve a base pela cadência (RN-014) com a precedência de RN-012 item 2, e submete a base à guarda de largura de janela (RF-CMP-021).
Como verificar (teste primeiro): com a edição do mês anterior publicada, vencer o ciclo e conferir que o rascunho nasceu com a base apontando para ela e com a base por extenso dentro da frase; forçar uma janela de largura diferente e conferir que sai recusada ou marcada como parcial; rodar a PRIMEIRA edição de uma agenda nova e conferir que ela nasce "sem base" e não reprova.
Aceite (H1): Dada uma agenda mensal ativa cuja edição do mês anterior está publicada, Quando o ciclo do mês corrente vence, Então a edição nasce sozinha em rascunho, E a base de comparação dela é a edição do mês anterior, E a base aparece por extenso dentro da frase de cada número comparativo, E uma edição cuja janela tenha largura diferente da anterior sai recusada ou marcada como parcial, E a primeira edição de uma agenda nasce no estado "sem base" sem reprovar.
RF-CMP-028, a automação não pode ser a rota de fuga das guardas.
O que implementar: nenhum caminho de publicação disparado por agenda; o publish continua sendo o mesmo endpoint com gesto humano; nenhum gate passa a olhar a origem da edição.
Como verificar (teste primeiro): vencer o ciclo e conferir que, sem gesto humano, o relatório fica em rascunho e nenhum link público responde; conferir por leitura de código que o caminho de publish é UM só e não recebe a origem como parâmetro; introduzir uma edição agendada que reprove o gate de proveniência e conferir que ela não publica; conferir por busca dirigida que não existe parâmetro de agência, de agenda nem de ambiente que autorize publicar sem gate, e registrar a ausência com o que foi procurado.
Aceite (H1): Dada uma edição nascida de agenda e completa, Quando nenhum operador a publica, Então ela permanece em rascunho indefinidamente e nenhum link público responde por ela, E ao ser publicada ela atravessa os mesmos gates que uma edição manual atravessa, E uma edição agendada que reprove qualquer gate não publica, E não existe parâmetro que permita publicar sem esses gates.
RF-CMP-029, silêncio é o modo de falhar mais caro de um agendamento.
O que implementar: todo ciclo vencido fecha com um dos seis desfechos; aviso ao responsável pela Central de Notificações interna (nunca canal novo); nenhuma retentativa automática.
Como verificar (teste primeiro): vencer o ciclo sem artefato e conferir o desfecho no_artifact gravado, a notificação publicada e a agenda ainda ativa; repetir com parse quebrado, com a cadeia de inteligência artificial indisponível e com uma guarda reprovando, e conferir os desfechos distintos; conferir que nenhum ciclo fecha sem desfecho e que nada é retentado sozinho.
Aceite (H1): Dada uma agenda ativa cujo ciclo venceu sem artefato disponível, Quando o ciclo se encerra, Então a agenda registra o ciclo como sem edição com um dos seis desfechos nominais, E o responsável recebe o aviso pela Central de Notificações interna, E a agenda continua ativa para o ciclo seguinte, E o ciclo falhado não é repetido por conta do sistema.
RF-ADM-019 a RF-ADM-022, RF-CMP-029, a superfície onde tudo isso é operado.
O que implementar: lista de agendas da agência com cliente, propósito, cadência, próximo vencimento e estado; criar, editar, pausar e retomar; e o histórico de ciclos de cada agenda com o desfecho de cada um. Segue o mockup aprovado e o design system da plataforma-mãe, com o chrome próprio do Reports (G.1).
Como verificar (teste primeiro): percorrer a jornada inteira pelo navegador, criar, editar, pausar, retomar e ler o histórico, sem beco sem saída e com caminho de volta pela própria interface em toda tela; conferir que uma agenda de outra agência nunca aparece; smoke visual em desktop e mobile.
Aceite (H1): Dado o operador na tela de agendas, Quando ele cria, edita, pausa e retoma uma agenda, Então cada gesto reflete na lista sem recarregar a página, E o histórico mostra cada ciclo com o desfecho dele, E nenhuma agenda de outra agência aparece, E toda tela alcançada tem caminho de volta pela própria interface.
FECHADO em 2026-08-03 com smoke real da tela: criar, listar, pausar, retomar, editar com o formulário já preenchido com os valores atuais, e ler o histórico de ciclos.
Decisão de implementação registrada (do dono): o próximo vencimento aparece na tela por extenso ("todo dia 5, mensal") e não como data calculada. O motivo é não duplicar a matemática de calendário em duas linguagens: a regra de vencimento vive num lugar só, no lado que dispara, e a tela descreve a regra em vez de recalcular o resultado dela. Duas implementações da mesma conta divergem, e a que o operador lê seria a errada.
Defeito achado e corrigido no caminho, e ele é registro de reincidência: a rota desta tela nasceu registrada no fim do arquivo e foi ENGOLIDA pela rota dinâmica do mesmo prefixo, terceira ocorrência da classe (RNF-MANUT-004). O agravante é que aconteceu em código escrito DEPOIS de o verificador já existir, o que produziu a lição do {F18-18}: ferramenta que existe mas não é trava não impede nada.
Definição de Pronto F19: ATENDIDA, fase FECHADA em 2026-08-03. A agenda existe por cliente e por propósito, única no par e imposta pelo banco; ela é editável sem perder identidade nem o vínculo com as edições geradas; pausar interrompe apenas o nascimento de edições futuras, com os cinco negativos exercitados um a um; retomar segue do ciclo à frente e nunca repõe o atraso em lote; a edição agendada nasce em rascunho já comparada com a anterior e sob a guarda de largura de janela; nenhuma edição agendada publica sem gesto humano e nenhum gate olha a origem; todo ciclo vencido fecha com um dos seis desfechos e avisa o responsável; e o operador faz tudo isso por uma tela sem beco sem saída.
Fase F20, Modelo de venda e camada de resultado (Fatia 20)
Origem: a planilha de gestão que a operação usa de verdade, apontada pelo dono em 2026-08-03 como a base a seguir. Ela FECHA o item de investigação que o Ap.9 mantinha aberto (o corte entre geração de leads e comércio eletrônico) e o fecha contra a parte cara da hipótese: o corte é estrutural, mas produz UMA estrutura com vocabulário trocável, e não duas. Ver Ap.10, RN-018, ADR-022 e ADR-023.
Nenhum tipo de bloco novo nasce nesta fase (ADR-023). O funil canônico é ofunnel_simplecom o perfil canônico aplicado e a camada de resultado éstat_cardsalimentada pelo catálogo do Ap.10. A lista nominal de dezesseis tipos de RF-REN-012 continua fechada e inalterada, e conferir isso é parte do aceite de {F20-03}.
FASE FECHADA em 2026-08-03, com os sete critérios provados.
A prova que sustenta a camada inteira, e ela é do tipo que quase nunca se faz. As 11 métricas com fórmula a conferir foram exercitadas com todos os operandos plausíveis PRESENTES no contexto, e nenhuma emitiu. A distinção é o ponto: não é "não emitiu porque faltou dado", que seria um teste fraco e passaria por acidente; é não emite mesmo com o dado na mão, porque a fórmula não foi conferida. E o prazo de retorno do CAC continua bloqueado mesmo tendo fórmula literal transcrita da planilha, porque um operando dele não existe no catálogo, testado inclusive com o custo real de aquisição hipoteticamente resolvido. É a prova de que a trava é a regra e não a ausência de insumo.
As contagens do catálogo são função CALCULADA, não constante escrita à mão. Os placares (2 mais 3 mais 15 igual a 20; 4 conferidas mais 11 a conferir igual a 15) são derivados do próprio catálogo em tempo de execução. É isto que impede o placar de mentir depois: uma entrada nova muda o número sozinha, e ninguém precisa lembrar de atualizar um total escrito à mão, que é exatamente como as quatro divergências de contagem desta sessão nasceram.
O funil canônico IGNORA o rótulo digitado. O vocabulário vem sempre do modelo de venda do cliente, e o teste prova pelo negativo: corrompe o rótulo de propósito no bloco e confirma que o texto digitado não chega ao HTML servido. E o funil livre, sem modelo declarado, roda inalterado contra o conjunto de calibração real: regressão zero, que era a condição para esta fase ser aditiva e não uma troca.
Dependência: {F11-01} e {F11-02} (importação pelo sandbox de parse), porque três entradas do Ap.10 são fatos do negócio do cliente (margem de contribuição, fee da agência e o multiplicador de LTV) e toda derivada cuja cadeia de operandos passe por elas fica indisponível sem a importação.
Ap.10.2, RF-CMP-031. Este cartão vem PRIMEIRO na fase de propósito. Uma fórmula errada aqui não quebra nada: produz um número verdadeiro na forma e falso no valor, exatamente a classe de erro que ninguém reporta.
Estado em 2026-08-03, com a primeira rodada de conferência já feita. A exportação da planilha de gestão não expõe fórmula de célula, mas traz notas explicativas do autor, e delas saíram 4 formas transcritas (Lucro Bruto, ROMI, CPR e CAC Payback), a definição de unidade do LTV (multiplicador de ciclos, não dinheiro) e as regras de leitura do Ap.10.3. As notas também revelaram uma métrica ausente da transcrição inicial, o CAC Payback, e por isso o catálogo foi de 19 para 20 entradas. Nenhuma forma foi inferida: onde a planilha não define, o Ap.10 diz que não define.
O que falta implementar: conferir as 11 fórmulas derivadas ainda em a conferir (2 Ticket médio, 3 ROAS, 7 Lucro operacional, 8 Margem final, 12 Valor de LTV, 13 a 15 os três de LTV, 16 Real CAC, 17 ROI LTV, 18 Índice de qualidade do lead), definir o operando Margem média por cliente que hoje bloqueia o CAC Payback, e mudar o estado de cada uma para conferida no documento republicado. A fonte é a planilha, e uma fórmula que ela não escrever continua a conferir em vez de virar inferência.
Como verificar (teste primeiro): cada uma das 20 entradas tem classe de origem declarada; a soma por classe fecha (2 artefato mais 3 parâmetro mais 15 derivada igual a 20); toda derivada tem estado, e a soma de conferidas mais a conferir fecha em 15; a guarda de código recusa emitir métrica cuja fórmula esteja a conferir E também recusa emitir métrica com fórmula conferida cujo operando não resolva, que é o caso do CAC Payback hoje.
Aceite (H1): Dado o Ap.10 conferido contra a planilha, Quando o compositor tenta emitir uma métrica de resultado, Então ela só é emitida se a fórmula estiver conferida E todos os operandos dela resolverem, E uma métrica com fórmula a conferir sai como lacuna declarada, E uma métrica com fórmula conferida e operando não resolvido também sai como lacuna declarada, E as 20 entradas têm classe de origem declarada.
RF-ADM-023, o modelo de venda é atributo do cliente e nada além disso.
O que implementar: tabela account_profile (DRS E.2), que é também o lugar do atributo de sazonalidade de RF-ADM-018, com sale_model em valor fechado de dois e nulo permitido; edição pelo admin da agência; RN-009 estendida à tabela nova.
Como verificar (teste primeiro): gravar um valor fora dos dois e ver o banco recusar; deixar nulo e ver aceito; trocar o modelo de um cliente e conferir que nenhum snapshot publicado dele mudou; conferir o negativo cross-tenant de leitura e de escrita.
Aceite (H1): Dado um cliente sem modelo de venda declarado, Quando o admin declara consultiva, Então o valor é gravado no perfil do cliente, E um valor fora dos dois é recusado pelo banco, E nulo continua sendo estado válido, E trocar o modelo depois não altera nenhum relatório já publicado, E o perfil de um cliente de outra agência não é legível nem gravável.
RF-REN-016, uma estrutura, dois vocabulários.
O que implementar: perfil canônico em funnel_simple acionado por sale_model; quatro etapas na ordem Atenção, Interesse, Consideração e Ação; rótulos vindos da tabela do Ap.10.1 e nunca digitados; sale_model copiado do cadastro no ato da composição para o bloco, tornando o snapshot auto-suficiente.
Como verificar (teste primeiro): renderizar o mesmo relatório para um cliente consultiva e um direta e conferir que as quatro etapas e a ordem são idênticas e só os rótulos mudam; renderizar um cliente SEM modelo declarado e conferir que o funil livre segue funcionando com o número de etapas que o operador montou (controle negativo de regressão, é o comportamento que existe hoje e não pode sumir); publicar um snapshot, trocar o modelo do cliente e conferir que o rótulo publicado não mudou; conferir que o registro de tipos de bloco continua com dezesseis.
Aceite (H1): Dado um cliente com modelo de venda direta, Quando o funil canônico dele renderiza, Então aparecem exatamente quatro etapas na ordem Atenção, Interesse, Consideração e Ação com os rótulos diretos do Ap.10, E o mesmo funil para um cliente consultiva tem as mesmas quatro etapas na mesma ordem com os rótulos consultivos, E um cliente sem modelo declarado renderiza o funil livre com o número de etapas que o operador montou, E o rótulo de um snapshot publicado não muda quando o modelo do cliente é alterado depois, E o registro de tipos continua com os dezesseis do Ap.3.
RF-CMP-030, três números por etapa, e a primeira sem taxa por construção.
O que implementar: campos unit_cost e conv_from_prev em funnel_simple, ambos com ponto de dado; a taxa é derived com fórmula e operandos declarados; etapa sem volume medido vira lacuna, renderiza tracejada e suprime a taxa da etapa seguinte.
Como verificar (teste primeiro): com as quatro etapas medidas, conferir os três números em cada uma e a ausência de taxa na primeira SEM entrada no bloco de lacunas; apagar o volume da segunda etapa e conferir que ela sai tracejada, que a taxa da terceira não é emitida, que os dois motivos entram no bloco de lacunas, e que em nenhum ponto aparece zero.
Aceite (H1): Dado um funil canônico com as quatro etapas medidas, Quando o relatório renderiza, Então cada etapa mostra volume, custo unitário e taxa da etapa anterior, E a primeira etapa mostra volume e custo unitário sem taxa e essa ausência não entra no bloco de lacunas, E cada taxa é um ponto de dado derivado com fórmula e operandos declarados, E com o volume da segunda etapa ausente a segunda sai tracejada, a taxa da terceira não é emitida e os dois motivos aparecem no bloco de lacunas.
RF-ING-008, o fee não é uma meta e tratá-lo como meta produz uma comparação sem sentido.
O que implementar: tabela business_parameter com vigência; importação pelo MESMO sandbox de parse da importação de metas ({F11-02}), sem segundo caminho de upload e sem chamada a modelo de linguagem; disponibilização como ponto de dado com proveniência de importação; congelamento no snapshot como já ocorre com as metas (RN-008).
Como verificar (teste primeiro): importar fee, margem de contribuição e multiplicador e conferir os três como pontos de dado com proveniência; conferir que nenhum deles aparece na leitura de planejado contra realizado; publicar e conferir que o snapshot congelou os valores vigentes; mudar um parâmetro e conferir que o snapshot publicado não mudou; conferir que a importação não criou monitoramento contínuo.
Aceite (H1): Dado um arquivo de parâmetros importado com fee, margem de contribuição e multiplicador vigentes, Quando o operador compõe um relatório do cliente, Então os três ficam disponíveis como pontos de dado com proveniência de importação, E nenhum deles aparece na leitura de planejado contra realizado, E o snapshot congela os valores vigentes na data do publish, E importar um parâmetro não cria monitoramento contínuo.
RF-CMP-031, é o par de {F20-01}: aquele cartão endurece o catálogo, este constrói em cima dele.
O que implementar: as métricas emitíveis do Ap.10 emitidas como stat_cards a partir do catálogo; as derivadas como ponto de dado derived com fórmula e operandos; gate que recusa métrica de resultado fora do catálogo; métrica cujo operando não foi importado sai como lacuna declarada e nunca como zero; nenhum campo digitável de métrica de resultado no compositor.
Como verificar (teste primeiro): compor um relatório com os três parâmetros importados e conferir que toda métrica emitida sai com proveniência e que as não emitíveis saem como lacuna declarada; remover a importação do fee e conferir que as métricas dependentes dele saem como lacuna declarada e que NENHUMA delas sai como zero (controle negativo); tentar exibir uma métrica de resultado fora do catálogo e conferir que o publish reprova.
Aceite (H1): Dado um relatório cuja camada de resultado exibe métricas do Ap.10, Quando o gate de publicação roda, Então cada métrica exibida resolve para uma entrada do Ap.10 com origem declarada, E uma métrica fora do catálogo reprova o publish, E uma métrica cujo operando de negócio não foi importado sai como lacuna declarada e não como número, E nenhum operando faltante é substituído por zero.
RF-REN-017, vazamento comercial que nenhum gate de honestidade pega, porque o número é verdadeiro.
O que implementar: visibilidade por métrica no catálogo, valor fechado de dois, com default interna para fee, margem e lucros; o corte acontece no PUBLISH, removendo a métrica do conteúdo do snapshot, e não apenas ocultando no render; afrouxamento só por parâmetro de agência e gesto do admin.
Como verificar (teste primeiro): publicar um relatório que exibe fee e lucro operacional ao operador e conferir, no conteúdo bruto do snapshot gravado no banco, que nenhuma das duas está lá; buscar as duas no HTML servido da página pública e nos metadados e conferir zero ocorrências; conferir que as duas seguem visíveis ao operador no compositor; marcar uma como visível ao cliente e conferir que ela passa a entrar nos snapshots publicados a partir dali e que nenhum snapshot anterior mudou.
Aceite (H1): Dado um relatório cuja camada de resultado exibe ao operador o fee da agência e o lucro operacional, Quando o operador publica, Então nenhuma das duas está presente no conteúdo do snapshot, E a página pública não as exibe nem no HTML nem em metadado, E as duas continuam visíveis ao operador e ao admin no compositor, E marcar uma como visível ao cliente passa a incluí-la nos publishes seguintes sem alterar nenhum snapshot anterior.
FECHADO em 2026-08-03. O aviso agora NOMEIA as séries em texto, na mesma caixa vermelha dos demais erros de composição, e marca o bloco na lista lateral. O contorno tracejado antigo foi preservado e não substituído, o que importa porque ele já era o sinal que alguém podia estar usando (a mudança acrescenta, não troca). Provado em navegador real com controle negativo dos dois lados.
Achado que a correção revelou, e ele é da família mais frequente desta sessão: a detecção já existia no backend e a API já devolvia os avisos. Faltava o painel consumir. Não era regra ausente nem cálculo errado, era resultado pronto que não chegava aos olhos de ninguém, exatamente como as quatro peças da onda de blocos e o aviso de falha da composição.
Motivo de abertura original, preservado como histórico. O sinal EXISTIA e FUNCIONAVA, e mesmo assim não avisava. Quando o gráfico tem conflito de eixo, o bloco recebe um contorno tracejado fino e nada mais: sem texto, sem dica ao passar o mouse, sem marca na lista de blocos. O desenho está lá e é tecnicamente correto.
A régua que condena isto é o próprio produto. Todo o resto do erro de composição (número sem proveniência, falha de honestidade) aparece numa caixa vermelha com texto dizendo o quê. O conflito de eixo é a exceção silenciosa dentro de um padrão de aviso já estabelecido, e é essa comparação que define o defeito: não é que falte um aviso ideal, é que este caso foge do jeito que a casa já avisa. Um operador pode publicar sem notar.
Relação com a regra da casa, e ela aqui se inverte. A regra diz que problema visível se corrige e não se explica em banner. Este é o caso oposto: existe um problema que nem sequer se anuncia. Corrigir aqui é fazer o sistema dizer o que ele já sabe, não escrever um banner para esconder defeito.
Como verificar (teste primeiro): compor um relatório com conflito de eixo e conferir que o aviso aparece com TEXTO que nomeia o problema, no mesmo padrão visual dos demais erros de composição; conferir que o bloco fica marcado na lista de blocos; conferir que o operador que não passa o mouse em lugar nenhum ainda assim vê o aviso.
Aceite (H1): Dado um documento com conflito de eixo em um gráfico, Quando o operador abre o compositor, Então existe um aviso com texto que NOMEIA o problema, E ele segue o mesmo padrão visual dos demais erros de composição, E o bloco afetado aparece marcado na lista de blocos, E o aviso é perceptível sem depender de passar o mouse nem de reparar num contorno.
FECHADO em 2026-08-04, e a decisão que o destravou foi de mecanismo, não de produto. O cartão estava travado como decisão de produto em aberto. A decisão tomada: reusar o mecanismo que já existe (a inteligência artificial propõe onde o número está no artefato, o código relê e reconfere no dado cru, Ap.8) apontando-o para as chaves canônicas do funil, em vez de inventar um terceiro caminho de extração.
Por que essa decisão NÃO subiu para o dono, e isto fica rastreável de propósito: as chaves canônicas já tinham sido decididas por ele, na planilha de referência que originou o Ap.10. O que restava era mecanismo, e mecanismo é nosso. Levar ao dono uma pergunta cuja parte de produto ele já respondeu é devolver trabalho com outro nome.
Três propriedades provadas ao vivo, com inteligência artificial real e banco real, e são o que este cartão garante daqui em diante. (1) A camada de resultado NÃO ramifica por modelo de venda: o mesmo insumo nos dois modelos produz entrada resolvida idêntica, o que impede a duplicação que viraria drift entre fatias (RN-018 item 4 deixa de ser promessa e passa a ser propriedade medida). (2) Etapa que a inteligência artificial não encontrou sai marcada como AUSENTE, nunca como zero, porque zero seria mentira com aparência de dado (RF-CMP-030, RF-REN-015). (3) A distinção dos dois nulos vale no caminho real (RN-015): sem modelo de venda declarado nenhum bloco canônico nasce e a composição fica idêntica à anterior (regressão zero), e artefato ilegível recusa com motivo em vez de fabricar bloco.
Motivo de abertura original, preservado como histórico. Os construtores do funil canônico e da camada de resultado estão prontos e provados ({F20-03} a {F20-07}), e mesmo assim nada os aciona durante a composição do assistente. A causa é uma lacuna deste documento, não do código: o DRS não especifica como mapear um dado do artefato do cliente para as chaves fixas de operando (o volume de cada etapa, o custo unitário de cada etapa, o investimento total, as entradas do Ap.10). Sem esse mapeamento, o construtor não sabe de onde tirar os números.
A recusa de inventar o mapeamento foi deliberada e está certa: é a mesma disciplina que mantém 11 fórmulas do Ap.10 em a conferir em vez de preenchidas por dedução. Um mapeamento inventado produziria funil com número verdadeiro na casa errada, que é a classe de erro que ninguém reporta.
Caminho provável, apontado e NÃO decidido: o produto já tem o mecanismo de localizar dado em artefato, e ele é justamente o que este cartão precisa (a inteligência artificial propõe onde o número está, o código lê e reconfere no arquivo cru, Ap.8). O que falta é apontá-lo para as chaves canônicas do funil e do Ap.10, em vez de para os slots do modelo. Qual é a forma exata desse apontamento é decisão de produto e entra neste documento antes de virar código.
Como verificar (teste primeiro): compor um relatório de cliente com modelo de venda declarado e artefato real, e conferir que o funil canônico e a camada de resultado nascem preenchidos a partir do artefato, cada número com proveniência (Ap.8); conferir que um dado ausente no artefato vira lacuna declarada e nunca zero; conferir que nenhuma chave de operando é preenchida por dedução do modelo de linguagem sem reconferência no arquivo cru.
Aceite (H1): Dado um cliente com modelo de venda declarado e um artefato que contém os dados do funil, Quando o assistente compõe, Então o funil canônico e a camada de resultado nascem preenchidos com pontos de dado de proveniência verificável, E cada valor foi lido do arquivo cru e não afirmado pelo modelo, E um dado ausente sai como lacuna declarada e não como zero.
FECHADO em 2026-08-03, e era DEFEITO REAL, não cobertura faltando. O gate de proveniência (RF-CMP-007) não enxergava os itens do bloco de plano de ação: um número cru ou uma referência quebrada dentro de um item publicava sem ser verificado. O gate existia, rodava e passava, porque não olhava para ali.
Provado ANTES de corrigir, que é o que separa correção de suposição: os testes negativos foram escritos primeiro e falharam contra o código não corrigido. Depois da correção, ligado ao caminho REAL de publicação e provado com requisição e banco reais, recusando com o código de conflito.
Família: é a mesma que este projeto já nomeou, o número verdadeiro na frase errada. O número dentro do item era verdadeiro ou inexistente; o que faltava era alguém conferir se ele tinha origem.
Aceite (H1) atendido: Dado um documento cujo plano de ação contém número sem proveniência ou referência quebrada, Quando o publish roda, Então ele é recusado pelo gate de proveniência, E a recusa acontece no caminho real de publicação e não apenas em teste de unidade, E um plano de ação íntegro publica normalmente.
FECHADO em 2026-08-03. O validador de tamanho e horizonte dos itens do plano de ação existia e era testado, e nenhum gate o chamava. Agora é chamado no gate de documento. É a sétima ocorrência da classe nesta sessão (peça pronta e não acionada), e a única diferença entre ela e um requisito não implementado é que esta dá a sensação de estar coberta, o que é pior.
FECHADO em 2026-10-16.
- Merge e deploy: pacote main 8c45d4b, Hub main 47343906. Mergecheck VERDE na rodada 2 (carimbo
249fd6d, pacote 2086/2086). A rodada 1 caiu no build-parity: um erro de tipo que o tsc --noEmit
não pega (regra nova no guardas.md). A migration 038 foi aplicada em produção ANTES do merge do
Hub, com backup em /root/f20-13-bkp/; ela só acrescenta, e o Hub velho não chama as funções novas.
API 15/15 e web publicado.
- Gates: review APPROVED; qa PASS.
- O que mudou: o casamento roda dentro da transação do publicar, logo depois de fn_publish_report.
1ª edição: identidade nova. A reescrita casou com similaridade 0,607. Republicar troca só os
avistamentos da própria edição, de forma idempotente. Tenant B não lê nem casa com A.
- Limiar: passou a ser o parâmetro recommendationMatchThreshold da agência (padrão 0,35, sem mudar
o valor). O TM edita; Workforce recebe 403.
- Baseline: em 6124618, o ledger ficava em 0/0.
- Perde: o publicar ganha um modo de falha (500 se o ledger falhar, com rollback completo). Item sem
owner/horizon é pulado e contado em skipped. O rollback precisa reverter o Hub ANTES do .down da 038.
- Nota (limite da esteira, sem dano): a guarda A do {INFRA-01} saiu com exit 0 contra produção
depois do merge (273/273), mas ela só confere db/migrations do HUB. As migrations do PACOTE (001 a
038) não deixam registro em control.applied_migrations, então nenhuma guarda mede se estão
aplicadas. A 038 foi conferida à mão: as 2 funções SECURITY DEFINER existem em reports. Isso só
vira card se alguma migration do pacote faltar ou divergir em produção (regra do item 6 do LOOP).
Nasceu do plano do {F20-10} em 2026-10-16. A função que decide se uma recomendação do plano de
ação é "a mesma" da edição anterior (com o limiar RECOMMENDATION_MATCH_THRESHOLD) existe no pacote,
mas hoje só os testes a chamam, e o Hub a lista como órfã (f8-17-known-orphans.ts:89/91). Por isso
reports.recommendation_occurrence não ganha linha em publicação nenhuma. A identidade estável de
recomendação e o anti-fadiga ({F18-06}) não funcionam de verdade, e não existe dado real para calibrar
o {F20-10}.
Aceite (H1): Dado um cliente com uma edição publicada que tem plano de ação, Quando a edição
seguinte é publicada, Então cada recomendação dela é casada (ou não) com as da edição anterior pela
função do pacote, E o casamento gera ou atualiza a linha em recommendation_occurrence na mesma
transação do publicar, E a função sai da lista de órfãs do Hub. Isso entra antes do {F20-10}.
Nasceu do plano do {F20-13} em 2026-10-16. Com o {F20-13} no ar, a recomendação passa a ter
identidade e contagem de ocorrências entre edições, mas a página publicada ainda não usa essa contagem.
A regra do {F18-06} ("repetida ESCALA, não se reescreve") continua sem efeito visível para o cliente.
Aceite (H1): Dado uma recomendação casada em 3 edições seguidas do mesmo cliente, Quando a terceira é
publicada, Então a página mostra a escalada no padrão do {F18-06}, E a primeira e a segunda ocorrências
aparecem como antes. Depende do {F20-13}.
DEPENDE DE {F20-13} + 2 EDIÇÕES REAIS SEGUIDAS DE UM CLIENTE COM PLANO DE AÇÃO.
BLOQUEADO POR FALTA DE DADO REAL em 2026-10-16 (plano em .claude/gates/plan/f20-10.gate, cuja
primeira linha é BLOCKED). O limiar em questão é RECOMMENDATION_MATCH_THRESHOLD = 0.35
(src/anti-fatigue.mjs:545), tirado de uma sonda sintética de 3 pares. Não confundir com o
SIMILARITY_THRESHOLD = 0.5, que já foi calibrado no golden set. Hoje nada em produção usa esse
limiar: só os testes chamam a função, e o Hub a lista como órfã. Em produção há zero relatórios,
recomendações e ocorrências. O golden set tem 7 documentos e nenhum bloco de plano de ação.
Correção do aceite: um documento real sozinho não calibra nada. É preciso a MESMA recomendação
reescrita em 2 edições seguidas do mesmo cliente, com cada par marcado por uma pessoa como "mesma" ou
"diferente" (no mínimo 10 de cada).
O que destrava, em ordem:
1. Ligar o match de recomendação no caminho real de publicação (card próprio).
2. Ter 2 edições reais seguidas de um cliente com plano de ação.
3. Rotular os pares.
Até lá, o card não sai da fila, e ninguém escolhe outro número sintético.
ABERTO, e é FILA NOSSA com dono e ordem, não pendência do dono do produto. O limiar que decide se duas recomendações são "a mesma" ({F18-06}) está calibrado por sonda sintética. Calibrá-lo de verdade exige um conjunto de dados real que contenha bloco de plano de ação, e hoje nenhum conjunto real tem esse bloco, porque ele nasceu nesta sessão. A ordem é essa: primeiro um relatório real com plano de ação entra no conjunto de referência, depois o limiar se calibra contra ele.
Aceite (H1): Dado um conjunto de referência que contenha ao menos um documento real com bloco de plano de ação, Quando o limiar é calibrado contra ele, Então o limiar deixa de ser sintético e o valor calibrado fica registrado com a amostra que o sustenta, E o {F18-06} passa a declarar o limite como resolvido em vez de declarado.
A família fechou INTEIRA em 2026-08-04, e a classificação dos quatro é o achado. Dois dos quatro já existiam prontos e só não estavam ligados: o log de auditoria já era rota genérica funcionando e faltava a tela chamar, e a tela de metas tinha o backend completo desde uma fatia anterior. Os outros dois foram construídos de fato. Ou seja, metade do que parecia trabalho pendente era trabalho já feito e inalcançável, que é exatamente a assinatura desta família e o motivo de ela merecer desenho e não cartão avulso. Nota de divergência registrada: a mensagem de commit da entrega agrupa TRÊS peças como já construídas, e a classificação recebida diz duas; a diferença não muda o fechamento de nenhum cartão e fica anotada em vez de arbitrada, porque nenhuma das duas contagens foi medida contra o código peça a peça.
A família "peça pronta que o operador não consegue acionar" teve QUATRO membros, e quatro casos é desenho, não cartão avulso. São eles: {F10-05} (contagem de uso na biblioteca de modelos), {F10-06} (leitura do log de auditoria de promoção exposta na tela), {F11-03} (metas importáveis sem tela que as importe ou liste) e este {F20-08} (funil e camada de resultado construídos sem gatilho que os acione). O padrão comum não é preguiça nem esquecimento: em cada caso a capacidade foi construída e provada por baixo, e a superfície que a torna alcançável ficou para depois, porque a prova de que funciona não exige a superfície. A consequência é que o sistema tem mais capacidade do que o operador consegue usar, e a diferença não aparece em teste nenhum, porque os testes exercitam a capacidade e não o alcance dela. Quatro ocorrências pedem uma decisão de desenho sobre quando a superfície entra, e não mais um cartão por caso.
Definição de Pronto F20: ATENDIDA, fase FECHADA em 2026-08-03. O catálogo do Ap.10 conferido campo a campo contra a planilha de gestão, com as 20 entradas classificadas e as 15 derivadas em estado conferida, e a leitura do Ap.10.3 (retorno sobre anúncios positivo pode conviver com prejuízo, LTV mínimo de 3 vezes o CAC, sinal do ROMI, unidade do CAC Payback em ciclos, LTV multiplicador distinto do valor em dinheiro) refletida nos rótulos; o modelo de venda vivendo no perfil do cliente com dois valores fechados e nulo permitido; o funil canônico de quatro etapas rendendo os dois vocabulários sobre a MESMA estrutura, com o funil livre preservado para cliente sem modelo declarado; a tripla por etapa completa, com a primeira etapa sem taxa por construção e sem entrar em lacuna; os parâmetros de negócio entrando pelo mesmo caminho de importação das metas e separados delas por conceito; toda métrica de resultado exibida com origem declarada e nenhuma saindo como zero por operando faltante; e as métricas comerciais da agência fora do snapshot publicado por default.
Onda DEF, Defeitos achados fora da obra (2026-08-11, fora da fila padrão)
Origem: varredura de erros de produção conduzida por outra frente em 2026-08-11, que estava investigando o registro de ocorrências e esbarrou nestes dois defeitos do módulo de relatórios. Nada aqui foi corrigido por quem achou: o arquivo é território da frente que constrói este módulo, e mexer nele em paralelo criaria colisão. O registro É a entrega, e a fila decide quando pegar. Prioridade: NENHUMA declarada. Os dois entram depois da fila vigente, salvo decisão em contrário de quem toca o módulo. O {DEF-01} tem mecanismo comprovado e nenhuma manifestação medida em produção; o {DEF-02} não quebra tela nenhuma, corrompe contagem. Medido emmainf4037d8. Este HEAD é mais novo que o5eb7575de outras notas deste dia, porque a frente que constrói o módulo avançou entre uma medição e outra.
FECHADO em 2026-10-16: Hub main a03cb25, carimbo 749d731. API 15/15 e web publicado. Review
APPROVED e qa PASS.
Medido de novo na main de hoje: dos 46 handlers, 2 tinham o defeito, não 1.
- Publicar. O QR era gerado depois do commit e, se falhasse, a rota devolvia 500.
- Eliminação LGPD. Entrou depois de agosto: os arquivos são apagados depois do commit, e uma exceção
virava 500 genérico com as tabelas já eliminadas.
Correção: um helper único para efeito pós-commit, que registra a linha JSON efeito_pos_compromisso_falhou.
- Publicar. Responde 200 com
side_effect_warnings. O modal mostra "Publicado. O QR não pode ser
gerado." e o botão "Gerar QR de novo", que chama GET /:id/share-qr. Esse endpoint devolve 409 em rascunho, 403 sem a permissão de publicar e 404 para outra agência.
- LGPD. Devolve
LGPD_ARQUIVOS_PARCIAL; a nova tentativa conclui e respeita a posse.
Controle negativo: falha dentro da transação continua 500 e não grava nada. Baseline em 3cd18a3: a rota dava 500 com o relatório já publicado. O que perde: REPORT_PUBLISH_FAILED só cobre falha dentro da transação (o texto mudou para "nada foi publicado"). RF-PUB-011 (NOVO, publicado no DRS em 2026-08-11, módulo reports-publish). Card PAR: nenhum requisito vigente reprovava o desfecho, então ele nasce junto do que passa a reprovar, nunca como "exige RF novo" solto. O defeito em uma frase: a publicação confirma a transação e só DEPOIS gera a imagem do código de acesso; se essa geração falhar, o operador recebe erro de uma operação que aconteceu. O mecanismo, lido por inteiro no código: o handler de publicação abre o bloco protegido em apps/api/src/routes/reports.ts:3624; a confirmação da transação acontece no RETORNO do callback (apps/api/src/lib/db.ts:47 confirma assim que o callback devolve, e o callback devolve em reports.ts:4307), de modo que quando a linha reports.ts:4367 gera a imagem do código de acesso o relatório JÁ está publicado, o instantâneo JÁ está gravado e o evento de auditoria JÁ foi registrado. O tratamento de falha em reports.ts:4376-4377 devolve o erro genérico da publicação, situação 500. O resultado é um falso negativo: a resposta afirma o contrário do que o sistema fez. CORREÇÃO DE PREMISSA, e ela ameniza o defeito (o card não pode exagerar o próprio caso): a suposição de que a segunda tentativa seria recusada com "não está mais em rascunho" é FALSA. Lida por inteiro a função de publicação no banco, ela é IDEMPOTENTE por desenho: com o relatório fora de rascunho ela audita como republicação e refaz o instantâneo e a situação, então uma segunda tentativa CONCLUI e desta vez devolve a imagem normalmente. A única guarda dura do handler é a do relatório revogado (reports.ts:3631, situação 409). Ou seja, o dano é transitório e se cura sozinho no retentar, e isso é consequência direta do RF-PUB-009 (republicar reusa o mesmo link). O que sobra, e continua sendo defeito, é a mentira da primeira resposta: o operador que acredita nela e NÃO tenta de novo fica achando que não publicou um relatório que está no ar. Contagem (o conjunto FECHA, com limite declarado):
- Universo: T = 46 handlers mutantes (não de leitura) do arquivo de rotas do módulo que abrem transação pelo padrão direto, levantados listando os handlers com transação (meio diferente do critério, que classifica lendo onde cada efeito colateral roda).
- Critério (COMPORTAMENTAL, nunca busca textual como prova final): o handler executa algum efeito colateral DEPOIS da confirmação da transação e ainda dentro do bloco cujo tratamento devolve erro 5xx da operação?
- Placar: seguros N = 45 / com efeito colateral pós-confirmação dentro do bloco M = 1 / N + M = T = 46. Fecha.
- Lado negativo, lido por inteiro: o lado M tem UM item só, e o card diz isso em vez de fingir um segundo. É
reports.ts:4367, a geração da imagem do código de acesso na publicação. Como controle, foi aberto por inteiro um handler do lado N que parecia arriscado, a regeneração de bloco por inteligência artificial (reports.ts:3272-3369): nele a chamada externa acontece ANTES da transação que grava, com releitura travada depois, e o comentário do próprio código declara esse desenho. Os demais candidatos de risco (envio de arquivo e mídia de bloco) também executam o efeito externo antes de abrir a transação de escrita, conferido lendo os dois. - LIMITE DECLARADO da contagem: o universo cobre os handlers que usam o padrão direto de transação. O handler de composição usa mecanismo próprio, numa biblioteca separada, e não foi contado nem classificado: se alguém quiser T incluindo ele, é uma passada própria naquela biblioteca. Além disso, dos 46 não foram abertos todos um a um: foram abertos os que a varredura sinalizou com chamada assíncrona fora da transação e os que os comentários do código já apontavam como historicamente arriscados. O placar é honesto sobre o método, e é por isso que ele está escrito aqui.
- Universo é 1 agência?: não se aplica, é código.
- Evidência de: 2026-08-11,
mainf4037d8. - Ocorrências em produção: ZERO comprovadas, e este card NÃO reivindica a que existe. Há exatamente 1 ocorrência do código de falha de publicação no registro, em 2026-08-08 às 22:13:42. Ela foi investigada e NÃO é este defeito: a busca por evento de publicação ou republicação na trilha de auditoria entre 22:10 e 22:20 daquele dia, para qualquer agência, devolveu 0 linhas, o que significa que a transação daquele caso NÃO foi confirmada e a falha aconteceu ANTES ou DENTRO dela, não depois. A única publicação daquela agência naquele dia foi às 22:21:18, oito minutos depois e sem falha associada. Portanto: mecanismo comprovado por leitura de código, manifestação medida em produção igual a zero. A causa daquela ocorrência de 2026-08-08 segue desconhecida e é assunto separado. Não foi possível medir "publicado sem imagem de código de acesso" por outro caminho porque a imagem nunca é persistida (não existe coluna para ela, ela só vai na resposta), e a checagem pela trilha de auditoria é o substituto válido.
- Reprova por: nenhum RF vigente reprova. O RF-PUB-005 exige que a imagem do código de acesso seja exibida ao abrir o modal de publicação; ele nada diz sobre o que acontece quando a geração falha, então o falso negativo passa nele sem arranhão. Não há requisito não funcional de atomicidade que alcance o caso. A partir da publicação, reprova o RF-PUB-011, H1 "Então a resposta ao operador declara o estado VERDADEIRO do sistema e nomeia em separado o que não pôde ser feito, E nenhum operador recebe falha de uma operação cujo compromisso foi confirmado".
Direção da correção (sem prescrever implementação, a decisão é de quem toca o módulo): há duas saídas legítimas e o requisito aceita as duas. Ou o efeito colateral entra no MESMO compromisso da publicação, e aí uma falha dele derruba tudo junto e a resposta de erro volta a ser verdadeira; ou a falha dele deixa de ser erro da operação e vira aviso que declara o estado verdadeiro, algo como "publicado, mas o código de acesso não pôde ser gerado", com o caminho para tentar gerar de novo. A segunda saída depende de julgar a imagem do código de acesso ACESSÓRIA à publicação, e o H1 proíbe explicitamente rebaixar a aviso um efeito colateral que seja indispensável, justamente para que a correção não vire "engolir a falha em silêncio". Essa classificação é decisão de produto de quem toca o módulo, não deste card. Como verificar (teste primeiro): forçar a falha do efeito colateral posterior (por exemplo tornando inválida a entrada da geração da imagem) e publicar: a resposta ao operador NÃO pode ser o erro genérico da publicação, e o que a tela afirma tem que bater com o que ficou gravado no banco (relatório publicado, instantâneo gravado, evento auditado); repetir com uma falha DENTRO da transação e conferir que aí sim a resposta é de erro e nada ficou gravado (este é o controle negativo, e sem ele a correção poderia ter engolido também as falhas reais); e conferir que o caminho normal de publicação continua devolvendo a imagem do código de acesso, que é o RF-PUB-005. Aceite (H1): Dado uma operação cuja transação já foi confirmada, Quando um efeito colateral executado depois dela falha, Então a resposta declara o estado verdadeiro do sistema e nomeia em separado o que não pôde ser feito, E a resposta nunca é o erro genérico da operação inteira, E nenhum operador recebe falha de uma operação cujo compromisso foi confirmado, E o efeito colateral indispensável ao sucesso passa a ser executado dentro do compromisso.
FECHADO em 2026-10-16: Hub main c94c9068, carimbo 2970fce, API 15/15 e web publicado. Review
APPROVED na rodada 2. qa PASS na rodada 2; a rodada 1 reprovou o item do Portal.
Reconciliação do card (a direção acima mudou):
- Remedido na main de hoje, são TRÊS chamadas, não duas: Compositor:466 (relatório + prévia) e o
efeito dos comentários do {F8-46}. O total bruto era T=173 (cerca de 75 visitas reais).
- "Nada muda no servidor" não se sustentou: só o cliente não consegue contar uma falha uma vez sem
serializar o sucesso.
O que mudou:
- A tela declara o grupo de carga em x-mhub-load-group. O gravador (error-events.ts) conta 1 por
(grupo, código, agência, ator).
- Só GET 404 é suprimível. 403, 422 e escrita continuam gravando sempre, para o cliente não conseguir
esconder os próprios erros.
- O sucesso continua paralelo, provado sem relógio.
- Prova: o compositor grava 1 com o cabeçalho e 3 sem ele; a main grava 3.
Fora do escopo, no {DEF-02b}: o Portal, cuja falha em par é 403 PORTAL_REQUIRES_CONTACT_SESSION.
Perde: prévia e comentários com 404 dentro da mesma visita deixam de aparecer na Central (o /:id
já conta). Um cabeçalho forjado esconde até 60 s só os GET 404 do próprio ator.
RF-CMP-032 (NOVO, publicado no DRS em 2026-08-11, módulo reports-compose). Card PAR. Achado menor em impacto de tela e relevante em impacto de MEDIÇÃO.
O defeito, medido: o compositor busca o relatório e a pré-visualização EM PARALELO, num único disparo (apps/web/src/routes/reports/ReportsCompositorPage.tsx:313, as duas chamadas na mesma espera conjunta, acionada uma vez na montagem da tela). Os dois endereços respondem a ausência do recurso de forma independente, cada um deixando a sua ocorrência. Uma visita humana, dois registros. A tela não quebra, o operador não sofre; quem sofre é qualquer contagem feita depois.
Contagem (o conjunto FECHA, e os pares NÃO batem, o que é informação e não ruído):
- Universo: T = 70 ocorrências brutas registradas nos dois endereços do relatório e da pré-visualização, somadas na tabela de ocorrências (meio diferente do critério, que pareia por dia).
- Critério (COMPORTAMENTAL): a ocorrência tem par no mesmo dia no outro endereço, isto é, ela veio de uma visita ao compositor, que dispara as duas chamadas juntas?
- Placar: pareadas (30 visitas duplicadas gerando 2 linhas cada) N = 60 / não pareadas M = 10 / N + M = T = 70. Fecha. Eventos humanos reais estimados: 40, contra 70 brutos, ou seja, a contagem bruta está inflada em 75%.
- Por que os pares NÃO batem exatamente, e a explicação foi medida, não suposta: as contagens por dia são 3 e 2 em 08/08, 16 e 12 em 09/08, 13 e 12 em 10/08, 7 e 4 em 11/08, mais 1 sozinho em 02/08. O endereço do relatório aparece sempre a mais. A causa é um SEGUNDO chamador não pareado: apps/web/src/routes/reports/ReportsNewPage.tsx:468 chama apenas a busca do relatório, sem a pré-visualização, para reidratar uma etapa do assistente. É ele que infla um lado. O critério de pareamento usado é o piso diário entre os dois endereços, e o excedente do lado do relatório NÃO é duplicação, é esse outro caminho de chamada.
- Lado negativo, lido por inteiro: (1) ReportsCompositorPage.tsx:313, as duas chamadas na mesma espera conjunta, e a função que as dispara é acionada uma única vez na montagem; (2) ReportsNewPage.tsx:468, a chamada solitária que explica o desbalanço e que NÃO deve ser tratada como defeito, porque ali só existe uma chamada por evento.
- Varredura do mesmo padrão em outros códigos de erro do módulo: foram abertos os demais disparos paralelos das telas de relatórios, 5 ao todo, e nenhum reproduz o padrão. Um deles trata a falha de cada chamada separadamente, e os outros chamam endereços de semânticas diferentes, que não são duas visões do MESMO recurso. Então o defeito é 1 ponto, não uma família.
- Universo é 1 agência?: não, a tabela agrega por agência e o levantamento somou todas.
- Evidência de: 2026-08-11, main f4037d8, código lido e tabela de ocorrências consultada.
- Reprova por: nenhum RF vigente reprova. Nenhum requisito do compositor fala do RASTRO que uma navegação deixa; eles falam do que a tela faz e do que ela grava no rascunho. A partir da publicação, reprova o RF-CMP-032, H1 "Então o número de ocorrências registradas para aquele evento é UM, E não uma por chamada disparada pela tela".
Direção da correção (a decisão de quando é da fila): a correção é no cliente e SÓ no caminho de FALHA: encadear as duas chamadas, ou reconhecer que a segunda é redundante quando a primeira já falhou. O caminho de SUCESSO deve continuar paralelo, porque é ele que faz a tela abrir rápido, e serializá-lo seria pagar desempenho para consertar contagem. Nada muda no servidor: os dois endereços respondendo a ausência de forma independente está correto.
Cruzamento que torna isto mais do que cosmético: qualquer limiar de volume ou placar que conte ocorrências deste módulo está lendo o número inflado. Quem for ajustar um limiar antes desta correção precisa saber que 70 aqui são 40.
Como verificar (teste primeiro): abrir UMA vez, num navegador limpo, o compositor num endereço que não resolve, e conferir na tabela de ocorrências que nasceu UMA linha nova, não duas; conferir que a mensagem e o caminho de saída da tela não mudaram para o operador; abrir uma vez o compositor num endereço que resolve e conferir que o carregamento continua paralelo e o tempo de abertura não piorou (este é o controle negativo do desempenho); e recontar os dois endereços por dia, com a expectativa de que o piso diário deixe de existir porque não há mais par.
Aceite (H1): Dado um operador que abre uma vez uma tela do compositor cujo recurso não resolve, Quando a tela termina de carregar, Então o número de ocorrências registradas para aquele evento é um, E não uma por chamada disparada pela tela, E a mensagem e o caminho de saída da tela não mudaram, E uma tela que precise mesmo de duas chamadas independentes declara isso e continua registrando um evento.
Resto aberto do {DEF-02}, saiu do escopo em 2026-10-16. O qa do {DEF-02} mediu que uma visita ao
Portal (ReportsPortalPage, por volta da linha 82) dispara 2 leituras em paralelo. Sem sessão de
contato, as duas falham com 403 PORTAL_REQUIRES_CONTACT_SESSION e cada uma grava a sua ocorrência:
2 por visita, com ou sem cabeçalho, igual à main. A contagem dupla CONTINUA. Ela saiu do {DEF-02} porque
a solução dele (grupo de carga declarado pelo cliente no cabeçalho x-mhub-load-group) só pode suprimir
GET 404. Deixar um cabeçalho vindo do cliente esconder um 403 seria a brecha que o review fechou: o
usuário apagaria os próprios erros de permissão. A mudança que tinha ficado sem efeito no Portal foi
retirada da branch.
Direção (a decidir no plano): a correção não pode depender de nada que o cliente declare. Dois
caminhos: (a) o cliente encadeia as 2 chamadas SÓ quando a primeira falha por sessão (o sucesso continua
paralelo); (b) o servidor deduplica o 403 de sessão por uma chave que ele mesmo deriva (token de link +
janela curta), nunca pelo cabeçalho.
Aceite (H1): Dado um visitante sem sessão de contato, Quando ele abre o Portal uma vez, Então nasce 1
ocorrência de PORTAL_REQUIRES_CONTACT_SESSION, E não 2, E nenhum 403 de outro código ou de outra rota
passa a ser suprimido, E o sucesso do Portal continua paralelo.
FECHADO em 2026-10-16: pacote main 80d587f (merge 80d587f, junto do {F18-11}, mesmo
mergecheck VERDE). Os 2 testes deixaram o relógio e passaram a contar o pico de passadas abertas
(3 em paralelo, 1 em série), com mock.timers no espaçamento de largada. Contra a versão em série
de f63fdfe, e contra uma versão serial escrita pelo QA, os 2 ficam vermelhos. Sob carga com
nice -n 19 passaram 20/20 e 10/10. Perde: a prova em milissegundos de que o tempo cai. Os outros
testes de relógio (2 no pacote, 5 no Hub) têm folga larga e nunca falharam; ficam fora, e se algum
falhar vira card próprio com a mesma receita.
Nasceu do qa do {F18-11} em 2026-10-16. Suíte inteira do pacote no clone do mhub-db-test: 1922
de 1924. As 2 falhas são testes de TEMPO em test/reading-dependency-map.test.mjs ({F18-15}), com a
VPS em load 4-5. Rodado de novo, falhou 2 de 3 vezes com nice -n 19 e passou 14 de 14 sem nice.
Não foi causado pelo {F18-11}, que não toca esse arquivo. Isso pesa porque o mergecheck roda o
test-all com nice -n 19 e numa máquina compartilhada, então esse teste pode reprovar qualquer merge
do pacote por um motivo que não tem a ver com o card.
Aceite (H1): Dado o test-all rodando com nice -n 19 e a VPS com load acima de 4, Quando a suíte do
pacote roda, Então os testes do {F18-15} passam de forma determinística, E a garantia que eles davam
(custo ou ordem do mapa de dependências) continua reprovando quando a regressão é real. Afrouxar o
limiar de tempo até ele parar de falhar NÃO serve, porque aí ele não reprova mais nada.
Definição de pronto da onda: os 2 cards [x] com gates verdes por fatia; o falso negativo reproduzido e provado corrigido, com o controle negativo da falha dentro da transação também provado; a contagem do compositor recontada e o par diário desaparecido; e DRS, roadmap e Plano de Testes republicados sem divergência.
Onda OP, Defeitos que só aparecem USANDO o produto (2026-08-17/18)
Origem, e ela é o ponto: o dono do produto abriu a ferramenta e ela não funcionava. "vc implementou, implementou e a ferramenta não funciona. Faça um teste de uso dela e vai ver que trava em várias etapas e simplesmente não tem como nem testar." Os cartões abaixo nasceram de percorrer o wizard como operador em produção, nas visões de account manager e de gestor de tráfego, com tipos e períodos variados - não de leitura de código, não de gate. Por que a obra inteira estava verde com o produto travado: todos os gates mediam o que se mandou medir.plannerplaneja o objetivo,qatesta os critérios de aceite (= o objetivo),code-reviewerlê o diff (= o que mudou, não o que falta ligar) e o smoke olha a tela nova. Ninguém percorria o fluxo inteiro de ponta a ponta com dado de verdade. Onze cartões, e nenhum deles exigiu feature nova - todos eram peça pronta que não se ligava, guarda boa demais que reprovava o caso normal, ou constante espelhando um valor que mudou do outro lado. Todos[x]: corrigidos, com teste que impede a volta, mergeados emmainnos dois repos e deployados. O denominador subir aqui é o processo funcionando: nenhum cartão fechado reabriu.
**/relatorios mostrava "Nenhum relatório publicado ainda" e prometia que "a listagem de
rascunhos e a composição completa chegam nos próximos passos do roadmap"** - enquanto o painel de
Analytics da MESMA agência, no mesmo instante, contava 4 publicados. Era o placeholder de
{F7-01}, escrito quando ainda não havia endpoint de coleção, e ficou no ar depois que a feature
inteira entrou. Dois defeitos num: a tela afirmava o contrário do estado real, e anunciava como
futuro o que já estava em produção - o banner que fix-not-banner proíbe.
O que entrou: a tela do mockup F.3 (relatorios.html) - 4 números, filtro por status e por
cliente, tabela com cliente, relatório, período, status, views, comentários e ação - mais
GET /reports/agency-reports, irmã de /accounts/:id/reports: mesma fonte de views
(v_honest_view_event, {F13-03}) e mesma ordem por período coberto, porque contar aqui de um
jeito e no analytics de outro faz o mesmo relatório exibir dois números.
Aceite (H1): Dada uma agência com relatórios, Quando o operador abre /relatorios, Então ele
vê a lista real, E falha de rede NÃO é exibida como "nenhum relatório" (são estados diferentes, e
confundi-los era o defeito).
"Total 24" convivendo com "Publicados 4" e "Rascunhos 15" - 19. Os 5 que faltavam tinham
situação revoked, um terceiro estado que a tela não conhecia: não entrava nos contadores, não
aparecia no filtro, e na tabela caía no published ? Publicado : Rascunho. Chamar de rascunho um
relatório que foi publicado e teve o link revogado não é imprecisão de rótulo: **afirma que ele
nunca saiu**.
A correção não foi acrescentar o terceiro caso ao ternário - foi parar de decidir por ternário.
Rótulo vem de mapa e situação fora do mapa aparece com o próprio nome do banco, então uma situação
nova nasce VISÍVEL em vez de virar rascunho em silêncio. Contadores viraram um balde por situação
presente, contados da própria lista: a soma bate com o total por construção.
Aceite (H1): Dada uma agência com relatórios em qualquer situação, Quando a home carrega,
Então a soma dos baldes é igual ao total, E nenhuma situação é exibida com o nome de outra.
Provado na tela: 4 + 15 + 5 = 24.
O passo 2 oferece Funil, Ranking, Retenção de vídeo e Planejado × realizado como blocos
preferidos, mas o montador só tinha componente dedicado para kpis e data_table: **todo o resto
caía num fallback stat_cards**. Os tipos existem no renderizador e no catálogo (Ap.3) desde
sempre - nunca eram emitidos. É a família "peça pronta e inalcançável", a mais cara deste projeto,
e atinge justamente o primeiro bloco que um gestor de tráfego pede.
Ordem faz parte do contrato dos dois, e por isso dataPoints entrou na assinatura do montador
(só para ordenar; nenhum valor entra no bloco, que segue carregando {dp:id}, Ap.8): um funil é
monotônico e um que sobe no meio lê como dado errado mesmo com cada número certo; um "ranking"
fora de ordem não é ranking.
Aceite (H1): Dado um briefing que pede Funil, Quando a composição roda, Então o documento tem
um bloco funnel_simple com as etapas em ordem decrescente, E nenhum stat_cards no lugar dele.
Provado na tela: 540.300 Impressões → 9.820 Cliques → 1.410 Carrinhos → 312 Compras.
O bloco dizia "0 bloco(s) esperado(s) não puderam ser calculados: nenhuma lacuna registrada" e,
na MESMA página, logo abaixo, um bloco gap listava "KPIs do período: dado esperado pela
intenção, mas ausente nos artefatos enviados". Havia duas fontes de lacuna - a da leitura
(parâmetro) e a que o próprio montador emite para slot sem dp - e só a primeira alimentava a
contagem.
**Os dois testes que afirmavam "gaps chega intacto" fixavam exatamente o comportamento que a
página real desmentiu** e foram reescritos com o motivo, mais o caso de não-duplicação e o de
documento sem lacuna nenhuma.
Aceite (H1): Dado um documento com lacuna emitida pelo montador, Quando o bloco de
confiabilidade é derivado, Então a contagem dele cobre essa lacuna, E lacuna já declarada pelo
chamador não é contada duas vezes.
A coluna roas do CSV traz "37.03" puro, sem símbolo, e a IA mandou type_hint: "percent". No
extrator o palpite da IA vence o dado (typeHint || inferType(...)), então a unidade exibida
virou uma afirmação do modelo sobre um dado que não a sustenta. Não é formatação: **"37,03%" e
"37,03x" são números diferentes** para quem lê, e o segundo é o que o dado diz.
O DRS já é explícito - "a IA nunca informa o valor de um número". A unidade é parte do valor
apresentado; deixá-la a cargo do palpite contradiz o mesmo princípio.
A correção usa a fonte que o produto já tem: a família da métrica, classificada por
vocabulário fechado, a MESMA que o gate de coerência audita depois. UNIT_BY_METRIC_FAMILY cobre
só as famílias em que a unidade é propriedade da MÉTRICA e não escolha de quem exporta (roas →
ratio, ctr → percent, frequência → number). Moeda ficou deliberadamente de fora: receita e
investimento podem vir em moeda ou em unidade, e o símbolo no próprio valor resolve.
Aceite (H1): Dado um dp cuja família tem unidade canônica, Quando o palpite da IA a contradiz,
Então a família vence, E o VALOR extraído nunca muda.
Um CSV impecável de 6 meses (8 linhas, 6 colunas) resolveu ZERO slots, e a tela exibiu **"O que
NÃO achei nos dados: Mensagem central, Leituras da IA, Tabela de dados, KPIs do período"**. Medido
no container: o modelo respondeu com o RACIOCÍNIO em texto (" Input: A CSV artifact… Goal:
Propose hypotheses…") em vez do JSON; JSON.parse falhou e todo slot virou lacuna.
Duas coisas erradas na mesma linha: desistir na primeira resposta malformada descarta o upload
inteiro por um erro que costuma não se repetir, e a lacuna resultante era indistinguível de dado
ausente - o arquivo do operador levava a culpa.
Entrou UMA segunda tentativa com a instrução reforçada; persistindo, a lacuna sai com
resposta_ia_invalida, e a tela passou a dizer "A leitura falhou - seus dados estão bem" com
o que fazer, em vez da lista de slots. Motivo fora do mapa cai no texto genérico: pior que o
específico, melhor que atribuir ao arquivo do operador uma falha que não é dele.
Aceite (H1): Dada uma resposta que não é JSON, Quando a leitura roda, Então há uma segunda
tentativa, E se ela também falhar a lacuna carrega o motivo real, E a tela nunca reporta falha de
leitura como dado ausente.
O achado de maior impacto da onda. Medido chamando o gateway de dentro do container de
produção: groq:llama-3.3-70b-versatile → 404, groq:llama-3.1-8b-instant → 404,
gemini-2.5-flash → 429, gemma-4-31b-it → 200. Os dois modelos Groq saíram do catálogo
(GET /openai/v1/models da própria chave não lista mais nenhum llama de chat) e ninguém percebeu,
porque o fallback é silencioso por desenho.
Toda leitura e toda narrativa do produto vinham do último elo da cadeia - e é ele que responde
em prosa em vez de JSON, porque o gemma não respeita responseMimeType. Isso explica o
comportamento errático que esta mesma onda já tinha "fechado" duas vezes atribuindo a outras
causas: não era aleatório, dependia de o gemini-2.5-flash ter cota naquele instante.
Os dois slots Groq passaram a apontar para modelos que existem hoje e suportam
response_format json_object; e expectJson força o formato no protocolo de cada provedor,
em vez de pedir no texto do prompt e torcer.
Aceite (H1): Dada a cadeia gratuita, Quando a leitura roda, Então ela responde no primeiro elo
com JSON válido. Provado: mesma leitura que falhava duas vezes seguidas passou a resolver 3 slots
e 8 data points, fallbackDepth 0, em 13s (era ~120s ou falha).
Fica aberto e é dívida real: a degradação da cadeia continua invisível - fallbackDepth é
registrado e ninguém o lê. Ver {F8-37}, que agora tem um caso medido.
A composição falhou com **"a geração automática deste relatório demorou mais do que o tempo
permitido"** num relatório de 8 linhas de CSV. O teto vem de PROXY_READ_TIMEOUT_MS = 60_000,
constante que ESPELHA o proxy_read_timeout do nginx e vive num arquivo que o nginx não lê nem
escreve. O comentário dela já avisava por escrito: "quem mudar o timeout do proxy sem olhar
aqui reabre o mesmo defeito". Foi exatamente o que aconteceu - horas antes, na mesma sessão, o
proxy de /api/reports/ subiu de 60s para 300s (para parar de devolver 504 na leitura de
artefato, que leva ~45s) e a constante ficou para trás.
Duplicar um valor que mora FORA dos dois repos é o defeito, não o número errado. Virou
REPORTS_PROXY_READ_TIMEOUT_MS, com default no valor antigo, valor de produção 300000, e a
regra escrita em deploy/mhub-api-run.md ao lado do bloco que descreve o proxy.
Aceite (H1): Dado o proxy configurado em N segundos, Quando a composição roda, Então o teto do
produto é N menos a margem, E o runbook obriga mudar os dois no mesmo deploy.
Nenhum relatório de fechamento mensal publicava. Medidos um a um, em publishes reais:
(a) o ano do período e o nome cadastrado do cliente ("Cliente Smoke ModularReports F8") - o
ano é a MESMA data que a lista já libera em outras grafias, e o dígito do nome tem origem
confirmada, o cadastro, que é exatamente o que este gate exige;
(b) data abreviada ("01 jan 2026") e numeração de seção ("### 1."), que a IA escreve em quase
todo texto longo;
(c) "taxa de conversão" classificada como total - a família não existia no vocabulário e
caía em conversions, a mesma de "conversões: 486", que é soma;
(d) a IA montando tabela markdown dentro da narrativa, o que introduzia "Campanha 1",
"Campanha 2".
O (d) não foi remendado com mais isenção: a tabela não deveria estar ali - existe o bloco
data_table, e tabela escrita à mão repete o mesmo número em dois lugares da página (o que
RN-012 existe para impedir). O prompt passou a pedir prosa e a dizer por quê.
O (c) ganhou rede que não depende de lista: RATE_METRIC_FAMILIES é inclusão nomeada, em que
só vira taxa a família que alguém lembrou de escrever; agora, se o valor resolvido é percentual ou
razão, aquilo é taxa, qualquer que seja a família.
Aceite (H1): Dado um relatório de fechamento mensal com narrativa em português natural,
Quando o operador publica, Então o gate não reprova por data, seção, nome do cliente ou taxa, E
continua reprovando métrica solta ("Fechamos 340 leads", "ROAS 4.95x").
"A campanha alcançou {{dp:dp1}} impressões e {{dp:dp2}} cliques" - frase correta, publish
recusado: o gate pegava a primeira métrica da oração e comparava com os DOIS dps, então um deles
sempre divergia, e nenhuma reescrita passava (só quebrar em duas orações, e ninguém escreve
relatório assim).
**Corrigido para metric - e a mesma classe reapareceu em aggregation e depois em period, no
mesmo relatório, em publishes seguidos.** Corrigir dimensão a dimensão, a cada relatório, é o que
transforma um gate em catraca. A regra passou a valer para TODAS: allClaimsFromText devolve tudo
que a oração afirma naquela dimensão, e só há divergência quando o valor REAL do dp está AUSENTE
dessa lista.
Aceite (H1): Dada uma oração que cita vários valores da mesma dimensão, Quando cada dp dela é
julgado, Então nenhum é acusado por um valor que pertence a outro, E métrica, período ou entidade
de fato errados continuam reprovando.
Lido na página pública de um relatório publicado, duas vezes no mesmo parágrafo: **"a campanha
aconteceu no mesmo período em que um total de 184 leads"**. O texto composto dizia "a campanha
gerou um total de 184 leads"; o guard {F18-04} degradou a afirmação causal, o que está certo,
mas usava UM substituto para os nove conectivos. Ele pede uma ORAÇÃO depois ("veio DA campanha" →
"…em que a campanha", correto) e quebra a frase nos conectivos de complemento NOMINAL.
Cada conectivo passou a poder declarar seu próprio substituto, igualmente não-causal: gerou →
registrou, resultou em → coincidiu com, o que explica → o que coincide com. Os seis que pedem
oração seguem com o substituto de sempre.
Aceite (H1): Dada uma afirmação causal sem evidência designada, Quando ela é degradada, Então
o resultado é uma frase gramatical, E nenhum conectivo do conjunto sobrevive.
O que a onda deixou como dívida nomeada, e é de VERACIDADE, não de usabilidade: o gate de período compara a GRANULARIDADE, não QUAL período. Publiquei de propósito "a taxa de conversão de junho de 2026 ficou em{{dp:dp3}}" com um dp de janeiro e o publish passou - os dois sãogranularity: "mes". É precisamente o defeito que este gate existe para pegar (número verdadeiro na frase errada) e ele não vê. O link foi revogado (410 confirmado) para não deixar relatório enganoso no ar. Comparar o rótulo direto não serve ("junho de 2026" vs "jun/2026" vs "último mês"): precisa de normalização de rótulo de período antes da comparação. Próximo card da fila - ver{OP-12}abaixo.
FECHADO em 2026-08-18, e provado no container de produção contra o par exato do achado: a
frase "a taxa de conversão de junho de 2026 ficou em {{dp:dp0}}" com um dp de janeiro **passou
a reprovar**, nomeando os dois períodos na mensagem, e a mesma frase com o mês certo publica.
Como foi resolvido sem reintroduzir o erro que {OP-10} acabou de tirar: os dois lados são
normalizados para {ano, mes} antes de comparar - "junho de 2026", "jun/2026", "06/2026" e
"2026-06" colapsam na mesma chave -, nunca comparados literalmente. Postura conservadora
preservada: parte ausente de um dos lados não diverge ("em janeiro" casa com "janeiro de 2026"),
oração sem período nomeado segue em silêncio, tupla sem período resolvível não é julgada, e
contexto geral MAIS o recorte certo ("no semestre, a taxa de janeiro") publica.
Contagem: 4 grafias usuais do mesmo mês passam, 2 trocas reprovam (mês e ano), 3 casos de
silêncio permanecem em silêncio, e 5 casos de unidade cobrem as peças - 14 testes, e o placar
fecha contra os 8 casos exercitados na medição.
Duas armadilhas que ficam escritas porque custaram tempo: numa string JS comum a sequência de
fronteira de palavra vira BACKSPACE (o padrão compila e nunca casa - mesma pegadinha de
{F8-23}, daí o String.raw); e a varredura de meses consumia a janela do ano, de modo que o
primeiro match engolia o segundo mês. Suíte 1627 verde; merge 52e6368, deployado.
Defeito de veracidade, medido em produção em 2026-08-18 com um publish deliberado: a narrativa
dizia "a taxa de conversão de junho de 2026" citando um dp cuja tupla declara janeiro de 2026, e o
publish passou. identity-coherence.mjs compara identity.period.granularity (mês vs mês),
nunca identity.period.label - então trocar janeiro por junho é invisível para o gate.
Por que isso é grave: é a definição do defeito que RN-012 e a família de gates de honestidade
existem para impedir - número verdadeiro na frase errada. Um relatório pode citar o mês bom de um
semestre ruim, com o número correto, e publicar.
Por que a correção não é comparar o rótulo direto: "junho de 2026", "jun/2026", "06/2026" e
"último mês do semestre" são o mesmo período escritos de quatro jeitos, e a oração da narrativa
usa a forma humana. Comparação literal reprovaria o texto correto - que é exatamente o erro que
{OP-10} acabou de tirar deste mesmo gate.
O que implementar: normalização de rótulo de período (mês/ano canônicos) aplicada aos DOIS
lados antes de comparar, com a mesma postura conservadora do resto do arquivo: **oração que não
nomeia um período específico segue em silêncio**, e só há divergência quando ela nomeia um período
resolvível diferente do que a tupla declara.
Contagem a fazer no cartão: universo = as formas de escrever um mês que aparecem nas
narrativas já compostas neste projeto; critério COMPORTAMENTAL (chamar o gate e ler o veredito);
o placar tem de fechar, e cada forma nova entra com o par: uma que passa e um mês trocado que
reprova.
Aceite (H1): Dada uma narrativa que nomeia um mês específico, Quando ela cita um dp de outro
mês, Então o publish é recusado apontando o período, E quando ela nomeia o mesmo mês em qualquer
grafia usual o publish passa, E oração sem período nomeado continua em silêncio.
Encerramento
Com as vinte fatias fechadas, a seção Construção chega a 100% e resta a seção Testes (Plano de Testes, Alfa e Beta) para o projeto chegar a 100% geral. Os ciclos reais de adoção (J.3 do DRS) começam assim que a Fatia 9 (publicação) estiver no ar, em paralelo às fatias seguintes. A ordem da fila não é a ordem da numeração: as Fases F17 e F18 vêm antes de F13 a F16, e F19 depende de {F17-05} e de {F18-05}, o que coloca F19 e F20 depois delas e não no fim por causa do número.