diff --git a/exemptions.json b/exemptions.json index fc5fc5a..cf3e637 100644 --- a/exemptions.json +++ b/exemptions.json @@ -3,105 +3,105 @@ "entries": [ { "file": "src/bonfire/cli/commands/install_skill.py", - "symbol_or_line": "88", + "symbol_or_line": "install_skill", "rule": "C901", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/cli/commands/install_skill.py", - "symbol_or_line": "88", + "symbol_or_line": "install_skill", "rule": "PLR0915", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/cost/analyzer.py", - "symbol_or_line": "129", + "symbol_or_line": "CostAnalyzer._load_if_needed", "rule": "C901", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/dispatch/runner.py", - "symbol_or_line": "208", + "symbol_or_line": "execute_with_retry", "rule": "BLE001", "reason": "Dispatch retry barrier: must catch ANY backend/SDK failure mode to record the failure and drive the retry verdict; narrowing would let an unexpected failure skip the retry loop. CancelledError still propagates (except Exception excludes it).", "approver": "BubbleGum/Elegance-Law architectural blessing (BON-1757); Anta gates via PR merge" }, { "file": "src/bonfire/dispatch/sdk_backend.py", - "symbol_or_line": "187", + "symbol_or_line": "ClaudeSDKBackend.execute", "rule": "BLE001", "reason": "Third-party Claude Agent SDK boundary: the SDK failure surface is open-set; contract is 'never raises — every failure becomes a FAILED ErrorDetail envelope'.", "approver": "BubbleGum/Elegance-Law architectural blessing (BON-1757); Anta gates via PR merge" }, { "file": "src/bonfire/dispatch/sdk_backend.py", - "symbol_or_line": "196", + "symbol_or_line": "ClaudeSDKBackend._do_execute", "rule": "C901", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/dispatch/security_hooks.py", - "symbol_or_line": "539", + "symbol_or_line": "_match_write_edit_sensitive_path", "rule": "C901", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/dispatch/security_hooks.py", - "symbol_or_line": "843", + "symbol_or_line": "_peel_one", "rule": "C901", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/dispatch/security_hooks.py", - "symbol_or_line": "895", + "symbol_or_line": "_unwrap", "rule": "C901", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/dispatch/security_hooks.py", - "symbol_or_line": "1084", + "symbol_or_line": "build_preexec_hook", "rule": "C901", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/dispatch/security_hooks.py", - "symbol_or_line": "1084", + "symbol_or_line": "build_preexec_hook", "rule": "PLR0915", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/dispatch/security_hooks.py", - "symbol_or_line": "1121", + "symbol_or_line": "build_preexec_hook._hook", "rule": "C901", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/dispatch/security_hooks.py", - "symbol_or_line": "1121", + "symbol_or_line": "build_preexec_hook._hook", "rule": "PLR0915", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/dispatch/security_hooks.py", - "symbol_or_line": "1149", + "symbol_or_line": "build_preexec_hook._hook", "rule": "S101", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/engine/advisor.py", - "symbol_or_line": "62", + "symbol_or_line": "VaultAdvisor.check", "rule": "BLE001", "reason": "Fail-open advisory boundary: a knowledge/vault query failure must degrade to '' (no advice), never raise into the pipeline; the backend is a pluggable Protocol.", "approver": "BubbleGum/Elegance-Law architectural blessing (BON-1757); Anta gates via PR merge" @@ -122,14 +122,14 @@ }, { "file": "src/bonfire/events/consumers/display.py", - "symbol_or_line": "49", + "symbol_or_line": "DisplayConsumer._call", "rule": "BLE001", "reason": "Observer boundary over a user-supplied display callback: an observer must NEVER let an arbitrary callback failure crash the event bus.", "approver": "BubbleGum/Elegance-Law architectural blessing (BON-1757); Anta gates via PR merge" }, { "file": "src/bonfire/events/consumers/logger.py", - "symbol_or_line": "28", + "symbol_or_line": "SessionLoggerConsumer.on_event", "rule": "BLE001", "reason": "Observer boundary over a pluggable persistence backend: must never crash the event bus on a backend failure.", "approver": "BubbleGum/Elegance-Law architectural blessing (BON-1757); Anta gates via PR merge" @@ -143,21 +143,21 @@ }, { "file": "src/bonfire/github/client.py", - "symbol_or_line": "111", + "symbol_or_line": "detect_github_repo", "rule": "S607", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/handlers/architect.py", - "symbol_or_line": "72", + "symbol_or_line": "ArchitectHandler.handle", "rule": "C901", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/handlers/architect.py", - "symbol_or_line": "214", + "symbol_or_line": "ArchitectHandler.handle", "rule": "BLE001", "reason": "StageHandler boundary: any handler failure becomes a typed ErrorDetail envelope (blessed boundary pattern).", "approver": "BubbleGum/Elegance-Law architectural blessing (BON-1757); Anta gates via PR merge" @@ -171,98 +171,98 @@ }, { "file": "src/bonfire/handlers/merge_preflight.py", - "symbol_or_line": "277", + "symbol_or_line": "MergePreflightHandler.handle", "rule": "BLE001", "reason": "StageHandler boundary: any preflight failure becomes a typed ErrorDetail envelope.", "approver": "BubbleGum/Elegance-Law architectural blessing (BON-1757); Anta gates via PR merge" }, { "file": "src/bonfire/handlers/merge_preflight.py", - "symbol_or_line": "564", + "symbol_or_line": "MergePreflightHandler._get_baseline_failures", "rule": "BLE001", "reason": "Best-effort baseline-pytest cache: any failure degrades to an empty baseline and must never block preflight.", "approver": "BubbleGum/Elegance-Law architectural blessing (BON-1757); Anta gates via PR merge" }, { "file": "src/bonfire/handlers/preflight_pytest.py", - "symbol_or_line": "321", + "symbol_or_line": "parse_pytest_junit_xml", "rule": "S314", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/handlers/sage_correction_bounce.py", - "symbol_or_line": "320", + "symbol_or_line": "SageCorrectionBounceHandler.handle", "rule": "BLE001", "reason": "StageHandler boundary: any handler failure becomes a typed ErrorDetail envelope; the co-located 'except CancelledError: raise' arm is preserved.", "approver": "BubbleGum/Elegance-Law architectural blessing (BON-1757); Anta gates via PR merge" }, { "file": "src/bonfire/handlers/sage_correction_bounce.py", - "symbol_or_line": "488", + "symbol_or_line": "SageCorrectionBounceHandler._run_correction_cycle", "rule": "C901", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/handlers/sage_correction_bounce.py", - "symbol_or_line": "539", + "symbol_or_line": "SageCorrectionBounceHandler._run_correction_cycle", "rule": "BLE001", "reason": "Backend dispatch boundary (open-set SDK failures) becomes a FAILED + escalated outcome; the co-located 'except CancelledError: raise' arm is preserved.", "approver": "BubbleGum/Elegance-Law architectural blessing (BON-1757); Anta gates via PR merge" }, { "file": "src/bonfire/handlers/steward.py", - "symbol_or_line": "166", + "symbol_or_line": "StewardHandler.handle", "rule": "BLE001", "reason": "StageHandler boundary: any handler failure becomes a typed ErrorDetail envelope.", "approver": "BubbleGum/Elegance-Law architectural blessing (BON-1757); Anta gates via PR merge" }, { "file": "src/bonfire/handlers/wizard.py", - "symbol_or_line": "492", + "symbol_or_line": "WizardHandler.handle", "rule": "BLE001", "reason": "StageHandler boundary: any handler failure becomes a typed ErrorDetail envelope.", "approver": "BubbleGum/Elegance-Law architectural blessing (BON-1757); Anta gates via PR merge" }, { "file": "src/bonfire/knowledge/backend.py", - "symbol_or_line": "78", + "symbol_or_line": "LanceDBBackend.query", "rule": "BLE001", "reason": "Pluggable LanceDB vector-search boundary: fail-open to [] so a search-backend failure never raises into callers.", "approver": "BubbleGum/Elegance-Law architectural blessing (BON-1757); Anta gates via PR merge" }, { "file": "src/bonfire/knowledge/backend.py", - "symbol_or_line": "117", + "symbol_or_line": "LanceDBBackend.get_by_source", "rule": "BLE001", "reason": "Pluggable LanceDB get-by-source boundary: fail-open to [].", "approver": "BubbleGum/Elegance-Law architectural blessing (BON-1757); Anta gates via PR merge" }, { "file": "src/bonfire/knowledge/chunker.py", - "symbol_or_line": "21", + "symbol_or_line": "chunk_markdown", "rule": "C901", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/knowledge/consumer.py", - "symbol_or_line": "127", + "symbol_or_line": "KnowledgeIngestConsumer._store", "rule": "BLE001", "reason": "Knowledge observer boundary over a pluggable backend: must never crash the event bus on a store failure.", "approver": "BubbleGum/Elegance-Law architectural blessing (BON-1757); Anta gates via PR merge" }, { "file": "src/bonfire/knowledge/scanner.py", - "symbol_or_line": "173", + "symbol_or_line": "ProjectScanner.extract_signatures", "rule": "C901", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/models/plan.py", - "symbol_or_line": "91", + "symbol_or_line": "WorkflowPlan._validate_dag", "rule": "C901", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" @@ -297,14 +297,14 @@ }, { "file": "src/bonfire/onboard/narration.py", - "symbol_or_line": "166", + "symbol_or_line": "_category_key", "rule": "C901", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/onboard/narration.py", - "symbol_or_line": "302", + "symbol_or_line": "NarrationEngine._pick_unused", "rule": "S311", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" @@ -318,14 +318,14 @@ }, { "file": "src/bonfire/onboard/scanners/git_state.py", - "symbol_or_line": "159", + "symbol_or_line": "scan", "rule": "C901", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/onboard/scanners/vault_seed.py", - "symbol_or_line": "255", + "symbol_or_line": "_scan_project_size", "rule": "C901", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" @@ -346,21 +346,21 @@ }, { "file": "src/bonfire/persona/loader.py", - "symbol_or_line": "246", + "symbol_or_line": "PersonaLoader._validate_raw", "rule": "C901", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/scan/tech_scanner.py", - "symbol_or_line": "72", + "symbol_or_line": "_extract_pyproject_deps", "rule": "C901", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" }, { "file": "src/bonfire/scan/tech_scanner.py", - "symbol_or_line": "245", + "symbol_or_line": "TechScanner._detect_frameworks", "rule": "C901", "reason": "grandfathered at v1.0.1 cf-gate adoption 2026-06-22 (ratchet: BON-1757) — pre-existing under the kit ruff config; tracked for fix-forward", "approver": "BubbleGum mount (v1.0.1 gate-adoption), 2026-06-22 — pending Anta's blessing via PR #227" diff --git a/file-budget.json b/file-budget.json index bb2104a..a0e9ed4 100644 --- a/file-budget.json +++ b/file-budget.json @@ -147,7 +147,7 @@ "src/bonfire/handlers": 3280, "src/bonfire/onboard": 4460, "tests/integration": 1702, - "tests/unit": 73777 + "tests/unit": 74023 }, "package_raises": [ { @@ -221,6 +221,14 @@ "lines": 44, "reason": "src/bonfire/onboard measured EXACTLY 4416 against a ceiling of 4416 -- zero headroom, verified by a one-line probe file producing 'package src/bonfire/onboard is 4417 lines'. The 44 lines fix two records in this package that reported a failure as a success. In orchestrator.py a scanner that CRASHED emitted ScanComplete(item_count=0), identical to a scanner that ran clean and found nothing, so the browser said 'we scanned and found nothing' over a scan that had died; _run_one now returns (count, failed) and reports the real exception, and run_scan totals failed panels so an all-crashed run does not summarise as total_items=0. In flow.py every ValidationError on a client frame was reported as message_too_long -- including a frame with no text field at all and a frame whose text was a number -- sending the user to shorten a message that was never long; _frame_rejection now keeps message_too_long only for a genuine pydantic string_too_long and names the actual cause otherwise. protocol.py carries the three new fields, all defaulted to the success shape so an older producer's frame still parses. Explicitly rejected: (1) reporting the crash only to the log, which is where it already went and is not visible to the browser that is the actual consumer; (2) compressing docstrings elsewhere in the package to buy the lines back, which trades documentation for budget and measures as nothing anyway since the unit is max(physical, statements); (3) moving the fix out of onboard into an unbudgeted package, which would take the producer off the path that produces. The protocol.py docstrings WERE trimmed once for size before this number was taken.", "approver": "Anta gates via PR merge -- raise surfaced in the PR body and in the lane report, not taken silently" + }, + { + "package": "tests/unit", + "from": 73777, + "to": 74023, + "lines": 246, + "reason": "exemptions.json anchored 44 of its 52 entries by LINE NUMBER, so inserting any line above a registered noqa silently un-registered a blessed exemption and cf-exemptions then reported UNREGISTERED_SUPPRESSION against the suppression -- naming the code as the culprit when the registry pointer was what rotted. That message is convincing: it cost three lanes an afternoon and nearly bought a rewrite of two load-bearing error barriers that a measurement showed would have turned 10 tests red. This PR converts 41 of the 44 to symbol anchors; the 246 lines are tests/unit/test_exemption_anchors_are_drift_proof.py, which stops the class from coming back. It pins three things nothing else pins: (1) every entry is symbol-anchored except an explicit map of the three that provably cannot be, each carrying the reason -- and the map must match the registry EXACTLY, so a re-introduced line anchor fails AND a stale pin fails; (2) every entry resolves to exactly one live suppression, which catches an ambiguous anchor that blanket-blesses a whole function and a dead entry that covers nothing; (3) the small ast+tokenize resolver the first two use agrees entry-for-entry with cf_quality's own _scan_src/_matches whenever the kit is importable, so the mirror cannot drift from the gate it mirrors. A fourth test is a control rod on the measure itself: a synthetic module with two S101 suppressions in one method must resolve to TWO lines, so the assertion is proved able to fail rather than only observed passing. Explicitly rejected: (1) parking the file at tests/ root, which is unbudgeted and would have cost zero -- there is a repo-wide pin already living there so the placement was arguable, but two earlier raises in this same file rejected the unbudgeted tree as dodging the ratchet and the same answer applies here; (2) a purpose entry, which registers the file out of the ratchet permanently; (3) dropping the kit-agreement test to save ~25 lines, which is the only thing keeping the local resolver honest and would leave a gate that grades a copy of the rule instead of the rule; (4) relying solely on the kit's existing EXEMPTION_ENTRY_OVERLOADED violation, which does catch an ambiguous anchor (rodded: it fails with 'entry 47 covers 2 live suppressions') but does NOT catch a re-introduced line anchor or a dead entry, which are the two failure modes this lane was fired over; (5) padding the ask -- 246 is the file's measured line count and nothing else. The file was tightened before the number was taken (a duplicated pin reason hoisted to one constant, the pin map collapsed from exploded tuples to single lines), which took the measurement from 261 to 246.", + "approver": "Anta gates via PR merge -- raise surfaced in the PR body and in the lane report, not taken silently" } ] } diff --git a/tests/unit/test_exemption_anchors_are_drift_proof.py b/tests/unit/test_exemption_anchors_are_drift_proof.py new file mode 100644 index 0000000..a4fc0af --- /dev/null +++ b/tests/unit/test_exemption_anchors_are_drift_proof.py @@ -0,0 +1,246 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 BonfireAI + +"""Pin test — ``exemptions.json`` anchors must be drift-proof and unambiguous. + +``exemptions.json`` registers every blessed suppression as +``{file, symbol_or_line, rule, reason, approver}``. The quality gate resolves an +entry against a live suppression by ``file + rule + (line OR enclosing symbol)``, +so an entry may be anchored either way. The two forms behave very differently: + +* A **line anchor** is a pointer into a file that any edit can move. Inserting a + single line above a registered ``# noqa`` silently un-registers a blessed + exemption, and the gate then reports ``UNREGISTERED_SUPPRESSION`` against the + suppression — naming the code as the culprit when the registry pointer is what + actually rotted. That message is convincing and it is wrong; it has cost real + review time and nearly provoked rewrites of load-bearing error barriers. +* A **symbol anchor** names the enclosing ``def``/``class`` by qualified name and + survives any insertion. + +So symbol anchors are the rule. But a symbol anchor is only correct when it +identifies **exactly one** suppression. If two suppressions of the same rule live +inside the same enclosing symbol, one symbol anchor covers both: one entry goes +dead, the register stops being a list of decisions and becomes a list of +permissions, and a *future* suppression of that rule dropped into the same +function is auto-blessed with nobody approving it. + +This test pins both halves: + +1. every entry is symbol-anchored, except the line pins documented in + :data:`_LINE_PINNED` — each of which is line-pinned because a symbol anchor + provably *cannot* identify it uniquely; +2. every entry — either form — resolves to exactly **one** live suppression. + +A third test cross-checks the small resolver below against the installed quality +kit, so this file cannot drift away from the gate whose behaviour it mirrors. +""" + +from __future__ import annotations + +import ast +import json +import re +import tokenize +from collections import Counter +from pathlib import Path + +import pytest + +# Repo root: tests/unit/test_exemption_anchors_are_drift_proof.py → parents[2]. +_REPO_ROOT = Path(__file__).resolve().parents[2] +_EXEMPTIONS = _REPO_ROOT / "exemptions.json" + +#: The entries that CANNOT be symbol-anchored, each with the reason a symbol +#: anchor would be wrong rather than merely inconvenient. Anything line-anchored +#: and absent from this map fails the test; anything listed here that becomes +#: symbol-anchorable must be removed from the map, so the list cannot go stale. +_SHARED_WS_HANDLER = ( + "two S101 suppressions share the enclosing symbol FrontDoorServer._ws_handler " + "(lines 446, 447); one symbol anchor would cover both, kill the other entry, and " + "blanket-bless any future assert dropped into that handler" +) +_LINE_PINNED: dict[tuple[str, str, str], str] = { + ("src/bonfire/git/scratch.py", "S105", "55"): ( + "module level: the suppression sits outside any def/class, so there is no " + "enclosing symbol to anchor to" + ), + ("src/bonfire/onboard/server.py", "S101", "446"): _SHARED_WS_HANDLER, + ("src/bonfire/onboard/server.py", "S101", "447"): _SHARED_WS_HANDLER, +} + + +def _load_entries() -> list[dict[str, str]]: + """The committed exemption entries, in file order.""" + data = json.loads(_EXEMPTIONS.read_text(encoding="utf-8")) + return list(data["entries"]) + + +def _symbol_spans(source: str) -> list[tuple[int, int, str]]: + """``(start, end, qualified name)`` for every def/class in *source*. + + Mirrors the quality kit: names are dotted paths (``Outer.run``), never bare + names, because bare names let same-named symbols in different scopes collapse + onto one registry entry. + """ + spans: list[tuple[int, int, str]] = [] + pending: list[tuple[ast.AST, str]] = [(ast.parse(source), "")] + while pending: + node, prefix = pending.pop() + for child in ast.iter_child_nodes(node): + if isinstance(child, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef): + qualname = f"{prefix}{child.name}" + if child.end_lineno is not None: + spans.append((child.lineno, child.end_lineno, qualname)) + pending.append((child, f"{qualname}.")) + else: + pending.append((child, prefix)) + return spans + + +def _enclosing_symbol(spans: list[tuple[int, int, str]], line: int) -> str | None: + """Innermost def/class enclosing *line*, or ``None`` at module level.""" + best: tuple[int, str] | None = None + for start, end, name in spans: + if start <= line <= end and (best is None or end - start < best[0]): + best = (end - start, name) + return best[1] if best else None + + +def _suppression_lines(path: Path, rule: str) -> list[int]: + """Lines of *path* carrying a ``# noqa``/``# nosec`` naming *rule*. + + Comments are read as COMMENT tokens, so suppression text inside a string + literal never counts. + """ + pattern = re.compile(rf"#\s*(?:noqa|nosec)\b[^#]*?\b{re.escape(rule)}\b") + with tokenize.open(path) as handle: + tokens = list(tokenize.generate_tokens(handle.readline)) + return [ + tok.start[0] + for tok in tokens + if tok.type == tokenize.COMMENT and pattern.search(tok.string) + ] + + +def _covered_lines(entry: dict[str, str], root: Path = _REPO_ROOT) -> list[int]: + """Every live suppression line that *entry* covers — the ambiguity measure. + + An entry covers a suppression when file and rule agree and the anchor equals + either the suppression's line number or its enclosing symbol, exactly as the + gate resolves it. + """ + path = root / entry["file"] + anchor = str(entry["symbol_or_line"]).strip() + spans = _symbol_spans(path.read_text(encoding="utf-8")) + return [ + line + for line in _suppression_lines(path, entry["rule"]) + if anchor == str(line) or anchor == _enclosing_symbol(spans, line) + ] + + +def test_every_anchor_is_a_symbol_except_the_documented_line_pins() -> None: + """Line anchors rot on insertion, so only the provably-stuck ones survive.""" + entries = _load_entries() + assert entries, "exemptions.json carries no entries — this test would be vacuous" + + line_anchored = { + (entry["file"], entry["rule"], str(entry["symbol_or_line"]).strip()) + for entry in entries + if str(entry["symbol_or_line"]).strip().isdigit() + } + assert line_anchored == set(_LINE_PINNED), ( + "line-anchored exemption entries must match _LINE_PINNED exactly.\n" + f"undocumented line anchors (re-anchor to the enclosing symbol): " + f"{sorted(line_anchored - set(_LINE_PINNED))}\n" + f"stale _LINE_PINNED rows (entry is gone or now symbol-anchored, drop them): " + f"{sorted(set(_LINE_PINNED) - line_anchored)}" + ) + # Control rod on this assertion: it must be measuring a real, non-empty + # population of symbol anchors, not passing because everything is exempt. + assert len(entries) - len(line_anchored) > 0 + + +def test_no_exemption_entry_covers_more_than_one_suppression() -> None: + """One entry, one suppression — an anchor is a decision, never a permission.""" + entries = _load_entries() + coverage = {index: _covered_lines(entry) for index, entry in enumerate(entries)} + + ambiguous = {index: lines for index, lines in coverage.items() if len(lines) > 1} + assert not ambiguous, ( + "these exemption entries each cover MORE THAN ONE live suppression, so " + "one entry silently blesses several sites and a future suppression of " + "the same rule in that symbol would be auto-covered: " + + "; ".join( + f"entry {i} {entries[i]['file']} {entries[i]['rule']} " + f"anchor={entries[i]['symbol_or_line']!r} -> lines {lines}" + for i, lines in sorted(ambiguous.items()) + ) + ) + + dead = {index: lines for index, lines in coverage.items() if not lines} + assert not dead, ( + "these exemption entries cover NO live suppression — the anchor is stale " + "or the suppression is gone, and the entry is dead weight on the ratchet: " + + "; ".join( + f"entry {i} {entries[i]['file']} {entries[i]['rule']} " + f"anchor={entries[i]['symbol_or_line']!r}" + for i in sorted(dead) + ) + ) + + +def test_the_ambiguity_measure_reports_two_for_a_deliberately_ambiguous_anchor( + tmp_path: Path, +) -> None: + """Control rod: the measure above must be able to fail, not only to pass. + + Two suppressions of one rule inside one function, anchored by that + function's name, is precisely the case the gate exists to reject. + """ + module = tmp_path / "src" / "probe.py" + module.parent.mkdir(parents=True) + module.write_text( + "class Handler:\n" + " def run(self) -> None:\n" + " assert True # noqa: S101\n" + " assert False # noqa: S101\n" + "\n" + " def other(self) -> None:\n" + " assert True # noqa: S101\n", + encoding="utf-8", + ) + ambiguous = {"file": "src/probe.py", "symbol_or_line": "Handler.run", "rule": "S101"} + assert _covered_lines(ambiguous, root=tmp_path) == [3, 4] + + unique = {"file": "src/probe.py", "symbol_or_line": "Handler.other", "rule": "S101"} + assert _covered_lines(unique, root=tmp_path) == [7] + + pinned = {"file": "src/probe.py", "symbol_or_line": "4", "rule": "S101"} + assert _covered_lines(pinned, root=tmp_path) == [4] + + +def test_the_local_resolver_agrees_with_the_installed_quality_kit() -> None: + """Round trip: this file's resolver must match the gate it mirrors. + + The gate that actually blocks a merge is ``cf-exemptions`` from the quality + kit. The kit is not a test-time dependency of this repo, so the tests above + resolve anchors themselves; this test asserts that the two agree entry by + entry whenever the kit IS importable, so the mirror cannot silently drift. + """ + exemptions = pytest.importorskip( + "cf_quality.exemptions", + reason="quality kit not installed here; cf-exemptions enforces this in CI", + ) + suppressions, _ = exemptions._scan_src(_REPO_ROOT) + assert suppressions, "the kit found no suppressions — the comparison would be vacuous" + + entries = _load_entries() + kit = Counter[int]() + for suppression in suppressions: + for index, entry in enumerate(entries): + if exemptions._matches(suppression, entry): + kit[index] += 1 + + mine = {index: len(_covered_lines(entry)) for index, entry in enumerate(entries)} + assert mine == {index: kit[index] for index in range(len(entries))}