ADR-049: Temporary codes at filing; concretized and aliased at the serialization point
Status Active · Filed 2026-08-12 · Issue #76 · Influenced by ADR-036 · ADR-040 · Influenced DP-013
Context
A sequential code is a claim on a global counter, and filing-time numbering
makes that claim from a branch — where nothing serializes it. Two branches
run luria new adr; both read 122 as the last number; both mint ADR-123.
At merge the collision is at least loud (an add/add conflict on the same
filename), but the loser’s cost is a rename plus a hand-fix of every
reference their branch sprinkled, and the failure is structural the moment
the workflow is N concurrent branches — which an agent-driven flow is.
The fragment collision fixed in ADR-036 v2 was this same bug at smaller stakes, and its fix points the way: identity at filing time must not require global coordination. The difference is that a fragment could simply switch identities (a timestamp is fine for a file nobody cites), while a scheme document’s number is its address — the record’s culture runs on “per ADR-013” — so the sequential number has to survive; only its assignment has to move to where merges serialize.
Filing-time numbers also quietly lie. The scaffold’s own template says numbering carries information — the order decisions were made — but under concurrency, branch A can file first and merge second, so the sequence records filing races, not the order decisions entered the record.
Decision
Allocation is a per-scheme dial. allocate = "filing" (the default,
today’s behaviour) numbers at creation. allocate = "merge" makes
luria new issue a temporary code instead: a tmp sentinel plus five
base-36 characters — ADR-tmp47fje. The alphabetic start keeps the
numeric and temporary patterns disjoint by construction, so no parser has
to guess; the spelled-out sentinel is for people and precision both — a
reader who has never met the convention still sees “provisional” at every
citation site, and the prose pattern stops false-matching six-letter
English (the first shape, any-alphanumeric-starting-with-a-letter, read
“the ADR-review process” as a temporary reference).
A temporary document is first-class on its branch. It is indexed, its
frontmatter is linted, [[ADR-tmp47fje]] and the bare code are found and
linked by the fixer, and cross-references resolve — the whole point is that
a branch can write against the document it just filed. The only thing a
temporary document cannot do is claim a place in the sequence.
luria concretize runs at the serialization point. Wherever merges are
serialized — a merge queue, the job that lands PRs on main — it assigns
each temporary document the next free number in merge order, renames the
file, rewrites every reference across the tree (the machinery is
ADR-040’s rename-with-references, wearing its first concrete
use), and appends the temporary code to the document’s formerly: frontmatter.
Aliases are permanent. The resolver honours formerly: forever: a temporary
code cited anywhere the rewriter cannot reach — a PR thread, an immutable
commit message, another repository’s LU--prefixed reference — resolves to
the concretized document. A code that ever named something never stops
naming it, which is ADR-014’s contract extended through the
rename.
The field is deliberately ADR-040’s formerly: — this
implements that proposal’s rung 1 (frontmatter identity, a derived alias
map, resolver support), and concretization itself is one specialized rung-2
operation: a mapping-driven rename whose mapping is computed (temp tail →
next free number, in commit order) rather than authored as a spec. One
identity field, so the day luria migrate arrives it parses the same
frontmatter this machinery already writes.
luria concretize --check guards the trunk. A temporary code on main
is always wrong and mechanically fixable — run the concretizer — so it
fails, which is exactly ADR-035’s bar for a check that may
fail a build rather than warn.
Alternatives considered
- Filing-time numbering (status quo). Fine for one careful human; the collision is loud but its cleanup is manual and lands on whoever merges second. Structurally broken for concurrent branches, and the sequence misrecords order under exactly the concurrency that breaks it.
- Timestamp codes forever, no concretization. The devlog’s answer, and genuinely simpler — no rename pass, no aliases. Rejected because it trades away the short citable number: “per ADR-1734003295” is not a culture anyone compiles.
- Reserve numbers against main at filing.
luria newpushes a claim to the trunk before creating the file. A coordination round-trip per document, useless offline, and racy unless something serializes the claims — at which point the serialization point exists and might as well do the whole job at merge. - A provisionality state in frontmatter (
id_state: provisional). The code’s state is already fully determined by its shape —temp_ofreads it off the filename — so a field would be a second copy of a derivable fact, the projection DP-3 says will drift: the concretizer renames the file and must also remember to flip the field, and the day it forgets, the two disagree. The document status field is not this either:Proposedis the decision’s state; the code’s state lives in its shape, where nothing has to keep it true. - Couple concretization to an intermediate branch. The branch answers a different question (batching documentation builds; see #76’s thread). Concretization needs a serialization point, not a branch: a merge queue or the merge-to-main job serializes fine. Deciding them together would couple a small mechanism to a large architecture change.
Consequences
Concurrent branches stop being able to collide on a number, and the sequence starts meaning what the template already claims: the order decisions entered the record.
The costs, plainly. Review reads temporary codes (ADR-tmp47fje) until
merge; labelled wikilinks ([[ADR-tmp47fje|the escalation ladder]]) blunt
this, and Proposed documents are cited less than landed ones. The
machinery grows a rewrite pass that must be trusted: it is
ADR-040’s migration machinery, so building one builds the
other, and it inherits the same obligation — fire it on a real tree before
trusting it, and read a sample of the diff. Resolution grows an alias table
scanned from frontmatter, which every resolver path must consult.
The default stays filing: a single-writer record keeps its current flow
untouched, and a project opts into merge allocation the day concurrency
becomes real. This repository’s own record decides separately whether to
switch — the mechanism landing and this record adopting it are two
decisions, and only the first is this one.
The history question, decided with ADR-040 rather than
against it. The first implementation left historical files unrewritten,
reasoning that a dated record stays byte-stable while the alias keeps it
resolving. Review ruled the other way, and the ruling names the principle:
“temporary” is relative to the historical record — wherever the tree can
be rewritten to promote a temporary code to its canonical ID, it should be.
So the sweep is full, journals and the collected changelog included, which
is ADR-040’s second commitment applied here: one spelling per
code in the tree, with immutability of what was actually written being
git’s guarantee rather than the working tree’s job. This is not the silent
revision ADR-019 forbids — the concretizer’s run is maximally
non-silent, one named commit at the serialization point. The alias’s job
narrows accordingly, to the citations that live where no rewrite can reach:
PR threads, commit messages, other repositories, and branches cut before
concretization — which merge clean, resolve through the alias, and are
modernized by their next luria link --fix.