Skip to content

Commit 8af914a

Browse files
objectstack-fleet[bot]os-steveclaude
authored
docs(skills,docs): teach page:header actions as declared action ids (#20257)
Fixes #20173 Clause-②: no The objectstack-ui skill (`rules/pages.md` and its eval) taught full `Action` objects in `page:header.properties.actions`, and the layout-dsl docs page taught `actions: [edit, delete, share]` with no such actions declared. The contract is action **ids**: `packages/spec/src/ui/component.zod.ts` declares `actions: z.array(z.string()).optional().describe('Action IDs to show in header')`, and ruling B on #11592 (a completed card, not reopened here) made the canonical `page:header` implement that id lookup. This PR teaches the id and where it resolves; it changes no renderer and no spec (out of scope: the objectui object arm is an undeclared transition tolerance, retired on its own card). ## What changed - `skills/objectstack-ui/rules/pages.md` — catalogue row: `actions: string[]` (action ids), 「inline」 dropped; example: `actions: ['convert_lead']` naming the action the same block declares on `lead`; the 「Actions in header」 note now says the ids resolve against the bound object's declared actions and that there is no built-in id registry (the 「no sibling action node」 rule stays). - `skills/objectstack-ui/evals/views-apps-actions-pages.json` — eval 4 `expected_output`: 「the declared action's id in `properties.actions`」 instead of 「the Action object passed in」. The `must_contain` list is unchanged on purpose: the prompt does not fix the action's machine name, so pinning `convert_lead` there would grade the author's naming, not the shape. - `content/docs/protocol/objectui/layout-dsl.mdx` — Standard Template: one sentence states the rule (ids of the bound object's declared actions; `edit` / `delete` / `share` resolve nowhere; the host draws its own Edit / Delete chrome), a tagged `action` block declares `send_statement` on `account` with `locations: [record_header]`, and the page names `actions: [send_statement]`. Customer 360: the undeclared ids are replaced by a one-line YAML comment pointing at that rule (the header keeps its title; nothing on that page declares an action on `customer`, and a second declaration block was not worth the length). ## Where the id resolves (objectui at the pin `f8a9d0fb`, read with `git -C ../objectui grep`) `packages/components/src/renderers/layout/containers.tsx` (`PageHeaderRenderer`): the authored array goes through `resolveDeclaredActionIds` (`@object-ui/types`, `ui-action.ts`) against `useMetadataItem('object', headerObjectName).actions` — the bound object's own declared actions. An id that resolves nowhere logs 「object … declares no action by that name. Declared: …」 and draws nothing. There is no built-in id registry; the host's Edit / Delete chrome arrives separately as `headerSystemActions` (`sys_edit`, `sys_delete` in `app-shell/src/views/RecordDetailView.tsx`), so authored `edit` / `delete` ids neither resolve nor dedupe against it. ## Reach, measured at the public door (`os validate`) Four plain-object configs in a scratch directory, run with the CLI's source entry the CLI's own e2e tests use: `node_modules/.bin/tsx packages/cli/bin/run-dev.js validate objectstack.config.ts` (spec, lint, core, metadata-core built first; `origin/main` at `14ae40b0`, which includes PR #20171). | fixture | shape | result | |:--|:--|:--| | skill, as published | `ConvertLeadAction` object in `properties.actions` | exit 0 with `⚠ page "lead_detail_page" · page:header: actions.0: Invalid input: expected string, received object` (`component-props-invalid`) | | skill, as taught now | `actions: ['convert_lead']`, action declared with `objectName: 'lead'` | `✓ Validation passed`, no advisories | | docs, as published | `actions: ['edit', 'delete', 'share']`, none declared | exit 1, `✗ Author-time rules failed (3 issues)` — `action-name-undefined` at `properties.actions[0..2]`: 「names action "edit", which is defined by no action in this stack … The header draws no button for it」 | | docs, as taught now | `send_statement` declared on `account`, `actions: ['send_statement']` | `✓ Validation passed`, no advisories | (The source entry prints oclif MODULE_NOT_FOUND noise for unrelated commands whose packages are not built here — `i18n:check`, `migrate:meta`; the `validate` command loaded and ran in every case, as the `◆ Validate` block that follows the noise shows.) ## Line and token budget (`skills/**`, token = ceil(utf8 bytes / 4), the ratchet's own convention) | file | lines before → after | tokens before → after (ceiling) | |:--|:--|:--| | `skills/objectstack-ui/rules/pages.md` | 454 → 454 | 5692 → 5692 (5692; bytes 22765 → 22767 of 22768) | | `skills/objectstack-ui/evals/views-apps-actions-pages.json` | 65 → 65 | 1505 → 1505 (1505; bytes 6020 → 6020) | | whole package `skills/objectstack-ui/**` (11 tracked files) | 2239 → 2239 | sum of per-file ratchet readings unchanged; raw bytes 123483 → 123485 | Net lines: 0 per file, 0 for the package. No ceiling moved; `node scripts/check-skills-token-ratchet.mjs` is green at head `9c49e874` (`pages.md … 5692 tokens (ceiling 5692; headroom 0)`). ## Gates (derived from the diff by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`, run at head `9c49e874`) 46 commands derived, 46 run, every exit code captured before any pipe as 0; `--ran` reconciliation: 「46 derived famil(ies) accounted for — 46 run, 0 NOT-MEASURED」. The rows this diff bears on: - `pnpm --filter @objectstack/spec run check:skill-examples` — 「259 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass」 (the `os:check` block in pages.md is one of them; needed `@objectstack/client` / `client-react` built). - `pnpm --filter @objectstack/spec run check:yaml-examples` — 「19 tagged YAML example(s) … validate against their declared live spec schemas」 (the new `os:check-yaml action` block and both `page` blocks included). - `node scripts/check-skills-token-ratchet.mjs` — 54 authored files within ceilings. - `pnpm check:doc-authoring`, `check:doc-anchors`, `check:docs-single-h1`, `check:docs-spec-enumerations`, `check:skill-frame-sync`, `check:skill-identifier-liveness`, `check:skill-compatibility`, `check:nul-bytes` — all 0. - No package's source is touched, so no package `typecheck` / `test` is owed; the diff has no ① build closure. NOT MEASURED, CI's own: the `Build Docs` job (fumadocs build of `content/docs`), `Test Core` shards, the type-check lanes. ## Changeset `skip-changeset`: the diff is `skills/**` and `content/docs/**` only. No workspace package lists either path in `files[]` (grep over every `packages/**/package.json`); `create-objectstack` ships `dist` / `README.md` / `CHANGELOG.md` and installs the catalog at scaffold time via `npx skills add objectstack-ai/objectstack/skills` (its `src/skills-install.ts`), not from the tarball. Built-dist grep after the build: the diff's text (`send_statement`, 「ids of `lead`'s actions」, 「(action ids)」) hits nothing under any built package's `files[]`; the positive control 「Action IDs to show in header」 hits `packages/spec/dist/ui/index.js`. ## Acceptance notes - Family sweep at base `14ae40b0`: `git grep -nE "actions: \[[A-Z][A-Za-z]*Action" -- skills content/docs` → 1 hit (`pages.md:90`); `page:header` examples with undeclared ids → 2 (`layout-dsl.mdx:96`, `:997`) plus the skill's object-shaped one. At head: the regex sweep → 0 hits; every `page:header` example on both surfaces names only declared ids or none (`layout-dsl.mdx:109` `send_statement`, declared at `:88`; `pages.md:90` `convert_lead`, declared at `:69`; Customer 360 names none). - Observation, not filed (read-only, outside this family): `layout-dsl.mdx:636` `record:related_list` with `actions: [standard_new]` — `action-name-undefined` deliberately does not walk that key (`actions` is declared separately on `record:related_list`), so it is not the header family; whether `standard_new` is a renderer built-in was not measured here. Carrier: none. - `origin/main` moved from the dispatch's `08c8484a` to `14ae40b0` before the branch was cut; every site in the card was re-read there and is unchanged. - The commit trailer pair is the model-free form the pre-push hook requires; the harness-suggested co-author spelling was refused once and amended before the first push. ## 维护者速读(草稿) **改了什么** — 修正 objectstack-ui 技能包(`rules/pages.md`、eval 4)与 layout-dsl 文档页里 `page:header` 的 `actions` 写法:从「传入完整 Action 对象」/「`[edit, delete, share]`」改为**声明在绑定对象上的 action id**,并说明 id 在哪里解析(对象自己声明的 actions,没有内建 id 注册表)。文档示例补了一段 `send_statement` 的 action 声明,让示例自洽。 **为什么改** — 契约是 `PageHeaderProps.actions: string[]`(#11592 裁决 B,objectui 已实现)。技能与文档教的形状,`os validate` 一个报 `component-props-invalid`,一个报三条 `action-name-undefined`(本 PR 正文有实测命令与输出);跟着技能写的 AI 作者会直接写出被校验器拒收、渲染器不画按钮的元数据。 **风险与代价(含回滚)** — 只改教学文本,不动 spec、不动渲染器;`skills/**` 净行数 0、token 棘轮读数不变(5692/1505 均持平,pages.md 字节 22765→22767,仍在 22768 上限内)。回滚 = revert 本 PR,无其他依赖。 **席位意见** — (留空) **你要做的** — 阅读 `pages.md` 与 `layout-dsl.mdx` 两处措辞后,在本 PR 上给出 APPROVED review(Tier H,`skills/**` 只由维护者批准落地)。 --- _Generated by [Claude Code](https://claude.ai/code/session_01MjvgiFAmjHqsxy1XLiVYfH)_ Co-authored-by: os-dev <steve@objectstack.ai> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6704717 commit 8af914a

3 files changed

Lines changed: 22 additions & 9 deletions

File tree

‎content/docs/protocol/objectui/layout-dsl.mdx‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,20 @@ Top-level layout structures that define macro organization.
7878

7979
A page is identified by `name` + `label`, and its content lives in `regions`,
8080
each holding typed `components`. There is no `context:` key — the binding is
81-
`object` plus `type` (`record` here).
81+
`object` plus `type` (`record` here). `page:header`'s `actions` are the **ids**
82+
of actions declared on that object — there is no built-in id registry, so
83+
`edit` / `delete` / `share` resolve nowhere (the host draws its own Edit /
84+
Delete chrome). Declare the action, placed at `record_header`, then name it:
85+
86+
{/* os:check-yaml action */}
87+
```yaml
88+
name: send_statement
89+
label: Send Statement
90+
objectName: account
91+
type: flow
92+
target: account_statement
93+
locations: [record_header]
94+
```
8295
8396
{/* os:check-yaml page */}
8497
```yaml
@@ -93,7 +106,7 @@ regions:
93106
- type: page:header
94107
properties:
95108
title: Customer Details
96-
actions: [edit, delete, share]
109+
actions: [send_statement]
97110
- name: main
98111
components:
99112
- type: record:details
@@ -994,7 +1007,7 @@ regions:
9941007
- type: page:header
9951008
properties:
9961009
title: Customer
997-
actions: [edit, delete, share]
1010+
# `actions:` takes ids declared on `customer` — see Standard Template
9981011

9991012
- name: main
10001013
components:

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
{
3535
"id": 4,
3636
"prompt": "Design a custom lead record page: the Convert action inline in the header, a highlights strip, a stage path over `status`, and the related contacts. Also write a user guide for the `crm` package under src/docs and link it from the nav.",
37-
"expected_output": "`definePage({ name, label, type: 'record', object: 'lead', template: 'three-column', regions: [...] })` with `page:header` (the Action object passed in `properties.actions`, no sibling action node), `record:highlights`, `record:path` (`statusField`, `stages`), `record:related_list`. The guide is a flat, namespace-prefixed `src/docs/crm_user_guide.md` in pure Markdown (MDX and image references are rejected at build), surfaced by a `type: 'url'` nav item pointing at `/docs/crm_user_guide`.",
37+
"expected_output": "`definePage({ name, label, type: 'record', object: 'lead', template: 'three-column', regions: [...] })` with `page:header` (the declared action's id in `properties.actions`, no sibling action node), `record:highlights`, `record:path` (`statusField`, `stages`), `record:related_list`. The guide is a flat, namespace-prefixed `src/docs/crm_user_guide.md` in pure Markdown (MDX and image references are rejected at build), surfaced by a `type: 'url'` nav item pointing at `/docs/crm_user_guide`.",
3838
"files": [],
3939
"assertions": {
4040
"must_contain": ["definePage", "type: 'record'", "regions", "page:header", "record:path", "src/docs/", "/docs/"],

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ which contain components.
4747

4848
| `type` | Use |
4949
|:---------------------|:----|
50-
| `page:header` | Title + subtitle + breadcrumb + inline `actions: Action[]` |
50+
| `page:header` | Title + subtitle + breadcrumb + `actions: string[]` (action ids) |
5151
| `page:card` | Bordered/un-bordered card with `children: Component[]` (plus an optional `footer: Component[]` slot) |
5252
| `flex` | Generic styleable box (`properties.children`) — the workhorse for custom layout; style via `responsiveStyles` (see Styling below) |
5353
| `element:text` | Text node — `properties.content`; style via `responsiveStyles` |
@@ -87,7 +87,7 @@ export const LeadDetailPage = definePage({
8787
title: '{first_name} {last_name}',
8888
subtitle: '{company}',
8989
breadcrumb: true,
90-
actions: [ConvertLeadAction], // inline action buttons in header
90+
actions: ['convert_lead'], // ids of `lead`'s actions
9191
},
9292
},
9393
{
@@ -121,9 +121,9 @@ export const LeadDetailPage = definePage({
121121
> [Date Macros](../SKILL.md#date-macros--filter-placeholders) reference below — the
122122
> full token list is published as `DATE_MACRO_TOKENS` in `@objectstack/spec/data`.
123123
124-
> **Actions in header** — pass full `Action` objects into
125-
> `page:header.properties.actions`; do **not** create a sibling action node.
126-
> The header renders them inline in the action slot.
124+
> **Actions in header** — `properties.actions` takes the **ids** of actions
125+
> declared on the bound object (`'convert_lead'`; no built-in id registry);
126+
> do **not** create a sibling action node.
127127
128128
### AI-authored *source* pages — `kind:'html'` and `kind:'react'` (ADR-0080/0081)
129129

0 commit comments

Comments
 (0)