From adbe79531964dcf533c6b7e2bb6b5972882ce7d1 Mon Sep 17 00:00:00 2001 From: Mozurok Date: Fri, 4 Sep 2026 14:44:14 -0300 Subject: [PATCH] fix(drift): nao reprovar quando o CGIBS recusa o runner O quarto alvo, adicionado no #7, deixou main vermelha no primeiro run: www.cgibs.gov.br da connect timeout em 443 a partir do runner do GitHub, tres tentativas, enquanto consumo.tributos.gov.br e piloto-cbs.tributos.gov.br respondem do mesmo runner (run 33901733965). E bloqueio do host, nao rede instavel, e nao e coisa que a gente conserte. Reprovar por isso e o defeito contra o qual o proprio arquivo ja advertia: um detector que grita lobo toda semana acaba silenciado. Mas rebaixar o alvo inteiro para "warn" tambem estaria errado, porque perderia o sinal que importa, que e drift. Entao a severidade de indisponibilidade passa a ser separada da severidade de drift, por alvo. O CGIBS declara severidadeIndisponivel: "ignore": aparece no relatorio, nao reprova, nao abre issue. Drift ali continua reprovando. Alvo sem o campo mantem exatamente o comportamento de antes. O limite fica declarado no README e na rotina, em vez de implicito: esse alvo so compara de verdade rodando de uma rede que alcance o host, entao para esse contrato a checagem manual e a cobertura principal, nao a redundancia. Foi justamente uma limitacao nao declarada que custou os 11 dias do v1.1.0. 6 testes novos, 36 no total. --- README.md | 6 ++-- docs/watch-routine.md | 3 +- scripts/drift-check.mjs | 33 ++++++++++++++++++++-- scripts/drift-check.test.mjs | 55 ++++++++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 743fe82..cb458d0 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,9 @@ Cada pacote tem README próprio com exemplos completos. O mock também roda via ## Contrato oficial e drift -Os artefatos oficiais (o OAS da Plataforma, manuais, NTs) estão em `vendor/` com SHA-256 pinado em `vendor/MANIFEST.md`. Um workflow semanal compara quatro alvos com os vendorados: os três contratos hospedados da família Calculadora, por conteúdo normalizado, e o inventário de artefatos da [página do Split Payment no CGIBS](https://www.cgibs.gov.br/split-payment), que é onde o contrato da Plataforma é publicado. Divergência vira issue, nunca atualização silenciosa. A Calculadora oficial não é redistribuída (a distribuição não declara licença); use `scripts/download-calculadora.sh`. +Os artefatos oficiais (o OAS da Plataforma, manuais, NTs) estão em `vendor/` com SHA-256 pinado em `vendor/MANIFEST.md`. Um workflow semanal compara quatro alvos com os vendorados: os três contratos hospedados da família Calculadora, por conteúdo normalizado, e o inventário de artefatos da [página do Split Payment no CGIBS](https://www.cgibs.gov.br/split-payment), que é onde o contrato da Plataforma é publicado. Divergência vira issue, nunca atualização silenciosa. + +O alvo do CGIBS tem uma limitação que vale declarar: `www.cgibs.gov.br` não aceita conexão do runner do GitHub Actions (connect timeout, enquanto os endpoints em `tributos.gov.br` respondem do mesmo runner). Na prática, esse alvo só compara de verdade quando o `drift-check` roda de uma rede que alcança o host, como a máquina do mantenedor. No CI ele reporta indisponibilidade sem reprovar o run. Drift ali continua reprovando, quando alcançável. Por isso a [rotina semanal](docs/watch-routine.md) continua sendo a cobertura real desse contrato, e não um complemento opcional. A Calculadora oficial não é redistribuída (a distribuição não declara licença); use `scripts/download-calculadora.sh`. A severidade é por alvo: portal e api-split reprovam o run tanto em divergência quanto em indisponibilidade; o piloto sinaliza sem reprovar, porque é infraestrutura de teste com janela até 31/12/2026 e mudar antes do portal é o comportamento esperado dele. @@ -75,7 +77,7 @@ Monorepo pnpm: `pnpm install && pnpm -r build && pnpm -r test` (Node >= 22). Con Engineering notes: -- The official contracts are vendored with a **pinned SHA-256**; a weekly CI diffs four targets against the vendored copies and **opens an issue on drift** instead of updating silently: the three live Calculadora contracts, by normalised content, plus the artifact inventory of the [CGIBS Split Payment page](https://www.cgibs.gov.br/split-payment), which is where the Platform contract itself is published. Severity is per target: the production endpoints fail the run, the pilot one reports without failing (it is test infrastructure and moving ahead is its job). +- The official contracts are vendored with a **pinned SHA-256**; a weekly CI diffs four targets against the vendored copies and **opens an issue on drift** instead of updating silently: the three live Calculadora contracts, by normalised content, plus the artifact inventory of the [CGIBS Split Payment page](https://www.cgibs.gov.br/split-payment), which is where the Platform contract itself is published. Severity is per target: the production endpoints fail the run, the pilot one reports without failing (it is test infrastructure and moving ahead is its job). The CGIBS target carries a stated limitation: `www.cgibs.gov.br` refuses connections from the GitHub Actions runner, so in CI it reports unreachable without failing the run, and it only really compares when run from a network that can reach the host. Drift there still fails when reachable. - **The Platform contract moved to v1.1.0 on 2026-08-24; the packages still generate from v0.0.10.** The new spec renames all 12 stream routes (`{idPsp}/tributos` to `{cnpjRaizPspRecDir}/transacoes`), adds 3 Mechanism-of-Occurrences routes, makes the `X-JWS-Signature` header required on all 43 operations, and grows the schema set from 57 to 78. v1.1.0 is vendored here but does not feed codegen yet: migrating is a major bump for both packages and has its own task. Until then, treat v1.1.0 as the source of truth if you integrate the real platform. Until 2026-09-04 this README claimed that spec had no public endpoint and so could not be monitored; that was false, and the error cost 11 days of undetected drift. - Money math is **integer cents only** (BigInt), never floating point, truncated toward zero to match the official rounding. - The interactive [demo](https://mozurok.github.io/splitbr/) computes every figure with the **same published function the SDK ships**, so it doubles as a live validation of the packages. diff --git a/docs/watch-routine.md b/docs/watch-routine.md index 2375ebc..481440e 100644 --- a/docs/watch-routine.md +++ b/docs/watch-routine.md @@ -43,7 +43,8 @@ A auditoria de 2026-09-04 respondeu, com evidência, a pergunta que estava em ab - O custo foi medido: o **OpenAPI v1.1.0** saiu em 24/08/2026 e passou **11 dias** sem detecção, junto com o Manual de Integração v1.1.0. Nenhuma das duas publicações apareceu em issue, PR ou entrada de novidades. - Outros quatro artefatos publicados entre 04/08 e 31/08 também passaram batido: NT 2025.002 v1.51, NT 2026.006, IT 2026.001 e o PL 010f. - O que já funcionava: o detector automático pegou o drift do contrato de produção da Calculadora e abriu a issue #6 em 31/08/2026. Ela ficou 4 dias sem tratamento, o que é falha de resposta, não de detecção. -- Correção aplicada nesta data: quarto alvo no `drift-check.mjs`, apontado para o inventário de artefatos daquela página, com o inventário pinado em `vendor/cgibs-split-payment-artefatos.json` e 6 testes cobrindo o comportamento. Artefato novo publicado ali agora reprova o run. +- Correção aplicada nesta data: quarto alvo no `drift-check.mjs`, apontado para o inventário de artefatos daquela página, com o inventário pinado em `vendor/cgibs-split-payment-artefatos.json` e testes cobrindo o comportamento. Artefato novo publicado ali reprova o run. +- **Limite desse alvo, medido no mesmo dia**: `www.cgibs.gov.br` não aceita conexão do runner do GitHub Actions (connect timeout em 443, três tentativas, enquanto `consumo.tributos.gov.br` e `piloto-cbs.tributos.gov.br` respondem do mesmo runner). É bloqueio do host, não rede instável. O alvo então declara `severidadeIndisponivel: "ignore"`: no CI ele reporta e não reprova, e só compara de verdade quando roda de uma rede que alcança o host, como a máquina do mantenedor via `pnpm drift-check`. Drift ali continua reprovando quando alcançável. **Consequência prática: para esse contrato, a checagem semanal manual não é redundância, é a cobertura principal.** Se um dia o bloqueio cair, o CI passa a cobrir sozinho e essa nota sai. A lição vale além deste caso: **uma limitação declarada em comentário nunca tinha sido verificada**. Ela virou verdade porque foi escrita com confiança, e sobreviveu a várias revisões porque ninguém testa um comentário. Quando um artefato afirma que algo é impossível, o barato é gastar cinco minutos tentando fazer. diff --git a/scripts/drift-check.mjs b/scripts/drift-check.mjs index d12c729..d2297d8 100644 --- a/scripts/drift-check.mjs +++ b/scripts/drift-check.mjs @@ -53,6 +53,14 @@ export const TARGETS = [ vendored: "vendor/cgibs-split-payment-artefatos.json", live: "https://www.cgibs.gov.br/split-payment", severity: "fail", + // O www.cgibs.gov.br nao aceita conexao do runner do GitHub: connect + // timeout em 443, tres tentativas, enquanto consumo.tributos.gov.br e + // piloto-cbs.tributos.gov.br respondem do MESMO runner (run 33901733965). + // E bloqueio do host, nao rede instavel. Reprovar por isso deixaria o + // detector vermelho toda semana por uma causa que nao vamos consertar, e + // detector que grita lobo acaba silenciado. Drift ali continua reprovando; + // so a indisponibilidade e ignorada. + severidadeIndisponivel: "ignore", kind: "inventario-html", }, ]; @@ -130,6 +138,10 @@ export function diffSummary(vendored, live, limit = 12) { export function verdictFor(target, vendoredDoc, liveResult) { const base = { name: target.name, severity: target.severity, changed: [] }; + // Um alvo pode reprovar em drift e nao reprovar em indisponibilidade: sao + // sinais diferentes. "o contrato mudou" e achado; "nao consegui falar com o + // host" pode ser so a topologia de rede de quem esta rodando. + const seIndisponivel = target.severidadeIndisponivel ?? target.severity; if (vendoredDoc === null || vendoredDoc === undefined) { return { ...base, status: "setup-error", detail: "arquivo vendorado ausente ou ilegível" }; } @@ -137,7 +149,12 @@ export function verdictFor(target, vendoredDoc, liveResult) { const status = liveResult?.kind === "parse" ? "malformed" : "unreachable"; const tentativas = liveResult?.tentativas; const sufixo = tentativas && tentativas > 1 ? ` (apos ${tentativas} tentativas)` : ""; - return { ...base, status, detail: `${liveResult?.error ?? "sem resposta"}${sufixo}` }; + return { + ...base, + severity: seIndisponivel, + status, + detail: `${liveResult?.error ?? "sem resposta"}${sufixo}`, + }; } if (canonical(vendoredDoc) === canonical(liveResult.doc)) return { ...base, status: "match" }; return { ...base, status: "drift", changed: diffSummary(vendoredDoc, liveResult.doc) }; @@ -148,6 +165,11 @@ export function verdictFor(target, vendoredDoc, liveResult) { // reprova o run; nao decide se alguem fica sabendo (D-2, D-3). const DIVERGENTE = new Set(["drift", "unreachable", "malformed"]); +// "ignore" e severidade de terceira via: aparece no relatorio, nao reprova o +// run e nao abre issue. Existe para uma indisponibilidade de causa conhecida e +// externa, que nao e sinal nenhum sobre o contrato. +const IGNORADO = (v) => v.severity === "ignore"; + // setup-error e problema nosso e reprova em qualquer alvo. export function exitCodeFor(verdicts) { if (verdicts.some((v) => v.status === "setup-error")) return 2; @@ -161,7 +183,7 @@ export function exitCodeFor(verdicts) { // (vendor/MANIFEST.md), entao engolir a divergencia dele mata o valor dele. export function overallStatus(verdicts) { if (verdicts.some((v) => v.status === "setup-error")) return "setup-error"; - const divergentes = verdicts.filter((v) => DIVERGENTE.has(v.status)); + const divergentes = verdicts.filter((v) => DIVERGENTE.has(v.status) && !IGNORADO(v)); const reprovando = divergentes.filter((v) => v.severity === "fail"); if (reprovando.some((v) => v.status === "drift")) return "drift"; if (reprovando.some((v) => v.status === "malformed")) return "malformed"; @@ -176,7 +198,12 @@ export function report(verdicts, log = console.log) { } else if (v.status === "setup-error") { log(`SETUP-ERROR ${v.name}: ${v.detail}`); } else if (v.status === "unreachable" || v.status === "malformed") { - const aviso = v.severity === "fail" ? "" : " (alvo informativo, não reprova)"; + const aviso = + v.severity === "ignore" + ? " (indisponibilidade esperada neste ambiente, não reprova; drift ali continua reprovando)" + : v.severity === "fail" + ? "" + : " (alvo informativo, não reprova)"; const rotulo = v.status === "malformed" ? "MALFORMED " : "UNREACHABLE"; log(`${rotulo} ${v.name}: ${v.detail}${aviso}`); } else { diff --git a/scripts/drift-check.test.mjs b/scripts/drift-check.test.mjs index 2cca37a..ec4990c 100644 --- a/scripts/drift-check.test.mjs +++ b/scripts/drift-check.test.mjs @@ -344,3 +344,58 @@ describe("inventario de artefatos do CGIBS (quarto alvo)", () => { expect(pinado._meta.fonte).toBe(alvo.live); }); }); + +// Terceira via de severidade: o host do CGIBS nao aceita conexao do runner do +// GitHub, e reprovar por isso deixaria o detector vermelho toda semana por uma +// causa externa que nao vamos consertar. +describe("severidade de indisponibilidade separada da de drift", () => { + const CGIBS = { + name: "cgibs (teste)", + severity: "fail", + severidadeIndisponivel: "ignore", + kind: "inventario-html", + }; + const pinado = { artefatos: { "/upload/arquivos/202606/x.zip": true } }; + + it("indisponibilidade no alvo ignorado nao reprova o run", () => { + const v = verdictFor(CGIBS, pinado, { ok: false, error: "Connect Timeout Error", tentativas: 3 }); + expect(v.status).toBe("unreachable"); + expect(v.severity).toBe("ignore"); + expect(exitCodeFor([v])).toBe(0); + }); + + it("indisponibilidade ignorada tambem nao abre issue", () => { + const v = verdictFor(CGIBS, pinado, { ok: false, error: "Connect Timeout Error" }); + expect(overallStatus([v])).toBe("ok"); + }); + + it("mas drift no mesmo alvo continua reprovando", () => { + const vivo = { artefatos: { "/upload/arquivos/202608/novo.zip": true } }; + const v = verdictFor(CGIBS, pinado, { ok: true, doc: vivo }); + expect(v.status).toBe("drift"); + expect(v.severity).toBe("fail"); + expect(exitCodeFor([v])).not.toBe(0); + expect(overallStatus([v])).toBe("drift"); + }); + + it("o relatorio diz por que nao reprovou, em vez de sumir com o alvo", () => { + const linhas = []; + report([verdictFor(CGIBS, pinado, { ok: false, error: "Connect Timeout Error" })], (l) => linhas.push(l)); + const texto = linhas.join("\n"); + expect(texto).toContain("UNREACHABLE"); + expect(texto).toContain("drift ali continua reprovando"); + }); + + it("um alvo sem severidadeIndisponivel mantem o comportamento antigo", () => { + const PORTAL = { name: "portal (teste)", severity: "fail" }; + const v = verdictFor(PORTAL, { a: 1 }, { ok: false, error: "fetch failed" }); + expect(v.severity).toBe("fail"); + expect(exitCodeFor([v])).not.toBe(0); + }); + + it("o alvo real do CGIBS declara a severidade separada", () => { + const alvo = TARGETS.find((t) => t.kind === "inventario-html"); + expect(alvo.severity).toBe("fail"); + expect(alvo.severidadeIndisponivel).toBe("ignore"); + }); +});