Skip to content

feat(gates): cover the permission authoring shapes in designer-field-key-parity - #7272

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-6606-permission-payload-shapes
Sep 2, 2026
Merged

yinlianghui merged 1 commit into
mainfrom
claude/issue-6606-permission-payload-shapes

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #6606

scripts/check-designer-field-key-parity.mjs watched exactly one authoring
surface. Its six PAYLOAD_SHAPES are all the object/field designer path, judged
by FieldSchema / ObjectSchema. The permission matrix is a second surface
carrying both of the gate's ingredients — statically declared payload shapes,
and strict zod schemas that refuse unknown keys by name — and it was in none of
them, so the retired-key class the gate exists to catch was invisible there.
objectui#6595 (the retired allowRestore / allowPurge checkboxes) is an
instance of precisely that class, and it reached this repo as a hand-written
card from the upstream retirement (objectstack#12497) rather than from CI.

What this adds

Two entries in PAYLOAD_SHAPES, both in
packages/app-shell/src/views/metadata-admin/permission-slice.ts:

shape oracle subpath reach
PermissionSetDraft PermissionSetSchema @objectstack/spec/security wire
ObjectPerm ObjectPermissionSchema @objectstack/spec/security wire

PermissionSetDraft is the record PermissionMatrixEditor.doSave hands to
client.save(type, name, toSave); ObjectPerm is one authorization row inside
that record's objects map, so it reaches the same saved body one level down —
the field-in-object nesting again, which is why it needs its own oracle rather
than riding on the record's.

The subpath is per-oracle now, and that was a measurement

The permission schemas are not on /data. Measured against the installed
@objectstack/spec 17.2.0: /data exports neither ObjectPermissionSchema nor
PermissionSetSchema; /security exports both. So the hardcoded
import("@objectstack/spec/data") becomes a declared map, ORACLE_SPECIFIERS,
and ORACLES is derived from it (Object.keys) so the two cannot drift.

The two header constraints are kept, not worked around:

  • Extraction failure is an ERROR, never a pass. An oracle the map does not
    carry throws ExtractionError rather than defaulting to /data — a silent
    default there would resolve the wrong module (or nothing) and produce the
    confident green the file exists to prevent. There is no silent-skip path on
    either new entry.
  • The accept set is read from the installed schema, never listed in the
    file.
    No permission key list is hardcoded; resolution stays through the
    dynamic import(). /security carries the identical dual-package export map
    (dist/security/index.js for require, dist/security/index.mjs for
    import), so the provenance argument the header spells out applies to it
    unchanged — and the self-test pins all four oracles by reference identity
    against a plain static import from the subpath named in the map.

Emit-side, deliberately

ObjectPerm is a hand-written subset of the spec's ObjectPermission (it
omits allowExport, readScope, writeScope — objectui#6605). Surfacing that
under-coverage stays a separate card, so this is the same emit-side check as
every other entry. A test asserts the gate stays silent about the omitted keys,
so widening the charter has to argue for itself rather than happen by drift.

Gate verdict lines

Before (on 67dadd6, two oracles, six shapes):

designer-field-key-parity: FieldSchema accepts 71 keys
designer-field-key-parity: ObjectSchema accepts 42 keys
...
designer-field-key-parity: OK

After (18c3f61, four oracles, eight shapes):

designer-field-key-parity: FieldSchema accepts 71 keys
designer-field-key-parity: ObjectSchema accepts 42 keys
designer-field-key-parity: PermissionSetSchema accepts 20 keys
designer-field-key-parity: ObjectPermissionSchema accepts 12 keys
  PermissionSetDraft       10 declared  [wire] vs PermissionSetSchema     (+ index signature — see coverage note 2)
  ObjectPerm                7 declared  [wire] vs ObjectPermissionSchema

designer-field-key-parity: OK

Green on today's tree, as the filing card predicted it would be once
objectui#6595 had landed: every key both shapes declare is accepted by the
schema that judges it.

Teeth, proven by ablation

The card is fenced against editing permission-slice.ts, so the mutation was
applied to a copy of that file in a throwaway root and the real
PAYLOAD_SHAPES entries — real interface, real schema, real reach — were
run against it. The thing under test is the entry that ships, not a fixture
look-alike.

fenced file at HEAD before: dd14740b589865816440e03bb652535ed8c978b8
real entries under test: PermissionSetDraft -> PermissionSetSchema [wire] | ObjectPerm -> ObjectPermissionSchema [wire]
mutation on disk: injected counts=[1,1] (source counts=[0,0])
MUTATED  violations: ["allowResurrect:ObjectPermissionSchema:ObjectPerm","zzzRetiredRecordKey:PermissionSetSchema:PermissionSetDraft"]
restore on disk: injected counts=[0,0]
RESTORED violations: []
fenced file at HEAD after:  dd14740b589865816440e03bb652535ed8c978b8
ABLATION VERDICT: teeth proven on both new entries; fenced file byte-identical to HEAD throughout

Both legs prove they landed on disk by counting the injected and the original
text, not by reading an editor's exit code, and the fenced file is shown
byte-identical to its HEAD blob before and after.

Self-test

The per-shape non-vacuity guard was a literal label on every shape. An
authorization row does not have one, so that assertion is now a witness key per
shape (ObjectPerm -> allowRead, PermissionSetDraft -> objects), with
every entry in PAYLOAD_SHAPES required to appear in the table — a shape cannot
land unwitnessed and read as verified. The ledger's oracle validation now reads
ORACLES instead of re-listing ['FieldSchema', 'ObjectSchema'], so adding an
oracle cannot leave it behind.

Thirteen new cases (it() count in this file goes 32 -> 45, and 45 + 7 in
zod-wrapper-keys.shared.test.ts is exactly the 52 vitest reports as passing, so
none of them was skipped): reference identity for both permission oracles; the
/data-exports-neither measurement asserted rather than trusted; both accept sets
real and different; strictness with a parsing control body; a realistic negative
control (readable, a FieldPerm facet, on an object-permission row); per-shape
oracle routing (objects legal on the record, refused on a row); the
unresolvable-schema throw; the unmapped-oracle throw; both real entries present
and wire-bound; the index signature on PermissionSetDraft; every emitted key
accepted on the real tree; and the emit-side charter assertion.

Verification

command verdict
pnpm check:designer-field-key-parity (before, 67dadd6) designer-field-key-parity: OK
pnpm check:designer-field-key-parity (after, 18c3f61) designer-field-key-parity: OK
pnpm exec vitest run scripts/__tests__/check-designer-field-key-parity.test.ts scripts/__tests__/zod-wrapper-keys.shared.test.ts --maxWorkers=2 Test Files 2 passed (2) · Tests 52 passed (52)
pnpm type-check:scripts exit 0, no diagnostics
pnpm check:control-bytes check-control-bytes: OK (scanned 5996 tracked text file(s); skipped 85 binary)
pnpm check:entry-guard check:entry-guard: 58 scripts/ file(s) — no entry guard outside the baseline
pnpm check:esm-specifiers no un-ledgered package emits an extensionless relative specifier
node scripts/check-changeset-presence.mjs No source or published contract of a released package changed in this range, so no changeset is owed.

pnpm check:node-esm-load (the full two-leg form) is not run here and the
narrowing is declared rather than implied: its subject is published packages and
its load leg builds them, which exceeds this container's foreground limit. Its
cheap specifier leg — the one that reads source — ran green above, and the
changeset-presence gate measured the population independently: of the 2 changed
files, 0 are published source of a package the release covers and 0 are a
manifest whose published contract moved. Nothing under scripts/ is a published
package entry, so this diff cannot move any package's emitted specifiers. CI runs
the full farm regardless.

Every row above was run on 18c3f61 with a clean working tree (git status --porcelain empty), so the greens describe the tree this PR proposes and not an
earlier one. The two test files are the complete set that names this script —
git grep -l check-designer-field-key-parity -- 'scripts/__tests__/*' returns
exactly those two — and ci.yml:359 is the only workflow line that runs the
gate itself.

No changeset: scripts/-only, per the presence gate's own verdict quoted above.
No packages/ source touched; permission-slice.ts is untouched, as the card
required.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b


Generated by Claude Code

…key-parity

`scripts/check-designer-field-key-parity.mjs` watched one authoring surface:
the six object/field designer shapes, judged by `FieldSchema` / `ObjectSchema`.
The permission matrix is a second surface with both of the gate's ingredients
and was in none of them, so the retired-key class the gate exists to catch was
invisible there — objectui#6595 (the retired `allowRestore` / `allowPurge`
checkboxes) is an instance of exactly that class, and it reached this repo as a
hand-written card from the upstream retirement rather than from CI.

Adds `PermissionSetDraft` (the record `PermissionMatrixEditor.doSave` hands to
`client.save`) and `ObjectPerm` (one authorization row inside its `objects`
map) to `PAYLOAD_SHAPES`, judged by `PermissionSetSchema` and
`ObjectPermissionSchema`.

Both permission schemas are exported from `@objectstack/spec/security`, not
`/data` — measured against the installed 17.2.0, which exports neither on
`/data`. So the subpath becomes per-oracle, declared in `ORACLE_SPECIFIERS`
(`ORACLES` is now derived from it, so the two cannot drift). `/security`
carries the identical dual-package export map, so the header's provenance
argument for resolving through a dynamic `import()` applies unchanged, and the
self-test pins all four oracles by reference identity. An oracle the map does
not carry is an `ExtractionError`, never a silent default to `/data`.

The gate stays emit-side. `ObjectPerm` is a hand-written subset of the spec's
`ObjectPermission` (objectui#6605); surfacing that under-coverage is a separate
card, and a test asserts the gate stays silent about the omitted keys so a
charter change has to argue for itself.

Self-test: the per-shape non-vacuity guard was a literal `label` on every
shape, which an authorization row does not have. It is now a witness key per
shape, and every entry in `PAYLOAD_SHAPES` must appear in that table, so a new
shape cannot land unwitnessed. The ledger's oracle validation reads `ORACLES`
rather than re-listing it.

No behaviour change outside the gate; no package source touched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@github-actions github-actions Bot added the tests label Sep 2, 2026
@yinlianghui
yinlianghui marked this pull request as ready for review September 2, 2026 02:59
@yinlianghui
yinlianghui added this pull request to the merge queue Sep 2, 2026
Merged via the queue into main with commit a64bd9c Sep 2, 2026
29 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-6606-permission-payload-shapes branch September 2, 2026 03:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants