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
Member

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
Member

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.