diff --git a/.cursor/rules/adr-trigger.mdc b/.cursor/rules/adr-trigger.mdc index 7019d0ac..6928fc84 100644 --- a/.cursor/rules/adr-trigger.mdc +++ b/.cursor/rules/adr-trigger.mdc @@ -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` diff --git a/docs/adr/ADR-0000-project-origin-and-adr-baseline.md b/docs/adr/ADR-0000-project-origin-and-adr-baseline.md index f7ada3af..4770dc9c 100644 --- a/docs/adr/ADR-0000-project-origin-and-adr-baseline.md +++ b/docs/adr/ADR-0000-project-origin-and-adr-baseline.md @@ -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. | diff --git a/docs/adr/ADR-0045-adr-metadata-and-format-standardization.md b/docs/adr/ADR-0045-adr-metadata-and-format-standardization.md index 656798a6..a008d871 100644 --- a/docs/adr/ADR-0045-adr-metadata-and-format-standardization.md +++ b/docs/adr/ADR-0045-adr-metadata-and-format-standardization.md @@ -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 @@ -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 diff --git a/docs/adr/INVENTORY.txt b/docs/adr/INVENTORY.txt index 4c575e4b..66331c9f 100644 --- a/docs/adr/INVENTORY.txt +++ b/docs/adr/INVENTORY.txt @@ -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 | - @@ -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 | - @@ -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 diff --git a/docs/plans/PLAN_ADR_GOVERNANCE_ENFORCEMENT.md b/docs/plans/PLAN_ADR_GOVERNANCE_ENFORCEMENT.md index e904dac0..8acdeb2d 100644 --- a/docs/plans/PLAN_ADR_GOVERNANCE_ENFORCEMENT.md +++ b/docs/plans/PLAN_ADR_GOVERNANCE_ENFORCEMENT.md @@ -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. @@ -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 @@ -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 diff --git a/tests/adr_governance_support.py b/tests/adr_governance_support.py index 34bf05ae..4a33c789 100644 --- a/tests/adr_governance_support.py +++ b/tests/adr_governance_support.py @@ -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, +) +# 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") @@ -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 + + +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()} diff --git a/tests/fixtures/adr_t7_synthetic_bad_embedded_spike.md b/tests/fixtures/adr_t7_synthetic_bad_embedded_spike.md new file mode 100644 index 00000000..774a7e42 --- /dev/null +++ b/tests/fixtures/adr_t7_synthetic_bad_embedded_spike.md @@ -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. diff --git a/tests/fixtures/adr_t7_synthetic_good_benchmark_reference.md b/tests/fixtures/adr_t7_synthetic_good_benchmark_reference.md new file mode 100644 index 00000000..d227f33d --- /dev/null +++ b/tests/fixtures/adr_t7_synthetic_good_benchmark_reference.md @@ -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. diff --git a/tests/test_adr_governance_phase1.py b/tests/test_adr_governance_phase1.py index 3de5463d..7d38cfa2 100644 --- a/tests/test_adr_governance_phase1.py +++ b/tests/test_adr_governance_phase1.py @@ -1,6 +1,7 @@ """ADR governance Phase 1 anti-regression tests (issue #1162, ADR-0045). -T1 lifecycle · T2 locale/structure · T5 anti-deletion (rename-aware) · T6 date immutability. +T1 lifecycle · T2 locale/structure · T5 anti-deletion (rename-aware) · T6 date immutability · +T7 no embedded experimental data (#1925). Does not duplicate test_adr_inventory_sync or test_adr_readme_index_sync. """ @@ -14,6 +15,7 @@ from tests.adr_governance_support import ( ADR_DIR, GENESIS_FIXTURE, + GRANDFATHER_FIXTURE, H1_RE, ISO_DATE_IN_DATE_LINE_RE, META_AUTHORS_RE, @@ -22,13 +24,18 @@ NEW_ADR_ALLOWED_STATUSES, PT_BR_HEADING_DENYLIST, REPO_ROOT, + T7_BAD_FIXTURE, + T7_GOOD_FIXTURE, + embedded_experimental_data_violations, extract_date_line, git_is_repo, governance_override_present, iter_adr_files, load_genesis_fixture, + load_grandfather_allowlist, parse_status, read_adr_text, + staged_adr_added_chunks, staged_adr_paths, staged_name_status_with_renames, ) @@ -187,3 +194,162 @@ def test_phase1_corpus_file_count_matches_fixture() -> None: f"fixture rows={len(baseline)} adr files={len(adrs)}; " "regenerate tests/fixtures/adr_genesis_date_lines.json" ) + + +@pytest.mark.parametrize( + ("snippet", "expect_violation"), + [ + ("- Operator decision (2026-09-10): adopt X", True), + ("**Operator decision (2026-09-10):** adopt X", True), + ("> Operator decision (2026-09-10): adopt X", True), + ( + "```yaml\nsteps:\n - status: failed\n```", + False, + ), + ( + "```text\nconnection failed: retrying in 5 s\n```", + False, + ), + ( + "| Tier | Coverage |\n| --- | --- |\n" + "| A | 80% |\n| B | 60% |\n| C | 40% |\n", + False, + ), + ( + "| Step | Wait |\n| --- | --- |\n" + "| connect | 5 s |\n| retry | 30 s |\n| ceiling | 60 s |\n", + False, + ), + ], + ids=[ + "operator_bullet", + "operator_bold", + "operator_blockquote", + "yaml_status_failed", + "log_retry_seconds", + "options_percent_table", + "timeout_seconds_table", + ], +) +def test_t7_auditor_probe_table(snippet: str, expect_violation: bool) -> None: + """T7: #1925 auditor probe table — 3 escapes flagged, 4 legitimate snippets clean.""" + hits = embedded_experimental_data_violations(snippet) + if expect_violation: + assert "operator_decision_line" in hits + else: + assert hits == [] + + +@pytest.mark.parametrize( + ("snippet", "expect_table_violation"), + [ + ( + "| a | 412 ms |\n| b | 388 ms |\n| c | 401 ms |\n", + True, + ), + ( + "| one | 5 seconds |\n| two | 6 seconds |\n| three | 7 seconds |\n", + False, + ), + ( + "| a | 8 users |\n| b | 12 users |\n| c | 3 users |\n", + False, + ), + ( + "| Throughput (MB/s) |\n| a | 1 |\n| b | 2 |\n| c | 3 |\n", + False, + ), + ( + "| Matriz A × B |\n| a | 1 |\n| b | 2 |\n| c | 3 |\n", + False, + ), + ( + "| not x slower |\n| a | 1 |\n| b | 2 |\n| c | 3 |\n", + False, + ), + ], + ids=[ + "staged_data_rows_only", + "seconds_word", + "users_word", + "throughput_mbs_label", + "matrix_times_label", + "not_x_slower_label", + ], +) +def test_t7_table_measurement_probes( + snippet: str, expect_table_violation: bool +) -> None: + """T7: partial staged tables and unit word boundaries (bugbot #2020).""" + hits = embedded_experimental_data_violations(snippet, check_tables=True) + if expect_table_violation: + assert "benchmark_dataset_table_3plus_rows" in hits + else: + assert "benchmark_dataset_table_3plus_rows" not in hits + + +def test_t7_synthetic_bad_fixture_is_flagged() -> None: + """T7: incident-shaped synthetic excerpt must trip the detector.""" + text = T7_BAD_FIXTURE.read_text(encoding="utf-8") + hits = embedded_experimental_data_violations(text) + assert "operator_decision_line" in hits + assert "benchmark_dataset_table_3plus_rows" in hits + + +def test_t7_synthetic_good_fixture_is_clean() -> None: + """T7: prose + pinned artifact path (ADR-0078 style) must not trip the detector.""" + text = T7_GOOD_FIXTURE.read_text(encoding="utf-8") + assert embedded_experimental_data_violations(text) == [] + + +def test_t7_real_adr_corpus_has_no_incident_signatures() -> None: + """T7: full corpus scan — list must stay empty (PR body cites this test).""" + violations: list[str] = [] + for path in iter_adr_files(): + hits = embedded_experimental_data_violations( + read_adr_text(path), check_tables=False + ) + if hits: + violations.append(f"{path.name}: {', '.join(hits)}") + assert not violations, ( + "ADR corpus T7 hits (add dated grandfather only if truly needed):\n" + + "\n".join(violations) + ) + + +def test_t7_grandfather_allowlist_still_justified() -> None: + """T7: allowlisted ADRs must still violate; drop entries when the ADR is cleaned.""" + if not GRANDFATHER_FIXTURE.is_file(): + return + allowlist = load_grandfather_allowlist() + if not allowlist: + return + stale: list[str] = [] + for name, meta in allowlist.items(): + path = ADR_DIR / name + if not path.is_file(): + stale.append(f"{name}: missing file") + continue + hits = embedded_experimental_data_violations( + read_adr_text(path), check_tables=True + ) + if not hits: + stale.append( + f"{name}: no longer violates ({meta.get('reason', 'no reason')})" + ) + assert not stale, "stale grandfather entries:\n" + "\n".join(stale) + + +@pytest.mark.skipif(not git_is_repo(), reason="git required for incremental ADR gates") +def test_t7_staged_adr_added_lines_no_embedded_data() -> None: + """T7: scan only + lines from staged A/M ADR diffs (not the full legacy file).""" + if governance_override_present(): + pytest.skip("operator override marker present") + violations: list[str] = [] + for rel, added in staged_adr_added_chunks(): + hits = embedded_experimental_data_violations(added) + if hits: + violations.append(f"{rel}: {', '.join(hits)} in staged additions") + assert not violations, "T7 staged ADR addition violations:\n" + "\n".join( + violations + )