From 49b7ca1bb19d0fd9d711968846e75779110af87f Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Sun, 23 Aug 2026 14:27:30 -0500 Subject: [PATCH] ci: run the dangling-citation detector on documentation-only PRs (BACKLOG #1235) The gate existed, passed its own 45 tests, and NEVER RAN ON THE SHAPE IT WAS BUILT FOR. #1235 is about a citation to an unallocated number, which is introduced BY EDITING PROSE -- and the detector ran only inside pytest, which a documentation-only pull request skips. `ci.yml`'s docs-only step named eighteen modules including TWO citation siblings (test_backlog_citation_check, test_claude_section_citations) and not this one. WHY ONE LINE IS THE WHOLE FIX, verified rather than assumed: the module ALREADY carries both arms the re-score asks for. `test_no_docs_citation_names_a_number_that_can_still_be _issued` is the must-trip; `test_the_docs_scan_actually_covers_something` pins the population at >200 files so a collapsed walk cannot report clean forever. It scans the real tree via `rglob`, not a fixture. So the residual really was wiring, not coverage. THE SECOND FILE IS THE PART THAT LASTS. Nothing pinned the module's MEMBERSHIP, so deleting the line would have regressed in silence -- `test_every_named_doc_guard_exists` only validates modules that ARE named, and `test_the_lane_is_not_empty` passes at any count above ten. A guard removed from the list is indistinguishable from one that was never in it, which is #1235's own defect wearing a different hat. MUTATION-VERIFIED: removing the line from DOC_GUARDS fails the new test and only that test, 1 of 4. Restored, 4 pass. A FLOOR MEMBER, NOT A CENSUS. The lane file's docstring says the list is "a FLOOR, NOT a census" and this does not change that -- it names the three guards whose absence was the defect and asserts nothing about the other fifteen. No ledger row: touches_code is *messagefoundry/*|*ide/*|*messagefoundry_webconsole/*, and this touches .github/ and tests/ only. Confirmed by running the gate's logic, not by reading it. 63 passed across test_required_contexts, test_ci_step_margin, test_lint_scope_parity and test_tooling_partition. DISPOSITION IS NOT MINE. The re-score names this as the item's last gap, so #1235 may now be closable -- that is the Dispatcher's call and I have not touched the banner. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 3 ++- tests/test_doc_guards_lane.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec7aed61..302557db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -240,7 +240,8 @@ jobs: tests/test_backlog_status_check.py tests/test_sds_rule_ids_are_stable.py tests/test_link_resolution.py tests/test_dast_claims.py tests/test_claude_section_citations.py tests/test_write_share_denominator.py - tests/test_cutover_slug_rot.py tests/test_backlog_citation_check.py" + tests/test_cutover_slug_rot.py tests/test_backlog_citation_check.py + tests/test_dangling_citation_check.py" # Every named module must EXIST. A path typo would otherwise make pytest error on an unknown # file, or — worse under a future -k/--ignore form — silently scan nothing and read as a pass. for m in $DOC_GUARDS; do diff --git a/tests/test_doc_guards_lane.py b/tests/test_doc_guards_lane.py index a33179c4..3367bdcd 100644 --- a/tests/test_doc_guards_lane.py +++ b/tests/test_doc_guards_lane.py @@ -71,6 +71,38 @@ def test_the_lane_is_not_empty_and_names_real_test_modules() -> None: @pytest.mark.skipif(shutil.which("git") is None, reason="needs git to build an isolated fixture") +def test_the_citation_guards_are_IN_the_lane(tmp_path: Path) -> None: + """A FLOOR MEMBER, NOT A CENSUS (BACKLOG #1235). + + This file's own docstring says the list is "a FLOOR, NOT a census", and this test does not + change that: it names the members whose ABSENCE was the defect, and says nothing about the rest. + + #1235's whole thesis is a citation to an unallocated number, which is introduced BY EDITING + PROSE. So its detector must run on the pull requests that edit prose -- and `ci.yml`'s + documentation-only step is the only leg that runs there. `tests/test_dangling_citation_check.py` + was absent from DOC_GUARDS while TWO citation siblings were present, so the gate existed, + passed its own tests, and never ran on the shape it was built for. + + WITHOUT THIS TEST, DELETING THAT LINE REGRESSES SILENTLY. The lane's other checks do not catch + it: `test_every_named_doc_guard_exists` only validates the modules that ARE named, and + `test_the_lane_is_not_empty` passes at any count above ten. A guard removed from the list is + indistinguishable from a guard that was never in it. + """ + guards = set(_doc_guards()) + required = { + "tests/test_dangling_citation_check.py", + "tests/test_backlog_citation_check.py", + "tests/test_claude_section_citations.py", + } + missing = sorted(required - guards) + assert not missing, ( + "citation guards dropped from the documentation-only lane: " + + ", ".join(missing) + + " -- a citation is introduced by editing prose, so a detector that does not run on a " + "docs-only PR does not run on the shape it exists for (BACKLOG #1235)." + ) + + def test_THE_LANE_CAN_FAIL_on_a_planted_documentation_violation(tmp_path: Path) -> None: """THE ARM THE ITEM WAS LEFT OPEN FOR: prove a docs-only violation turns this lane RED.