Skip to content

Commit 13aea18

Browse files
docs(skills): objectstack-ui action examples and the SKILL.md action rows follow the ActionSchema refinements (#22184)
Fixes #22176 Clause-②: no The published objectstack-ui skill taught action shapes that `ActionSchema` refuses; `defineAction` is `ActionSchema.parse`, so copying either `rules/actions.md` example threw at module load, and two `SKILL.md` rows taught the same refused shape in prose (a `modal` that executes a `body`). This PR changes the two example blocks (plus the one eval sentence that quoted the first) so both parse, and rewords the two `SKILL.md` rows so each states a shape the schema accepts — the smallest prose change that leaves no sentence beside a block, or in a table, false. Draft on a Tier H surface (`skills/**`): it lands on an authorized APPROVED review or the maintainer's hand; no seat readies it. ## What changed (one rule file, one eval sentence, two `SKILL.md` rows) 1. **"Modal-typed action (collect params, then execute server body)"** — the block authored `type: 'modal'` + `target` + `params` + `body`. The schema's refinement refuses a `body` on any non-script action ("a non-script action dispatches on `target` and silently ignores its body. To collect `params` and then run the body, use `type: 'script'`"). The lesson is the body, so the block is now `type: 'script'`, the heading moves with it ("Script-typed action"), and the `target` line goes (a script with an inline `body` has nothing for `target` to name). `params`, `body`, `locations: ['list_toolbar']` and the `ctx` section under it are unchanged. The eval sentence that described the bulk action as `type: 'modal'` beside a `body` now says `type: 'script'`; its `must_contain` list is untouched. 2. **`opensInNewTab` + `newTabUrl` example** — the block authored `type: 'url'` with no `target`; the first refinement requires `target` for `url`. The block is now `type: 'script'` + `target: 'open_invoice_pdf'` (see the re-route below); `opensInNewTab`, `newTabUrl` and `locations` are unchanged. 3. One comment deleted from block 1 (`// Field-backed params resolve label/type/options from object metadata:`) — it restates the "Action Parameter Patterns" paragraph in the same file and pays the token ratchet for the added `target` line (the file sat 4 bytes under its ceiling). 4. **`SKILL.md`, two rows (patch round, same family).** The Action Types table row for `modal` read "Open a dialog (typically collecting `params`, then executing `body`)" and the CRM blueprint row sent parameterized bulk mutations to `modal` — the shape the refinement refuses and this PR removes from `rules/actions.md`. The table row now reads "Open the page named by `target`; a `body` runs only on `script`" — the schema's own words (`target` is "the modal/page name to open"; the refinement message says "keep `type: 'modal'` with `target` naming the page") and what the renderer does (objectui's modal executor opens `action.target` as the dialog schema). The blueprint row now sends parameterized bulk mutations to `script`, the lesson of the "Script-typed action" block. The required-field column, the column widths and every other row are unchanged; no eval states the shape (`modal` and `dialog` have zero hits under `evals/`). ## Positive control (measured, both legs) A one-off node probe parses the two fenced blocks AS WRITTEN ON DISK against the built `packages/spec/dist/ui/index.mjs` (build-input-hash `e5e882202edc406c`, built under the verify lock on this tree, rebuilt and re-measured on the patch-round head): | block | before (`959c209d56`) | after (`eb522522f2`; re-measured byte-identical on `28a140bb45`) | |---|---|---| | `AddToCampaignAction` | `safeParse.success: false`, issue path `body`, `defineAction` throws | `success: true`, no issues, no throw | | `OpenInvoicePdfAction` | `safeParse.success: false`, issue path `target`, `defineAction` throws | `success: true`, no issues, no throw | Before-leg messages, verbatim: "`body` only runs for `type: 'script'` — a non-script action dispatches on `target` and silently ignores its body. To collect `params` and then run the body, use `type: 'script'`; to open a page/modal, drop the `body` and keep `type: 'modal'` with `target` naming the page." and "Action 'target' is required when type is 'url', 'flow', 'modal', 'api', or 'form'." ## Dispatch assumption 3 — measured, falsified, re-routed The order assumed the second block's repair is "keep `type: 'url'`, add `target`". Measured on the sibling renderer (objectui checkout `9990f9e`): - `ActionRunner.builtinExecutors.url` goes to `executeUrl`, which reads `action.target || action.redirect` and navigates; it never consults `opensInNewTab` or `newTabUrl` (`packages/core/src/actions/ActionRunner.ts`). - `newTabUrl` is read in exactly one place — the console server-action wrapper, behind `action.opensInNewTab && newTabUrl` (`packages/app-shell/src/utils/consoleServerAction.ts`), and that wrapper is bound as the `script` handler (`useConsoleActionRuntime.tsx` and `RecordDetailView.tsx`: `script: serverActionHandler`). The spec's own refinement comment on the `newTabUrl` pair says the same: no other path reads the key at all. - Every producer and pin pairs the key with `type: 'script'`: the spec's legal-pair pin in `action-newtaburl-pair.test.ts` (`type: 'script', target: 'ssoOpen', opensInNewTab: true, newTabUrl: …`), the doubled-redirect pin, and all objectui `consoleServerAction` tests. `content/docs/ui/actions.mdx` and `content/docs/protocol/objectui/actions.mdx` both say "for an async handler that redirects, use `opensInNewTab`" and give a `type: 'url'` action the `openIn` control instead. So `type: 'url'` + any `target` would parse and teach a dead pair: the renderer would navigate to `target` and `newTabUrl` would never fire (the schema text itself routes a `url` action's new tab through `openIn`, which is the previous subsection's lesson). Per the ruling intent — spec declaration first, and no wrong sentence on the shipped surface — the block takes the spec's pinned legal pair: `type: 'script'` + `target` naming the registered handler, with an inline comment that the handler is the POST half and is skipped while `newTabUrl` is set. No second URL is invented; `newTabUrl` keeps the one URL. The alternative the order suggested (keep `url`, add `target: '/api/v1/invoice/${ctx.recordId}/pdf'`) would make the example parse but contradict the prose above it and the renderer; if the maintainer prefers it anyway, say so on this PR and it is a one-line change. ## `skills/**` readings (base `959c209d56` → `28a140bb45`) - `rules/actions.md`: 208 → 207 lines; 2107 → 2104 tokens (ceiling 2108; headroom 1 → 4); 8428 → 8414 bytes. - `evals/views-apps-actions-pages.json`: 65 → 65 lines; 1502 → 1502 tokens (ceiling 1505); 6005 → 6006 bytes. - `SKILL.md`: 308 → 308 lines; 3843 → 3844 tokens (ceiling 3856; headroom 13 → 12); 15372 → 15373 bytes — one byte of growth, paid inside the file's headroom. - Whole package: all `skills/*/SKILL.md` 4408 → 4408 lines (210279 → 210280 bytes); `skills/objectstack-ui/**` 2232 → 2231 lines, 124058 → 124046 bytes; ratcheted bundle 144047 → 144045 tokens; whole shipped tree 154833 → 154831 tokens. - Growth in `rules/actions.md` paid by content deletion (the restated comment); the one byte in `SKILL.md` paid inside its headroom. Nothing re-wrapped. ## Verification (tree `28a140bb45`, worktree clean) - `check:skill-examples` (the control): exit 0 on `28a140bb45` after building `@objectstack/client` and `@objectstack/client-react` under the lock — 262 marked examples across 3 surfaces type-check, the "Script-typed action" block among them (`SKILL.md` carries no marked block). On `eb522522f2` the first run had refused with PREREQUISITE NOT MET (exit 3, no built declarations, nothing measured) and passed after the same build. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 24 families on this tree: the first round's 23 plus `pnpm --filter @objectstack/spec run check:skill-docs`, brought in by `SKILL.md` entering the changeset (its generator reads only the frontmatter; it reports "Skill docs in sync"). All 24 ran on `28a140bb45` with exit codes captured before any pipe, all 0; the `--ran` reconciliation reads "24 derived, 24 run, 0 NOT-MEASURED, 0 UNRUN". Families: check-ci-filter-parity; check-closing-keyword-parity (+ self-test); check-comment-mask-corpus; check-doc-route-spelling (advisory + self-test); check-skills-token-ratchet (+ self-test); lint check:doc-formula-expressions; spec check:skill-docs; check:agent-test-spelling; check:corpus-claim-drift; check:cross-package-test-inputs; check:doc-authoring; check:driver-memory-census; check:gitlink-declared; check:nul-bytes; check:pm-governed-merges; check:refd-timer-probe; check:role-word; check:skill-compatibility; check:skill-frame-sync; check:skill-identifier-liveness; check:watch-hint-literal. - Token ratchet on `28a140bb45`: "skills/objectstack-ui/SKILL.md is 3844 tokens (ceiling 3856; headroom 12)" and "54 authored bundle file(s) within their ceilings". - NOT MEASURED locally (CI-owned): the artifact-roster families, whole-root scans, the type-check lanes and the Test Core shards. No package source changed, so no package `test`/`typecheck` is owed. - Changeset: none — `skills/**` publishes nothing from a released package (`skip-changeset`). ## Acceptance notes - The `SKILL.md` same-family finding reported on the first round (the `modal` table row and the blueprint row) is folded into this PR in the patch round on the seat's revised file surface; it is item 4 above. - The `opensInNewTab` block carries no `os:check` marker, so `check:skill-examples` does not see it; a marker would add a type check only (the refinement that refused it is invisible to tsc either way). Not added (scope). - "`input` (the modal params)" in the `ctx` section is left as is: the params dialog is still a modal for a script action with `params`. ## 维护者速读(草稿) **改了什么** — 发布的 objectstack-ui skill 里有三处教 AI 写 schema 会拒收的 action 写法。① `rules/actions.md` 的"收集参数再执行服务端 body"示例由 `type: 'modal'` 改为 `type: 'script'`,标题同步,去掉多余的 `target`;② 同文件新标签页 `newTabUrl` 示例由 `type: 'url'` 改为 `type: 'script'` 并补上 schema 要求的 `target`;③ `SKILL.md` 两行:Action Types 表里 `modal` 一行原写"打开对话框(通常收集 `params` 再执行 `body`)",改为"打开 `target` 命名的页面;`body` 只在 `script` 上运行",蓝图表里"参数化批量变更用 `modal`"改为用 `script`。另删一句与下文重复的注释以抵消 token 棘轮;评测文件里描述该示例的一句话同步改为 `script`。 **为什么改** — spec 声明优先:`ActionSchema` 的两条 refinement 明确拒绝这两种写法(非 `script` 带 `body`、`url` 缺 `target`),错误信息本身就给出了修法;渲染端只在 `script` 路径读取 `newTabUrl`,保留 `type: 'url'` 只会让示例"能过校验但不起作用"。`SKILL.md` 的两行与示例是同一个错误,同一次审批一并收口。北极星优先级 4:发布面上写给 AI 看的错句是产品缺陷。 **风险与代价(含回滚)** — 只改文档与评测文字(三个文件),不改代码、不改 spec,不发包、无 changeset。`SKILL.md` 净增 1 字节,在该文件棘轮余量内。风险在于第二处示例的改法与派发单建议(保留 `url` 加 `target`)不同,正文已给出实测依据;若维护者更倾向原建议,改回是一行。回滚 = revert 本 PR。 **席位意见** — (留空) **你要做的** — 审阅本 PR 并 APPROVE(或在评论里指明第二处改用「保留 `url` 加 `target`」),由席位落地。 --- _Generated by [Claude Code](https://claude.ai/code/session_01CXydFDyiQwNbGFkmwrcRQq)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5d1d1ac commit 13aea18

3 files changed

Lines changed: 7 additions & 8 deletions

File tree

‎skills/objectstack-ui/SKILL.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ Use this CRM-style structure as the canonical UI assembly reference:
209209
| Dashboards | `src/dashboards/*.dashboard.ts` | Combine KPI + chart + table widgets with shared `dateRange` and `globalFilters` |
210210
| Reports | `src/reports/*.report.ts` | Select `rows` (dimensions) + `values` (measures) from that dataset; tabular/summary/matrix/joined |
211211
| Record pages | `src/pages/*.page.ts` | Compose `regions` + components (`page:header`, `record:highlights`, related lists, tabs) |
212-
| User actions | `src/actions/*.actions.ts` | Use `flow` for orchestration and `modal` for parameterized bulk mutations |
212+
| User actions | `src/actions/*.actions.ts` | Use `flow` for orchestration and `script` for parameterized bulk mutations |
213213

214214
This blueprint is the default for “build a complete metadata app UI” tasks.
215215

@@ -249,7 +249,7 @@ Register them under `defineStack({ actions: [...] })`.
249249
|:---------|:-------------------------------------------------------------------|:---------------|
250250
| `script` | Run an inline L2 hook body (sandboxed JS) on the server | `body` (or `target` = registered function name) |
251251
| `url` | Navigate to an internal route or external URL | `target` |
252-
| `modal` | Open a dialog (typically collecting `params`, then executing `body`) | `target` |
252+
| `modal` | Open the page named by `target`; a `body` runs only on `script` | `target` |
253253
| `flow` | Launch a screen/auto-launched flow by name | `target` |
254254
| `api` | Call a registered API endpoint | `target` |
255255
| `form` | Open a FormView by name (routed to `/_console/forms/:name`) | `target` |

‎skills/objectstack-ui/evals/views-apps-actions-pages.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
{
2525
"id": 3,
2626
"prompt": "Add a Convert action on `lead` — only for qualified, not-yet-converted leads, from the record header and each list row, launching the `lead_conversion` flow — and a bulk 'Add to campaign' that asks which campaign, for the selected rows.",
27-
"expected_output": "Two `defineAction` items. Convert: `type: 'flow'`, `target: 'lead_conversion'`, `locations: ['record_header', 'list_item']`, `visible: P`record.status == \"qualified\" && record.is_converted == false`` (bare CEL, never brace-wrapped), `confirmText`, `successMessage`, `refreshAfter: true`. Bulk: `type: 'modal'`, `locations: ['list_toolbar']`, a field-backed `params: [{ field: 'campaign_id', objectOverride: 'campaign', required: true }]`, and a `body` that reads `input.selectedIds`.",
27+
"expected_output": "Two `defineAction` items. Convert: `type: 'flow'`, `target: 'lead_conversion'`, `locations: ['record_header', 'list_item']`, `visible: P`record.status == \"qualified\" && record.is_converted == false`` (bare CEL, never brace-wrapped), `confirmText`, `successMessage`, `refreshAfter: true`. Bulk: `type: 'script'`, `locations: ['list_toolbar']`, a field-backed `params: [{ field: 'campaign_id', objectOverride: 'campaign', required: true }]`, and a `body` that reads `input.selectedIds`.",
2828
"files": [],
2929
"assertions": {
3030
"must_contain": ["defineAction", "locations", "record_header", "list_toolbar", "visible", "successMessage", "selectedIds"],

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export const ConvertLeadAction = defineAction({
7171
});
7272
```
7373

74-
**Modal-typed action** (collect params, then execute server body):
74+
**Script-typed action** (collect params, then execute server body):
7575

7676
<!-- os:check -->
7777
```typescript
@@ -82,11 +82,9 @@ export const AddToCampaignAction = defineAction({
8282
label: 'Add to Campaign',
8383
objectName: 'lead',
8484
icon: 'send',
85-
type: 'modal',
86-
target: 'create_campaign',
85+
type: 'script',
8786
locations: ['list_toolbar'],
8887
params: [
89-
// Field-backed params resolve label/type/options from object metadata:
9088
{ field: 'campaign_id', objectOverride: 'campaign', required: true },
9189
],
9290
body: {
@@ -178,7 +176,8 @@ export const OpenInvoicePdfAction = defineAction({
178176
name: 'open_invoice_pdf',
179177
label: 'Open PDF',
180178
objectName: 'invoice',
181-
type: 'url',
179+
type: 'script',
180+
target: 'open_invoice_pdf', // registered handler; skipped while newTabUrl is set
182181
opensInNewTab: true,
183182
newTabUrl: '/api/v1/invoice/{recordId}/pdf', // zero-roundtrip; endpoint self-auths
184183
locations: ['record_header'],

0 commit comments

Comments
 (0)