Repository navigation
feat(app-shell): a ref-multi:permission widget picks declared permission sets by name (objectui#12126) - #12136
Conversation
…ssion sets by name (objectui#12126) The position form's `permissionSets` row is moving to `widget: 'ref-multi:permission'` (objectstack#22794, ADR-0131 D4). The metadata-admin widget registry had no such key, so the row would have rendered as the announced raw-JSON fallback. This registers the widget: a list of the stored names, each removable, flagged `(not found)` when the loaded catalog does not declare it, plus an add picker of the declared sets not yet picked. `WidgetContext` gains `permissionSets`, and `ResourceEditPage` loads it only for a form that declares the hint. Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD Co-authored-by: Claude <noreply@anthropic.com>
…he ref-multi:permission widget (objectui#12126) Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD 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: card objectui#12126 (body and both comments: the dispatch claim ① Derived judgments
② Semver levelChangeset ③ Boundary flags
Check-runs on the head, read at 2026-10-11T10:50Z: 42 runs, 0 failures. 37 completed success: Action Ref Convention; Build & E2E; Build Docs; Bundle Analysis; Changeset Bump Policy; Changeset Claim Re-read; Changeset Declaration; Changeset Fixed Group Check; Changeset Overwrite Report; Control Byte Scan; Doc Component Type Check; Doc Example Id Check; Doc Fence Language Check; Doc Snippet Type Check; Docs Route Eager Closure Check; Governed Surface Queue Guard; Inert vi.mock Specifier Check; Internal Docs Link Check; Line Citation Gate; Lint; Live E2E (informational); Pre-Install Import Graph Check; README Export Check; Shell Escape Residue Scan; Skill Eval Token Check; Skill Example Check; Skill Guide Path Check; Spec Main Shape Gate; Test (dist pins); Test (shard 1/8); Test (shard 2/8); Test (shard 5/8); Test (shard 6/8); Test (shard 7/8); Test (shard 8/8); Type Check; label. 3 completed skipped: dependabot; Test (coverage); Test (coverage shard, matrix placeholder). 2 still in progress at this reading: Test (shard 3/8); Test (shard 4/8). Those two are the derived-gate families this record does not re-run; the queue's green-check condition reads them when they complete, and this verdict is on the contract. Implemented-by: VERDICT: PASS |
Fixes #12126
Clause-②: yes
What this does
objectstack-ai/objectstack#22794 moves the position form's
permissionSetsrow fromtype: 'tags'towidget: 'ref-multi:permission'(ADR-0131 D4: a reference to a declared item is by machine name, resolved registry-first). That PR is held in draft until this one lands and objectstack's.objectui-shapin carries it. The metadata-admin widget registry had no such key, so the row would have turned into the announced raw-JSON fallback. This registers it, the same orderref:datasettook (objectui#11601, then objectstack#21714).widgets.tsx: oneRefMultiWidget, parameterised by the registry binding it reads (the catalog offWidgetContextplus the add control's copy), registered for exactly one key,ref-multi:permission. No speculative keys for other registry types.(not found). The flag is only made once the catalog has answered.RefDatasetWidget: FAILED shows the sharedPickerLoadFailurenotice beside a text box that still adds a typed name; LOADING disables the add box; idle (no host feeds a catalog) or a completed load that found nothing gives a text box that adds typed names, never the raw-JSON face; LOADED gives the picker.'group', for the measured reasonfield-multi/action-multicarry (objectui#4871): the add control is gated behind!readOnly, so no single labelable element carries the field in both states. The host label names the list.WidgetContextgains one optional member,permissionSets(aLoadStateOFRegistryItemOption[], whereRegistryItemOptionis{ name, label? }).ResourceEditPage.tsxloadsclient.list('permission')throughusePickerLoadand feeds it aswidgetContext.permissionSets. It is the whole registry list, no package scope: a position names permission sets from the environment catalog, platform-shipped sets included (ADR-0131 D3). The request is gated by a newformDeclaresWidget(form, 'ref-multi:permission')(sections, legacygroups, and nested repeater/composite rows), so no other editor sends it.i18n.ts: three strings, en and zh —engine.form.addPermissionSetPlain(the add control's accessible name),engine.form.addPermissionSet,engine.form.allPermissionSetsAdded.Before and after, measured
BEFORE: BASE
1071393source (the three source files checked out at BASE, trap-restored from HEAD, restore proven by blob hash equal to HEAD and an emptygit diff HEAD), the new pins run on it: 19 of 20 cases red. The first case fails on its own message "the announced raw-JSON fallback is on screen": the row renders the JSON textarea under "widget ref-multi:permission — falling back to JSON until a custom renderer is registered.". The one green case is the control (a tag-box form sends no permission request), true on BASE and HEAD alike.AFTER (HEAD
56761dd): 20 of 20 green. The row renders the named list and the add picker; no textarea, no fallback note.Ablations, through
ablation-replace.mjs(anchor hit counted, mutation proven on disk by blob change, restore proven by blob hash equal to HEAD and an emptygit diff HEAD):const notFound = false;): 3 red — the flag pin, the read-only flag pin, and the ResourceEditPage feed pin.true || formDeclaresWidget(): 1 red — "the tag-box row reads no catalog, so the permission list is never asked for".Clause-②: the published type widens (H6)
Read on the built
packages/app-shell/dist, rebuilt from this branch:index.d.tsre-exportsSchemaForm;SchemaForm.d.tsdeclareswidgetContext?: WidgetContext, imported fromwidgets.js;widgets.d.tsnow declarespermissionSets?: LoadStateOFRegistryItemOption[], beside the positive controldatasets?: LoadStateOFDatasetCatalogEntry[]in the same interface.A scratch probe compiled against that
dist(not the source),tsc --strict: a loaded catalog is accepted onWidgetContextand onSchemaFormProps['widgetContext']; a bare list of names is refused; a misspelledpermissionSetis refused (the dark control, so the green lines are a live excess-property check). Both refusals are@ts-expect-error, tsc exit 0. Reverse leg: the same probe with a catalog row key the new type refuses (title): tsc exit 2, TS2353 onRegistryItemOption.formDeclaresWidgetis exported fromwidgets.tsxonly; the package barrel does not name it (zero hits indist/index.d.ts). Changeset.changeset/12126-ref-multi-permission-widget.md:@object-ui/app-shellminor, additive.Zone 2 hypotheses, re-measured on BASE
1071393WIDGETSandWIDGET_LABELLINGlive inwidgets.tsx;RefDatasetWidgetis the model for the arms and the(not found)flag.ResourceEditPage.PermissionMatrixEditorreadsclient.list('permission', {})into its own component state and swallows a failure (an empty.catch): not aLoadState, not exported, and in a file outside this card.catalog-scope.tsandcatalog-activation.tsexport no permission-set name loader.StudioDesignSurfacereads a package-scoped list, the wrong population for a position. objectui#12089 leftResourceEditPage'swidgetContextconstruction intact; the new member is added there.permissionon both sides.CASESmust name everyWIDGETSkey andWIDGET_LABELLINGmust equal the key set. Two rows added (loaded catalog, and no catalog). The widget is'group', soSchemaForm.controlWidgetFailureArmNaming-9931.test.tsx(population: the'control'entries) is unchanged and green.metadata-admin/i18n.ts, en and zh; the published packs are untouched.Tests and gates (all at HEAD
56761dd)SchemaForm.refMultiPermission-12126.test.tsx(16 cases: section and repeater grid/card picks, the stored list, unknown name flagged, removal, read-only, FAILED / LOADING / no-catalog arms,formDeclaresWidget),ResourceEditPage.permissionSetsFeed-12126.test.tsx(4 cases on the real page with a stubbed client: the options and the flag, one unscoped request, a failed list reaching the picker as a failure, and the no-request control). Parity pinSchemaForm.widgetLabelling.test.tsxgains the twoCASESrows.pnpm exec vitest run packages/app-shell/src/views/metadata-admin/in 4 shards: 471 files passed; 5344 tests passed, 1 skipped.pnpm --filter @object-ui/app-shell type-check(echoedtsc --noEmit && tsc -p tsconfig.test.json): exit 0, afterturbo run build --filter=@object-ui/app-shell...(29 of 29 tasks).--listFilesOnlyshows the three test files in the test program.pnpm exec vitest run scripts/__tests__/in 3 shards: 179 files passed, 2 skipped. Ratchetscolumn-identity.ratchet.test.tsandone-authority-per-exported-name-6273.test.ts: 2 files, 18 tests passed.check:i18n-designer-parity,check:i18n-keys,check:control-bytes,check:new-line-citations(0 new),check:changeset-claims,check:pending-changeset-literals,check:test-path-roots,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:phantom-deps,check:designer-field-key-parity,check:component-surface-parity(report-only),check-changeset-presence.mjs,check-changeset-no-major.mjs;check-governed-queue-guard.mjs --teston the 7 paths: NOT GOVERNED.react-refresh/only-export-componentsonformDeclaresWidget, the same class aswidgetLabellingandcollectPageComponentIdsbeside it.Eager closure
The change reaches the Console's first load: both the
SchemaFormchunk and the metadata-admini18nchunk are in the eager closure (apps/console/dist/eager-closure.json). Twovite builds ofapps/console, HEAD and BASE source: eager gzip 3,250,219 to 3,250,896 bytes, +677 bytes gzipped (+3,064 raw);SchemaForm+619,i18n+51, the rest is 1 to 4 bytes of chunk-hash churn; the eager chunk count is unchanged.pnpm check:eager-closureat HEAD: exit 0, "Console eager closure is 3174.7 KB gzipped across 290 of 2474 chunks (budget: 3204.6 KB, headroom: 29.9 KB)".Acceptance notes
content/docsor the app-shell README lists the metadata-admin widget vocabulary, andref:datasetshipped none either, so there is no docs change.(not found), since the registry answered and declares none, and offers the typed-name box, asref:dataset's freeform arm does.SchemaFormandResourceEditPagerenders above cover the face, and the cloud box had no backend running for this card..objectui-shapin carries it. Nothing here edits objectstack.Generated by Claude Code