Skip to content

Commit ea9fd99

Browse files
feat(spec): the position form picks permissionSets from the permission-set registry (ref-multi:permission), with the console pin moved to 4997995022f8 (#22794)
Fixes #22682 Clause-②: no Item 3 of #22682, the card's last `packages/spec` item (items 1 and 2 landed with PR #22710 as `b32ee3eaf3`). ADR-0131 D4, verbatim: "References to declared items are **by machine name**, resolved registry-first." The position form is the one authoring surface for the position to permission-set binding, and its `permissionSets` row (added by PR #22723 as `type: 'tags'`, a free-text tag box) now names the permission-set registry picker, so the designer offers the declared set names instead of free text. This PR now also moves the bundled Console's objectui pin to `4997995022f8`, objectui#12136's merge (objectui#12126), which registers the `ref-multi:permission` widget. That is landing order B from seat 1's ACCEPT `6106524871` on #22682: the objectui widget first, then the pin, then this row. Triage's unlock `6108789665` put the pin bump into this PR. - **Round 1** (accepted): claim `6105901038` (seat `domain:spec#1`), base `e84aeb36ce`, accepted at `8996b566b5` by ACCEPT `6106524871`. The three files of that round are unchanged here. - **Landing round:** claim `6109075588` (seat `domain:spec#3`, session `session_01KNKBCRDJCu5tGy3TEbvtrF`), branch `claude/issue-22682-position-permission-sets-picker` continued from `8996b566b5`. It merges `main` (`aa734f0924` onto `efcbac73cc`, then `483fddb3fc` onto `1eff3224d7`, both through `scripts/pm/os-regen-merge.sh`), and adds one pin-bump commit, `aaa50c66ec`. ## What changes - **The row** (`packages/spec/src/identity/position.form.ts`): `{ field: 'permissionSets', type: 'tags', colSpan: 2, helpText: … }` becomes `{ field: 'permissionSets', widget: 'ref-multi:permission', colSpan: 2, helpText: … }`, with a comment saying why. The helpText is unchanged. No other row, section or form changes. - **One pin** (`packages/spec/src/identity/position.test.ts`, a new `describe` at the end, 4 tests). It reads the position form through `METADATA_FORM_REGISTRY`, the registry `/meta/types` serves, which holds the output of `defineForm`'s parse, so a parse that dropped `widget` would also turn it red. It asserts: - a lit and a dark control for the row probe; - exactly one `permissionSets` row, whose `widget` is `ref-multi:permission`; - the registry type the key names is `permission`, and `getMetadataTypeSchema('permission')` is `PermissionSetSchema`; - `permissionSets` refuses a bare string with `invalid_type` at `permissionSets`, so the list-valued key sits on a list-valued field. - **Changeset:** `@objectstack/spec` `patch`, `Clause-②: no`. - ⛔ No schema, parse, export or accept-set change. `PositionSchema` is untouched (`permissionSets` stays `z.array(SnakeCaseIdentifierSchema).optional()`), the approver bindings are untouched, and `FormField.widget` stays `z.string().optional()`: the spec has no form widget-key vocabulary, so no published vocabulary is added. No objectui file. - **The pin hop** (commit `aaa50c66ec`, 20 files, +312 / -83): `.objectui-sha` from `0df67f237c5a` to `4997995022f8`, with its regeneration. The next section has the details. ## The console pin: `0df67f237c5a` to `4997995022f8` This moves the bundled Console's objectui pin from `0df67f237c5a` (PR #22812) to `4997995022f8e65946f6362b7434685539cc72b7`, objectui#12136's merge (objectui#12126): metadata-admin registers `ref-multi:permission`, so the position form's `permissionSets` row renders a multi-picker of the declared permission sets instead of the raw-JSON fallback. This is not a release act. The Version Packages PR is untouched, and this diff edits neither `packages/console/CHANGELOG.md` nor `packages/console/package.json`. The console's release input is the new `.changeset/console-4997995022f8.md`. ### Range - **REST `compare` on objectstack-ai/objectui**, `0df67f237c5a...4997995022f8` (read 2026-10-11T12:55:58Z): `status: ahead`, `ahead_by: 7`, `behind_by: 0`, 80 files, merge base `0df67f237c5a`. - In a scratch repository that borrows the objectui object store, `git merge-base --is-ancestor 4997995022 origin/main` exits 0 (objectui `main` read at `1d1cfe0c119e`, 2026-10-11T12:39:18Z). `0df67f237c5a..4997995022f8` has 7 commits and 0 merges. Its 80 paths are 17 added, 62 modified, 1 renamed and 0 deleted. - **Why this commit and not a later one:** it is the nearest objectui `main` commit that carries the widget, as the dispatch ruled. objectui `main` has two more commits (`9b75509`, `1d1cfe0`). I measured no reason to take them, so this bump does not chase them. ### What each commit brings, and which regenerated change it causes | objectui commit | what it is | what it changes here | |:--|:--|:--| | `69bca0a` (objectui#12081 item 3) | a paged grid's footer reads the whole matched set, or says it reads the page | `ObjectGrid.tsx` (+75 / -41) and the `en` / `zh` / `de` packs: hop sentences in the four `ObjectGrid.tsx` records and the calendar record; corpus counts | | `802f0bc` (objectui#5250 slice C, the strict authoring face) | repairs the keys the strict authoring face refuses in objectui's examples | examples JSON and `packages/types/examples/dashboard.ts` only. No record cites them. Its one regenerated effect is a corpus control: `timeout` 1705 to 1704 in two migration entries | | `1071393` (objectui#12098) | marketplace install answers say what the runtime served | app-shell marketplace and the locale packs: the calendar record's pack sentence; corpus counts | | `7525045` (objectui#12082 leg A) | more write affordances read the affordance-to-grant map | `ObjectGrid.tsx` (+14 / -3: a bulk `update` def needs the edit verdict) and `ObjectGantt.tsx` (+27 / -4): hop sentences in five gantt records and the grid records | | `c9fc10b` (objectui#12121) | a boolean with a CEL default is left to the server on create | `form.tsx` (+37 / -25): the form-layout record's call-site anchor moves `:3246` to `:3258` | | `66eb560` (objectui#12131 phase 1, the strict authoring face) | docs stop teaching keys the strict authoring face refuses | README, docs pages and one renamed example. No record cites them, and no corpus count moves | | `4997995` (objectui#12126) | the `ref-multi:permission` widget | `widgets.tsx`, `ResourceEditPage.tsx` and metadata-admin `i18n.ts`. No record cites them; corpus counts | Neither of the two strict-authoring-face commits touches a registered component, so neither moves the SDUI manifest. ### What the regeneration changed (20 files) | file | written by | gate that required it | |---|---|---| | `.objectui-sha` | `scripts/bump-objectui.sh 4997995022f8… --no-commit` | Console Pin Gate builds what it names | | `.changeset/console-4997995022f8.md` | the same run (the digest): 5 releasing changesets of 5, 2 commits without a changeset, highest level `minor` | `check:objectui-changeset`, Check Changeset | | `scripts/sdui-manifest.record.json` | `node scripts/gen-sdui-manifest-node.mjs` over `.cache/objectui-4997995022f8` | `check-sdui-manifest.mjs` | | `packages/sdui-parser/objectui-lockstep.json` | `node scripts/check-sdui-lockstep.mjs --update`, `OBJECTUI_ROOT` at the built tree | `check:sdui-lockstep` | | 55 asserting citations: 49 in 14 committed files under `packages/spec/src`, and 6 in the generated `registry.ts` | re-measured by hand (below) | `check:objectui-pin-citations` (and `--verify-anchors`) | | `.changeset/objectui-pin-citations-4997995022f8.md` | hand-written, `@objectstack/spec` patch, `Clause-②: no` | Check Changeset (the shipped describes name the pin) | | `content/docs/references/ui/view.mdx` | `pnpm --filter @objectstack/spec gen:docs`, the one artifact `check:generated` named stale | `check:docs` | - **`sdui.manifest.json` is byte-identical** (`cmp` against the tracked file: equal). It keeps 107 components and sha256 `9d87162a3ef6…`. Only its record moves: `objectuiSha` and `modulesRoot`. - The lockstep records 214 grammar lines (blob `0131f27cf86d`), 25 diagnostic codes and containment predicate `76c18fb95d1f`. All are unchanged; objectui's `packages/sdui-parser` is untouched by the range, so no port is owed. Only `rev`, `revDate` and `recordedAgainstPin` move. - `packages/spec/src/migrations/registry.ts` is gitignored and regenerated (`gen:migration-registry`), not committed. - The bump script and the console build read objectui through a scratch repository with no refs of the shared checkout's own, borrowing its object store as an alternate. So nothing was written into the shared objectui checkout. ### How the citations were re-measured - **Method.** Each asserting record's cited objectui files were resolved at `0df67f237c5a` and intersected with the range's 80 changed paths. A record whose cited files are all byte-identical (`git diff --quiet`) gains that sentence and no re-pointing: 29 records. The other 20 were re-read one by one. - **Changed cited files:** `ObjectGrid.tsx`, `ObjectGantt.tsx` and `form.tsx`, plus the `en` / `zh` / `de` packs, whose `calendar.configRequired` lines hash the same at both pins. Every cited line in them moved or held byte-identical, measured by exact-text search of each old range in the new file: - `ObjectGrid.tsx`: the selection block `4897-4924` to `4981-5008`, still hashing to `c88443302d40`; `:1786` to `:1787`; `:1850` to `:1851`; `:5097-5116` to `:5165-5184`; `:5132` to `:5200`; `:5570` to `:5615`; `:6587-6588` to `:6632-6633`; `:6604-6605` to `:6649-6650`. - `ObjectGantt.tsx`: +2 above the new grant block and +23 below it, for example `getGanttConfig` `:610` to `:612` and the refusal arm `:2307` to `:2330`. The gantt record's 25 read-point anchors are re-cited at this pin. - `form.tsx`: `spanLadderFor` held at `:274-301`; its call site moved `:3246` to `:3258`. - **Readings beside a cited read**, each said in its hop sentence: - `ObjectGrid.tsx` (objectui#12082): a bulk `update` def is now withheld unless the edit verdict allows it. That narrows the declared defs and adds no implicit one, so the `sys_api_key` reading in `api-methods-batch-conformance.test.ts` holds. - `ObjectGantt.tsx` (objectui#12082): the in-chart write handlers are withheld without the grant. The renderer's `schema.*` read set is the same 49 names at both pins, so no declared key set moves. - **Counts re-taken by each record's own method.** - `keyboardNavigation`: 15 hit lines against 3 for `schema.editable`, at both pins. - `ObjectKanban.tsx`'s `quickAdd` / `onQuickAdd` / `onCardClick`: 2 / 2 / 11. No `'kanban-ui'` literal in objectui's non-test sources at either pin. - The `ElementDataSourceGate` occurrences in the five `src/index.tsx` shells: 0, 3, 3, 4 and 3, all five shells byte-identical. - **Corpus counts.** The six migration entries' counts were re-taken with `git grep -o -F`. That method first reproduced every `0df67f237c5a` number: 8424 files, `objectstack` 18248, `@objectstack/spec` 7582, `useState` 2645, `timeout` 1705, `RuntimeConfig` 347, `resourceLimits` 2, `window` 4596, `period` 251, `interval` 233, `metrics` 456, `Span` 517, `SpanSchema` 57, `TTL` 184 and `tenant` 1357. - At the new pin they read: 8441 files, `objectstack` 18271, `@objectstack/spec` 7587, `useState` 2648, `timeout` 1704, `RuntimeConfig` 352, `resourceLimits` 2 (the same two `packages/app-shell` hits), `window` 4604, `period` 251, `interval` 238, `metrics` 456, `Span` 517, `SpanSchema` 57, `TTL` 184 and `tenant` 1362. - The zero tokens: 98 were checked (the exports of `plugin-lifecycle-advanced.zod.ts`, `tracing.zod.ts` and `metrics.zod.ts`, plus every named key). 96 read 0 at both pins; `Span` / `SpanSchema` read 517 / 57 at both. ### Clause-②: the pin hop moves no published contract, measured - **`.zod.ts` accept sets, defaults and exports:** none moves. Each of the 14 changed `.ts` files under `packages/spec/src` was scanned with the TypeScript scanner at `aa734f0924` and at the head, comments skipped and every string, template and regular-expression literal replaced by one placeholder token. The token sequences are identical in 12 of the 14. The other two, `18.system-metrics-jsdoc-durations-unit-in-key.ts` and `18.system-tracing-otel-exporter-durations-unit-in-key.ts`, each gain one `+` joining one new string fragment (the older count) into a `reason` string. No identifier, key, value or export moves. The one shipped `describe` that changes is `FormField.span`'s, and only its sha moves. - **Baselines:** `check:api-surface` ("public API surface + factory signatures unchanged") and `check:authorable-surface` are green, and `check:generated` reads "All 14 generated artifacts are up to date". - **The html tier:** the SDUI manifest the save gate and `os validate` compile against is byte-identical, so no page source changes verdict. - So the hop's line is `Clause-②: no`, the same as this PR's line 2 and as both new changesets. No line in this body moves. ### The widget renders at the new pin - `packages/app-shell/src/views/metadata-admin/widgets.tsx:3242` is `export const WIDGETS = {`, and `:3246` is `'ref-multi:permission': RefMultiPermissionWidget,`. `WIDGET_LABELLING` declares it `'group'` at `:3358`. The key is absent from `widgets.tsx` at `0df67f237c5a`. - `ResourceEditPage.tsx:1001-1004` loads the `permission` catalog (`client.list('permission')`) only when the form on screen declares the widget, which the position form now does. ## The key: `ref-multi:permission`, measured objectui readings are `git show` in a read-only clone, at objectui `origin/main` `206505c` and at this repo's pin `.objectui-sha` `20c6d351ad74`; both read the same at every line cited. | question | reading | |:--|:--| | Does a multi-value `ref:` spelling exist anywhere? | No. Census of quoted `ref:` widget literals: this repo has `ref:object`, `ref:dataset` and `ref:component` only; objectui has the same three. All three are single-valued. | | How does the vocabulary spell a multi-value picker? | As its own key: `field-ref` / `field-multi`, `select` / `multiselect`, and `action-multi`. No key in either repo serves both arities. | | Can objectui's single `ref:` widgets take an array? | No. `RefObjectWidget`, `RefComponentWidget` and `RefDatasetWidget` (`widgets.tsx:464`, `:607`, `:684`) each read `String(value)` and write one string. The registry also declares ONE labelling per key (`WIDGET_LABELLING`, `widgets.tsx:3142`): the single `ref:` pickers are `'control'`, while `field-multi`, `action-multi` and `multiselect` are `'group'`. | | Which registry type names permission sets? | `permission`, on both sides. Spec: `MetadataTypeSchema` (`kernel/metadata-plugin.zod.ts:166`), the `DEFAULT_METADATA_TYPE_REGISTRY` row labelled `Permission Set` (`:1117`), and `getMetadataTypeSchema('permission')` returns `PermissionSetSchema`. objectui: `registerMetadataResource({ type: 'permission', … })` (`PermissionMatrixEditor.tsx:28`), and `anchors.ts:374`. | | Does `type: 'tags'` override an explicit widget? | No. `inferWidget` returns an explicit `fieldSpec.widget` first (`SchemaForm.tsx:447`). `tags` maps to `string-tags` only when no widget is declared (`:467`). So the row drops `type`, matching every other `ref:` and `-multi` row in the spec forms, none of which declares a `type`. | So the key is a distinct list key in the `ref:` family. The prefix carries the arity, and the token after the colon is the exact registry type, so the same split reads `ref:object` and `ref-multi:permission`. Alternatives considered: - `ref:permission` on the array field, with the arity read from the schema. It is one key per type, but no widget in the vocabulary is arity-polymorphic, a single `ref:` widget writes a string into a list, and one key would have to carry one labelling for two shapes. - `permission-multi`, mirroring `field-multi`. It drops the `ref:` namespace the claim names. - `ref:permission-multi`. The token after the colon would no longer be a registry type. ## What objectui rendered for this key before the pin moved, and the landing order Round 1's reading, measured by code path at objectui `206505c` and at the then pin `20c6d351ad74`: - The position editor is the generic `ResourceEditPage`. Its form is `entry?.form` from `/meta/types` (`ResourceEditPage.tsx:456`, `:457`, `:2943`). Position registers only a preview (`previews/index.ts:87`), not an edit page. So the key reaches every console as soon as the server runs this spec. - `WIDGETS` (`widgets.tsx:3033`) had no `ref-multi:permission` entry. `resolveFieldFace` therefore sent an unregistered, non-passthrough hint to the `raw-json` face (`SchemaForm.tsx:870`), rendered at `:2223`: a JSON textarea under the note `widget ref-multi:permission — falling back to JSON until a custom renderer is registered.` (`i18n.ts:2166`). - No objectui test pins "every widget key a spec form names is registered", so the change turned no objectui gate red; it would have degraded to the announced fallback. - Precedent: objectui#11601 records the same shape for `ref:dataset`, and the joined-block row in #21714 declared `ref:dataset` only after the pin carried the renderer. Seat 1 set landing order B in ACCEPT `6106524871`: the objectui companion first, then the pin, then this row. The companion was filed as objectui#12126 and landed as objectui#12136 (`4997995022`). This PR now carries the pin as well, so the row and the widget that renders it ship in one console build. The section above has the reading at the new pin. ## Tests and gates This round's readings are at head `483fddb3fc` (`git rev-parse --short HEAD`) unless a line says otherwise. Every build and test went through `scripts/pm/os-verify-lock.sh`, and each result below is quoted from its `VERDICT` line or from the gate's own verdict line. Exit codes were captured before any pipe. - **`@objectstack/spec`:** - `pnpm --filter @objectstack/spec build`: VERDICT command-exit 0. Its declaration stamp (`9278c1ed04e9…`) still matches at `483fddb3fc`; the second merge brought no `packages/spec` path. - `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2`: `Test Files 648 passed (648)`, `Tests 19346 passed | 1 todo (19347)`, VERDICT command-exit 0. It read the same at `aaa50c66ec`, before the second merge. - `pnpm --filter @objectstack/spec typecheck` (`tsc --noEmit`, the scripts program and the test layer): VERDICT command-exit 0. The test-layer line reads `52 file(s) / 246 error(s) / 135 pinned signature(s) held`. - `pnpm --filter @objectstack/spec check:generated`: "All 14 generated artifacts are up to date". The one stale artifact before `gen:docs` was `content/docs/references/**` (`check:docs`), from the `FormField.span` describe. - **`@objectstack/sdui-parser`** (its lockstep record moved): `pnpm --filter @objectstack/sdui-parser test`: `Test Files 14 passed (14)`, `Tests 225 passed (225)`; its typecheck exits 0. - **The bump's own gates:** - `check-sdui-manifest.mjs`: "present, intact (sha256 9d87162a3ef6…, 107 components) and recorded at the live objectui pin 4997995022f8…" (exit 0). - `check:sdui-lockstep`: "OK — this copy is byte-identical to objectui@4997995022f8 (2026-10-11T10:56:33+00:00) over 214 grammar line(s) [blob 0131f27cf86d], agrees on all 25 diagnostic code(s) and on the `not-a-container` predicate [blob 76c18fb95d1f]" (exit 0). - `check:objectui-pin-citations`: "55 asserting objectui pin citation(s) match .objectui-sha (499799502), 145 historical citation(s) recorded and not checked, across 1983 spec source(s)." With `--verify-anchors` (`OBJECTUI_ROOT` at the scratch repository): "13 anchor content assertion(s) verified against objectui at 499799502" (exit 0). - **Console Pin Gate, local equivalent.** CI's job runs `turbo run build --filter=@objectstack/client...`, `bash scripts/build-console.sh`, the dist assertion, `check:console-sha` and `check:console-injection --require-stamp`. Run here, at the pin commit's tree: - `turbo run build --filter=@objectstack/client... --filter=@objectstack/sdui-parser --concurrency=2`: `Tasks: 34 successful, 34 total`, VERDICT command-exit 0. - `bash scripts/build-console.sh`: VERDICT command-exit 0 (lock held 8m18s). Its log reads "Bundle canary 'import/jobs' present", "Single-zod canary: exactly one zod version literal {major:4,minor:6,patch:5}", "Console bundle carries THIS tree's @objectstack/spec, and only it" and "@objectstack/console dist ready (64384 KB) from objectui@4997995022f8". - The dist holds a non-empty `index.html`, `assets/` and a `.objectui-sha` stamp reading `4997995022f8e65946f6362b7434685539cc72b7`. - `check:console-sha`: "Console dist matches the objectui pin (objectui@4997995022f8)" (exit 0). `check:console-injection --require-stamp`: "the dist carries this tree's copy, and not the published one" (exit 0). Both also ran green at `483fddb3fc` inside the derived union. - The CI job itself is not measured here; it is CI's run, and the pin change moves its dist-cache key, so it rebuilds. - **Derived gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, run with no paths at `483fddb3fc`, derives 126 commands from 23 changed paths against merge base `1eff3224d`. All 126 ran. - The `--ran` reconciliation reads "126 derived famil(ies) accounted for — 126 run, 0 NOT-MEASURED (a DERIVED zero — all 126 recorded an exit code and none of them is 3)". - Three first refused with exit 3, PREREQUISITE NOT MET, which is not a reading: `check:skill-examples`, `check:i18n` and `check:dual-build-cjs-loads`. All three exit 0 after building their closures: `pnpm --filter @objectstack/client-react build`, then the CLI closure `check:i18n` names (`Tasks: 59 successful`, restored from the turbo cache). `check:i18n` reads "9 package(s) — all bundles in sync". `check:dual-build-cjs-loads` reads "107 published require entry point(s) across 66 package(s) load". - The derivation at `aaa50c66ec` warned that the tree was behind `origin/main` on six gate files. That is why `main` was merged a second time; the derivation at `483fddb3fc` is current and names the same 126 commands. - **Artifact rosters.** The 46 commands the no-path run lists under "Artifact rosters" were run too. 43 exit 0. The three PR-context guards (`check-closing-target-claim.mjs`, `check-partof-closing-keyword.mjs`, `check-single-claim-paths.mjs`) ran with `PR_NUMBER=22794`, this body as `PR_BODY`, the branch as `PR_HEAD_REF` and a read token, as their workflows wire them: all three exit 0. - **Lint, narrowed and proven.** ESLint (`--no-inline-config`) over the 16 changed TS files gives 0 errors and 0 warnings, 16 files by the `--format json` count. The population is the `eslint.config.mjs` block that lints every `ts` file (line 971). The config never enables type-aware linting (line 328), so this diff cannot move the verdict on any file it does not touch. The repo-wide `pnpm lint` is CI's run. - NOT MEASURED, declared for CI: the seven path-scheduled CI jobs the derivation names (`Test Core`, `Temporal Conformance`, `Dogfood Regression Gate`, `Dogfood Verify CLI`, `Build Core`, `Build Docs`, `Console Pin Gate`), the four workspace type-check lanes with no local invocation, and the six families that take a value from the workflow. **Round 1, at `8996b566b5`** (unchanged files, so still the record for them): `position.test.ts` 23 tests (19 + 4 new). An ablation through `scripts/ablation-replace.mjs` reverted the row to `type: 'tags'` and turned exactly the widget pin and the registry-type pin red (`Tests 2 failed | 21 passed (23)`); the restore was proven blob-equal to HEAD and re-ran 23 of 23. Size: 23 files, +372 / -84 (456 changed lines) against `1eff3224d`, under the 3,000-line threshold. ## Acceptance notes - **The pin hop moves no published contract** (measured above), so it adds no `Clause-②` line of its own. Both new changesets say `Clause-②: no`. The console changeset is `minor` because objectui declared `minor` for three of its five entries. - **objectui `main` is two commits past the pin** (`9b75509`, objectui#5250 slice B; `1d1cfe0`, objectui#12106). Neither is needed here. - **Cloud's own `.objectui-sha`** is untouched; this PR moves only this repository's pin. - **Writes this round:** `git push` (no force) of three commits; one body and title update and one assignee swap on this PR (`zhuangjianguo` replaces `os-project-manager`, mirroring the card), both through `scripts/pm/`; no label; no MCP write. Landing round written by `domain:spec` seat 3, session `session_01KNKBCRDJCu5tGy3TEbvtrF`; round 1 by seat 1, session `session_01S3aAf11JjbW1mSGL1EhfFj`. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 12b9daf commit ea9fd99

23 files changed

Lines changed: 372 additions & 84 deletions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`positionForm`: the `permissionSets` row now declares `widget: 'ref-multi:permission'` in place of `type: 'tags'`. ADR-0131 D4 resolves a reference to a declared item by machine name, registry-first, so the row names the permission-set registry (metadata type `permission`) for the designer to offer the declared sets and commit their names, instead of a free-text tag box where a name that does not exist can be typed.
6+
7+
Clause-②: no
8+
9+
- **The key.** `ref-multi:` followed by a metadata type is the list form of the forms' `ref:` pickers (`ref:object`, `ref:dataset`, `ref:component`, all single-valued). A distinct key, as `field-ref` / `field-multi` already are, because a single `ref:` picker writes one string and the widget registry declares one labelling per key.
10+
- **Which renderer honours it.** A console whose metadata-admin widget registry has no `ref-multi:permission` entry renders this row as its announced fallback: a JSON editor with the note that no custom renderer is registered. The stored value is the same list of names either way.
11+
- ⛔ Only this one form row changes. No schema, parse, export or accept-set change: `PositionSchema.permissionSets` stays a list of snake_case names, and `FormField.widget` stays a free string.

‎.changeset/console-4997995022f8.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@objectstack/console": minor
3+
---
4+
5+
Console (objectui) refreshed to `4997995022f8`. Frontend changes in this range:
6+
7+
Derived from the changesets objectui declared over the range — 5 releasing of 5 changesets added across 7 non-merge commits; omitted: 2 commits carrying no changeset (they ship no package code).
8+
9+
- **minor** — A Studio form field that declares the `ref-multi:permission` widget now renders a picker of the declared permission sets instead of the JSON editor fallback (objectui#12126). (objectui `499799502`)
10+
- **minor** — The Marketplace's install answers now say what the runtime served (objectui#12098). (objectui `107139379`)
11+
- **minor** — A paged grid's column footer reads the whole matched set, or says it reads the page (objectui#12081 item 3). (objectui `69bca0af6`)
12+
- **patch** — A boolean field whose `defaultValue` is a CEL expression or a runtime token now gets that default on create (objectui#12121). (objectui `c9fc10b1e`)
13+
- **patch** — More write affordances read the affordance-to-grant map, so a caller without the grant is no longer offered a write the server then refuses (objectui#12082, leg A of the write-cen… (objectui `752504520`)
14+
15+
**In this console build, declared nowhere** — objectui merged 2 commits in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared them, so they appear in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387.
16+
17+
- _(no changeset)_ docs: stop teaching keys the strict authoring face refuses (objectui#12131, phase 1) (#12134) (objectui `66eb5604e`)
18+
- _(no changeset)_ fix(examples): repair the class (i) keys the strict authoring face refuses (objectui#5250 slice C) (#12128) (objectui `802f0bc41`)
19+
20+
objectui range: `0df67f237c5a...4997995022f8`
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
The spec's objectui citations, and the shipped description text that names the `.objectui-sha` pin (the `FormField.span` describe and six migration-entry descriptions), are re-measured against the new console pin, objectui `4997995022f8`.
6+
7+
Clause-②: no
8+
9+
Every anchor was mapped through the objectui diff `0df67f237c5a..4997995022f8`: 80 paths over 7 commits, none deleted and one renamed (a schema-catalog example, which no record cites). Three files a record cites changed on the hop, plus the `en` / `zh` / `de` packs (whose `calendar.configRequired` strings did not change): `ObjectGrid.tsx` (objectui#12081 item 3 and objectui#12082), `ObjectGantt.tsx` (objectui#12082) and `form.tsx` (objectui#12121). Every cited line in them moved or held with its text byte-identical, and each record that cites one is re-pointed with a hop sentence that says by how much. The records that cite only byte-identical files gain a hop sentence that says so.
10+
11+
Two readings sit beside a cited read, and each hop sentence says what it found:
12+
13+
- `ObjectGrid.tsx`: a bulk `update` def is now withheld unless the edit verdict allows it (objectui#12082), which narrows the declared defs and adds no implicit one, so the `sys_api_key` reading holds. The selection block still hashes to `c88443302d40`.
14+
- `ObjectGantt.tsx`: the chart's in-chart write handlers are withheld from a caller without the grant (objectui#12082). No `schema.*` read moved; the renderer reads the same 49 names at both pins.
15+
16+
The six migration entries' corpus counts were re-taken with `git grep -o -F`, the method that reproduces every `0df67f237c5a` number. The corpus is now 8441 tracked files. Every token an entry counts as zero still reads zero (96 tokens; `Span` and `SpanSchema` read 517 and 57 at both pins, as the tracing entry records). The controls moved with the corpus, for example `objectstack` from 18248 to 18271 and `timeout` from 1705 to 1704.
17+
18+
No key, default, enum member or export moves.

‎.objectui-sha‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0df67f237c5ab46cbc65252c5f6bd05f2be44c48
1+
4997995022f8e65946f6362b7434685539cc72b7

‎content/docs/references/ui/view.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ Column footer summary configuration
213213
| **required** | `boolean` | optional | Required override |
214214
| **hidden** | `boolean` | optional | Hidden override |
215215
| **colSpan** | `integer` | optional | Absolute column span (1-4). The renderer clamps it to the form grid's current column count, so the cell starts at a real column boundary at every surface width and never overflows (`colSpan: 4` in a 3-column grid renders as 3); a `colSpan` within the column count renders as authored, and `colSpan: 1` emits no span class at all. |
216-
| **span** | `Enum<'auto' \| 'full'>` | optional (default: `"auto"`) | Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — at the pin this repo builds against (`.objectui-sha` = `0df67f237c5a`), only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide `field:grid` widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: at that same pin the renderer emits one clamped col-span class per multi-column tier (`@md:col-span-2 @2xl:col-span-3` for a 3-column grid), so the field takes the whole row at every multi-column tier, not just the widest. |
216+
| **span** | `Enum<'auto' \| 'full'>` | optional (default: `"auto"`) | Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — at the pin this repo builds against (`.objectui-sha` = `4997995022f8`), only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide `field:grid` widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: at that same pin the renderer emits one clamped col-span class per multi-column tier (`@md:col-span-2 @2xl:col-span-3` for a 3-column grid), so the field takes the whole row at every multi-column tier, not just the widest. |
217217
| **widget** | `string` | optional | Custom widget/component name (overrides type-based inference) |
218218
| **language** | `string` | optional | Code editor language (for type=code) |
219219
| **keyField** | `{ field?: string; label?: string \| Record<string, string>; placeholder?: string \| Record<string, string>; helpText?: string \| Record<string, string>; … }` | optional | Key column config for record-typed fields |
@@ -343,7 +343,7 @@ Form-view select option — the object-field option shape minus the per-option `
343343
| **required** | `boolean` | optional | Required override |
344344
| **hidden** | `boolean` | optional | Hidden override |
345345
| **colSpan** | `integer` | optional | Absolute column span (1-4). The renderer clamps it to the form grid's current column count, so the cell starts at a real column boundary at every surface width and never overflows (`colSpan: 4` in a 3-column grid renders as 3); a `colSpan` within the column count renders as authored, and `colSpan: 1` emits no span class at all. |
346-
| **span** | `Enum<'auto' \| 'full'>` | optional (default: `"auto"`) | Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — at the pin this repo builds against (`.objectui-sha` = `0df67f237c5a`), only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide `field:grid` widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: at that same pin the renderer emits one clamped col-span class per multi-column tier (`@md:col-span-2 @2xl:col-span-3` for a 3-column grid), so the field takes the whole row at every multi-column tier, not just the widest. |
346+
| **span** | `Enum<'auto' \| 'full'>` | optional (default: `"auto"`) | Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count — at the pin this repo builds against (`.objectui-sha` = `4997995022f8`), only textarea, markdown, html, richtext and repeater resolve to the full column count (repeater reaches it through the wide `field:grid` widget it maps to). 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: at that same pin the renderer emits one clamped col-span class per multi-column tier (`@md:col-span-2 @2xl:col-span-3` for a 3-column grid), so the field takes the whole row at every multi-column tier, not just the widest. |
347347
| **widget** | `string` | optional | Custom widget/component name (overrides type-based inference) |
348348
| **language** | `string` | optional | Code editor language (for type=code) |
349349
| **keyField** | `{ field?: string; label?: string \| Record<string, string>; placeholder?: string \| Record<string, string>; helpText?: string \| Record<string, string>; … }` | optional | Key column config for record-typed fields |

‎packages/sdui-parser/objectui-lockstep.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
],
77
"objectui": {
88
"repo": "https://github.com/objectstack-ai/objectui.git",
9-
"rev": "0df67f237c5ab46cbc65252c5f6bd05f2be44c48",
10-
"revDate": "2026-10-11T07:26:10+00:00",
9+
"rev": "4997995022f8e65946f6362b7434685539cc72b7",
10+
"revDate": "2026-10-11T10:56:33+00:00",
1111
"source": "packages/sdui-parser/src",
1212
"files": [
1313
"packages/sdui-parser/src/body-dialect.ts",
@@ -21,7 +21,7 @@
2121
"packages/sdui-parser/src/validate.ts"
2222
]
2323
},
24-
"recordedAgainstPin": "0df67f237c5ab46cbc65252c5f6bd05f2be44c48",
24+
"recordedAgainstPin": "4997995022f8e65946f6362b7434685539cc72b7",
2525
"grammarRegion": {
2626
"file": "packages/sdui-parser/src/parse.ts",
2727
"delimiter": "/* ---------------------- the JS literal subset (#6614) ---------------------- */",

‎packages/spec/src/data/api-methods-batch-conformance.test.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,24 @@ const SINGLE_RECORD_WRITE_ONLY: Record<string, string> = {
6161
// `revoked` on ONE key. The multi-select surface this rule protects does not
6262
// exist for API keys, and the shape a future one would take does not need
6363
// `bulk` either — both read off the console build this release pins
64-
// (`.objectui-sha` = `0df67f237`, re-read there 2026-10-11:
64+
// (`.objectui-sha` = `499799502`, re-read there 2026-10-11:
65+
// in `packages/plugin-grid`, on the hop off `0df67f237`, `ObjectGrid.tsx`
66+
// changed again (89 insertions, 44 deletions: objectui#12081 item 3's
67+
// whole-set footer and objectui#12082 leg A), none of it inside the
68+
// selection block, which only MOVED by +84: `ObjectGrid.tsx:4981-5008` here
69+
// (`4897-4924` at `0df67f237`), still hashing to
70+
// `c88443302d40c2db739ddb235470bafa29056e2e` (hash-object of the block),
71+
// re-READ with the same reading below. Two hunks sit beside that reading and
72+
// were re-READ as well: objectui#12081 item 3 moved the `bulkFanoutParams`
73+
// and `resolvedTotalMatching` declarations, their code byte-identical, above
74+
// the footer's hook; and objectui#12082 now also withholds a bulk `update`
75+
// def unless the edit verdict (`rowEditVerdict`) allows it, which narrows the
76+
// declared defs and adds no implicit one. The `delete` verdict line MOVED
77+
// `:1786` -> `:1787` and the promoted def's params `:5097-5116` ->
78+
// `:5165-5184` (called at `:5132` -> `:5200`), each byte-identical.
79+
// `hooks/useBulkExecutor.ts` is byte-identical to `0df67f237` and its
80+
// `:298-303` still hashes to `01083348330f10a201cdf1078b4c21c236402b6a`. At
81+
// `0df67f237`, re-read there 2026-10-11:
6582
// in `packages/plugin-grid`, on the hop off `20c6d351a`, `ObjectGrid.tsx`
6683
// changed again (82 insertions, 32 deletions: objectui#12082's
6784
// affordance-to-grant map, objectui#12103's per-column write grant and

‎packages/spec/src/identity/position.form.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@ export const positionForm = defineForm({
2525
{ field: 'description', colSpan: 2 },
2626
// [ADR-0131 D3/D4] The sets a holder of this position holds, by name —
2727
// what the authorization resolver reads (the junction rows grant nothing).
28-
{ field: 'permissionSets', type: 'tags', colSpan: 2, helpText: 'Permission sets this position distributes, by name (snake_case).' },
28+
// D4 resolves a reference registry-first, so the row binds the
29+
// permission-set registry (metadata type `permission`) rather than a
30+
// free-text tag box: `ref-multi:<type>` is the list form of the
31+
// `ref:<type>` pickers (`ref:object`, `ref:dataset`), offering the
32+
// declared names and committing each pick's name. An explicit widget
33+
// outranks type inference, so the row declares no `type`.
34+
{ field: 'permissionSets', widget: 'ref-multi:permission', colSpan: 2, helpText: 'Permission sets this position distributes, by name (snake_case).' },
2935
// #19331 — `delegatable` is declared by PositionSchema and was offered by
3036
// no control, so the only door to an ADR-0091 D3 self-service delegation
3137
// was the Source tab's free-text JSON. A boolean node takes the boolean

‎packages/spec/src/identity/position.test.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { describe, it, expect } from 'vitest';
22
import { PositionSchema, type Position } from './position.zod';
3+
import { getMetadataTypeSchema } from '../kernel/metadata-type-schemas';
4+
import { PermissionSetSchema } from '../security/permission.zod';
5+
import { METADATA_FORM_REGISTRY } from '../system/metadata-form-registry';
36

47
describe('PositionSchema', () => {
58
describe('Basic Properties', () => {
@@ -285,3 +288,42 @@ describe('unknown keys are rejected, not stripped', () => {
285288
expect(parsed._lock).toBe('full');
286289
});
287290
});
291+
292+
// [ADR-0131 D4] "References to declared items are by machine name, resolved
293+
// registry-first." The position form is the one authoring surface for the
294+
// position → permission-set binding, so its `permissionSets` row names the
295+
// permission-set registry picker rather than a free-text tag box. The form is
296+
// read through the registry `/meta/types` serves, so a parse that dropped
297+
// `widget` would turn this red too.
298+
describe('the position form picks `permissionSets` from the permission-set registry', () => {
299+
type Row = { field?: string; widget?: string; type?: string };
300+
const rows = (): Row[] =>
301+
((METADATA_FORM_REGISTRY.position?.sections ?? []) as Array<{ fields?: Row[] }>)
302+
.flatMap((s) => s.fields ?? []);
303+
const rowFor = (key: string) => rows().filter((r) => r.field === key);
304+
305+
it('CONTROLS: the row probe finds a known row and reports a known absence', () => {
306+
expect(rowFor('name'), 'lit control: the position form has always offered `name`').toHaveLength(1);
307+
expect(rowFor('noSuchKeyAtAll'), 'dark control: a key the form does not offer').toHaveLength(0);
308+
});
309+
310+
it('offers `permissionSets` once, as the `ref-multi:permission` picker', () => {
311+
const [row, ...extra] = rowFor('permissionSets');
312+
expect(extra, 'one row only').toEqual([]);
313+
expect(row?.widget).toBe('ref-multi:permission');
314+
});
315+
316+
it('the registry type the row names is the permission-set catalog', () => {
317+
const [row] = rowFor('permissionSets');
318+
const registryType = row?.widget?.split(':')[1];
319+
expect(registryType).toBe('permission');
320+
expect(getMetadataTypeSchema(registryType!)).toBe(PermissionSetSchema);
321+
});
322+
323+
it('the multi-value key sits on a list: `permissionSets` refuses a bare name', () => {
324+
const result = PositionSchema.safeParse({ name: 'auditor', label: 'Auditor', permissionSets: 'read_all' });
325+
expect(result.success).toBe(false);
326+
const issue = result.error!.issues.find((i) => i.path.join('.') === 'permissionSets');
327+
expect(issue?.code).toBe('invalid_type');
328+
});
329+
});

0 commit comments

Comments
 (0)