docs(spec): state the retired allowRestore / allowPurge parse-time accept set exactly - #17485
Conversation
…exactly `ObjectPermissionSchema`'s retired `allowRestore` / `allowPurge` tolerate exactly ONE value — the boolean literal `false` the published 17.x toolchain materialized into every built artifact, accepted as inert residue and stripped (#12840). The prose around them said only "every other value" and "a loud publish-time error", which reads as a truthy/falsy split and as a refusal that covers `false` too. Neither is what the schema does. State it precisely in the three places a consumer reads — the tombstone docblock, the exported schema's docblock, and the two hand-written permission docs — including the consequence: on validated data neither key can ever appear, so `'allowRestore' in permissions`, `if (permissions.allowRestore)` and `=== true` are all dead code, and only pre-parse tooling over raw sources can tell legacy `false` residue from an ADR-0049 violation. Pins: extend the refusal matrix to the full measured set (`'true'` and `1` join `true` / `0` / `''` / `'false'` / `null`), assert the issue shape (`invalid_type`, `expected: 'never'`) at the key's own path for both keys, and record the one post-parse observation that does survive — an explicit `undefined` in an in-memory input keeps the own key, which JSON can never spell. No accept set moves. Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH Co-authored-by: Claude <noreply@anthropic.com>
…correction The tarball ships `src/**/*.zod.ts` and the `dist/*.d.ts` that carries the same TSDoc, so the corrected prose is published bytes even though no value, key or exported symbol moves. Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 135 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 4803c24787890b6f062be379aeb0845782dfeac0 && git checkout 4803c24787890b6f062be379aeb0845782dfeac0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 010c48a09d0ccfb75be2106e98b09558aa2e542a a9469d6b445955cdcef58d6113c02a6cc66aba23 && git checkout -B drift-repro 010c48a09d0ccfb75be2106e98b09558aa2e542a && git merge --no-ff a9469d6b445955cdcef58d6113c02a6cc66aba23
node scripts/docs-audit/affected-docs.mjs --json 010c48a09d0ccfb75be2106e98b09558aa2e542a
|
This PR has been green and accepted for ~32 hours and was not landed. That is this seat's miss, not a gate's.Read 2026-09-12T00:55Z on head All four landing pre-checks, measured now on THIS head
Why it is still not landed right now⛔ Not a gate and ⛔ not a review question — the draft→ready flip is failing route-wide: ⭐ The #17485 attempt is the control for that claim: the 503 reproduces on a second, unrelated PR, so it is the route and ⛔ not something about #17711. Read back after every attempt:
|
Part of #17425.
Clause-②: no — measured. No accept set moves in either direction: the only source file this PR touches under
packages/spec/srcchanges comments and nothing else (0 non-comment changed lines vsorigin/main, against a lit control of 24 on the test file in the same diff). The claim on the card declaredClause-②: yesfor the narrowing triage asked for; the fence below stopped that narrowing, so what lands here is the card's own ask only.needs:contract-reviewstays hung, because the fork this PR reports is a contract question — re-declaring the clause is the seat's act, not this PR's.The fence stopped the narrowing — the card belongs in the decision box
Triage asked for the retirement to be completed: refuse every spelling of
allowRestore/allowPurge, not just the non-falseones. The dispatch fenced that behind a question: is thefalseadmission a general deliberate rule, or an incidental hand-written shape? It is the general rule, and these two keys are its founding case. Both sources agree; there is no disagreement fork.From #12840, the issue that commissioned the admission:
From
packages/spec/src/shared/retired-key.ts, on the helper itself:And the shape is not hand-written:
permission.zod.tsbuilds bothObjectPermissionSchemaandEffectiveObjectPermissionSchemaby calling that shared helper with a captured literal (acceptRetiredDefaultResidue(ObjectPermissionBaseSchema, OBJECT_PERMISSION_RETIRED_KEY_RESIDUE)). Refusingfalsefor these two keys is therefore a local exception to a general, maintainer-ruled, explicitly non-re-adjudicable rule — a contract decision, not ordinary work. Fence branch 1. Not written here.Two further readings the decision needs:
z.inputtypes the keynever, so writingallowRestore: falsein.tssource failstscbefore anything runs, and that channel is pinned. For a JSON author (objectstack.json) there is no such channel: the samesafeParseserves the hand-written source and the machine-built artifact, so a JSON source writingallowRestore: falsegets the artifact's silent acceptance. Triage's "an author writingallowRestore: falsegets no signal at all" is true for JSON sources and false for TypeScript ones.permission.test.tscarries a whole[#12840]block asserting the strip, the round-trip fixpoint, a 75-occurrence artifact parsing with no warning storm, copy-on-write pass-through, and the@ts-expect-errorcompile channel. Narrowing the accept set means deleting or inverting that commissioned suite — which is the seat's call to commission, not a dev's to take.Liveness, measured on raw source (the precondition)
Raw-source sweep over the tracked tree at
origin/main(git grep -o -P, occurrence counts, not line counts — the parsed-output probe the card warns about would read 0 everywhere for a structural reason and prove nothing):allowRestore: false/allowPurge: false— every spelling, whole repopackages/metadata/src/__fixtures__/hotcrm-17.1-built-permissions.artifact.json)runtime,metadata,metadata-core,specallowTransfer: false(a live key on the same objects)allowCreate: falseallowTeleport: false(fabricated)The 150-occurrence fixture is the #12840 founding artifact itself, and
packages/metadata/src/plugin-artifact-forward-conversion.test.tsparses it. Sibling repoobjectui(origin/main) carries 26 mentions, including two test files named*.retiredLifecycleKeys.test.*that pin the console's handling of these bits — cross-repo carriers a narrowing would also have to move. So: refusingfalsebreaks in-tree data today, and the population is not small.What this PR does land: the card's own ask
The card asked that whichever surface discusses this state the parse-time behaviour precisely. Three of them were imprecise in the same direction — they described a refusal that covers
false, which is not what the schema does:content/docs/permissions/permission-metadata.mdx: "Authoring either key is now a loud publish-time error" — true of every value except the one an upgrading author is most likely to still have.content/docs/protocol/objectql/security.mdx: "the schema now refuses them" — same.packages/spec/src/security/permission.zod.ts: "Every other value still lands here" — accurate but reads as a truthy/falsy split when paired with the sentence above it.All three now state: exactly one value is tolerated, the boolean literal
false, compared by identity;"false",0,1,"true"andnullare refused exactly liketrue, samecode/expected/ message; and no post-parse guard is meaningful, because on JSON-derived validated data the key is alwaysundefined— presence, truthiness and=== trueare all dead code — so thefalse-versus-other distinction has a live consumer only in pre-parse tooling over raw sources.Truth table, re-run on this tree (not inherited)
The card's table was taken at
cef399be82's base. Re-measured at merge base65ad77d5aviapnpm --filter @objectstack/spec exec tsx, for both keys, onObjectPermissionSchemaand on the wire clone:false→ parses, key stripped;true,"true","false",0,1,null→ refused,code: 'invalid_type',expected: 'never', path["allowRestore"]; key absent → parses, never added. Confirmed byte-for-byte.One correction to the card, measured. The card states a parsed object "can never carry
allowRestoreorallowPurgeat all" and that'allowRestore' in permissionsis dead code. That is true for every input reachable from JSON, and false for one input that is not: an in-memory TS/JS object carrying an explicitundefined({ allowRestore: undefined }— what spreading an object that once held the key produces) parses, and the key survives as an own property with valueundefined. So'allowRestore' in parsedcan betrue;parsed.allowRestore === trueand the truthiness check stay dead in every case, andJSON.parse(JSON.stringify(parsed))drops the key again. This is now pinned, and the prose says it.Pins added: the existing refusal matrix grows
'true'and1, runs over both keys, and now asserts the issue shape (invalid_type/expected: 'never') at the key's own path rather than only the message; plus the explicit-undefinedcharacterization above.Verification
Merge base
65ad77d5a; head as pushed. Everything below captured ascmd > log 2>&1; EXIT=$?, never through a pipe.pnpm --filter '@objectstack/spec^...' build+pnpm --filter @objectstack/spec build— VERDICT command-exit 0 (underscripts/pm/os-verify-lock.sh).pnpm --filter @objectstack/spec exec vitest run src/security/permission.test.ts— exit 0, 63 tests passed.pnpm --filter @objectstack/spec typecheck— exit 0.pnpm --filter @objectstack/spec check:generated— exit 0, all 15 generated artifacts up to date (check:docs,check:authorable-surface,check:api-surface,check:migration-registry,check:spec-changes,check:upgrade-guideamong them).migrations/registry.tsis untouched: no ADR-0087 conversion is owed, because nothing authorable changes shape.check:react-declaration-parity— exit 0,MANIFEST=/…/objectstack-issue-17425/sdui.manifest.json(the committed root manifest; the first attempt pointed atpackages/spec/sdui.manifest.json, which does not exist, and its exit 1 was a wrong-reason red, discarded).check:adr-0087-registration,check:empty-changeset,check:changeset-no-major,check:changeset-fixed,check:closing-keyword-parity,check:nul-bytes,check:spec-docblock-symbol-anchors,check:doc-frontmatter,check:doc-anchors,check:doc-authoring,check:docs-single-h1,check:docs-audit-scope,check:affected-docs,check:cross-package-test-inputs,check:test-source-alias,check:liveness,check:exported-any.@objectstack/specsuite and the repo-wide gate farm are left to CI. The narrowing is measured, not assumed — the schema diff is comment-only (0 non-comment changed lines, lit control 24 on the test file), so no runtime behaviour can move for any other suite.Scope note
The file face the claim declared was
permission.zod.ts, its tests, an ADR-0087 entry with its regenerated registry block, and a changeset. The face moved in both directions and the seat should amend it: it shrank (no ADR-0087 entry and nomigrations/registry.tsregeneration — the fence removed the change that would have owed them) and it grew by two hand-written docs pages,content/docs/permissions/permission-metadata.mdxandcontent/docs/protocol/objectql/security.mdx, which are the "downstream consumer docs" the card's ask names and which carried the same imprecision. Both were measured free of every open PR's branch at write time.Written by an
os-devsubagent under thedomain:specexecution seat's claim, in sessionsession_01MkQhmuuJAVDjmeWNixwDDH; it posted no second claim and did not write the assignee.Generated by Claude Code