Collapse docs/adr/ and docs/rfc/ into a single docs/rfc/ with proposed/, implemented/, and rejected/ subfolders. Every file is renamed to yyyy-mm-dd-topic-title.md, where the date is when the topic was first proposed (from git history). ADRs and RFCs that covered exactly the same topic are merged (property-based testing, session persistence); the umbrella RFC 005 stays split across its three implemented decisions, and RFC 006's deferred part-3 (API extractor reports) splits into its own proposed RFC. All cross-references become machine-checkable relative links instead of bare "ADR NNNN" / "RFC NNN" prose. Add a verify-md-links doc-sync gate (scripts/verify-md-links.ts) that checks every relative Markdown cross-link resolves, wired into doc-sync alongside verify-md-wrap. This makes the reorganization self-verifying: the same change that rewrote ~forty inter-doc links adds the check that proves none dangle. Document the cross-link convention in a new docs/AGENTS.md and record the gate as an implemented RFC. doc-sync, typecheck, lint, and the full test suite (667) all pass.
3.2 KiB
RFC: Markdown cross-link validity linting
Status: implemented (proposed 2026-06-18, accepted 2026-06-18)
Context
Docs in this repo link to each other by relative path — [topic](../implemented/2026-…-….md), [the cookbook](adding-a-tool.md), [architecture.md](../../architecture.md). Nothing verified those targets exist. A rename or a move silently breaks every inbound link, and the break is invisible until a reader clicks it. Doc-sync enforcement already mechanized two classes of doc drift (uncompilable code blocks, a stale event-taxonomy table) and verify-md-wrap a third (hard-wrapped prose) — but a dead cross-link is a fourth, equally mechanical class that was still verified by eyeball.
The motivating case is the RFC tree reorganization that introduced this gate: unifying docs/adr/ + docs/rfc/ into one docs/rfc/ with proposed//implemented//rejected/ subfolders renamed roughly forty inter-doc links by hand. A single fat-fingered path would have shipped a broken link with nothing to catch it.
Decision
A fourth doc-sync gate, verify-md-links (scripts/verify-md-links.ts), mirroring the verify-md-wrap style (tsx ESM, AST-based, verify-don't-generate):
- Parse each in-scope Markdown file with
mdast-util-from-markdown+ GFM and walk everylink,image, anddefinitionnode. - Check a target only when it is a relative path. Skip scheme-qualified URLs (
https:,mailto:, …), protocol-relative (//host), root-absolute (/path— no stable base in a checkout), and pure in-page anchors (#section). Strip any#fragment/?query, resolve the path against the linking file's directory, and assert it exists on disk. - Report and never rewrite; exit non-zero on the first broken link found.
Scope matches the other gates plus the AGENTS.md pair: README.md, docs/**/*.md, packages/*/README.md, AGENTS.md, packages/AGENTS.md, deduped by real path (the CLAUDE.md symlinks resolve onto the AGENTS.md files). It is wired into the doc-sync script that the lefthook pre-push hook and CI both run, so a broken link fails locally before a push — consistent with mechanical quality gates.
This gate checks existence, not anchor validity: a link to a real file with a #wrong-heading fragment still passes (the file resolves; the fragment is stripped). Anchor-level checking is a heavier, lower-value follow-up — file-level dead links are the failure that actually bit us.
Consequences
- Renames and moves that orphan a cross-link now fail the pre-push hook and CI instead of waiting for a reader to click a dead link. This made the RFC reorganization that introduced the gate self-verifying: the same PR that rewrote forty links also added the check that proves none dangle.
- One more fast tsx script in the
doc-syncchain; no new dependency (the mdast/GFM stack is already in devDependencies forverify-md-wrap). - Fragment/anchor validity remains unchecked — a known, deliberate scope cut.
- The convention this enforces — cross-reference docs by machine-checkable relative link, never by bare prose or a number — is documented in docs/AGENTS.md so authors know the gate exists and why.