Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion docs/watch-routine.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
33 changes: 30 additions & 3 deletions scripts/drift-check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
];
Expand Down Expand Up @@ -130,14 +138,23 @@ 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" };
}
if (!liveResult || !liveResult.ok) {
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) };
Expand All @@ -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;
Expand All @@ -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";
Expand All @@ -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 {
Expand Down
55 changes: 55 additions & 0 deletions scripts/drift-check.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
Loading