Skip to content

feat(app-shell): a ref-multi:permission widget picks declared permission sets by name (objectui#12126) - #12136

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-12126-ref-multi-permission-widget
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-12126-ref-multi-permission-widget

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12126

Clause-②: yes

What this does

objectstack-ai/objectstack#22794 moves the position form's permissionSets row from type: 'tags' to widget: '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-sha pin 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 order ref:dataset took (objectui#11601, then objectstack#21714).

  • widgets.tsx: one RefMultiWidget, parameterised by the registry binding it reads (the catalog off WidgetContext plus the add control's copy), registered for exactly one key, ref-multi:permission. No speculative keys for other registry types.
    • It lists the stored names, each with a remove button, the catalog's label beside the machine name when it has one. An add picker offers the declared sets not yet picked; a pick appends that set's NAME. The stored value stays a list of names.
    • A stored name the LOADED catalog does not declare stays listed, flagged (not found). The flag is only made once the catalog has answered.
    • The four catalog arms follow RefDatasetWidget: FAILED shows the shared PickerLoadFailure notice 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.
    • Labelling 'group', for the measured reason field-multi / action-multi carry (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.
  • WidgetContext gains one optional member, permissionSets (a LoadState OF RegistryItemOption[], where RegistryItemOption is { name, label? }).
  • ResourceEditPage.tsx loads client.list('permission') through usePickerLoad and feeds it as widgetContext.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 new formDeclaresWidget(form, 'ref-multi:permission') (sections, legacy groups, 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 1071393 source (the three source files checked out at BASE, trap-restored from HEAD, restore proven by blob hash equal to HEAD and an empty git 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 empty git diff HEAD):

  • A1, the not-found flag never set (const notFound = false;): 3 red — the flag pin, the read-only flag pin, and the ResourceEditPage feed pin.
  • A2, the catalog request sent for every form (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.ts re-exports SchemaForm; SchemaForm.d.ts declares widgetContext?: WidgetContext, imported from widgets.js; widgets.d.ts now declares permissionSets?: LoadState OF RegistryItemOption[], beside the positive control datasets?: LoadState OF DatasetCatalogEntry[] in the same interface.

A scratch probe compiled against that dist (not the source), tsc --strict: a loaded catalog is accepted on WidgetContext and on SchemaFormProps['widgetContext']; a bare list of names is refused; a misspelled permissionSet is 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 on RegistryItemOption.

formDeclaresWidget is exported from widgets.tsx only; the package barrel does not name it (zero hits in dist/index.d.ts). Changeset .changeset/12126-ref-multi-permission-widget.md: @object-ui/app-shell minor, additive.

Zone 2 hypotheses, re-measured on BASE 1071393

  • H1 confirmed: WIDGETS and WIDGET_LABELLING live in widgets.tsx; RefDatasetWidget is the model for the arms and the (not found) flag.
  • H2: no existing loader fits, so the loader is new, one fetch, in ResourceEditPage. PermissionMatrixEditor reads client.list('permission', {}) into its own component state and swallows a failure (an empty .catch): not a LoadState, not exported, and in a file outside this card. catalog-scope.ts and catalog-activation.ts export no permission-set name loader. StudioDesignSurface reads a package-scoped list, the wrong population for a position. objectui#12089 left ResourceEditPage's widgetContext construction intact; the new member is added there.
  • H3 confirmed: the registry type is permission on both sides.
  • H4 confirmed: CASES must name every WIDGETS key and WIDGET_LABELLING must equal the key set. Two rows added (loaded catalog, and no catalog). The widget is 'group', so SchemaForm.controlWidgetFailureArmNaming-9931.test.tsx (population: the 'control' entries) is unchanged and green.
  • H5 confirmed: the strings live in metadata-admin/i18n.ts, en and zh; the published packs are untouched.
  • H6 confirmed, above.

Tests and gates (all at HEAD 56761dd)

  • New pins: 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 pin SchemaForm.widgetLabelling.test.tsx gains the two CASES rows.
  • 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 (echoed tsc --noEmit && tsc -p tsconfig.test.json): exit 0, after turbo run build --filter=@object-ui/app-shell... (29 of 29 tasks). --listFilesOnly shows the three test files in the test program.
  • Root suite pnpm exec vitest run scripts/__tests__/ in 3 shards: 179 files passed, 2 skipped. Ratchets column-identity.ratchet.test.ts and one-authority-per-exported-name-6273.test.ts: 2 files, 18 tests passed.
  • Gates, each exit 0: 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 --test on the 7 paths: NOT GOVERNED.
  • eslint as the package lint script runs it, on the 6 touched source and test files: 0 errors. One new warning, react-refresh/only-export-components on formDeclaresWidget, the same class as widgetLabelling and collectPageComponentIds beside it.

Eager closure

The change reaches the Console's first load: both the SchemaForm chunk and the metadata-admin i18n chunk are in the eager closure (apps/console/dist/eager-closure.json). Two vite builds of apps/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-closure at 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

  • Docs (AGENTS.md Add automated testing infrastructure and CI/CD workflows #2): no page in content/docs or the app-shell README lists the metadata-admin widget vocabulary, and ref:dataset shipped none either, so there is no docs change.
  • A completed but empty catalog flags every stored name (not found), since the registry answered and declares none, and offers the typed-name box, as ref:dataset's freeform arm does.
  • Not driven in a browser: NOT MEASURED, reason: the real SchemaForm and ResourceEditPage renders above cover the face, and the cloud box had no backend running for this card.
  • objectstack#22794 lands after this PR, once objectstack's .objectui-sha pin carries it. Nothing here edits objectstack.

Generated by Claude Code

…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>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 290 chunks) 3174.9 KB 3204.6 KB
Main entry chunk (gzip) 74.2 KB 350 KB
Entry file index-H06RF6zR.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 20.05KB 7.41KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 587.00KB 141.35KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 275.07KB 70.00KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 54.31KB 15.86KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.52KB 47.99KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.53KB 49.80KB
plugin-detail (index.js) 249.44KB 65.79KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.80KB 46.04KB
plugin-gantt (index.js) 179.37KB 45.17KB
plugin-grid (index.js) 255.15KB 70.92KB
plugin-kanban (index.js) 53.23KB 16.71KB
plugin-list (index.js) 122.77KB 31.19KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 92.11KB 23.27KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 56761dd213f9da4f5c24bc0950af4b3c99bda1b5
Local-runs: none

Inputs: card objectui#12126 (body and both comments: the dispatch claim 6106885644, the dev report 6108144870); PR #12136 (body, file list, the net diff against main at merge base 1071393: 7 files, +763 / -3, draft, head repo is the base repo); the head's check-runs; file contents at the head for @object-ui/app-shell's package.json exports, src/index.ts, SchemaForm.tsx, widgets.tsx, loadState.ts, form-spec.ts, ResourceEditPage.tsx, i18n.ts, PermissionMatrixEditor.tsx, useMetadata.ts and the two parity pins; the spec side on objectstack main (identity/position.form.ts, identity/position.zod.ts) with objectstack#22794's diff; ADR-0131 D3/D4.

① Derived judgments

  1. WIDGETS gains 'ref-multi:permission', so RegisteredWidgetKey widens by one literal and resolveFieldFace answers registered for the hint where it answered raw-json with the "falling back to JSON" note. RIGHT: it is the row objectstack#22794 declares (widget: 'ref-multi:permission', no type; inferWidget takes the explicit widget first), and the registry type after the colon is permission on both sides (spec getMetadataTypeSchema('permission') is PermissionSetSchema, pinned by 22794's own test; objectui registerMetadataResource({ type: 'permission' })). Exactly one key is registered, no speculative siblings; KNOWN_PASSTHROUGH_WIDGETS is untouched.
  2. WIDGET_LABELLING['ref-multi:permission'] is 'group'. RIGHT: the add control is gated behind !readOnly, so no single labelable element carries the field in both states, the measured field-multi / action-multi reason; the widget answers the IDREF on its role="group" container in both states (pinned). The 9931 control-arm population derives from the table itself, so it is unchanged by construction.
  3. Public surface: WidgetContext gains permissionSets?: LoadState OF RegistryItemOption[], and widgets.tsx exports the new RegistryItemOption (name, optional label). The barrel names only SchemaForm and MetadataResourceEditPage; the member reaches consumers through SchemaFormProps['widgetContext'] in the d.ts chain, as the dev's H6 read on dist says. An optional member whose absence reads NOT_ASKED: additive, RIGHT. formDeclaresWidget is exported from widgets.tsx only and not from the barrel: no public surface. No in-repo consumer passes widgetContext outside app-shell at the merge base.
  4. The widget's value accept-set: a non-array reads as an empty list; non-string items are filtered and are dropped on the next write; the typed-name box splits on comma or newline, trims, dedupes, and does not validate snake_case (the host's schema issues do, as for string-tags); a pick appends that set's NAME; remove writes the list without it; the stored value stays a list of names. RIGHT against PositionSchema.permissionSets (z.array(SnakeCaseIdentifierSchema).optional()); the widget narrows only on an author's act.
  5. The (not found) flag is a claim about the registry's answer: made only on a LOADED catalog (a completed load that found nothing included), never on FAILED, LOADING or idle. RIGHT under ADR-0131 D4 (a name that resolves nowhere is the registry's verdict); it matches RefDatasetWidget's flagged-value arm, and the acceptance note states the empty-catalog consequence.
  6. The four arms: FAILED → the shared PickerLoadFailure beside an enabled typed-name box; LOADING → the box disabled under the loading placeholder; idle or completed-empty → the typed-name box; LOADED → a Select offering only the unpicked names, disabled once none remain. RIGHT, the RefDatasetWidget shape; never the raw-JSON face, pinned positively (no fallback notice, no textarea).
  7. ResourceEditPage: one client.list('permission') through usePickerLoad (the shared loader: a failure is the error arm, never an empty list), rows mapped to { name, label }, nameless rows dropped, sorted by name, fed as widgetContext.permissionSets; the whole registry, no package scope. RIGHT: ADR-0131 D3 gives the catalog one environment-level home, and PermissionMatrixEditor reads the same list('permission', {}). The gate formDeclaresWidget(createMode && config.createSchema ? undefined : entry?.form, 'ref-multi:permission') is the very expression both SchemaForm render sites pass as form= (the plain layout and the designer's inspector fallback; position has a preview registered and no inspector, so its fields render through that SchemaForm with widgetContext). RIGHT; the no-request control is pinned on the real page.
    Narrowness noted, not a defect for this card: formDeclaresWidget walks sections[].fields, legacy groups[].fields and nested fields; it does not follow a { group } section reference, which SchemaForm resolves through resolveSectionGroupReferences against an object definition. The position form is plain sections, so gate and render agree on every form /meta/types serves today; a metadata-admin form that one day declares the widget inside an object field-group reference would render the typed-name arm without a request. Follow-up territory.
  8. i18n: three new keys in EN and ZH (addPermissionSetPlain, addPermissionSet, allPermissionSetsAdded), phrased as the addField* siblings; every reused key (notFound, removeNamed, loadingOptions, tagsPlaceholder, optionsLoadFailedTitle, fallbackJson) exists in both locales at the head. RIGHT.
  9. Parity pins: the two CASES rows (loaded catalog, no catalog) keep probes every registered key and the WIDGET_LABELLING equals WIDGETS key-set assertion true. RIGHT. The new pins fail positively on BASE (the fallback notice is the first failure message in the dev's before leg) and the two ablations each turn their own pins red.
  10. Docs: no page lists the metadata-admin widget vocabulary at the merge base (ref:dataset is named only by its own changeset), so no docs change. RIGHT.
  11. Nit, not a contract matter: li key={name} collides on a stored list that already holds a duplicate name (a React key warning; add dedupes authored picks).

② Semver level

Changeset .changeset/12126-ref-multi-permission-widget.md: '@object-ui/app-shell': minor. What the diff publishes: one new widget key SchemaForm honours (a ref-multi:permission field renders the picker where it rendered the JSON fallback), one new optional WidgetContext member reaching SchemaFormProps['widgetContext'], one new type RegistryItemOption, three i18n keys. Nothing removed, renamed or narrowed; no major, as objectui's fixed group requires. Clause-②: yes on the PR body and on the dispatch claim; yes takes at least minor and no (narrowing) arm applies, so minor is RIGHT. The changeset body states the behaviour change and the additive widening in consumer terms and carries no model name. The changeset check-runs on the head (Bump Policy, Declaration, Claim Re-read, Fixed Group Check, Overwrite Report) read success.

③ Boundary flags

  • open_questions: none declared; none found.
  • Deviation 1 (a heap raise on the HEAD vite build leg): process only; the eager-closure reading comes from check:eager-closure at HEAD and Bundle Analysis reads success. Answered: no bearing on the diff.
  • Deviation 2 (model-free commit trailers and the session-URL PR footer per objectui AGENTS.md): that is the rule in both repos' AGENTS.md; the harness reminder yields by its own precedence clause. Answered: correct, not a deviation.
  • Deviations 3 and 4 (shards moved to the background and waited on by pid; one verify-lock exit 99 re-run and not counted): process; the results were read from the logs. Answered.
  • Deviation 5 (eslint --no-inline-config is objectstack's lint form; objectui's package lint is eslint .): the Lint check-run on the head is the verdict and reads success. Escalated to the seat: the os-dev lane text's lint spelling does not hold in objectui; a standing-text correction, outside this PR.
  • Deviation 6 (the ResourceEditPage pin clicks Edit first because a form-only type opens in view mode): test-only, no product change; the pin still reads the flag and the picker on the real page. Answered.
  • out_of_scope_findings (PermissionMatrixEditor's client.list('permission', {}) swallows a failure with an empty .catch, so a failed load renders as an empty assignable allowlist, the objectui#5170 shape): outside this card's file surface, recorded in the PR body's H2, not reproduced. Escalated to the seat: file it as a defect card under Prime Directive 10 (dedupe words are in the report) or leave it noted; not a blocker here.
  • Not driven in a browser: declared NOT MEASURED with its reason; the real-page pins, Build & E2E and Live E2E (informational) on the head cover the face. Accepted.
  • Sequencing: objectstack#22794 stays draft until objectstack's .objectui-sha pin carries this head; nothing here edits objectstack. The ref:dataset order, right.
  • Governance: no governed path (Governed Surface Queue Guard reads success), head repo is the base repo, 766 changed lines, the PR is draft with no auto-merge armed.

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: claude/issue-12126-ref-multi-permission-widget
Reviewed-by: session_01TYgwmFK1q4KJ6Qq2WRLzsD

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 11, 2026 10:55
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 11, 2026 10:55
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 11, 2026
Merged via the queue into main with commit 4997995 Oct 11, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12126-ref-multi-permission-widget branch October 11, 2026 11:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants