Skip to content

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

@os-sales

Found while removing the retired allowRestore / allowPurge columns 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.mjs exists 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 in PAYLOAD_SHAPES and compares its key set against the strict zod schema that judges it.

PAYLOAD_SHAPES currently names six shapes, and all six are the object/field designer path:

shape oracle
FieldMetadataPayload FieldSchema
ServerFieldSchema FieldSchema
DesignerFieldDefinition FieldSchema
ObjectMetadataPayload ObjectSchema
ServerObjectSchema ObjectSchema
ObjectDefinition ObjectSchema

The permission authoring path has the same two ingredients the gate needs and is in none of them: ObjectPerm / PermissionSetDraft in packages/app-shell/src/views/metadata-admin/permission-slice.ts are statically declared payload shapes, and ObjectPermissionSchema / PermissionSetSchema are exported strict zod objects that refuse unknown keys by name (measured 2026-08-27 against @objectstack/spec 17.2.0: an unknown key comes back as unrecognized_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/spec bump 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

ObjectPerm is a hand-written subset of the spec's ObjectPermission (it omits allowExport, 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.

Activity

  1. added
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    and removed on Aug 27, 2026
  2. added theissue type on Aug 27, 2026
  3. os-litant commented on Aug 27, 2026

    @os-litant
    Collaborator

    Triage (first-touch grading of this finding): promoted — a scoped gate extension with a named landing point (add the permission authoring shapes to PAYLOAD_SHAPES in scripts/check-designer-field-key-parity.mjs, oracle ObjectPermissionSchema / 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, ObjectPerm still authors the retired allowRestore / allowPurge keys, 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. finding label removed (grading done).

    This comment is from the triage seat Routine and is not a claim.


    Generated by Claude Code

  4. huangyiirene commented on Aug 27, 2026

    @huangyiirene
    Collaborator

    Unlock scan: blocker released, card returned to the queue. pm:blocked → pm:queue; domain:devx and 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: ObjectPerm still authored the retired allowRestore / allowPurge keys, so a gate entry added then would have gone red on main by 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.mjs on main:

    • PAYLOAD_SHAPES still carries exactly six entries — FieldMetadataPayload, ServerFieldSchema, DesignerFieldDefinition, ObjectMetadataPayload, ServerObjectSchema, ObjectDefinition. ⇒ No permission authoring shape. The card's work is not done.
    • ORACLES is 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 by FieldSchema or ObjectSchema. The good news is that the machinery already generalises: schemaAcceptSet(exportName) resolves any named export off @objectstack/spec/data, oracleOf() reads shape.schema with a default, and analyze() builds its accept sets from [...new Set(shapes.map(oracleOf))]. So it is still mechanical and still Task — but the diff touches ORACLES (report ordering) as well as PAYLOAD_SHAPES, and the self-test asserts a non-trivial key set for every shape in PAYLOAD_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/data for the dual-package provenance reason the header spells out.

    ⭐ Also worth noting from the same read: KNOWN_UNPARSEABLE_KEYS is now empty — formula (#6043), field-level sortOrder (#6045) and object-level enabled (#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

  5. self-assigned this
    on Sep 2, 2026
  6. yinlianghui commented on Sep 2, 2026

    @yinlianghui
    Collaborator

    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 under scripts/__tests__/ (stop on breach; explain in the report). ⛔ No edit to packages/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 (no dispatch-gates.mjs in objectui).
    Clause-②: no
    Serial constraints cleared: unblocked 2026-08-27 (#6595 merged; merged-ref re-verification recorded on this thread). On origin/main 67dadd6: PAYLOAD_SHAPES at :198 still carries no permission shape; ObjectPerm (permission-slice.ts:21) and PermissionSetDraft (: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

  7. yinlianghui commented on Sep 2, 2026

    @yinlianghui
    Collaborator

    Claim (dev seat): starting implementation now.
    Session: session_01BGMDbrVa8JjZcCQ7DWYH1b
    Branch: claude/issue-6606-permission-payload-shapes (pushed empty at 67dadd6)
    Worktree: /home/user/objectui-issue-6606
    File surface: scripts/check-designer-field-key-parity.mjs and its self-test under scripts/__tests__/. No edit to packages/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

  8. yinlianghui commented on Sep 2, 2026

    @yinlianghui
    Collaborator

    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

  9. yinlianghui commented on Sep 2, 2026

    @yinlianghui
    Collaborator

    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 session session_01BGMDbrVa8JjZcCQ7DWYH1b, R40. Verified against GitHub, not against the report.

    Checklist

    • PR shape: draft ✓ · base main ✓ · first body line Fixes #6606 ✓ (whole card delivered) · body scanned for closing keywords near other refs: metadata-admin permission matrix still authors the retired allowRestore / allowPurge bits — spec now rejects them at publish #6595 / Permission matrix bulk buttons (R / CRUD / All / None) silently delete allowExport, readScope and writeScope from 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) and scripts/__tests__/check-designer-field-key-parity.test.ts (+202/−4) — inside the claimed surface; permission-slice.ts untouched, as fenced ✓.
    • Both header constraints kept, read from the diff: no silent-skip — an oracle absent from ORACLE_SPECIFIERS throws ExtractionError ("no spec subpath is declared for the oracle…"); no hardcoded key list — accept sets still come off the installed schema through the dynamic import(), now from the subpath the map declares. The PM's /data assumption was falsified with a measurement (/data exports neither permission schema; /security exports 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 real PAYLOAD_SHAPES entries against a mutated copy of the fenced file; mutation and restore proven by text counts, fenced file's blob hash dd14740b… identical before and after ✓.
    • Changeset: scripts/-only; presence gate's own verdict "no changeset is owed" ✓.
    • Declared narrowing: full check:node-esm-load killed 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, readScope and writeScope from the row #6605 under-coverage stays a separate card) ✓.
    • Report hygiene: mcp_calls 8, 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 head 18c3f61f0; on green ⇒ ready + auto-merge into the queue. Card stays pm:dispatched until MERGED.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions