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 iswhat to do
a typofix the number
another project’s decisionwrite it as a link out, not a code
an illustrative code in an exampleacknowledge 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-032 to 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.md says so.
  • Scan now carries two pools and annotations() 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.