Skip to content

chore(release): promove a 1.0.0-rc.3 para a main — a alçada do aprovador e o pré-voo do Pix - #1020

Merged
GabrielAderaldo merged 24 commits into
mainfrom
dev
Sep 10, 2026
Merged

GabrielAderaldo merged 24 commits into
mainfrom
dev

Conversation

@GabrielAderaldo

Copy link
Copy Markdown
Contributor

O que entra

Integração da dev na main, que é o deploy de produção. São 6 unidades de entrega desde a v1.0.0-rc.2, e a última delas é o próprio ato de promoção:

b89ea9bd chore(release): 1.0.0-rc.3 — bump do manifesto + CHANGELOG gerado (#1019)
72cf83a8 financial: sob o bypass do RBAC, a alçada deixa de recusar quem o /me acabou de liberar (#1015)
8870d3e7 auth,financial: papel sem alçada deixa de restringir quem já era irrestrito (#1011)
8a1b9148 financial: CNPJ alfanumérico deixa de virar outra inscrição em silêncio (#863) (#1010)
a6e8f87a inquiries: a inquiry-0037 versiona o laudo do Multipag Pix e o arquivo que o banco aprovou (#986)
87ad9be3 deps: mysql2 3.22.3 → 3.23.1 (#973)

O CHANGELOG.md da rc.3 cobre um range maior — as 22 unidades desde a tag v1.0.0-rc.2 — porque quatro PRs dev → main intermediárias (#978, #1002, #1005, #1009) já levaram parte delas a produção sem tag. A tabela acima é o que esta merge move; o CHANGELOG é o que a versão declara.

Superfície tocada

36 arquivos, 1876 inserções, 82 remoções.

Área Natureza
src/modules/financial/domain/payout/ inscrição alfanumérica, pré-voo (payout-readiness, inscription)
src/modules/financial/domain/document/approval-policy.ts alçada do aprovador
src/modules/financial/adapters/read/approver-authority-reader.rbac-bypass.ts novo — decorator da policy sob bypass
src/modules/auth/.../user-read.drizzle.ts + port papel sem teto volta a ser candidato na cascata
src/modules/financial/adapters/cnab/ posicional e segmentos do Multipag
src/server.ts, adapters/http/composition.ts fiação do decorator
handbook/architecture/adr/0069-*.md novo — ADR da policy sob bypass (supersede parcial do 0052)
handbook/inquiries/0037-* laudo do Multipag Pix + o .REM que o banco aprovou
package.json, pnpm-lock.yaml mysql2 3.23.1, nodemailer 9.1.1, --test-concurrency=6

Sem migration, sem mudança de env, sem infra.

Por que -rc.3 e não 1.0.0

A #873 continua aberta e a cerimônia de validação em produção do ADR-0066 §D10 não rodou. Pela escada do §D1 a árvore vai a produção declarando que ainda não foi validada, e o 1.0.0 permanece intacto para o GA (§D3).

⚠️ A ordem do §D10 é o inverso da leitura intuitiva: a promoção é pré-condição da cerimônia, não o contrário. Esta merge é o passo 2. O passo 3 — demonstrar a conexão ponta a ponta até a VAN, em produção — e o passo 4 (bump para 1.0.0 e tag sobre a mesma árvore, sem tocar src/) vêm depois.

⚠️ E as duas consequências que o §D10 torna inevitáveis seguem valendo: o RBAC vai a produção em bypass (src/server.ts), então todo autenticado é super-usuário em toda rota — e gravar no saida/ do bucket é enfileirar pagamento (ADR-0060). A cerimônia precisa de combinação prévia sobre valor, favorecido e reversão.

Sem quebras de contrato

O range não traz mudança incompatível. As duas últimas foram na rc.1 (#825 expectedDueDate, #811 cedenteAccountId).

Observação sobre a topologia

git merge-base --is-ancestor origin/main origin/dev acusa divergência, mas o único commit que a main tem a mais é e5faba72 — o merge commit da PR #1009, cujo conteúdo já está na dev. Não há diff na direção main → dev.

Gate

typecheck, format:check, lint e test verdes na árvore — 11844 testes, 0 falhas, 20 skipped (integração, que exige MYSQL_INTEGRATION=1). Somam-se os 20 checks verdes da #1019, incluindo as 14 matrizes de integração e o semgrep (ADR-enforcer).

Depois do merge

A tag anotada v1.0.0-rc.3 (ADR-0066 §D4) aponta para o commit de merge desta PR, e a Release do GitHub é criada sobre ela. Nenhum dos dois existe antes daqui.

https://claude.ai/code/session_015Ye4CiJx7Chni1viHZSs9L

lekadecastro and others added 23 commits September 5, 2026 15:47
…arquivo que o banco aprovou

Até hoje a evidência primária da frente Pix existia SÓ na pasta de Downloads da P.O. As
issues #923, #945, #948 e #980 citavam os dois arquivos pelo nome e nada os guardava —
perder aquela pasta apagaria a base de diff do Pix e o relatório que prova os falsos
positivos do Validador.

O QUE ENTRA

- `TESTE_PIX_BLOQUEADO_CNAB240_REV2.REM` — o arquivo que o Bradesco validou em 05/09/2026.
  É a PRIMEIRA base de diff que existe para Pix, no papel que o
  `PAG-GOLDEN_TESTE_AJUSTADO.REM` cumpre para as outras formas (inquiry-0033). Dados
  fictícios, conferidos: CNPJ 12345678/0001-95, `EMPRESA TESTE PIX`, chave sintética.
- `validador-universal-relatorio-02092026.pdf` — vale como prova dos 17 falsos positivos,
  NÃO como laudo.
- A inquiry, no molde da 0033: contexto, as 6 perguntas, a resposta literal, a triagem dos
  21 achados um a um, o que mudou no código e o que segue em aberto.

O REGISTRO SUBSTANTIVO

Os 6 pontos confirmados (45 · 009 · G100=04 · ISPB 00000000 · movimento 0 + instrução 09 ·
bloco bancário zerado) e a descoberta que vale mais que eles: o Validador Universal NÃO
APLICA O LAYOUT PIX. O portal lista Pix como layout separado de Multipag, e o resumo do
lote imprime `45-Modalidade não localizada`. 17 dos 21 achados são falsos positivos.

Isso inverte a hierarquia da skill `cnab240-bradesco` para esta modalidade: em `launchForm
45` o validador é FONTE INVÁLIDA (#980). O risco não é hipotético — a nota de
`CLEARING_PIX` já existia porque o `009` "parece errado" para quem lê o manual.

Ficam registrados também o enquadramento das duas etapas do pré-voo (TED/transf/boleto
validados em produção pelo cliente × Pix em construção, em remessa separada) e a premissa
de negócio da P.O.: a chave Pix tem de corresponder à inscrição do título, cobrada pelo
banco via `PF`/`PG` — que hoje voltam como `unknown` (#984).

⚠️ Os assets vão em `assets/0037/`, e NÃO em `0037-assets/`: o gate
`handbook-numbering.test.ts` lê o prefixo `^\d{4}-` do topo do diretório, e uma pasta
`0037-assets` colidiria com a inquiry. `INDEX.md` e `PERGUNTAS-EM-ABERTO.md` regenerados
por `scripts/handbook/inquiry-index.ts` — são derivados, não se editam à mão.

Gate: 11672 testes, 0 falhas.

Refs: #923, #945, #948, #980, #863, #984
Assisted-by: Claude-Code:claude-opus-5
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJKkFN7sQDoBS8AvteK1kZ
Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.22.3 to 3.23.1.
- [Release notes](https://github.com/sidorares/node-mysql2/releases)
- [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md)
- [Commits](sidorares/node-mysql2@v3.22.3...v3.23.1)

---
updated-dependencies:
- dependency-name: mysql2
  dependency-version: 3.23.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
…ilêncio (#863)

`digits()` existe para tirar MÁSCARA — `12.345.678/0001-99` e `12345678000199` são o mesmo
CNPJ, e converter um no outro é tradução de formato. Aplicada a um documento
ALFANUMÉRICO, ela deixava de tirar pontuação e passava a destruir conteúdo:

    digits('12ABC34501DE35', 14)  ->  '00000123450135'

Catorze dígitos, sintaticamente perfeitos, e de outra pessoa. O arquivo era aceito, o
pagamento saía, e o favorecido chegava ao banco identificado por um documento que não é o
dele. Nada no caminho reclamava: o `remittance-inspector` aprova, o validador do banco
aprova, e o erro só apareceria na conciliação ou numa contestação.

A Receita emite CNPJ com letras desde 07/2026 (ADR-0044). O layout do Bradesco na v08
(jul/2025) é anterior e declara o campo `Num` — ele não acompanhou.

O LAUDO DE 05/09 AGRAVOU ISTO, sem falar dele

Ficou estabelecido que no Pix o PSP do recebedor cruza a inscrição do arquivo com o
titular da chave no DICT, recusando com `PF` quando não bate. Uma inscrição deformada
deixou de ser só um dado errado: é a recusa do pagamento, sem que nada no ERP aponte a
causa.

O DEFEITO ERA USAR O HELPER ERRADO

`digits()` continua certo para agência, conta e CEP — ali o que ela remove é máscara, e
máscara não é conteúdo. O que faltava era um campo que soubesse que inscrição é outra
coisa. Entra `inscription()`, em `positional.ts`, e os SETE pontos de escrita passam a
usá-lo: Segmento B da transferência e do Pix (019-032), o `G031` do Segmento A, o par
sacado/cedente do Segmento J-52 (021-035 e 077-091) e os dois headers do cedente.

⚠️ A carve-out do VAZIO é obrigatória, e um teste do Segmento J-52 a cobrou: sem ela,
campo em branco passaria a sair como `inscription-alphanumeric-unsupported`, mandando
escalar ao banco um caso que é simplesmente cadastro faltando. Vazio e estouro continuam
sendo o que sempre foram.

RECUSAR, E NÃO EMITIR — porque a pergunta ao banco está aberta

As três saídas foram consideradas. Emitir com as letras num campo `Num` produz arquivo que
o banco pode recusar depois do NSA queimado. Remover as letras é o defeito. Converter (o
`ASCII − 48` da RFB) não é escolha que se faça sozinho: aquela regra é para CALCULAR o
dígito verificador, não para transmitir, e usá-la aqui inventaria um formato que o banco
não pediu.

A pergunta — o Bradesco aceita CNPJ alfanumérico no CNAB 240, e em que forma? — está
registrada na #863 e segue sem resposta. Enquanto isso, recusar com nome próprio é a única
saída que não inventa layout nem paga errado.

A CONTRAPARTE NO PRÉ-VOO, FORA DO SWITCH DE ROTA

`readAlphanumericInscription` é a única régua do arquivo verificada fora do `switch`, e é
deliberado: ela não é propriedade da rota. TODA rota com emissor escreve a inscrição —
são sete pontos de escrita em três rotas. Dentro do `switch` seriam três cópias, e uma
rota nova nasceria sem ela.

O motivo é `unmappable`, não `malformed`: o CNPJ com letras está BEM FORMADO, e mandar
"corrigir o formato" mandaria o operador estragar uma inscrição correta.

⚠️ É a única lacuna do módulo cuja ação não é "vá ao cadastro" — é ESCALAR. Por isso o erro
sobe até a application com nome próprio e converge para `remittance-build-failed`, e não
para `remittance-payments-unavailable`, que diria ao operador "confira o pré-voo" quando
não há nada lá que ele possa consertar.

A FIXTURE QUE PRECISOU MUDAR, E É UM ACHADO

`PAYEE_DOCUMENT` era `'inscricao-opaca'`, com razão escrita: provar que a régua "só
pergunta se HÁ inscrição". A afirmação deixou de ser inteiramente verdadeira, e as quatro
suítes que a usavam quebraram — corretamente.

A distinção que sobrevive é fina e ficou registrada na fixture: a régua continua NÃO
validando CPF/CNPJ (não confere dígito nem comprimento — há teste fixando que `'123'`
passa). Ela só recusa o que `digits()` destruiria. Um valor sem nenhum dígito não
distinguia os dois casos: falhava pelas duas razões ao mesmo tempo.

Gate: typecheck + format:check + lint + 11692 testes, 0 falhas.

⚠️ Empilhado sobre o PR #982 (que está sobre o #981).
⚠️ NÃO fecha a #863: o CA1 admite as duas saídas, e esta é a defensiva. A issue fecha
quando o banco responder como quer receber a inscrição alfanumérica.

Refs: #863, #948 (CA11), #891, #856
Assisted-by: Claude-Code:claude-opus-5
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJKkFN7sQDoBS8AvteK1kZ
…ção vazia (#863)

Três achados da revisão do #983.

1. Documento só com pontuação divergia entre as duas pontas. `'---'`, `'.'` e `'./-'`
sobrevivem ao `trim()` e normalizam para vazio: o emissor sempre os viu como campo sem
conteúdo (`numeric-field-invalid`), enquanto o pré-voo os via como inscrição PRESENTE e —
não sendo numéricos — concluía "alfanumérica", mandando ESCALAR ao banco um cadastro que só
está incompleto. É a classe de divergência que a #837 fechou, e custa mais aqui do que nas
outras: `unmappable` é a única lacuna do módulo cuja ação não é "vá ao cadastro".

`hasInscription` passa a ser a pergunta única de presença, e o caso vira `missing` — a mesma
ação do campo em branco. Só o boleto tinha régua de presença (`readBilletPayee`), então sem
esta linha o caso passaria do pré-voo direto ao emissor na TED, com o NSA já queimado.

2. O reader mantinha `cleanDocument`, cópia idêntica de `normalizeInscription` — a mesma
configuração que produziu a #863: duas decisões sobre o mesmo dado, nenhuma citando a outra,
concordando por acidente até o dia em que uma mudasse. Passa a importar a do domínio, e o
acoplamento fica declarado: `documentTypeOf` mede o COMPRIMENTO do que a normalização
devolve, então qualquer mudança nela reclassifica pessoa física e jurídica no arquivo.

3. Comentários. Dois ainda citavam `digits()` para inscrição, que deixou de ser verdade. E o
de `readAlphanumericInscription` prometia cobrir "sete pontos de escrita": a régua lê só
`candidate.payee.document`, então são QUATRO. Os outros três — header de arquivo, header de
lote e sacado do J-52 — escrevem a inscrição da própria empresa, que não chega num
`PayoutCandidate`. Risco baixo, porque a empresa já existe e sua inscrição é numérica, mas o
comentário prometia cobertura que não existe.

Gate: typecheck, lint e format:check verdes; 11695 testes, 0 falhas.

Assisted-by: Claude-Code:claude-opus-5
Claude-Session: https://claude.ai/code/session_01YZHvfYtCefkh7AUNwxnYaP
…ada efetiva

`maxLimit` filtrava os limites nulos antes do `Math.max`, lendo `null` como
ausência de dado. Mas `null` é "sem teto" — a regra binária da #299, que
`approval-policy.ts:28` enforça. As duas leituras conviviam em arquivos
diferentes e nada as confrontava.

O efeito era o inverso do pretendido: acrescentar um papel COM alçada a quem já
tinha um SEM alçada passava a barrá-lo no teto do papel novo. Ganhar papel tirava
permissão. Medido contra MySQL 8.4 real: com o papel irrestrito sozinho, o
usuário aprovava R$ 6.000; o mesmo usuário com o irrestrito MAIS um de teto
R$ 5.000 era recusado com `approver-limit-exceeded`.

Agora o `null` absorve o máximo. `listApproversWithAuthority` compartilha o mesmo
`maxLimit`, então o dropdown de Aprovador deixa de anunciar teto inexistente.

O CA6f cobre o cruzamento `null` × número, que as fixtures nunca produziam: `u2`
tinha dois papéis ambos com limite, `u4` tinha um só e sem limite — os dois casos
nunca se encontravam.

Refs: #881
Assisted-by: Claude-Code:claude-opus-5
…MySQL real

CA4 da #881 — o teste de rota que roda sem o seed de dev. Todo teste de aprovação
existente usa `fakeAuthorityPort` ou o seed que concede o catálogo inteiro ao
admin; nenhum exercita a junção que o gate do #609 lê de fato. É o vão por onde o
defeito de produção passou.

Percorre o provisionamento inteiro só por HTTP, com `rbacMode: 'enforced'` (sob
`bypass` o guard vira no-op e o teste não provaria nada sobre permissão): o admin
cria o papel, cria a conta, atribui; a conta ativa a senha pelo token do convite
— lido do `auth_outbox`, a mesma fonte do worker `email-dispatch`, sem minter
fake —, autentica e aprova. O caso de controle prova o outro lado: conta sem o
papel é recusada no mesmo caminho.

É o primeiro teste HTTP do repositório contra driver `mysql`; os demais montam
`driver: 'memory'` com hooks de auth falsos.

Três armadilhas ficaram documentadas no próprio arquivo, porque nenhuma delas dá
erro legível:

- o `financial` nomeia o campo `writerUrl`, não `connectionString`. O nome errado
  não falha: `composition.ts:777` faz `?? ''` e o mysql2 pendura em silêncio.
- o fechamento é incremental. Um teardown montado no fim do `before` não fecha os
  pools quando o `before` falha no meio; o event loop não esvazia, o processo não
  morre, e o resumo de falhas — que só é impresso na saída — nunca aparece. Uma
  falha de 400 ms se disfarça de travamento infinito.
- o `after` limpa `auth_password_reset`. Seis suítes irmãs fazem
  `delete from auth_user` sem limpar essa filha, e a FK é RESTRICT: o resíduo
  daqui as derrubaria com ER_ROW_IS_REFERENCED_2.

Verificado nas três medições que a rule de testes exige: banco limpo, repetição
sem recriar, e convivência com a suíte irmã.

⚠️ Ainda NÃO está no manifesto de `scripts/ci/test-integration.ts`, que hoje só
lista testes de persistência — enquanto não entrar, não roda no CI.

Refs: #881
Assisted-by: Claude-Code:claude-opus-5
Achados do code-review desta branch. O primeiro é regressão introduzida pelos dois
commits anteriores; os demais são qualidade do teste novo.

`escalate` filtrava `c.limit !== null`, excluindo da cascata justamente os aprovadores
irrestritos — os únicos que cobrem qualquer valor. O defeito é antigo, mas a correção
do `maxLimit` o agravou: quem acumula um papel sem teto e um com teto passou a se
projetar como irrestrito e, com isso, sumiu da cascata. Cenário concreto: documento
cujo aprovador indicado estoura o limite deixava de escalar e a CRIAÇÃO falhava com
`no-approver-with-sufficient-limit` (`save-document.ts:274`, `submit-draft.ts:71`),
havendo aprovador apto.

Agora o irrestrito é candidato, e por ÚLTIMO: quem tem teto suficiente continua tendo
precedência, preservando o "menor que basta" que evita escalar ao diretor quando o
gerente resolve. CA6 e CA7 cobrem os dois lados; o CA3 foi renomeado, porque chamava
o `null` de ignorado e isso deixou de ser verdade.

O contrato do port (`user-read.ts:22`) dizia `null = sem alçada (não aprova)` — o
oposto do que o código faz desde a #299. Corrigido: é a fonte cross-módulo que o
financial lê, e deixá-la invertida é o padrão "registro mente sobre o código".

No teste novo:
- usa `mysqlTestUrl()` em vez de interpolar a porta à mão, que reintroduzia o bug da
  porta vazia (`MYSQL_PORT=''` → `mysql://…@127.0.0.1:/core`) que o helper da #500
  existe para impedir, e descartava a precedência de `MYSQL_TEST_URL`;
- `after` guarda `handle` e usa `try/finally`: sem isso, um `before` que falhe antes
  das migrations estoura TypeError síncrono — que o `.catch` não intercepta — e o
  teardown nunca roda, que é o travamento que o fechamento incremental evita;
- limpa `fin_*` na entrada e na saída (o arquivo cria dois documentos e seus títulos);
  `fin_categories`/`fin_cost_centers` ficam de fora por terem seed de migration;
- o controle assere 403 e o documento seguir `Open`. `notEqual(200)` passava com 401,
  404 ou 500 — verde pelo motivo errado, sem provar que foi o RBAC que negou.

Fica em aberto, por exigir decisão de infraestrutura: o teste HTTP ainda não está no
manifesto de `scripts/ci/test-integration.ts`. Registrá-lo pede ou uma suíte nova na
matriz de `.github/workflows/integration.yml`, ou acrescentar as envs `S3_*`/`VAN_S3_*`
à suíte `financial` inteira — que hoje roda ~100 testes sem elas.

Refs: #881
Assisted-by: Claude-Code:claude-opus-5
…sql2-3.23.1

chore(deps): Bump mysql2 from 3.22.3 to 3.23.1
…tipag

docs(inquiries): a inquiry-0037 versiona o laudo do Multipag Pix e o arquivo que o banco aprovou
… o emissor ganha a rede que faltava

Os dois achados do code-review desta branch.

**1. Dado de cadastro em fixture (anti-padrão #9).** O documento que seis arquivos de teste usavam
como "sintético" não era: tinha DV válido e correspondia à inscrição raiz de uma instituição
financeira existente. Os repositórios são públicos, e o gate mecânico não alcança este caso —
`bank-fixture-masking.test.ts` cobre convênio, e a rule do CNAB registra que "o resto depende de
julgamento".

Trocado pelo exemplo que o próprio repositório já consagra em `.claude/rules/domain.md`, sem
correspondência a inscrição emitida e já usado em testes de `contracts`, `etl` e `jobs`. Nada
dependia de o valor ser um CNPJ existente: `isCnabEmittableInscription` só exige dígitos.

⚠️ O mesmo valor sobrevive em três arquivos FORA desta branch (um em `financial`, dois em
`partners`). Não entraram aqui para não misturar módulos num PR de CNAB.

**2. O teste prometia uma rede de regressão que não existia.** O comentário afirmava que, se alguém
trocasse `inscription` de volta por `digits`, o caso falharia "apontando exatamente o que voltaria a
acontecer". Não falharia: o arquivo chama `inscription()` DIRETO e não monta registro nenhum — ele
prova que a régua existe, não que o emissor a usa. É a distinção entre USO e MENÇÃO, que é como o
defeito original sobreviveu.

E o teste de ponta a ponta não cobre a lacuna: em `generate-remittance.test.ts` (CA3) a recusa é do
PRÉ-VOO, que barra antes de o emissor ser chamado. A camada que de fato produziria o arquivo aceito
com a inscrição de outro não tinha teste executável em lugar nenhum.

Acrescentados quatro casos que montam os registros de verdade — `fileHeader`, `batchHeader`,
`segmentB` e `segmentBPix` — com documento alfanumérico, exigindo
`inscription-alphanumeric-unsupported`. Por registro, e não por varredura de texto no fonte: o que
precisa ser verdade é comportamento, e um grep acusaria também quem só menciona o nome.

**Prova invertida (medida, não suposta):** revertendo os dois call sites de `multipag-records.ts`
para `digits()`, os 8 casos que já existiam passam TODOS — inclusive o que prometia falhar — e só os
casos novos acusam. É a demonstração de que a rede não existia.

Gate verde: typecheck + format:check + lint + test (11.812 testes, 0 falhas).

Refs: #863
Assisted-by: Claude-Code:claude-opus-5
…ric-863-v2

fix(financial): CNPJ alfanumérico deixa de virar outra inscrição em silêncio (#863) — traslado do #983
…l-sem-teto

fix(auth,financial): papel sem alçada deixa de restringir quem já era irrestrito
…o /me acabou de liberar

Sob `AUTH_RBAC_MODE=bypass` (ADR-0052) existem **três** camadas que decidem sobre permissão, e só
duas o honravam: `authorize` vira no-op ✅, o `GET /me` anuncia o catálogo inteiro
(`list-user-permissions.ts:35`) ✅, e a `approval-policy` do domínio (`approval-policy.ts:27`) lia
`canApprove` do banco cru e recusava ❌.

**Medido em 09/09, ambiente local:** o `/me` de um usuário provisionado pelo ETL legado devolvia
**47 permissões, incluindo `payable:approve`**; o banco lhe dava **uma**, e não era essa. Ao aprovar,
ele tomava um `approver-missing-permission` que contradizia o que o `/me` acabara de lhe dizer —
depois de o `authorize` no-op o ter deixado entrar até o fundo para morrer lá. Do lado do usuário,
prometer e negar a mesma coisa não se distingue de defeito.

**A decisão está no ADR-0069**, `supersedes` parcial do 0052: sob bypass a `approval-policy` deixa de
barrar por PERMISSÃO. Aplicada como **decorator** — `withRbacBypass` sobre o
`ApproverAuthorityReader` —, composto num ponto só, no composition root do `financial`. Embutir isto
no `user-read.drizzle.ts` faria o `auth` mentir sobre os próprios papéis para **todo** consumidor,
inclusive a tela de gestão de acessos; comportamento transversal é decorator e nunca código dentro do
provedor. O `server.ts` traduz o modo num booleano — o `financial` não passa a conhecer `RbacMode`,
que é vocabulário do `auth`.

⚠️ **O decorator envolve o reader que vai ao `approveDocument`, e SÓ ele** (`depsForApprove`). O
mesmo port responde a duas perguntas, e só a primeira é controle de acesso:

- `approveDocument` — o **chamador autenticado** pode aprovar este valor? (#609) → **afrouxada**;
- `saveDocument`/`submitDraft` — o `approverRef` **indicado** tem alçada? (#289/#297) → **enforçada**.

Compor no `deps` compartilhado é o caminho óbvio, e foi o primeiro que escrevi. Afrouxa também a
segunda — e o efeito não é simétrico: a indicação **grava** `approverRef` apontando para quem não é
aprovador, e a linha **sobrevive ao religar da flag**. Seria dano que o #634 não desfaz. **Prova
invertida (medida, não suposta):** devolvendo o decorator ao `deps`, o `POST /documents` responde
**201 com o `approverRef` inválido persistido**, e o caso novo da borda acusa.

**Três limites são parte da decisão, e é por eles que ela se desfaz por engano.** Os cinco casos de
`approver-authority-reader.rbac-bypass.test.ts` existem para fixá-los:

- `null` continua `null` — o bypass afrouxa **permissão**, nunca a existência do sujeito, e
  `approver-not-found` sobrevive;
- o **teto** passa intacto — quem tem papel com alçada continua limitado por ele (#299/#609);
- `list` passa intacto — `escalate` é **roteamento de negócio**, não controle de acesso: mexer nele
  mudaria para quem o documento é encaminhado, não quem pode aprová-lo.

Na borda, o par bypass ligado × desligado sobre a **mesma** autoridade (`canApprove: false`) é o que
prova que o flag é a variável, e não outra diferença de montagem: ligado dá 200 e `Approved`;
desligado dá 422, o documento permanece `Open` e o slug interno não vaza no body.

⚠️ **O custo aceito, e é o principal: sob bypass todo autenticado aprova qualquer valor.** Quem não
tem papel aprovador tem teto `null` — `maxLimit` devolve `null` para conjunto vazio
(`user-read.drizzle.ts:42`) — e `null` é **SEM TETO** pela regra binária do #299
(`approval-policy.ts:28-31`). Então "o teto continua valendo" **não protege** exatamente a população
que esta mudança libera. Reabre, enquanto o bypass durar, o buraco que o #609 fechou — e **vale em
produção**, porque `server.ts:165` fixa o modo por código, com o risco assumido por escrito no #634.

**O gatilho de reversão é o #634 — e ele custa DUAS linhas, não zero.** São duas marcas `← religar`
no `server.ts`: o `rbacMode` fixado e o `rbacBypass: true` da composição do `financial`. Apagar só a
primeira religa a rota e o `/me` e **deixa a policy do domínio afrouxada**, com o RBAC já enforçado —
o pior dos dois mundos, e **nada mecânico acusa**: não há erro de tipo, teste vermelho nem lint. O
literal não deriva de `rbacMode` porque o ESLint recusa comparação que o compilador prova sempre
verdadeira (a mesma razão que impede envolver o banner num `if`); o preço é a nota, que é o único
guarda existente. O ADR-0052 ganha o ponteiro para cá — sem ele, quem abrir o 0052 lê como norma
corrente um alcance que já mudou, que é o defeito que a `auth-module.md` já registra para o par
0024/0055.

As citações passam a ser por **marcador**, não por número de linha: a primeira versão deste ADR dizia
`server.ts:165`, e as duas linhas que este mesmo commit acrescentou empurraram o alvo para a `:166` —
a `:165` virou justamente o `resolveRbacMode` que a instrução manda voltar a usar. Quem seguisse a
citação apagaria a leitura da env e deixaria o hardcode de pé.

**Alternativa recusada pelo dono:** consertar o `/me`, subtraindo `payable:approve` do catálogo
anunciado sob bypass e mantendo a policy a barrar. Também elimina a contradição, e tinha precedente
no próprio repositório (`revoke-role.ts:88` chama `authorize` direto para sobreviver ao bypass). Foi
recusada porque, sob bypass, a promessa é *"todo autenticado é super-usuário"*, e uma regra de
domínio que segue cobrando permissão é **exceção** à promessa, não correção dela. A análise ficou
escrita no ADR para quem for reabrir.

⚠️ Esta decisão **não alcança** outras policies de domínio que leiam permissão do banco. Hoje esta é
a única identificada, e o `financial` já recebe o booleano — seguir é barato, mas é deliberado.

Gate verde: typecheck + format:check + lint + test (11.844 testes, 0 falhas, 20 skip esperados).

Refs: #634, #609, #299

Assisted-by: Claude-Code:claude-opus-5
Claude-Session: https://claude.ai/code/session_01T3DKgMk376thR2ygRa3rA7
`node --test` sem `--test-concurrency` dimensiona o paralelismo por `os.availableParallelism()`. Na
máquina de desenvolvimento isso devolve **28**, contra **15 GiB** de RAM — e cada worker do runner
custa ~370 MB medidos (Node completo com `--experimental-strip-types` + `--enable-source-maps`). O
default pedia ~10,4 GB num box que já roda a sessão gráfica: o gate empurrava desktop, `mysqld` e
navegador para a swap de 4 GiB, que não voltava.

Não é micro-otimização de estilo. `pnpm test` é chamado pelo hook `Stop`
(`stop-quality-gate.sh:97`) e pelo pre-commit, então o pico acontece a **cada turno encerrado**, não
só quando alguém roda a suíte à mão.

Medido depois do cap: 6 workers, **2,2 GB de pico**, ~155s, suíte verde.

O cap não muda o que é executado, só quantos arquivos concorrem. No CI (`ubuntu-latest`, 4 vCPU /
16 GiB) ele **sobe** a concorrência de 4 para 6, a ~2,2 GB de pico — folgado dentro do orçamento do
runner. A suíte de integração continua em `--test-concurrency=1`, por outro motivo: isolamento de
banco (`.claude/rules/testing.md`).

⚠️ Não elevar nem remover "para acelerar" sem antes medir `free -h` contra `nproc`. A razão RAM/core
aqui é ~0,54 GB e o worker quer 0,37 GB. Suíte lenta demais se resolve reduzindo o custo por worker
ou dividindo a suíte, não soltando o paralelismo.

Assisted-by: Claude-Code:claude-opus-5
Claude-Session: https://claude.ai/code/session_01T3DKgMk376thR2ygRa3rA7
…a recusar

GHSA-2x7j-588g-ccc2 (`high`, CVSS 7.5 `AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H`) foi publicada em
**08/09/2026 21:33 UTC**: complexidade quadrática no `addressparser` do nodemailer permite DoS
remoto por lista de endereços forjada. Vulneráveis `<9.1.0`; corrigido em `>=9.1.0`.

O pin era `9.0.1` (`package.json:124`, de c66ff54), e nodemailer é `dependencies` — então quem
acusa é o job **bloqueante** `audit (produção — blocking)`, não o report-only:

    pnpm audit --prod --audit-level=high --ignore-registry-errors  →  exit 1
    │ Paths │ .>nodemailer │      4 vulnerabilities found · 3 moderate | 1 high

O vermelho não veio de diff nenhum: é o relógio. A advisory saiu 28h antes do run do PR #1015, e a
`dev` só está verde porque o último `audit` dela é de **06/09** — dois dias antes de a advisory
existir. Qualquer branch que rodar o gate a partir de agora pega o mesmo exit 1.

Sobe para **9.1.1**, a última da linha 9.x (publicada em 01/09, fora da quarentena de
`minimumReleaseAge: 1440`). A `10.0.2` existe, mas saiu em **09/09 20:19 UTC** — major de menos de
24h, que a quarentena recusaria de todo modo, e que exigiria revisar a API antes: o adapter em
`src/modules/notifications/adapters/email/nodemailer.ts:15-17` importa `createTransport` mais os
tipos de `nodemailer/lib/smtp-pool` e `smtp-transport`, caminhos internos que major move sem aviso.

Pin exato porque `dependencies` carrega o que serve tráfego (`.claude/rules/supply-chain.md`), e a
graça do pin é exatamente esta: subir de versão vira ato deliberado, visível em diff de PR.

Os demais `high` do relatório — axios, form-data, undici — entram por `@usebruno/cli`, que é
devDependency; ficam no job report-only, por desenho, e este commit não os toca.

Verificado: `pnpm audit --prod --audit-level=high` responde `No known vulnerabilities found`.
Gate completo verde — typecheck, format:check, lint e 11.844 testes, 0 falhas, 20 skip esperados.

Assisted-by: Claude-Code:claude-opus-5
Claude-Session: https://claude.ai/code/session_01G17M4M3A3c6bd6jmCPAk2W
…rbac-bypass

fix(financial): sob o bypass do RBAC, a alçada deixa de recusar quem o /me acabou de liberar
Terceira candidata da linha 1.0.0, sobre 22 unidades de entrega desde a v1.0.0-rc.2. A árvore
segue candidata: pela escada do ADR-0066 §D1 ela vai a produção declarando que ainda não foi
validada, e o número 1.0.0 permanece intacto para o GA (§D3). A cerimônia do §D10 não rodou —
a #873 continua aberta.

O que o range traz: a alçada do aprovador deixa de recusar quem o /me acabou de liberar sob o
bypass do RBAC (#1015, ADR-0069) e papel sem teto volta a ser candidato na cascata (#1011); o
CNPJ alfanumérico para de virar outra inscrição em silêncio (#1010); o DV da agência do
favorecido passa a ser conferido por cálculo (#1008); o pré-voo do Pix ganha governança e
exclusividade de arquivo (#1004, #982, #981); a sequência de NSA passa a ser do convênio (#997)
e a conta-cedente ganha chave natural canônica, reabertura e saldo editável (#1001, #998, #999).

O CHANGELOG foi gerado por `pnpm release:notes v1.0.0-rc.2..HEAD` (ADR-0066 §D5), nunca
redigido. As seções da rc.2 e da rc.1 foram preservadas à mão porque o gerador monta o documento
do zero e sobrescreve o arquivo — o mesmo gap registrado no commit da rc.2, ainda aberto.

Gate verde na árvore: typecheck, format:check, lint e test — 11844 testes, 0 falhas, 20 skipped
(integração, que exige MYSQL_INTEGRATION=1).

Assisted-by: Claude-Code:claude-opus-5
Claude-Session: https://claude.ai/code/session_015Ye4CiJx7Chni1viHZSs9L
chore(release): 1.0.0-rc.3 — a alçada do aprovador e o pré-voo do Pix
Copilot AI lite review requested due to automatic review settings September 10, 2026 18:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@GabrielAderaldo
GabrielAderaldo merged commit a16e1de into main Sep 10, 2026
20 checks passed
Comment on lines 318 to +328

const data = checkRouteData(candidate, route);

// #863 — ACUMULA com o que a rota já apontou, em vez de substituir. Quem tem chave Pix longa demais
// E inscrição alfanumérica precisa ver as duas: são pendências independentes, e resolver uma não
// libera o título. Mesma disciplina do boleto, que soma código de barras e inscrição.
const inscriptionGap = readAlphanumericInscription(candidate, route);
if (inscriptionGap !== null) {
const routeGaps = data.status === 'incomplete' ? data.gaps : [];
return incomplete(route, immutable([...routeGaps, ...inscriptionGap.gaps]));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: readAlphanumericInscription é chamada incondicionalmente para toda VanRoute, incluindo 'tax-guide' — mas essa rota não tem emissor e nunca escreve inscrição do favorecido em nenhum segmento CNAB (ROUTES_WITH_ISSUER em van-routes.ts exclui tax-guide). Isso contradiz o próprio comentário do código, que em duas passagens documenta que a guia fica fora dessa exigência (ver linhas 222-225 e 246-251).

Efeito prático: uma guia de recolhimento cujo fornecedor tenha CNPJ alfanumérico (12ABC34501DE35, válido desde a ADR-0044) ou documento só-com-pontuação ('---') passa a retornar incomplete com payee-document/unmappable — a única lacuna do módulo cuja instrução ao operador é "escalar ao banco" — em vez do no-issuer correto, sobre um campo que essa rota nunca escreve. O caso não tem cobertura de teste: o loop de rotas em payout-readiness.test.ts cobre só pix/transfer/billet, e o teste que hoje afirma no-issuer para guia só passa porque a fixture usa documento numérico.

Sugestão: condicionar a chamada a hasRemittanceIssuer(route), por exemplo:

const inscriptionGap = hasRemittanceIssuer(route) ? readAlphanumericInscription(candidate, route) : null;

e adicionar o caso tax-guide + documento alfanumérico → no-issuer ao teste parametrizado.

Ref.: CLAUDE.md (regra de fonte-de-verdade — o comentário do próprio arquivo é a norma que o código deveria seguir e não segue).

@claude

claude Bot commented Sep 10, 2026

Copy link
Copy Markdown

Code review

Revisei este merge de release com foco em bugs e conformidade com CLAUDE.md. Um achado foi para comentário inline (bug de classificação em payout-readiness.ts). O achado abaixo não pôde ser comentado inline porque os arquivos envolvidos não fazem parte do diff deste PR — segue aqui.

CLAUDE.md — anti-padrões #7 e #9: vazamento de CNPJ real de instituição financeira em fixture, não registrado como issue

O commit 578b66d9 (incluído neste merge) corrige, em 5 arquivos de tests/modules/financial/, um valor de documento que tinha DV válido e coincidia com a inscrição raiz de uma instituição financeira real, substituindo-o pelo valor sintético canônico do repositório. A mensagem do próprio commit declara que o mesmo valor sobrevive em três arquivos fora do escopo deste PR, por isolamento de módulo:

  • tests/modules/financial/application/use-cases/pix-remittance-end-to-end.test.ts
  • tests/modules/partners/adapters/outbox/outbox.in-memory.test.ts
  • tests/modules/partners/adapters/persistence/outbox-repository.drizzle-mysql.test.ts

Confirmei que o valor ainda está presente nos três arquivos hoje, e uma busca em gh issue list (por termos relacionados ao vazamento, ao commit e aos arquivos) não encontrou nenhuma issue rastreando esse remanescente.

Isso viola dois anti-padrões do CLAUDE.md raiz:

7. Consertar problema fora do escopo atual — registrar via skill issue-report e seguir a tarefa.

9. Pôr dado real de cadastro em fixture — convênio, conta, agência, documento de parceiro. Os três repositórios são públicos [...] os demais dependem de julgamento.

Adiar o conserto por isolamento de módulo é uma decisão de escopo legítima — mas a regra exige que a lacuna seja registrada como issue, não apenas mencionada em prosa de commit. Sugiro abrir a issue (sem citar o valor do documento no título/corpo, por instrução do próprio CLAUDE.md) antes de fechar este PR, ou logo em seguida.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants