fix(dre): fecha a auditoria da DRE — folha contada 2x, recorte que mentia e estorno que sumia - #41
Merged
Merged
Conversation
`totaisPorCategoria[catId]` tinha DOIS significados. Em mes aberto era por-categoria de verdade; em mes fechado a reconciliacao jogava a L50 INTEIRA em `despesas_administrativas` e deixava `pessoas` com o valor do motor TS. Os leitores erravam em direcoes opostas: quem somava os dois ids contava a folha 2x (anomaly-detection, DFC); quem lia so `admin` perdia a folha (break-even, ponto de equilibrio, indice de gastos fixos) — esses tres o handoff da auditoria nao listava. A tabela `CATEGORIA_LINHA_SINAL` morreu: o `sinal` do catalogo reproduz ela inteira. A v3 fecha por LINHA e o `dre_detalhado` guarda por CATEGORIA, entao a reconciliacao passa a RATEAR a linha entre as categorias que a dividem, na proporcao da composicao TS, com a ultima absorvendo o residuo — a soma bate exato, que e a invariante de que os leitores dependem. Valor nao atribuivel cai na RESIDUAL: afirmar "a folha foi X" sem saber e pior que dizer "administrativas foi X". Convencao registrada em `categorization-rules.ts`. Os 5 leitores pararam de enumerar `categoria_id` a mao — agora pedem a linha via `somarLinha`. Junto vao tres defeitos de recorte: - `addMonths` transbordava: em 31/05, "Mes passado" devolvia MAIO. Idem 31/07, 31/10, 31/12 e 29–31/03; "Ultimos 6 meses" em 31/07 comecava em marco. O dia agora e grampeado no ultimo dia valido do mes de destino. - recorte de UM mes abria em coluna SEMANAL enquanto o rotulo prometia "ultimo mes fechado". Era esse recorte que levava erro de cascata aos olhos do dono. O toggle DIA/SEM/MES do PeriodPicker segue disponivel. - transacao sem categoria SUMIA da tela: o balde existia no acumulador e o loop iterava so `CATEGORIAS`. Agora vira linha visivel "Nao categorizado", em L99, fora do resultado. Isso exigiu alinhar o fallback do `dfc-direct-mapper`, que mandava id desconhecido pra "operacional" — o inverso da cascata — e quebraria a invariante op+inv+fin === resultado liquido. Os 30 snapshots ja gravados estavam TODOS corrompidos (R$ 13.962.202,16 de folha em dobro). O `refechar-snapshots-catalogo` nao pegava: o gate dele era `headline || subs`, e esse drift nao mexe em nenhum dos dois. Ganhou o terceiro detector, `categoriasComDrift`. Re-fechamento rodado: 30 gravados, diagnostico depois deu 0 corrompidos, novo dry-run da 0 mudancas. Gate: 1463/1463 testes, build ok, 0 erro/warning novo de eslint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`calculate_monthly_dre_v3` somava UM LADO so conforme o `sinal` da categoria (`'+' -> t.credito`, `'-' -> t.debito`). Um estorno — credito numa categoria de despesa (devolucao de compra, reembolso de fornecedor) ou debito numa de receita (chargeback) — contribuia ZERO. E nao caia em L90 nem em L99: o dinheiro sumia da cascata INTEIRA. O extrato mexia e a DRE nao sabia. O motor TS sempre fez `credito - debito`, e em regime de caixa e ele que esta certo: se o dinheiro voltou pra conta, ele voltou, e reduz a despesa do mes em que caiu. A migration passa a somar MAGNITUDE LIQUIDA. Escolha deliberada de NAO inverter a convencao do JSONB `linhas`: linha de despesa continua positiva quando e despesa liquida e a escada continua subtraindo, entao nenhum leitor de `linhas_dre` foi tocado (`montarKpisDeV3`, `cash-cycle-analyzer`, `dre-v3-aggregate`, `sinalDaLinha`). O caminho obvio — SQL fazendo credito-debito e escada virando soma — teria raio de explosao enorme a toa. O corpo da funcao e copia byte-identica do dump, com as duas linhas do CASE trocadas. Blast radius medido ANTES de aplicar: 18.004 transacoes, 0 estornos. A divergencia era 100% latente. Por isso a hora de corrigir era essa: com estorno real em campo, a correcao passaria a mexer em numero que o cliente ja viu. Dry-run do re-fechamento depois da migration: 30/30 identicos — nada a gravar. Os dois `it.fails` viraram `it`, mais dois casos que faltavam: a devolucao MAIOR que a compra (a linha inverte e o lucro sobe — o caso que quebraria com o fix ingenuo) e a prova de que o dinheiro do estorno volta pra propria linha em vez de cair em L90/L99. O gabarito `motorSqlV3` dentro do teste e uma COPIA da algebra do SQL, nao uma leitura dela: trocar o CASE da RPC sem trocar o gabarito deixaria a paridade verde e mentirosa — o mesmo modo de falha que deixou a folha sumir. Aviso no cabecalho do arquivo. Terceira copia da convencao, que a auditoria nao citava: `visao-service.ts` (ranking top-8 da Visao do Projeto) tambem somava um lado so — a DRE diria 35.000 e o ranking 40.000 pro mesmo fornecedor no mesmo mes. Alinhada junto. Re-dump do banco confirma a aplicacao: diff contra o dump de 30/07 da exatamente 3 linhas, todas da migration (as 2 do CASE + o COMMENT); `roles.sql` sem mudanca; zero drift fora de migration. Snapshot volta a autoritativo (20260804000000 = max(repo)). Com 0 estornos a saida da RPC e identica nas duas versoes, entao ler a funcao era a UNICA prova possivel. Gate: 1465/1465 testes, build ok, 0 erro/warning novo de eslint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Updates to Preview Branch (fix/dre-ondas-3-4-5) ↗︎
Tasks are run on every commit but only new migration files are pushed.
❌ Branch Error • Tue, 04 Aug 2026 23:15:06 UTC View logs for this Workflow Run ↗︎. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fecha as Ondas 3, 4 e 5 da auditoria da DRE (
docs/atros-v3/handoff-2026-07-31-auditoria-dre.md). As Ondas 1 e 2 já estavam em produção (#39, #40).Dois commits, um por gate.
f1cf688— Ondas 3 e 4: a folha para de ser contada duas vezestotaisPorCategoria[catId]tinha dois significados. Em mês aberto era por-categoria de verdade; em mês fechado a reconciliação jogava a L50 inteira emdespesas_administrativase deixavapessoascom o valor do motor TS.Por isso os leitores erravam em direções opostas:
anomaly-detection,dfc-direct-mapperextrato-processor)kpi-advanced-service)Os três de baixo o handoff da auditoria não listava.
A tabela
CATEGORIA_LINHA_SINALmorreu — osinaldo catálogo reproduz ela inteira. Como a v3 fecha por linha e odre_detalhadoguarda por categoria, a reconciliação passa a ratear a linha entre as categorias que a dividem, com a última absorvendo o resíduo (a soma bate exato, que é a invariante de que os leitores dependem). Valor não atribuível cai na residual: afirmar "a folha foi X" sem saber é pior que dizer "administrativas foi X". Convenção registrada emcategorization-rules.ts.Junto vão três defeitos de recorte:
addMonthstransbordava — em 31/05, "Mês passado" devolvia MAIO. Idem 31/07, 31/10, 31/12 e 29–31/03.CATEGORIAS. Agora vira linha visível "Não categorizado", em L99, fora do resultado.Re-fechamento aplicado
Os 30 snapshots gravados estavam todos corrompidos — R$ 13.962.202,16 de folha em dobro, o mesmo número da Onda 1. O fix de código sozinho não alcançava produção, e o
refechar-snapshots-catalogonão pegava: o gate dele eraheadline || subs, e esse drift não mexe em nenhum dos dois. Ganhou o terceiro detector,categoriasComDrift.Rodado: 30 gravados, diagnóstico depois deu 0 corrompidos, novo dry-run dá 0 mudanças (idempotente). Resultado Líquido não mudou em nenhum mês — muda só o split.
1c05831— Onda 5: o estorno para de sumircalculate_monthly_dre_v3somava um lado só conforme osinal. Um estorno — crédito numa categoria de despesa (devolução de compra, reembolso) ou débito numa de receita (chargeback) — contribuía zero. E não caía em L90 nem L99: o dinheiro sumia da cascata inteira. O extrato mexia e a DRE não sabia.Em regime de caixa quem está certo é o TS: se o dinheiro voltou pra conta, ele voltou, e reduz a despesa do mês em que caiu.
A migration passa a somar magnitude líquida. Escolha deliberada de não inverter a convenção do JSONB
linhas: linha de despesa continua positiva quando é despesa líquida e a escada continua subtraindo, então nenhum leitor delinhas_drefoi tocado (montarKpisDeV3,cash-cycle-analyzer,dre-v3-aggregate,sinalDaLinha). O caminho óbvio — SQL fazendocredito−debitoe escada virando soma — teria raio de explosão enorme à toa. O corpo da função é cópia byte-idêntica do dump com as duas linhas doCASEtrocadas.Por que agora
Medido antes de aplicar: 18.004 transações, 0 estornos. A divergência era 100% latente. Dry-run depois da migration: 30/30 idênticos — nada a gravar. Era essa a hora: com estorno real em campo, a correção passaria a mexer em número que o cliente já viu.
Terceira cópia da convenção
visao-service.ts(ranking top-8 da Visão do Projeto) também somava um lado só — a DRE diria 35.000 e o ranking 40.000 pro mesmo fornecedor no mesmo mês. Alinhada junto.Banco
Migration
20260804000000_dre_v3_sinal_liquidojá aplicada no remoto. Re-dump confirma: diff contra o dump de 30/07 dá exatamente 3 linhas, todas da migration (as 2 doCASE+ oCOMMENT);roles.sqlsem mudança de conteúdo; zero drift fora de migration. Snapshot volta a 🟢 autoritativo (20260804000000=max(repo)).Com 0 estornos a saída da RPC é idêntica nas duas versões — então ler a função era a única prova possível de que a migration entrou. Nenhum teste de comportamento serviria.
Gate
f1cf688verificado verde isoladamente (164 testes) — sem commit intermediário quebradonext buildok · tsc limpo · 0 erro e 0 warning novo de eslintArmadilha que ficou documentada
O gabarito
motorSqlV3dentro decascata.test.tsé uma cópia da álgebra do SQL, não uma leitura dela. Trocar oCASEda RPC sem trocar o gabarito deixaria a paridade verde e mentirosa — exatamente o modo de falha que deixou a folha sumir na primeira vez. Aviso no cabeçalho do arquivo.Como revisar
O check ao vivo que falta é na tela: abrir a DRE da Vertímetal e conferir que entra em coluna mensal e que Administrativas × Pessoas aparecem separadas somando a L50.
🤖 Generated with Claude Code