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 discovery

From 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:

rungsourcewhen
1an explicit url template in the configthe remote’s paths are computable
2a discovered filename, from remotes.lock.jsonalways, once refreshed
3the code-only convention, {dir}/{CODE}.mdthe 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-104 and 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-N as 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-099 stays a reported typo; SG-ADR-099 would 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-ok acknowledgements turned back into real references — an acknowledgement is a suppression, and the best outcome for one is that it stops being needed.
  • remotes.lock.json carries all 188 of the ancestor’s documents, not just the cited ones. It is a generated file: luria remotes --refresh rewrites 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-004 in prose would become a namespace nobody declared; the pattern is built from the registry precisely so that unregistered text stays text.