Resolves the line-number citations this repository's documents make into the decision records, and reports the ones that no longer point at the text they were written against.
A document cites a record in docs/adr/ by path and line - for example
docs/adr/0002-block-type-behaviour.md:4108 - often followed by more line
numbers for the same file (`:4121-4122`, `:4126`). Nothing about
a line number survives an edit above it: an insert or a re-wrap in the cited
record silently moves every line below it, and the citing document, on main
and untouched, now points at different text. That failure is invisible to
every other stage of the gate, which is why this one exists.
What it reads
Two roles, and they are not the same set of files. The targets are the
records under docs/adr/, and only a citation into one of them is checked -
a citation into lib/ or test/ is left to the compiler and the suite. The
sources are the documents scanned for citations, and they are wider than
the records, because a plan or CLAUDE.md cites a record by line exactly as
a record does and drifts in exactly the same way. The default globs are
docs/adr/*.md, docs/plans/*.md and CLAUDE.md; analyze/2 takes a
:sources option that replaces them, which is how a test scans a fixture
tree and how another document root would join the check.
What it checks
Three layers, two of which fail the gate:
- Resolvable (fails). The cited file exists under
docs/adr/and the cited range is non-empty and inside it. - Unmoved (fails). The cited range's text still hashes to what the recorded baseline says it did. This is the layer that catches the drift: a line inserted above a cited range changes the text at that range, and the digest changes with it.
- Anchored (warns). The citing paragraph quotes something - a phrase in quotation marks, or a multi-word backticked span - that is still found inside one of the ranges the paragraph cites. A paragraph that quotes nothing is reported as unanchored rather than failed: most of the corpus cites a section heading and paraphrases it, and treating a paraphrase as drift would falsify history rather than protect it.
The baseline
Layer 2 reads docs/adr/.cite-baseline.json, which records one entry per
citation: the normalized text at the cited range and its SHA-256. An entry is
keyed by the citing document's base name and the range it cites, not by
the citing document's path. Two sources sharing a base name therefore share a
key - harmlessly, because an entry's value is the text at the cited range and
says nothing about the source, so the colliding entries are identical. Keying
by path instead would rewrite every recorded key the moment the sources
widened, and every citation would fall back to the warning layer for a cycle:
the protection would lapse exactly when it was being extended. It is
regenerated with mix adr.cites --update, and the regenerated file belongs
in the same request as the record edit that moved the line.
A citation the baseline has never seen warns rather than fails. Records
land serially and a new citation arrives with the request that writes it;
failing on an unrecorded citation would red the gate of a request that had
done nothing wrong. Protection therefore accrues at the next --update
rather than at the moment the citation is written, which is the deliberate
trade this layer makes.
Nothing here rewrites a record. Merged records keep the line-number citations they were written with.
Summary
Functions
Runs every layer over the documents under root that the source globs name,
resolving what they cite into root's docs/adr/.
Path of the baseline file for a project root.
Reads the checked-in baseline, or an empty map when there is none yet.
Serializes baseline entries as the JSON document the tree checks in.
Types
Functions
Runs every layer over the documents under root that the source globs name,
resolving what they cite into root's docs/adr/.
Returns the failing findings, the advisory ones, and the baseline entries the current tree would record.
Options
:sources- globs, relative toroot, of the documents to scan for citations. Replaces the defaultdocs/adr/*.md,docs/plans/*.mdandCLAUDE.mdrather than adding to them.
Path of the baseline file for a project root.
Reads the checked-in baseline, or an empty map when there is none yet.
Serializes baseline entries as the JSON document the tree checks in.