ADR-015: A foreign record gets a prefix; the config turns it into a URL
Status Superseded — by ADR-016; drops the local-clone discovery path and makes Luria its own worked example · Filed 2026-08-03
Context
An extracted package cites its ancestor constantly, and
ADR-014 left that citation in an unsatisfying place. A bare
ADR-032 in this repo is reported as resolving to nothing — correctly, because
it doesn’t — so the only way to write it was an absolute URL typed by hand, or
an unresolved-ok acknowledgement that suppressed the warning without making
the reference followable.
Both are bad in the same way. Eight provenance references in ADR-009 were 120-character GitHub URLs pasted into prose; nothing checked them, nothing could rewrite them if the ancestor moved a file, and the reader could not tell at a glance which decision each pointed at.
ADR-014 considered exactly the fix adopted here and rejected it:
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.
That reasoning was sound and the conclusion was wrong, because it assumed the only input was a template.
Decision
A foreign reference composes a registered remote prefix with the foreign
scheme’s own code: SG-ADR-032. One config entry makes it resolvable:
[luria.remotes.SG]
name = "strata-g"
repo = "dmarx/strata-g"
path = "../strata-g" # optional local clone, for discoveryFrom there it is a first-class reference. The scanner claims the composed
span before the local pattern can read ADR-032 out of the middle of it,
resolve() returns a URL instead of a relative path, luria link --fix writes
the link, and luria lint fails on a bare one — the same treatment a local
code gets, with no new vocabulary for an author to remember.
The lockfile is what makes it work
The URL is built on the strongest available rung:
| rung | source | when |
|---|---|---|
| 1 | an explicit url template in the config | the remote’s paths are computable |
| 2 | a discovered filename, from remotes.lock.json | always, once refreshed |
| 3 | the code-only convention, {dir}/{CODE}.md | the remote follows ADR-013 |
Rung 2 is the answer to ADR-014’s objection. A slug filename
cannot be computed, but it can be read: luria remotes --refresh builds a
code→filename map from a local clone or the GitHub contents API, and commits
it. Discovery reads the remote’s own luria.toml to learn where its documents
live, so dir comes from the authority rather than from a guess — which is the
“leverage the upstream config” case, and it works for a private remote too,
because a clone on disk is a config you can see.
Committed, not looked up live. CI, an offline checkout and a laptop on a
train have to resolve the same reference the same way. Discovery is the one
command in Luria that opens a socket, it is explicit, and its output is a file
in the repo. Nothing in luria lint touches the network.
Reachability is a separate command
luria remotes --check HEADs every cited foreign reference and reports what
doesn’t answer 200. It is not part of luria lint and never will be: a lint
that depends on someone else’s uptime fails for reasons that have nothing to do
with the change under test, and the first flaky failure teaches everyone to
ignore it. It also cannot distinguish “this document was deleted” from “this
repository is private and you are anonymous” — both are 404 — so it reports the
status rather than a verdict.
Alternatives considered
- A hand-written absolute URL — the status quo, and what ADR-014 recommended. Unrewritable, uncheckable, and unreadable in the source. Eight of them in one document is what prompted this.
- A flat second scheme (
SG-032, no inner prefix). Shorter, and it throws away which of the remote’s schemes a code belongs to: a remote with both decisions and principles has two number 4s, and the flat form can name only one of them. Composition costs four characters and keeps the remote’s own namespace intact. - Resolve live, on every lint. No lockfile to refresh, and the lint now depends on GitHub being up, on rate limits, and on network access that a private remote denies anyway. A guard that fails for unrelated reasons gets ignored, which is ADR-007’s argument in a different costume.
- Prune the lockfile to cited codes only. Smaller diff, and citing a new
foreign decision would then require a refresh — which requires the private
clone. The full map means anyone can cite
SG-ADR-104and get a correct link with no access at all. That is the whole point of committing it. - Infer the remote from an unresolvable code — treat any dangling
ADR-Nas the ancestor’s. Silently wrong on a typo, which is the failure DP-3 rules out and ADR-014 already rejected for the same reason.ADR-099stays a reported typo;SG-ADR-099would be a claim about strata-g.
Consequences
- Eight pasted URLs in ADR-009 became eight bare codes that the
fixer linked automatically, and several
unresolved-okacknowledgements turned back into real references — an acknowledgement is a suppression, and the best outcome for one is that it stops being needed. remotes.lock.jsoncarries all 188 of the ancestor’s documents, not just the cited ones. It is a generated file:luria remotes --refreshrewrites it wholesale, and it is committed for the same reason the decision index is.- A remote that adopts ADR-013 needs no lockfile at all — rung 3 is exact. The lockfile exists for records that predate the convention, which is most of them.
- The reference scanner now runs a config-derived pattern before its fixed
ones. An unconfigured prefix must not match, or
MY-ADR-004in prose would become a namespace nobody declared; the pattern is built from the registry precisely so that unregistered text stays text.