diff --git a/AGENTS.md b/AGENTS.md index 1f9239d274b..05c74b8f81f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -250,9 +250,9 @@ See `ui/AGENTS.md` for the app-id / localStorage / auth gotchas. "be liberal in what you accept" does **not** apply. Change the **spec** only when the spec itself is genuinely wrong, and then deliberately (edit the Zod schema + migrate). When an alias must be tolerated at all, declare it as an **ADR-0087 conversion-layer entry** (never a bare `??`, no executor shims) so it is declared, loud, tested, - and *removable on a schedule*. Stored `sys_metadata` rows are covered from the other side: every rehydration seam - replays the **full** conversion chain — retired entries included — via `applyConversionsToStoredItem` (ADR-0087 - addendum), so a consumer never needs its own accommodation for a legacy stored shape either. Strengthens #5. + and *removable on a schedule*. Stored rows: `applyConversionsToStoredItem` replays the chain the installed release + carries, retired entries included; kits age out one major after retiring (ADR-0087 as amended, ruling 6434038284), + so run `os migrate meta --stored --apply` before leaving a release that still converts the row. Strengthens #5. 13. **An accepted ADR binds until a superseding ADR says otherwise.** Reversing a recorded decision is itself a decision: it needs a **new ADR** (or an amended status line on the old one), not a changeset that quietly does the opposite. Before changing behaviour in `docs/adr/`-governed territory, **grep the ADRs for the surface you are diff --git a/packages/metadata-protocol/src/stored-migration.ts b/packages/metadata-protocol/src/stored-migration.ts index e3e8477559d..fcc137f9848 100644 --- a/packages/metadata-protocol/src/stored-migration.ts +++ b/packages/metadata-protocol/src/stored-migration.ts @@ -5,9 +5,15 @@ * (#4327), the follow-up to #3903's read-path guarantee. * * #4317 closed the correctness gap from the read side: every stored-row - * rehydration seam replays the full ADR-0087 conversion chain - * (`applyConversionsToStoredItem`, retired entries included), so a row written - * under any past protocol is *served* canonical forever. What it deliberately + * rehydration seam replays the ADR-0087 conversion chain the installed release + * carries (`applyConversionsToStoredItem`, retired entries included), so a row + * is *served* canonical while that release still carries its conversions. A + * retirement kit ages out one major after it retires (ADR-0087 as amended, + * ruling 6434038284), so this pass must run on a release that still carries a + * row's conversions, before the deployment moves to the release that drops + * them. Past that, the seams still read and serve the row's legacy bytes, but + * no conversion recognizes its shape, so this pass counts it `canonical`: a + * false clean, not evidence. What it deliberately * did not do is make the rows themselves canonical — a pre-17 row keeps its * legacy bytes, the chain re-lowers it on every load, and each affected row * emits one conversion notice per process. @@ -19,10 +25,13 @@ * the types rather than in the CLI so an admin route renders the same run the * same way. * - * **Not load-bearing.** #3855's conclusion still holds — an operator-run - * migration cannot be relied on, so the read path stays the guarantee and - * nothing gates on this having run. No `sys_migration` flag is recorded for - * exactly that reason: a flag row would advertise a gate that does not exist. + * **Not load-bearing while the release carries a row's conversions.** #3855's + * conclusion holds within that window — an operator-run migration cannot be + * relied on, so the read path stays the guarantee there and nothing gates on + * this having run; for a row whose conversions the next release drops, this + * pass is the required step above, not an optional one. No `sys_migration` + * flag is recorded for exactly that reason: a flag row would advertise a gate + * that does not exist. * The verifiable statement operators wanted is the *re-run* — a second pass * that reports every row canonical is the evidence, and it costs one command. */ diff --git a/packages/spec/src/conversions/apply.ts b/packages/spec/src/conversions/apply.ts index eeaa58bb9c1..81b64fca5c3 100644 --- a/packages/spec/src/conversions/apply.ts +++ b/packages/spec/src/conversions/apply.ts @@ -52,7 +52,9 @@ export interface ApplyConversionsOptions { * have no author to teach, so each replays the FULL chain, retired entries * included — ADR-0087's `## Addendum (2026-07-31)` for the first two, the * #12772 ruling for the third. The fixture CI sets it as well, so graduated - * transforms stay covered forever (ADR-0087 D3). + * transforms stay covered while the installed release carries them; a + * retirement kit ages out one major after it retires (ADR-0087 as amended, + * ruling 6434038284), and its transform leaves coverage with it. * * `objectstack migrate meta` is NOT one of these callers: `applyMetaMigrations` * (`../migrations/chain.js`) looks each step's conversion up in diff --git a/packages/spec/src/conversions/stored.ts b/packages/spec/src/conversions/stored.ts index c85d548a9df..6da63ab77ce 100644 --- a/packages/spec/src/conversions/stored.ts +++ b/packages/spec/src/conversions/stored.ts @@ -14,13 +14,18 @@ * {@link applyConversionsToStoredItem} is the one primitive every stored-row * rehydration seam calls, and it encodes the policy for data at rest: * - * - **The full chain replays, including `retiredFromLoadPath` entries.** + * - **The chain the installed release carries replays, including + * `retiredFromLoadPath` entries.** * Retirement is an *authoring-surface* event — the schema tombstones the * key so a live author is taught the canonical spelling. A stored row has * no author; refusing its historical shape would not teach anyone, it - * would only break data that once worked. D3 keeps every conversion - * forever precisely so any past major can replay forward — a row at rest - * is the perpetual "consumer arriving late". + * would only break data that once worked. But a retirement kit ages out + * one major after it retires (ADR-0087 as amended, ruling 6434038284), and + * its conversion leaves with it: a row still in that shape is no longer + * recognized here, so it must be canonicalized by the stored pass + * (`os migrate meta --stored --apply`) on a release that still carries + * the row's conversions, before the deployment moves to the release that + * drops them. * - **Idempotent by construction.** Conversions only rewrite shapes they * positively recognize, so re-converting an already-canonical item is a * no-op — seams may safely stack (e.g. a row converted here and again at