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
5 changes: 5 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions docs/plans/PLANS_HUB.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
1 change: 1 addition & 0 deletions docs/plans/PLANS_TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
74 changes: 74 additions & 0 deletions docs/plans/PLAN_ADR_GOVERNANCE_ENFORCEMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Plan: ADR governance enforcement (deterministic test suite)

<!-- plans-hub-summary: Phase 1 anti-regression tests for ADR-0045 lifecycle, locale, immutability, and anti-deletion; Phase 2 prose/dangling refs. GitHub #1162. -->

**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)
149 changes: 149 additions & 0 deletions tests/adr_governance_support.py
Original file line number Diff line number Diff line change
@@ -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()))
Loading