From 70aef1c61725fb6f4d9a08b3e82405d7ac278ee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?FabioLeit=C3=A3o?= Date: Sat, 4 Jul 2026 03:24:57 -0300 Subject: [PATCH 1/2] test(adr): Phase 1 ADR governance anti-regression suite (#1162) Add T1 lifecycle, T2 locale/structure, T5 rename-aware anti-deletion, and T6 genesis Date immutability tests with EOL-normalized parsers and operator override trailer. Wire pre-commit hook; add PLAN_ADR_GOVERNANCE_ENFORCEMENT and PLANS_TODO row. Bring ADR-0080 to ADR-0045 shape so corpus is GREEN. Closes #1162 --- .pre-commit-config.yaml | 5 + CHANGELOG.md | 2 + ...R-0080-local-validation-gate-inviolable.md | 80 +++++--- docs/adr/INVENTORY.txt | 6 +- docs/plans/PLANS_HUB.md | 1 + docs/plans/PLANS_TODO.md | 1 + docs/plans/PLAN_ADR_GOVERNANCE_ENFORCEMENT.md | 74 +++++++ tests/adr_governance_support.py | 149 ++++++++++++++ tests/fixtures/adr_genesis_date_lines.json | 81 ++++++++ tests/test_adr_governance_phase1.py | 189 ++++++++++++++++++ 10 files changed, 554 insertions(+), 34 deletions(-) create mode 100644 docs/plans/PLAN_ADR_GOVERNANCE_ENFORCEMENT.md create mode 100644 tests/adr_governance_support.py create mode 100644 tests/fixtures/adr_genesis_date_lines.json create mode 100644 tests/test_adr_governance_phase1.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 94ee24a7d..68836aa3f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -69,6 +69,11 @@ repos: entry: uv run pytest tests/test_no_legacy_cursorrules.py -q -W error language: system pass_filenames: false + - id: adr-governance-phase1 + name: ADR governance Phase 1 (ADR-0045 anti-regression) + entry: uv run pytest tests/test_adr_governance_phase1.py -q -W error + language: system + pass_filenames: false - id: workflow-security-lint name: zizmor workflow security lint (optional manual stage) entry: pwsh -NoProfile -File scripts/workflow-security-lint.ps1 -SkipIfMissing diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cca0dc39..9da91a696 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Human-readable summary of user-facing changes. **Detailed release notes:** [docs **Targeting (next dev line):** **`1.8.0-beta`** (#772) — after operator **release-ritual** for **1.7.4** (PR **#1024**). +- **ADR governance (Phase 1, #1162):** deterministic tests T1/T2/T5/T6 for ADR-0045 (`tests/test_adr_governance_phase1.py`); pre-commit hook `adr-governance-phase1`; plan [PLAN_ADR_GOVERNANCE_ENFORCEMENT.md](docs/plans/PLAN_ADR_GOVERNANCE_ENFORCEMENT.md). + --- ## 1.7.4 (2026-06-26) diff --git a/docs/adr/ADR-0080-local-validation-gate-inviolable.md b/docs/adr/ADR-0080-local-validation-gate-inviolable.md index c53e8f224..d93b6a4a8 100644 --- a/docs/adr/ADR-0080-local-validation-gate-inviolable.md +++ b/docs/adr/ADR-0080-local-validation-gate-inviolable.md @@ -1,51 +1,69 @@ -# ADR-0080 — O gate de validação LOCAL é INVIOLÁVEL: check-all completo (all-greens, zero regressões) antes de QUALQUER push ou PR +# ADR 0080 — Local validation gate is inviolable: full check-all (all-greens, zero regressions) before ANY push or PR -- **Status:** Accepted -- **Date:** 2026-07-03 -- **Deciders:** Fabio Tavares Leitão (operador / owner do repositório) -- **Gate-Change-Approved-By:** Fabio Tavares Leitão +- **Date (UTC):** 2026-07-03 +- **Authors:** Fabio Leitao +- **Deciders:** Fabio Leitao -## Contexto +## Status -O tier de validação **LOCAL** — `pre-commit` + os testes anti-regressão completos (`pytest`, ~1600) + **Bandit** + **Zizmor** + **Semgrep** — foi construído ao longo de meses **exatamente para NÃO depender do round-trip do GitHub** (esperar minutos por erro remoto e refazer). As regras `check-all-gate.mdc` e `never-weaken-security-gates.mdc` sempre mandaram rodar isso local antes de push/PR. +Proposed -Em 2026-07-03 constatou-se: (a) não havia `pre-push` hook forçando o gate; (b) o executor pushou rodando apenas testes *targeted* (`2 passed`), não o suite completo; (c) um modelo chegou a **sugerir confiar no CI remoto**. E a tentativa de "reforçar" o mandato **degenerou em proliferar regras `alwaysApply: true`** — queimando contexto, **o oposto do objetivo.** Este ADR corrige as duas pontas: **rigor absoluto** no gate **e** **economia de contexto** no enforcement. +### Status history -## Decisão (absoluta, não-negociável) +- 2026-07-03 — Born in violation. Created with Status: Accepted, breaching ADR-0045 §5 — the ADR that governs the ADR lifecycle itself: agents MUST NOT set Accepted on first commit; new ADRs are born Proposed until the operator ratifies. Every checkpoint of the review mesh failed: the Cursor executor committed it, the Claude Code auditor reviewed the prose but never checked the Status field, the claude.ai steering node validated progress, and all three celebrated the operator Ed25519 attestation on an ADR that was illegal at birth, merged via PR #1160 (commit e5722193) with no agent objecting at any stage. The operator caught what three AI auditors did not. PR #1160 is preserved as the scar. +- 2026-07-04 — Amended: corrected to Proposed; premature ratification metadata removed, lifecycle restored via PR #1161. +- 2026-07-04 — Amended: brought into full ADR-0045 conformance after two further violations surfaced, again missed by the review mesh and flagged by the operator: (a) the entire body had been authored in pt-BR, breaching §7 (ADRs are en_US technical documents) — the only pt-BR ADR of 78; (b) the H1 title used the ADR-0080 filename-hyphen form instead of the canonical ADR NNNN heading (§3). Body translated to en_US, title corrected, sections aligned to the §3 canonical set. +- 2026-07-04 — Root cause and accountability (recorded verbatim per operator directive, kept as scar, not a footnote): (1) These violations breached not only ADR-0045 (§5 — agents must not materialize an ADR as Accepted; §7 — ADRs are en_US) but also always-on governance loaded at every session cold-start and ignored: .cursor/rules/docs-locale-pt-br-contract.mdc (alwaysApply: true) and AGENTS.md (session-start contract; English-only prose uses en-US). (2) Despite the operator repeatedly and explicitly ordering it, no agent performed a complete reading of ADR-0045 — the ADR that governs ADRs — nor a single full sanity check of the ADR-0080 they were authoring; each surfaced one defect, declared it "the" error, and missed the rest. (3) With every rule, guardrail, guideline, skill, and ADR in place, the review mesh earned no trust: the operator personally identified every violation across multiple rounds. (4) The three reviewers ran in different environments, on different models, as different agents — the decorrelation meant to make their blind spots independent. It did not: all three showed the identical failure (plausibility over conformance; never reading the governing law in full). When the failure mode is architectural to LLMs, vendor/model decorrelation buys nothing — three "independent" auditors sharing one habit are one check repeated three times. +- 2026-07-04 — Additional violation logged: when the operator first flagged the Accepted-at-birth error, the corrective instinct of the review mesh was to DELETE the ADR (a straight revert — the original intent of PR #1161), itself a breach of ADR-0045 §Decision item 1 (ADRs are NEVER deleted; correction is by status transition / amendment only). The operator rejected delete-for-amend; #1160 is preserved as the scar and the record is corrected in place, not erased. -**Nenhum agente, modelo, ferramenta ou automação tem o direito de sequer SUSPEITAR que o `check-all` e todo o ritual de validações locais possa não rodar antes de QUALQUER push ou PR.** +## Context -1. O código **NUNCA** sai desta máquina com a menor chance de qualquer coisa exceto **ALL GREENS** e **NO REGRESSIONS**. -2. O ritual local completo (`./scripts/check-all.sh --enforced`) **DEVE passar 100% verde LOCAL, ANTES** de todo push e de toda abertura/atualização de PR. *(Commit local intermediário = pre-commit + checks leves; o full check-all é antes de PUBLICAR no GitHub.)* -3. O que o GitHub CI faz **DEPOIS** é **IRRELEVANTE** para esta regra. CI é *backstop*, **NUNCA substituto**. **Confiar no CI em vez do gate local é PROIBIDO.** -4. **Sugerir, adiar, reduzir ou agir como se** o gate pudesse ser pulado = **VIOLAÇÃO desta ADR.** +The **LOCAL** validation tier — `pre-commit` plus the full anti-regression test suite (`pytest`, ~1600) plus **Bandit**, **Zizmor**, and **Semgrep** — was built over months **specifically to avoid depending on the GitHub round-trip** (waiting minutes for a remote failure and redoing work). The rules `check-all-gate.mdc` and `never-weaken-security-gates.mdc` have always required running this locally before push/PR. -## Como é imposto — ENFORCEMENT (mecânico + econômico em contexto) +On 2026-07-03 it was found that: (a) there was no `pre-push` hook enforcing the gate; (b) the executor pushed after running only *targeted* tests (`2 passed`), not the full suite; (c) a model **suggested relying on remote CI**. And the attempt to "reinforce" the mandate **degenerated into proliferating `alwaysApply: true` rules** — burning context, **the opposite of the goal.** This ADR corrects both ends: **absolute rigor** on the gate **and** **enforcement economy** in context. -São **duas peças, e só duas**: +## Decision -1. **TRAVA mecânica:** o pre-push hook **VERSIONADO** (`.githooks/pre-push` + `core.hooksPath`) roda `check-all.sh --enforced`; falhou → **push abortado**. + teste **tamper-evident** no CI-*required* (removeu o hook → CI falha → não merge). *(issue #1151)* -2. **UMA regra que JÁ é always-on:** `check-all-gate.mdc` (two-tier: commit-local leve / push-PR full). **Nada além dela.** +**Absolute, non-negotiable:** No agent, model, tool, or automation may even **suspect** that `check-all` and the full local validation ritual may be skipped before **ANY** push or PR. -### Economia de enforcement (parte da decisão — igual peso ao rigor) +1. Code **NEVER** leaves this machine with anything other than **ALL GREENS** and **NO REGRESSIONS**. +2. The full local ritual (`./scripts/check-all.sh --enforced`) **MUST pass 100% green LOCALLY, BEFORE** every push and every PR open/update. *(Intermediate local commit = pre-commit plus light checks; full check-all is before PUBLISHING to GitHub.)* +3. What GitHub CI does **AFTER** is **IRRELEVANT** to this rule. CI is a *backstop*, **NEVER a substitute**. **Relying on CI instead of the local gate is FORBIDDEN.** +4. **Suggesting, deferring, reducing, or acting as if** the gate may be skipped = **VIOLATION of this ADR.** -**É PROIBIDO criar regras `alwaysApply: true` novas para reafirmar este mandato.** Repetir a regra em N always-on **queima contexto sem adicionar enforcement.** A trava é o **hook**; a regra always-on é **uma só** (`check-all-gate.mdc`). Documentação **contextual/situacional** (globs, `@rule`, session-token) é permitida e encorajada; **always-on nova, não.** Novas always-on ferem `docs/ops/CURSOR_RULES_PHASE2_SITUATIONALIZATION.md` (economia de contexto) e são **anti-padrão**. +### Enforcement (mechanical + context-economical) -## Registro ≠ Enforcement (não confundir — foi a raiz do incidente) +**Two pieces, and only two:** -- **Este ADR** = registro durável do **PORQUÊ** + governança. É **DOC** (`docs/adr/`), lido **on-demand** — **NÃO carrega em toda sessão, NÃO causa bloat.** -- **O enforcement** = **hook** (mecânico) + **1 regra** já-always-on. **O rigor vem do mecanismo, não de repetir a regra.** +1. **Mechanical lock:** the versioned pre-push hook (`.githooks/pre-push` + `core.hooksPath`) runs `check-all.sh --enforced`; on failure → **push aborted**. Plus a **tamper-evident** test in CI-*required* (hook removed → CI fails → no merge). *(issue #1151)* +2. **ONE already-always-on rule:** `check-all-gate.mdc` (two-tier: light commit-local / full push-PR). **Nothing beyond it.** -## Governança da mudança +**Enforcement economy (same weight as rigor):** **FORBIDDEN** to create new `alwaysApply: true` rules to restate this mandate. Repeating the rule in N always-on rules **burns context without adding enforcement.** The lock is the **hook**; the always-on rule is **one only** (`check-all-gate.mdc`). **Contextual/situational** documentation (globs, `@rule`, session-token) is permitted and encouraged; **new always-on, not.** New always-on rules violate `docs/ops/CURSOR_RULES_PHASE2_SITUATIONALIZATION.md` (context economy) and are **anti-pattern.** -Mudar/remover/bypassar o gate exige trailer **`Gate-Change-Approved-By: `**. `git push --no-verify` só com aprovação **explícita e logada**. Violação → **Safe-Hold** + report ao operador. +### Gate change governance -## Consequências +Changing, removing, or bypassing the gate requires trailer **`Gate-Change-Approved-By: `**. `git push --no-verify` only with **explicit, logged** operator approval. Violation → **Safe-Hold** + report to the operator. -- Pushes mais lentos localmente (1600 + scans). **ACEITÁVEL e INTENCIONAL** — custo de nunca entregar quebrado e nunca depender do round-trip remoto. -- Todo clone (main dev box — hoje a workstation Linux primária, pode voltar à primária Windows — + secondary) herda o gate no setup. -- Violação = falha de governança. +**Authority:** Owner = **operator (Fabio Leitao)**. Claude = read-only auditor. Cursor = executor. This ADR **supersedes any agent or model default.** -## Autoridade +## Rationale -Owner = **operador (Fabio Tavares Leitão)**. Claude = auditor read-only. Cursor = executor. Esta ADR **supera qualquer default de agente ou modelo.** +1. **Record ≠ Enforcement (do not conflate — root of the incident):** This ADR = durable record of **WHY** + governance. It is **DOC** (`docs/adr/`), read **on-demand** — **does NOT load every session, does NOT cause bloat.** Enforcement = **hook** (mechanical) + **1** already-always-on rule. **Rigor comes from the mechanism, not from repeating the rule.** +2. **CI backstop vs local gate:** Remote CI cannot replace the cost of a broken push round-trip; local all-green is the contract every contributor and agent must internalize before publish. +3. **Enforcement economy:** Proliferating always-on rules to "reinforce" discipline without mechanical locks only inflates agent context; the hook plus one rule is the minimum sufficient stack. + +## Consequences + +- **Positive:** Broken code does not leave the dev machine; agents cannot treat CI as a substitute for local validation. +- **Negative:** Slower local pushes (~1600 tests + scans). **ACCEPTABLE and INTENTIONAL** — cost of never shipping broken and never depending on the remote round-trip. +- Every clone (primary dev box — today the primary Linux workstation, may return to the primary Windows workstation — plus secondary) inherits the gate on setup. +- Violation = governance failure. + +## Related Decisions + +- [ADR 0045 — ADR metadata and format standardization](ADR-0045-adr-metadata-and-format-standardization.md) (§5 new ADR materialization → **Proposed**; §7 en_US) +- [ADR 0072 — Commit Gate vs Release Gate: distinct criteria](ADR-0072-commit-gate-vs-release-gate-distinct-criteria.md) (commit gate = `check-all`; distinct from release gate) +- GitHub [#1151](https://github.com/DataBoar/data-boar/issues/1151) — versioned pre-push hook (enforcement) +- GitHub [#1152](https://github.com/DataBoar/data-boar/issues/1152) — ADR materialization +- GitHub [#1160](https://github.com/DataBoar/data-boar/pull/1160) — scar (illegal Accepted birth) +- GitHub [#1161](https://github.com/DataBoar/data-boar/pull/1161) — lifecycle correction amend diff --git a/docs/adr/INVENTORY.txt b/docs/adr/INVENTORY.txt index 04f5f3b01..37df256d9 100644 --- a/docs/adr/INVENTORY.txt +++ b/docs/adr/INVENTORY.txt @@ -1,5 +1,5 @@ # ADR Inventory - generated by scripts/inv-adr.ps1 -# GeneratedUtc: 2026-07-03T23:48:05Z +# GeneratedUtc: 2026-07-04T06:20:48Z # Algorithm: SHA256 # Format: NUMBER | STATUS | FILE_HASH | FILENAME | TITLE | RATIFIED_BY # RATIFIED_BY: - = N/A; PENDING = ratification line with signature PENDING; @op = stamped @@ -81,6 +81,6 @@ 0077 | Accepted | F8A4C9D3B2BE2BEE3721C5AE09476F9CD066DB0B58E6B3174D4B783B123A1BAF | ADR-0077-filesystem-scan-no-client-gitignore-by-design.md | Filesystem scan does not honor client `.gitignore` (deliberate design) | - 0078 | Proposed | 5FEF0DCAC3DA292E33519F24FE6387584C04A850771958FF78B19D3EEB856990 | ADR-0078-multi-pattern-regex-benchmark-gate-regexset-before-vectorscan.md | Multi-pattern regex acceleration gated by benchmark (RegexSet before Vectorscan) | - 0079 | Proposed | 158C68D90B9C6934F0FAAC392E86C6C889FA8F5A6E9AD8197168205946435F9D | ADR-0079-ecosystem-engineering-rigor-canon.md | Ecosystem engineering rigor canon (UMADR satellites) | - -0080 | Accepted | B217F4C1FA5C96661EEF968C37500E20C09023C3D86186580496CEE7CB61E422 | ADR-0080-local-validation-gate-inviolable.md | local validation gate inviolable | - +0080 | Proposed | 5084990188A98B0454A3FA377C6180DBE5D9CC23B4F71398DBEDFDBB73D31045 | ADR-0080-local-validation-gate-inviolable.md | Local validation gate is inviolable: full check-all (all-greens, zero regressions) before ANY push or PR | - # -# InventoryHash: 87BD2025208408CE4DB3386EEC40EC0FC161078FBF38E3D7217BE236087A5F5B +# InventoryHash: 6E7B04DF01C09300B8C43A593786854D90BFA59EFEC6781C09A831BF0573E97A diff --git a/docs/plans/PLANS_HUB.md b/docs/plans/PLANS_HUB.md index b0336ea5c..04e34de1a 100644 --- a/docs/plans/PLANS_HUB.md +++ b/docs/plans/PLANS_HUB.md @@ -46,6 +46,7 @@ Do **not** edit the table manually; refresh with `python scripts/plans_hub_sync. | ------ | -------- | ----- | ------- | -------------- | | **Open** | [PLAN_ACTION_PLAN_GENERATOR_POST_SCAN.md](PLAN_ACTION_PLAN_GENERATOR_POST_SCAN.md) | Plan: Action Plan Generator (APG) — post-scan accountability and remediation hints | Post-scan Action Plan Generator: triage tiers, suggested mitigations, optional JSON/SQL snippets and evidence log—evidence for diligence, not legal conclusions. | [PLAN_SYNTHETIC_DATA_AND_CONFIDENCE_VALIDATION.md](PLAN_SYNTHETIC_DATA_AND_CONFIDENCE_VALIDATION.md) [PLAN_MATURITY_SELF_ASSESSMENT_GRC_QUESTIONNAIRE.md](completed/PLAN_MATURITY_SELF_ASSESSMENT_GRC_QUESTIONNAIRE.md) [REPORTS_AND_COMPLIANCE_OUTPUTS.md](../REPORTS_AND_COMPLIANCE_OUTPUTS.md) | | **Open** | [PLAN_ADDITIONAL_DETECTION_TECHNIQUES_AND_FN_REDUCTION.md](PLAN_ADDITIONAL_DETECTION_TECHNIQUES_AND_FN_REDUCTION.md) | Plan: Additional detection techniques and false-negative reduction | POC priorities 1-11 on main; v1.8.0 #1056 adds entity taxonomy, country checksum gates, span-alignment, plugin validators; priorities 5-7 NER/dictionaries still backlog. | [PLAN_EXTENDED_SENSITIVE_DISCOVERY_POSITIONING.md](PLAN_EXTENDED_SENSITIVE_DISCOVERY_POSITIONING.md) [PLAN_YAML_PLUGIN_SYSTEM.md](PLAN_YAML_PLUGIN_SYSTEM.md) [PLAN_SYNTHETIC_DATA_AND_CONFIDENCE_VALIDATION.md](PLAN_SYNTHETIC_DATA_AND_CONFIDENCE_VALIDATION.md) | +| **Open** | [PLAN_ADR_GOVERNANCE_ENFORCEMENT.md](PLAN_ADR_GOVERNANCE_ENFORCEMENT.md) | Plan: ADR governance enforcement (deterministic test suite) | Phase 1 anti-regression tests for ADR-0045 lifecycle, locale, immutability, and anti-deletion; Phase 2 prose/dangling refs. GitHub #1162. | — | | **Open** | [PLAN_ADR_GOVERNANCE_LIFECYCLE.md](PLAN_ADR_GOVERNANCE_LIFECYCLE.md) | Plan: ADR governance lifecycle (ADR 0045 amendment) | UMADR constitution — append-only Status history, Obsolete/Quarantined/Duplicate statuses, en_US ADRs; GitHub #803 | — | | **Open** | [PLAN_BUILD_IDENTITY_RELEASE_INTEGRITY.md](PLAN_BUILD_IDENTITY_RELEASE_INTEGRITY.md) | Plan: Build identity, runtime version display, and release integrity | **Status:** In progress — Phase E core landed (#856): SQLite integrity anchor (`core/integrity_anchor.py`), startup re-verify in any mode, TINTED/`-alpha` trust surfaces, `integrity_events`, open-mode worker clamp. Signe | — | | **Open** | [PLAN_CLAIMS_CONSISTENCY_AND_ANTI_OVERCLAIM.md](PLAN_CLAIMS_CONSISTENCY_AND_ANTI_OVERCLAIM.md) | PLAN: Claims consistency and anti-overclaim gate | gate determinístico offline anti-overclaim — invariante connector↔tier (build-time do #854) + manifesto docs/CLAIMS.yml com backed_by verificável; contraparte light do auditor on-demand claim-audit (lab-op) | [PLAN_CONNECTOR_TIER_GATING.md](PLAN_CONNECTOR_TIER_GATING.md) [PLAN_PRODUCT_TIERS_AND_OPEN_CORE.md](PLAN_PRODUCT_TIERS_AND_OPEN_CORE.md) | diff --git a/docs/plans/PLANS_TODO.md b/docs/plans/PLANS_TODO.md index aee6f8d4d..e15bfe556 100644 --- a/docs/plans/PLANS_TODO.md +++ b/docs/plans/PLANS_TODO.md @@ -89,6 +89,7 @@ When **partners** or **buyers** anchor on a vertical (MSP, insurance, RPO, real - **Sprint mirror:** [SPRINTS_AND_MILESTONES.md](SPRINTS_AND_MILESTONES.md) §3 + **M-RICH** when milestones change. - **Gemini / LLM doc triage (optional):** After a public-bundle review, use [PLAN_GEMINI_FEEDBACK_TRIAGE.md](PLAN_GEMINI_FEEDBACK_TRIAGE.md) for non-authoritative optional to-dos and promotion gates—does not override CI, pytest, or agreed sequencing until promoted here or in an issue. - **Taxonomy axes (G0-G3 + hub):** [PLAN_G_TIER.md](PLAN_G_TIER.md) ([pt-BR](PLAN_G_TIER.pt_BR.md)) formalizes **gravity** for findings; [PLAN_TAXONOMY_AXES.md](PLAN_TAXONOMY_AXES.md) maps orthogonal axes; [ADR-0055](../adr/ADR-0055-orthogonal-priority-axes-anti-collision-contract.md) records the anti-collision contract. **`scripts/inv-adr.ps1`** regenerates **`docs/adr/INVENTORY.txt`**. +- **ADR governance enforcement (#1162):** Phase 1 deterministic anti-regression tests (T1 lifecycle, T2 locale/structure, T5 anti-deletion rename-aware, T6 date immutability) — [PLAN_ADR_GOVERNANCE_ENFORCEMENT.md](PLAN_ADR_GOVERNANCE_ENFORCEMENT.md); pre-commit + CI. Phase 2 (T3 prose density, T4 anti-dangling refs, ADR-0074 smoke) ⬜ deferred. - **Hybrid taxonomy (#1071, Fideslang eval — closed 2026-06-30):** RO recommendation **B-interno + A-export-adapter** — three implementation slices (all **optional**, backward compatible): **(1)** [PLAN_NORM_TAG_HIERARCHY_AND_DATA_SUBJECT.md](PLAN_NORM_TAG_HIERARCHY_AND_DATA_SUBJECT.md) **[#1074](https://github.com/FabioLeitao/data-boar/issues/1074)** — hierarchical `norm_tag` + Data-Subject → closes **#1069** (supersedes [#1072](https://github.com/FabioLeitao/data-boar/issues/1072)); **(2)** [PLAN_DATA_USE_AXIS.md](PLAN_DATA_USE_AXIS.md) **[#1075](https://github.com/FabioLeitao/data-boar/issues/1075)**; **(3)** [PLAN_FIDESLANG_EXPORT_ADAPTER.md](PLAN_FIDESLANG_EXPORT_ADAPTER.md) **[#1076](https://github.com/FabioLeitao/data-boar/issues/1076)** → SWOT **W-novo-1** when customer-pull. Legacy Bearer-only plan: [PLAN_SUBJECT_CATEGORY_AXIS.md](PLAN_SUBJECT_CATEGORY_AXIS.md). - **Plan checkbox discipline:** When code ships for a named plan slice, update **`PLAN_*.md`** checkboxes in the **same PR** — see **`AGENTS.md`** (*Plan checkbox discipline*); **`plans-status-pl-sync.mdc`** stays situational (plan globs only). - **Licensing enforcement (open issues — PMO index):** Cluster **#704** [P0] (Maestro + JWT on four lab hosts) → **#719** [P1] **`[U1]`** (silent bypass via `DATA_BOAR_ENV=development` / `DEBUG=1` — CRITICAL log + Docker doc) → **#708–#722** (issuer, trial/grace/revocation ADRs). Table below under **`[H0][U1]` Licensing enforcement**; **A6** `license-smoke` should gain a **#719** regression when the fix lands. **After** Wave 1 **#656** U0/U1 slices and **#406** release gate — not a substitute for **#606** [P0] plugin hooks unless operator reprioritizes. diff --git a/docs/plans/PLAN_ADR_GOVERNANCE_ENFORCEMENT.md b/docs/plans/PLAN_ADR_GOVERNANCE_ENFORCEMENT.md new file mode 100644 index 000000000..e904dac09 --- /dev/null +++ b/docs/plans/PLAN_ADR_GOVERNANCE_ENFORCEMENT.md @@ -0,0 +1,74 @@ +# Plan: ADR governance enforcement (deterministic test suite) + + + +**Status:** Active +**Date:** 2026-07-04 +**Authors:** Fabio Leitao +**Priority:** P1 +**GitHub:** [#1162](https://github.com/DataBoar/data-boar/issues/1162) +**Related ADR:** [ADR 0045](../adr/ADR-0045-adr-metadata-and-format-standardization.md) · [ADR 0080](../adr/ADR-0080-local-validation-gate-inviolable.md) + +**Synced with:** [PLANS_TODO.md](PLANS_TODO.md) + +--- + +## Context + +The **ADR-0080 incident** showed that policy in `.cursor/rules/` and ADR-0045 did not prevent agents from materializing an illegal ADR (born `Accepted`, pt-BR body, wrong H1, delete attempt). Issue **#1162** adds **mechanical** anti-regression tests so governance holds regardless of which agent is at the keyboard. + +Golden rules (from #1162 — non-negotiable): + +1. **Normalize EOL** (`\r\n` → `\n`) before parsing — working tree may be CRLF. +2. **Prove GREEN** against the full ADR corpus before wiring a hard gate; tune tests, not legitimate ADRs. +3. **Never irrecoverable false-block** — smarter tests (rename-aware deletion) or operator override trailer (`ADR-Governance-Override-Approved-By:`). + +## Scope + +| Track | Deliverable | +| ----- | ----------- | +| Phase 1 | `tests/test_adr_governance_phase1.py` + `tests/adr_governance_support.py` — T1, T2, T5, T6 | +| Fixture | `tests/fixtures/adr_genesis_date_lines.json` — frozen genesis `Date (UTC)` lines (T6 corpus) | +| Hooks | `.pre-commit-config.yaml` — `adr-governance-phase1` pytest hook | +| Phase 2 | T3 prose locale density · T4 anti-dangling `ADR-NNNN` refs (CI) — **deferred** | +| ADR-0074 smoke | Materialize phantom ADR + planted violations — **deferred** (separate slice) | +| ADR-0045 amend | Codify “every mechanizable rule has a test” — **deferred** (in-place §8) | + +**Out of scope (existing coverage):** `test_adr_inventory_sync`, `test_adr_readme_index_sync`, cryptographic `inv-adr.ps1` inventory. + +## Phase 1 tests (deterministic) + +| ID | Rule | Mechanism | +| -- | ---- | ----------- | +| **T1** | New ADR `## Status` ∈ {`Proposed`, `Reserved`} | Staged `git diff --diff-filter=A` on `docs/adr/ADR-*.md` | +| **T2** | H1 `^# ADR \d{4} — `; exact metadata labels; no pt-BR `##`/`###` headings | Full corpus scan | +| **T5** | ADRs never deleted | Staged diff; `R*` rename allowed; bare `D` blocked | +| **T6** | `Date (UTC)` immutable | Frozen fixture vs working tree; staged diff must not mutate line | + +## Implementation checklist + +| Phase | Task | Status | +| ----- | ---- | ------ | +| 1a | `tests/adr_governance_support.py` + Phase 1 tests | ✅ | +| 1b | Genesis date fixture (78 ADRs) | ✅ | +| 1c | Pre-commit hook `adr-governance-phase1` | ✅ | +| 1d | ADR-0080 brought to ADR-0045 shape on branch (corpus GREEN for T2) | ✅ | +| 1e | `./scripts/check-all.sh --enforced` green | ✅ | +| 1f | PR `Closes #1162` — **operator merge only** | ⬜ | +| 2 | T3 + T4 + ADR-0074 smoke + ADR-0045 governance amend | ⬜ | + +## Operator override + +When a legitimate exception is required (rare), add to the commit message: + +```text +ADR-Governance-Override-Approved-By: Fabio Tavares Leitão +``` + +Incremental T1/T5/T6 staged checks skip when this trailer is present. Prefer fixing the test or using `git mv` for renames instead. + +## References + +- [ADR 0045](../adr/ADR-0045-adr-metadata-and-format-standardization.md) — UMADR constitution +- [PLAN_ADR_GOVERNANCE_LIFECYCLE.md](PLAN_ADR_GOVERNANCE_LIFECYCLE.md) — lifecycle manifesto +- GitHub [#1162](https://github.com/DataBoar/data-boar/issues/1162) — spec (Claude Code auditor) diff --git a/tests/adr_governance_support.py b/tests/adr_governance_support.py new file mode 100644 index 000000000..34bf05ae7 --- /dev/null +++ b/tests/adr_governance_support.py @@ -0,0 +1,149 @@ +"""Shared helpers for ADR governance anti-regression tests (issue #1162, ADR-0045).""" + +from __future__ import annotations + +import json +import re +import subprocess +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +ADR_DIR = REPO_ROOT / "docs" / "adr" +ADR_GLOB = "docs/adr/ADR-[0-9]*.md" +GENESIS_FIXTURE = ( + Path(__file__).resolve().parent / "fixtures" / "adr_genesis_date_lines.json" +) + +# H1 uses em dash (U+2014), not filename hyphen — ADR-0045 §3. +H1_RE = re.compile(r"^# ADR \d{4} — ") + +META_DATE_RE = re.compile(r"^- \*\*Date \(UTC\):\*\*", re.MULTILINE) +META_AUTHORS_RE = re.compile(r"^- \*\*Authors:\*\*", re.MULTILINE) +META_DECIDERS_RE = re.compile(r"^- \*\*Deciders:\*\*", re.MULTILINE) +DATE_LINE_RE = re.compile(r"^- \*\*Date \(UTC\):\*\*.*$", re.MULTILINE) +ISO_DATE_IN_DATE_LINE_RE = re.compile( + r"^- \*\*Date \(UTC\):\*\*.*?(\d{4}-\d{2}-\d{2})", re.MULTILINE +) + +STATUS_SECTION_RE = re.compile(r"(?ms)^## Status\s*\r?\n\s*([^\r\n#]+?)\s*(?:\r?\n|$)") + +PT_BR_HEADING_DENYLIST: tuple[tuple[str, re.Pattern[str]], ...] = ( + ("Contexto", re.compile(r"\bContexto\b")), + ("Decisão", re.compile(r"\bDecis[aã]o\b")), + ("Consequências", re.compile(r"\bConsequ[eê]ncias\b")), + ("Governança", re.compile(r"\bGovernan[cç]a\b")), + ("Autoridade", re.compile(r"\bAutoridade\b")), + ("Justificativa", re.compile(r"\bJustificativa\b")), + ( + "Alternativas Consideradas", + re.compile(r"\bAlternativas Consideradas\b"), + ), + ( + "Decisões Relacionadas", + re.compile(r"\bDecis[oõ]es Relacionadas\b"), + ), + ("Referências", re.compile(r"\bRefer[eê]ncias\b")), +) + +NEW_ADR_ALLOWED_STATUSES = frozenset({"Proposed", "Reserved"}) + +OVERRIDE_MARKER_RE = re.compile(r"(?im)^\s*ADR-Governance-Override-Approved-By:\s*\S+") + + +def normalize_eol(text: str) -> str: + return text.replace("\r\n", "\n").replace("\r", "\n") + + +def read_adr_text(path: Path) -> str: + return normalize_eol(path.read_text(encoding="utf-8")) + + +def iter_adr_files() -> list[Path]: + return sorted(ADR_DIR.glob("ADR-*.md")) + + +def parse_status(text: str) -> str | None: + normalized = normalize_eol(text) + match = STATUS_SECTION_RE.search(normalized) + if not match: + return None + return match.group(1).strip() + + +def extract_date_line(text: str) -> str | None: + match = DATE_LINE_RE.search(normalize_eol(text)) + return match.group(0).strip() if match else None + + +def load_genesis_fixture() -> dict[str, str | None]: + data = json.loads(GENESIS_FIXTURE.read_text(encoding="utf-8")) + return {str(k): (None if v is None else str(v)) for k, v in data.items()} + + +def _git(args: list[str]) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["git", "-C", str(REPO_ROOT), *args], + capture_output=True, + text=True, + check=False, + ) + + +def git_is_repo() -> bool: + return _git(["rev-parse", "--git-dir"]).returncode == 0 + + +def staged_adr_paths(*, diff_filter: str | None = None) -> list[str]: + args = ["diff", "--cached", "--name-only"] + if diff_filter: + args.insert(3, f"--diff-filter={diff_filter}") + args.append("--") + args.append(ADR_GLOB) + proc = _git(args) + if proc.returncode != 0: + return [] + return [line.strip() for line in proc.stdout.splitlines() if line.strip()] + + +def staged_name_status_with_renames() -> list[tuple[str, str, str | None]]: + """Return (status, old_path, new_path) from cached diff with rename detection.""" + proc = _git( + [ + "diff", + "--cached", + "-M", + "--find-renames", + "--name-status", + "--", + ADR_GLOB, + ] + ) + if proc.returncode != 0: + return [] + rows: list[tuple[str, str, str | None]] = [] + for line in proc.stdout.splitlines(): + if not line.strip(): + continue + parts = line.split("\t") + status = parts[0] + if status.startswith("R") and len(parts) >= 3: + rows.append((status, parts[1], parts[2])) + elif status == "D" and len(parts) >= 2: + rows.append((status, parts[1], None)) + elif status == "A" and len(parts) >= 2: + rows.append((status, parts[1], parts[1])) + elif len(parts) >= 2: + rows.append((status, parts[1], parts[1])) + return rows + + +def pending_commit_message() -> str: + for path in (REPO_ROOT / ".git" / "COMMIT_EDITMSG",): + if path.is_file(): + return path.read_text(encoding="utf-8", errors="replace") + proc = _git(["log", "-1", "--format=%B"]) + return proc.stdout if proc.returncode == 0 else "" + + +def governance_override_present() -> bool: + return bool(OVERRIDE_MARKER_RE.search(pending_commit_message())) diff --git a/tests/fixtures/adr_genesis_date_lines.json b/tests/fixtures/adr_genesis_date_lines.json new file mode 100644 index 000000000..fb14cb079 --- /dev/null +++ b/tests/fixtures/adr_genesis_date_lines.json @@ -0,0 +1,81 @@ +{ + "ADR-0000-project-origin-and-adr-baseline.md": "- **Date (UTC):** 2026-03-26", + "ADR-0001-markdown-fix-script-md029-and-semantic-step-lists.md": "- **Date (UTC):** 2026-03-25", + "ADR-0002-operator-facing-security-and-technical-docs.md": "- **Date (UTC):** 2026-03-25", + "ADR-0003-sbom-roadmap-cyclonedx-then-syft.md": "- **Date (UTC):** 2026-03-26", + "ADR-0004-external-docs-no-markdown-links-to-plans.md": "- **Date (UTC):** 2026-03-27", + "ADR-0005-ci-github-actions-supply-chain-pins.md": "- **Date (UTC):** 2026-03-27", + "ADR-0006-operator-today-mode-layout-and-published-sync.md": "- **Date (UTC):** 2026-03-30", + "ADR-0007-synthetic-data-corpus-before-real-data.md": "- **Date (UTC):** 2026-04-03", + "ADR-0008-docker-ce-swarm-over-docker-io-and-podman-only.md": "- **Date (UTC):** 2026-04-03", + "ADR-0009-ansible-idempotent-roles-as-single-automation-source.md": "- **Date (UTC):** 2026-04-03", + "ADR-0010-ip-declaration-prior-art-protection-at-employment.md": "- **Date (UTC):** 2026-03-27 (IP Declaration sent; git-commit: 2026-04-04)", + "ADR-0011-lab-op-observability-stack-layered.md": "- **Date (UTC):** 2026-04-03", + "ADR-0012-ocr-image-sensitive-data-detection.md": "- **Date (UTC):** 2026-04-03", + "ADR-0013-browser-artifact-sqlite-leveldb-scan-strategy.md": "- **Date (UTC):** 2026-04-03", + "ADR-0014-rename-repo-and-package-python3-lgpd-crawler-to-data-boar.md": "- **Date (UTC):** 2026-04-05", + "ADR-0015-poc-test-infrastructure-synthetic-corpus-and-api-testing.md": "- **Date (UTC):** 2026-04-05", + "ADR-0016-opentofu-corporate-iac-path-alongside-ansible.md": "- **Date (UTC):** 2026-04-05", + "ADR-0017-quasi-identification-risk-confidence-contract-and-lgpd-guardrails.md": "- **Date (UTC):** 2026-04-07", + "ADR-0018-pii-anti-recurrence-guardrails-for-tracked-files-and-branch-history.md": "- **Date (UTC):** 2026-04-07", + "ADR-0019-pii-verification-cadence-and-manual-review-gate.md": "- **Date (UTC):** 2026-04-07", + "ADR-0020-ci-full-git-history-pii-gate.md": "- **Date (UTC):** 2026-04-08", + "ADR-0021-public-web-presence-dns-alias-and-hosting.md": "- **Date (UTC):** 2026-04-08", + "ADR-0022-public-glossary-compliance-and-platform-terms.md": "- **Date (UTC):** 2026-04-08", + "ADR-0023-windows-primary-dev-filename-search-everything-es-first-with-fallback.md": "- **Date (UTC):** 2026-04-09", + "ADR-0024-enterprise-discovery-three-complementary-tracks.md": "- **Date (UTC):** 2026-04-15", + "ADR-0025-compliance-positioning-evidence-inventory-not-legal-conclusion-engine.md": "- **Date (UTC):** 2026-04-08", + "ADR-0026-optional-jurisdiction-hints-dpo-facing-heuristic-metadata-only.md": "- **Date (UTC):** 2026-04-08", + "ADR-0027-commercial-tier-boundaries-licensing-docs-and-future-jwt-claims.md": "- **Date (UTC):** 2026-04-17", + "ADR-0028-lab-external-connectivity-eval-playbook.md": "- **Date (UTC):** 2026-04-18", + "ADR-0029-cursor-markdown-preview-guardrail-and-lab-smoke-ansible-hook.md": "- **Date (UTC):** 2026-04-18", + "ADR-0030-python-dependency-update-closure-single-pass.md": "- **Date (UTC):** 2026-04-19", + "ADR-0031-pypi-packaging-hatchling-flat-layout.md": "- **Date (UTC):** 2026-04-19", + "ADR-0032-maturity-assessment-batch-history-sqlite.md": "- **Date (UTC):** 2026-04-20", + "ADR-0033-webauthn-open-relying-party-json-endpoints.md": "- **Date (UTC):** 2026-04-21", + "ADR-0034-outbound-http-user-agent-data-boar-prospector.md": "- **Date (UTC):** 2026-04-08", + "ADR-0035-readme-stakeholder-pitch-vs-deck-vocabulary.md": "- **Date (UTC):** 2026-04-22", + "ADR-0036-exception-and-log-pii-redaction-pipeline.md": "- **Date (UTC):** 2026-04-22", + "ADR-0037-data-boar-self-audit-log-governance.md": "- **Date (UTC):** 2026-04-22", + "ADR-0038-jurisdictional-ambiguity-alert-dont-decide.md": "- **Date (UTC):** 2026-04-08", + "ADR-0039-retention-evidence-posture-bonded-customs-adjacent-contexts.md": "- **Date (UTC):** 2026-04-08", + "ADR-0040-assistant-private-stack-evidence-mirrors-default.md": "- **Date (UTC):** 2026-04-22", + "ADR-0041-lab-completao-data-contract-preflight.md": "- **Date (UTC):** 2026-04-26", + "ADR-0042-lab-lessons-learned-archive-contract.md": "- **Date (UTC):** 2026-04-26", + "ADR-0043-sql-column-sampling-non-null-and-strategy-hook.md": "- **Date (UTC):** 2026-04-26", + "ADR-0044-dependabot-uv-ecosystem-for-pyproject-lock-closure.md": "- **Date (UTC):** 2026-05-09", + "ADR-0045-adr-metadata-and-format-standardization.md": "- **Date (UTC):** 2026-05-12", + "ADR-0046-operator-intent-and-blameless-collaboration.md": "- **Date (UTC):** 2026-05-12", + "ADR-0047-rca-first-defect-investigation-and-fix-discipline.md": "- **Date (UTC):** 2026-05-12", + "ADR-0048-operator-facing-taxonomy-and-naming-contract-preservation.md": "- **Date (UTC):** 2026-05-12", + "ADR-0049-no-brittle-mitigations-robust-input-handling.md": "- **Date (UTC):** 2026-05-12", + "ADR-0050-plan-document-metadata-standard.md": "- **Date (UTC):** 2026-05-12", + "ADR-0051-incremental-filesystem-scan-file-identity-fingerprint.md": "- **Date (UTC):** 2026-05-14", + "ADR-0052-yaml-plugin-system-centralized-schema.md": "- **Date (UTC):** 2026-05-14", + "ADR-0053-ebcdic-direct-upper-bound-and-dependabot-ignore.md": "- **Date (UTC):** 2026-05-15", + "ADR-0054-chardet-pinned-by-cyclonedx-bom.md": "- **Date (UTC):** 2026-05-15", + "ADR-0055-orthogonal-priority-axes-anti-collision-contract.md": "- **Date (UTC):** 2026-05-19", + "ADR-0056-cryptographic-adr-inventory-inv-adr-ssh-attestation.md": "- **Date (UTC):** 2026-05-20", + "ADR-0057-lightweight-hub-index-co-located-links.md": "- **Date (UTC):** 2026-05-20", + "ADR-0058-primer-hub-registration-ritual.md": "- **Date (UTC):** 2026-05-20", + "ADR-0060-db-lint-bandit-exclusion-risk-accepted.md": "- **Date (UTC):** 2026-06-21", + "ADR-0061-u-axis-issue-suborder-and-cross-milestone-gate.md": "- **Date (UTC):** 2026-05-22", + "ADR-0062-agent-containment-triple-audit-offband-pingpong.md": "- **Date (UTC):** 2026-05-22", + "ADR-0063-ed25519-license-jwt-signing.md": "- **Date (UTC):** 2026-06-21", + "ADR-0064-license-enforcement-additive-model.md": "- **Date (UTC):** 2026-06-21", + "ADR-0065-nist-sp800228a-api-security-reference.md": "- **Date (UTC):** 2026-06-11", + "ADR-0066-tampered-state-behavior.md": "- **Date (UTC):** 2026-05-25", + "ADR-0068-primary-linux-dev-workstation-temporary.md": "- **Date (UTC):** 2026-06-09", + "ADR-0069-cap-rpds-py-below-the-2026-calver-pivot.md": "- **Date (UTC):** 2026-06-17", + "ADR-0070-primer-taxonomy-and-home.md": "- **Date (UTC):** 2026-06-17", + "ADR-0071-self-protecting-pii-gate.md": "- **Date (UTC):** 2026-06-18", + "ADR-0072-commit-gate-vs-release-gate-distinct-criteria.md": "- **Date (UTC):** 2026-06-21", + "ADR-0073-version-scheme-octet-maturity-and-roadmap.md": "- **Date (UTC):** 2026-06-21", + "ADR-0075-plugin-auth-file-based-vs-bearer.md": "- **Date (UTC):** 2026-06-21", + "ADR-0076-opa-rego-ci-tier-drift-linter-not-runtime.md": "- **Date (UTC):** 2026-06-30", + "ADR-0077-filesystem-scan-no-client-gitignore-by-design.md": "- **Date (UTC):** 2026-06-30", + "ADR-0078-multi-pattern-regex-benchmark-gate-regexset-before-vectorscan.md": "- **Date (UTC):** 2026-06-30", + "ADR-0079-ecosystem-engineering-rigor-canon.md": "- **Date (UTC):** 2026-07-01", + "ADR-0080-local-validation-gate-inviolable.md": "- **Date (UTC):** 2026-07-03" +} + diff --git a/tests/test_adr_governance_phase1.py b/tests/test_adr_governance_phase1.py new file mode 100644 index 000000000..3de5463d9 --- /dev/null +++ b/tests/test_adr_governance_phase1.py @@ -0,0 +1,189 @@ +"""ADR governance Phase 1 anti-regression tests (issue #1162, ADR-0045). + +T1 lifecycle · T2 locale/structure · T5 anti-deletion (rename-aware) · T6 date immutability. + +Does not duplicate test_adr_inventory_sync or test_adr_readme_index_sync. +""" + +from __future__ import annotations + +import subprocess + +import pytest + +from tests.adr_governance_support import ( + ADR_DIR, + GENESIS_FIXTURE, + H1_RE, + ISO_DATE_IN_DATE_LINE_RE, + META_AUTHORS_RE, + META_DATE_RE, + META_DECIDERS_RE, + NEW_ADR_ALLOWED_STATUSES, + PT_BR_HEADING_DENYLIST, + REPO_ROOT, + extract_date_line, + git_is_repo, + governance_override_present, + iter_adr_files, + load_genesis_fixture, + parse_status, + read_adr_text, + staged_adr_paths, + staged_name_status_with_renames, +) + + +def test_t2_h1_uses_em_dash_title_form() -> None: + """T2: H1 must be '# ADR NNNN — Title' (space + em dash), not filename hyphen.""" + violations: list[str] = [] + for path in iter_adr_files(): + lines = read_adr_text(path).split("\n") + first = lines[0] if lines else "" + if not H1_RE.match(first): + violations.append(f"{path.name}: {first!r}") + assert not violations, "ADR H1 violations:\n" + "\n".join(violations) + + +def test_t2_metadata_labels_exact_anchored() -> None: + """T2: metadata labels are exact anchored tokens (not a loose 'Data*' prefix).""" + violations: list[str] = [] + for path in iter_adr_files(): + text = read_adr_text(path) + if not META_DATE_RE.search(text): + violations.append(f"{path.name}: missing '- **Date (UTC):**'") + if not META_AUTHORS_RE.search(text): + violations.append(f"{path.name}: missing '- **Authors:**'") + if not META_DECIDERS_RE.search(text): + violations.append(f"{path.name}: missing '- **Deciders:**'") + assert not violations, "ADR metadata label violations:\n" + "\n".join(violations) + + +def test_t2_headings_free_of_pt_br_denylist() -> None: + """T2: ## / ### headings must not use pt-BR section titles (ADR-0045 §7).""" + violations: list[str] = [] + for path in iter_adr_files(): + for line in read_adr_text(path).split("\n"): + if not (line.startswith("##") or line.startswith("###")): + continue + for label, pattern in PT_BR_HEADING_DENYLIST: + if pattern.search(line): + violations.append(f"{path.name}: {label} in {line!r}") + assert not violations, "pt-BR heading violations:\n" + "\n".join(violations) + + +def test_t6_genesis_date_lines_immutable_against_fixture() -> None: + """T6: '- **Date (UTC):**' line must not change on later edits (frozen corpus).""" + assert GENESIS_FIXTURE.is_file(), f"missing fixture: {GENESIS_FIXTURE}" + baseline = load_genesis_fixture() + adrs = {p.name for p in iter_adr_files()} + violations: list[str] = [] + for name in sorted(adrs): + path = ADR_DIR / name + current = extract_date_line(read_adr_text(path)) + expected = baseline.get(name) + if expected is None: + violations.append( + f"{name}: not in genesis fixture (add row when materializing)" + ) + continue + if current != expected: + violations.append(f"{name}: expected {expected!r}, got {current!r}") + extra = sorted(set(baseline) - adrs) + if extra: + violations.append(f"fixture lists removed ADRs: {extra}") + assert not violations, "genesis Date (UTC) immutability violations:\n" + "\n".join( + violations + ) + + +@pytest.mark.skipif(not git_is_repo(), reason="git required for incremental ADR gates") +def test_t1_new_staged_adrs_born_proposed_or_reserved() -> None: + """T1: newly added ADRs in the index must have Status Proposed or Reserved.""" + if governance_override_present(): + pytest.skip("operator override marker present") + violations: list[str] = [] + for rel in staged_adr_paths(diff_filter="A"): + path = ADR_DIR / rel.split("/")[-1] + if not path.is_file(): + path = REPO_ROOT / rel + status = parse_status(read_adr_text(path)) + if status not in NEW_ADR_ALLOWED_STATUSES: + violations.append(f"{rel}: Status={status!r} (want Proposed or Reserved)") + assert not violations, "new ADR lifecycle violations:\n" + "\n".join(violations) + + +@pytest.mark.skipif(not git_is_repo(), reason="git required for incremental ADR gates") +def test_t5_staged_adr_deletions_blocked_rename_aware() -> None: + """T5: true ADR erasure in the index is forbidden; renames/reformats are allowed.""" + if governance_override_present(): + pytest.skip("operator override marker present") + renamed_sources = { + old + for status, old, new in staged_name_status_with_renames() + if status.startswith("R") and new + } + violations: list[str] = [] + for status, old, _new in staged_name_status_with_renames(): + if status != "D": + continue + if old in renamed_sources: + continue + violations.append(old) + assert not violations, ( + "ADR deletion blocked (ADR-0045 §1). True erasures in index:\n" + + "\n".join(violations) + + "\nUse status transition or rename (git mv); override only with " + "ADR-Governance-Override-Approved-By: trailer." + ) + + +@pytest.mark.skipif(not git_is_repo(), reason="git required for incremental ADR gates") +def test_t6_staged_date_line_unchanged_for_existing_adrs() -> None: + """T6: staged edits must not mutate '- **Date (UTC):**' on existing ADRs.""" + if governance_override_present(): + pytest.skip("operator override marker present") + violations: list[str] = [] + for rel in staged_adr_paths(): + if rel in staged_adr_paths(diff_filter="A"): + continue + path = REPO_ROOT / rel + if not path.is_file(): + continue + proc_old = subprocess.run( + ["git", "-C", str(REPO_ROOT), "show", f"HEAD:{rel}"], + capture_output=True, + text=True, + check=False, + ) + if proc_old.returncode != 0: + continue + old_line = extract_date_line(proc_old.stdout) + new_line = extract_date_line(read_adr_text(path)) + if old_line and new_line and old_line != new_line: + violations.append(f"{rel}: {old_line!r} -> {new_line!r}") + assert not violations, "Date (UTC) immutability violations in index:\n" + "\n".join( + violations + ) + + +@pytest.mark.skipif(not git_is_repo(), reason="git required for incremental ADR gates") +def test_t6_new_staged_adrs_have_parseable_iso_date() -> None: + """T6: new ADRs must declare a parseable YYYY-MM-DD in the Date (UTC) line.""" + violations: list[str] = [] + for rel in staged_adr_paths(diff_filter="A"): + path = REPO_ROOT / rel + text = read_adr_text(path) + if not ISO_DATE_IN_DATE_LINE_RE.search(text): + violations.append(f"{rel}: no YYYY-MM-DD in '- **Date (UTC):**' line") + assert not violations, "new ADR date violations:\n" + "\n".join(violations) + + +def test_phase1_corpus_file_count_matches_fixture() -> None: + """Sanity: genesis fixture stays aligned with docs/adr/ADR-*.md count.""" + baseline = load_genesis_fixture() + adrs = [p.name for p in iter_adr_files()] + assert len(baseline) == len(adrs), ( + f"fixture rows={len(baseline)} adr files={len(adrs)}; " + "regenerate tests/fixtures/adr_genesis_date_lines.json" + ) From 40d8462e61cb4a92d4972e1545595f21b1c6e1f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?FabioLeit=C3=A3o?= Date: Sat, 4 Jul 2026 03:48:55 -0300 Subject: [PATCH 2/2] =?UTF-8?q?chore(adr):=20segregate=20#1163=20scope=20?= =?UTF-8?q?=E2=80=94=20INVENTORY=20from=20main=20(#1161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop operator-gated INVENTORY drift from governance-tests PR; ADR-0080 silver remains on main via #1161. --- docs/adr/INVENTORY.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/INVENTORY.txt b/docs/adr/INVENTORY.txt index 37df256d9..9d1f87749 100644 --- a/docs/adr/INVENTORY.txt +++ b/docs/adr/INVENTORY.txt @@ -1,5 +1,5 @@ # ADR Inventory - generated by scripts/inv-adr.ps1 -# GeneratedUtc: 2026-07-04T06:20:48Z +# GeneratedUtc: 2026-07-04T05:17:52Z # Algorithm: SHA256 # Format: NUMBER | STATUS | FILE_HASH | FILENAME | TITLE | RATIFIED_BY # RATIFIED_BY: - = N/A; PENDING = ratification line with signature PENDING; @op = stamped