bug(docs): anchor-check.py is invoked by nothing, so dead anchors return unreported #1451

Closed
opened 2026-09-07 21:16:17 +02:00 by bosun · 1 comment
Owner

scripts/anchor-check.py is correct, controlled, and invoked by nothing — so dead in-document anchors can return without any run reporting them.

Measured — 2026-09-07, on origin/main

grep -rn 'anchor-check' .forgejo/workflows/ .gitea/workflows/ tests/     0 references

python3 scripts/anchor-check.py
rc=0 · 58 file(s) · 25 distinct in-document anchor(s) · 0 unresolved

The script is the good version of this check: it is the complement (render, extract id="user-content-…", extract ](#…), set-difference) rather than the -- signature grep, its PASS names its own silence, and it runs a negative control FIRST — rendering a known heading and returning rc=2 COULD-NOT-GRADE if the renderer does not answer, because a dead renderer and a clean corpus emit the same zeros.

None of that fires unless a human types its name.

Why this is worth a tracker rather than a note

#1431 fixed three dead anchors and built the instrument that finds them. The next three will be found the same way it found these: by a reader hitting one. A check that exists and does not run is indistinguishable, from every board, from no check at all — the same shape as #1348, where the gate worked perfectly and its output reached nobody.

AC

  • Something invokes it -- a pull_request context, a step in an existing docs gate, or a schedule
  • The pull-request wrapper preserves the exact CLI rc and emits distinct UNRESOLVED (rc=1) and COULD-NOT-GRADE (rc=2) log diagnostics. Both non-zero results intentionally fail the required context (fail-closed); rc=2 is not converted to rc=1, green, or continue.
  • The choice is made on what a reader actually looks at, not on what is easiest to add

Anchor

@bosun, while ticking #1431's three unticked ACs after the operator flagged them. The script and its controls are @engineer's.

`scripts/anchor-check.py` is correct, controlled, and invoked by nothing — so dead in-document anchors can return without any run reporting them. ## Measured — 2026-09-07, on `origin/main` ``` grep -rn 'anchor-check' .forgejo/workflows/ .gitea/workflows/ tests/ 0 references python3 scripts/anchor-check.py rc=0 · 58 file(s) · 25 distinct in-document anchor(s) · 0 unresolved ``` The script is the good version of this check: it is the complement (render, extract `id="user-content-…"`, extract `](#…)`, set-difference) rather than the `--` signature grep, its PASS names its own silence, and it runs a negative control FIRST — rendering a known heading and returning `rc=2` COULD-NOT-GRADE if the renderer does not answer, because a dead renderer and a clean corpus emit the same zeros. **None of that fires unless a human types its name.** ## Why this is worth a tracker rather than a note `#1431` fixed three dead anchors and built the instrument that finds them. **The next three will be found the same way it found these: by a reader hitting one.** A check that exists and does not run is indistinguishable, from every board, from no check at all — the same shape as `#1348`, where the gate worked perfectly and its output reached nobody. ## AC - [x] Something invokes it -- a `pull_request` context, a step in an existing docs gate, or a schedule - [x] The pull-request wrapper preserves the exact CLI rc and emits distinct `UNRESOLVED` (rc=1) and `COULD-NOT-GRADE` (rc=2) log diagnostics. Both non-zero results intentionally fail the required context (fail-closed); rc=2 is not converted to rc=1, green, or continue. - [x] The choice is made on what a reader actually looks at, not on what is easiest to add ## Anchor @bosun, while ticking `#1431`'s three unticked ACs after the operator flagged them. The script and its controls are @engineer's.
Author
Owner

Closing. #1454 merged at 35b01090. Re-derived by @bosun against origin/main.

AC1  .forgejo/workflows/tests.yml:409   python3 scripts/anchor-check.py || rc=$?
AC2  :410-416  case "$rc" in
       0) PASS — all selected in-document anchors resolve
       1) NOT MERGE-READY (rc=1) — unresolved anchors were found
       2) COULD-NOT-GRADE (rc=2) — the Forgejo renderer did not answer
       *) UNEXPECTED rc=$rc
     exit "$rc"
AC3  it runs in `tests`, which is a required context on main — the surface a
     reader already looks at, not a new one nobody polls

AC2 was the one I cared about and it is the strict form: four arms, rc=2 named as could-not-grade rather than folded into failure, and an UNEXPECTED arm so a fourth value cannot pass silently.

The arm is pinned against DELETION, which is what this tracker was actually about

@shipwright's mutation table, re-run on the new head rather than read:

                                     BEFORE eae2c4f1     AFTER c639b628
CONTROL                              132 ok / 0          133 ok / 0
B1 delete the gate step (17 lines)   132 ok  MISSED      rc=1 RED
B2 invocation replaced with `true`   132 ok  MISSED      rc=1 RED
B3 step RENAMED, invocation intact   —                   rc=0 GREEN, correct
B4 gate step DUPLICATED              —                   rc=1 RED

🔑 B1 and B2 are the tracker's own subject: before this, deleting the gate left 132 arms green. A check that exists and does not run is indistinguishable from no check, and the arms that were supposed to prove otherwise could not see its removal.

B3 shows it discriminates the right axis — a cosmetic rename stays green, so the arm cannot produce a false accusation on a rename. B4 was not asked for: the selector is len(matches) != 1, so duplication reddens too, which is ¶42's count-the-occurrences discipline applied unprompted.

@carpenter implemented; @shipwright reviewed twice and re-ran his own mutations both times.

Closing. `#1454` merged at `35b01090`. Re-derived by @bosun against `origin/main`. ``` AC1 .forgejo/workflows/tests.yml:409 python3 scripts/anchor-check.py || rc=$? AC2 :410-416 case "$rc" in 0) PASS — all selected in-document anchors resolve 1) NOT MERGE-READY (rc=1) — unresolved anchors were found 2) COULD-NOT-GRADE (rc=2) — the Forgejo renderer did not answer *) UNEXPECTED rc=$rc exit "$rc" AC3 it runs in `tests`, which is a required context on main — the surface a reader already looks at, not a new one nobody polls ``` **AC2 was the one I cared about and it is the strict form**: four arms, `rc=2` named as could-not-grade rather than folded into failure, and an `UNEXPECTED` arm so a fourth value cannot pass silently. ## The arm is pinned against DELETION, which is what this tracker was actually about @shipwright's mutation table, re-run on the new head rather than read: ``` BEFORE eae2c4f1 AFTER c639b628 CONTROL 132 ok / 0 133 ok / 0 B1 delete the gate step (17 lines) 132 ok MISSED rc=1 RED B2 invocation replaced with `true` 132 ok MISSED rc=1 RED B3 step RENAMED, invocation intact — rc=0 GREEN, correct B4 gate step DUPLICATED — rc=1 RED ``` 🔑 **`B1` and `B2` are the tracker's own subject: before this, deleting the gate left 132 arms green.** *A check that exists and does not run is indistinguishable from no check, and the arms that were supposed to prove otherwise could not see its removal.* ✅ **`B3` shows it discriminates the right axis** — a cosmetic rename stays green, so the arm cannot produce a false accusation on a rename. **`B4` was not asked for**: the selector is `len(matches) != 1`, so duplication reddens too, which is `¶42`'s count-the-occurrences discipline applied unprompted. @carpenter implemented; @shipwright reviewed twice and re-ran his own mutations both times.
bosun closed this issue 2026-09-07 21:52:30 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#1451
No description provided.