Skip to content
Merged
1 change: 1 addition & 0 deletions .cursor/rules/adr-trigger.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,6 @@ A decision was **explicitly deferred** and should be documented so future sessio
At the end of any session where a trigger fires:
1. List candidate decisions in chat (one line each)
2. For each: `new-adr.ps1 -Title ... -Summary ...` → fill Context/Decision/Consequences
- **Never** paste raw spike/benchmark tables, test-runner logs, or `Operator decision (YYYY-MM-DD)` lines into the ADR — link to `tests/benchmarks/`, issue comments, or spike docs ([ADR 0045](docs/adr/ADR-0045-adr-metadata-and-format-standardization.md) item 9, **#1925** / T7).
3. Update `docs/adr/README.md` index
4. Commit as `docs(adr): add ADR NNNN — short title`
2 changes: 1 addition & 1 deletion docs/adr/ADR-0000-project-origin-and-adr-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ This repository is the **canonical UMADR regency** for the ecosystem. Satellite
| **Numbering** | Four-digit zero-padded IDs: `ADR-0001`, not `ADR-001`. Renumber only when normalizing legacy three-digit filenames; do **not** force-renumber an established corpus except for format compliance. |
| **Filename** | `ADR-NNNN-kebab-synopsis.md` — synopsis in the slug; stable after merge. |
| **Locale** | ADR body text is **English-only** (`en_US`). Paired pt-BR mirrors apply to operator README/index prose, not to numbered ADR files (see [ADR 0045](ADR-0045-adr-metadata-and-format-standardization.md)). |
| **Sections** | Prefer **Context**, **Decision**, **Consequences**, **References**; use `## Status history` for lifecycle (UMADR). |
| **Sections** | Prefer **Context**, **Decision**, **Consequences**, **References**; use `## Status history` for lifecycle (UMADR). Do **not** embed raw benchmark/test tables or runner output in the body — **reference** pinned artifacts instead ([ADR 0045](ADR-0045-adr-metadata-and-format-standardization.md) §Decision item 9, **#1925**). |
| **Status taxonomy** | **Proposed**, **Accepted**, **Duplicate of ADR-NNNN**, **Obsolete**, **Quarantined**, plus compound forms (**Superseded by ADR-NNNN**, **Deprecated**, **Reserved**) as defined in [ADR 0045](ADR-0045-adr-metadata-and-format-standardization.md). **U = Human-in-the-Loop:** agents set **Proposed** on materialization; **Accepted** requires explicit operator ratification per [ADR 0056](ADR-0056-cryptographic-adr-inventory-inv-adr-ssh-attestation.md). |
| **Rebrand** | Each repository rename/rebrand is recorded in **its own ADR** (pattern: [ADR 0014](ADR-0014-rename-repo-and-package-python3-lgpd-crawler-to-data-boar.md)). Use a **low number** (0001–0005) when the corpus is small; otherwise the **next free** number — without renumbering unrelated ADRs. |
| **Canonical + reference-stub** | **data-boar** holds the full house rule (this ADR + [ADR 0045](ADR-0045-adr-metadata-and-format-standardization.md)). Satellites add a **short reference-stub ADR** that links here — no duplicate constitution text. |
Expand Down
15 changes: 15 additions & 0 deletions docs/adr/ADR-0045-adr-metadata-and-format-standardization.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Accepted
- 2026-06-09 — Amended: append-only Status history, immutable Date (UTC), extended status enum (Duplicate of ADR-NNNN), locale en_US, immutability clause — [GitHub #803](https://github.com/FabioLeitao/data-boar/issues/803)
- 2026-06-09 — Amended: UMADR declaration + Obsolete/Quarantined statuses — [GitHub #803](https://github.com/FabioLeitao/data-boar/issues/803)
- 2026-06-21 — Amended: `Reserved` status + new ADR materialization defaults to **Proposed** — [GitHub #993](https://github.com/FabioLeitao/data-boar/issues/993)
- 2026-09-28 — Amended: no embedded raw test/benchmark/spike data in ADR body — [GitHub #1925](https://github.com/DataBoar/data-boar/issues/1925)

## Context

Expand Down Expand Up @@ -151,6 +152,20 @@ locale rule in the ADR constitution itself.
proportionate edits; solo + human-in-the-loop governance may amend in-place when the
amendment is recorded in Status history (dogfood on this ADR).

9. **No embedded experimental raw data:** An ADR records the **decision and rationale** —
never raw test/benchmark/spike evidence pasted into the body. **Forbidden shapes**
(mechanically enforced — `tests/test_adr_governance_phase1.py` **T7**, issue **#1925**):
- A line `Operator decision (YYYY-MM-DD)` in the body (fabricated ratification pattern).
- A Markdown table with **three or more** data rows whose cells look like **measurement
datasets** (timings, `×` slowdown factors, `%` wall-time splits) — not governance
matrices (rules, statuses, taxonomies).
- A fenced code block containing test-runner output (`PASSED` / `FAILED` / `passed in N`).
**Allowed:** prose summary with a **link or pinned path** to versioned evidence (for example
`tests/benchmarks/*.json`, issue comment, spike doc) without reproducing the raw table or
log. The **measurement-table** heuristic runs on **staged `+` lines** (create or amend);
full-corpus CI scan uses the operator-decision and test-runner shapes only (non-retroactive
posture for legacy Accepted ADRs — same as item 5).

## Rationale

1. **Accountability:** Date, Authors, and Deciders establish decision ownership; immutable
Expand Down
8 changes: 4 additions & 4 deletions docs/adr/INVENTORY.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# ADR Inventory - generated by scripts/inv-adr.ps1
# GeneratedUtc: 2026-09-22T14:09:16Z
# GeneratedUtc: 2026-09-29T00:58:17Z
# Algorithm: SHA256
# Format: NUMBER | STATUS | FILE_HASH | FILENAME | TITLE | RATIFIED_BY
# RATIFIED_BY: - = N/A; PENDING = ratification line with signature PENDING; @op = stamped
#
0000 | Accepted | 426CC5E6A9592A0DBFBDFD6861807B1175521FC22FB8F1D81FE7B3510F5C0262 | ADR-0000-project-origin-and-adr-baseline.md | Project origin and ADR baseline | -
0000 | Accepted | 6B95D70BAA51B782D36800F4FECFD2A228BA571B3DC0C5CF8A8F1F1A886CEA73 | ADR-0000-project-origin-and-adr-baseline.md | Project origin and ADR baseline | -
0001 | Accepted | 9AB8B17D2486FA8CCBAA2F9A6D1586DEF5A3FA1C25DF7477FC18E10523E81A2A | ADR-0001-markdown-fix-script-md029-and-semantic-step-lists.md | Markdown fix script, MD029, and semantic step lists | -
0002 | Accepted | 044BB37A80808F78699470D0BB7B59D67697690F3D3EE7485297B85C8C338D9F | ADR-0002-operator-facing-security-and-technical-docs.md | Operator-facing security and technical documentation | -
0003 | Accepted | 9C3B0B0E3B93A13487260886B5EE25AD8981DE152D20188FB231F92E10C09B19 | ADR-0003-sbom-roadmap-cyclonedx-then-syft.md | SBOM roadmap — CycloneDX (JSON) first, then Syft on Docker images | -
Expand Down Expand Up @@ -49,7 +49,7 @@
0042 | Accepted | A23FB9FFAF01E4017713AFE4BF16B30AB651A8D0BA157E0C474F48DC7B6F8FEC | ADR-0042-lab-lessons-learned-archive-contract.md | Public LAB lessons archive + hub (dated snapshots) | -
0043 | Accepted | 695CDB10C5F9393E0997CDF3E5C6CD81E4E8303C130877F23FA89AC212D0F25D | ADR-0043-sql-column-sampling-non-null-and-strategy-hook.md | SQL column sampling — non-null filter and strategy hook | -
0044 | Accepted | 30A9E955AB1E49F6DF413BABC478440C970F22A6789DBC7D6176B4DDB5507376 | ADR-0044-dependabot-uv-ecosystem-for-pyproject-lock-closure.md | Dependabot uses the `uv` ecosystem for Python deps (pyproject + lock closure) | -
0045 | Accepted | 6226BB8F1184C1D5A1D3BF371C25C23ACAD78CCA4AABD19327012E282622F8C2 | ADR-0045-adr-metadata-and-format-standardization.md | ADR metadata and format standardization | -
0045 | Accepted | 605AB0262424C2AC1AB659C051A75C58981638D10D1A7B428476F3DED0A9B6C0 | ADR-0045-adr-metadata-and-format-standardization.md | ADR metadata and format standardization | -
0046 | Accepted | 07CA56EB399E5F06E6A2BB19C3940FB84A3F8B425036A6CE278A35E77564FFCC | ADR-0046-operator-intent-and-blameless-collaboration.md | Operator intent and blameless collaboration posture | -
0047 | Accepted | E4BF0EAE26EA9C357D6BB1AD79ADEC52B66011C509D620A7263D7FF2A8CB9037 | ADR-0047-rca-first-defect-investigation-and-fix-discipline.md | RCA-first defect investigation and fix discipline | -
0048 | Accepted | 018AC2337ECD324FDDA4C12593025D3F05FD04F88B1CB3486B40E2B142A2B98D | ADR-0048-operator-facing-taxonomy-and-naming-contract-preservation.md | Operator-facing taxonomy and naming contract preservation | -
Expand Down Expand Up @@ -97,4 +97,4 @@
0091 | Proposed | 22DFED4F0EC4D523F149D44F5DA866C4AFDAE0730324429A31F5825C02158519 | ADR-0091-container-cp314t-license-gil-gate.md | One published container image (cp314t); GIL restored by license at ENTRYPOINT | -
0092 | Accepted | 6D3DFD0071E7382810DC99CE86E639C7A4A49A26540EFD1A56F25EC354E292EE | ADR-0092-build-provenance-scorecard-and-github-release-attestations.md | Build provenance: OpenSSF Scorecard + GitHub Release attestations (not PyPI badges) | -
#
# InventoryHash: 3C00B0C2A4B8B56EA89E6793481CE1BC4FBCB1076051049EBA41F75CCA676CC7
# InventoryHash: DC8369015B05EF0D8993C7CCB3743B235DA907BB17054146BAE01DCBC2DF50BA
8 changes: 5 additions & 3 deletions docs/plans/PLAN_ADR_GOVERNANCE_ENFORCEMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ Golden rules (from #1162 — non-negotiable):

| Track | Deliverable |
| ----- | ----------- |
| Phase 1 | `tests/test_adr_governance_phase1.py` + `tests/adr_governance_support.py` — T1, T2, T5, T6 |
| Phase 1 | `tests/test_adr_governance_phase1.py` + `tests/adr_governance_support.py` — T1, T2, T5, T6, **T7** |
| 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) |
| ADR-0045 amend | Item 9 — no embedded raw benchmark/test data (**#1925**) — **in PR with T7** |

**Out of scope (existing coverage):** `test_adr_inventory_sync`, `test_adr_readme_index_sync`, cryptographic `inv-adr.ps1` inventory.

Expand All @@ -44,6 +44,7 @@ Golden rules (from #1162 — non-negotiable):
| **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 |
| **T7** | No embedded raw test/benchmark/spike data in ADR body | Incident-shaped detector on **staged `+` lines** (A/M); full-corpus scan must stay empty; synthetic good/bad fixtures in `tests/fixtures/adr_t7_*` (**#1925**) |

## Implementation checklist

Expand All @@ -55,7 +56,8 @@ Golden rules (from #1162 — non-negotiable):
| 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 | ⬜ |
| 2 | T3 + T4 + ADR-0074 smoke | ⬜ |
| 2b | T7 + ADR-0045 item 9 + ADR-0000 pointer (**#1925**) | ⬜ |

## Operator override

Expand Down
138 changes: 138 additions & 0 deletions tests/adr_governance_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,39 @@

OVERRIDE_MARKER_RE = re.compile(r"(?im)^\s*ADR-Governance-Override-Approved-By:\s*\S+")

# T7 (#1925): incident-shaped embedded experimental data (keen-platypus class), not loose numbers.
OPERATOR_DECISION_LINE_RE = re.compile(
r"^[\s]*(?:>[\s]*)?(?:[-*+][\s]+)?(?:\*\*)?"
r"Operator decision\s*\(\d{4}-\d{2}-\d{2}\)",
re.MULTILINE | re.IGNORECASE,
)
BENCHMARK_TABLE_CELL_RE = re.compile(
r"\d[\d.,]*\s*(?:(?:ms|µs|us|sec)(?![A-Za-z])|×|x\s*slower|req/s|MB/s)"
r"|(?:\d+\.\d+|\d+)\s*×",
re.IGNORECASE,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unit regex matches English prefixes

Low Severity

BENCHMARK_TABLE_CELL_RE matches sec, ms, and us without a trailing boundary, so ordinary words such as seconds, messages, or users after a number count as measurement cells. A timeout or options table written in English can then fail the T7 gate.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 07e020b. Configure here.

# Case-sensitive PASSED/FAILED (pytest); avoid matching `status: failed` in YAML samples.
TEST_RUNNER_IN_FENCE_RE = re.compile(
r"\bPASSED\b|\bFAILED\b|\d+\s+passed\b|passed in \d+(?:\.\d+)?s",
)
FENCE_RE = re.compile(r"```[^\n]*\n(.*?)```", re.DOTALL)

T7_BAD_FIXTURE = (
Path(__file__).resolve().parent
/ "fixtures"
/ "adr_t7_synthetic_bad_embedded_spike.md"
)
T7_GOOD_FIXTURE = (
Path(__file__).resolve().parent
/ "fixtures"
/ "adr_t7_synthetic_good_benchmark_reference.md"
)
GRANDFATHER_FIXTURE = (
Path(__file__).resolve().parent
/ "fixtures"
/ "adr_embedded_benchmark_grandfather.json"
)


def normalize_eol(text: str) -> str:
return text.replace("\r\n", "\n").replace("\r", "\n")
Expand Down Expand Up @@ -147,3 +180,108 @@ def pending_commit_message() -> str:

def governance_override_present() -> bool:
return bool(OVERRIDE_MARKER_RE.search(pending_commit_message()))


def _markdown_table_blocks(text: str) -> list[list[str]]:
blocks: list[list[str]] = []
lines = normalize_eol(text).split("\n")
index = 0
while index < len(lines):
if not lines[index].strip().startswith("|"):
index += 1
continue
block: list[str] = []
while index < len(lines) and lines[index].strip().startswith("|"):
block.append(lines[index])
index += 1
if len(block) >= 3:
blocks.append(block)
return blocks


def _is_table_separator_row(line: str) -> bool:
return bool(re.match(r"^\|\s*[-: ]+\|", line.replace(" ", "")))


def _table_data_rows(block: list[str]) -> list[str]:
if not block:
return []
# Full table: header + separator + data. Staged + chunks may be data rows only.
start = 2 if len(block) >= 2 and _is_table_separator_row(block[1]) else 0
rows: list[str] = []
for line in block[start:]:
if _is_table_separator_row(line):
continue
rows.append(line)
return rows


def _benchmark_dataset_table_violation(block: list[str]) -> bool:
data_rows = _table_data_rows(block)
if len(data_rows) < 3:
return False
measurement_rows = sum(
1 for row in data_rows if BENCHMARK_TABLE_CELL_RE.search(row)
)
return measurement_rows >= 3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Staged table scan drops data rows

Medium Severity

_table_data_rows always discards the first two pipe-lines as header and separator. The staged T7 gate then runs that parser on concatenated + lines only, so adding or replacing three measurement rows on an existing table never meets the 3-row threshold and is not flagged.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 07e020b. Configure here.



def _test_runner_codeblock_violation(text: str) -> bool:
for match in FENCE_RE.finditer(normalize_eol(text)):
body = match.group(1)
if TEST_RUNNER_IN_FENCE_RE.search(body):
return True
return False


def embedded_experimental_data_violations(
text: str, *, check_tables: bool = True
) -> list[str]:
"""Return stable violation codes for incident-shaped embedded benchmark/spike data.

``check_tables=False`` skips the 3+ row measurement-table heuristic (legacy Accepted
ADRs may cite µs/call tables; non-retroactive posture — table rule targets staged
additions and fixtures).
"""
normalized = normalize_eol(text)
hits: list[str] = []
if OPERATOR_DECISION_LINE_RE.search(normalized):
hits.append("operator_decision_line")
if check_tables:
for block in _markdown_table_blocks(normalized):
if _benchmark_dataset_table_violation(block):
hits.append("benchmark_dataset_table_3plus_rows")
break
if _test_runner_codeblock_violation(normalized):
hits.append("test_runner_codeblock")
return hits


def staged_adr_added_chunks() -> list[tuple[str, str]]:
"""Per staged ADR path, text formed only from added lines in the cached diff (A or M)."""
proc = _git(["diff", "--cached", "-U0", "--", ADR_GLOB])
if proc.returncode != 0 or not proc.stdout.strip():
return []
chunks: dict[str, list[str]] = {}
current: str | None = None
for line in proc.stdout.splitlines():
if line.startswith("+++ b/"):
current = line.removeprefix("+++ b/").strip()
if current.startswith("docs/adr/"):
chunks.setdefault(current, [])
else:
current = None
continue
if current is None or not line.startswith("+") or line.startswith("+++"):
continue
if line == r"\ No newline at end of file":
continue
chunks[current].append(line[1:])
return [(path, "\n".join(lines)) for path, lines in chunks.items() if lines]


def load_grandfather_allowlist() -> dict[str, dict[str, str]]:
if not GRANDFATHER_FIXTURE.is_file():
return {}
data = json.loads(GRANDFATHER_FIXTURE.read_text(encoding="utf-8"))
return {str(k): dict(v) for k, v in data.items()}
21 changes: 21 additions & 0 deletions tests/fixtures/adr_t7_synthetic_bad_embedded_spike.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# ADR 9998 — Synthetic T7 negative fixture (not a real ADR)

- **Date (UTC):** 2026-01-01
- **Authors:** governance test harness
- **Deciders:** governance test harness

## Status

Proposed

## Context

Local model spike (fabricated numbers for pytest only):

| Model | Latency | Score |
| --- | --- | --- |
| alpha-v1 | 412 ms | 0.71 |
| beta-v2 | 388 ms | 0.74 |
| gamma-v3 | 401 ms | 0.73 |

Operator decision (2026-01-15): adopt gamma-v3 as the permanent architecture.
22 changes: 22 additions & 0 deletions tests/fixtures/adr_t7_synthetic_good_benchmark_reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# ADR 9999 — Synthetic T7 positive fixture (not a real ADR)

- **Date (UTC):** 2026-01-01
- **Authors:** governance test harness
- **Deciders:** governance test harness

## Status

Proposed

## Context

RegexSet probe on a pinned workload failed the performance gate (~**4.7×** slower than a cached
`Regex` loop; wall time ~**99.7%** in matching on the declared profile). Do not paste raw JSON here.

Pinned artifacts live under `tests/benchmarks/rust_prefilter_hotspot.json` and
`tests/benchmarks/filesystem_phase_breakdown.json`. Reproduction steps are tracked in GitHub
issue #1078 (public tracker id only — no private lab hostnames).

## Decision

1. Ship the cached-`Regex` path; link evidence, never embed runner output in this ADR.
Loading
Loading