Repository navigation
finding(gates): the permission authoring shapes are not in PAYLOAD_SHAPES, so the retired-key class the gate exists to catch is invisible on the permission surface #6606
Description
Activity
- addeddomain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repoobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repoand removed
on Aug 27, 2026 Triage (first-touch grading of this finding): promoted — a scoped gate extension with a named landing point (add the permission authoring shapes to
PAYLOAD_SHAPESinscripts/check-designer-field-key-parity.mjs, oracleObjectPermissionSchema/PermissionSetSchema) ⇒ engineering/gate surface ⇒domain:devx; type Task — no contract change, and the gate's own header already declares subset coverage, so naming a seventh shape is within its existing charter.State: pm:blocked rather than pm:queue — until #6595 lands,
ObjectPermstill authors the retiredallowRestore/allowPurgekeys, so adding this gate entry today would go red on main by design; it is green only after the removal merges.Blocked-by: #6595
On unlock, re-verify on the merged ref (the entry may even have been added by that PR). The emit-side vs under-coverage question in the card body is implementer discretion within the gate's emit-side charter; surfacing under-coverage would be a separate card.
findinglabel removed (grading done).This comment is from the triage seat Routine and is not a claim.
Generated by Claude Code
huangyiirene commented
on Aug 27, 2026 CollaboratorMore actionsUnlock scan: blocker released, card returned to the queue.
pm:blocked→pm:queue;domain:devxand type Task unchanged.#6595 closed 2026-08-27T13:31:09Z — 13 minutes after the blocking comment above was written. The block was correct while it stood:
ObjectPermstill authored the retiredallowRestore/allowPurgekeys, so a gate entry added then would have gone red onmainby design.Merged-ref re-verification — done, and it was not a formality. The unlock note flagged that the entry "may even have been added by that PR". Read from
scripts/check-designer-field-key-parity.mjsonmain:PAYLOAD_SHAPESstill carries exactly six entries —FieldMetadataPayload,ServerFieldSchema,DesignerFieldDefinition,ObjectMetadataPayload,ServerObjectSchema,ObjectDefinition. ⇒ No permission authoring shape. The card's work is not done.ORACLESis still["FieldSchema", "ObjectSchema"]— no permission oracle.
⇒ The card stands as filed and is dispatchable now.
⚠️ One correction to the original grading's scope estimate, so it is not under-priced at dispatch. That comment described the work as "naming a seventh shape … within its existing charter". Adding the permission surface needs a third and fourth oracle (ObjectPermissionSchema/PermissionSetSchema), not just a seventh row — every existing shape is judged byFieldSchemaorObjectSchema. The good news is that the machinery already generalises:schemaAcceptSet(exportName)resolves any named export off@objectstack/spec/data,oracleOf()readsshape.schemawith a default, andanalyze()builds its accept sets from[...new Set(shapes.map(oracleOf))]. So it is still mechanical and still Task — but the diff touchesORACLES(report ordering) as well asPAYLOAD_SHAPES, and the self-test asserts a non-trivial key set for every shape inPAYLOAD_SHAPES, so a new entry that cannot be walked fails loudly rather than silently. That is the desired behaviour; just budget for it.Two constraints from the gate's own header that bear directly on this card (⛔ do not rediscover them the hard way):
- Extraction failure is an ERROR, never a pass. If a permission shape's interface cannot be found or the schema export does not resolve, the gate must throw
ExtractionError. ⛔ Do not add a permission entry with any silent-skip fallback — "a wrongly resolved or loosened schema produces a CONFIDENT GREEN over everything" is the failure this file exists to prevent. - The accept set is read from the installed schema, never listed in the file. ⛔ No hardcoded permission key list, and resolution stays through the dynamic
import()of@objectstack/spec/datafor the dual-package provenance reason the header spells out.
⭐ Also worth noting from the same read:
KNOWN_UNPARSEABLE_KEYSis now empty —formula(#6043), field-levelsortOrder(#6045) and object-levelenabled(#6238) have all been resolved and their entries removed. The both-directions ratchet is doing its job, so a new permission entry lands on a clean ledger.The emit-side vs under-coverage question in the card body remains implementer discretion within the gate's emit-side charter; surfacing under-coverage stays a separate card.
Generated by Claude Code
Claim: PM loop round R40 (batch widened to 5 on the maintainer's instruction)
Session:session_01BGMDbrVa8JjZcCQ7DWYH1b
Branch:claude/issue-6606-permission-payload-shapes
Worktree:objectui-issue-6606
Domain:domain:devx
File surface:scripts/check-designer-field-key-parity.mjs(PAYLOAD_SHAPES, the oracle list, and whatever the self-test asserts per shape) and its test underscripts/__tests__/(stop on breach; explain in the report). ⛔ No edit topackages/app-shell/src/views/metadata-admin/permission-slice.ts— if the gate goes red on a key that shape emits, that is the gate doing its job and a finding for the ui lane, not a fix here.
Container & model: M,mode:subagent,model: opus. Tier is this seat's explicit decision (nodispatch-gates.mjsin objectui).
Clause-②: no
Serial constraints cleared: unblocked 2026-08-27 (#6595 merged; merged-ref re-verification recorded on this thread). Onorigin/main67dadd6:PAYLOAD_SHAPESat:198still carries no permission shape;ObjectPerm(permission-slice.ts:21) andPermissionSetDraft(:58) still exist. #6699 (pm:blocked) names the same script but is not in flight. No in-flight PR touches this script; batch siblings (#6640 · #6705 · #6291 · #6451) are disjoint by construction.
Generated by Claude Code
Claim (dev seat): starting implementation now.
Session:session_01BGMDbrVa8JjZcCQ7DWYH1b
Branch:claude/issue-6606-permission-payload-shapes(pushed empty at67dadd6)
Worktree:/home/user/objectui-issue-6606
File surface:scripts/check-designer-field-key-parity.mjsand its self-test underscripts/__tests__/. No edit topackages/app-shell/src/views/metadata-admin/permission-slice.ts— a red on a key that shape emits gets reported, not patched.I do not touch the assignee (PM owns it).
Generated by Claude Code
os-dev-report
{ "issue": 6606, "status": "done", "branch": "claude/issue-6606-permission-payload-shapes", "pr": "https://github.com/objectstack-ai/objectui/pull/7272", "premise_still_valid": true, "summary": "Added the two permission authoring shapes to PAYLOAD_SHAPES in scripts/check-designer-field-key-parity.mjs, judged by the installed spec's permission schemas. PM assumption (c) was correct and load-bearing: the permission schemas are NOT on @objectstack/spec/data (measured on 17.2.0, which exports neither there) but on @objectstack/spec/security, so the hardcoded single subpath became a declared per-oracle map ORACLE_SPECIFIERS, with ORACLES derived from it via Object.keys so the two cannot drift. An oracle the map does not carry throws ExtractionError rather than defaulting to /data — no silent-skip path was added, and no permission key list is hardcoded; resolution stays through the dynamic import(), and /security carries the identical dual-package export map so the header's provenance argument applies unchanged. The gate is GREEN on today's tree: every key both shapes declare is accepted by the schema that judges it, exactly as the filing card predicted once #6595 had landed. permission-slice.ts is untouched. The gate stays emit-side; a test now pins that it reports nothing about the keys ObjectPerm omits (#6605), so widening that charter has to argue for itself.", "entries_added": [ "PermissionSetDraft (packages/app-shell/src/views/metadata-admin/permission-slice.ts:58) -> PermissionSetSchema -> @objectstack/spec/security -> reach wire, writer PermissionMatrixEditor.doSave (client.save). 10 declared keys, all accepted; carries an index signature, reported as coverage note 2.", "ObjectPerm (packages/app-shell/src/views/metadata-admin/permission-slice.ts:21) -> ObjectPermissionSchema -> @objectstack/spec/security -> reach wire, writer PermissionMatrixEditor.doSave (objects[name] row). 7 declared keys, all accepted." ], "gate_verdict_before": "pnpm check:designer-field-key-parity on 67dadd6 (pre-edit worktree), exit 0: 'designer-field-key-parity: FieldSchema accepts 71 keys' / 'designer-field-key-parity: ObjectSchema accepts 42 keys' / six shapes listed / 'designer-field-key-parity: OK'. Two oracles, no permission shape.", "gate_verdict_after": "pnpm check:designer-field-key-parity on 18c3f61 (clean tree), exit 0: 'designer-field-key-parity: FieldSchema accepts 71 keys' / 'ObjectSchema accepts 42 keys' / 'PermissionSetSchema accepts 20 keys' / 'ObjectPermissionSchema accepts 12 keys'; new rows 'PermissionSetDraft 10 declared [wire] vs PermissionSetSchema (+ index signature — see coverage note 2)' and 'ObjectPerm 7 declared [wire] vs ObjectPermissionSchema'; final line 'designer-field-key-parity: OK'.", "ablation": "Fence-safe copy route (the route the dispatch sanctioned), run through the REAL PAYLOAD_SHAPES permission entries — real interface, real schema, real reach — pointed at a throwaway root holding a mutated COPY of permission-slice.ts, so the subject is the shipped entry and not a fixture look-alike. No build/dist leg is involved (the gate reads TypeScript source with the TS parser and imports the published spec package). Verbatim: 'fenced file at HEAD before: dd14740b589865816440e03bb652535ed8c978b8' / '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'. Both legs prove they landed on disk by counting the injected AND the original text (not by an editor's exit code); anchors were asserted to match exactly once, so a no-op mutation would have aborted rather than read as a pass; the fenced file is proven byte-identical to its HEAD blob before and after.", "tests": "All runs on HEAD=18c3f61f0 with `git status --porcelain` empty (0 lines). UNION after the final commit: `pnpm exec vitest run scripts/__tests__/check-designer-field-key-parity.test.ts scripts/__tests__/zod-wrapper-keys.shared.test.ts --maxWorkers=2` -> exit 0, 'Test Files 2 passed (2)', 'Tests 52 passed (52)', duration 2.11s. Non-vacuity of the count is mechanical: it() count in the parity test goes 32 -> 45 (13 new) and 45 + 7 in zod-wrapper-keys.shared.test.ts is exactly the 52 reported, so no case was skipped. Those two files are the COMPLETE set naming this script (`git grep -l check-designer-field-key-parity -- 'scripts/__tests__/*'`). Other gates, each with exit code captured before any pipe: `pnpm type-check:scripts` exit 0, no diagnostics; `pnpm check:control-bytes` exit 0 'check-control-bytes: OK (scanned 5996 tracked text file(s); skipped 85 binary)'; `pnpm check:entry-guard` exit 0 'check:entry-guard: 58 scripts/ file(s) — no entry guard outside the baseline'; `pnpm check:esm-specifiers` exit 0 'Specifier leg: no un-ledgered package emits an extensionless relative specifier'; `pnpm lint:root` exit 0, 29 problems (0 errors, 29 warnings) — all 29 pre-existing in e2e/ and other scripts/__tests__ files, zero in either file I touched (grep of the log for 'designer-field-key-parity' returns nothing); `node scripts/check-changeset-presence.mjs` exit 0 'No source or published contract of a released package changed in this range, so no changeset is owed.' Also self-scanned both edited files and the PR body for control bytes: no hits.", "mcp_calls": "7", "open_questions": [], "out_of_scope_findings": [], "files_changed": [ "scripts/check-designer-field-key-parity.mjs (+125/-19 region: two PAYLOAD_SHAPES entries, ORACLE_SPECIFIERS map with ORACLES derived from it, schemaAcceptSet resolves per-oracle subpath and throws on an unmapped oracle, header updated for the third/fourth oracle and the emit-side charter)", "scripts/__tests__/check-designer-field-key-parity.test.ts (+206: new 'the permission oracles' describe with 13 cases; per-shape witness table replacing the literal `label` assertion; ledger oracle validation now reads ORACLES)" ], "deviations": [ "SUBPATH (PM assumption (c), confirmed): the permission schemas resolve from @objectstack/spec/security, not /data. The header's constraint is the provenance reasoning, not the subpath spelling, so the dynamic import() and the never-hardcode rule are kept and the subpath became a declared per-oracle map. Measured: `/data` exports neither permission schema; a test asserts that measurement rather than trusting it, so a future move back to /data fails loudly.", "ORACLES: the unlock comment described the diff as touching ORACLES for 'report ordering'. Read on main, ORACLES had no consumer at all — report order comes from shape order inside analyze(). It is now derived from ORACLE_SPECIFIERS and consumed by the self-test's ledger validation, which previously re-listed ['FieldSchema','ObjectSchema'] and would have gone stale.", "SELF-TEST (PM assumption (b), confirmed and preserved): the per-shape non-vacuity assertion required EVERY shape to declare `label`. An authorization row has none, so ObjectPerm would have failed it. Replaced with a per-shape witness key table (ObjectPerm -> allowRead, PermissionSetDraft -> objects) that every PAYLOAD_SHAPES entry must appear in — strictly stronger than the old form, since a new shape can no longer land unwitnessed.", "ABLATION ROUTE: used the copy route rather than a scratch commit on permission-slice.ts, so the fenced file was never written at all. Proven, not asserted: its blob hash equals its HEAD blob before and after the run.", "NARROWED, DECLARED: `pnpm check:node-esm-load` (full two-leg form) not run — its load leg builds every published package and exceeds this container's ~10 minute foreground cap (it was started and killed at the cap, exit 143). The narrowing is proven, not assumed: the population is defined by the gate itself (published type:module packages); the changeset-presence gate independently measured that 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. Its cheap specifier leg (`pnpm check:esm-specifiers`) ran green.", "LOCK: heavy runs went through scripts/pm/os-verify-lock.sh with slot 'dev-6606' (install, the first vitest, type-check:scripts, lint:root). The container queue was saturated by issue-6640's 8-shard dom run and returned exit 99 (queue-timeout, never acquired) twice; the kept slot was reused and the intervals were spent on lock-free work (PR body, derivation greps, push). The FINAL union vitest was then run UNLOCKED and this is declared: measured at 2.11s over two script test files, it is the same weight class as the `check:*` gates the lock's own banner explicitly excludes from its scope, and burning another 5-minute queue wait for a 2-second job would have starved the shard run for nothing.", "PR body: draft PR #7272 opened against main, first line `Fixes #6606`. Body read back in full after creation — only HTML-entity encoding of quotes and `>`; no truncation, no sanitizer damage." ] }
Generated by Claude Code
ACCEPT — PR #7272 (draft → ready + auto-merge once the gate jobs are green on its head)
Reviewer of record:
domain:devx@ objectui execution seat, PM sessionsession_01BGMDbrVa8JjZcCQ7DWYH1b, R40. Verified against GitHub, not against the report.Checklist
- PR shape: draft ✓ · base
main✓ · first body lineFixes #6606✓ (whole card delivered) · body scanned for closing keywords near other refs: metadata-admin permission matrix still authors the retiredallowRestore/allowPurgebits — spec now rejects them at publish #6595 / Permission matrix bulk buttons (R / CRUD / All / None) silently deleteallowExport,readScopeandwriteScopefrom the row #6605 / objectstack#12497 appear without any keyword ✓. - Changed files (from the PR): exactly two —
scripts/check-designer-field-key-parity.mjs(+110/−15) andscripts/__tests__/check-designer-field-key-parity.test.ts(+202/−4) — inside the claimed surface;permission-slice.tsuntouched, as fenced ✓. - Both header constraints kept, read from the diff: no silent-skip — an oracle absent from
ORACLE_SPECIFIERSthrowsExtractionError("no spec subpath is declared for the oracle…"); no hardcoded key list — accept sets still come off the installed schema through the dynamicimport(), now from the subpath the map declares. The PM's/dataassumption was falsified with a measurement (/dataexports neither permission schema;/securityexports both), and the test asserts that measurement rather than trusting it ✓. - Tests:
52 passed (52)across the two files that name the script (it()count 32 → 45, plus 7 = 52, so no case skipped); gate verdict lines before (67dadd6, two oracles, six shapes) and after (18c3f61, four oracles, eight shapes,OK) quoted ✓. Ablation run through the realPAYLOAD_SHAPESentries against a mutated copy of the fenced file; mutation and restore proven by text counts, fenced file's blob hashdd14740b…identical before and after ✓. - Changeset:
scripts/-only; presence gate's own verdict "no changeset is owed" ✓. - Declared narrowing: full
check:node-esm-loadkilled at the foreground cap; the cheap specifier leg green and the population argument (0 published source touched) holds; CI runs the farm ✓. - Emit-side charter preserved and now pinned by a test (the Permission matrix bulk buttons (R / CRUD / All / None) silently delete
allowExport,readScopeandwriteScopefrom the row #6605 under-coverage stays a separate card) ✓. - Report hygiene:
mcp_calls8, all writes plus one read-back; lock used for heavy runs, final 2 s union run unlocked and declared ✓.
Landing path: not governed (
scripts/**). Flip checkpoint armed for the gate jobs (Lint,Type Check) on head18c3f61f0; on green ⇒ ready + auto-merge into the queue. Card stayspm:dispatcheduntil MERGED.
Generated by Claude Code
- PR shape: draft ✓ · base
Found while removing the retired
allowRestore/allowPurgecolumns in objectui#6595. Recording only, deliberately unassigned — a coverage observation about the gate, not a defect on today's tree.The observation
scripts/check-designer-field-key-parity.mjsexists for exactly one failure class, in its own words: "a designer offers a control that writes a key the spec refuses BY NAME." It reads each statically declared payload shape inPAYLOAD_SHAPESand compares its key set against the strict zod schema that judges it.PAYLOAD_SHAPEScurrently names six shapes, and all six are the object/field designer path:FieldMetadataPayloadFieldSchemaServerFieldSchemaFieldSchemaDesignerFieldDefinitionFieldSchemaObjectMetadataPayloadObjectSchemaServerObjectSchemaObjectSchemaObjectDefinitionObjectSchemaThe permission authoring path has the same two ingredients the gate needs and is in none of them:
ObjectPerm/PermissionSetDraftinpackages/app-shell/src/views/metadata-admin/permission-slice.tsare statically declared payload shapes, andObjectPermissionSchema/PermissionSetSchemaare exported strict zod objects that refuse unknown keys by name (measured 2026-08-27 against@objectstack/spec17.2.0: an unknown key comes back asunrecognized_keys).Why it is worth recording
objectui#6595 is an instance of precisely the class this gate was built for — an authoring control writing keys the spec is retiring — and it reached this repo as a hand-written card from the upstream retirement (objectstack#12497), not from CI. Nothing here would have flagged it, because the surface it lives on is outside the gate's reach.
The same holds for the return direction: once the
@objectstack/specbump carrying the retirement lands, a permission set still carrying the removed keys is a body the schema refuses, and no gate in this repo would say so.Not a claim that the gate is broken
The gate's header is explicit that it covers a SUBSET, and states four ways a key can escape it. This is a seventh shape it does not yet name, not a hole it hides — filing so the extension is a decision someone makes rather than an omission nobody sees.
Consideration for whoever picks it up
ObjectPermis a hand-written subset of the spec'sObjectPermission(it omitsallowExport,readScope,writeScope— see objectui#6605). So a key-set comparison here would be one-sided in a useful way: the gate asks whether every key the shape can EMIT is accepted, and the omissions are a separate problem from the one this gate judges. Worth deciding deliberately whether the entry should also surface under-coverage, or stay strictly the emit-side check it is everywhere else.