Skip to content

feat(ui): an action param's widget draws the named widget, so the action dialog can pick a permission set (#30260) - #30543

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-ijgrij
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-ijgrij

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #30260

Clause-②: yes (widening) — @object-ui/core's exported ActionParamDef gains one optional member, widget?: string, and @object-ui/fields' RawActionParam gains the same optional member (measured on the built ui/packages/core/dist/actions/ActionRunner.d.ts and ui/packages/fields/dist/resolveActionParams.d.ts). No accepted key, value or answer is taken away.
Metadata-protocol: semantic
Skills-docs: follow-up owed — skills/objectui/rules/protocol.md, skills/objectstack-ui/rules/actions.md

维护者速读

  • 改了什么:动作参数上写的 widget 现在真的生效。参数写 widget: 'permission-ref' 时,动作对话框画权限集选择器(列出标签与机器名,提交机器名);未注册的名字退回 type 控件。
  • 为什么改:spec 半边(spec: an action param can name the widget its picker draws (permission-ref), so Create User asks for a permission set by picker (#30117 slice 1) #30259)已声明这个键,但控制台没人读,Create User 的权限集只能手敲名字;本卡是读者半边,之后 Create User: the permission set is a free text box that only accepts the machine name #30117 的服务端切换才有意义。
  • 风险与回滚:只影响写了 widget 的参数(或继承字段 widget 的字段型参数);未写的参数渲染逐字节不变。回滚即 revert 本 PR。
  • 先看:ui/packages/app-shell/src/utils/paramToField.ts(authoredWidgetKey 与降级判断)、ui/packages/fields/src/resolveActionParams.ts(inheritedWidget)、packages/spec/liveness/action.json(widget 行翻 live)。
  • 两分钟验证:pnpm --dir ui exec vitest run -t 30260 packages/app-shell/src/views/ActionParamDialog.test.tsx,预期 3 条通过(选择器提交 support_agent、无 widget 的对照是文本框、未注册名退回 textarea)。
  • 需要你做的:无,席位落地。

What changes

  • ActionParamDef.widget (ui/packages/core/src/actions/ActionRunner.ts, the type only): the resolved param carries the spec's ActionParamSchema.widget.
  • resolveActionParam (ui/packages/fields/src/resolveActionParams.ts): copies the param's widget on the inline branch and the unresolved-field branch. On the field-backed branch the param's own widget wins; otherwise it inherits the field's FieldSchema.widget, but only while the param also inherits the field's type (or restates it). A param that overrides type asks for a different value, so a widget chosen for the field's value is not carried onto it (inheritedWidget).
  • paramToField (ui/packages/app-shell/src/utils/paramToField.ts): a widget that names a registered field widget replaces the type widget key the dialog mounts through getLazyFieldWidget; a name nothing registers keeps the type widget. The test is the shared resolver's own answer (resolveFormWidgetType, read for its documented text fallback), the same reading the console's FormPage makes of a field's widget. paramDegradesWithoutTarget answers false when a registered widget is drawn, so a targetless lookup param with such a widget gets neither the text fallback nor the paste-an-id hints, and the predicate/adapter equivalence still holds.
  • Spec seam (comment 6652583019): packages/spec/liveness/action.json's params.widget row is live, citing both readers, with authorWarn / authorHint dropped; packages/spec/src/ui/action.zod.ts loses the describe's "Not read yet" sentence, the // pointer comment and the JSDoc "Planned" sentence. Generated with them: content/docs/references/ui/action.mdx (gen:schema && gen:docs) and packages/spec/liveness/state-counts/action.md (gen:liveness-counts).
  • One root changeset (minor on @object-ui/core, @object-ui/fields, @object-ui/app-shell, @objectstack/spec).

Why

ActionParamSchema.widget was accepted and stored but read by nothing, so a param that asked for the permission-ref picker drew a text box. This is the console half of #30117's split; Create User's param switch stays with #30117 (packages/platform-objects/** untouched).

Tests

New pins, each a top-level describe in the existing harness file:

  • resolveActionParams.test.ts — inline copy, field-backed inheritance, restated type inherits, overridden type does not, param beats field, no-widget control, unresolved branch keeps it, the public ActionParam type accepts it (8).
  • paramToField.test.ts — registered widget drawn, no-widget control, unregistered falls back to the param's own type widget (text / select / textarea), an authored text honoured, a targetless lookup with a registered widget is drawn and does not degrade, with the unregistered control still degrading (5).
  • ActionParamDialog.test.tsx — mounted inside SchemaRendererProvider with a catalog: the picker lists Support Agent (support_agent), submits { permissionSet: 'support_agent' }, and the emitted value matches expectedParamShape (string); CONTROL without widget is an input that lists nothing; an unregistered name on a textarea param is the textarea (3).

Acceptance notes

  • Bulk dialog not covered. The selection bar's bulk dialog maps resolved params through plugin-grid's toBulkParam / bulkParamToField, whose BulkActionParam shape has no widget, so a promoted bulk action's param still draws its type widget. Outside this card's file surface; recorded in the liveness note. carrier: seat.
  • A private copy of the "names a registered widget" test. authoredWidgetKey restates FormPage's namesRegisteredWidget (one line over resolveFormWidgetType). A shared export would mean editing @object-ui/fields' barrel and ui/apps/console, both outside the surface. carrier: seat.
  • Field-widget inheritance rule (a field-backed param inherits the field's widget only while it inherits or restates the field's type, a restated type matched through the param-type fold; a param that overrides type does not) is this PR's reading of the spec's "widget mapping from object metadata"; the seat accepted it at ACCEPT, and the spec JSDoc says the same.
  • Contract-review should-fixes (review 6661565862) landed in ae7e6fa: paramDegradesWithoutTarget judges the widget the param draws, inheritedWidget matches a restated type through the param-type fold, and the spec JSDoc says inherits or restates; 57e88ed turns the lint pin of the planned params.widget warning into its inverse. Delta review PASS 6662404259.
  • Branch is the card thread's harness branch claude/project-thread-ijgrij (claim amendment 6659791806), not claude/issue-30260-*.
  • paramValueShape.ts needed no edit: expectedParamShape resolves through paramToField, so a permission-ref param already reads its string contract row.

Verification

Ablations (each with scripts/ablation-replace.mjs, mutation proven on disk, restored to the HEAD blob; the ui suites alias @object-ui/* to source, so no build leg):

  • paramToField: authored widget leg removed ⇒ 5 of the 16 new tests red (the picker render, registered widget drawn, text honoured, targetless lookup, public type); controls green.
  • resolveActionParam inline widget: param.widget ⇒ widget: undefined ⇒ 2 red.
  • field-backed widget: inheritedWidget(param, field) deleted ⇒ 3 red.

Local gates on 4d88bf5a (the head after merging origin/main): node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 105 families, all 105 run, each exit code captured before any pipe; --ran reconciliation: 105 derived, 105 run, 0 NOT-MEASURED, 0 UNRUN. Plus the order's extras: pnpm --dir ui run check:spec-symbols exit 0 and node scripts/check-sdui-manifest.mjs exit 0 (in the derived set). check:dual-build-cjs-loads read built output after a full turbo run build (131 packages, exit 0).

Type-check: pnpm --filter @object-ui/core run type-check, @object-ui/fields, @object-ui/app-shell (the last re-run after the merge), all exit 0. Tests after the merge: resolveActionParams.test.ts, paramToField.test.ts, ActionParamDialog.test.tsx, paramValueShape.test.ts exit 0.

Dogfood battery: n/a — no ! in the title and no (narrowing) arm.

Gate run record (105 derived + 1 extra)
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-adr-links.mjs :: exit 0
node scripts/check-adr-links.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dev-prereqs.mjs --self-test :: exit 0
node scripts/check-doc-route-spelling.mjs --advisory :: exit 0
node scripts/check-doc-route-spelling.mjs --self-test :: exit 0
node scripts/check-docs-section-name.mjs :: exit 0
node scripts/check-docs-section-name.mjs --self-test :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
node scripts/check-section-landing-index.mjs :: exit 0
node scripts/check-section-landing-index.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0
pnpm --filter @objectstack/spec run check:api-surface :: exit 0
pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0
pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0
pnpm --filter @objectstack/spec run check:docs :: exit 0
pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm --filter @objectstack/spec run check:empty-state :: exit 0
pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0
pnpm --filter @objectstack/spec run check:export-origins :: exit 0
pnpm --filter @objectstack/spec run check:exported-any :: exit 0
pnpm --filter @objectstack/spec run check:generated :: exit 0
pnpm --filter @objectstack/spec run check:liveness :: exit 0
pnpm --filter @objectstack/spec run check:llms-txt :: exit 0
pnpm --filter @objectstack/spec run check:skill-examples :: exit 0
pnpm --filter @objectstack/spec run check:skill-refs :: exit 0
pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0
pnpm --filter @objectstack/spec run check:variant-docs :: exit 0
pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:corpus-claim-drift :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-anchors :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:doc-frontmatter :: exit 0
pnpm check:docs-audit-scope :: exit 0
pnpm check:docs-redirects :: exit 0
pnpm check:docs-single-h1 :: exit 0
pnpm check:docs-spec-enumerations :: exit 0
pnpm check:docs-transcript-drift :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:merge-driver :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-checklist :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-prior-rulings :: exit 0
pnpm check:published-files :: exit 0
pnpm check:published-readme-links :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:react-page-adapter-contract :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:role-word :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:spec-docblock-symbol-anchors :: exit 0
pnpm check:spec-parsed-alias :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:vendor-version-stamps :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
pnpm --dir ui run check:spec-symbols :: exit 0

Generated by Claude Code

ActionParamDef gains widget; resolveActionParam copies the param's own
widget (field-backed: the field's while the param inherits its type) and
paramToField draws a registered widget in place of the type widget. The
spec liveness row params.widget flips to live and the zod describe drops
its not-read-yet sentence.

Claude-Session: https://claude.ai/code/session_01L6bhgXcNPmrQhpwYYn7fEq
Co-authored-by: Claude <[email protected]>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:ui tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 1 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/spec/liveness/action.json, packages/spec/liveness/state-counts/action.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

⛔ 2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via ActionParamSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-0.mdx (via ActionParamSchema (symbol, a top-level const))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/liveness/action.json, packages/spec/liveness/state-counts/action.md) — pages documenting those are invisible to this run
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 170 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 8e5fabe58b860203774acf212e78b54761499513 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from de3349a55ae358a17c25bbae6c38773d10ece7b3 — the merge of head 57e88ede62adc3fe3804fcb2fdf22cfe37f17e3c into base 8e5fabe58b860203774acf212e78b54761499513, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin de3349a55ae358a17c25bbae6c38773d10ece7b3 && git checkout de3349a55ae358a17c25bbae6c38773d10ece7b3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8e5fabe58b860203774acf212e78b54761499513 57e88ede62adc3fe3804fcb2fdf22cfe37f17e3c && git checkout -B drift-repro 8e5fabe58b860203774acf212e78b54761499513 && git merge --no-ff 57e88ede62adc3fe3804fcb2fdf22cfe37f17e3c

node scripts/docs-audit/affected-docs.mjs --json 8e5fabe58b860203774acf212e78b54761499513

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 8e5fabe58b860203774acf212e78b54761499513 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-sales · session_01L6bhgVfPeFZkN1FP93imKU

Contract-review-claim: session_01L6bhgVfPeFZkN1FP93imKU · head 4d88bf5 · 2026-11-05T15:04Z

The dispatching seat runs the CONTRACT_REVIEW_TIER review of packages/spec/src/ui/action.zod.ts (the hits path) through an at-tier subagent; the rest of the diff is this seat's ACCEPT at TIER_DEFAULT.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-sales · session_01L6bhgVfPeFZkN1FP93imKU

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4d88bf5a91ef89d0644e209411d7371dd7a8acf1
Local-runs: none
Scope: packages/spec/src/ui/action.zod.ts + packages/spec/liveness/action.json, packages/spec/liveness/state-counts/action.md, content/docs/references/ui/action.mdx, .changeset/30260-action-param-widget-reader.md, ui/packages/fields/src/resolveActionParams.ts, ui/packages/fields/src/index.tsx (fieldWidgetMap, resolveFormWidgetType, getLazyFieldWidget), ui/packages/fields/src/widgets/RegistryRefField.tsx, ui/packages/fields/src/widgets/LookupField.tsx, ui/packages/app-shell/src/utils/paramToField.ts, ui/packages/app-shell/src/utils/paramValueShape.ts, ui/packages/app-shell/src/views/ActionParamDialog.tsx, ui/packages/core/src/actions/ActionRunner.ts, ui/packages/types/src/ui-action.ts, ui/apps/console/src/components/FormPage.tsx, ui/packages/components/src/renderers/form/form.tsx, ui/packages/plugin-grid/src/components/bulkParamToField.ts, packages/platform-objects/src/identity/sys-user.object.ts (on main), packages/spec/src/data/field.zod.ts (FieldSchema.widget), docs/adr/0131 D4, skills/objectui/rules/protocol.md and skills/objectstack-ui/rules/actions.md (on main), scripts/check-changeset-no-major.mjs, .changeset/config.json, AGENTS.md (Post-Task Checklist step 3, Prime Directive 14).

Inputs read: card #30260 (body and all four comments, including ruling comment 6652583019 and os-dev-report 6661310924), PR #30543 (body, file list, both comments), the net diff origin/main...4d88bf5a (11 files, +310/−17), and the 42 check-runs on the head.

① Derived judgments

  • ActionParamSchema accept set — unchanged (right). widget: z.string().optional() is byte-identical before and after; the hunk removes the describe's last sentence ("Not read yet …"), the // pointer comment and the JSDoc "Planned" sentence, and adds the reader description. No key, value or refinement moves; the .refine on targetless inline pickers still keys on type alone. Not a Clause-② event on @objectstack/spec's parse door.
  • Published describe text → content/docs/references/ui/action.mdx (right). All three widget rows in the generated page drop exactly the removed sentence and nothing else; check:docs is in the PR's gate record and "Build Docs" is still running at read time (named below).
  • Liveness row flip params.widget planned → live (right). packages/spec/liveness/action.json cites both readers by symbol (resolveActionParams.ts#resolveActionParam, paramToField.ts#paramToField), drops authorWarn/authorHint, and the container note loses its "planned member" clause; state-counts/action.md moves 70/3 → 71/2 with classified 74 unchanged. Both symbols exist at the head; the "Spec property liveness" check-run concluded success, so the evidence pointers resolve. The note truthfully records the one reader that does not read it (the bulk dialog).
  • @object-ui/core ActionParamDef.widget?: string — a new optional member on an exported interface (right, widening). @object-ui/fields RawActionParam.widget?: string likewise; RuntimeField.widget is module-private. Nothing is removed or narrowed.
  • @object-ui/types ActionParam is correctly NOT in the changeset (right). It extends Omit of z.input of the spec's ActionParamSchema (minus type), so it already carried widget from the spec; the new test "accepts the widget through the public ActionParam authoring type" pins that, not a widening.
  • paramDegradesWithoutTarget (app-shell, exported) changes its answer (right, declared in the PR body). A param whose authored widget names a registered widget now answers false before the lookup-without-target test runs. The documented equivalence with "paramToField replaced the widget with text" still holds, but see ④ (d) for the safety-net cost.
  • Metadata-protocol: semantic (right). No key added or refused; an accepted key goes from inert to read, which is a changed meaning.

② Semver level

  • Changeset .changeset/30260-action-param-widget-reader.md: minor on @object-ui/core, @object-ui/fields, @object-ui/app-shell, @objectstack/spec; Clause-②: yes (widening); Metadata-protocol: semantic. Both declaration lines are line-initial in the PR body and the changeset body, as AGENTS.md Post-Task Checklist step 3 requires.
  • Level — right. The rule is "a kind takes at least minor on a package the diff moves"; semantic is a kind, and the diff moves all four packages (the spec's published describe text and liveness ledger ship in the tarball). check-changeset-no-major.mjs needs at least one moved package at minor; four is within the rule. A patch on @objectstack/spec for a describe-only edit would have been arguable only under none, which this PR correctly does not declare. All four are in .changeset/config.json's single fixed group, so they version together regardless; @object-ui/app-shell is private: true but privatePackages.version: true admits its entry. "Check Changeset" concluded success.
  • Clause-②: yes (widening) — right on the measured declaration closure (ActionParamDef, RawActionParam gain one optional member each). No (narrowing) arm, no !, no ADR-0087 disposition owed.

③ Boundary flags

Every flag in the PR body's Acceptance notes and the os-dev-report (6661310924); open_questions is empty.

  1. Bulk dialog not covered (carrier: seat) — answered, not a FAIL. The selection bar's bulkParamToField reads a BulkActionParam with no widget, so a promoted bulk action's param keeps its type widget. The first consumer (Create User, locations: ['list_toolbar']) never takes that path, and the liveness note states the limit. Escalated to the seat: file the follow-up card (dedupe words the report gives: bulk dialog param widget, toBulkParam widget, bulkParamToField widget) so the ledger's "draws it in the dialog" becomes true of both dialogs.
  2. Private copy of the registered-widget test (carrier: seat) — answered, should-fix as a follow-up. authoredWidgetKey is one line over the shared resolveFormWidgetType with the same text-fallback reading as FormPage's namesRegisteredWidget; no widget list is copied, so it is not the table-fork class. A shared export lives in @object-ui/fields' barrel and ui/apps/console, outside the claim's surface; acceptable here, folded into the bulk-dialog card or its own.
  3. Field-widget inheritance rule (param overriding type drops the field's widget) — judged acceptable, with one should-fix. The spec's mode-1 text says a field-backed param takes its "widget mapping from object metadata"; a widget is a picker FOR the field's value type, and ADR-0104 D2 validates the submitted value by the PARAM's type, so carrying a permission-ref picker onto a param that re-typed itself textarea would draw a picker over a value the param no longer declares. The rule is now stated in the hits file's JSDoc and in inheritedWidget. Should-fix: the comparison param.type === field.type is on raw spellings, so a param restating the field's type through a folded alias (reference for a lookup field, PARAM_TYPE_ALIASES) drops the widget although it inherits the same widget key; compare through resolveParamWidgetType(param.type) === resolveFormWidgetType(field.type), or say in the JSDoc that only the literal spelling counts.
  4. Skills-docs: follow-up owed — answered; escalated to the seat. The claim barred skills/**, so the lines PR feat(spec): an action param can name the widget that draws it (permission-ref picker) #30321 listed stay owed. On main, skills/objectstack-ui/rules/actions.md already says a field-backed param takes "widget" from the object metadata — a sentence this PR makes true (with the type caveat above) — and skills/objectui/rules/protocol.md's "Action Params Use Field Types" rule lists no widget among the inline param keys. The report names a "follow-up card" carrier but no card number: the seat files it.
  5. Report deviations outside this review's judgment, recorded: the merge commit without the session trailer pair; PR unassigned and no reviewer requested (the seat's hand-over step); two generated files outside the claim's named surface (both regenerated from the named spec edits, consistent with AGENTS.md "Touched packages/spec").

④ Design judgment

packages/spec/src/ui/action.zod.ts — the widget describe and JSDoc, judged against the two readers on main + this PR, the card's ruling (6652583019) and the first consumer (#30117's Create User permissionSet param).

Against the ruling. Comment 6652583019 asked for three things in this PR: the liveness row to live citing the console reader with authorWarn/authorHint dropped (done, both readers cited); the describe's "Not read yet" sentence and the // pointer comment deleted (done, plus the JSDoc "Planned" sentence); the Skills-docs lines (deferred under the claim's skills/** bar, ③ 4). Holds.

Does the describe now tell the truth? Clause by clause, read in the tree:

  • "names a registered field component (resolved as field: + name) to draw it instead of the type default": resolveActionParam copies param.widget on the inline and unresolved branches and inheritedWidget(param, field) on the field-backed branch; paramToField sets type = authoredWidgetKey(param) ?? resolveParamWidgetType(param.type); ActionParamDialog mounts getLazyFieldWidget(field.type), whose loaders are the same fieldWidgetMap entries registerField registers under field: keys. True.
  • "same key and vocabulary as FieldSchema.widget, and type stays the value's type": FieldSchema.widget's describe (field.zod.ts) is the same sentence; the resolver leaves type untouched, paramFormatErrorKey validates by resolveParamWidgetType(param.type), and the dispatcher's ADR-0104 D2 check is unchanged. True.
  • "e.g. permission-ref on a text param picks a permission set by name (ADR-0131 D4)": fieldWidgetMap['permission-ref'] is PermissionRefField, which lists GET /api/v1/meta/permission through DataSource.listMetadataItems and writes only the picked or typed machine name — D4's "a picker lists the registry, an assignment names one of its entries". paramValueShape already carries the permission-ref: string contract row. The new dialog pin mounts exactly this shape inside SchemaRendererProvider and resolves { permissionSet: 'support_agent' }. True.
  • "Degrades to the type renderer when unregistered": authoredWidgetKey answers undefined for a spelling resolveFormWidgetType folds to its text fallback (unless the author wrote text), so the param's OWN type widget is drawn (select stays select, textarea stays textarea; pinned). True.

Against the first consumer. On main Create User's param is inline { name: 'permissionSet', type: 'text', … } with a comment saying a picker "is a widget hint params do not inherit". Adding widget: 'permission-ref' there (#30117's server PR) takes the inline branch → permission-ref key → the picker, with the submitted value still a text name the route validates as a string. Holds; #30117's PR should also retire that now-false comment (noted for that PR, not this one).

Attempts to break it.

  • (a) A widget name registered only at runtime — a host's or plugin's field: + name entry in ComponentRegistry that is not a fieldWidgetMap key. The SDUI form renderer (form.tsx#renderFieldComponent) consults ComponentRegistry.get('field:' + type) and draws it; the action dialog's resolveFormWidgetType reads the module-literal fieldWidgetMap only, so the same name is "unregistered" there and the param degrades to type. The console's FormPage (namesRegisteredWidget) has the identical limit, so "same vocabulary as FieldSchema.widget" is true of the console path and not of the SDUI form path. Should-fix: say in the describe/JSDoc that the vocabulary is @object-ui/fields' widget map (the keys FORM_FIELD_TYPES lists), or have authoredWidgetKey also accept a ComponentRegistry field: hit. Degradation is safe and the first consumer is in the map, so not must-fix.
  • (b) Bulk dialog path — ③ 1; a should-fix carried by the follow-up card, not a defect of the describe, which the liveness note already qualifies.
  • (c) A field-backed param restating its type — { field: 'permission_set', type: 'text' } inherits (pinned); the alias-spelling gap is ③ 3's should-fix. The hits file's JSDoc says "a field-backed param that inherits its type inherits the field's widget"; the resolver also inherits on a restated type. Should-fix (wording): "inherits or restates its type".
  • (d) A registered widget that is itself a target-needing picker — { name: 'x', type: 'text', widget: 'lookup' } with no reference passes the spec (the refinement keys on type), and paramDegradesWithoutTarget now answers false for ANY registered authored widget, so LookupField is mounted with reference: undefined (its own no-data-source arm; it does not crash) and the dev warning plus the "paste an id" hints are lost. Same for a field-backed lookup whose field carries no reference. Should-fix for the lane seat's ACCEPT: test the DRAWN key (authoredWidgetKey(param) ?? resolveParamWidgetType(param.type)) against LOOKUP_WIDGET_TYPES && !param.referenceTo instead of short-circuiting on the authored widget alone; the equivalence pin still holds. Author-error shape, no consumer today, so not a FAIL.
  • (e) A retired spelling (widget: 'owner'): resolveFormWidgetType returns it unchanged, so getLazyFieldWidget mounts the tombstone — a visible refusal, not a "degrade to type". That is FieldSchema.widget's documented semantic (objectui#4814) and the hits file's JSDoc already says it degrades "like the field key", so no change asked.
  • (f) widget: 'text' on a textarea param is honoured as text (pinned); the unresolved-field branch copies the widget but the dialog refuses before paramToField, so nothing draws it; expectedParamShape's "only FORM_FIELD_TYPES keys or text" invariant survives because authoredWidgetKey returns map keys or retired keys only, the same set resolveParamWidgetType already produced.

Must-fix: none.
Should-fix (one line each): (1) ③ 3 / ④ (c): compare the restated type through the alias fold, or say only the literal spelling counts; (2) ④ (c): JSDoc "inherits or restates its type"; (3) ④ (a): name the vocabulary as @object-ui/fields' widget map, or consult the field: registry too; (4) ④ (d): paramDegradesWithoutTarget tests the drawn key, not "any authored widget"; (5) ③ 1 + ③ 4: the seat files the bulk-dialog card and the Skills-docs card.

Check-runs on the head at read time (42): 12 success — Auto Label, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint · ESLint, PR-body guards, Plan Test Core shards, Spec property liveness, filter; 7 skipped (opt-in and schedule gates); 23 in_progress — Build Core, Build Docs, Build UI Deps, the seven Lint groups, Live E2E UI, Test Core 1/8–8/8, Type Check · consumer gates / debt ledger / source gates / workspace. 0 failure. This verdict is the design verdict; the Tier S landing rule's "every check is green" is read off the head separately, and the 23 still running are named here so the seat does not read this record as that reading.

Implemented-by: claude/project-thread-ijgrij
Reviewed-by: session_01L6bhgVfPeFZkN1FP93imKU

VERDICT: PASS


Generated by Claude Code

…h a restated field type through the param-type fold

Contract-review should-fixes: paramDegradesWithoutTarget asks the drawn
widget key, so a text param naming the lookup widget with no target
degrades and warns; inheritedWidget compares the param and field types
through resolveParamWidgetType, so an alias spelling still restates the
type; the spec JSDoc says inherits or restates.

Claude-Session: https://claude.ai/code/session_01L6bhgXcNPmrQhpwYYn7fEq
Co-authored-by: Claude <[email protected]>
…erty warning

The params.widget liveness row is live now that the console reads it, so
the pin that expected the planned warning becomes its inverse, with a
control that the same walk still warns on a planned row.

Claude-Session: https://claude.ai/code/session_01L6bhgXcNPmrQhpwYYn7fEq
Co-authored-by: Claude <[email protected]>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-sales · session_01L6bhgVfPeFZkN1FP93imKU

Contract review · delta · 2026-11-05T15:47Z

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 57e88ede62adc3fe3804fcb2fdf22cfe37f17e3c
Local-runs: none
Scope: packages/spec/src/ui/action.zod.ts + ui/packages/fields/src/resolveActionParams.ts (inheritedWidget, PARAM_TYPE_ALIASES, resolveParamWidgetType), ui/packages/app-shell/src/utils/paramToField.ts (paramDegradesWithoutTarget, authoredWidgetKey, paramToField), ui/packages/app-shell/src/utils/paramToField.test.ts, ui/packages/app-shell/src/utils/resolveActionParams.test.ts, ui/packages/core/src/actions/ActionRunner.ts, packages/lint/src/lint-liveness-properties.test.ts, ui/packages/app-shell/src/views/metadata-admin/previews/ActionPreview.tsx (runtimeWidgetFor, read on the head, unchanged by the delta).

Delta record on PR #30543: the two commits after the PASS record 6661565862 on 4d88bf5a — ae7e6fa4 (should-fixes 1, 2 and 4 of that record) and 57e88ede (the @objectstack/lint liveness pin). Delta = git diff 4d88bf5a 57e88ede: 7 files, +67/−23; merge base with main unchanged (fa9c0aa2). Everything that record judged and the delta does not touch stands as written there.

① Derived judgments

  • ActionParamSchema accept set — still unchanged (right). The hits file's delta is two JSDoc lines ("inherits or restates its type"); the .describe() text is byte-identical, so content/docs/references/ui/action.mdx rightly has no delta and no regeneration was owed. No key, value or refinement moves.
  • No new public surface (right). ActionParamDef's delta is one doc line; inheritedWidget is module-private; paramDegradesWithoutTarget keeps its signature and changes its answer only for a param whose authored widget is itself a target-needing picker. Nothing for the changeset to add; Clause-②: yes (widening) and Metadata-protocol: semantic stand.
  • The lint pin flip (right, and owed). packages/lint/src/lint-liveness-properties.test.ts pinned the params.widget row as planned + authorWarn through the SHIPPED ledger; the first head flipped that ledger row to live without flipping the pin, and that is exactly the red the first head's final check-run state showed (Test Core (5/8): "× warns on an action param that names a widget — planned, nothing reads it yet"). The delta turns it into a negative pin ("no longer warns … live since its console reader") with the live siblings still silent and a CONTROL that the same walk still fires liveness-planned-property on a drilled planned row elsewhere (flows[].variables.objectName), so the silence is the widget row's verdict and not a walk that reaches nothing. Right.

② Semver level

Unchanged by the delta: minor on the four packages, Clause-②: yes (widening), Metadata-protocol: semantic. The delta publishes nothing beyond what the first record graded; "Check Changeset" is success on the head.

③ Boundary flags

Should-fixes of record 6661565862, each judged on the tree:

  1. Should-fix 1 — alias fold in inheritedWidget: folded correctly. param.type === undefined inherits; otherwise resolveParamWidgetType(param.type) === resolveParamWidgetType(field.type) decides, with field.type undefined answering no. Folding the FIELD type through the param alias table is harmless (no spec FieldType is a PARAM_TYPE_ALIASES key) and equivalent to the resolveFormWidgetType comparison the record asked for. Pinned: { field: 'account', type: 'reference' } on a lookup field inherits record-ref, and type: 'text' on it does not. Residual, noted and no change asked: the fold's text fallback makes two spellings the form draws as text compare equal, so a param restating text over a field of a type with no widget of its own inherits that field's widget — the widget was chosen over a text-drawn value either way.
  2. Should-fix 2 — JSDoc wording: folded correctly in the hits file and mirrored in ActionParamDef's doc, and the resolver's own comment now states the fold ("in any spelling the param-type fold maps onto the same widget"). The three readings agree.
  3. Should-fix 4 — degradation judged by the drawn key: folded correctly. paramDegradesWithoutTarget computes drawn = authoredWidgetKey(param) ?? resolveParamWidgetType(param.type) and answers LOOKUP_WIDGET_TYPES.has(drawn) && !param.referenceTo — the same expression paramToField draws with, so the documented equivalence ("true iff paramToField replaced the widget with the text fallback") holds mechanically again. Pinned: { type: 'text', widget: 'lookup' } with no target degrades to text and warns once; with referenceTo it draws lookup and does not warn; { type: 'lookup', widget: 'permission-ref' } never degrades (the earlier pin). ActionParamDialog reads the predicate for its hints, so the paste-an-id placeholder returns on that shape too.
  4. Should-fixes 3 (vocabulary wording / registry hit) and 5 (the bulk-dialog and Skills-docs follow-up cards) were not ordered into this delta and stay open with the seat, as carried in the first record.
  5. open_questions on the card: none new; no new dev flag accompanies the delta.

④ Design judgment

packages/spec/src/ui/action.zod.ts — the delta is the JSDoc sentence only. Judged against the reader it describes: inheritedWidget on the head inherits the field's widget when the param declares no type or restates it through the fold, and drops it otherwise, so "a field-backed param that inherits or restates its type inherits the field's widget with it" is now true of the code in both the literal and the alias spelling. The describe text was already true at the first head and is untouched; the first consumer (Create User's inline permissionSet + widget: 'permission-ref') takes the inline branch, which the delta does not touch. Nothing new reaches the contract face.

Attempts to break the delta.

  • (a) A field with type undefined and a param restating a type: restated is false → no widget inherited. Correct — nothing says what the field's widget was for.
  • (b) { type: 'lookup', widget: 'no_such_widget' } with no target: authoredWidgetKey is undefined, drawn = lookup, degrades and warns exactly as before the PR (the first record's CONTROL pin still holds).
  • (c) A retired spelling as the authored widget (widget: 'owner'): drawn = owner, not in LOOKUP_WIDGET_TYPES, so the tombstone is mounted — the visible refusal, unchanged.
  • (d) The dev-mode warning on the new degradation shape reads Param "x" is type "text" but has no reference target: the prescription (declare reference, or go field-backed) is right, but the sentence names param.type where the picker came from widget. Should-fix (cosmetic, lane seat): name the drawn widget in the message when it came from widget.
  • (e) ActionPreview.tsx#runtimeWidgetFor (Studio's action preview, outside the claim's surface, unchanged by the PR) builds the predicate's input from type and reference only and answers resolveParamWidgetType(p.type), so a widget: 'permission-ref' param previews as a text box while the runtime dialog draws the picker. Not a defect of the describe — the preview is a third reader that was never told about widget — and not introduced by the delta. Should-fix (follow-up card, seat): pass widget through runtimeWidgetFor and answer the drawn key, so the preview's "what will render" matches paramToField.

Must-fix: none.
Should-fix still named (one line each): (1) ④ (d): the degradation warning names the drawn widget when it came from widget; (2) ④ (e): Studio ActionPreview.runtimeWidgetFor reads widget — follow-up card; (3) carried from 6661565862: should-fix 3 (vocabulary = @object-ui/fields' widget map, or consult the field: registry) and should-fix 5 (the bulk-dialog and Skills-docs cards).

Check-runs on the head at read time (65): 58 success, 7 skipped, 0 failure, 0 in progress — every gate family the first record named as still running (Build Core/Docs/UI Deps, the seven Lint groups, Live E2E UI, Test Core 1/8–8/8, Test UI 1/16–16/16, Type Check ×4 and Type Check UI) concluded success on this head. For the record: the first head 4d88bf5a finished red on Test Core (5/8) (the lint pin above) and on the Test UI rollup only because its shards were cancelled by the push of this head; both are answered by 57e88ede.

Implemented-by: claude/project-thread-ijgrij
Reviewed-by: session_01L6bhgVfPeFZkN1FP93imKU

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-sales · session_01L6bhgVfPeFZkN1FP93imKU

维护者速读

  • 改了什么:动作参数写了 widget 时,动作对话框用这个已注册的字段控件来画(例如 type: 'text' 加 widget: 'permission-ref' 画成权限集选择器);名字没注册就照旧按 type 画。字段来源的参数只在继承或重申字段的 type 时才继承字段的 widget。spec 里 params.widget 的 liveness 由 planned 改为 live。服务器仍按 type 校验提交值。
  • 为什么改:ui: resolveActionParams and paramToField pass a param widget through, so an action dialog draws the permission-set picker (#30117 slice 2) #30260。Create User 的权限集是只认机器名的纯文本框(Create User: the permission set is a free text box that only accepts the machine name #30117),要给它一个选择器,先要有控制台读 widget。
  • 风险与回滚:只加不减。@object-ui/core 的 ActionParamDef 与 @object-ui/fields 的 RawActionParam 各加一个可选成员 widget?: string(Clause-② yes,加宽);Metadata-protocol: semantic,所以四个包的 changeset 是 minor。今天没有内联参数写 widget;会变的只有字段来源、字段本身带 widget 且参数没改 type 的参数,它们改画字段的控件,这正是本卡要的。回滚即撤本 PR。
  • 先看:ui/packages/fields/src/resolveActionParams.ts 的 inheritedWidget;ui/packages/app-shell/src/utils/paramToField.ts 的 authoredWidgetKey 与 paramDegradesWithoutTarget;packages/spec/src/ui/action.zod.ts 的 describe 与 JSDoc。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/utils/paramToField.test.ts packages/app-shell/src/utils/resolveActionParams.test.ts packages/app-shell/src/utils/paramValueShape.test.ts packages/app-shell/src/views/ActionParamDialog.test.tsx,预期 177 passed。
  • 需要你做的:无,席位落地。
  • 席位意见:可合。

席位核对 — ACCEPT · head 57e88ed

  • 形态:12 文件 +369/−32(源码 resolveActionParams.ts +47、paramToField.ts +38/−3、ActionRunner.ts +13、action.zod.ts +5/−6;三份 ui 测试 +219;lint 测试 +20/−14;生成物 action.mdx、liveness/action.json、state-counts/action.md;changeset 一份,@object-ui/core、@object-ui/fields、@object-ui/app-shell、@objectstack/spec 各 minor)。check-governed-merges --pr 30543:NOT governed(0/12),401 行 ≤ 3000。Fixes #30260,draft,目标 main,标题无 !。
  • 契约:reviewFace 命中 packages/spec/src/ui/action.zod.ts ⇒ CONTRACT_REVIEW_TIER。认领行 6661361322;首轮复核 PASS 6661565862(head 4d88bf5a,无 must-fix);本席把其中 should-fix 1、2、4 派进本 PR,delta 复核 PASS 6662404259(head 57e88ede,无 must-fix,同一复核者)。其余声明面(Clause-②: yes (widening),两个导出类型各加一个可选成员,在构建出的 .d.ts 上量过)是 TIER_DEFAULT,由本席在此写出:只加宽,没有收窄,没有新导出名。Check Changeset 在本 head 上 success。
  • 席位逐行读:resolveActionParam 在每个分支都把参数自己的 widget 带上;字段来源分支里,参数没写 type,或写的 type 经参数类型折叠后与字段相同(resolveParamWidgetType 两边比),才继承字段的 widget,字段没有 type 时不继承。paramToField 先看 authoredWidgetKey(只认 resolveFormWidgetType 能映射到已注册控件的名字,回落 text 视为未注册),有就画它,否则按 type;paramDegradesWithoutTarget 用同一个「实际要画的 key」判断,所以「没有目标时降级成文本框」与 paramToField 机械一致。ActionRunner.ts 只给 ActionParamDef 加 widget? 和文档。spec 只改 describe 与 JSDoc 的措辞(去掉「尚未读取」,写明继承或重申),accept 集不变;liveness 行转 live 并引用两个读者,packages/lint 的旧 planned 钉改为反面钉,带一例 CONTROL 证明遍历仍会在别处的 planned 行上报警。
  • 前提实测:dev 报告 6661310924(首个 head)与 6662497551(复核修正后的 head)。首轮:三份测试新钉 16 例;消融三腿(去掉作者 widget 一支 5 红、内联 widget 置空 2 红、删继承一行 3 红),还原后 blob 与 HEAD 相同;派生门禁 105 条全部 exit 0,0 NOT-MEASURED;check:spec-symbols、check-sdui-manifest、三包 type-check exit 0。复核修正一轮:新增两例(widget: 'lookup' 无目标降级并告警一次、带目标画 lookup 不告警;lookup 字段上写 type: 'reference' 继承、写 text 不继承),消融两腿(drawn key 只剩 type 一支 2 红、重申比较退回原拼写 1 红),还原后 blob 与 HEAD 相同;lint 测试改前 1 红复现 CI、改后 113/113;四个 ui 测试文件 177/177;派生门禁 106 条全部 exit 0,0 NOT-MEASURED;check:docs 绿,JSDoc 改动没有移动生成物。
  • 门禁 check-runs:head 上 65 个(按名取最新一次),56 success,9 skipped,0 红;本席改正文后 Check Changeset 与 PR-body guards 重跑仍 success,Auto Label、Check PR Size 在 edited 事件上照例跳过(首跑 success)。首个 head 的 Test Core (5/8) 红是 lint 钉(本 PR 自己的,已由 57e88ede 改正),Test UI 汇总红是被新推送取消的分片,不是真红。mergeable_state clean。
  • 接受的偏差:字段 widget 只在继承或重申 type 时继承,这是 dev 对 spec「widget 来自对象元数据」的读法,本席采纳,spec JSDoc 已写成同一句。控制台只认 @object-ui/fields 的控件表,运行时 ComponentRegistry 里另注册的控件不算(复核 should-fix 3),与 FormPage 现有限制相同,本席接受。降级时的开发期告警写的是 param.type,而选择器其实来自 widget(delta 复核 ④(d)),只影响开发者控制台的一句话,本席不为它再跑一轮。两个生成物在认领文件面外,由所列 spec 改动生成;packages/lint/src/lint-liveness-properties.test.ts 也在文件面外,是 liveness 翻转后必须改的钉(本席 15:17Z 同意)。PR 正文的 Acceptance notes 已由本席经中继补上复核修正一句、并把继承规则改成「继承或重申」。合并提交 4d88bf5a 没有 Claude-Session 尾行,已推送,不改写历史。
  • 范围外去向:Skills 文档两行已立卡 skills: teach an action param's widget (objectui protocol inline params; objectstack-ui actions field-backed inheritance) once #30260 lands #30549(Blocked-by ui: resolveActionParams and paramToField pass a param widget through, so an action dialog draws the permission-set picker (#30117 slice 2) #30260)。选择栏的批量对话框(plugin-grid bulkParamToField)不读 widget:今天没有批量参数写它,liveness 注记已写明,不立卡。Studio 动作预览 ActionPreview.tsx 的 runtimeWidgetFor 不读 widget(delta 复核 ④(e)):今天没有内联参数写 widget,第一个写它的是 Create User: the permission set is a free text box that only accepts the machine name #30117,已在 Create User: the permission set is a free text box that only accepts the machine name #30117 留言 6662463937 携带,并顺带请那张卡的 PR 删掉 sys-user.object.ts 约 359 行「widget hint params do not inherit」那句过时注释。authoredWidgetKey 与 FormPage 的 namesRegisteredWidget 重复一行:要共享就得动 @object-ui/fields 入口与 ui/apps/console,在文件面外,记录不立卡。
  • 落地条款:中继 pr_ready + automerge_enable。合并后由 Fixes 关卡,席位同一笔摘掉 pm:dispatched 并留落地评论;skills: teach an action param's widget (objectui protocol inline params; objectstack-ui actions field-backed inheritance) once #30260 lands #30549 与 Create User: the permission set is a free text box that only accepts the machine name #30117 由此解锁。

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 15:58
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit f5aaa0b Nov 5, 2026
70 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-ijgrij branch November 5, 2026 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ui: resolveActionParams and paramToField pass a param widget through, so an action dialog draws the permission-set picker (#30117 slice 2)

1 participant