Uh oh!
There was an error while loading. Please reload this page.
feat(spec): retired-defaulted-key tolerance — the retired default parses as inert residue and strips (#12497 class rule) - #12845
Conversation
…ses as inert residue and strips (#12840) A RETIRED key that carried a schema default is refused only when it carries a NON-default value (maintainer ruling 2026-08-28, recorded on objectstack-ai/cloud#1685). The retired default — materialized by the published 17.x toolchain into every built artifact's permission entries — parses as inert residue and is STRIPPED before the closed shape sees it; the normalized output carries neither key and a round-trip converges. allowRestore/allowPurge: true keep the #12497 refusal byte-for-byte. Ships as the reusable class helper acceptRetiredDefaultResidue() (shared/retired-key.ts): a z.preprocess stage ahead of the closed shape (the ViewMetadataSchema / pipeAuthorableSide precedent), residue captured as literals at retirement time, copy-on-write, silent by design (75 occurrences in the measured HotCRM artifact would otherwise be a storm). Nothing is un-retired: z.input stays never (tsc channel pinned), the authorable-surface/JSON-schema artifacts keep the [RETIRED] rows, and the D2 conversion stays migrate-meta-only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 126 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 49e328955c342944dab31024e1c3d962e624d04e && git checkout 49e328955c342944dab31024e1c3d962e624d04e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f907fbe9ed62c68d3de1a8a49d5ac3b9ba13abfc 8599de8587453c239ef18a207b993aed6a5ce12b && git checkout -B drift-repro f907fbe9ed62c68d3de1a8a49d5ac3b9ba13abfc && git merge --no-ff 8599de8587453c239ef18a207b993aed6a5ce12b
node scripts/docs-audit/affected-docs.mjs --json f907fbe9ed62c68d3de1a8a49d5ac3b9ba13abfc
|
Uh oh!
There was an error while loading. Please reload this page.
…sal surface under the #12845 residue ruling Main landed acceptRetiredDefaultResidue (maintainer ruling 2026-08-28): a retired DEFAULTED key's emitted default parses as inert residue and strips silently at the schema layer; only a NON-default value keeps the #12497 tombstone. The real 17.1-built fixture carries exclusively the emitted default (150 x false, premise-guarded), so the old current-version refusal pin now describes a shape the ruled contract accepts - the merge queue measured exactly that on its merge tree. The pin is re-aimed, not weakened: current-version + NON-default value (true, authorable on the 17.1 surface, never emitted by a default) still refuses with the tombstone and prescription; and a new pin holds the other half of the ruling at the door - current-version + pure residue parses clean with the keys stripped by the SCHEMA layer while this door's versioned window stays closed (no conversion notice), keeping the door's amnesty versioned. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVYmuhHW6qZmNBqciaS7BN
Fixes#12840
Part of objectstack-ai/cloud#1653 (ruling record: objectstack-ai/cloud#1685 — maintainer 2026-08-28, the three-question review; objectstack-ai/cloud#1672 is the blocked consumer). cloud#1685 remains open.
What this implements (ruled semantics, not re-adjudicated)
A RETIRED key that carried a schema default is refused only when it carries a NON-default value:
allowRestore: false/allowPurge: false— the defaults the published@objectstack/spec17.x toolchain materialized into every built artifact's permission entries (75 occurrences in the measured HotCRM artifact; its sources declare neither) — now parse as inert residue and are STRIPPED: the normalized output carries neither key, and parse → serialize → parse is a fixpoint on the clean shape (no re-emission).allowRestore: true/allowPurge: truekeep the spec: retire theallowRestore/allowPurgepermission props (ruled 2026-08-26; M2 anchor stays open, keys return with M2) #12497 refusal byte-for-byte: same guidance string (pinned equal to the tombstone's own declared prescription), same issue shape (expected: 'never',code: 'invalid_type', located path). The guidance strings are untouched by this diff.The reusable helper (name and placement argued)
acceptRetiredDefaultResidue(schema, residue)inpackages/spec/src/shared/retired-key.ts— besideretiredKey(), because this is the second half of the same doctrine: the tombstone answers authored values, the residue stage answers the emitted default a released toolchain froze into artifacts.residueis the retired default captured as a literal at retirement time (the schema no longer has the default — the tombstone replaced it — so nothing live can be re-read; compared by identity, so falsy near-misses like0/ empty string /nullstill refuse). The next defaulted-key retirement reuses the helper with its own literal.Mechanism: a
z.preprocessstage ahead of the closed shape — the shippedViewMetadataSchema/translationretired-dialect precedent, whose pipe orientation every schema walker already resolves viapipeAuthorableSide(#4488/#5074/#5317). The strip is copy-on-write and runs before the per-key tombstone, so a non-default value never touches the stage and the refusal cannot drift.EffectiveObjectPermissionSchema(the wire clone) extends the same closed base and gets the same stage — a server still on the published 17.x toolchain emits the residue on the wire too.The card's boundary — the seam, examined
The card reserved a stop if the tombstone's seam cannot distinguish authoring from artifact parsing. Examined: the Zod parse sees no provenance (JSON carries none), and the ruling itself discriminates by VALUE, not by path — so the reserved stop does not trigger. What the authoring surface keeps, distinguishably and pinned:
z.inputstaysneverfor both keys — writing either in TypeScript source fails to compile exactly as spec: retire theallowRestore/allowPurgepermission props (ruled 2026-08-26; M2 anchor stays open, keys return with M2) #12497 ruled (pinned with a compiledts-expect-errorinpermission.test.ts; the file is in the test-typecheck program and not in its debt ledger — verified via--listFiles, 2 hits).[RETIRED]rows (verified inauthorable-surface/security.jsonafter regen), and the JSON-schema manifest is unchanged —check:generatedreports all 14 artifacts up to date with zero regeneration needed.What is deliberately tolerated everywhere (source JSON included): exactly the one value the released toolchain emitted, which the maintainer ruled behaviourally identical to absence for the key's entire history.
Notice posture
Zero notices, by design — the ruled bound is "at most low-noise, never per-occurrence storms". A spec schema parse has no notice channel, and inventing one for this would be a new side channel; the loud channels for authored sources (tsc, the D2 conversion
permission-allow-restore-purge-removed,os migrate meta) are untouched. Pinned: a 38-entry (76-residue-key) permission set parses with console spies asserting no output. The D2 conversion staysretiredFromLoadPath(migrate-meta-only) — it rewrites sources includingtrue, which must keep refusing at load; its docblock now records the split.Tests
security/permission.test.ts): the published-toolchain entry shape from cloud#1685 parses and carries neither key; each key strips independently;truerefused per key with the byte-for-byte pin; falsy near-misses refused; round-trip fixpoint; wire-clone tolerance; no-storm spy test.ObjectStackDefinitionSchemaparse with the residue at the exact measured coordinatepermissions[5].objects.crm_campaign_member.allowRestore— accepted and normalized.shared/retired-key.test.ts, synthetic schema with a boolean and a string default): reuse-shaped, captured-literal identity semantics, tombstone refusal byte-equality, strictness not loosened through the stage, walker-orientation pin (pipe with OUT = object).pnpm --filter @objectstack/spec test→ Test Files 433 passed | 4 skipped; Tests 11556 passed | 22 skipped.pnpm --filter @objectstack/spec typecheck→ clean (src + scripts + test layer,check:test-typecheck: OK).Ablation (disk-proven): mutated the helper's pipe to a pass-through (
stripdisabled; injected marker grep-counted 1, original line 0 — the mutation landed), reran the two suites: 9 failed | 61 passed, the failures exactly the residue-acceptance/round-trip/no-storm legs, everytrue-refusal leg green. Restore viagit checkout HEADwith an EXIT/INT/TERM trap on absolute paths; proven by emptygit diff HEADandgit hash-objectequal to the HEAD blob. Resolution path: spec's own vitest resolves the subject fromsrc/via relative imports (no dist in the loop), so neither leg needed a rebuild — stated per the ablation contract.Gates (all run at 8599de8, unlocked runs — no flock on this host)
check:generatedsweep: "All 14 generated artifacts are up to date" (api-surface, authorable-surface + JSON schemas + authorable-defaults, docs, spec-changes, upgrade-guide, migration-registry among them). The dispatch-gates derivation (no paths passed; stderr names this repo at 8599de8) yielded 22 families; all green: authorable-surface, docs, empty-state, liveness, skill-refs, spec-changes, strictness-ledger, changeset-gate-self-tests, cross-package-test-inputs, doc-authoring, doc-formula-expressions (after building its@objectstack/formula+@objectstack/lintprerequisites — first runs were PREREQUISITE NOT MET, not measurements), merge-driver, objectql-double-limit, objectui-changeset, page-declaration-shape, pm-half-states, published-files, query-options-erasure, slot-lookup, spec-parsed-alias, test-source-alias, type-source-resolution, pluscheck:nul-bytes.Changeset:
minorfor@objectstack/spec(launch-window rule; widens acceptance, not breaking — no ADR-0087 disposition marker owed, the gate judges declared-breaking changesets only).Consumer section — what cloud #1672 must re-verify after the bump
.objectstack-shato a SHA that includes this merge (it is an ancestor question, not a version-number one — the 17.2.0 number does not move here).apps/objectos-ee's HotCRM multi-tenant acceptance. The composed artifact built from the existing.hotcrm-sha(published spec 17.0.0/17.1.0, emitting both keys asfalse, 75 occurrences) must now boot — the step that died at first request (fetch failed/ ECONNREFUSED) should pass without touching.hotcrm-shaand without stripping keys cloud-side.truefor either key must still refuse with the spec: retire theallowRestore/allowPurgepermission props (ruled 2026-08-26; M2 anchor stays open, keys return with M2) #12497 guidance — the acceptance's subject stays "the shipped artifact boots", not "the parse tolerates everything".What was delivered here is the schema-level matrix + round-trip convergence + a composed-artifact-door reproduction at the exact measured path (
ObjectStackDefinitionSchema); a full HotCRM artifact boot was not reproduced locally — that is cloud#1672's acceptance step itself.Session: https://claude.ai/code/session_71836b57-5db6-459d-ae4d-c6d9d7dd2cc6
Generated by Claude Code