{
  "markdown": "# Clara Finanças\n\nA Clara é uma aplicação de finanças pessoais orientada por conversa. A pessoa envia uma fatura, conversa com a Clara sobre o próprio dinheiro e recebe explicações rastreáveis até as transações que originaram cada valor.\n\nO projeto é um monorepo TypeScript gerenciado por pnpm e Turborepo. Ele possui duas superfícies de runtime:\n\n- **Control plane (`apps/web`)**: aplicação Next.js que cuida da interface, autenticação, tenants, upload de documentos e APIs auxiliares.\n- **Agent plane (`packages/agent`)**: agente Eve executado separadamente. O navegador se conecta a ele diretamente, usando um JWT curto emitido pelo control plane.\n\nDocumentação complementar:\n\n- [Deploy na Vercel](docs/deploy.md) — os dois projetos, variáveis por plano e armadilhas\n- [Diagrama navegável da arquitetura](docs/arquitetura-clara.html)\n\n## Arquitetura\n\n```text\nNavegador\n  ├── HTTPS → Next.js / Control plane\n  │             ├── Better Auth (sessão)\n  │             ├── /api/token (JWT para o agente)\n  │             ├── /api/documents/prepare (hash, deduplicação, modo)\n  │             ├── /api/documents/upload (token de escrita no Blob)\n  │             ├── /api/documents (registro e idempotência)\n  │             └── Server Components → PostgreSQL\n  │\n  ├── HTTPS → Vercel Blob (PDF direto, sem passar pela função)\n  │\n  └── HTTPS + Bearer JWT → Eve / Agent plane\n                           ├── Coordenadora Clara (gerente de conta)\n                           ├── Extrator de PDFs (escrituração)\n                           ├── Analista do razão (análise)\n                           ├── Categorizador (guarda-livros)\n                           └── Tools + HITL + schedules\n\nPostgreSQL ← Drizzle, RLS e escopo transacional por tenant\nBlob/filesystem ← PDFs; o banco guarda somente chave e hash\nOpenAI/AI Gateway ← inferência; cálculos financeiros são determinísticos\n```\n\nO fluxo financeiro principal é:\n\n```text\nupload PDF → extração → lote proposto → checksum/conferência\n           → aprovação explícita da pessoa → transações confirmadas no razão\n```\n\n### O PDF não atravessa a API\n\nO arquivo vai do navegador **direto** para o armazenamento; a função só recebe\num registro com o URL, o caminho e o hash. Três motivos, em ordem de peso:\n\n1. **O teto de 4,5 MB some.** Uma Vercel Function recusa corpos acima disso\n   antes de qualquer código rodar. O limite de 20 MB que o app anunciava era,\n   em produção, um 413 da plataforma sem mensagem para a pessoa.\n2. **O arquivo trafega uma vez, não duas.** Antes era navegador → função →\n   Blob, com a fatura inteira bufferizada na memória da função no meio.\n3. **O erro chega antes do upload.** O parser roda no navegador\n   (`lib/pdf-precheck.ts`): abre o PDF, confere que existe camada de texto e\n   descarta o que leu. Um escaneado é recusado na hora, em vez de subir, virar\n   linha no banco, virar mensagem no chat e só então falhar no extrator.\n\nO passo `prepare` calcula o caminho a partir do tenant da sessão e do SHA-256\nque o navegador computou — e, quando aquele conteúdo já é um documento do\ntenant, responde `reused` e **nada sobe**. O token de escrita é emitido para\naquele caminho, com `application/pdf` e teto de tamanho; o registro re-deriva o\ncaminho e confere o blob com um `head` autenticado antes de gravar a linha.\n\nQuem extrai o texto continua sendo o agente, lendo o PDF do armazenamento\n(`read_pdf_pages`). Nenhum texto extraído é transmitido nem armazenado.\n\nSem `BLOB_READ_WRITE_TOKEN` — o desenvolvimento local — `prepare` responde\n`proxy` e o arquivo volta a passar pela função, para o disco.\n\nUm lote `proposed` não é considerado gasto confirmado. A tool `commit_batch` é o único caminho de escrita no razão e exige aprovação humana. Consultas e agregações usam `packages/ledger`, que preserva a proveniência através dos `transactionIds`.\n\n### O estado do razão entra no contexto a cada turno\n\nA Clara não descobre o que existe perguntando. Antes de cada turno, instruções dinâmicas do eve (`packages/agent/agent/instructions/estado.ts`) leem `loadSnapshot()` e injetam no contexto: a data de hoje em São Paulo, as faturas registradas com ciclo, vencimento, total e resultado da conferência, a cobertura do razão e quantos lançamentos seguem sem categoria.\n\nResolve em `turn.started`, não em `session.started`: dentro da mesma conversa a pessoa aprova uma fatura, e o turno seguinte precisa enxergar o razão já atualizado.\n\nO eve rebaixa instruções dinâmicas a mensagens de sistema guardadas por slug, e o valor do turno substitui o do turno anterior — existe sempre um snapshot só no request, e ele não entra no histórico da conversa. O que ele não pode ser é uma segunda fonte de verdade sobre algo que muda no meio do turno: `read_batch` e `resolve_invoice_reference` reescrevem o foco da sessão depois que o bloco já foi lido. Por isso o campo se chama `activeInvoiceAtTurnStart` e o bloco declara a precedência — resultado de ferramenta do turno vence o snapshot —, e por isso ele avisa que ali só há id de fatura e de documento: um `transactionId` existe apenas no retorno de `read_batch`.\n\nO analista e o categorizador têm cada um o seu próprio — um subagente declarado não herda nada do root, e essa duplicação é o preço do isolamento que garante que o extrator não alcance o razão.\n\nO snapshot é deliberadamente limitado às 12 faturas mais recentes. Quando a\npessoa menciona uma fatura mais antiga, a Clara usa `list_invoices` para\nrecuperar o histórico sob demanda. Assim o custo de contexto não cresce sem\nlimite junto com a vida financeira da pessoa.\n\n### A conversa traduz a execução\n\nExtrator, analista e categorizador devolvem contratos estruturados validados\npor Zod. A coordenadora escolhe a apresentação, mas não precisa copiar números\nou ids de uma resposta livre. Painéis financeiros falham fechado quando falta\nproveniência.\n\nNa interface, nomes de tools, raciocínio e JSON não aparecem. O stream vira\nprogresso orientado à tarefa — “Lendo o documento”, “Conferindo os valores” —\ne cada escrita durável tem um cartão próprio com objeto, alcance e\nconsequência. Chamar a tool abre a decisão; não existe um “sim” em prosa\nseguido de uma segunda confirmação.\n\nUpload também não fabrica uma mensagem com `documentId`: o texto visível é\nnatural e o identificador viaja em `clientContext` de um turno. Senhas de PDF\nusam resposta livre protegida e são removidas do histórico guardado neste\ndispositivo.\n\n### Fatura não é intervalo de datas\n\nUm ciclo que fecha em 07/07 cobre compras de 31/05 a 30/06, e duas faturas consecutivas se tocam na virada. Por isso as ferramentas do analista aceitam `batchId`: recortar por data conta a compra da fronteira dos dois lados. O snapshot entrega os `batchId` disponíveis, então \"nesta fatura\" tem resposta exata.\n\n### Identidade do comerciante\n\nO emissor imprime o mesmo comerciante de formas diferentes a cada fatura — máscara do cartão, câmbio na descrição, número da parcela, prefixo do intermediário, invólucro do `IOF de \"…\"`. `merchantKey()` em `packages/ledger/src/merchant.ts` deriva uma identidade determinística, gravada em `transactions.merchant_key` no momento da proposta.\n\nEla resolve a parte mecânica e **deliberadamente não adivinha** que \"Anthropic\" e \"Claude.Ai Subscription\" são a mesma empresa: fusão errada some com dinheiro de um comerciante e o faz aparecer em outro, sem sinal na tela. Esse caso é aprendizado com aprovação — um conceito `MerchantAlias`, consumido por `detectRecurrences`.\n\n### O razão por operadora e por mês\n\n`/transacoes?vista=operadoras` cruza o gasto confirmado: operadora nas linhas, mês nas colunas, com as duas margens fechando no mesmo total. A operadora é `documents.issuer` — ela é do DOCUMENTO, não da linha, porque é a mesma informação para toda a fatura e duplicá-la abriria a chance de uma linha discordar da fatura de onde veio. O mês é o da **compra**, não o do fechamento: uma fatura fechada em julho cobre gastos de maio e junho.\n\nA agregação é `aggregateByIssuerMonth()` em `packages/ledger/src/analysis.ts` — pura, testada e usada pelos dois lados, pelo mesmo motivo das demais: dois caminhos de cálculo acabariam divergindo entre a tela e a conversa. Cada célula carrega os `transactionIds` que a compõem, e é deles que a lista \"de onde vem cada número\" é montada, em vez de um segundo filtro parecido.\n\nNa conversa, quem a alcança é `aggregate_by_month`, do analista: uma chamada devolve a série inteira, cada mês com proveniência e com a composição por operadora. Antes dela só a tela chegava a esta conta, e \"quanto gastei mês a mês\" exigia uma chamada por mês, adivinhando quantos meses existem — a mesma falha da fila de revisão atrás de `server-only`: a resposta existia na aba ao lado e a conversa dizia que não sabia.\n\nDocumento sem operadora identificada não some da matriz: vira a linha \"Sem operadora\", e a tela de revisão oferece nomeá-lo.\n\n### Revisão manual: o que a IA não fechou\n\n`/revisar` é a fila do trabalho que a extração não concluiu. Entram lançamentos **sem categoria** (a análise por categoria fica com um buraco), de **confiança baixa** (o extrator avisou que pode ter lido errado, e o valor conta como gasto de qualquer forma) e **sem comerciante** (a linha não se agrupa com nada). Fatura cuja soma não fechou e documento sem operadora aparecem em listas próprias — são o documento inteiro, não a linha.\n\n\"Sem categoria\" quer dizer **gasto** sem categoria: pagamento de fatura e ajuste de saldo não esperam categoria nenhuma, e ficam fora do motivo (podem entrar pelos outros dois). O predicado é um só — `uncategorizedSpendCondition()` em `packages/db/src/queries/review.ts` —, compartilhado pela fila, pelo estado do razão que a Clara lê a cada turno e pela triagem do guarda-livros. Quando divergiam, a conversa afirmava \"há 2 itens sem categoria\" e, na frase seguinte, não conseguia listá-los: a fila devolvia outra contagem, e o item já atestado não aparecia em nenhuma das duas.\n\nA tela mostra a **descrição crua do documento**: é contra ela que a pessoa confere, e escondê-la transformaria a revisão em adivinhação sobre o palpite da Clara.\n\nAs escritas seguem a mesma disciplina da tool `recategorize_transactions` — o caminho humano não é um atalho que escapa da auditoria. Categoria e comerciante são leitura e podem mudar, cada mudança gravando uma linha em `transaction_reclassifications` com autor `human:<id>`. Valor, data, descrição e origem seguem recusados pelo trigger; correção de valor continua sendo linha de ajuste, feita pela conversa.\n\nO que é novo é o **atestado** (`transactions.reviewed_at` / `reviewed_by`, migração 0015): registra que uma pessoa olhou, mesmo quando nada muda. Sem ele a fila devolveria para sempre os itens cuja conclusão foi \"a leitura já estava certa\" — que é a conclusão mais comum — e a pessoa aprenderia a ignorá-la.\n\n### Isolamento por tenant\n\nO tenant é derivado da sessão autenticada; ele nunca é aceito do input do modelo ou do corpo enviado pelo cliente. O JWT contém `tenantId` e `userId`, é validado pelo canal Eve e, em cada tool, passa por `requireTenantCaller`. A função `forTenant()` define `app.tenant_id` localmente na transação para que as políticas RLS do PostgreSQL filtrem os dados na própria query.\n\n## Estrutura do repositório\n\n```text\nclara-financas/\n├── apps/\n│   └── web/                 # Next.js 16, páginas, componentes e APIs\n├── packages/\n│   ├── agent/               # Eve, coordenadora, subagentes, tools e schedules\n│   ├── auth/                # Better Auth + adapter Drizzle\n│   ├── config/              # Configuração TypeScript compartilhada\n│   ├── db/                  # Drizzle, schemas, migrações, RLS e tenant scope\n│   ├── env/                 # Validação de variáveis de ambiente com Zod\n│   ├── ledger/              # Regras, checksum e análises puras do razão\n│   ├── okf/                 # Parser/validador dos bundles de conhecimento\n│   ├── ui/                  # Primitivos visuais compartilhados\n│   └── views/               # Contratos dos painéis e eventos HITL\n├── bundles/constitution/    # Categorias, convenções e regras financeiras\n├── docs/                    # Deploy e diagrama de arquitetura\n├── PRODUCT.md               # Usuários, propósito e princípios estratégicos\n├── DESIGN.md / DESIGN.json  # Sistema visual e tokens para agentes de interface\n├── scripts/                 # Env da Vercel e reset do razão\n├── turbo.json               # Pipeline do Turborepo\n└── pnpm-workspace.yaml      # Workspace e catálogo de dependências\n```\n\n### Pacotes de domínio\n\n- `@clara-financas/ledger` contém funções puras para total, categorias, comparação de períodos, recorrências, identidade de comerciante e checksum. Nenhuma delas chama um modelo.\n- `@clara-financas/views` define, com Zod, os formatos de painel (`metric`, `breakdown`, `comparison`, `recurrences`, `transactions`, `invoices`, `commitments`, `proposal` e `checksum`) e a leitura de pedidos de aprovação.\n\n  Toda linha com valor exige `transactionIds`, e as três exceções são as mesmas três coisas: `commitments` (um lembrete não saiu de lançamento nenhum), `checksum` e `invoices` (o total de uma fatura é o que o documento declara, não uma soma escolhida). A exceção não é indulgência — sem ela, a forma certa fica inexprimível e o sintoma é a pessoa pedir uma lista e não receber painel nenhum. Foi assim com a conferência, e depois com \"liste as faturas mês a mês\".\n- `@clara-financas/okf` carrega e valida conceitos Markdown/YAML. A constituição define o contrato do domínio; aprendizados são separados e reversíveis.\n\n## Pré-requisitos\n\n- Node.js 24.x\n- pnpm 10.x\n- PostgreSQL acessível pela aplicação\n- credenciais do Better Auth e, se usado, Google OAuth\n- chave da API do modelo ou configuração do AI Gateway\n\nInstale as dependências com:\n\n```bash\npnpm install\n```\n\nAs variáveis do control plane são validadas em `packages/env/src/server.ts`; as variáveis públicas do navegador ficam em `packages/env/src/web.ts`. Em desenvolvimento, o arquivo de ambiente normalmente usado pelo app é `apps/web/.env`.\n\nVariáveis essenciais incluem `DATABASE_URL`, `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`, `APP_ORIGIN`, `AGENT_TOKEN_SECRET`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` e `NEXT_PUBLIC_AGENT_HOST`. Para migrações e para o `db:reset`, também `DATABASE_ADMIN_URL`. O agente tem o seu próprio `packages/agent/.env`, com as variáveis de modelo e, no modo silo, `TENANT_ID`.\n\nEm produção, `BLOB_READ_WRITE_TOKEN` é obrigatório: o armazenamento local de PDFs só existe em desenvolvimento. A tabela completa por projeto está em [docs/deploy.md](docs/deploy.md).\n\n## Desenvolvimento\n\nSuba o web app e o agente em terminais separados:\n\n```bash\npnpm dev:web\npnpm dev:agent\n```\n\nOu execute todos os pacotes que possuem uma tarefa `dev`:\n\n```bash\npnpm dev\n```\n\nPor padrão, o web app usa a porta `3000`. O agente Eve usa a porta definida pelo próprio ambiente de desenvolvimento.\n\n### Banco de dados\n\n```bash\npnpm db:generate   # gera uma migração a partir do schema\npnpm db:migrate    # aplica migrações existentes\npnpm db:push       # sincroniza o schema diretamente (desenvolvimento)\npnpm db:studio     # abre o Drizzle Studio\npnpm db:reset      # zera o razão para recomeçar os testes\n```\n\nO banco atual é PostgreSQL. Os PDFs não são armazenados como bytes no banco: em produção o navegador os envia direto ao Vercel Blob e o banco guarda apenas chave e hash; localmente, quando `BLOB_READ_WRITE_TOKEN` não está definido, são gravados em `.data/documents` pela função.\n\nDois papéis de banco, e a distinção é de segurança:\n\n- `DATABASE_URL` — papel de **aplicação** (`clara_app`), sem superusuário e sem `BYPASSRLS`. É o que o app e o agente usam. Conectar como `postgres` faria a RLS virar enfeite.\n- `DATABASE_ADMIN_URL` — papel de **migração**, dono do schema. Usado pelo `drizzle-kit` e pelo `db:reset`. Nunca é enviado para a Vercel.\n\n### `pnpm db:reset`\n\nApaga documentos, faturas, transações, reclassificações, compromissos, notificações e os aprendizados. Preserva usuários, sessões de login, tenants e a constituição. Pede confirmação; `--yes` pula.\n\nUsa `TRUNCATE` como dono do schema porque o trigger `clara_transactions_immutable` recusa apagar transação confirmada — afrouxar o trigger para limpar seria trocar a garantia pela conveniência.\n\n## Testes e verificações\n\n```bash\n./scripts/dev-db.sh    # PostgreSQL local, os dois papéis e as migrações\npnpm check-types\npnpm test\npnpm test:isolation\n```\n\nO primeiro comando é pré-requisito dos outros dois, e não por conveniência:\n`forTenant` recusa conectar como superusuário (a RLS viraria enfeite), então\ntestar exige um papel de aplicação de verdade, separado do dono do schema. O\nscript cria os dois, sobe o cluster, aplica as migrações e escreve as URLs em\n`apps/web/.env` e `packages/agent/.env` sem sobrescrever o que já estiver lá.\n`--reset` recomeça do zero.\n\nAs senhas são **geradas** na primeira execução e vivem só no `.env`, que é\nignorado pelo git — nenhuma literal no script, nem de brincadeira. Execuções\nseguintes reaproveitam a que já está lá, para o banco e o arquivo não\ndivergirem. Como o papel nasce antes da migração, a senha fraca de bootstrap\nque a migração `0001` define não chega a ser usada: é o mesmo caminho que\n[docs/deploy.md](docs/deploy.md) manda seguir em produção.\n\nOs testes de `packages/agent/tests/tools/` exercitam cada ferramenta do agente\ncontra esse banco — propor uma fatura, conferir, corrigir a natureza de um\nlançamento, registrar, ajustar, recategorizar, revisar, descartar — e as\nasserções olham o estado do banco, não o texto da resposta. Nenhum modelo\nparticipa: uma tool é uma função, e é assim que ela é testada.\n\nO Turborepo usa o grafo de dependências declarado nos `package.json` para ordenar builds e verificações. Para trabalhar apenas no app web e suas dependências, use filtros:\n\n```bash\npnpm exec turbo run build --filter=web...\npnpm exec turbo run check-types --filter=web...\n```\n\nPara mudanças incrementais em CI, o modo recomendado é:\n\n```bash\npnpm exec turbo run build --affected\n```\n\n## Comandos disponíveis na raiz\n\n| Comando | Função |\n| --- | --- |\n| `pnpm dev` | Executa as tarefas de desenvolvimento via Turborepo |\n| `pnpm dev:web` | Inicia somente o Next.js |\n| `pnpm dev:agent` | Inicia somente o agente Eve |\n| `pnpm build` | Compila os pacotes e a aplicação |\n| `pnpm check-types` | Verifica os tipos em todo o workspace |\n| `pnpm test` | Executa os testes dos pacotes |\n| `pnpm test:isolation` | Executa os testes de isolamento do banco |\n| `pnpm db:generate` | Gera migrações Drizzle |\n| `pnpm db:migrate` | Aplica migrações |\n| `pnpm db:push` | Faz push do schema em desenvolvimento |\n| `pnpm db:studio` | Abre o Drizzle Studio |\n| `pnpm db:reset` | Zera o razão preservando login e constituição |\n| `pnpm deploy:setup` | Vincula a raiz ao projeto Vercel do control plane |\n| `pnpm agent:link` | Vincula `packages/agent` ao projeto do agente |\n| `pnpm env:production` | Envia as variáveis do control plane (`--plan` só mostra) |\n| `pnpm env:agent:production` | Envia as variáveis do agente |\n| `pnpm deploy` | Deploy de preview do control plane |\n| `pnpm deploy:prod` | Deploy de produção do control plane |\n| `pnpm deploy:agent` | Deploy do agente (`eve deploy`) |\n| `pnpm deploy:check` | Dry-run de deploy Vercel |\n\n## Implantação\n\nNo ar:\n\n```\ncontrol plane  https://clara-financas-six.vercel.app\nagente         https://clara-financas-agent.vercel.app\n```\n\nSão **dois projetos Vercel**: o control plane (Next.js, na raiz) e a instância do agente (eve, em `packages/agent`). O navegador fala com o agente cross-origin, autenticado por um JWT curto emitido pelo control plane, e os dois compartilham `AGENT_TOKEN_SECRET` no modo pool atual.\n\nO passo a passo, a dependência circular entre `APP_ORIGIN` e `NEXT_PUBLIC_AGENT_HOST`, a tabela de variáveis por projeto e as armadilhas conhecidas estão em **[docs/deploy.md](docs/deploy.md)**.\n\nNão coloque arquivos `.env` ou segredos no repositório. O envio de variáveis é por allowlist, declarada por plano em `scripts/sync-vercel-env.ts`: uma variável fora dela não sobe — o que inclui, por construção, o papel de migração do banco.\n\n## Decisões importantes\n\n- **Modelo não é fonte de verdade financeira:** o modelo interpreta e escolhe tools; somas e comparações são funções determinísticas.\n- **Aprovação é um gate real:** o estado durável do Eve pode esperar dias por uma decisão sem manter compute ativo.\n- **Proveniência é obrigatória:** toda métrica exibida pode apontar para as transações que a compõem.\n- **Contexto não se pede, se injeta:** o estado do razão entra por instruções dinâmicas antes do primeiro token, em vez de depender de o modelo lembrar de consultar.\n- **Constituição versionada:** categorias e convenções são copiadas para o tenant e atualizadas quando a versão do bundle muda.\n- **Aprendizado que não se aplica é anotação:** todo conceito gravado tem um consumidor — `CategorizationRule` em `apply_learned_rules`, `MerchantAlias` em `detectRecurrences`.\n\n### Idempotência: o que está coberto e o que não está\n\nCoberto, **por documento**: o SHA-256 do PDF com índice único `(tenant_id, content_hash)` impede o mesmo arquivo entrar duas vezes; `propose_batch` apaga rascunho anterior do mesmo documento e recusa propor um documento já registrado.\n\n**Não coberto, por transação.** Não existe comparação de lançamentos entre lotes. O caso concreto: enviar a fatura ainda aberta, aprovar, e depois enviar a fatura fechada do mesmo ciclo. São arquivos diferentes, hashes diferentes, documentos diferentes — nada barra, e as compras que aparecem nas duas entram duas vezes no razão.\n\nO que torna isso perigoso é que **as duas conferências passam**: o checksum prova fidelidade *documento → extração*, não consistência *extração → razão*. Cada fatura bate com o próprio total declarado e o cartão fica verde nas duas.\n\nEnquanto não houver detecção por impressão digital do lançamento e supersessão de lote, a orientação é **não aprovar fatura parcial** — deixá-la como rascunho, que o snapshot mostra e permite retomar.\n",
  "bytes": 22019,
  "sha": "da8483b4add4834bd1593fdbefcab313c0880ed4941088464a52e77a4fd52496",
  "repo_slug": "regisleandro/clara-financas",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/okf_regisleandro_clara_financas_bundles_cons_5a98d2f5/readme"
}