Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/retired-defaulted-key-tolerance.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
---
'@objectstack/spec': minor
---

Retired-defaulted-key tolerance (#12840, class rule): a RETIRED key that carried a schema default is now refused only when it carries a NON-default value. The retired default parses as inert residue and is STRIPPED — the normalized output does not carry the key, and a parse → serialize round-trip converges to the clean shape.

Founding case: `permissions[].objects.<object>.allowRestore` / `.allowPurge` (#12497). Every artifact built by the published `@objectstack/spec` 17.x has both keys materialized as `false` in every permission entry (the pre-retirement schemas emitted `z.boolean().default(false)`), so the #12497 refusal was sentencing every previously built artifact — marketplace packages, installed environments — to a boot failure on the next runtime upgrade. Those artifacts now parse; `allowRestore: true` / `allowPurge: true` keep the full #12497 refusal with the prescription byte-for-byte, and nothing is un-retired: the keys stay tsc-`never` tombstones on the authoring surface, and the authorable-surface/JSON-schema artifacts still publish the `[RETIRED]` rows.

Ships as a reusable helper — `acceptRetiredDefaultResidue(schema, residue)` in `packages/spec/src/shared/retired-key.ts` — where `residue` is the retired default captured as a literal at retirement time (never re-read from anywhere live). The next retirement of a defaulted key reuses the helper instead of re-inventing the judgement.
9 changes: 8 additions & 1 deletion packages/spec/src/conversions/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8114,7 +8114,14 @@ const objectGridDefaultSortRemoved: MetadataConversion = {
*
* `retiredFromLoadPath`: ObjectPermissionSchema tombstones both keys
* (`retiredKey`, tsc `never` + the parse-time prescription), the
* `permission-rls-priority-removed` posture one block over.
* `permission-rls-priority-removed` posture one block over — with one ruled
* exception the tombstone carries itself (#12840, maintainer 2026-08-28):
* the keys' retired default (`false`), which every artifact built by the
* published 17.x toolchain has materialized in every entry, parses as inert
* residue and is stripped by the `acceptRetiredDefaultResidue` stage on the
* schema. This conversion stays migrate-meta-only: it rewrites SOURCES (both
* values, `true` included); the load-path tolerance covers only the emitted
* default and leaves `true` on the refusal.
*/
const permissionAllowRestorePurgeRemoved: MetadataConversion = {
id: 'permission-allow-restore-purge-removed',
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,10 +4,13 @@
// (see that entry for the full rationale: ADR-0049 enforce-or-remove,
// maintainer ruling 2026-08-26 accepting #1883's recommendation B; the key
// returns with the M2 lifecycle initiative). `EffectiveObjectPermissionSchema`
// is `ObjectPermissionSchema.extend({ apiOperations }).strip()` — the clone
// extends the same closed base shape (`.extend({ apiOperations }).strip()`,
// both faces behind the #12840 residue stage since 2026-08-28) — the clone
// shares the authoring shape's per-property schema instances, so the
// `retiredKey()` tombstone rides into the effective surface and this def's
// walked shape carries the same `[RETIRED]` row. Registered so the aging clock
// walked shape carries the same `[RETIRED]` row, while the retired default
// (`false`) an older published-toolchain server still emits on the wire is
// accepted as inert residue and stripped. Registered so the aging clock
// (#5898) has an exact-key entry for BOTH rows the tombstone produces. The
// effective surface is server-resolved, never authored, so no D2 conversion
// clause targets it — the authoring-side strip in
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,10 +4,13 @@
// (see that entry for the full rationale: ADR-0049 enforce-or-remove,
// maintainer ruling 2026-08-26 accepting #1883's recommendation B; the key
// returns with the M2 lifecycle initiative). `EffectiveObjectPermissionSchema`
// is `ObjectPermissionSchema.extend({ apiOperations }).strip()` — the clone
// extends the same closed base shape (`.extend({ apiOperations }).strip()`,
// both faces behind the #12840 residue stage since 2026-08-28) — the clone
// shares the authoring shape's per-property schema instances, so the
// `retiredKey()` tombstone rides into the effective surface and this def's
// walked shape carries the same `[RETIRED]` row. Registered so the aging clock
// walked shape carries the same `[RETIRED]` row, while the retired default
// (`false`) an older published-toolchain server still emits on the wire is
// accepted as inert residue and stripped. Registered so the aging clock
// (#5898) has an exact-key entry for BOTH rows the tombstone produces. The
// effective surface is server-resolved, never authored, so no D2 conversion
// clause targets it — the authoring-side strip in
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,11 @@
// `permission` metadata root, so the route is the `retiredKey()` tombstone
// (the `rls.priority` posture) — the key stays in the walked shape as
// `[RETIRED]`, and authoring it is a tsc error and a parse error carrying the
// prescription. Sources are rewritten by the D2 conversion
// `permission-allow-restore-purge-removed`, which strips the key from every
// object grant in `permissions[].objects`.
// prescription — with ONE ruled exception (#12840, maintainer 2026-08-28):
// the key's own retired default (`false`), which the published 17.x toolchain
// materialized into every built artifact's entries, parses as inert residue
// and is stripped by the `acceptRetiredDefaultResidue` stage ahead of the
// shape; every other value keeps this refusal. Sources are rewritten by the
// D2 conversion `permission-allow-restore-purge-removed`, which strips the
// key from every object grant in `permissions[].objects`.
export const entry = 'security/ObjectPermission:allowPurge';
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,11 @@
// `permission` metadata root, so the route is the `retiredKey()` tombstone
// (the `rls.priority` posture) — the key stays in the walked shape as
// `[RETIRED]`, and authoring it is a tsc error and a parse error carrying the
// prescription. Sources are rewritten by the D2 conversion
// `permission-allow-restore-purge-removed`, which strips the key from every
// object grant in `permissions[].objects`.
// prescription — with ONE ruled exception (#12840, maintainer 2026-08-28):
// the key's own retired default (`false`), which the published 17.x toolchain
// materialized into every built artifact's entries, parses as inert residue
// and is stripped by the `acceptRetiredDefaultResidue` stage ahead of the
// shape; every other value keeps this refusal. Sources are rewritten by the
// D2 conversion `permission-allow-restore-purge-removed`, which strips the
// key from every object grant in `permissions[].objects`.
export const entry = 'security/ObjectPermission:allowRestore';
34 changes: 24 additions & 10 deletions packages/spec/src/migrations/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7939,10 +7939,13 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly<Record<number, readonly string[]>>
// (see that entry for the full rationale: ADR-0049 enforce-or-remove,
// maintainer ruling 2026-08-26 accepting #1883's recommendation B; the key
// returns with the M2 lifecycle initiative). `EffectiveObjectPermissionSchema`
// is `ObjectPermissionSchema.extend({ apiOperations }).strip()` — the clone
// extends the same closed base shape (`.extend({ apiOperations }).strip()`,
// both faces behind the #12840 residue stage since 2026-08-28) — the clone
// shares the authoring shape's per-property schema instances, so the
// `retiredKey()` tombstone rides into the effective surface and this def's
// walked shape carries the same `[RETIRED]` row. Registered so the aging clock
// walked shape carries the same `[RETIRED]` row, while the retired default
// (`false`) an older published-toolchain server still emits on the wire is
// accepted as inert residue and stripped. Registered so the aging clock
// (#5898) has an exact-key entry for BOTH rows the tombstone produces. The
// effective surface is server-resolved, never authored, so no D2 conversion
// clause targets it — the authoring-side strip in
Expand All@@ -7953,10 +7956,13 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly<Record<number, readonly string[]>>
// (see that entry for the full rationale: ADR-0049 enforce-or-remove,
// maintainer ruling 2026-08-26 accepting #1883's recommendation B; the key
// returns with the M2 lifecycle initiative). `EffectiveObjectPermissionSchema`
// is `ObjectPermissionSchema.extend({ apiOperations }).strip()` — the clone
// extends the same closed base shape (`.extend({ apiOperations }).strip()`,
// both faces behind the #12840 residue stage since 2026-08-28) — the clone
// shares the authoring shape's per-property schema instances, so the
// `retiredKey()` tombstone rides into the effective surface and this def's
// walked shape carries the same `[RETIRED]` row. Registered so the aging clock
// walked shape carries the same `[RETIRED]` row, while the retired default
// (`false`) an older published-toolchain server still emits on the wire is
// accepted as inert residue and stripped. Registered so the aging clock
// (#5898) has an exact-key entry for BOTH rows the tombstone produces. The
// effective surface is server-resolved, never authored, so no D2 conversion
// clause targets it — the authoring-side strip in
Expand DownExpand Up@@ -7987,9 +7993,13 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly<Record<number, readonly string[]>>
// `permission` metadata root, so the route is the `retiredKey()` tombstone
// (the `rls.priority` posture) — the key stays in the walked shape as
// `[RETIRED]`, and authoring it is a tsc error and a parse error carrying the
// prescription. Sources are rewritten by the D2 conversion
// `permission-allow-restore-purge-removed`, which strips the key from every
// object grant in `permissions[].objects`.
// prescription — with ONE ruled exception (#12840, maintainer 2026-08-28):
// the key's own retired default (`false`), which the published 17.x toolchain
// materialized into every built artifact's entries, parses as inert residue
// and is stripped by the `acceptRetiredDefaultResidue` stage ahead of the
// shape; every other value keeps this refusal. Sources are rewritten by the
// D2 conversion `permission-allow-restore-purge-removed`, which strips the
// key from every object grant in `permissions[].objects`.
'security/ObjectPermission:allowPurge',
// #12497 — ADR-0049 enforce-or-remove (maintainer ruling 2026-08-26, decision-
// inbox batch 5, accepting #1883's recommendation B). `allowRestore` claimed to
Expand All@@ -8014,9 +8024,13 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly<Record<number, readonly string[]>>
// `permission` metadata root, so the route is the `retiredKey()` tombstone
// (the `rls.priority` posture) — the key stays in the walked shape as
// `[RETIRED]`, and authoring it is a tsc error and a parse error carrying the
// prescription. Sources are rewritten by the D2 conversion
// `permission-allow-restore-purge-removed`, which strips the key from every
// object grant in `permissions[].objects`.
// prescription — with ONE ruled exception (#12840, maintainer 2026-08-28):
// the key's own retired default (`false`), which the published 17.x toolchain
// materialized into every built artifact's entries, parses as inert residue
// and is stripped by the `acceptRetiredDefaultResidue` stage ahead of the
// shape; every other value keeps this refusal. Sources are rewritten by the
// D2 conversion `permission-allow-restore-purge-removed`, which strips the
// key from every object grant in `permissions[].objects`.
'security/ObjectPermission:allowRestore',
// #9220 — ADR-0049 enforce-or-remove at ELEMENT grain. `element:filter` never
// had a renderer or reader anywhere: objectui registers none (its
Expand Down
Loading
Loading