Context

The lint’s reference vocabulary — unresolved-codes, broken-targets, retired-citations — asks one question in three shapes: does this pointer resolve? remote-drift asks a fourth: have the bytes behind an endorsed pin changed since a human vouched for them?

None of them asks whether an identifier and the document carrying it were ever about the same thing. In anthology-of-the-sota, 53 of 139 arxiv: identifiers did not resolve to the paper the note named: quantum secret sharing filed as the PaLM 2 technical report, electron losses in hypersonic flows filed as checkpointing advice, chromatic number and Hamiltonicity filed as compiler optimization.

None of these was a typo. Every one was syntactically valid, in a plausible year range, and in the right format — 2111.09432 reads as a late-2021 systems paper unless you fetch it. That is why two years of review missed them, and why the failure is worth a check rather than a convention.

The gap also falsifies a claim that record makes about itself, in the principle this whole mechanism serves: that the remotes “turn those ids into resolvable references rather than strings, so ‘cite the paper’ and ‘the citation resolves’ are the same check rather than two”. They were two. Resolution was available and never performed.

Decision

A remote declares how to ask what one of its identifiers is:

[luria.remotes.ARXIV]
uris.title = "https://export.arxiv.org/api/query?id_list={1}.{2}"
title_re   = "<entry>.*?<title>(.*?)</title>"

uris.title is one more name in the URI table read and bytes already live in — the table’s docstring anticipated this: “a relation Luria does not ship yet is one more name”. title_re’s first capture group is the title.

luria remotes --resolve fetches every one and records what came back in remotes.lock.json. luria lint compares against the document’s own title:, and may ask about what the lockfile cannot answer:

[luria.lint]
network = "auto"      # default: ask about what is not already known
#         "never"     # answer only from the lockfile — the hermetic build
#         "require"   # not being able to ask is a finding

The lockfile is a cache with an endorsement in it, not the boundary of what may be known. Under auto the common case touches no network at all — every identifier is already answered — and the case that does is the one citation a contribution just added, which is precisely the one worth asking about. Disagreement is source-mismatch; an identifier nothing has verified is source-unchecked; source-ok: acknowledges a deliberate disagreement.

Failure kinds are distinguished by HTTP status, because they mean opposite things: 404/410 is upstream saying the identifier names nothing — an answer, recorded as such and not retried — while 429/503 is upstream declining to say, which is retried with backoff and, if it persists, reported as unchecked rather than written down as an absence.

Which fields hold identifiers needs no new configuration: a frontmatter key equal to a remote’s prefix, lowercased. A project that declared ARXIV has already said what arxiv: means.

Alternatives considered

Make the lockfile the boundary: never fetch during lint. This is what shipped first, on a reading of ADR-016’s “a check that reaches the network is a check that fails on a train” as a prohibition rather than as a requirement that the build stay possible offline. It has a hole big enough to defeat the check’s purpose: an identifier the lockfile had never seen was treated as exempt, so a freshly typed wrong citation passed silently until somebody remembered to run --resolve. That is the same dependence on ritual that let 53 bad identifiers survive two years, reintroduced by the guard meant to end it.

The synthesis is that determinism matters where a check fails, not where it asks. never keeps the hermetic build exactly. auto asks only about what is unknown, falls back to reporting it unchecked, and writes what it learns back — so the next run is offline and a reviewer can see in the diff what upstream said. require is the CI posture: a green build means the references were verified rather than remembered.

Guess the metadata API from the host. arxiv.org → the Atom API, doi.org → Crossref, and a project would configure nothing. Rejected for the reason pin_url is declared rather than derived: only the project can vouch that a URL serves what it appears to, and a guess that silently stops matching when a provider changes its response shape fails closed — the lockfile gains no entry, and no entry reads exactly like agreement. A check that quietly stops checking is worse than one that was never installed.

Fuzzy matching on similarity. Would forgive the 22 cosmetic cases in that record automatically — a dropped subtitle, a nickname the project prefers. Rejected because it converts a mechanical check into a judgement call, and the standing rule is that a check joins the lint only when the violation is always wrong and mechanically fixable. “The recorded title is not the resolved title” is mechanical; “the recorded title is 0.82 similar” is a threshold argument in every future review. The directive puts the judgement on a person, once, with a reason, where the disagreement is.

Leave unresolved identifiers silent. Rejected, having shipped it once. The argument was that a project which never ran --resolve would see every document become a finding, teaching people to run a command to silence the lint. It is backwards: running the command is the remedy, it is mechanical, and under auto the lint runs it for you on the one identifier that needs it. What the silence actually bought was a check that could not see the case it exists for.

Consequences

No project’s output changes until it declares uris.title on a remote: with no way to ask, the check has no opinion, and network governs nothing. Once declared, auto means luria lint may make a request for an identifier it has no answer for — bounded by how many citations a contribution adds, and zero on a record whose lockfile is current. A project that wants the old behaviour sets network = "never".

The lockfile grows a titles section, which write_lock preserves like the others, and which the lint now writes to as well as reads.

The check is deliberately shallow: it compares titles, not authors or years, because the title is the field a record already carries for its own reasons and therefore the one that cannot drift out of maintenance. A record that wants author and date checked can add them later against the same lockfile entries; the fetch already has them in hand.