ADR-040: Migrations: renaming schemes and moving documents without losing the record’s memory
Status Active · Filed 2026-08-07 · Issue #29 · #54 · Influenced ADR-049 · DP-013
Context
Issue #29 asked a small question — should “design principles” become “guiding principles”? — that turned out to be an instance of a general one. A record’s schemes are named before the project knows what it is. This project will rename DP to GP; a future project may split “norms” out from between decisions and principles, or grow “values” above them, and want to move existing documents to the rung where they now belong. Today every such change is a hand-executed sweep: edit the config, rename the files, chase every reference, and hope grep caught the anchors.
Two forces make the naive approaches wrong:
- Codes appear in places a rename must not touch. Test fixtures carry DP
codes on purpose (
DP-018intests/test_remotes.py,DP-004in a docstring example inremotes.py, both excused withunresolved-ok), and composed remote codes likeSG-DP-18address another project’s scheme — strata-g’s namespace is theirs, whatever this record renames locally. Any sweep driven by “things shaped likeDP-\d+” rewrites all of these. - An unrewritten history is not preserved history — it is unwatched history. A spelling that matches no configured scheme is invisible to the reference scanner, so the linter silently stops checking every file that still uses it. Old spellings don’t age gracefully; they fall out of the record’s guarantees entirely.
Decision
The record gains a migrations mechanism, defined by five commitments:
1. Migrations rewrite addresses, never claims. A migration may touch
codes, filenames, anchors, and link targets — the machinery-authored layer.
It never rewrites prose. The test is ownership: if luria link --fix could
have written it, a migration may rewrite it (ADR-016 draws the
same line for the fixer).
2. Full rewrite, history included. After a migration, exactly one
spelling of each code exists in the tree — journals and the changelog too.
This looks like it collides with the revision rule
(ADR-019), and doesn’t: that rule objects to silent
revision, and a migration is maximally non-silent — a named spec, its own
commit, its own formerly: trail. Immutability of what was actually written
is git’s guarantee, not the working tree’s job. The tree’s job is to be
greppable and lintable, and a mixed-spelling tree is neither. Blame noise is
handled mechanically: the migration tooling appends its commit to
.git-blame-ignore-revs.
3. The sweep is mapping-driven, never prefix-driven. A migration
enumerates exact pairs (DP-001 → GP-001, …) and rewrites only those. The
fixtures, the docstring examples, and every SG-DP-* composed code survive
because they are not in the mapping — not because the sweep was clever about
context.
4. formerly: is identity; everything else is derived. A moved document
gains a frontmatter field — formerly: [DP-4], appending on later moves —
and that is the only persistent bookkeeping. The alias map (old code →
current document) is derived from these fields at runtime; the spec file in
record/migrations.d/ is the executable plan and audit trail. Config gets no
migrations ledger: config describes the present, documents carry their pasts.
References written in the old spelling still resolve through the alias map
but draw a legacy-spellings warning — a class on the
ADR-035 ladder, so a project can escalate it to failing — and
luria link --fix upgrades them. That same loop is what makes the migration
safe to land while branches are in flight: a pre-migration branch merges
clean, warns, and is modernized on its next --fix.
5. Capability arrives as a ladder, and a project boards at the cheapest rung that solves its problem:
- Rung 0 — prose relabel. No codes change; no tooling exists to run. (The companion decision to call ADRs “atomic” rather than “architecture” decision records is this rung: the prefix stays, the expansion changes.)
- Rung 1 — alias resolution.
formerly:parsing, the derived alias map, thelegacy-spellingsclass, the fixer upgrade. Useful standalone, and the safety net every higher rung stands on. - Rung 2 —
luria migrate. Executes a spec fromrecord/migrations.d/(scaffolded byluria new migration). Operations:rename_scheme(config edit,git mvof every document, the mapped sweep,formerly:stamps, renamed view outputs, the blame-ignore append);move_doc(full source code plus target prefix —luria migrate move DP-4 NRM— auto-numbered in the target scheme, then the same sweep for one mapping entry); andstrategy = "supersede", the low-rent mode that marks each source document Superseded pointing at a fresh copy — supported as a style, never the default.--dry-runprints the mapping and the files the sweep would touch. - Rung 3 — downstream replay (deferred). A project that adopted another record’s scheme replays the published spec. The spec format is designed so this stays possible; nothing is built for it now.
Cross-scheme moves meet template mismatch head-on: a target scheme may
demand frontmatter the source never had (values require approvers; norms do
not). Per-scheme requirements live in config — [luria.schemes.VAL] requires = ["approvers"] — and the existing frontmatter check enforces them.
A moved document missing a required field fails lint until a human supplies
it: the machinery moves the document, a human vouches for it.
The vocabulary the machinery is defined over, pinned here because the
operations are meaningless without it: a scheme is a code-bearing
document family declared in config; a document is a coded file in a
scheme’s directory; a code is PREFIX-NNN; a kind is anything
luria new can scaffold (schemes, the journal, fragment directories); an
entry is a journal file; a fragment is a changelog piece awaiting
collection; a view is generated output. Migrations operate on schemes
and documents; entries and fragments are only ever swept as reference
carriers.
Alternatives considered
- Supersede-and-copy as the mechanism — mark every DP superseded, point
each at a fresh GP. Zero new machinery, and it leaves the record twice as
long and half as readable: every old code persists, every browse crosses a
tombstone. Kept as an explicit
strategy = "supersede"for projects that want the paper trail in the documents themselves; rejected as the answer to #29. - Rewrite targets, keep labels in historical files — the “history is
holy” middle path: journals keep saying
DP-4while the link underneath points at GP-4. Worst of both worlds. The label/target split doubles the spellings grep must know, and since old spellings match no configured scheme, the linter ignores exactly the files the preservation was meant to honor. Preserved history that nothing watches is not preserved. - A
[[migrations]]ledger in config — the alias map kept by hand inluria.toml. The bookkeeping is identical toformerly:but lives in the wrong place: it grows without bound, drifts from the documents it describes, and makes config a history file. Derived-from-documents wins. - Prefix-driven sweep (
s/DP-/GP-/with an exclusion list) — simpler to write, and the exclusion list is a standing invitation to eat a fixture the day someone adds one without updating it. The mapping is the exclusion list, expressed as what to touch instead of what to spare. - Status quo — migrate by hand when the day comes. The DP→GP rename alone touches the config, nine documents, a view, its anchors, and every citing file; done by hand it is exactly the kind of sweep that misses one anchor and poisons the lint’s coverage silently.
Consequences
- The rename asked for in #29 waits for rung 2 and then becomes its acceptance test (#55): if DP→GP needs hand edits beyond writing the spec file, the machinery isn’t done.
- Rung 1 ships first and alone (#54):
formerly:parsing, the alias map,legacy-spellings, the fixer upgrade. record/migrations.d/joins the record’s layout;luria new migrationjoins the kinds. Each executed migration leaves three artifacts: the spec, theformerly:stamps, and the blame-ignore line.- Every future reference-handling change inherits an obligation: resolution must consult the alias map, or renamed documents silently lose their inbound links from in-flight branches.
- What this obliges humans to keep doing: supplying required frontmatter
after a cross-scheme move, and merging the migration PR quickly — the
mapping is correct the day it is generated, and every new reference
written in the old spelling afterwards is one more
legacy-spellingswarning to burn down.