# `Mix.StatifierBlocks.AdrCites`
[🔗](https://github.com/riddler/statifier_blocks/blob/v0.44.1/lib/mix/statifier_blocks/adr_cites.ex#L1)

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:

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. 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.

# `finding`

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

# `report`

```elixir
@type report() :: %{
  findings: [finding()],
  warnings: [finding()],
  entries: %{optional(String.t()) =&gt; map()}
}
```

# `analyze`

```elixir
@spec analyze(
  String.t(),
  keyword()
) :: report()
```

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 to `root`, of the documents to scan for
    citations. Replaces the default `docs/adr/*.md`, `docs/plans/*.md` and
    `CLAUDE.md` rather than adding to them.

# `baseline_path`

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

Path of the baseline file for a project root.

# `read_baseline`

```elixir
@spec read_baseline(String.t()) :: %{optional(String.t()) =&gt; map()}
```

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

# `render_baseline`

```elixir
@spec render_baseline(%{optional(String.t()) =&gt; map()}) :: String.t()
```

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
