Skip to content

Commit a4c3246

Browse files
docs(skills): teach an action param's widget key in the action dialog (#30684)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DRWthi8yfG9DK3ZmXw4eGpW)_ domain:skills#1 · os-elon-musk · session_016WV4cyNZ7aKsfF5vTBgKKn Fixes #30549 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** 两个技能文件教动作参数的 `widget`:objectui 协议规则的内联参数清单加上 `widget`(写注册过的字段控件名,如 `text` 参数写 `'permission-ref'` 就是权限集选择器;未注册的名字退回 `type` 控件,批量对话框不读它,服务端仍按 `type` 校验);objectstack-ui 动作规则说清字段型参数只在沿用或重述字段 `type` 时继承字段的 `widget`,改了 `type` 要自己写 `widget`。 - **为什么改:** PR #30543 落地后控制台已读这个键,但技能没教,作者和 AI 仍写自由文本参数;原句还暗示字段型参数总会继承字段控件,与代码不符。 - **风险与回滚:** 只改文档,无 schema、键或行为变化;token 预算靠同文件删重复句付清,不抬上限。回滚即撤销本 PR。 - **先看:** ① `skills/objectui/rules/protocol.md` 的 Inline params 条;② `skills/objectstack-ui/rules/actions.md` 的 Action Parameter Patterns 段;③ 两文件中被删的重复句(付账用)。 - **两分钟验证:** `node scripts/check-skills-token-ratchet.mjs`,预期 actions.md 2259/2259、protocol.md 4352/4368,整体通过。 - **需要你做的:** 审阅并批准(`skills/**` 为 Tier H,需人工合并)。 ## What changes - `skills/objectui/rules/protocol.md`, "Inline params": the widget-config list gains `widget`. It names a registered field widget that draws the param instead of its `type` widget (`'permission-ref'` on a `text` param is the permission-set picker); an unregistered name keeps the `type` widget; the selection bar's bulk dialog ignores it; the server validates by `type`. - `skills/objectstack-ui/rules/actions.md`, "Action Parameter Patterns": the field's `widget` is inherited only while the param keeps or restates the field's `type`; a param that overrides `type` names its own `widget`. - One root changeset, `'@objectstack/skills': patch`. Every taught fact is read off `origin/main` (`b77c160a`), not the card's prose: - `ui/packages/fields/src/resolveActionParams.ts` `inheritedWidget`: the param's own `widget` wins; with no `type` the field's `widget` is inherited; with a `type`, only when `resolveParamWidgetType(param.type) === resolveParamWidgetType(field.type)`. Inline and unresolved branches copy `param.widget`. - `ui/packages/app-shell/src/utils/paramToField.ts` `authoredWidgetKey`: a name `resolveFormWidgetType` resolves (not the `text` fallback) replaces the `type` widget key; otherwise the `type` widget stays. - `packages/spec/liveness/action.json` `params.widget`: `live`; the dispatcher still validates the submitted value by `type`; not read by plugin-grid `bulkParamToField`. - `permission-ref` is a registered widget (`ui/packages/fields/src/FieldEditWidget.tsx`, `ui/packages/components/src/renderers/form/form.tsx`). ### Token ratchet: paid by same-file deletion, no ceiling raise | file | before (bytes / tokens) | after (bytes / tokens) | ceiling | |---|---|---|---| | `skills/objectstack-ui/rules/actions.md` | 9024 / 2256 | 9033 / 2259 | 2259 | | `skills/objectui/rules/protocol.md` | 17456 / 4364 | 17408 / 4352 | 4368 | Lines: actions.md 179 → 180, protocol.md 377 → 377 (whole-file `wc -l`). No SKILL.md changes, so the package's SKILL.md sum is unchanged. Deleted restatements (each already said elsewhere in the same file): - protocol.md: "resolve the value in the host … or carry it on a `text` node's `content`" under *Fields That are NOT Evaluated* (the Bindable Text Keys rule says it, and now carries the `SchemaRenderer` wording); "There is no way to opt a type in from metadata …" (the closed-list sentence above it says it); "the third row works today, but its channel is an open contract question" (the Keys Live on the Node rule says it, and the sentence already points there). - actions.md: "so a body that must scope by org reads it from `ctx` explicitly" (the preceding sentence says how to read the org); the `skipped while newTabUrl is set` comment (the prose says `newTabUrl` skips the round-trip); `static template;` in a comment (the heading line says static `target`). ## Why After PR #30543 the action dialog draws a param's `widget`, but neither skill named the key, and the actions rule promised that every field-backed param inherits the field widget. Authors kept writing free-text params where a picker exists. ## Acceptance notes - The bulk dialog's limit is stated as a limit, not taught as supported (card "Not in scope"). - The field-backed bullet in `protocol.md` still lists what is inherited without `widget`; the inheritance rule is taught once, in `actions.md`, to stay inside the ceiling. ## Verification All local readings are on head `f49a7c33` (the only commit), base `b77c160a`. <details><summary>Gates: 44 derived families + pnpm lint</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 44 families from the diff; each ran with its exit code captured before any pipe, then `--ran` reconciled: `✓ dispatch-gates --ran: 44 derived famil(ies) accounted for — 43 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).` - 43 families: exit 0, including `check-skills-token-ratchet.mjs` (and `--self-test`), `pnpm --dir ui exec vitest run scripts/`, `check:skill-examples`, `check:doc-formula-expressions`, `check:doc-authoring`, `check:skill-identifier-liveness`, `check:nul-bytes`, `check-empty-changeset`, `check-changeset-no-major`, `check-adr-0087-registration`. - NOT MEASURED: `pnpm check:dual-build-cjs-loads`, reason: exit 3, PREREQUISITE NOT MET (needs a whole-tree build; only the `@objectstack/client-react...`, `@objectstack/formula`, `@objectstack/lint...` closures were built). CI runs it. - `pnpm lint`: exit 0. - `check-commit-card-trailers.mjs --range origin/main..HEAD`: pass. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_016WV4cyNZ7aKsfF5vTBgKKn)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 254f2ec commit a4c3246

3 files changed

Lines changed: 28 additions & 15 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): teach an action param's `widget` key in the action dialog
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- `objectui` (`rules/protocol.md`, inline params): the widget-config list gains `widget`. It names a registered field widget that draws the param instead of its `type` widget (`'permission-ref'` on a `text` param is the permission-set picker); an unregistered name keeps the `type` widget, and the selection bar's bulk dialog does not read it. The server still validates the submitted value by `type`.
11+
- `objectstack-ui` (`rules/actions.md`, field-backed params): a field-backed param inherits the field's `widget` only while it keeps the field's `type` or restates it; a param that overrides `type` names its own `widget`.
12+
- Paid inside the token ceilings by removing restatements in the same files. No schema, key or behaviour changes.

‎skills/objectstack-ui/rules/actions.md‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,7 @@ A server-side `body` (or a registered `handler`) receives `ctx` with `input` (th
115115
the active organization as `ctx.user?.organizationId ?? ctx.session?.organizationId`
116116
— the blessed name (same axes and `undefined` cases as hooks, objectstack-data).
117117
Bodies execute **trusted** (`ctx.api` /
118-
`ctx.engine` bypass RLS/FLS), so a body that must scope by org reads it from
119-
`ctx` explicitly. `ctx.session.positions` (absent when the caller holds none) is
118+
`ctx.engine` bypass RLS/FLS). `ctx.session.positions` (absent when the caller holds none) is
120119
**not an authorization input** — ask the security service for privilege (ADR-0095).
121120

122121
## Opening in a New Tab (`openIn` / `opensInNewTab` / `newTabUrl`)
@@ -135,7 +134,7 @@ export const PrintA3Action = defineAction({
135134
name: 'print_a3',
136135
label: 'Print Summary Sheet (A3)',
137136
type: 'url',
138-
target: '/print/a3?id=${record.id}', // static template; interpolated at click
137+
target: '/print/a3?id=${record.id}', // interpolated at click
139138
openIn: 'new-tab',
140139
locations: ['list_toolbar'],
141140
});
@@ -156,7 +155,7 @@ export const OpenInvoicePdfAction = defineAction({
156155
label: 'Open PDF',
157156
objectName: 'invoice',
158157
type: 'script',
159-
target: 'open_invoice_pdf', // registered handler; skipped while newTabUrl is set
158+
target: 'open_invoice_pdf', // registered handler
160159
opensInNewTab: true,
161160
newTabUrl: '/api/v1/invoice/{recordId}/pdf',
162161
locations: ['record_header'],
@@ -172,7 +171,9 @@ export const OpenInvoicePdfAction = defineAction({
172171
## Action Parameter Patterns
173172

174173
Prefer **field-backed** params (`{ field: 'email' }`): label (i18n), type,
175-
validation, options, placeholder and widget come from the object metadata;
174+
validation, options, placeholder and widget come from the object metadata
175+
(`widget` only while the param keeps or restates the field's `type`; a param
176+
that overrides `type` names its own `widget`);
176177
`objectOverride` borrows a field of another object, and `defaultFromRow: true`
177178
pre-fills from the selected row in `list_item` contexts.
178179

‎skills/objectui/rules/protocol.md‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,7 @@ These top-level schema fields are passed as raw strings:
3131
types that declare them** (see "Rule: Bindable Text Keys"); read raw
3232
everywhere else. **Do not "move them to `props`"** to make an expression work
3333
on a type that does not declare them: under `props` they are evaluated and
34-
then discarded, so the component paints an empty frame instead. On an
35-
undeclared type, resolve the value in the host before handing the schema to
36-
`SchemaRenderer`, or carry it on a `text` node's `content`.
34+
then discarded, so the component paints an empty frame instead.
3735
- `className` — always a static Tailwind class string
3836
- `id` — always a static string
3937
- `type` — component type identifier
@@ -95,10 +93,9 @@ declaration rather than keeping its own copy.
9593
```
9694

9795
On any other component type these four keys are read **raw**: the expression
98-
reaches the screen as literal text. There is no way to opt a type in from
99-
metadata — a new type/key pair is a change to the spec's declaration, not
100-
something the renderer infers. Until then, resolve the value in the host, or
101-
use a `text` node's `content`, which every type evaluates.
96+
reaches the screen as literal text. Resolve the value in the host before
97+
handing the schema to `SchemaRenderer`, or use a `text` node's `content`,
98+
which every type evaluates.
10299

103100
## Rule: Keys Live on the Node, Not in a `props` Envelope
104101

@@ -247,8 +244,7 @@ nothing thrown, a console warning each (`[ObjectUI] DataTable data:`; the
247244
dev-build `props`-bag warning). **Do not read
248245
that empty table as "the provider has no data."** The route this skill teaches
249246
is the last row — the host resolves the array and puts it on the node — for the
250-
reason given under "Rule: Keys Live on the Node": the third row works today,
251-
but its channel is an open contract question, not a taught surface.
247+
reason given under "Rule: Keys Live on the Node".
252248

253249
## Rule: Actions Are Node Types, Not An Event Bag
254250

@@ -307,7 +303,11 @@ form-supported type (`select`, `lookup`, `date`, `file`, `image`, `richtext`,
307303
```
308304

309305
- Inline params: `name` + `type` + widget config (`options`, `multiple`,
310-
`accept`, `maxSize`, `defaultValue`, `placeholder`, `helpText`).
306+
`accept`, `maxSize`, `defaultValue`, `placeholder`, `helpText`, `widget`).
307+
`widget` names a registered field widget that draws the param instead of
308+
its `type` widget (`'permission-ref'` on a `text` param is the
309+
permission-set picker); an unregistered name keeps the `type` widget, and
310+
the selection bar's bulk dialog ignores it. The server validates by `type`.
311311
- Field-backed params: `field` (+ `objectOverride`) inherits label, type,
312312
options, lookup config, `multiple`/`accept`/`maxSize` from the object field;
313313
inline properties override.

0 commit comments

Comments
 (0)