docs(backlog): wire the dangling-citation detector to report, advisory only (BACKLOG #1431) - #821
Conversation
…y only (BACKLOG #1431)
scripts/docs/dangling_citation_check.py works and its findings are real, but
nothing ran the script. Verified at HEAD with controls in the same pass rather
than trusted from its docstring: the script path occurs 0 times across .github/
plus .pre-commit-config.yaml, while backlog_citation_check.py (a real `run:` at
backlog-hygiene.yml:236) occurs once and an invented path occurs 0 times. The
one .github/ line naming this module is ci.yml:250, and it names the TEST.
Adds a `dangling backlog citations (advisory)` job to quality-advisory.yml, the
one workflow in this repository that holds no required context. It reports on
every pull request, including documentation-only ones, which is exactly the
change shape that introduces a citation and exactly the shape the pytest tier
holding this tool's unit test is skipped for.
IT CANNOT BLOCK A MERGE, by four independent means:
1. the workflow holds no required status-check context, and
.github/required-contexts.txt records that it must never be promoted;
2. --advisory makes a live-shape finding exit 0 (the tool's default is 1);
3. continue-on-error: true, so even the checker's empty-population refusal
cannot fail the job;
4. the job is absent from the liveness meta-gate's `needs`.
Promoting it to blocking is an owner decision and is deliberately untaken, on
the #353 precedent. tests/test_dangling_citation_advisory.py pins the three of
those four a repository test can see, each with a positive control, and each
was mutated to confirm it reddens rather than passing inertly.
ONE GUARD WAS INERT ON FIRST WRITING AND THE MUTATION CAUGHT IT. The advisory
check asked whether "--advisory" appeared anywhere in the step, and it did --
in the step's own warning text -- so deleting the flag from the command left
the test green. It now asserts on the invocation line, and the warning no
longer carries the string.
The item records the measurement, the wiring gap, the recognition rule (the
window is [1000,9000), which is why a planted #999998 control did not fire),
and a triage of the nine numbers into kinds: one is another project's issue
number, seven are the ledger deliberately narrating a burned or abandoned
number, and one points at real dangling citations in non-Markdown files the
default scan never reads. No citation is repaired here; that is a separate act.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
CONSOLE SEAT: you corrected my premise twice and both corrections are right. Labelled. I BRIEFED YOU THAT THE TOOL "EXITS 0 ON FINDINGS". IT DOES NOT. It fails closed on the live shape by default and Your measurement of MY CONTROL FAILURE IS ANSWERED, and the answer is better than the guesses I offered. I ALSO NAMED THE WRONG STRONGEST CASE. I told you, and separately told a peer Console seat, that #1084 was the keeper. You have shown it is explicitly recorded as dropped and burned, and that #1364 is the real one -- five live instances in #1459 as a false positive resolving to TWO THINGS IN YOUR REPORT I RATE ABOVE THE WIRING ITSELF: The tool catching your own draft -- your item carried two live-shape citations and exit 1 named them -- is the best possible evidence that the detector works, and you got it by accident rather than by design. Worth keeping in the row. The inert guard is the finding I would promote. Your advisory check asked whether Advisory wiring confirmed as I asked: no required context, absent from ci-gate's needs, zero occurrences in required-contexts.txt, plus --advisory and continue-on-error. Enforcement stays available and untaken, which is the owner's call and not ours. |
Both sides append a new section at the same point in docs/BACKLOG.md, under different item numbers. Neither supersedes the other, so taking a side would drop a filed item outright. Both blocks are kept, in the ascending item order the file already uses. Verified with parse_items from scripts/docs/backlog_status_check.py rather than a hand-rolled scan, and by an ADDED/LOST item-set difference against both parents, which is empty. A count alone would not catch two blocks folding into one.
Files BACKLOG #1431 and wires
scripts/docs/dangling_citation_check.pyso it reports. It cannot fail a build.What was measured
python scripts/docs/dangling_citation_check.py docs/BACKLOG.mdat46ea10a78reports 53 tokens across 9 distinct numbers: #1084, #1203, #1231, #1297, #1303, #1326, #1364, #1411 and one pyodbc issue number.The exit code is 0 and that is correct -- a correction to the framing this work started from. The tool fails closed on the live shape by DEFAULT;
--advisoryis the opt-out. It exits 0 today because zero of the 53 tokens are live-shape: 50 sit at or below the allocator high-water mark and can never be issued, and 3 belong to another project. Measured directly, not through a pipe ($?after| tailreadstail). The defect is purely that nothing ran the script.The wiring gap, verified at HEAD rather than read from the docstring
.github/+.pre-commit-config.yamlscripts/docs/dangling_citation_check.py(subject)scripts/docs/backlog_citation_check.py(positive control, a realrun:)scripts/docs/zzz_nonsense.py(null control)The one
.github/line naming this module isci.yml:250, and it names the TEST..mefor-hooks/pre-commitexecsmessagefoundry checkand nothing else.The recognition rule, and why the earlier control did not fire
A planted
See BACKLOG #999998 for details.produced "No unresolved backlog citation". The cause is the window, not a path or file-type filter:_LOW = 1000,_HIGH = 9000, so#999998is out of range. A token is recognised only when it is#glued to digits, ending at a non-alphanumeric boundary, with the number in[1000, 9000). A space after#disqualifies it, which is why a## 1431.heading cannot report as a citation of itself.Made to fire on demand: an in-window number above the floor, not preceded by a PR/issue word or an identifier character, is reported as This is the live shape and exits 1. The same token behind
PRis reported and annotated but leaves the exit code alone. Confirmed against a control file with nine shapes covering every rule.Triage of the nine
All are below the allocator floor except the foreign-repo one, so none can arm. They are three different things:
#1364 is the one worth acting on: commit
4c8845754wrote five citations into.gitattributes,scripts/quality/licence_header_check.pyandtests/test_licence_header_gate.py-- none of them Markdown, so the defaultdocs/**/*.mdscan reads about them and never reads them. No citation is repaired here. That is a separate act, and #1411's own entry prescribes a repointing this PR does not perform.Verdict on the number above the floor: FALSE POSITIVE
All three of its tokens are
mkleehammer/pyodbc#1459-- the upstream pyodbc issue whose fix would license removingscripts/ci/retry-native-crash.shfrom the database legs. The arithmetic that makes it look like the forbidden "cite a number you have not allocated" defect is right; it is right about the wrong namespace. The tool annotates all three as foreign-repo shaped and its exit code passes over them, which is the correct call. Worth noting as load-bearing: the_FOREIGN_REPOrule is the only thing between that token and a red gate.What was wired, and that it cannot block a merge
A
dangling backlog citations (advisory)job in.github/workflows/quality-advisory.yml-- the one workflow here that holds no required context, and which.github/required-contexts.txtrecords as "advisory BY DESIGN and must never be promoted". Four independent things keep it non-blocking:--advisorymakes a live-shape finding exit 0;continue-on-error: true, so even the checker's empty-population refusal cannot fail the job;livenessmeta-gate'sneeds, so it cannot redden the one job in that file built to go red.It is not in a pre-commit hook, not in
.github/required-contexts.txt, and does not exit non-zero on findings. Promoting it to blocking is deliberately left on the table as an owner decision, on the #353 precedent and against the current suspension of the reviewer process.The exit code is deliberately NOT swallowed with
|| true. Under--advisorythe only non-zero outcome is a genuine malfunction -- most plausibly the tool's refusal to report on an empty population, which is what running from the wrong directory looks like. That stays visible as a red step inside a green job, because a clean scan and a scan that read nothing must not render alike.A number reconcilers should expect: the job scans all of
docs/, not justBACKLOG.md, so it reports 83 tokens across 11 numbers with this row filed. The two extra are the same kinds already triaged (a PR number indocs/adr/, and one more wrong-worktree hole in the closed archive). The item says so.The tool caught me while I was writing about it
Two live-shape citations were introduced by the draft item itself: a copy-pasteable worked example, and bare spellings of the pyodbc number. Running the detector over the edited file returned exit 1 and named them. Both are fixed -- the example is now spelled out in words rather than shown, and the pyodbc number appears only in its qualified form -- and the item records why the one artifact that most wants a worked example is the one that cannot hold it.
Guard quality
tests/test_dangling_citation_advisory.pypins the three advisory properties a repository test can see, each absence assertion carrying a positive control. Every guard was mutated to confirm it reddens, and one did not: the advisory check asked whether--advisoryappeared anywhere in the step, and it did -- in the step's own warning text -- so deleting the flag from the command left the test green. It now asserts on the invocation line, the warning no longer carries the string, and all four controls fire.Checks run
ruff format+ruff checkpytest tests/test_dangling_citation_advisory.pytest_quality_advisory_invariants+test_tooling_partition+test_doc_guards_lane+test_dangling_citation_check+test_backlog_status_checktest_required_contexts+test_link_resolution+test_ledger_check+test_backlog_citation_check(+2)test_workflow_shell_syntax+test_cp1252_console_safetybacklog_status_check.pydangling_citation_check.pyover all ofdocs/bash -e,GITHUB_STEP_SUMMARYredirected)bash -non the step body (with a failing control)NOT RUN, and not claimed:
mypy. It is not installed in this worktree (there is no.venv), andpytestneeded--noconftest -c /dev/nullbecauseconftest.pyimportsmessagefoundry, which needspydantic. Note the configured scope ismypy messagefoundry messagefoundry_webconsole(ci.yml:417) and this change touches neither package -- it changes a workflow, a test, the tooling manifest and the ledger -- so mypy is structurally unaffected. The full suite and every hosted-runner leg still need reading after this process exits.Open question for the owner
Should this check be promoted to blocking? Everything needed is in place; the decision is deliberately untaken. Promoting it means adding the context to branch protection FIRST, then
.github/required-contexts.txtand the count intests/test_required_contexts.pyin the same PR -- and it would mean moving the job out ofquality-advisory.yml, whose advisory invariant is itself pinned by a test.🤖 Generated with Claude Code