ci: no-new-decay gate for line-number citations - #1168
Conversation
A citation to `<file>:NNN` into an append-only PREPENDED file decays on every new entry, with no edit on either side. Two measured instances today: a plan cited EPIPHANIES.md:438 for a claim, and by ratification day :438 had become the heading of the ruling that RETRACTED it -- a citation to a removal reads identically to a citation to the thing removed. The same plan cited :13401 where the target was :13615. The gate recovers an anchor near each citation (board id, backticked symbol, quoted phrase; nearest wins) and asks whether it is still within +-3 lines of the cited line. Exits non-zero ONLY on confirmed decay. No recoverable anchor, an unresolvable path, or a bare basename is UNVERIFIABLE: reported, never failed, and never guessed -- a guessed path would manufacture verdicts. Three orchestrator corrections to the worker's draft, each from a test that failed rather than from review: 1. Scoped to changed LINES, not changed files. File scoping is not enough and it was measured: EPIPHANIES.md alone carries 10 pre-existing decays and nearly every PR touches it under the board-hygiene rule, so a file-scoped gate would have failed almost every PR for decay it did not introduce. That is the fires-on- everything defect, and shipping it would have contradicted this session's own advice against the untracked-plan check. 2. The citation regex could not match ANY dot-prefixed path. Its lookbehind rejected a preceding dot, so every `.claude/...` citation -- most of the paths this workspace cites -- was invisible to the scanner. Found because a probe citation was silently ignored. 3. Fails closed when the diff cannot be computed, rather than falling back to checking every line. Verified on real committed diffs, all three cases: - added line, wrong number -> EXIT 1, names file, target and anchor - added line, correct number -> EXIT 0 - untouched file with 10 known decays -> EXIT 0 Measured backlog on main: 124 confirmed decays, 2153 unverifiable of 2314 citations. The 93% unverifiable is one cause -- the house style cites bare basenames -- so today's real coverage is the ~7% carrying a path. That is an honest ceiling, not a tuning knob, and the backlog is a separate deliberate pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCfrD5y19cvFc4AoyydXYv
|
Warning Review limit reachedNext included review available in 59 minutes. View limit detailsLimit details: You’ve used the included review currently available. Your 87 included PR review attempts over the past 7 days set your current allowance at 1 review per hour. Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Essentials Run ID: 📒 Files selected for processing (2)
Comment |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_49dbccc0-1186-42db-b5be-3cdd5993b8c8) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4496e6c143
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # board-hygiene rule means nearly every PR touches it. `--added-lines-only` | ||
| # restricts the verdict to lines this PR actually added or modified, | ||
| # which is what makes it a NO-NEW-DECAY gate. The backlog is a |
There was a problem hiding this comment.
Check citations affected by target-file shifts
When a PR prepends content to EPIPHANIES.md—the exact motivating scenario—existing references in other files decay even though their citing lines remain unchanged. Supplying only changed files and then restricting findings to added or modified citation lines means none of those references are checked, so the job succeeds despite newly introducing decay. Compare citation verdicts between the merge base and HEAD, or otherwise include unchanged citations whose resolved targets shifted, while excluding the pre-existing backlog.
Useful? React with 👍 / 👎.
| FILES=$(git diff --name-only --diff-filter=d "$BASE"...HEAD \ | ||
| -- '.claude/plans/*.md' '.claude/board/*.md' || true) |
There was a problem hiding this comment.
Propagate failures from the file-list diff
When this initial git diff fails, such as when the merge base is unavailable, || true converts the error into an empty FILES value and the following branch exits successfully. This bypasses the script's explicit fail-closed handling in added_lines, allowing the gate to pass without checking anything; preserve the command's failure or handle its status separately from a legitimately empty diff.
Useful? React with 👍 / 👎.
…d the gate (#1169) Both PRs added a deliverable and merged with no board entry in-commit, which is the retroactive-hygiene anti-pattern the Mandatory Board-Hygiene Rule names by that name. Recording it rather than quietly backfilling, because the subject of both PRs is making exactly this class of lapse mechanical rather than remembered. Carries the measured numbers that justify each gate's scope: the append-only gate's four verification levels including a real fire test, and the citation backlog (2314 / 37 OK / 124 DECAYED / 2153 UNVERIFIABLE) that forced line-scoping over file-scoping. Claude-Session: https://claude.ai/code/session_01DCfrD5y19cvFc4AoyydXYv Co-authored-by: Claude <noreply@anthropic.com>
Records the three review findings on the two board gates, and the uncomfortable half: #1167, #1168 and #1170 each shipped without their board hygiene in-commit -- three times running, by the session whose entire subject was making board discipline mechanical rather than remembered. #1169 recorded the first two; this records the third and its own lateness. The two defects are the same shape as the failure each gate exists to prevent: a substitution that preserves the count (IDEAS.md swapped out of an eight-tuple that stayed eight long), and a scope that excludes the case it was built for (added-lines filtering, blind to an EPIPHANIES.md prepend by construction). Three sub-findings kept: a count is not a set; a self-test can assert nothing while printing a number; a green CI tick is not evidence the gate ran -- the job log was pulled instead, precisely because the finding was a gate reporting success on the case it exists to catch. The argument for mechanical checks does not rest on the author being careless. Over one session the same author, holding the rule in mind, writing the tooling for the rule, broke it three times and caught it zero times. Both gates run green on this commit; the supersession index regenerates byte-identical (the entry cites no D-ids). Claude-Session: https://claude.ai/code/session_01DCfrD5y19cvFc4AoyydXYv Co-authored-by: Claude <noreply@anthropic.com>
…he lint name Merging origin/main brought in the new `citation-decay` gate (#1168/#1170), which fired on exactly one new citation — mine: TECH_DEBT.md -> crates/sigker/src/signature.rs:133 anchor=symbol:"needs_range_loop" (anchor absent within +-3 lines) The gate is right and the finding is a real defect, not a false positive. `needs_range_loop` is clippy's LINT NAME; it appears nowhere in the cited source. The gate extracts the backticked token nearest the citation as the anchor, so the lint name won that race and then could not be found at the line it supposedly addressed. A citation whose anchor is absent from its target is unverifiable by construction — which is the whole point of the gate. Re-anchored on `for flat in 0..len`, which is the actual text at signature.rs:133, and the lint name is now named in prose instead. The entry also records why, so the next reader does not "fix" it back. Local gate runs after the merge, all green: - citation_decay.py --self-test: passed (both halves) - citation_decay.py --since <base>: 0 new decays (148 pre-existing backlog, unchanged and not failing) - append_only_gate.py --self-test: passed, 7 cases - append_only_gate.py origin/main: no protected file shrank, 9 checked (TECH_DEBT 4161 -> 4212, +51) - supersession_index.py: regenerated after the merge, byte-identical - plan_dids.py: no added plans, nothing to check - cargo test --manifest-path crates/sigker/Cargo.toml: 62 passed, 0 failed - cargo fmt --check: clean Also merges origin/main (f30e300..ac9148f) to clear the merge conflict that made this PR un-mergeable. The merge itself was clean; EPIPHANIES.md auto-merged, both sides having prepended. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WkNBjHc2e3zuyz9i8qJEv
Two files, no code touched. Second of two CI gates. Pairs with #1167.
The defect it catches. A citation to
<file>:NNNinto an append-only prepended file decays on every new entry, with no edit on either side. Two measured instances today: a plan citedEPIPHANIES.md:438for a claim, and by ratification day:438had become the heading of the ruling that retracted it — a citation to a removal reads identically to a citation to the thing removed. The same plan cited:13401where the target was:13615.How it decides. Recovers an anchor near each citation (board id › backticked symbol › quoted phrase; nearest wins) and asks whether it is still within ±3 lines of the cited line. Exits non-zero only on confirmed decay. No recoverable anchor, an unresolvable path, or a bare basename is
UNVERIFIABLE— reported, never failed, and never guessed, because a guessed path manufactures verdicts.Three orchestrator corrections, each from a test that failed rather than from review
1. Scoped to changed LINES, not changed files — and file scoping was measured insufficient.
EPIPHANIES.mdalone carries 10 pre-existing decays, and the board-hygiene rule means nearly every PR touches it. A file-scoped gate would have failed almost every PR for decay it did not introduce. That is the fires-on-everything defect, and shipping it would have contradicted this session's own advice against the untracked-plan check.2. The citation regex could not match ANY dot-prefixed path. Its lookbehind rejected a preceding dot, so every
.claude/...citation — most of the paths this workspace cites — was invisible to the scanner. Found because a probe citation was silently ignored, not by reading the regex.3. Fails closed when the diff cannot be computed, rather than falling back to checking every line.
Verified on real committed diffs
The third case is what makes this a no-new-decay gate rather than a backlog alarm; the second is what proves it isn't simply rejecting all new citations.
The honest ceiling
Repo-wide today: 124 confirmed decays, 2153 unverifiable of 2314 citations. The 93% unverifiable has one cause — the house style cites bare basenames (
counterfactual.rs:432,EPIPHANIES.md:355) — so real coverage is the ~7% that carry a path. That is a ceiling, not a tuning knob, and the 124-item backlog is a separate deliberate pass.The failure message names the real fix: replace the line number with a heading-text or D-id anchor, because correcting the number re-arms the same bomb on the next prepend.
🤖 Generated with Claude Code
https://claude.ai/code/session_01DCfrD5y19cvFc4AoyydXYv
Generated by Claude Code