ADR-014: A code that resolves to nothing is reported, not dropped
Status Active · Filed 2026-08-03
Context
ADR-005 made every reference a hyperlink, and made the linter and the fixer
share one scanner so the linter could never demand a rewrite the fixer wouldn’t
make. That invariant has a shadow: when the fixer can’t write a link, the lint
says nothing at all.
The commonest reason it can’t is that the code names no document here.
ADR-032 in this repo is strata-g’s thirty-second decision; there is no local
file, so resolve() returns None, the reference is dropped, and nothing is
printed. The same silence covers a typo — ADR-99 for ADR-9 — which is not a
foreign reference at all but a broken one.
Asked directly whether some bare codes in
ADR-009 were a lint failure, the honest answer was “no, by
design” — and the follow-up question was how many others the design was hiding.
Fifteen codes across the corpus. Four were stale strata-g numbering left in
ported docstrings (ADR-187, ADR-188, ADR-123, ADR-158), each citing a
decision that says the right thing in the wrong project. One of those was worse
than a bare code: [ADR-123](adr-123-adr-status-vocabulary-docs-lint.md), a
link to a file that has never existed here. The reference lint skips it —
it’s already a link — and nothing else was looking.
Decision
A cited code that names no document is recorded and reported. scan() keeps
it in Scan.dangling instead of dropping it; luria ref-status, luria lint
and the CI report each grew a section.
It is a warning, not an error (ADR-007). Three things look identical from inside the scanner:
| what it is | what to do |
|---|---|
| a typo | fix the number |
| another project’s decision | write it as a link out, not a code |
| an illustrative code in an example | acknowledge it |
Only a human can tell them apart, and a guard that is wrong most of the time gets suppressed — after which the times it is right go unread.
unresolved-ok:, and the inverted check
The third case is acknowledged the way the second kind of reference already was, in the same vocabulary at the same three scopes (ADR-008):
# unresolved-ok-file: ADR-019, ADR-163 — fixture codes, deliberately not real
One rule is inverted rather than shared, and it is the interesting part.
inactive-ok is malformed when it names a code that doesn’t resolve — it
would excuse nothing. unresolved-ok is malformed when it names one that
does — there is nothing to excuse. Same check, opposite sign, so an
acknowledgement that stops applying gets reported either way: the day someone
adds a real fifty-third decision, FX-ADR-053 here becomes a citation of it.
Codes inside URLs are not citations
https://github.com/dmarx/luria/blob/main/docs/decisions/ADR-013.md contains
the text ADR-013, and it is an address, not a claim. Masking URLs is not a
convenience — it is what makes the correct way to cite a foreign document
(a link out, per ADR-009) survive this check. Without it, the
luria init template failed its own scaffolded lint the moment its comments
pointed a new project at Luria’s own decisions.
Alternatives considered
- Make it a lint error. Naming another project’s decision is legitimate and common in an extracted package, and the first run on any adopting project would fail on examples in its own templates. This is the same argument ADR-007 already settled for retired documents.
- Infer a URL for unresolvable codes — link
ADR-032to the ancestor project automatically. Right for the eight real ones and silently wrong for every typo, which is the fail-stale polarity DP-3 rules out. A code means whatever the scheme resolving it says; a foreign one has no scheme here, so it gets a URL written by a human. - A separate scheme for the ancestor (
SG-032, with a URL template). The cleanest-looking option, and it doesn’t work: strata-g’s filenames carry title slugs, so no template can turn a number into a path. It becomes available if the ancestor ever adopts ADR-013. - Exclude tests from the scan to avoid fixture-code noise. Cheaper, and it blinds the check to exactly the files where a ported docstring is most likely to still cite the wrong project — one of the four stale numbers was in a test.
- Status quo. Free, and it had already hidden ten real defects for the whole life of the repo.
Consequences
- Ten stale references corrected, and every remaining illustrative code acknowledged. The corpus reports zero, so the next one shows up alone.
- The acknowledgement count grew from 6 to 17, and every one of them is a place someone deliberately wrote a code that names nothing. That number being visible is the point (DP-1): a suppression nobody counts is a suppression nobody notices.
- An adopting project will see this list on its first run, sometimes at length.
That is the same trade ADR-005 made — the first run is a
measurement, not a verdict — and
docs/adopting.mdsays so. Scannow carries two pools andannotations()takes a directive name, which is the shape a third acknowledgement would use. The parser was already generic; this is the first evidence that it is.