Skip to content

skills(ui): the page skill learns the print page — print, the printable block subset and its lint (#22271) - #22287

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-22271-ui-skill-print-page
Oct 8, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/issue-22271-ui-skill-print-page

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #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 #8346 (comment 6051470224, letter B′, maintainer 「8346 B′」) landed as #22158 through PR #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). #8346, #22158, #22193 and #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 feat(spec)!: a page gains an optional print declaration and a linted printable block subset; the zero-reader document schemas retire whole (#22158) #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」示例旁加一句指向打印页。

为什么改 — #8346 裁决 B′ 原话:「the Studio author's skill learns the print page when ① lands」;① 已随 PR #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

objectstack-fleet Bot and others added 2 commits October 8, 2026 11:52
…able block subset and its lint

A printable document is a page that declares `print`; teach the keys and
their paper meaning, the three page types that may carry it, the printable
subset by reason, the `print-page-block-unprintable` refusal and the two list
facts (no `'pdf'` export; `allowPrinting`). Paid inside `pages.md` by deleting
the Record Detail example (its lessons move into the component catalogue
rows) and restated lines; one routing word in SKILL.md; one pointer sentence
in actions.md paid by its restated best-practice bullets.

Claude-Session: https://claude.ai/code/session_01CXydFDyiQwNbGFkmwrcRQq
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation labels Oct 8, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 8, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4bfaf6efc2febe80b78dcedf57b7e3920f2a5aef
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-08T12:23Z. Face hit: published skill text (skills/objectstack-ui/**, Tier H; dispatch-gates --tier MANDATORY). Read-only shape held: the diff (3 files, +71/−92, two commits), the card #22271 and its thread (the claim 6058929757, the os-dev-report 6059499743), the head's check-runs; nothing built or run locally. Premises re-read on origin/main fbcbcf124f (it contains the branch base c8bb3c8d9c; skills/objectstack-ui/** is untouched between them; merge-tree clean).

① Derived judgments

  1. Every print fact is read off packages/spec/src/ui/page.zod.ts on origin/main: PagePrintSchema holds the eight keys the section lists, with the defaults it states (paperSize A4 when omitted, orientation portrait, margins in millimetres, repeatHeader / repeatFooter off and refused without the region, pageNumbers off, repeatTableHeaders on when omitted, avoidBreakInside off); checkPagePrintComposition refuses print on the slotted / html / react kinds, on type: 'list' naming interfaceConfig.allowPrinting, on utility, and on a full page with no regions; PRINTABLE_PAGE_COMPONENT_TYPES is the 13 blocks the section names, grouped as it groups them. Correct.
  2. The four refusal reasons compress PRINT_REFUSED_PAGE_COMPONENT_TYPES faithfully: a window of rows (record:related_list, object-grid), laid out to the screen (page:header, sidebars, kanban / calendar / gantt / map), nothing to print (record:path, buttons, inputs, forms), outside the vocabulary (the lint's fallback for flex, object-chart, plugin widgets). One grouping is looser than the source: tabs and accordions are refused because a closed panel never reaches paper, which the section files under "nothing to print"; the refusal and the remedy the author needs are the same. Accepted as teaching text; flagged under ③.
  3. Rendering truth: packages/spec/liveness/page.json row print is planned with authorWarn (validated, nothing applies it; card ② of the export: PDF / print document generation — re-planned for v18, superseding the #1301 NOT_PLANNED closure #8346 ruling is the carrier). The section states this in one clause and invents no renderer; actions.md keeps the per-project PDF endpoint example and adds one pointer sentence. List facts: exportOptions refuses a retired 'pdf' (view.zod.ts, spec: ComponentPropsMap['object-grid'].exportOptions is z.unknown(), so a bare exportOptions: ['csv'] passes every door and objectui's grid silently exports csv/json instead #21229) and a list prints through allowPrinting. Correct.
  4. The invoice example is an os:check block whose blocks are all in the printable set and which declares the header and footer regions its two repeat keys need; the dev's control parsed it (PageSchema.safeParse success, validatePrintPageBlocks empty) and check:skill-examples type-checked 262 blocks at the head (exit 0). The seat did not re-run it. Consistent with the schema the seat read.
  5. Ratchet, recomputed by the seat from the head blobs (ceil of utf8 bytes over 4): pages.md 22762 bytes = 5691 tokens (ceiling 5692), SKILL.md 15386 = 3847 (3856), actions.md 8404 = 2101 (2108); no ceiling row changed; the new-file route is refused by the ratchet's own MAINTAINER-ONLY remedy, so the section is paid inside pages.md. Each deletion survives where the PR body says — the seat checked three-column (Templates & Regions), the className rule (Styling a page), the extended page:header / record:highlights / record:path catalogue rows, convert_lead with confirmText / visible / selectedIds (actions.md), the Date Macros pointer, rule 2's filename stem and the /docs/ routing, the three-kinds table, and AGENTS.md's gen:react-blocks row. One deletion is not a restatement: the "Documentation portal is a later concern" roadmap remark — no authoring instruction behind it; flagged for the approver in the brief. No link elsewhere in the tree points at the removed #example--record-detail-page anchor (grep over skills, content, docs: 0). Correct.
  6. Scope: git diff --stat base..head = the three claimed markdown files; no eval changed, and eval 4's expected_output (three-column, page:header ids, record:path statusField / stages) still finds its lessons in the catalogue rows. 24 gate families derived, each exit captured, --ran reconciliation 24 derived / 24 run / 0 NOT-MEASURED; two exit-3 prerequisites re-run to 0 after the closure build under the verify lock. The CI reading at this record's clock is in the ACCEPT on skills: the Studio author's page skill learns the print page (page.print, the printable block subset, its lint) now that #22158 has landed — #8346 ruling B′ #22271.

② Semver level

None. Published skill text; no package content, API or schema. skip-changeset is the correct declaration, applied by this seat after its own reading (the dev's label-write was refused by its session classifier and recorded in its report).

③ Boundary flags

  • Dev flag: label-write refused by its classifier ("External System Writes") — recorded; the label and assignee are the seat's own act.
  • Seat note (judgment 2): tabs / accordion sit under "nothing to print" where the source's reason is "a closed panel never reaches paper"; same refusal, same remedy; a one-word fix rides the next edit of the section (headroom 1 token today).
  • Dev finding (class b, this lane): pages.md Routing model says there is no doc nav-item type and eval 4 expects a url item, while app.zod.ts:108 declares type: 'doc'; the seat files it as a card of this lane — not this PR's.
  • Forward-looking lines (two in pages.md, one in actions.md) are the ones to revise when card ② lands at a .objectui-sha pin; the carrier is named in the PR body.
  • open_questions: none; premise_still_valid: true.

Implemented-by: claude/issue-22271-ui-skill-print-page
Reviewed-by: session_01CXydFDyiQwNbGFkmwrcRQq

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)

席位:skills 席 1,session_01CXydFDyiQwNbGFkmwrcRQq,2026-10-08T12:28Z。对照席位自己读的 diff(head 4bfaf6efc2,三个 markdown 文件 +71/−92)与 origin/main 上的 spec 源码(packages/spec/src/ui/page.zod.ts 的 PagePrintSchema、checkPagePrintComposition、两张块清单;liveness/page.json 的 print 行)校正 dev 草稿;契约复核记录(PASS)在本 PR 上一条评论,ACCEPT 在卡片 #22271。

改了什么:给 Studio 作者技能的页面规则(skills/objectstack-ui/rules/pages.md)新增一节「Print pages」:可打印文档(发票、送货单、信函、报表)就是声明了 print 的普通页面,没有模板类型;八个 print 键各自对应纸面上的什么;只有带 regions 的 record / home / app 整页可以声明(list 页走视图的 allowPrinting);打印页里只允许「整页画全、任意宽度布局不变」的 13 种块,其余被 print-page-block-unprintable 拒绝并给出原因;列表导出没有 'pdf'。附一个会被 check:skill-examples 类型检查的发票页示例。SKILL.md 路由行加「print pages」一词;actions.md 的「打开发票 PDF」示例旁加一句指向打印页,示例本身保留(卡 ② 未落地前,按项目自建接口仍是今天唯一能出 PDF 的路)。

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

风险与代价(含回滚):发布技能包有 token 棘轮,且棘轮自己拒绝作者新增文件(加 CEILINGS 行是维护者专属动作),所以新节在 pages.md 内靠删除付账:整块删除「记录详情页示例」(其教学点并入组件目录表格三行)和 11 处重复句,逐条列在 PR 正文表格里,席位逐条核过落点。唯一一条非重复的删除是「文档门户是后续事项」这句路线图备注——后面没有作者指令,但请你知情。净 +12 token,落在 5691/5692(余量 1:下次改 pages.md 必须再删);SKILL.md 3847/3856、actions.md 2101/2108,三个上限都没动。风险一:AI 作者把 print 加到普通页面——文本写明只有文档才声明,spec 也拒绝不合格的页面;风险二:今天没有渲染器读 print(裁决的卡 ② objectui 未落地),技能用一句如实说「已校验、渲染随后」,os validate 也提示作者(liveness 行 planned + authorWarn)。回滚 = revert 落地的那一个 commit,无代码、无 changeset。

席位意见:同意合入,席内契约复核 PASS。席位自己核过:八个键与默认值、三种页型、四种拒绝形态、13 种可打印块与 spec 源码一致;删除的内容在 PR 正文说的位置都在;树上没有别处链接到被删的 #example--record-detail-page 锚点。一处措辞比源码松:tabs / accordion 被拒的原因是「收起的面板到不了纸上」,文本把它们归在「无可打印内容」一组——拒绝与补救相同,留待下次动这一节时顺手改(今天余量 1 token)。dev 顺带发现的旧错(pages.md 路由节说「还没有 doc 导航项类型」,eval 第 4 条也期望 url 项,而 spec 的 app.zod.ts:108 早已声明 type: 'doc')由本席在本车道另立卡,不在本 PR。卡 ② 落地时要回改的三句(两句在 pages.md、一句在 actions.md)已在 PR 正文点名。

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


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review October 8, 2026 12:33
@os-zhuang
os-zhuang enabled auto-merge October 8, 2026 12:33
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 238222d Oct 8, 2026
40 of 41 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-22271-ui-skill-print-page branch October 8, 2026 13:06
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 9, 2026
…m; eval 4 expects it (objectstack-ai#22303)

Fixes objectstack-ai#22291

Clause-②: no

The Studio author's page skill (`skills/objectstack-ui/rules/pages.md`,
shipped to every project by `npx skills add`) told the author in its
Routing model section that "There is no dedicated `doc` nav-item type
yet, so use a `url` item pointing at `/docs/NAME`", with a `url`
example, while `@objectstack/spec` declares the `doc` navigation item
(`packages/spec/src/ui/app.zod.ts:108` names `'doc'` in
`NavItemVariant`; `DocNavItemSchema` at `:715` with the contract
documented at `:676-714`), the console renders it at the `.objectui-sha`
pin `a58626c88` (`packages/layout/src/NavigationRenderer.tsx` `case
'doc'` at lines 498 and 917,
`packages/app-shell/src/views/nav-menu-renderer.tsx:281`,
`packages/app-shell/src/hooks/useNavDocTargetCheck.ts`), and
`SKILL.md`'s Navigation Item Types table already lists it. Eval 4 of
`skills/objectstack-ui/evals/views-apps-actions-pages.json` pinned the
wrong answer (`type: 'url'` pointing at `/docs/crm_user_guide`). This PR
teaches the declared item and makes the eval expect it. objectstack-ai#22271 and PR
objectstack-ai#22287 are landed references (PR objectstack-ai#22287 rewrote `pages.md` today and
left 1 token of headroom); objectui#11197 is the console-side reference.
None of them is addressed here.

Premise re-checked on `origin/main` at `238222d8cd` (this branch's base,
PR objectstack-ai#22287's merge commit): the sentence sits at `pages.md:361-362` (the
card read it at line 375 on `fbcbcf124f`, before PR objectstack-ai#22287 moved it),
the `url` example at `:366`, eval 4 unchanged; the `doc` contract is at
the lines above (the card's `:681-686` doc comment is now `:676-714`).
Premise valid.

## What changes (two files, nothing else)

- `skills/objectstack-ui/rules/pages.md` — Routing model: the paragraph
becomes "To surface a doc inside an app, add a `doc` navigation item (a
`url` item is for an external or custom URL): `doc: 'NAME'` opens that
page, `book: 'BOOK'` opens the book at its first readable page (both:
that page in that book). It inherits the docs audience gate, and `os
build` refuses a target the package lacks (`docs/nav-target`):" and the
example line becomes `{ id: 'nav_help', type: 'doc', doc:
'crm_user_guide', label: 'User Guide', icon: 'book-open' }`. Every
clause is read from the source: the two targets and the both-keys case
from `DocNavItemSchema`'s doc comment and `requiredOneOf(['book',
'doc'])`; the audience gate from the same comment (ADR-0046 §6.7); the
existence check from `packages/cli/src/utils/collect-docs.ts`
(`lintDocNavTargets`, rule `docs/nav-target`, severity `error`); the
`url` item's use from `UrlNavItemSchema.url` ("Target external URL") and
`SKILL.md`'s table row ("External or custom URL"). The surrounding true
lines (the global `/docs/NAME` route paragraph, the `*.md` link
rewriting, the Live instances note) are untouched.
- `skills/objectstack-ui/evals/views-apps-actions-pages.json` — eval 4:
the `expected_output` tail becomes "surfaced by a `type: 'doc'` nav item
(`doc: 'crm_user_guide'`)."; `must_contain` swaps `"/docs/"` for `"type:
'doc'"`; `must_not_contain` gains `"type: 'url'"`. Measured, not
assumed: `"/docs/"` was a dead assertion — `"src/docs/"` is in the same
`must_contain` and contains `/docs/` as a substring, so under the eval
README's string-check semantics it could never fail on its own; with the
`doc` item a correct answer carries no `/docs/` route at all (the item
names the doc, the platform owns the route), so the entry is swapped for
the live pin rather than kept beside it (adding instead of swapping
would overshoot the eval's 1505 ceiling by 7 bytes). Eval 4's prompt (a
record page plus a user guide linked from the nav) needs no `url` item,
checked against the whole eval file; evals 1-3 and 5-6 are untouched.

## Ratchet readings (`scripts/check-skills-token-ratchet.mjs`,
convention ceil(utf8 bytes / 4); measured on `238222d8cd` before and on
this PR's head `2bdf0f6317` after)

| file | lines before → after | tokens before → after | ceiling |
headroom after |
| --- | --- | --- | --- | --- |
| `skills/objectstack-ui/rules/pages.md` | 433 → 433 (0) | 5691 → 5686
(−5) | 5692 | 6 |
| `skills/objectstack-ui/evals/views-apps-actions-pages.json` | 65 → 65
(0) | 1502 → 1505 (+3) | 1505 | 0 |
| whole package, all `skills/*/SKILL.md` (10 files) | 4409 → 4409 (0) |
47524 → 47524 (0) | — | — |
| whole `skills/objectstack-ui` authored bundle (9 priced files) | 2030
→ 2030 (0) | 26108 → 26106 (−2) | — | — |
| bundle total (whole shipped tree, the gate's own line) | — | 154842 →
154840 (−2) | — | — |

No ceiling moves, no re-wrap counted (the new paragraph is wrapped once,
as new text), no new file. The new Routing block is 477 bytes against
the 329 it replaces (+148); `pages.md` had 6 bytes of headroom, so the
difference is paid inside the file by one deletion:

| deleted (`pages.md`) | survives in |
| --- | --- |
| Page Types: "Former roadmap-only types (`dashboard`, `form`,
`record_detail`, `record_review`, `overview`, `blank`) were removed from
the enum because they never shipped a renderer." (169 bytes) | the
section's lead — "`PageTypeSchema` has exactly **five** values — only
types with a dedicated renderer are authorizable (ADR-0049
enforce-or-remove)" — and the five-row table; `record_detail` stays
named in the Disambiguation sentence (eval 4's `must_not_contain` still
reads it there). The six former names as a list are the one thing not
restated verbatim: a tombstone of removed values, not an authoring
instruction — `PageTypeSchema` refuses any of them at parse with the
five legal values in the error. Flagged here for the reviewer. |
| Routing model: "add a navigation item that **links into** that global
URL. There is no dedicated `doc` nav-item type yet, so use a `url` item
pointing at `/docs/NAME`:" and the `url` example line | replaced by the
true sentence and the `doc` example (the budget the card names) |

## Controls (against the built `@objectstack/spec` dist at the head;
`NavigationItemSchema.safeParse`)

- Positive: the skill's example `{ id: 'nav_help', type: 'doc', doc:
'crm_user_guide', label: 'User Guide', icon: 'book-open' }` → `success:
true`, parsed as `{"type":"doc","doc":"crm_user_guide",…}`; `book:
'crm_manual'` alone → success; both keys → success.
- Refused, with the remedy the skill paraphrases: neither key → "A `doc`
navigation item needs a target: set `book` (opens that book at its first
readable page), `doc` (opens that page), or both (that page in that
book's context)."; `doc: 'docs/crm_user_guide'` and `doc:
'crm_user_guide.md'` → "A doc target is a doc NAME — the source filename
stem in lowercase snake_case"; a `url` key on a `doc` item →
"Unrecognized key(s) on this `doc` navigation item: `url`. Did you mean
`url` → `type: 'url' (with url)`?".
- The old answer `{ type: 'url', url: '/docs/crm_user_guide' }` still
parses (`success: true`, `target: '_self'` defaulted): nothing on the
platform refuses it, which is why the skill text was the only thing
steering an author to it — the card's responsibility reading holds.
- The console consumer at the pin resolves a `doc` item to `/docs/DOC`,
`/docs/BOOK` or `/docs/BOOK/DOC` (`resolveDocHref`,
`NavigationRenderer.tsx:752`) and hides an entry whose target the member
may not read (`nav-menu-renderer.tsx:281`).
- The edited example is a `navigation:` fragment, not an `os:check`
block, so `check:skill-examples` is not its control and was not marked.

## Gates (derived on this tree by `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` at `2bdf0f6317`: 23
families, identical to the PM's list derived on `238222d8cd`; every exit
code captured before any pipe; `--ran` reconciliation: "23 derived, 23
run, 0 NOT-MEASURED, 0 UNRUN")

| command | exit |
| --- | --- |
| `node scripts/check-skills-token-ratchet.mjs` (+ `--self-test`, 65
cases) | 0 / 0 — "54 authored bundle file(s) within their ceilings" |
| `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` |
0, after `turbo run build --filter=@objectstack/lint --concurrency=2`
under `scripts/pm/os-verify-lock.sh` (VERDICT command-exit 0, 4 tasks,
lock held 99 s) |
| `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; the diff is two files under `skills/`).
No changeset: the diff publishes nothing from a released package
(`skills/**` is in no package's `files`), so `skip-changeset` is the
declaration.

## Acceptance notes (not filed; carrier named)

- `"/docs/"` in eval 4's `must_contain` was subsumed by `"src/docs/"`
(substring), so it had never been able to fail on its own — repaired
here by the swap above, recorded so the reviewer can see the mechanism
rather than infer it. No other eval in the two `objectstack-ui` eval
files carries an entry subsumed by a sibling entry (read, not grepped).
- `UrlNavItemSchema.url` is described as "Target external URL" while the
console resolves an internal path through it and `SKILL.md`'s table says
"External or custom URL" — a describe-text observation with no behaviour
behind it; not a finding class, noted only. Carrier: none.

## 维护者速读(草稿)

**改了什么** — 发布给每个客户项目的 Studio
作者技能(`skills/objectstack-ui/rules/pages.md`)「Routing model」一段原来写着「还没有专门的
`doc` 导航项类型,用 `url` 项指向 `/docs/…`」,示例也是 `url` 项;而 spec 早已声明 `doc`
导航项(`doc` 打开一页,`book` 打开一本书的第一可读页,两键同给即该书中的该页,继承文档受众门禁),控制台在当前 pin
已渲染,SKILL.md 的导航项类型表也已列出。本 PR 把这一段和示例改成教 `doc` 项(并带一句 `url`
项仍用于外部或自定义地址、`os build` 会拒绝包里不存在的目标),评测 4 的预期输出与断言改为要求 `type: 'doc'`、拒绝
`type: 'url'`。

**为什么改** — 技能文本说错一句话就是产品缺陷(北极星第 4 项):入口文件说有 `doc` 类型,规则文件说没有,AI 作者照规则文件写
`url` 项,平台有类型化入口却没人用,文档受众门禁也被绕开(`url` 项不继承它);评测把错答案钉成了标准答案。平台没有任何一处拒绝指向
`/docs/` 的 `url` 项,所以只有改技能文本这一条路。

**风险与代价(含回滚)** — 发布技能包有 token 棘轮:`pages.md` 只剩 1 token 余量,新段落比旧段落多 148
字节,靠删除「Page Types」里被开头句重述的一句(「以前路线图类型(六个名字)已从枚举移除」,169 字节)付账,净 −5
token,落在 5686/5692;评测文件 +3 token,恰好
1505/1505;上限未动、无换行凑数、无新文件。唯一不是逐字重述的信息是那六个旧类型名,spec
枚举会在解析时响亮拒绝它们并列出五个合法值,`record_detail` 仍在消歧句里。风险:AI 作者把 `doc` 目标写成路径或文件名
—— spec 拒绝并给出正确拼写;写了包里没有的名字 —— `os build` 的 `docs/nav-target` 拒绝。回滚:只动两个
markdown/json 文件,revert 即可,无代码、无 changeset。

**席位意见** —

**你要做的** — 核对 Routing model 新段落的措辞与删除的那一句;认可则 APPROVE 这个 draft
PR,由你或授权审批落地(`skills/**` 为 Tier H)。

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

Co-authored-by: objectstack-fleet[bot] <332303061+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/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants