Repository navigation
feat(types): NamedListView mirrors spec 18's list-view requiredPermissions — no client gate - #12360
Conversation
…ssions
The audience key `@objectstack/spec` 18 declares on the list-view shape
(objectstack#22611 ruling A) and the server enforces (objectstack PR
#22731). Typed by reference through ListViewSchema['requiredPermissions'].
No renderer reads it; no client-side gate is added.
The objectui#12030 PENDING pin in the objectui#7779 census is flipped: the
narrower-than-protocol set is empty again, and the member sits in a fifth,
server-enforced bucket rather than bucket one ("implementation owed").
Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
Co-authored-by: Claude <noreply@anthropic.com>
…fth bucket Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs read: card #12341 (body, triage 6162030734, claim 6163589922, os-dev-report 6164268813), PR #12360 (body, 4-file list, net diff against ① Derived judgmentsAccept-set and public-surface changes the diff implies, each judged:
② Semver level
Clause-②: yes The PR body and the claim both say ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #12341
Clause-②: yes
The objectui half of objectstack#22611 ruling A:
@object-ui/typesmirrors the list view's audience keyrequiredPermissionsfrom@objectstack/spec18. ⛔ No client-side gate is added. The server enforces the key (objectstack PR #22731) and the console surfaces its refusal (objectui#12112).What changed
NamedListViewdeclaresrequiredPermissions(packages/types/src/objectql.ts), typed by reference asListViewSchema['requiredPermissions']. That member is the spec slot, taken byspecFieldsExceptin the zod mirror, so the type isstring[] | undefined, the same as the spec'sListView['requiredPermissions'].list-viewnode (ListViewSchema) takes every spec key it does not override, andlist-view-spec-parity.test.tsasserts that.ObjectViewSchema.listViewshas been the spec'sObjectListViewSchemarecord since objectui#7928. Only the hand-writtenNamedListViewlacked the key. objectui#12030 had pinned it there asPENDING_SERVER_ENFORCED_DECLARATION.object-view-unmirrored-keys-7779.test.ts). The "narrower than the protocol" set is now[]. A declared member that no renderer reads would otherwise land in bucket ①, whose emptiness means "implementation owed". A server-enforced gate owes the renderer nothing, so the census gets a fifth bucket,BUCKET_SERVER_ENFORCED_UNREAD = ['requiredPermissions']. The partition is now 0 + 8 + 11 + 2 + 1 = 22 unread. Declared members go from 64 to 65, read members stay at 43, and the loose-regex count goes from 73 to 74 (the gap stays at 9). Bucket ① asserts that it holds exactly the server-enforced bucket and nothing else.list-view-required-permissions-12341.test.ts), accept and refuse on every face. Type-level: the member on all three faces equals the spec'sstring[] | undefined. OnNamedListView, a bare string and a number list are each a@ts-expect-error. Runtime: thelist-viewmirror accepts and keeps a capability list, and refuses a string, a number list, an object and a boolean at the key, after a base-only parse control.ObjectViewSchema.listViewsaccepts the key and refuses a bare string atlistViews.legal_desk.requiredPermissions..changeset/12341-list-view-required-permissions.md:@object-ui/typesminor. The published accept set widens.Widening, measured on the built declaration closure
pnpm --filter @object-ui/types build, then readpackages/types/dist/objectql.d.ts:export interface NamedListViewgains exactly one member,requiredPermissions?: ListViewSchema['requiredPermissions'];(dist line 291 of the interface block). A probe compiled againstdist/indexwithtsc --strict(exit 0) checks three things: the member equalsstring[] | undefined,{ label, requiredPermissions: ['a.b'] }is accepted, andrequiredPermissions: 'a.b'is refused. Nothing else in the published surface moved.ListViewSchemaandObjectViewSchema['listViews']already carried the key.Reverse verification (a one-time proof, no permanent file)
The fix was committed first (4f01c04). Then
ablation-replace.mjs --deleteremoved the declaration line (anchor 1 to 0, blob 76eef18c1220 to 0270a098714c), with this result:tsc -p tsconfig.test.jsonwent red with 3 errors in the new test file:Property 'requiredPermissions' does not exist on type 'NamedListView'.git diff HEADis empty.The one measurement the card asks for: the D4 viewer
Answer: yes, the console offers such a view. No, opening it does not answer
403, so objectui#12112's surface is never reached./meta/objectserves an object WHOLE to a caller ADR-0106 D4 exempts. See objectstackpackages/rest/src/meta-item-read-gate.ts:1657on origin/main b5b1bf72:if (!sysPerms || isObjectSchemaMaskExempt(ctx)) return serve(document);. Its stated reason is that a pruned load saved back would delete the withheld list views. Every other caller getsfilterObjectListViewsForUser(lines 768-772).packages/app-shell/src/views/ObjectView.tsx:2335-2336builds the switcher tabs fromobjectDef.listViews(buildViewTabs({ definedViews: objectDef.listViews … })). No audience check is applied there, and none should be.ObjectView.tsx:2441-2455,missedViewNamethenuseMetaItemRefusal('view', …)). A D4 viewer's gated view is in the list, so it resolves, renders and never asks. The refusal face (ObjectView.tsx:3844-3848,view-access-denied) is not reached. In any case the server has no by-name/meta/viewitem for an object's own list view that it could refuse.packages/app-shell/src/views/ObjectView.viewRefusal-12112.test.tsx:221-230, the case "D4 (measured): an object's own list view served to its editor is listed and opens — no client rule hides or refuses it". In that case the probe is not called and noview-access-deniedface appears.app: 'author-exempt'policy already makes. That is an objectstack decision, raised in the report'sopen_questions. Nothing in this PR touches it.Gates (worktree HEAD 9e6cb65)
pnpm --filter @object-ui/types build: exit 0.pnpm --filter @object-ui/types type-check(tsc + examples +tsconfig.test.json;--listFilescounts both touched test files in): exit 0.pnpm exec vitest run packages/types/: 378 files / 10262 tests passed.pnpm turbo run build --concurrency=2 --filter='@object-ui/plugin-view^...'(15/15) and thenpnpm --filter @object-ui/plugin-view type-check: exit 0. plugin-view is the one package whose tests importNamedListViewas a type. Other mentions (app-shell, data-objectstack, console) are in comments only.pnpm exec eslint --no-inline-configon the 3 touched.tsfiles: 0 errors. There are 27 warnings, all pre-existingno-explicit-anylines outside the diff. The config is rooteslint.config.jsand is not type-aware (noparserOptions.projectorprojectService), so the diff cannot move any verdict on an untouched file. The repo-widepnpm lintis left to CI.check:phantom-deps,self-import,unreferenced-sources,doc-example-readers,handler-key-reads,side-effects-array,element-data-source-declaration,spec-symbols,action-forward-parity,designer-field-key-parity,icon-record-names,i18n-keys,i18n-drift,i18n-designer-parity: all exit 0.check-changeset-fixed.mjsandcheck-changeset-presence.mjs: exit 0.check:spec-floors: exit 1, the known failure on main ("The scan collapsed: 0 package(s) declare @objectstack/spec …"). objectui#12334 carries it, and it is not touched here.check:doc-typeswas not run because no docs changed. objectui has nocheck:nul-bytesscript, so a control-byte grep over the 4 touched files was run instead: 0 hits.Acceptance notes
NamedListViewdocblock still says its fate "follows objectui#7924", and this PR does not change that..changeset/object-view-unmirrored-keys-7779.md, which no longer exists (it was consumed at release). Only its member count was corrected here.The session for this work is
https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB.Generated by Claude Code