MeridiansMeridians

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

  1. 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.ts for canonical bodies; a versioned rebuild for projections. Features, reducers, prompts, queries, and analytics receive the current contract only. A ?? legacyValue, an Array.isArray(...) guard on a required field, or a LEGACY_* alias map outside that boundary is a defect, not caution.
  2. 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 (LegacyOffRamp in ordered-migrations.ts). Steps declare this on their plan; everything else is listed in src/lib/core/storage/legacy-ledger.ts. A legacy path that is not in the ledger is a bug.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. A retire date is enforced, not remembered. src/__tests__/legacy-ledger.test.ts fails 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.
  8. 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 LegacyOffRamp and a fixture from a real record. It opens (or reuses) the tracking issue and sets retireBy two 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-rampFamilyRetire whenByIssue
Domain v7 experience vocabulary, v8 scene arraysrecord dialectforward-stamp fleet-wide; probe legacyArcKeys = legacyReadingKeys = sparseScenes = 02026-11-30#218
frame_legacy_* ids, frameId-less deltasrecord dialectprobe frameDeltasNoId = 02026-11-30#219
"undefined = legacy" optional fields and their readersrecord dialectprobe framesNoTime = threadsNoCreated = 0; fields required2026-12-31#220
IndexedDB projection migrations, LegacyBoardLensprojectionversion bump + rehydrate shipped one release earlier2026-10-31#221
URL / artifact / position-kind aliases, key-store sweepaliasthe date2026-10-15#222
Plaintext aux secrets, DEK move, PBKDF2 fallback, local-only Directorsecretseal-all / rehash op has run fleet-wide2027-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.