Repository navigation
chore(release): promove a 1.0.0-rc.3 para a main — a alçada do aprovador e o pré-voo do Pix - #1020
Conversation
…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
…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
|
|
||
| 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])); | ||
| } |
There was a problem hiding this comment.
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).
Code reviewRevisei 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 CLAUDE.md — anti-padrões #7 e #9: vazamento de CNPJ real de instituição financeira em fixture, não registrado como issueO commit
Confirmei que o valor ainda está presente nos três arquivos hoje, e uma busca em Isso viola dois anti-padrões do CLAUDE.md raiz:
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. |
O que entra
Integração da
devnamain, que é o deploy de produção. São 6 unidades de entrega desde av1.0.0-rc.2, e a última delas é o próprio ato de promoção:b89ea9bdchore(release): 1.0.0-rc.3— bump do manifesto + CHANGELOG gerado (#1019)72cf83a8/meacabou de liberar (#1015)8870d3e78a1b9148a6e8f87a87ad9be3O
CHANGELOG.mddarc.3cobre um range maior — as 22 unidades desde a tagv1.0.0-rc.2— porque quatro PRsdev → mainintermediá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.
src/modules/financial/domain/payout/payout-readiness,inscription)src/modules/financial/domain/document/approval-policy.tssrc/modules/financial/adapters/read/approver-authority-reader.rbac-bypass.tssrc/modules/auth/.../user-read.drizzle.ts+ portsrc/modules/financial/adapters/cnab/src/server.ts,adapters/http/composition.tshandbook/architecture/adr/0069-*.mdhandbook/inquiries/0037-*.REMque o banco aprovoupackage.json,pnpm-lock.yaml--test-concurrency=6Sem migration, sem mudança de env, sem infra.
Por que
-rc.3e não1.0.0A #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.0permanece intacto para o GA (§D3).1.0.0e tag sobre a mesma árvore, sem tocarsrc/) vêm depois.bypass(src/server.ts), então todo autenticado é super-usuário em toda rota — e gravar nosaida/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(#825expectedDueDate, #811cedenteAccountId).Observação sobre a topologia
git merge-base --is-ancestor origin/main origin/devacusa divergência, mas o único commit que amaintem a mais ée5faba72— o merge commit da PR #1009, cujo conteúdo já está nadev. Não há diff na direçãomain → dev.Gate
typecheck,format:check,lintetestverdes na árvore — 11844 testes, 0 falhas, 20 skipped (integração, que exigeMYSQL_INTEGRATION=1). Somam-se os 20 checks verdes da #1019, incluindo as 14 matrizes de integração e osemgrep (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