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 new pushes 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_of reads 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: Proposed is 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.