Skip to content

docs(backlog): wire the dangling-citation detector to report, advisory only (BACKLOG #1431) - #821

Merged
wshallwshall merged 2 commits into
mainfrom
worktree-agent-a04ee69cca322268f
Sep 4, 2026
Merged

docs(backlog): wire the dangling-citation detector to report, advisory only (BACKLOG #1431)#821
wshallwshall merged 2 commits into
mainfrom
worktree-agent-a04ee69cca322268f

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Files BACKLOG #1431 and wires scripts/docs/dangling_citation_check.py so it reports. It cannot fail a build.

What was measured

python scripts/docs/dangling_citation_check.py docs/BACKLOG.md at 46ea10a78 reports 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; --advisory is 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 | tail reads tail). The defect is purely that nothing ran the script.

The wiring gap, verified at HEAD rather than read from the docstring

probe over .github/ + .pre-commit-config.yaml lines
scripts/docs/dangling_citation_check.py (subject) 0
scripts/docs/backlog_citation_check.py (positive control, a real run:) 1
scripts/docs/zzz_nonsense.py (null control) 0

The one .github/ line naming this module is ci.yml:250, and it names the TEST. .mefor-hooks/pre-commit execs messagefoundry check and 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 #999998 is 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 PR is 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:

kind numbers tokens
Not a backlog citation at all the pyodbc issue 3
The ledger deliberately narrating a burned, abandoned, wrong-worktree or duplicated number #1084, #1203, #1231, #1297, #1303, #1326, #1411 46
Narrative about real dangling citations that live where the scan does not look #1364 4

#1364 is the one worth acting on: commit 4c8845754 wrote five citations into .gitattributes, scripts/quality/licence_header_check.py and tests/test_licence_header_gate.py -- none of them Markdown, so the default docs/**/*.md scan 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 removing scripts/ci/retry-native-crash.sh from 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_REPO rule 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.txt records as "advisory BY DESIGN and must never be promoted". Four independent things keep it non-blocking:

  1. the workflow holds no required status-check context;
  2. --advisory makes a live-shape finding exit 0;
  3. continue-on-error: true, so even the checker's empty-population refusal cannot fail the job;
  4. it is absent from the liveness meta-gate's needs, 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 --advisory the 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 just BACKLOG.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 in docs/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.py pins 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 --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, the warning no longer carries the string, and all four controls fire.

Checks run

check result
ruff format + ruff check PASSED via pre-commit (no standalone ruff in this worktree)
pytest tests/test_dangling_citation_advisory.py 7 passed
negative controls (4 mutations) all four reddened, all restored green
test_quality_advisory_invariants + test_tooling_partition + test_doc_guards_lane + test_dangling_citation_check + test_backlog_status_check 125 passed
test_required_contexts + test_link_resolution + test_ledger_check + test_backlog_citation_check (+2) 123 passed
test_workflow_shell_syntax + test_cp1252_console_safety 35 passed
backlog_status_check.py OK, 658 items, each declaring exactly one status
dangling_citation_check.py over all of docs/ exit 0, no live shape
step body driven end to end (bash -e, GITHUB_STEP_SUMMARY redirected) exit 0, summary written
bash -n on the step body (with a failing control) OK
glyph scan of every added line, with a positive control 1 hit, the sanctioned status banner

NOT RUN, and not claimed: mypy. It is not installed in this worktree (there is no .venv), and pytest needed --noconftest -c /dev/null because conftest.py imports messagefoundry, which needs pydantic. Note the configured scope is mypy 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.txt and the count in tests/test_required_contexts.py in the same PR -- and it would mean moving the job out of quality-advisory.yml, whose advisory invariant is itself pinned by a test.

🤖 Generated with Claude Code

…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>
@wshallwshall wshallwshall added the reviewed A reviewer has read this. Removed automatically when new commits arrive. label Sep 3, 2026
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

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 --advisory is the opt-out. It exits 0 today because zero of the 53 tokens are live-shape, which is a completely different statement. I inferred behaviour from one observed exit code and wrote it into your brief as a property. That is the same shape as reading a correct refusal as a broken allocator, which I did twice today on a different item.

Your measurement of $? after a pipe reading tail rather than the script is the reason to distrust exactly the kind of reading I gave you. CLAUDE.md names that one specifically.

MY CONTROL FAILURE IS ANSWERED, and the answer is better than the guesses I offered. _HIGH = 9000, so #999998 was out of window and invisible. Not a path filter, not a file-type filter, not a citation-form rule. I listed three possibilities and none of them was it, which is the argument for making you determine it rather than letting you inherit my hypothesis.

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 .gitattributes, scripts/quality/licence_header_check.py and tests/test_licence_header_gate.py, none of them Markdown, so the scan cannot reach them. That is a genuine dangling citation in a place the detector structurally cannot see, which is a better finding than the one I sent you looking for. I am correcting it with the peer.

#1459 as a false positive resolving to mkleehammer/pyodbc#1459 is a good catch, and your point that _FOREIGN_REPO is load-bearing rather than cosmetic is the part to keep. It is the only thing between a foreign-repo token and a red gate.

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 appeared anywhere in the step, and it did, inside the step's own warning text, so deleting the actual flag left the test green. A test that cannot fail is worse than no test, because it occupies the slot where a real one would go. You found it by mutation and not by review, which is the whole argument for mutation-checking a guard you just wrote.

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.
@github-actions github-actions Bot removed the reviewed A reviewer has read this. Removed automatically when new commits arrive. label Sep 4, 2026
@wshallwshall wshallwshall added the reviewed A reviewer has read this. Removed automatically when new commits arrive. label Sep 4, 2026
@wshallwshall
wshallwshall added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit 33e36d4 Sep 4, 2026
67 of 74 checks passed
@wshallwshall
wshallwshall deleted the worktree-agent-a04ee69cca322268f branch September 4, 2026 14:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

reviewed A reviewer has read this. Removed automatically when new commits arrive.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant