Skip to content

Commit 5231501

Browse files
hyperpolymathclaude
andcommitted
feat(readme): enforce anchor-ID convention + document .adoc bootstrap (ADR-004)
Discovered while verifying the Phase C pilot on boj-server's real README: a machine md->adoc conversion of a README with an intra-doc TOC produces dead TOC links, because Asciidoctor's default section IDs are '_prefixed_underscored' while GitHub Markdown anchors are 'lowercase-hyphenated'. The vocabulary gate cannot catch this (it checks words, not link targets). - readme-derive-reusable.yml: new 'Anchor-ID convention guard' step — if the canonical .adoc has link:#… anchors but does not set ':idprefix:' (empty) and ':idseparator: -', fail-and-tell before deriving. - ADR-004: new 'Bootstrapping a canonical .adoc' section — the mandatory idprefix/idseparator convention + the one-time md->adoc bootstrap recipe, both verified end-to-end on boj-server (11/11 TOC anchors resolve, 12/12 badges intact, >=98% vocab with only AsciiDoc scaffolding tokens 'lost'). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent db12a6a commit 5231501

2 files changed

Lines changed: 75 additions & 0 deletions

File tree

.github/workflows/readme-derive-reusable.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,31 @@ jobs:
126126
https://github.com/hyperpolymath/standards.git "$RUNNER_TEMP/standards"
127127
chmod +x "$RUNNER_TEMP/standards/.github/scripts/readme-vocab-gate.sh"
128128
129+
- name: Anchor-ID convention guard
130+
if: steps.decl.outputs.derive == 'true'
131+
env:
132+
CANONICAL: ${{ steps.decl.outputs.canonical }}
133+
run: |
134+
set -euo pipefail
135+
# A canonical .adoc with intra-document anchor links (link:#foo[] or
136+
# <<foo>> written as #foo) only renders a working TOC on GitHub and
137+
# re-derives to matching Markdown anchors when it opts out of
138+
# Asciidoctor's default '_prefixed_underscored' section IDs. So if the
139+
# source uses #anchors, it MUST set :idprefix: (empty) and
140+
# :idseparator: - — otherwise every TOC link silently dead-links
141+
# (a failure the vocabulary gate cannot see, since it checks words not
142+
# link targets). See ADR-004 "Bootstrapping a canonical .adoc".
143+
if grep -qE 'link:#[a-z0-9-]+\[' "$CANONICAL"; then
144+
if ! grep -qE '^:idprefix:[[:space:]]*$' "$CANONICAL" \
145+
|| ! grep -qE '^:idseparator:[[:space:]]+-[[:space:]]*$' "$CANONICAL"; then
146+
echo "::error::$CANONICAL has intra-doc anchor links (link:#…) but does not set ':idprefix:' (empty) and ':idseparator: -' in its header. Without them Asciidoctor generates '_underscored' section IDs and every #anchor TOC link dead-links on GitHub and in the derived README.md. Add both attributes to the document header (see ADR-004)."
147+
exit 1
148+
fi
149+
echo "✓ anchor-ID convention satisfied (idprefix/idseparator set)"
150+
else
151+
echo "✓ no intra-doc anchors — convention not required"
152+
fi
153+
129154
- name: Derive Markdown from AsciiDoc
130155
if: steps.decl.outputs.derive == 'true'
131156
env:

docs/decisions/ADR-004-readme-single-source-derive.adoc

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,56 @@ Rules:
9898
* Any estate sweep touching README format MUST read this block first
9999
and skip undeclared repos. This is the anti-runaway guard.
100100

101+
=== Bootstrapping a canonical `.adoc` from an existing `README.md`
102+
103+
The derive direction (`.adoc → .md`) is a clean one-shot. Producing the
104+
*initial* canonical `.adoc` from a rich existing `README.md` is a
105+
one-time authoring step, not part of the CI path, and has one sharp edge
106+
verified on boj-server's real README (264 lines, 12 badges, 15-entry TOC):
107+
108+
**Anchor-ID convention (mandatory when the README has an intra-doc TOC).**
109+
GitHub Markdown auto-anchors headings as lowercase-hyphenated
110+
(`## Capabilities overview` → `#capabilities-overview`). Asciidoctor
111+
instead defaults to `_prefixed_underscored` IDs
112+
(`_capabilities_overview`), so a converted `link:#capabilities-overview[]`
113+
TOC would dead-link on GitHub *and* re-derive to a broken Markdown anchor.
114+
The canonical `.adoc` MUST therefore set, in its document header:
115+
116+
[source,asciidoc]
117+
----
118+
= Project Title
119+
:idprefix:
120+
:idseparator: -
121+
----
122+
123+
With those two attributes, `== Features` → id `features`,
124+
`== Capabilities overview` → id `capabilities-overview` — matching GitHub
125+
Markdown exactly. `readme-derive-reusable.yml` enforces this: if the
126+
canonical source contains `link:#…[]` anchors without both attributes, the
127+
job fails-and-tells (the vocabulary gate cannot catch this — it checks
128+
words, not link targets).
129+
130+
**One-time bootstrap recipe** (verified end-to-end on boj-server —
131+
faithful round-trip: 11/11 TOC anchors resolve, 12/12 badges intact,
132+
≥98% vocab, only AsciiDoc scaffolding tokens "lost"):
133+
134+
[source,bash]
135+
----
136+
# 1. md -> asciidoc body
137+
pandoc -f gfm -t asciidoc --wrap=preserve README.md -o body.adoc
138+
# 2. promote every heading one level (== -> =, === -> ==, …) so the top
139+
# heading becomes the '= doctitle' and section nesting stays correct
140+
sed -E 's/^==(=*) /=\1 /' body.adoc > body2.adoc
141+
# 3. assemble canonical README.adoc: SPDX header + '= Title' +
142+
# :idprefix:/:idseparator: - + body2 (minus its first title line)
143+
# 4. sanity: render to HTML, confirm section ids match the TOC anchors
144+
----
145+
146+
pandoc wraps inline code containing `_ * { +` in AsciiDoc passthroughs
147+
(`++_*++`, `{plus}`) — correct and harmless (renders as literal text; the
148+
derived `.md` comes back clean), just visible noise in the source. Accept
149+
it, or tidy by hand; do not "fix" it in a way that changes rendered output.
150+
101151
=== Derivation pipeline (reusable workflow, `standards`)
102152

103153
One reusable workflow, `readme-derive.yml`, called by opted-in repos

0 commit comments

Comments
 (0)