fix(dre): a folha de pagamento volta pro resultado + resguardo de provider da IA - #39
Merged
Conversation
"Gerar nova versão" do Plano de Voo falhou com "IA retornou narrativas inválidas: resposta vazia da IA [modelo=gpt-5-nano]". A IA não devolveu vazio — a OpenAI recusou por cobrança. A telemetria da casa (`ai_calls`) tinha o erro cru guardado o tempo todo: openai_http_429: "You exceeded your current quota, please check your plan and billing" Padrão nos últimos 30 dias: TODA chamada que foi pro gpt-5-nano falhou assim (15/07, 21/07 ×2, 23/07, 27/07); TODA que foi pro gemini-2.5-flash-lite passou com valid=true e zero retry — inclusive as 5 gerações de Plano de Voo que estão no ar. E `fallback_used` NULL em todas as falhas: o resguardo nunca entrou. ## Por que o resguardo não entrou — e não entraria Três defeitos empilhados, cada um suficiente pra derrubar a geração: 1. FALLBACK DESLIGADO POR PADRÃO. `getFallbackProvider` devolvia null a menos que `AI_FALLBACK_PROVIDER` estivesse setada à mão. Ninguém setou. Com o Gemini ao lado, funcionando e de graça, um 429 de cobrança derrubava tudo. Agora o padrão é LIGADO: o outro provider assume, desde que tenha chave. `AI_FALLBACK_PROVIDER=off` desliga de propósito. 2. O OVERRIDE DE MODELO VAZAVA ENTRE PROVIDERS — e este é o que tornava o resguardo decorativo. O caller passa `model: "gpt-5-nano"`; a OpenAI estoura a quota; o cliente cai no Gemini... e chama o Gemini pedindo "gpt-5-nano", que não existe. Mesmo com a env setada, o fallback morria na porta. `resolveModel` agora ignora override que pertence claramente ao OUTRO provider (nome desconhecido — fine-tune, alias — segue respeitado). 3. DOIS DEFAULTS CONTRADITÓRIOS. `getProvider()` resolve gemini quando `AI_PROVIDER` está ausente; os callers do Plano de Voo e do Retrato faziam `(process.env.AI_PROVIDER ?? "openai")` por conta própria só pra decidir o override. Efeito: sem a env, o caller mandava nome de modelo da OpenAI e o cliente chamava o Gemini com ele. Ou seja, "só remover a variável" teria trocado um erro por outro. Os callers agora sempre passam o override da família OpenAI e deixam o cliente decidir — com (2) no lugar, é seguro. ## A mensagem também mentia `meta.error` carregava o 429 e a mensagem montada só mostrava modelo/latência/ retries. Quem depurava ia atrás do prompt em vez da conta. Agora provider, fallback e a causa crua entram no texto. Mesma correção no `retrato/narrativa`, que respondia só "IA indisponível". Nada aqui muda qual provider é o primário: isso segue em `AI_PROVIDER` (e o default do cliente, gemini, é o plano grátis). O que muda é que a queda de um lado deixa de ser queda do sistema. Validação: 151 arquivos, 1403/1403 (13 novos cobrindo default, fallback com/sem chave, desligamento explícito, o não-vazamento de modelo nos dois sentidos e o caminho exato do bug) · tsc limpo · eslint 0 · zero migration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A cascata da DRE estava escrita à mão em três lugares em TS, cada um
enumerando `categoria_id`:
resultadoOperacional = lucroBruto + despVendas + despAdm
Quando a migration 20260713000000 criou a categoria `pessoas` na MESMA
linha 50 de `despesas_administrativas`, o motor SQL absorveu sozinho (ele
agrupa por `linha_dre`), mas os três motores TS não. A folha saiu do
resultado sem nenhum teste ficar vermelho. A migration declarava
"ZERO-SHIFT" — era verdade só para quem lia o catálogo.
Medido na Vertímetal: R$ 465.406,74/mês, R$ 13.962.202,16 em 30 meses.
Na tela isso aparecia como uma cascata que não fecha: em jan/25,
554.319,91 (Lucro Bruto) − 91.069,88 (Administrativas) = 463.250,03, mas
o EBITDA exibido era 35.090,51. Os 428.159,52 da folha estavam
desenhados em "Informativo · fora do resultado", ao lado de empréstimo e
transferência entre contas — enquanto o SQL já os subtraía.
Agora a cascata sai do catálogo (linhaDre / entraNoResultado / sinal),
como o tipo `Categoria` já documentava desde sempre: "Motor de DRE filtra
por essa flag em vez de hardcoded list". Categoria nova entra sozinha.
- src/lib/dre/cascata.ts — primitivo único (agrupa por linha, aplica
sinal, respeita entraNoResultado; fora do catálogo cai em L99 como
na blindagem do LEFT JOIN da RPC)
- extrato-processor: as duas escadas (multi-mensal e forense) repointadas
- DREPivotTable: ordem de leitura e subtotais vêm do catálogo; abaixo da
linha fica só L90/L95/L99
Teste de paridade contra a álgebra do calculate_monthly_dre_v3 escrito
ANTES do primeiro repoint — era a rede que faltava. A divergência de
convenção de sinal TS x SQL (estorno dentro da categoria) fica documentada
em dois it.fails; unificar invalida snapshot e é trilha própria.
Gate (dump fresco de 2026-07-30): replay da Vertímetal 2025 pelo caminho
de MÊS ABERTO bate com os 12 snapshots ao centavo, EBITDA e Resultado
Líquido. Total 2025 = -620.231,74 nos dois caminhos.
Inclui re-dump do banco: schema.sql volta a autoritativo (diff de 10
linhas = as duas migrations pendentes; zero drift fora de migration;
calculate_monthly_dre_v3 byte-idêntica).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
This was referenced Jul 31, 2026
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.
Dois fixes que estavam parados fora da
main.1.
fix(dre)— a folha de pagamento volta pro resultadoA cascata da DRE estava escrita à mão em três lugares em TS, cada um enumerando
categoria_id:Quando a migration
20260713000000criou a categoriapessoasna mesma linha 50 dedespesas_administrativas, o motor SQL absorveu sozinho (ele agrupa porlinha_dre), mas os três motores TS não. A folha saiu do resultado sem nenhum teste ficar vermelho. A migration declarava "ZERO-SHIFT" — era verdade só para quem lia o catálogo.Medido na Vertímetal: R$ 465.406,74/mês · R$ 13.962.202,16 em 30 meses.
Na tela aparecia como uma cascata que não fecha. Em jan/25:
Os R$ 428.159,52 estavam desenhados em "Informativo · fora do resultado", ao lado de empréstimo e transferência entre contas — enquanto o SQL já os subtraía dentro do EBITDA.
O que mudou
src/lib/dre/cascata.ts(novo) — primitivo único. Agrupa porlinhaDre, aplicasinal, respeitaentraNoResultado. Categoria fora do catálogo cai em L99, espelhando a blindagem doLEFT JOINda RPC. Categoria nova entra na cascata sozinha.extrato-processor— as duas escadas (multi-mensal e forense) repointadas.DREPivotTable— ordem de leitura e subtotais vêm do catálogo; abaixo da linha fica só L90/L95/L99.O tipo
Categoriajá documentava a intenção desde sempre: "Motor de DRE filtra por essa flag em vez de hardcoded list". Agora cumpre.Gate
Replay da Vertímetal 2025 inteira pelo caminho de mês ABERTO (motor TS, snapshots ignorados de propósito), contra os 12 snapshots do SQL:
Delta zero nos 12 meses, em EBITDA e Resultado Líquido. Os dois motores passaram a dizer a mesma coisa, e nenhum número que já estava certo se moveu.
Teste de paridade contra a álgebra do
calculate_monthly_dre_v3escrito antes do primeiro repoint — era a rede que faltava. A divergência de convenção de sinal TS×SQL (estorno dentro da categoria) fica documentada em doisit.fails; unificar invalida snapshot e é trilha própria.Sem migration, sem re-fechamento
Os KPIs do snapshot sempre vieram do SQL v3 — nunca estiveram errados. O pivot de mês fechado lê
col.dre.totais, reconciliado com a v3 no fechamento. Os 120 snapshots seguem válidos.Ressalva: o
dre_detalhado.resultadosdos snapshots antigos foi gravado pela escada velha e está errado lá dentro. Conferi quem lê esse campo: ninguém. É lixo dormente; fechamentos novos já gravam certo.2.
fix(ia)— o resguardo de provider agora existe de verdadeJá estava pronto, só não tinha subido. "Gerar nova versão" do Plano de Voo caía com "resposta vazia da IA" quando a OpenAI recusava por quota (429). Três defeitos empilhados: fallback desligado por padrão, override de modelo vazando entre providers, e dois defaults contraditórios.
Zero migration. Não muda qual provider é o primário — muda que a queda de um lado deixa de derrubar o sistema.
AI_PROVIDEReAI_FALLBACK_PROVIDERseguem opcionais.Verificação
next buildlimpotsclimpo emsrc/DREPivotTable.tsxsão idênticos emHEAD(débito pré-existente: overlay hand-rolled +.cockpit-card-accent)schema.sqlvolta a 🟢 autoritativo (diff de 10 linhas = as duas migrations pendentes; zero drift fora de migration;calculate_monthly_dre_v3byte-idêntica)Falta
Smoke visual na tela real — o gate acima é replay de dados, não render no browser.
🤖 Generated with Claude Code