Files
deepseek-harness/docs/rfc/implemented/process/2026-06-18-markdown-cross-link-lint.md
T
Tianyi Cui e6fad266a6 docs(rfc): define and enforce a uniform RFC format; adopt it across the corpus
Define the in-file RFC contract in docs/rfc/README.md § The file format:
the header block (`# RFC: <title>` plus a dateless Status enum
cross-checked against the lifecycle folder), the per-lifecycle body
skeleton (a Problem opener everywhere; Proposal/Alternatives considered/
Acceptance criteria/Risks in proposed/; present-tense Decision/
Consequences with proposal-era headings banned in implemented/; the
frozen proposal shape in rejected/), and a mandatory Alternatives
considered section with a date-fenced grandfather comment for pre-format
RFCs whose alternatives are not reconstructible from the record.

Enforce it with a new doc-sync gate, scripts/verify-rfc-format.ts, and
normalize all 112 RFCs to it: ~15 Status-line spellings collapse to the
enum, 29 Context openers become Problem, the 39 legacy-format XXX debt
markers are resolved and banned from reappearing, proposal-era sections
in implemented RFCs are rewritten to shipped reality (including the
web/fs/subagent seam RFCs' migration plans and test checklists, closing
the doc-tiers deferred-work item on the web seam), every RFC gains an
Alternatives considered section or the grandfather comment, and the
bilingual pair is re-mirrored and re-recorded.

Move the generated index tables out of README.md into a fully generated
docs/rfc/INDEX.md — gen-rfc-index now writes the whole file, and
verify-rfc-classification checks its freshness and rejects index-shaped
rows in the curated README — which makes room for the format contract to
live in the README front door instead of a separate FORMAT.md.

The decision record, and the first RFC written in the new format, is
docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.md.
2026-07-05 22:58:25 +08:00

3.4 KiB

RFC: Markdown cross-link validity linting

Status: implemented

Problem

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 every link, image, and definition node.
  • 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 and the repo-authored agent-skill Markdown under .agents/skills/ (those skill files cross-link into the docs tree, so this reorg rewrote links in them too): README.md, docs/**/*.md, packages/*/README.md, AGENTS.md, packages/AGENTS.md, .agents/skills/**/*.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).

Alternatives considered

Anchor-level validity checking — heavier and lower-value; file-level dead links are the failure that actually bit. The scope cut is deliberate: authors verify #fragment anchors themselves when linking to one.

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-sync chain; no new dependency (the mdast/GFM stack is already in devDependencies for verify-md-wrap).
  • 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.