Skip to content

metadata-admin: register a ref-multi:permission widget (multi-pick declared permission sets by name) so the position form's permissionSets row can bind the registry (objectstack#22682 item 3, ADR-0131 D4) #12126

Description

@objectstack-fleet

Filing gate: ④, the objectui layer of a cross-layer item. Its spec layer is objectstack-ai/objectstack#22682 item 3, ready as objectstack-ai/objectstack#22794 and held in draft until this lands. Acting reader: the objectui domain:ui seat. When it lands and objectstack's .objectui-sha pin carries it, objectstack's domain:spec seat lands objectstack#22794.
Dedupe: the semantic issue search "position form permissionSets permission set multi picker widget ref-multi metadata-admin" on objectstack-ai/objectstack-ai/objectui returned 1 hit, #5092 (closed, unrelated: duplicate SchemaForm ids).

What objectui needs

objectstack#22794 sets the position form's permissionSets row (packages/spec/src/identity/position.form.ts) to widget: 'ref-multi:permission', where today it is type: 'tags'. This is ADR-0131 D4: references to declared items are by machine name, resolved registry-first, so the designer should offer the declared permission sets rather than free text.

objectui registers no such widget today. Measured at origin/main 206505c and at objectstack's pin 20c6d351ad74, which read the same at every line below:

  • inferWidget returns an explicit widget first (SchemaForm.tsx:447).
  • resolveFieldFace sends an unregistered hint to { kind: 'raw-json', hint } (:870), rendered at :2223 as a JSON textarea under "widget ref-multi:permission — falling back to JSON until a custom renderer is registered."
  • So landing the spec first would turn today's tag box into a JSON box. That is why this lands first, the same order ref:dataset took (objectui#11601, then objectstack#21714).

Where (measured by the objectstack dev, report objectstack 6106502193)

  • packages/app-shell/src/views/metadata-admin/widgets.tsx:
    • a WIDGETS entry (:3033) and a WIDGET_LABELLING entry (:3142);
    • a WidgetContext catalog member for the permission-set names;
    • the widget itself. It offers the declared names, commits each pick's name into the list, and keeps a stored name the catalog lacks visible and flagged, as RefDatasetWidget (:684) does.
    • The key's grammar: the ref: family's list form, with the arity in the prefix and the exact registry type after the colon. The vocabulary already spells multi-value pickers as their own keys (field-ref / field-multi, select / multiselect, action-multi), and the single ref: widgets read String(value) and write one string.
  • packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx: feed the permission list into widgetContext. The position form arrives as entry?.form from /meta/types (:456, :457, :2943).
  • SchemaForm.widgetLabelling.test.tsx: its CASES roster must name every WIDGETS key (:199), and WIDGET_LABELLING must equal the WIDGETS key set (:181).
  • __tests__/SchemaForm.controlWidgetFailureArmNaming-9931.test.tsx, if the widget is declared 'control'.
  • i18n.ts: the placeholder and failure strings (en and zh).
  • The registry type is permission on both sides: spec MetadataTypeSchema, where getMetadataTypeSchema('permission') is PermissionSetSchema, and objectui registerMetadataResource({ type: 'permission' }) (PermissionMatrixEditor.tsx:28).

Done when

  • The position form's permissionSets row, given widget: 'ref-multi:permission', renders a multi-picker of declared permission-set names; the stored value stays a list of names.
  • A stored name not in the catalog stays visible and flagged.
  • The widget-labelling parities pass.
  • objectstack's .objectui-sha pin is then bumped past it, so objectstack#22794 can land.

Activity

  1. added
    enhancementNew feature or request
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    area:studioChanging a running app without code — authoring, publish, docs and the portal
    and removed on Oct 11, 2026
  2. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01TYgwmFK1q4KJ6Qq2WRLzsD
    Account: zhuangjianguo
    Branch: claude/issue-12126-ref-multi-permission-widget
    Worktree: objectui-issue-12126
    Domain: domain:ui
    Seat: domain:ui#2
    File surface: packages/app-shell/src/views/metadata-admin/widgets.tsx (the ref-multi:permission widget, its WIDGETS and WIDGET_LABELLING entries, and a WidgetContext catalog member for the permission-set names); packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx (feeding that list into widgetContext only); packages/app-shell/src/views/metadata-admin/i18n.ts (the widget's placeholder and failure strings, en and zh); the widget-labelling parity pins SchemaForm.widgetLabelling.test.tsx and, if the widget is declared 'control', __tests__/SchemaForm.controlWidgetFailureArmNaming-9931.test.tsx; new pins beside them; the docs page that lists the form widget vocabulary, if one does; .changeset/12126-ref-multi-permission-widget.md (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default tier — dispatch-gates --tier --repo objectstack-ai/objectui: "no path-derived mandate"; the Clause-②: yes below owes a review at CONTRACT_REVIEW_TIER by an isolated reviewer before the queue
    Clause-②: yes
    Responsibility: n/a — not a defect card
    Thread-read: none
    Serial constraints cleared: none blocking. Read at 2026-10-11T07:55Z:

    Clause-② is yes: SchemaForm is re-exported from @object-ui/app-shell's entry, so a new WidgetContext member widens its published widgetContext prop type. objectui#11601 (ref:dataset) measured the same thing on its built dist.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 12126,
    "status": "done",
    "branch": "claude/issue-12126-ref-multi-permission-widget",
    "pr": "#12136",
    "session": "session_01TYgwmFK1q4KJ6Qq2WRLzsD (subagent: the parent PM's id, per the dispatch)",
    "premise_still_valid": true,
    "summary": "Registered ref-multi:permission in metadata-admin's WIDGETS (labelling 'group', the field-multi/action-multi measured reason): one RefMultiWidget, parameterised by its registry binding, lists the stored names with remove buttons, offers the declared permission sets not yet picked, appends the picked NAME (the stored value stays a list of names), and flags a stored name the LOADED catalog lacks as '(not found)'; FAILED/LOADING/idle arms follow RefDatasetWidget and never fall to the raw-JSON face. WidgetContext gains permissionSets (LoadState OF RegistryItemOption[]); ResourceEditPage loads client.list('permission') (whole registry, no package scope) through usePickerLoad, gated by a new formDeclaresWidget(form, 'ref-multi:permission') so no other editor sends the request; three en/zh strings in metadata-admin/i18n.ts. BEFORE on BASE 1071393 source: the row renders the announced raw-JSON textarea ('falling back to JSON'); the new pins are 19/20 red there and 20/20 green at HEAD 56761dd. Draft PR objectui#12136 open, assignee zhuangjianguo; CI in_progress at report time (17 success, 3 skipped, 22 in progress, 0 failed).",
    "tests": "All at HEAD 56761dd. (1) New pins SchemaForm.refMultiPermission-12126.test.tsx (16) + ResourceEditPage.permissionSetsFeed-12126.test.tsx (4) + parity SchemaForm.widgetLabelling.test.tsx (+2 CASES rows): 'Test Files 3 passed', 90 tests. (2) BEFORE leg: widgets.tsx/ResourceEditPage.tsx/i18n.ts checked out at BASE 1071393 under a trap restoring from HEAD (on-disk count of 'ref-multi:permission' in widgets.tsx = 0), new pins: 'Tests 19 failed | 1 passed (20)'; first failure 'the announced raw-JSON fallback is on screen'; the 1 green is the no-request control; restore proven: diff-HEAD-bytes=0 and blob == HEAD for all three files. (3) Ablations via objectstack scripts/ablation-replace.mjs (anchor x1 -> x0, blob changed, restore blob == HEAD, git diff HEAD empty): A1 const notFound = false; -> 3 failed | 17 passed; A2 true || formDeclaresWidget( -> 1 failed | 3 passed ('the tag-box row reads no catalog, so the permission list is never asked for'). Source-resolved (relative imports), so no dist preflight applies. (4) pnpm exec vitest run packages/app-shell/src/views/metadata-admin/ --shard 1..4/4: 118+118+118+117 = 471 files passed; 1307+1194+1562+1281 tests passed, 1 skipped. (5) turbo run build --filter=@object-ui/app-shell... --concurrency=2: 'Tasks: 29 successful, 29 total'; then pnpm --filter @object-ui/app-shell type-check echoed '@object-ui/app-shell@17.7.0 type-check' / 'tsc --noEmit && tsc -p tsconfig.test.json', VERDICT command-exit 0; tsc -p tsconfig.test.json --listFilesOnly lists the three test files. (6) H6 on built dist: index.d.ts re-exports SchemaForm; SchemaForm.d.ts widgetContext?: WidgetContext; widgets.d.ts permissionSets?: LoadState OF RegistryItemOption[] beside positive control datasets?:; scratch probe against dist: tsc exit 0 (two @ts-expect-error refusals hold: bare list, misspelled permissionSet); reverse leg with a refused row key title: tsc exit 2, TS2353 on RegistryItemOption. (7) Root suite scripts/__tests__/ --shard 1..3/3: 60+59+60 = 179 files passed, 2 skipped; ratchets column-identity.ratchet + one-authority-per-exported-name-6273: 2 files, 18 tests passed. (8) Eager closure: vite build of apps/console at HEAD and at BASE source (trap-restored, proven): eagerGzipBytes 3250219 -> 3250896 = +677 B gzip (+3064 raw); SchemaForm chunk +619, metadata-admin i18n chunk +51, rest 1-4 B hash churn; eager chunk count 290 of 2474 both; 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)'. NOT MEASURED: browser drive (no backend run for this card); packages outside app-shell (public face widened additively; no in-repo consumer passes widgetContext besides app-shell itself).",
    "mcp_calls": "0",
    "api_writes": "2 relay strokes via objectstack scripts/pm/fleet-write/dispatch.mjs (each = POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create -> POST /repos/objectstack-ai/objectui/pulls (draft) + POST /repos//issues/12136/assignees [zhuangjianguo], read-back 9591 bytes sent = stored; (2) comment -> POST /repos//issues/12126/comments (this report). git push x2 (empty-branch probe, then 9df6a95..56761dd), not REST. Zero label writes, zero PR-body PATCH.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted, not filed — PermissionMatrixEditor's own client.list('permission', {}) read (the admin-scope assignable allowlist) swallows a failed load with an empty .catch, so a failure renders as an empty allowlist: the objectui#5170 shape. Read-only inference, not reproduced through a public door, file outside this card's surface. It is not in PR #12136's Acceptance notes (the body is written once); if the seat wants it there, append one Acceptance-notes bullet saying exactly this. Dedupe words: PermissionMatrixEditor allSetNames catch failure allowlist"
    ],
    "gates": {
    "vitest metadata-admin/ (4 shards)": "pass — 471 files",
    "new pins + widgetLabelling parity": "pass — 3 files, 90 tests",
    "app-shell type-check": "pass — VERDICT command-exit 0",
    "build closure (turbo, app-shell...)": "pass — 29/29 tasks",
    "root suite scripts/tests (3 shards)": "pass — 179 files, 2 skipped",
    "ratchets column-identity + one-authority-6273": "pass — 2 files, 18 tests",
    "check:i18n-designer-parity": "exit 0",
    "check:i18n-keys": "exit 0",
    "check:control-bytes": "exit 0",
    "check:new-line-citations": "exit 0 — 0 new",
    "check:changeset-claims": "exit 0",
    "check:pending-changeset-literals": "exit 0",
    "check:test-path-roots": "exit 0",
    "check:vi-mock-specifiers": "exit 0",
    "check:vi-mock-inherit": "exit 0",
    "check:vi-mock-override-shape": "exit 0",
    "check:phantom-deps": "exit 0",
    "check:designer-field-key-parity": "exit 0",
    "check:component-surface-parity": "exit 0 (report-only by design)",
    "check-changeset-presence.mjs": "exit 0",
    "check-changeset-no-major.mjs": "exit 0",
    "check-governed-queue-guard --test (7 paths)": "NOT GOVERNED",
    "check:eager-closure (after apps/console vite build)": "exit 0 — +677 B gzip vs BASE, headroom 29.9 KB",
    "eslint (package lint form eslint .) on 6 touched files": "0 errors; +1 warning react-refresh/only-export-components on formDeclaresWidget",
    "check:readme-exports": "not run — no README changed",
    "CI on PR #12136": "in_progress at report time (17 success, 3 skipped, 22 in progress, 0 failed)"
    },
    "line_budget": "git diff --numstat 1071393..56761dd: +763 / -3 over 7 files — widgets.tsx +230/-2, ResourceEditPage.tsx +31/-1, i18n.ts +10/-0, SchemaForm.refMultiPermission-12126.test.tsx +290, ResourceEditPage.permissionSetsFeed-12126.test.tsx +177, SchemaForm.widgetLabelling.test.tsx +3, changeset +22. No skills/** or governed ledger touched, so no line ratchet applies.",
    "deviations": [
    "The HEAD leg of the apps/console vite build ran with NODE_OPTIONS=--max-old-space-size=6144, above the 4096 default, without a measured need; the BASE leg ran at 4096 and succeeded, so the raise was unnecessary.",
    "Commit trailers and PR footer follow objectui AGENTS.md (model-free Claude-Session + Co-authored-by: Claude) and the os-dev session-URL footer, not the harness reminder's model-named Co-Authored-By line and its other footer form; the reminder yields to repo rules by its own precedence clause.",
    "The call running metadata-admin shards 1+2 back-to-back exceeded the foreground cap and the harness moved it to the background; I blocked on the shard-2 lock process with tail --pid and read both logs (both exit 0). Shards 3-4 and the root suite then ran detached, one per call, each waited on with tail --pid.",
    "One verify-lock call returned exit 99 (queue-timeout, NOT MEASURED); it was re-run on the kept slot ui2-12126 and not counted as a result.",
    "eslint was first run with --no-inline-config (the os-dev text's objectstack spelling): 3 errors, all react-hooks/static-components on existing lines in ResourceEditPage.tsx that carry inline disables. objectui's package lint is eslint . (inline config honoured): 0 errors. The second form is the one reported. Standing-text note for the seat: os-dev's pnpm lint = eslint . --no-inline-config is objectstack's form and does not hold in objectui.",
    "The first run of ResourceEditPage.permissionSetsFeed-12126 was red because the page opens form-only types in view mode. The test now clicks Edit first (commit 56761dd); no product change."
    ],
    "files_changed": [
    ".changeset/12126-ref-multi-permission-widget.md",
    "packages/app-shell/src/views/metadata-admin/widgets.tsx",
    "packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx",
    "packages/app-shell/src/views/metadata-admin/i18n.ts",
    "packages/app-shell/src/views/metadata-admin/SchemaForm.widgetLabelling.test.tsx",
    "packages/app-shell/src/views/metadata-admin/SchemaForm.refMultiPermission-12126.test.tsx",
    "packages/app-shell/src/views/metadata-admin/ResourceEditPage.permissionSetsFeed-12126.test.tsx"
    ]
    }

  4. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR objectui#12136 (head 56761dd213) · domain:ui seat 2, session_01TYgwmFK1q4KJ6Qq2WRLzsD, 2026-10-11T10:54Z

    Reviewed against GitHub and the diff, not the report (6108144870).

    Verdict

    • Shape:
    • Scope: 7 files, +763/−3, all on the claim's surface (6106885644): widgets.tsx, ResourceEditPage.tsx, i18n.ts, the widget-labelling parity pin, two new pins and one changeset.
    • What lands:
      • ref-multi:permission is registered in WIDGETS.
        • One RefMultiWidget lists the stored names, each removable, and offers the declared permission sets not yet picked. A pick appends the set's name, so the stored value stays a list of names.
        • A stored name that the LOADED catalog lacks stays visible and is flagged (not found).
        • The FAILED, LOADING and idle arms follow RefDatasetWidget. The row never falls back to the raw-JSON face.
      • WidgetContext gains the optional permissionSets member. ResourceEditPage loads client.list('permission') through the shared usePickerLoad, and only for a form that declares the widget.
      • Labelling: WIDGET_LABELLING reads 'group', as field-multi and action-multi do.
      • All three "Done when" lines of the card are met.
    • Changeset prose, checked sentence by sentence against the diff: @object-ui/app-shell: minor, the published widening of SchemaForm's widgetContext type.
    • Contract review: 6108250004 on this head reads PASS, with Served-tier: CONTRACT_REVIEW_TIER and Local-runs: none, and Implemented-by and Reviewed-by signed.
    • Tests:
      • On BASE source, the new pins went 19 red and 1 green; the green one is the no-request control. At the head they are 20/20 green.
      • Ablations A1 (the not-found flag) and A2 (the load gate) each turned their own pins red. Every restore was blob-equal.
      • metadata-admin/ passes in 4 shards (471 files), the root suite and the ratchets pass, and the built dist shows the new member beside datasets?:.
    • Eager closure: +677 B gzip, measured by the dev base to head. The Bundle Analysis bot reads 3174.7 KB of 3204.6 KB.
    • Gates on this head (read at 2026-10-11T10:54Z): 40 success, 3 skipped by design (dependabot and the two coverage placeholders), 0 failure. Lint and Type Check both read success.

    Acceptance notes (carried, not filed)

    • The (not found) pill uses border-amber-300 bg-amber-50 text-amber-900 with no dark: variant. widgets.tsx already has both shapes: a light-only amber box, and one with dark: variants. Not a contract matter.
    • formDeclaresWidget does not follow a { group } section reference that SchemaForm resolves. No form served today declares the widget that way. Follow-up only.
    • Duplicate names: a stored list that already holds a duplicate name collides on li key, which logs a React warning. Authored picks are deduplicated.
    • PermissionMatrixEditor's own client.list('permission', {}) swallows a failed load with an empty .catch (the objectui#5170 shape). This is a code reading only, outside this card, with no measured reach. Not filed.

    Landing: ready and auto-merge through the merge queue now. Then objectstack's .objectui-sha must carry this merge before objectstack-ai/objectstack#22794 can land (Restart-when: on objectstack-ai/objectstack#22682). The seat notes the merge on the epic's .objectui-sha stage (objectstack-ai/objectstack#15194), which owns that file.


    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

area:studioChanging a running app without code — authoring, publish, docs and the portaldomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatenhancementNew feature or requestpriority:p1

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions