Skip to content

docs(skills): objectstack-ui action examples and the SKILL.md action rows follow the ActionSchema refinements - #22184

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-22176-ui-actions-examples-schema
Oct 8, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/issue-22176-ui-actions-examples-schema

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

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

…ema refinements

The published objectstack-ui skill taught two action shapes that
`ActionSchema` refuses, so `defineAction(...)` threw at module load for
anyone who copied them:

- the "collect params, then execute server body" example authored
  `type: 'modal'` beside a `body`; a non-script action dispatches on
  `target` and never runs a body. It is now `type: 'script'` (the shape
  the refinement message prescribes), the heading moves with it, and the
  dead `target` goes.
- the `opensInNewTab` + `newTabUrl` example authored `type: 'url'` with
  no `target`. `target` is required for `url`, and the zero-roundtrip
  `newTabUrl` fast path is read only by the server-action handler a
  `script` action reaches (a `url` action navigates to `target` and never
  consults `newTabUrl`), so the example takes the spec's own pinned legal
  pair: `type: 'script'` + `target` naming the registered handler.

The one comment deleted from the first block restated the "Action
Parameter Patterns" paragraph; it pays the token ratchet for the added
`target` line. The eval's expected output for the bulk action names the
corrected type.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXydFDyiQwNbGFkmwrcRQq
…y on a modal

The Action Types table taught `modal` as "Open a dialog (typically
collecting `params`, then executing `body`)" and the CRM blueprint row
sent parameterized bulk mutations to `modal` — the shape `ActionSchema`
refuses (`body` only runs for `type: 'script'`; a non-script action
dispatches on `target` and never runs a body). The two rows now state
what the schema accepts and what rules/actions.md teaches: the
blueprint's parameterized bulk mutation is a `script`, and a `modal`
opens the page its `target` names, with the note that a `body` runs only
on `script`. One byte of growth, paid inside the file's ratchet headroom
(3843 → 3844 tokens of 3856). Nothing else in the file moves.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXydFDyiQwNbGFkmwrcRQq
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 28a140bb4522fc07ec607c1a58996b746afbebef
Local-runs: none

Reviewed in-seat by the skills seat 1 (dispatching seat; this session is served at the contract-review tier, read off get_session), 2026-10-08T05:25Z. Face hit: governed rule text (skills/**, Tier H). Read-only shape held: the diff (get_files = 3 files, +7/−8), the card #22176 and its thread (claim, two os-dev-reports, the in-round file-surface revision 6052511643), the head's check-runs; nothing built or run locally. Premises re-read on origin/main (959c209d56 at the first round, 6ed0c0f3e5 at this record; skills/** untouched between them; merge-tree clean).

① Derived judgments

  1. rules/actions.md:77, "Modal-typed action" → "Script-typed action", type: 'modal' + target → type: 'script' (params and body unchanged): correct. ActionSchema's second refinement (packages/spec/src/ui/action.zod.ts:2016-2036) refuses body on any non-script type, and its message names this exact alternative for this lesson ("To collect params and then run the body, use type: 'script'"). target has no meaning on a script that carries a body and is rightly dropped.
  2. The deleted comment inside that block ("Field-backed params resolve label/type/options from object metadata"): survives as the "Action Parameter Patterns" section (head :195-199), which states the same rule with objectOverride. Nothing lost; the ratchet payment is content, not wrapping.
  3. rules/actions.md:176, type: 'url' → type: 'script' + target: 'open_invoice_pdf' beside opensInNewTab + newTabUrl: correct, and the dispatch order's assumption ("keep url, add target") was wrong. Seat reading on origin/main: the spec's own pin packages/spec/src/ui/action-newtaburl-pair.test.ts pairs newTabUrl with type: 'script' + target; action.zod.ts:2161-2166 records that only the console server-action wrapper (bound as the script handler) reads newTabUrl and "no other path reads the key at all"; the field's description (:1654) says the pre-opened tab navigates there with no action POST, which is what the new inline comment ("registered handler; skipped while newTabUrl is set") states. The prose above the block (computed redirect after a fetch; newTabUrl for the zero-roundtrip case) reads true for a script handler. The dev corrected the PM's premise; acknowledged on the card.
  4. SKILL.md:212, blueprint row modal → script "for parameterized bulk mutations": correct; a modal cannot execute a body, a script collects params and does.
  5. SKILL.md:252, Action Types row modal: "Open a dialog (typically collecting params, then executing body)" → "Open the page named by target; a body runs only on script": correct; target is described in the schema as the modal/page name to open, and the refinement's remedy ("keep type: 'modal' with target naming the page") is what the row now says. Required-field column untouched; the table's grammar kept.
  6. evals/views-apps-actions-pages.json, one token in expected_output (modal → script for the bulk action beside a body): correct and in the revised file surface (6052511643); must_contain untouched, so the eval's assertions are unchanged.
  7. Positive control, quoted by the dev with the built dist's input hash (e5e882202edc406c, unchanged across rounds): both blocks safeParse.success: false at body / target on 959c209d56, both true with defineAction not throwing on eb522522f2 and again on 28a140bb45. The control check:skill-examples green on both heads. Not re-run by this seat (read-only shape); the before/after pair is the card's own acceptance criterion and the issue paths quoted match the refinements' path values.
  8. Ratchet on the head: SKILL.md 3843 → 3844 tokens (ceiling 3856, headroom 12); rules/actions.md 2107 → 2104 (ceiling 2108); the eval 1502 → 1502 (ceiling 1505); nothing re-wrapped. CI reading: Lint & Repo Gates and TypeScript Type Check both success on 28a140bb45; 38 check-runs, 27 success, 11 skipped, 0 other.
  9. Nothing else in the published catalog teaches a modal with a body after this diff: the dev's grep over skills/objectstack-ui/evals/ for modal/dialog shapes is 0; the seat's earlier reading of SKILL.md found only the two rows now changed.

② Semver level

None. Prose-only edits to published skill text and one eval sentence; no package files[] content, no API, no schema. skip-changeset on the PR is the correct declaration for this repository (docs-only), read back; it is the seat's own reading, applied after review.

③ Boundary flags

  • Dev flag (first report): dispatch assumption 3 falsified at :176 — answered in judgment 3: the dev's shape is the spec's pinned pair; the order's alternative is named in the PR body for the maintainer and is not preferred by this seat.
  • Dev flag: the opensInNewTab block carries no os:check marker, so the TypeScript gate never sees it — stays a note (a marker would add a types-only check; the refinement that refused it is invisible to tsc either way).
  • Dev flag: PR title names only rules/actions.md — the seat retitles through the relay in this act (issue_patch), no push needed.
  • Dev flag (first report): its label-write was refused by its session's classifier — recorded in its report; the label and assignee on the PR are this seat's own act after its docs-only reading, not a relay of that write.
  • open_questions: none.

Implemented-by: claude/issue-22176-ui-actions-examples-schema
Reviewed-by: session_01CXydFDyiQwNbGFkmwrcRQq

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)

席位:skills 席 1,session_01CXydFDyiQwNbGFkmwrcRQq,2026-10-08T05:26Z。对照席位自己读的 diff(head 28a140bb45,三个文件 +7/−8)校正 dev 草稿;契约复核记录(PASS)在本 PR 上一条评论,ACCEPT 在卡片 #22176。

改了什么:只改对外发布的 objectstack-ui 技能的三处文字。① rules/actions.md:77 "收集参数再执行服务端 body"的示例由 type: 'modal' 改为 type: 'script',标题同步改为 Script-typed,去掉对 script 无意义的 target;② 同文件 :176 新标签页 newTabUrl 示例由 type: 'url' 改为 type: 'script' 并补上 schema 必填的 target;③ SKILL.md 两行——Action Types 表 modal 行由"打开对话框(通常收集 params 再执行 body)"改为"打开 target 命名的页面;body 只在 script 上运行",蓝图表"参数化批量变更用 modal"改为用 script。另删一句与下文"Action Parameter Patterns"重复的注释支付棘轮;评测文件里描述该示例的一句话同步改为 script(must_contain 未动)。

为什么改:这三处教的都是 ActionSchema 直接拒收的写法:非 script 带 body、url 缺 target;defineAction 就是 ActionSchema.parse,照抄即在模块加载时抛错。它们是 #22059 探针在当前目录里找到的仅有的两处真实教学错误(:77 还在 os:check 块里,类型检查看不见 refinement 所以一直绿)。北极星优先级 4:发布面上写给 AI 的错句是产品缺陷。

风险与代价(含回滚):纯文字,无代码、无 spec、无 changeset(skip-changeset,docs-only)。棘轮:SKILL.md 3843 → 3844(上限 3856,余量 12),rules/actions.md 2107 → 2104(上限 2108),未折行。阳性对照已量:两个示例在 main 上 safeParse 均为 false(路径 body/target),在本 PR head 上均为 true 且 defineAction 不再抛错;check:skill-examples 两端都绿。回滚 = revert 本 PR 的两个提交。

席位意见:同意合入,席内契约复核 PASS。一处与派发单不同、席位已核实并采纳::176 没有按派发单"保留 url 加 target",而是改成 script + target + newTabUrl——这是 spec 自己 pin 住的合法组合(action-newtaburl-pair.test.ts),且 action.zod.ts:2161 记录只有 script 路径的控制台包装器读取 newTabUrl,url 路径从不读它;"保留 url 加 target"能过校验但新标签页不会按 newTabUrl 打开。SKILL.md 两行与示例是同一个错误,并进本 PR 让你一次批准覆盖整个家族。一条备注、未立卡:newTabUrl 那个示例块没有 os:check 标记,加标记只会多一道看不见 refinement 的类型检查,不值得。

你要做的:用授权账号(os-zhuang 或 hotlong)在本 PR 上 APPROVE 一次,或亲手合入。PR 保持 draft;收到授权 APPROVED 后由本席按落地前检翻 ready、挂 auto-merge 入队并收口 #22176(若批准者已自行翻 ready 入队,本席只做收口)。


Generated by Claude Code

@objectstack-fleet objectstack-fleet Bot changed the title docs(skills): objectstack-ui actions.md examples follow the ActionSchema refinements docs(skills): objectstack-ui action examples and the SKILL.md action rows follow the ActionSchema refinements Oct 8, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review October 8, 2026 05:39
@os-zhuang
os-zhuang enabled auto-merge October 8, 2026 05:39
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 13aea18 Oct 8, 2026
50 of 51 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-22176-ui-actions-examples-schema branch October 8, 2026 05:55
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 9, 2026
…able block subset and its lint (objectstack-ai#22271) (objectstack-ai#22287)

Fixes objectstack-ai#22271

Clause-②: no

The Studio author's page skill (`skills/objectstack-ui/rules/pages.md`)
learns the print page that card ① of the ruling of record on objectstack-ai#8346
(comment 6051470224, letter B′, maintainer 「8346 B′」) landed as objectstack-ai#22158
through PR objectstack-ai#22193: a printable document is a page that declares `print`;
the eight `print` keys and what each means on paper; the three page
types that may carry it; the printable block subset taught by reason,
not as a copied list; the `print-page-block-unprintable` refusal; and
the two list facts (list export has no `'pdf'`; a list prints through
the view's `allowPrinting`). objectstack-ai#8346, objectstack-ai#22158, objectstack-ai#22193 and objectstack-ai#22184 are
references — none of them is addressed here.

Every sentence is true of `origin/main` at `c8bb3c8d9c` (this branch's
base): the declaration is parsed and refused by `@objectstack/spec`
(`PagePrintSchema`, `checkPagePrintComposition`) and by
`@objectstack/lint` (`validatePrintPageBlocks`, rule
`print-page-block-unprintable`, on `os validate` / `os build` / `os
lint` and the page save door). What renders it is the console's print
rendering — card ② of the same ruling, not on this tree — so the skill
says in one clause that the rendering follows and that `os validate`
warns the author today (the liveness row `print` is `planned` with
`authorWarn`, `packages/spec/liveness/page.json`). No rendering is
invented.

## What changes (three markdown files, nothing else)

- `skills/objectstack-ui/rules/pages.md` — a new `### Print pages`
section under Pages: what a print page is and the refused page
kinds/types (`list` → `interfaceConfig.allowPrinting`, `utility`,
`slotted` / `html` / `react`, a `full` page with no regions); the keys
`paperSize`, `orientation`, `margins` (millimetres), `repeatHeader` /
`repeatFooter` (the page's own `header` / `footer` regions; refused
without that region), `pageNumbers`, `repeatTableHeaders` (on by
default), `avoidBreakInside`; the printable subset stated as the
property every member has (draws all it declares, in full, the same at
any width) with the 13 members grouped by that property; the four
refusal reasons with the blocks an author reaches for first
(`record:related_list` / `object-grid` → `record:line_items`;
`page:header`, sidebars, boards; `record:path`, tabs, buttons, inputs,
forms → `record:details`; `flex` / `object-chart` / plugin widgets are
outside the vocabulary); the rendering status in one clause; the two
list facts; and one `os:check` invoice example (`type: 'record'`,
`print` with margins / running header and footer / page numbers,
`element:text` letterhead, `record:details`, `record:line_items`, a
footer region).
- `skills/objectstack-ui/SKILL.md` — one word in the Detailed Rules
routing row for pages.md ("print pages"), so an author routing from the
entry file finds the section.
- `skills/objectstack-ui/rules/actions.md` — one sentence beside the
`OpenInvoicePdfAction` example: a printable document is a page that
declares `print` (link to the section), validated today, rendered once
the console's print rendering ships. The example itself is untouched
(with only card ① landed, a per-project endpoint is still the only
working route to a PDF today). Paid by deleting the three "Best
practices" bullets the file already states elsewhere: `confirmText` for
destructive actions (line 20), `visible` to hide an action (line 18),
`input.selectedIds` for bulk actions (the `list_toolbar` row and the
`AddToCampaignAction` example); the `refreshAfter` bullet, which had no
other home, stays.
- `skills/objectstack-ui/evals/*.json` — untouched: no eval quotes a
changed line. Eval 4's `expected_output` names the record-page lessons
(`page:header` with `properties.actions`, `record:path` with
`statusField` / `stages`) that now live in the component catalogue rows.

## Ratchet route and readings (`scripts/check-skills-token-ratchet.mjs`,
convention ceil(utf8 bytes / 4))

**Why the section lives inside `pages.md` and not in a new file.** The
ratchet admits no new file on an author's own: a discovered authored
file with no ceiling is RED, and its remedy carries the maintainer-only
marker — "adding a CEILINGS row prices new text into the bundle that
ships to every customer project, which is the maintainer ruling this
ratchet implements — not a step an author takes while landing the file."
So the section is paid inside `pages.md` by deletion, with no re-wrap
counted and no ceiling moved.

| file | lines before → after | tokens before → after | ceiling |
headroom after |
| --- | --- | --- | --- | --- |
| `skills/objectstack-ui/rules/pages.md` | 453 → 434 (−19) | 5679 → 5691
(+12) | 5692 | 1 |
| `skills/objectstack-ui/SKILL.md` | 308 → 308 (0) | 3844 → 3847 (+3) |
3856 | 9 |
| `skills/objectstack-ui/rules/actions.md` | 207 → 206 (−1) | 2104 →
2101 (−3) | 2108 | 7 |
| whole package, all `skills/*/SKILL.md` (10 files) | 4408 → 4408 (0) |
52572 → 52575 (+3) | — | — |
| whole `skills/objectstack-ui` authored bundle (9 priced files) | 2051
→ 2031 (−20) | 26096 → 26108 (+12) | — | — |

Measured on `c8bb3c8d9c` (before) and on this PR's head (after); `node
scripts/check-skills-token-ratchet.mjs` at the head: `pages.md is 5691
tokens (ceiling 5692; headroom 1)`, `SKILL.md … 3847 (ceiling 3856;
headroom 9)`, `actions.md … 2101 (ceiling 2108; headroom 7)`, exit 0.

**Every deletion in `pages.md`, and where its content survives:**

| deleted | survives in |
| --- | --- |
| "Pages let designers assemble record details, home pages, app
launchers, and utility bars without writing React." | the Page Types
table (`record` / `home` / `app` / `utility` rows) |
| the whole `### Example — Record Detail Page` `os:check` block (52
lines) | the `definePage` + `regions` shape → the new invoice `os:check`
block; `page:header` `title` / `subtitle` / `actions` → its catalogue
row (extended); `record:highlights` `properties.fields` → its catalogue
row (extended); `record:path` `statusField` / `stages` → its catalogue
row (extended); the inlined `ConvertLeadAction` → `rules/actions.md`
(same action, same skill); `template: 'three-column'` → Templates &
Regions |
| the relative-date placeholder list and the `DATE_MACRO_TOKENS` pointer
in the Variable substitution note | `SKILL.md` → Date Macros — Filter
Placeholders (the pointer stays, shortened) |
| the "Actions in header" note | the `page:header` catalogue row ("ids
of the bound object's actions, never a sibling action node") |
| react tier: "Compose layout with inline style (real CSS); use the
injected blocks for data. Do NOT use Tailwind className — see Styling a
page below" | Styling a page → the `kind:'react'` bullet (inline style
object) and the section's no-`className` rule |
| react-blocks note: "It is the authoritative answer to what props does
ObjectForm / ListView take — author against it, not from memory." | the
same note's bold lead "Do not guess props — read the contract." |
| react-blocks note: "(Maintainers: regenerate with pnpm --filter
@objectstack/spec gen:react-blocks.)" | a maintainer process note, not
customer teaching — AGENTS.md's regeneration table and
`packages/spec/package.json` |
| "See ADR-0065 (SDUI styling model)." | the section heading "Styling a
page (ADR-0065)" |
| the `src/docs/` tree diagram | Authoring rule 2 (filename stem = doc
name) and Routing model (`/docs/` + name) |
| "A platform-level Documentation portal … is a later, additive concern
— author-side, nothing to model now." | the one deletion that is not a
restatement: a roadmap remark; no authoring instruction is lost (there
is no portal key to author) — flagged here for the reviewer |
| "Live instances vs. structural views", second half (structural
metadata → a `metadata` fence) | Inline metadata views — the `metadata`
fence (the three-kinds table and its lint) |
| "Scope is deliberately narrow: only state_machine, flow, permission."
| the same section's "Three view kinds" table and the lint line ("`type`
must be one of the three") |

## Controls (against the built `@objectstack/spec` dist at the head,
`scripts/pm/os-verify-lock.sh` VERDICT command-exit 0)

- Positive: the invoice example exactly as `pages.md` carries it →
`PageSchema.safeParse` success = true, `print` parsed as
`{"paperSize":"A4","margins":{"top":15,"right":12,"bottom":15,"left":12},"repeatHeader":true,"repeatFooter":true,"pageNumbers":true}`;
`validatePrintPageBlocks({ pages: [example] })` → `[]`.
- Refused by the lint rule (the same page holding unprintable blocks):
rule `print-page-block-unprintable`, severity `error`, each with its
reason — "`page:header` cannot be placed in a print page … it lays its
action bar out to the screen width"; "`record:related_list` … draws only
the first `limit` related records (5 unless set) … a window of the
rows"; "`flex` … is not in the printable block subset, and nothing
answers for how it prints"; "`record:path` … is an interactive stage
control; the current stage is a field value, which `record:details`
prints".
- Refused at the parse: `print` on a `type: 'list'` page ("… set
`interfaceConfig.allowPrinting: true`; to print a document, declare
`print` on a page of `type: 'record'`, `'home'` or `'app'` with its
blocks in `regions`"); `repeatHeader: true` with no `header` region
("Add a `{ name: 'header', components: [...] }` region holding the
letterhead blocks, or delete `repeatHeader`"); `paperSize: 'A3'`
("expected one of A4 | A5 | Letter | Legal"); `print` on a `utility`
page; `print` on a `kind: 'react'` page.
- `pnpm --filter @objectstack/spec run check:skill-examples` at the
head: "262 prose examples type-check across 3 surface(s) — every marked
block parsed, so tsc ran the SEMANTIC pass on all of them", exit 0 (the
new invoice block is one of the 228 skills + docs blocks).

## Gates (derived on this tree by `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack`: 24 families; every exit
code captured before any pipe; `--ran` reconciliation: 24 accounted, 0
NOT-MEASURED)

| command | exit |
| --- | --- |
| `node scripts/check-skills-token-ratchet.mjs` (+ `--self-test`) | 0 /
0 |
| `pnpm --filter @objectstack/spec run check:skill-docs` | 0 |
| `pnpm check:skill-identifier-liveness` · `check:skill-compatibility` ·
`check:skill-frame-sync` | 0 · 0 · 0 |
| `pnpm check:doc-authoring` · `check:corpus-claim-drift` ·
`check:nul-bytes` | 0 · 0 · 0 |
| `node scripts/check-doc-route-spelling.mjs --advisory` (+
`--self-test`) | 0 / 0 |
| `pnpm --filter @objectstack/lint run check:doc-formula-expressions` |
3 before the lint/formula build (prerequisite, not a verdict) → 0 after
`turbo run build --filter=@objectstack/formula
--filter=@objectstack/lint --filter=@objectstack/client-react
--filter=@objectstack/client` (34/34 tasks) |
| `node scripts/check-ci-filter-parity.mjs` ·
`check-closing-keyword-parity.mjs` (+ `--self-test`) ·
`check-comment-mask-corpus.mjs` | 0 · 0 / 0 · 0 |
| `pnpm check:agent-test-spelling` · `check:cross-package-test-inputs` ·
`check:driver-memory-census` · `check:gitlink-declared` ·
`check:pm-governed-merges` · `check:refd-timer-probe` ·
`check:role-word` · `check:watch-hint-literal` | all 0 |

Not run locally, CI's own: the type-check lanes and `pnpm lint` (no
TypeScript or source file moves). No changeset: the diff publishes
nothing from a released package (`skills/**` is not in any package's
`files`), so `skip-changeset` is the declaration.

## Acceptance notes (not filed; carrier named)

- `packages/spec/src/ui/app.zod.ts:108` declares a `doc` navigation-item
type (`{ type: 'doc', book }` / `{ type: 'doc', doc }`) and `SKILL.md`'s
nav table lists it, but `pages.md` → Routing model still says "There is
no dedicated `doc` nav-item type yet, so use a `url` item", and eval 4's
`expected_output` expects a `type: 'url'` nav item. A stale line in the
published skill; a different defect class from this card, so not touched
here. Carrier: the `domain:skills` seat, reported in this card's
os-dev-report for filing.
- The print-page example omits `template` (spec default `'default'`);
whether a print page constrains `template` is card ②'s call (PR objectstack-ai#22193's
own note). The skill does not speak about it.
- Rendering: the skill's one clause ("the console's print rendering that
applies it follows") is the only forward-looking sentence; the day card
② lands at a `.objectui-sha` pin, that clause and the `os validate`
warning sentence are the two lines to revise.

## 维护者速读(草稿)

**改了什么** — 给 Studio
作者技能的页面规则(`skills/objectstack-ui/rules/pages.md`)新增一节「Print
pages」:可打印文档(发票、送货单、信函、报表)就是声明了 `print` 的普通页面,没有模板类型;八个 `print`
键各自对应纸面上的什么;只有带 `regions` 的 `record` / `home` / `app`
页面可以声明;打印页里只允许「整页画全、任意宽度布局不变」的块,其余块被 `print-page-block-unprintable`
拒绝并给出原因(只画一窗行、按屏幕布局、无可打印内容、词汇表之外);列表导出没有 `'pdf'`,列表靠视图的
`allowPrinting`。附一个会被 `check:skill-examples` 类型检查的发票页示例。SKILL.md
路由行加「print pages」一词;actions.md 的「打开发票 PDF」示例旁加一句指向打印页。

**为什么改** — objectstack-ai#8346 裁决 B′ 原话:「the Studio author's skill learns the print
page when ① lands」;① 已随 PR objectstack-ai#22193 落地。spec 与 lint 会响亮拒绝写错的打印页,但没学过这个声明的
AI 作者根本不会去写它,只会继续给每个项目手搓一个 PDF 接口;技能文本是 `npx skills add` 直接发到每个客户项目的那份。

**风险与代价(含回滚)** — 发布技能包有 token 棘轮:`pages.md` 只剩 13 token
余量,新节靠删除同文件里重复的内容付账(记录详情页示例整块删除、其教学点并入组件目录表格;另 11
处重复句,逐条列在上文表格里,唯一一条非重复的是「文档门户是后续事项」这句路线图备注),净 +12 token,落在
5691/5692;SKILL.md +3、actions.md −3,三个上限都没动。风险一:AI 作者看到打印页示例,把 `print`
加到普通页面 —— 文本已写明只有文档才声明,spec 也会拒绝不合格的页面;风险二:今天没有渲染器读 `print`(裁决的卡 ②
objectui 未落地),技能用一句话如实说明「已校验、渲染随后」,`os validate` 也会提示作者。回滚:整个 PR 只动三个
markdown 文件,revert 即可,无代码、无 changeset。

**席位意见** —

**你要做的** — 核对「Print pages」一节的措辞与上文的删除清单;认可则 APPROVE 这个 draft
PR,由你或授权审批落地(`skills/**` 为 Tier H)。

---
_Generated by [Claude
Code](https://claude.ai/code/session_01CXydFDyiQwNbGFkmwrcRQq)_

---------

Co-authored-by: objectstack-fleet[bot] <objectstack-fleet[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
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 size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants