Legacy lifecycle — how compatibility enters, rolls through the fleet, and leaves
From the Meridians Wiki · Public · Maintained · joint
Scope. This doc owns the management of legacy: the principles under which a historical shape may be tolerated, the one place every tolerated shape is listed, the mechanism that upgrades tenants so the tolerance can end, and the enforcement that makes it end on a date. The migration boundary itself (what a step is, how a body is stamped) is owned by Record and write path §versioning; this doc is what governs the steps' arrival and departure. Living document: when the code and this disagree, fix whichever is wrong.
1. Why legacy decays a system
Legacy code is not old code. It is a reader that tolerates a shape the writer no longer produces.
Each such reader is a second dialect of the record: it must be understood before any feature that
touches the shape can be changed, it is exercised only by data nobody can see in a fixture, and it
never announces when it has stopped being needed. Left alone, tolerance accumulates until the current
contract is a minority of the code that reads it. The 2026-09-09 audit found ~309 legacy sites in
100+ runtime files against a policy that says there should be none downstream, and a hosted record
(jasonyu0100.meridians.global) in which 18 of 22 domains still depended on them.
The cause was never the migrations. It was that migrations were written for the reader and never run against the record, so the condition that would have let them leave never arrived.
2. Principles
- Tolerance lives at one boundary. A historical shape is translated in exactly one place — a
step in a plan in
src/lib/core/storage/document-migrations.tsfor canonical bodies; a versioned rebuild for projections. Features, reducers, prompts, queries, and analytics receive the current contract only. A?? legacyValue, anArray.isArray(...)guard on a required field, or aLEGACY_*alias map outside that boundary is a defect, not caution. - Every tolerance is a ledger entry. A retained transform carries, in code, what it translates,
when it landed, the observable condition that licenses deletion, the date by which that must
have happened, and the GitHub issue that tracks it (
LegacyOffRampinordered-migrations.ts). Steps declare this on their plan; everything else is listed insrc/lib/core/storage/legacy-ledger.ts. A legacy path that is not in the ledger is a bug. - Upgrade the record, not the reader. A step exists to be run, fleet-wide, through the forward-stamp operation (§4). "Supported records no longer need it" is a probe reading of zero on every supported tenant, not an impression.
- Projections are rebuilt, never migrated. IndexedDB and every other derived cache get a version bump, a clear, and a rehydrate from the record. Projection-migration code is a policy violation with a date on it.
- Aliases die on a date. Renamed URL tokens, artifact kinds, storage keys: there is no record dependency, so the ledger date is the off-ramp. Old links land on the surface default.
- Secrets and credentials get an explicit operation. Plaintext pass-through, key-store moves, and hash-cost fallbacks are the one family where silent removal locks people out. They leave only after an explicit seal / rehash operation has run, and never lazily.
- A retire date is enforced, not remembered.
src/__tests__/legacy-ledger.test.tsfails the suite the day an off-ramp is overdue. The only two ways to go green are to delete the code with its fixtures, or to re-plan the date on the issue and in the ledger together. Re-planning is allowed; quiet slippage is not. - Snapshot before any record write; verify after. The forward-stamp op snapshots first, writes through the ordinary CAS path, and re-runs the probe. Nothing in a supported tenant is lost by an off-ramp if the probe read zero before the reader was deleted.
3. Lifecycle of one shape
contract change ──▶ step + fixture + off-ramp ──▶ forward-stamp fleet ──▶ probe = 0 ──▶ delete step,
(PR) (same PR, dated issue) (admin MCP, per tenant) (issue) fixtures, guards
raise baseline
- Enter. The PR that changes a canonical contract adds the step and its
LegacyOffRampand a fixture from a real record. It opens (or reuses) the tracking issue and setsretireBytwo releases out, never "later". - Roll. The forward-stamp op is run per tenant through the admin MCP (Fleet ops 🔒 read order first, then the write). The issue records each tenant's before/after probe.
- Leave. When the probe reads zero fleet-wide, one PR: raise the plan baseline, delete the step
and fixture, delete every downstream guard the type-tightening surfaces, remove the ledger entry,
close the issue.
record-and-write-path.md§versioning is updated in the same PR.
4. The forward-stamp operation
A Tier-2 admin operation, one tenant at a time, that: snapshots the record; reads every document
through the migration boundary plus the one-time fills the boundary cannot infer (frame ids on
deltas, timestamps on frames and threads, current vocabulary on arcs and readings); writes back through
the ordinary CAS path so DocMeta.version advances and SSE echoes; and reports the legacy-counter
probe before and after. It is tracked as
#217 and is the prerequisite for every
record-dialect off-ramp. It is specified, not yet built.
5. The ledger today (2026-09-09)
| Off-ramp | Family | Retire when | By | Issue |
|---|---|---|---|---|
| Domain v7 experience vocabulary, v8 scene arrays | record dialect | forward-stamp fleet-wide; probe legacyArcKeys = legacyReadingKeys = sparseScenes = 0 | 2026-11-30 | #218 |
frame_legacy_* ids, frameId-less deltas | record dialect | probe frameDeltasNoId = 0 | 2026-11-30 | #219 |
| "undefined = legacy" optional fields and their readers | record dialect | probe framesNoTime = threadsNoCreated = 0; fields required | 2026-12-31 | #220 |
IndexedDB projection migrations, LegacyBoardLens | projection | version bump + rehydrate shipped one release earlier | 2026-10-31 | #221 |
| URL / artifact / position-kind aliases, key-store sweep | alias | the date | 2026-10-15 | #222 |
| Plaintext aux secrets, DEK move, PBKDF2 fallback, local-only Director | secret | seal-all / rehash op has run fleet-wide | 2027-01-31 | #223 |
The code is the source of truth for this table (legacy-ledger.ts and the Domain plan); regenerate the
table from it rather than editing the two apart.
6. What this is not
- Not a licence to keep a shape because a tenant has it. The tenant is upgraded; the reader is not preserved.
- Not a blanket delete. Removing a reader before the probe reads zero on every supported tenant destroys data; the ledger exists so that order is never inverted.
- Not a second migration framework. There is one boundary and one runner; the ledger is metadata on what already exists.