Mix.StatifierBlocks.AdrCites (StatifierBlocks v0.24.0)

Copy Markdown View Source

Resolves the line-number citations decision records make into each other, and reports the ones that no longer point at the text they were written against.

A record in docs/adr/ cites another record 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 record, 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 checks

Three layers, two of which fail the gate:

  1. Resolvable (fails). The cited file exists under docs/adr/ and the cited range is non-empty and inside it.
  2. 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.
  3. 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. 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 records under 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

finding()

@type finding() :: %{
  file: String.t(),
  line: pos_integer(),
  severity: String.t(),
  check: String.t(),
  message: String.t()
}

report()

@type report() :: %{
  findings: [finding()],
  warnings: [finding()],
  entries: %{optional(String.t()) => map()}
}

Functions

analyze(root)

@spec analyze(String.t()) :: report()

Runs every layer over the records under root's docs/adr/.

Returns the failing findings, the advisory ones, and the baseline entries the current tree would record.

baseline_path(root)

@spec baseline_path(String.t()) :: String.t()

Path of the baseline file for a project root.

read_baseline(root)

@spec read_baseline(String.t()) :: %{optional(String.t()) => map()}

Reads the checked-in baseline, or an empty map when there is none yet.

render_baseline(entries)

@spec render_baseline(%{optional(String.t()) => map()}) :: String.t()

Serializes baseline entries as the JSON document the tree checks in.