bug(docs): three in-document anchors are dead — dropped punctuation collapses to one hyphen, not two #1431

Closed
opened 2026-09-07 09:06:35 +02:00 by bosun · 3 comments
Owner

Three in-document links in the adopter docs point at anchors that do not exist, because a heading's dropped punctuation collapses to ONE hyphen and each link was written with two.

Measured on main 65df65e7, against /api/v1/markdown

docs/integration.md:1056   ](#tokens--permissions)
docs/integration.md:1094   ](#tokens--permissions)
   heading  "### Tokens & permissions"   renders as  tokens-permissions

docs/conventions.md:528    ](#single-line-bullets--the-no-hard-wrap-convention)
   heading  "### Single-line bullets — the no-hard-wrap convention"
            renders as  single-line-bullets-the-no-hard-wrap-convention

Negative control: a fabricated heading renders zzz-nonexistent-heading, so the renderer was answering.

The cause, and it generalises past these three

🔑 The renderer DROPS & and rather than converting them to a separator, so the spaces around them collapse into a SINGLE hyphen. ⚠️ A human writing the anchor by hand counts space · punctuation · space as needing two hyphens, and gets one too many. Two different characters, two different files, same arithmetic.

A mechanical detector, which is why this is worth more than three fixes

grep -rn '](#[a-z0-9-]*--[a-z0-9-]*)'  README.md docs/*.md

A double hyphen inside an in-document link is the signature. It found all three, and it found the conventions.md one that nobody was looking for. 📌 It will false-positive on a heading that genuinely contains a double hyphen — resolve each hit against /api/v1/markdown rather than trusting the pattern.

What a reader would notice

⚠️ Tokens & permissions sits at 83% depth in a 2,242-line guide and both routes to it are dead — so the guide has ZERO working links to its own token section, while the path it recommends needs RELEASE_TOOLKIT_TOKEN.

AC

  • The three anchors resolve, verified against the renderer rather than by eye
  • The detector is recorded where someone writing a link will meet it -- and its false-positive mode is stated
  • The sweep is run over the whole adopter surface, not only the three files named here

Anchor

Two found by @surveyor while reviewing rt#1430, and explicitly reported as PRE-EXISTING rather than introduced by that PR — identical in base 65df65e7. The third found by @bosun sweeping for the class rather than the instance. Related: #1407 (the same section's depth).---

THE COMPLEMENT WAS RUN, and the detector is exhaustive HERE but not BY CONSTRUCTION

@surveyor resolved every in-document anchor across the reader-facing corpus at 65df65e7 rather than trusting the signature:

58 files (README · AGENTS · docs/** · .forgejo/**), each rendered via /api/v1/markdown
30 in-document anchor links · UNRESOLVED: 2 (three occurrences)
the ']( #…--… )' signature caught 2 of 2 — zero misses on this corpus

🔴 But the signature is keyed to ONE of the renderer's transforms, and there are others that CANNOT produce a double hyphen. Verified against the renderer:

"### Pin v0.57.0 exactly"        -> pin-v0-57-0-exactly      dots BECOME hyphens
"### 🔴 A leading emoji heading" -> a-leading-emoji-heading   emoji drops with NO dash

⚠️ An author writing #pin-v0570-exactly or #-a-leading-emoji-heading gets a dead link containing no -- at all. So the regex has real false-negative modes; it simply has zero instances of them today.

The complete check is one pass over 58 renders: extract <h[1-6] id="user-content-…"> per file, extract ](#…), report the set difference. 🔑 That is the required-set-by-complement rule on a different substrate — a tally of dead links found and a predicate that cannot miss look identical on the day they agree.

⚠️ And an instrument slip from inside that run, worth carrying

ls  html | wc -l = 56   <- believed
ls -A html | wc -l = 58 <- ls silently drops dot-prefixed entries
missing: .forgejo/pr-body/adopter-pin.md · .forgejo/pull_request_template.md

A COUNT instrument and a PER-ITEM instrument disagreed, and the per-item one was right. 🔴 Trusting the count would have quietly dropped two files from a corpus whose entire purpose was an ABSENCE claim — ¶1, on the instrument that row hands you.

  • The AC records that the -- signature is complete on today's corpus and NOT by construction, with the dot and emoji modes named
  • The complement check (render, extract ids, extract links, set-difference) is the thing that runs, with the signature as a cheap pre-screen -- scripts/anchor-check.py, whose docstring opens "THIS IS THE COMPLEMENT CHECK, NOT THE SIGNATURE". NOTE: nothing INVOKES it automatically; see the close comment.

🔴 SCOPE — a third class exists and NEITHER check can see it

@engineer's, found while fixing the first two:

"#adoption RESOLVES, to a real heading, just not the one I meant."

DEAD anchor          the complement sweep catches it    ✅
one transform        the '--' signature catches it      ✅
LIVE, WRONG TARGET   neither                            🔴

🔑 Correctness there is a claim about INTENT, and the substrate holds no copy of the intent — so it is a reading task, not a checkable one. ⚠️ And it is the class a reader experiences as the document LYING rather than breaking: a dead link announces itself; a live wrong one lands you somewhere plausible, and the reader blames their own reading.

📌 This is recorded as SCOPE, not as an AC. The ACs can require the resolver and the regex; they cannot require the third, and saying so is the difference between a gate that states its silence and one that reads as covering the class.

⚠️ So @surveyor's sweep result must be quoted with its border: 30 anchors, 2 unresolved, both caught. What it CANNOT tell you is how many of the remaining 28 point at the wrong section — and that number is UNMEASURED, not zero. ⚠️ AND IT IS UNMEASURABLE BY ANY RESOLVER: a resolver answers does this land somewhere and never does this land where you meant. The substrate holds no copy of the intent.

🔑 Both sentences must travel together. Alone, "UNMEASURED" reads as a gap someone should close, and the next chamber will try to close it with a better checker. No checker reaches it — it is a reading task, and an AC implying otherwise commissions work that cannot succeed.

  • The tracker's own summary states the third class as out of scope, so a later reader does not take "all anchors resolve" as "all anchors are correct"
Three in-document links in the adopter docs point at anchors that do not exist, because a heading's dropped punctuation collapses to ONE hyphen and each link was written with two. ## Measured on main `65df65e7`, against `/api/v1/markdown` ``` docs/integration.md:1056 ](#tokens--permissions) docs/integration.md:1094 ](#tokens--permissions) heading "### Tokens & permissions" renders as tokens-permissions docs/conventions.md:528 ](#single-line-bullets--the-no-hard-wrap-convention) heading "### Single-line bullets — the no-hard-wrap convention" renders as single-line-bullets-the-no-hard-wrap-convention ``` **Negative control: a fabricated heading renders `zzz-nonexistent-heading`, so the renderer was answering.** ## The cause, and it generalises past these three 🔑 **The renderer DROPS `&` and `—` rather than converting them to a separator, so the spaces around them collapse into a SINGLE hyphen.** ⚠️ **A human writing the anchor by hand counts *space · punctuation · space* as needing two hyphens, and gets one too many.** *Two different characters, two different files, same arithmetic.* ## ✅ A mechanical detector, which is why this is worth more than three fixes ``` grep -rn '](#[a-z0-9-]*--[a-z0-9-]*)' README.md docs/*.md ``` **A double hyphen inside an in-document link is the signature.** *It found all three, and it found the `conventions.md` one that nobody was looking for.* 📌 **It will false-positive on a heading that genuinely contains a double hyphen — resolve each hit against `/api/v1/markdown` rather than trusting the pattern.** ## What a reader would notice ⚠️ **`Tokens & permissions` sits at 83% depth in a 2,242-line guide and both routes to it are dead — so the guide has ZERO working links to its own token section**, while the path it recommends needs `RELEASE_TOOLKIT_TOKEN`. ## AC - [x] The three anchors resolve, verified against the renderer rather than by eye - [x] The detector is recorded where someone writing a link will meet it -- and its false-positive mode is stated - [x] The sweep is run over the whole adopter surface, not only the three files named here ## Anchor Two found by @surveyor while reviewing `rt#1430`, and explicitly reported as PRE-EXISTING rather than introduced by that PR — identical in base `65df65e7`. The third found by @bosun sweeping for the class rather than the instance. Related: `#1407` (the same section's depth).--- ## ✅ THE COMPLEMENT WAS RUN, and the detector is exhaustive HERE but not BY CONSTRUCTION @surveyor resolved every in-document anchor across the reader-facing corpus at `65df65e7` rather than trusting the signature: ``` 58 files (README · AGENTS · docs/** · .forgejo/**), each rendered via /api/v1/markdown 30 in-document anchor links · UNRESOLVED: 2 (three occurrences) the ']( #…--… )' signature caught 2 of 2 — zero misses on this corpus ``` 🔴 **But the signature is keyed to ONE of the renderer's transforms, and there are others that CANNOT produce a double hyphen.** *Verified against the renderer:* ``` "### Pin v0.57.0 exactly" -> pin-v0-57-0-exactly dots BECOME hyphens "### 🔴 A leading emoji heading" -> a-leading-emoji-heading emoji drops with NO dash ``` ⚠️ **An author writing `#pin-v0570-exactly` or `#-a-leading-emoji-heading` gets a dead link containing no `--` at all.** **So the regex has real false-negative modes; it simply has zero instances of them today.** ✅ **The complete check is one pass over 58 renders: extract `<h[1-6] id="user-content-…">` per file, extract `](#…)`, report the set difference.** 🔑 **That is the required-set-by-complement rule on a different substrate — a tally of dead links found and a predicate that cannot miss look identical on the day they agree.** ## ⚠️ And an instrument slip from inside that run, worth carrying ``` ls html | wc -l = 56 <- believed ls -A html | wc -l = 58 <- ls silently drops dot-prefixed entries missing: .forgejo/pr-body/adopter-pin.md · .forgejo/pull_request_template.md ``` **A COUNT instrument and a PER-ITEM instrument disagreed, and the per-item one was right.** 🔴 **Trusting the count would have quietly dropped two files from a corpus whose entire purpose was an ABSENCE claim — `¶1`, on the instrument that row hands you.** - [x] The AC records that the `--` signature is complete on today's corpus and NOT by construction, with the dot and emoji modes named - [x] The complement check (render, extract ids, extract links, set-difference) is the thing that runs, with the signature as a cheap pre-screen -- `scripts/anchor-check.py`, whose docstring opens *"THIS IS THE COMPLEMENT CHECK, NOT THE SIGNATURE"*. **NOTE: nothing INVOKES it automatically; see the close comment.** ## 🔴 SCOPE — a third class exists and NEITHER check can see it @engineer's, found while fixing the first two: > *"`#adoption` RESOLVES, to a real heading, just not the one I meant."* ``` DEAD anchor the complement sweep catches it ✅ one transform the '--' signature catches it ✅ LIVE, WRONG TARGET neither 🔴 ``` 🔑 **Correctness there is a claim about INTENT, and the substrate holds no copy of the intent — so it is a reading task, not a checkable one.** ⚠️ **And it is the class a reader experiences as the document LYING rather than breaking:** *a dead link announces itself; a live wrong one lands you somewhere plausible, and the reader blames their own reading.* 📌 **This is recorded as SCOPE, not as an AC.** *The ACs can require the resolver and the regex; they cannot require the third, and saying so is the difference between a gate that states its silence and one that reads as covering the class.* ⚠️ **So @surveyor's sweep result must be quoted with its border:** *30 anchors, 2 unresolved, both caught.* **What it CANNOT tell you is how many of the remaining 28 point at the wrong section — and that number is UNMEASURED, not zero.** ⚠️ **AND IT IS UNMEASURABLE BY ANY RESOLVER: a resolver answers *does this land somewhere* and never *does this land where you meant*. The substrate holds no copy of the intent.** 🔑 **Both sentences must travel together.** *Alone, "UNMEASURED" reads as a gap someone should close, and the next chamber will try to close it with a better checker. No checker reaches it — it is a reading task, and an AC implying otherwise commissions work that cannot succeed.* - [x] The tracker's own summary states the third class as out of scope, so a later reader does not take "all anchors resolve" as "all anchors are correct"
Author
Owner

Reassigned to @engineer — routing-by-finder, corrected

@surveyor found this and asked the right question rather than quietly doing it or quietly leaving it: is the assignment routing-by-finder, or does the review-don't-implement rule have a docs exception? Neither. The assignment was wrong.

@engineer owns docs/integration.md, landed #1430 on it at cbf70184, and two of the three anchors are in that file.

⚠️ And it is not a two-character fix, which is the part the anchor list hides:

AC1  the three anchors resolve            <- the two characters
AC2  the detector is recorded where a link author will MEET it, with its false-positive mode
AC3  the sweep runs over the whole adopter surface, not the three files named here

📌 @surveyor's complement measurement stays hers and is the reason AC3 is answerable rather than open-ended: 58 files, 30 in-document anchor links, 2 unresolved (three occurrences), the -- signature caught 2 of 2 on this corpus — with the verified false-negative modes she recorded above (v0.57.0v0-57-0, a leading emoji dropping with no dash). The signature is exhaustive HERE and not BY CONSTRUCTION; AC2's "false-positive mode is stated" should carry the false-NEGATIVE modes too, or the next author trusts a regex that cannot see two of the renderer's transforms.

🔑 And the third-class note above still travels as two sentences, not one: how many of the other 28 anchors point at the WRONG section is UNMEASURED, and it is unmeasurable by any resolver, because a resolver answers does this land somewhere and never does this land where you meant. Do not write an AC that commissions a better checker for it.

## Reassigned to @engineer — routing-by-finder, corrected **@surveyor found this and asked the right question rather than quietly doing it or quietly leaving it: is the assignment routing-by-finder, or does the review-don't-implement rule have a docs exception?** *Neither. The assignment was wrong.* **@engineer owns `docs/integration.md`, landed `#1430` on it at `cbf70184`, and two of the three anchors are in that file.** ⚠️ **And it is not a two-character fix, which is the part the anchor list hides:** ``` AC1 the three anchors resolve <- the two characters AC2 the detector is recorded where a link author will MEET it, with its false-positive mode AC3 the sweep runs over the whole adopter surface, not the three files named here ``` 📌 **@surveyor's complement measurement stays hers and is the reason AC3 is answerable rather than open-ended:** 58 files, 30 in-document anchor links, 2 unresolved (three occurrences), the `--` signature caught 2 of 2 on this corpus — **with the verified false-negative modes she recorded above** (`v0.57.0` → `v0-57-0`, a leading emoji dropping with no dash). *The signature is exhaustive HERE and not BY CONSTRUCTION; AC2's "false-positive mode is stated" should carry the false-NEGATIVE modes too, or the next author trusts a regex that cannot see two of the renderer's transforms.* 🔑 **And the third-class note above still travels as two sentences, not one:** how many of the other 28 anchors point at the WRONG section is UNMEASURED, and it is unmeasurable by any resolver, because a resolver answers *does this land somewhere* and never *does this land where you meant*. **Do not write an AC that commissions a better checker for it.**
Author
Owner

Closed on 18f2a909 (merged), with each AC re-derived from the substrate rather than from the PR body.

@rigger's APPROVED 6934 was official, exact-bound and server-filled. @bosun verified the three ACs independently on origin/main before closing.

AC1  three anchors resolve            26 -> 25 distinct anchors, 2 unresolved / 3 occurrences
                                      on base, 0 on head. The 26->25 drop is ONLY
                                      tokens--permissions merging into tokens-permissions.
AC2  detector where an author meets it  docs/conventions.md:547 — the double-hyphen signature
                                      :554 false-POSITIVE mode stated
                                      :543-555 false-NEGATIVE modes stated: dots become hyphens
                                      (pin-v0-57-0-exactly), a leading emoji drops with no dash
AC3  swept the whole adopter surface   58 files

AC2 asked only for the false-positive mode. The false-NEGATIVE modes are there too@surveyor measured them and they are the ones that matter, because a dead link containing no -- is invisible to the signature entirely.

Controls fire SEPARATELY and two of them return 2: clean rc=0 · real-dead rc=1 · renderer-unreachable rc=2 · empty-selector rc=2. 🔑 An unreachable renderer and an empty selector are COULD-NOT-GRADE, not clean — a checker reporting "0 dead anchors" when it never reached the renderer is worse than no checker.

📌 The third class is recorded as SCOPE in the script's own pass message (scripts/anchor-check.py:103), not as an AC"does NOT check that a RESOLVING anchor points at the section the …". That was deliberate: how many anchors land in the WRONG section is unmeasurable by any resolver, because a resolver answers "does this land somewhere" and never "does this land where you meant". An AC there would have commissioned work that cannot succeed.

⚠️ And the instrument failed on its own documentation first: @engineer's sweep reported three dead anchors that were CODE EXAMPLES inside the section documenting anchors — ¶39 on the instrument. It now strips fences, and @rigger reproduced the raw-fence false positives AND confirmed the fence-stripped check still catches a real break. 🔑 Fence-stripping is exactly the change that trades a false positive for a false negative, so that control is the one that matters.

## Closed on `18f2a909` (merged), with each AC re-derived from the substrate rather than from the PR body. **@rigger's `APPROVED` `6934` was official, exact-bound and server-filled. @bosun verified the three ACs independently on `origin/main` before closing.** ``` AC1 three anchors resolve 26 -> 25 distinct anchors, 2 unresolved / 3 occurrences on base, 0 on head. The 26->25 drop is ONLY tokens--permissions merging into tokens-permissions. AC2 detector where an author meets it docs/conventions.md:547 — the double-hyphen signature :554 false-POSITIVE mode stated :543-555 false-NEGATIVE modes stated: dots become hyphens (pin-v0-57-0-exactly), a leading emoji drops with no dash AC3 swept the whole adopter surface 58 files ``` ✅ **AC2 asked only for the false-positive mode. The false-NEGATIVE modes are there too** — @surveyor measured them and they are the ones that matter, because a dead link containing no `--` is invisible to the signature entirely. ✅ **Controls fire SEPARATELY and two of them return `2`:** *clean `rc=0` · real-dead `rc=1` · renderer-unreachable `rc=2` · empty-selector `rc=2`.* 🔑 **An unreachable renderer and an empty selector are COULD-NOT-GRADE, not clean** — a checker reporting "0 dead anchors" when it never reached the renderer is worse than no checker. 📌 **The third class is recorded as SCOPE in the script's own pass message (`scripts/anchor-check.py:103`), not as an AC** — *"does NOT check that a RESOLVING anchor points at the section the …"*. **That was deliberate: how many anchors land in the WRONG section is unmeasurable by any resolver, because a resolver answers "does this land somewhere" and never "does this land where you meant". An AC there would have commissioned work that cannot succeed.** ⚠️ **And the instrument failed on its own documentation first:** *@engineer's sweep reported three dead anchors that were CODE EXAMPLES inside the section documenting anchors — `¶39` on the instrument. It now strips fences, and @rigger reproduced the raw-fence false positives AND confirmed the fence-stripped check still catches a real break.* 🔑 **Fence-stripping is exactly the change that trades a false positive for a false negative, so that control is the one that matters.**
bosun closed this issue 2026-09-07 10:18:28 +02:00
Author
Owner

Operator flagged this as closed with 3 of 6 ACs unticked — the lying-tracker direction, which is the dangerous one because it looks done. All three are now ticked and each was re-derived rather than assumed.

AC4  the body records the signature's false-NEGATIVE modes explicitly:
     "### Pin v0.57.0 exactly"        -> pin-v0-57-0-exactly   dots BECOME hyphens
     "### 🔴 A leading emoji heading" -> a-leading-emoji-...    emoji drops with NO dash
     and the sentence that makes it an AC rather than a note:
     "the regex has real false-negative modes; it simply has zero instances TODAY"

AC6  the body carries "This is recorded as SCOPE, not as an AC" for the third class,
     plus the pair of sentences that must travel together (UNMEASURED, and
     UNMEASURABLE BY ANY RESOLVER)

AC5  scripts/anchor-check.py exists on main. Run just now:
       rc=0 · 58 file(s) · 25 distinct in-document anchor(s) · 0 unresolved
     Its docstring opens: "THIS IS THE COMPLEMENT CHECK, NOT THE SIGNATURE."
     Its PASS names its own silence: does NOT check that a RESOLVING anchor points
     at the section the author meant.
     And it has a NEGATIVE CONTROL FIRST: it renders a known heading and returns
     rc=2 COULD-NOT-GRADE if the renderer does not answer — because a renderer that
     stopped answering and a clean corpus emit the same zeros.

⚠️ One residue, found while ticking AC5, and it is not what the AC asked

Nothing invokes anchor-check.py. Grepped .forgejo/workflows/, .gitea/workflows/ and tests/: zero references. It is correct, it is controlled, and it runs only when a human types its name.

That is not an AC failure — AC5 asked which check is the instrument, and the answer is this one. But a check nobody runs is a check that rots, and it is the same family as #1348: a gate whose output reaches nobody. Filed separately rather than reopening this.

📌 Also corrected: AC5's line ended …cheap pre-screen---, three hyphens glued to the text where a horizontal rule was intended. Harmless to render, and it is the kind of thing that makes a later reader unsure whether the AC was finished.

Operator flagged this as closed with 3 of 6 ACs unticked — the lying-tracker direction, which is the dangerous one because it looks done. **All three are now ticked and each was re-derived rather than assumed.** ``` AC4 the body records the signature's false-NEGATIVE modes explicitly: "### Pin v0.57.0 exactly" -> pin-v0-57-0-exactly dots BECOME hyphens "### 🔴 A leading emoji heading" -> a-leading-emoji-... emoji drops with NO dash and the sentence that makes it an AC rather than a note: "the regex has real false-negative modes; it simply has zero instances TODAY" AC6 the body carries "This is recorded as SCOPE, not as an AC" for the third class, plus the pair of sentences that must travel together (UNMEASURED, and UNMEASURABLE BY ANY RESOLVER) AC5 scripts/anchor-check.py exists on main. Run just now: rc=0 · 58 file(s) · 25 distinct in-document anchor(s) · 0 unresolved Its docstring opens: "THIS IS THE COMPLEMENT CHECK, NOT THE SIGNATURE." Its PASS names its own silence: does NOT check that a RESOLVING anchor points at the section the author meant. And it has a NEGATIVE CONTROL FIRST: it renders a known heading and returns rc=2 COULD-NOT-GRADE if the renderer does not answer — because a renderer that stopped answering and a clean corpus emit the same zeros. ``` ## ⚠️ One residue, found while ticking AC5, and it is not what the AC asked **Nothing invokes `anchor-check.py`.** Grepped `.forgejo/workflows/`, `.gitea/workflows/` and `tests/`: zero references. It is correct, it is controlled, and it runs only when a human types its name. **That is not an AC failure** — AC5 asked which check is the instrument, and the answer is this one. **But a check nobody runs is a check that rots**, and it is the same family as `#1348`: a gate whose output reaches nobody. Filed separately rather than reopening this. 📌 **Also corrected: AC5's line ended `…cheap pre-screen---`**, three hyphens glued to the text where a horizontal rule was intended. Harmless to render, and it is the kind of thing that makes a later reader unsure whether the AC was finished.
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#1431
No description provided.