Skip to content

Commit efb1620

Browse files
objectstack-fleet[bot]os-teslaclaude
authored
docs(skills): objectstack-ui teaches the record page as the Console ships it (#28498)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6Jao3RTKFDeacUTyxyZSaPbuLw)_ Fixes #27059 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** `skills/objectstack-ui` 新增一份按顺序讲记录页的规则文件 `rules/record-page.md`(六条:对象声明产出页面 → 关系标志塑造关联列 → slotted 页只覆盖一个槽位 → `kind: 'full'` 是逃生口 → 默认抽屉、对象声明才整页 → 读者自己的选择不是元数据);`SKILL.md` / `rules/pages.md` / `rules/navigation.md` 里写着 Details / Related / Timeline 三页签的旧句子换成一行指针;新增一份 eval;文档站「How a list opens a record」改成现行的打开方式与读者自选项,记录页一节补上关联面板的读者排布。 - **为什么改:** 主干的记录页已是页签行 + 右栏(#28116、#28394),skill 还在教三页签、`tabs` 槽位和 `modal` / `split` / `popover`;照着写的 AI 会为「线索页把任务放最前」去写整页,而正确答案只是子对象外键上的 `relatedList: 'primary'`。 - **风险与回滚:** 纯教学文本加一条 ratchet 行,无 schema、键或运行时改动;回滚即 revert 本 PR。 - **先看:** `skills/objectstack-ui/rules/record-page.md`,再看 `scripts/check-skills-token-ratchet.mjs` 新增的那一行及其出处注释。 - **两分钟验证:** `node scripts/check-skills-token-ratchet.mjs`、`pnpm check:role-word`、`cd ui && node scripts/check-skill-eval-tokens.mjs`;再读 `skills/objectstack-ui/evals/record-page.json` 的两道题,确认各只有一个正确答案。 - **需要你做的:** Tier H(governed `skills/**`):席位 ACCEPT + 同 head 契约 PASS 后经 os-zhuang 批准;批准同时覆盖为新文件定价的 CEILINGS 行(ratchet 脚本对新行印 ⛔ MAINTAINER-ONLY,此处依卡 #27059 的 40 行预算与座席「记录页单独一份规则文件」的裁定写入)。 ## What changes - **`skills/objectstack-ui/rules/record-page.md`** (new, 65 lines). Six numbered items, read in order, each the remedy for the one before it: (1) the object's declarations produce the page (`nameField`, `highlightFields`, `stageField` with option `kind`, `fieldGroups` + `Field.group`, `relatedLists`, `editMode`), and the surface's width lays it out (two columns from 760 px, three field columns from 1120 px, one column below; a tab row that enlarges one panel at `?related=object.field`; no per-surface layout key); (2) the relationship flags shape the related column (`relatedList: 'primary'` body table / `true` panel / `false` off, `relatedListTitle`, `relatedListColumns`, `relatedListFilter` AND-composed with the parent condition and honoured by the count, rows in the child's default list-view `sort`, the parent's `relatedLists` order) — "put the tasks first on the lead page" is `relatedList: 'primary'` on `task.lead`, no page file; (3) `kind: 'slotted'` overrides one of six slots, each whole, no `tabs` slot (`slots.tabs` / `slots.related` / `slots.relatedLists` refused), `isDefault`; (4) `kind: 'full'` is the escape hatch; (5) the drawer unless the object's list view declares `navigation: { mode: 'page' }` (`new_window`, `none`; `modal` / `split` / `popover` removed in 18.3.0 and refused; field count never decides; a phone always pages); (6) the reader's own choices are not metadata (Open records split / drawer / page, the ⊟ right column per object, the panels' order and hidden set per user × object, the pane's width and ⤢; the `size` bucket only nudges the drawer's starting width). One `<!-- os:check -->` example: `Field.masterDetail('lead', { relatedList: 'primary', relatedListFilter: … })` on a `controlled_by_parent` child. - **`skills/objectstack-ui/SKILL.md`.** The two related-list paragraphs that taught a Details / Related / Timeline tab strip, a shared Related tab and the removed `relatedLayout` keys become a two-line pointer at the new file; one Detailed Rules link; the CRM blueprint's "Record pages" row now says the page is synthesized and a `*.page.ts` is for a slotted slot or a `kind: 'full'` layout. - **`rules/pages.md`**: the `tabs`-slot sentence is a one-line pointer. **`rules/navigation.md`**: the Surface bullet points at §5–6 of the new file and no longer promises a mobile page or a record `Page` for layout; the Overlay-width bullet moved into §6 whole. - **`evals/record-page.json`** (new) + a README bullet. Eval 1: tasks first on the lead page → `relatedList: 'primary'` + `relatedLists`; never `definePage`, `slots.tabs`, `kind: 'full'`. Eval 2: contracts open as a full page, and "open beside the list" → `navigation: { mode: 'page' }` on the list view and the Display menu for the reader; never `mode: 'split'` / `'modal'` / `'popover'` / `definePage`. - **`scripts/check-skills-token-ratchet.mjs`.** A CEILINGS row for the new file pinned at its landed count (996) with its provenance (the card's budget, the seat's ruling, the 403 tokens the three rows below gave up). `SKILL.md` 3851 → 3544, `rules/navigation.md` 2273 → 2197, `rules/pages.md` 5692 → 5669, each lowered to the measurement. - **`content/docs/ui/views.mdx`** "How a list opens a record": the live modes (`drawer` default, `page` by declaration, `new_window`, `none`; `modal` / `split` / `popover` removed in 18.3.0, `os migrate meta --from 17` drops a stored one; a phone always pages), the reader's **Open records** choice (Split default / Drawer / Full page, kept in the browser, not offered for a page-declared object, ↗ always the full page), the pane's edge, ⤢ / Open as full page / Collapse to drawer, the `size` bucket. **`content/docs/ui/pages.mdx`** record pages: the related panels are the reader's to arrange (⠿ / ⌥↑ ⌥↓, hide, bring back; per user × object). Both pages are the redirect targets of the two guide pages the card names (`/docs/guide/slotted-pages`, `/docs/guide/user-state-persistence`, `apps/docs/redirects.mjs`). - **`.changeset/27059-skills-record-page-rules.md`**: `@objectstack/skills` patch, `Clause-②: no`, `Metadata-protocol: none`. ## Why `main` ships record page v4 (#28116 right column + ⊟, #28394 tab row, #28477 phone pass) and @objectstack/spec 18.3.0 retired `modal` / `split` / `popover` and the `tabs` slot, but the published ui skill still taught the three-tab page, a `tabs` slot and the retired modes — so an agent writing from the skill authored a full page for a one-flag change and metadata for choices that belong to the reader. The card asks for one ordered 「记录页」 section that teaches what `main` ships now and an eval that catches the full-page reflex. ## Line budget PM-set and binding: the package's hand-authored markdown (`SKILL.md` + `rules/*.md`) grows by at most 40 lines; paid with the sentences the retirements made false. | File | Lines before | Lines after | Δ lines | Tokens before | Tokens after | |:--|--:|--:|--:|--:|--:| | `SKILL.md` | 307 | 287 | −20 | 3851 | 3544 | | `rules/actions.md` | 204 | 204 | 0 | 2258 | 2258 | | `rules/dashboards.md` | 464 | 464 | 0 | 6259 | 6259 | | `rules/list-views.md` | 311 | 311 | 0 | 3129 | 3129 | | `rules/navigation.md` | 162 | 157 | −5 | 2273 | 2197 | | `rules/pages.md` | 428 | 427 | −1 | 5689 | 5669 | | `rules/record-page.md` | — | 65 | +65 | — | 996 | | **Package** | **1876** | **1915** | **+39** | **23459** | **24052 (+593)** | Tokens are the ratchet's own reading (`ceil(bytes / 4)`, the SKILL.md stamp line excluded). Outside the teaching set: `evals/README.md` 36 → 40 lines, `evals/record-page.json` new (40 lines). ## Tests Run in the worktree at head 198adea, stdin closed, exit captured before any pipe: - `node scripts/check-skills-token-ratchet.mjs` → every objectstack-ui row at or under its ceiling: `SKILL.md` 3544/3544, `rules/navigation.md` 2197/2197, `rules/pages.md` 5669/5669, `rules/record-page.md` 996/996 (headroom 0 each); `--self-test` 97 cases pass. Exit 0 / 0. - `cd ui && node scripts/check-skill-eval-tokens.mjs` → every `must_contain` token of `evals/record-page.json` is taught by the bundle (`relatedList: 'primary'`, `relatedLists`, `navigation: { mode: 'page' }`, `Display menu`); 0 untaught rows. Exit 0. - `pnpm check:role-word` → "OK, no new occurrences of the reserved word" (318 files; baseline 48 files / 156 occurrences unchanged). Exit 0. - `pnpm check:skill-identifier-liveness` → Leg 1: 486 citations over 71 files checked, Leg 2: 8 sections, 0 gaps. Exit 0. - `pnpm --filter @objectstack/spec run check:skill-examples` → 278 marked examples across 114 files type-check against the dts build of `@objectstack/spec` and `@objectstack/client-react...` (built with `env -u OS_SKIP_DTS` under the verify lock, VERDICT command-exit 0); the new `Field.masterDetail('lead', { relatedList: 'primary', relatedListFilter: … })` block included. Exit 0. - `pnpm --filter @objectstack/lint run check:doc-security-posture` → 32 `ObjectSchema.create` examples in 259 marked blocks carry an `os validate`-clean posture. Exit 0 (the first run refused the example for `security-owd-unset`; the second commit declares `sharingModel: 'controlled_by_parent'`). - `pnpm check:doc-authoring`, `pnpm --filter @objectstack/spec run check:skill-docs`, `check:docs`, `check:liveness`, `check:corpus-claim-drift`, `check:docs-transcript-drift`, `check:docs-redirects`, `check:doc-anchors` → exit 0 each. - `node scripts/check-sdui-manifest.mjs` → manifest present and intact (sha256 029ea9f8fca7…, 106 components). Exit 0. - Eval read-through: eval 1's model answer contains `relatedList: 'primary'` and `relatedLists` and none of `definePage` / `slots.tabs` / `kind: 'full'`; eval 2's contains `navigation: { mode: 'page' }` and `Display menu` and none of `mode: 'split'` / `mode: 'modal'` / `mode: 'popover'` / `definePage`. ## Acceptance notes - The card names `skills/objectstack-ui/references/pages.md`; that path does not exist (`references/` holds only the generator-owned `_index.md` and `react-blocks.md`). The page rules live in `rules/pages.md`, edited here. - The two guide pages the card names were folded by K2 into `content/docs/ui/pages.mdx#record-pages` (already current for the tab row, the tiers and the slot table) and `content/docs/ui/views.mdx` (the stale half, rewritten here); no page moved, so no generated reference regenerates. - The card's ruling 2 (a per-user *timeline column* switch) is superseded on `main` by #28116: 动态 is the right column's first panel, never a column, and the reader's switch is the right column (⊟, `⌘⇧,`) plus the panel arrangement. The skill teaches what ships, per the dispatch. - Every rule sentence names a `main` code path at 8c3f9d9: `ui/packages/plugin-detail/src/synth/recordPageColumns.ts` (760 / 1120 px tiers), `RecordTabRow.tsx` + `synth/recordTabRow.ts` (the tab row, `?related=`), `synth/relatedArrangement.ts` (order / hidden per user × object), `ui/packages/app-shell/src/views/objectViewRecordNavigation.ts` (the view's `navigation`, else drawer whatever the field count; a phone → page), `ui/packages/app-shell/src/utils/recordOpenPreference.ts` (`split` default, drawer-mode objects only), `packages/spec/src/ui/view.zod.ts` `NavigationModeSchema` (the 18.3.0 retirements and their fix), `packages/spec/src/ui/page.zod.ts` (six slots, `tabs` retired, `isDefault`), `packages/spec/src/data/field.zod.ts` (`relatedList*`, the inherited sort), `packages/spec/src/data/object.zod.ts` (`nameField`, `highlightFields`, `stageField`, `fieldGroups`, `relatedLists`, `editMode`). - Out of this PR's file surface, two places still say "Details tab / shared Related tab": `skills/objectstack-data/rules/relationships.md` (the `relatedList` tri-state bullets) and the `relatedList` `.describe()` text in `packages/spec/src/data/field.zod.ts`, which ships to the generated references. Carrier: ui seat 1 (the first is a sibling skill with its own ratchet row, the second is spec text); not touched here. - The four files PR #28488 holds open (`rules/actions.md`, `rules/dashboards.md`, `rules/list-views.md`, `evals/views-apps-actions-pages.json`) are untouched, per the seat's overlap ruling. - The new CEILINGS row: the ratchet prints ⛔ MAINTAINER-ONLY for a file carrying no ceiling. It is written here under the card's PM-set line budget and the seat's ruling that the record page gets its own rules file, pinned at the landed count with the deletions that pay it down named; the Tier H approval of this PR is the maintainer's say on it. - The word "role" does not appear in the new file (`check:role-word` baseline unchanged); the ADR is cited by number only. ## Verification - Derived: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` against this diff → 87 commands (54 pnpm, 33 node). Every one run with `</dev/null`, its exit code recorded on disk as `<command> :: exit <code>` before any pipe. - Reconciled: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.list` → **87 derived, 87 run, 0 NOT-MEASURED, 0 UNRUN**; all 87 exits are 0. - Three gates were re-run after a fix or a prerequisite and recorded at their final exit: `check:doc-security-posture` (exit 1 on the first example, 0 after the `sharingModel` commit), `check:dual-build-cjs-loads` (exit 3 until the packages closure was built with declarations, then 0), `check:lean-entry-closure` (exit 1 while that build was rewriting `packages/spec/dist` under it, 0 after). - Builds under the shared verify lock (`scripts/pm/os-verify-lock.sh`, slot kept): spec dts + lint closure (VERDICT command-exit 0, 3m20s), `@objectstack/client-react...` (0, 5m17s), `turbo run build --filter='./packages/**' --filter='@objectstack/dogfood^...'` (0, 8m52s). - Not measured here, CI's: the type-check lanes, Build Core / Build Docs, the artifact-roster families and the always-runs tail the derivation lists outside its 87. No ui package or test changed (teaching text, docs and one gate script only), so no ui test imports a changed package. - Writes: `git push` ×2, relay `pr_create` ×1 (this PR), `os-dev-report` comment ×1 via `post-stamped`; GitHub MCP read calls only (`issue_read`, `pull_request_read`), 0 MCP writes. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No schema change.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; the lint-tier gates above pass. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: n/a (not touched). - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 block is the maintainer's requested Chinese digest). - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01L9qibyV5bs9mFPca9yJkR5)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01L9qibyV5bs9mFPca9yJkR5 --------- Co-authored-by: os-tesla <yinlianghui@steedos.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 044d201 commit efb1620

10 files changed

Lines changed: 187 additions & 48 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): objectstack-ui teaches the record page as the Console ships it, in one ordered rules file from the object's declarations to the reader's own choices
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** `SKILL.md` described a record page with Details / Related / Timeline tabs and a shared Related tab for side cards, `rules/pages.md` and `rules/navigation.md` each carried a sentence of it, nothing taught `relatedListFilter`, the inherited row order, the six slots, `isDefault` or which record-page choices are the reader's, and an agent asked to "put the tasks first on the lead page" reached for a full page.
11+
- **Now.** `rules/record-page.md` says, in order: the object's declarations (`nameField`, `highlightFields`, `stageField`, `fieldGroups`, `relatedLists`, `editMode`) produce the page and the surface's width lays it out (two columns from 760 px, three field columns from 1120 px, a tab row that enlarges one panel); the relationship flags shape the related column (`relatedList`, `relatedListTitle`, `relatedListColumns`, `relatedListFilter`, rows in the child's default list-view sort); a `slotted` page overrides one of six slots and has no `tabs` slot; `kind: 'full'` is the escape hatch; a record opens in the drawer unless the object's list view declares `navigation: { mode: 'page' }` (`modal` / `split` / `popover` are refused); and the reader's Open-records choice, ⊟ right column, panel order and pane width are never metadata. `SKILL.md`, `rules/pages.md` and `rules/navigation.md` keep one pointer each, and `evals/record-page.json` grades the lead-page task as `relatedList: 'primary'` and the full-page ask as the list view's declaration.
12+
- The package's hand-authored markdown grows by 39 lines against a budget of 40, paid with the tab sentences the record-page retirements made false; the docs' "How a list opens a record" names the live modes and the reader's choices. No schema, key or behaviour changes.

‎content/docs/ui/pages.mdx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,10 @@ pane or the drawer), never by the viewport, and by one rule. From 760 px the det
203203
the right column, 320 px wide; from 1120 px the right column is 380 px and a details section
204204
takes up to three field columns. Below 760 px (the split pane, a phone) the page is one column:
205205
the details, then the right column's panels in their order. The **⊟** switch at the end of the
206-
record header's actions (`⌘⇧,`) hides the right column for the reader, per object.
206+
record header's actions (`⌘⇧,`) hides the right column for the reader, per object. The related
207+
panels are the reader's to arrange, too: drag a panel's **⠿** handle (or **⌥↑** / **⌥↓** on it)
208+
to reorder, hide a panel from its header, and bring hidden panels back from the column's last
209+
line. The arrangement is kept per user and object, never in the object's declaration.
207210

208211
The related panels load in steps, and the details never wait for them. The page asks every
209212
panel's count in one request set, sent beside the record read, and the panels show

‎content/docs/ui/views.mdx‎

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -515,19 +515,31 @@ app page or action, where `features.*` stays bound and stays legal. A record *fi
515515

516516
## How a list opens a record
517517

518-
Clicking a row, card, node, bar or event opens the record in an overlay whose mode is
519-
the view's `navigation.mode`: `drawer`, `modal`, `split` or `popover`. Every list-type
520-
view (grid, tree, kanban, calendar, gantt, list, gallery, map and timeline) opens the
521-
same record panel, so a record reads with its declared labels and typed values, without
522-
its `hidden` fields, whichever view it came from. In `split` the view moves into the
523-
left panel; in `popover` the panel is anchored to what was clicked.
518+
Clicking a row, card, node, bar or event opens the record the one way its object declares:
519+
in the **drawer**, which expands to the full page, unless the view's `navigation.mode` is
520+
`page`, declared only for an object whose record should open as a full page (`new_window`
521+
and `none` are the other two values). Every list-type view (grid, tree, kanban, calendar,
522+
gantt, list, gallery, map and timeline) opens the same record page, so a record reads with
523+
its declared labels and typed values, without its `hidden` fields, whichever view it came
524+
from. `modal`, `split` and `popover` were removed in @objectstack/spec 18.3.0 (ADR-0087 D2):
525+
a stored value is refused with its fix, and `os migrate meta --from 17` drops it. On a phone
526+
every record opens as a page, whatever the view declares.
527+
528+
Whether the record opens beside the list is the reader's choice, not the view's. The list's
529+
**Display** menu offers **Open records**: **Split** (the default) draws the record page in a
530+
pane beside the list, **Drawer** draws it over the list, and **Full page** navigates. The
531+
choice is kept in the browser for every drawer-mode object, and is not offered for an object
532+
declared to open as a page; a record's **↗** always opens the full page. The reader also
533+
drags the pane's edge, and **⤢** or **Open as full page** leaves the pane, while **Collapse
534+
to drawer** returns to the stack it left. The `size` bucket on `navigation` sets only the
535+
drawer's starting width; `width` is deprecated.
524536

525537
The record opened from a list is addressable: `?recordId=` on the list's URL, so a
526538
reload, a shared link or Back reopens it. A record opened inside the drawer (a related
527539
row, a lookup value) opens one level down in the same drawer; those levels are
528-
in-memory, so a reload shows the root record. `⤢` opens a level's full page, and
529-
**Collapse to drawer** returns to the stack it left. A record page's own layout is
530-
described in [Pages](/docs/ui/pages).
540+
in-memory, so a reload shows the root record. A record page's own layout, and the
541+
reader's right-column and panel choices on it, are described in
542+
[Pages](/docs/ui/pages#record-pages).
531543

532544
### A user's list state never writes the view
533545

‎scripts/check-skills-token-ratchet.mjs‎

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,13 @@ export const CEILINGS = new Map([
639639
// ceil($(wc -c < skills/objectstack-ui/SKILL.md) / 4) == ceil(15424 / 4)
640640
// 3856 -> 3851: A2′ — the release-stamp line left the count (see
641641
// STAMP_LINE_REPIN); the file's count fell by the same 5, headroom unchanged.
642-
['skills/objectstack-ui/SKILL.md', 3851],
642+
// 3851 -> 3544 (#27059, record-page epic #26676 child 8): LOWERED. The two related-list
643+
// paragraphs that taught a Details / Related / Timeline tab strip (false since the
644+
// record page's tab row and right column landed) left for a two-line pointer at
645+
// `rules/record-page.md`; one Detailed Rules link and the CRM blueprint row reworded.
646+
// The file is 14194 bytes less its stamp line -> 3544 tokens; the ceiling follows it
647+
// down to the measurement, the ratchet's ordinary direction.
648+
['skills/objectstack-ui/SKILL.md', 3544],
643649
// 8333 -> 6193: RE-LOCK at the landed count after the #19715 split (decision
644650
// batch #213 item 1, maintainer 「1 2 4 同意」): the worked R1, the report
645651
// template and the v17-canonical shapes block moved VERBATIM into
@@ -855,13 +861,36 @@ export const CEILINGS = new Map([
855861
// prescription that objectstack-data → Search Fields carries whole — so the file nets -4
856862
// and the ceiling follows it down to the new measurement.
857863
['skills/objectstack-ui/rules/list-views.md', 3129],
858-
['skills/objectstack-ui/rules/navigation.md', 2273],
864+
// 2273 -> 2196 (#27059): LOWERED. The Surface bullet points at `rules/record-page.md`,
865+
// says create / edit is a modal or the `editMode: 'page'` route, and the Overlay-width
866+
// bullet moved there whole (the `size` bucket, the deprecated pixel keys); the ceiling
867+
// follows the file down to the measurement (8784 bytes).
868+
['skills/objectstack-ui/rules/navigation.md', 2196],
859869
// 5501 -> 5692 (#19715, card item 4): a 5-line table of contents at the top,
860870
// anchors to the file's existing headings, nothing else changed. +191 tokens,
861871
// zero headroom to absorb any of it, ceiling +191. Maintainer ruling
862872
// 2026-09-22 on decision batch #213 item 1, verbatim and untranslated:
863873
// 「1 2 4 同意」.
864-
['skills/objectstack-ui/rules/pages.md', 5692],
874+
// 5692 -> 5669 (#27059): LOWERED. The `tabs`-slot sentence is a one-line pointer at
875+
// `rules/record-page.md`; the ceiling follows the file down to the measurement (22673 bytes).
876+
['skills/objectstack-ui/rules/pages.md', 5669],
877+
// objectstack-ui `rules/record-page.md` — the sixth rules file, ordered by card #27059
878+
// (record-page epic #26676 child 8, dispatched by ui seat 1 2026-11-01, with the seat's
879+
// ruling that the record page gets its own rules file rather than rows in the files
880+
// PR #28488 holds open). INITIAL measurement AT its landed count, on the same terms as
881+
// the #14296 rows above: zero headroom, the next token added here is paid for by
882+
// deleting one from this file. What it teaches, in order: the object's declarations
883+
// produce the page, the relationship flags shape the related column, a slotted page
884+
// overrides one of six slots (no `tabs`), `kind: 'full'` is the escape hatch, the
885+
// drawer unless the object declares `page`, and the reader's own choices are not
886+
// metadata. PAID DOWN FIRST by the 404 tokens the three rows above gave up (the tab
887+
// strip the record page no longer has, SKILL.md 307 + navigation.md 77 + pages.md 20, its
888+
// ceiling 23);
889+
// the card's binding budget is the package's hand-authored LINE count, +40 at most,
890+
// landed at +39 (1876 -> 1915), so the package nets +592 tokens, every one of them
891+
// teaching the surface `main` ships today and none of it restating a sibling file.
892+
// ceil($(wc -c < skills/objectstack-ui/rules/record-page.md) / 4) == ceil(3981 / 4)
893+
['skills/objectstack-ui/rules/record-page.md', 996],
865894

866895
// ── 2026-09-22 the #19715 split — decision batch #213 item 1 ─────────────
867896
// The maintainer ruled the four over-500-line published SKILL.md files split

‎skills/objectstack-ui/SKILL.md‎

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -102,29 +102,8 @@ authored on this side: a trailing **ghost** row always exists, so never add an
102102
never persisted); `autofill: false` opts one column out of the typeahead copy;
103103
and the form's `taxRateField` overrides which parent field drives the totals.
104104

105-
**Read side — detail-page related lists.** The mirror of `inlineEdit` is the
106-
related list on the parent's record DETAIL page. You don't author it: every
107-
child relationship is shown as a related list by default (owned `master_detail`
108-
children first). Refine on the relationship:
109-
- `relatedList: 'primary'` — main content: a full-width body table (see layout
110-
below); it places, the parent's `relatedLists` orders (ADR-0085).
111-
- `relatedList: false` — suppress a noisy child from the detail page.
112-
- `relatedListTitle` / `relatedListColumns` — override the derived title /
113-
columns (both optional; columns otherwise auto-derive from the child object's
114-
`highlightFields`). See objectstack-data → Relationships → Detail-page related lists.
115-
116-
**Related-list layout.** On the synthesized record page, each
117-
`relatedList: 'primary'` child (default: inline-edited `master_detail`) is a
118-
body table in the details column after the first field section (in the
119-
Details tab, no tab of its own); every other list is a side card in one shared
120-
**Related** tab, both in the object's `relatedLists` order. Field changes
121-
(`trackHistory`) join the Timeline tab under its Changes filter; there is no
122-
History tab. The object declares no per-surface *layout* hints: the
123-
`detail.relatedLayout` toggle, object-level `detail: {...}` block and app-level
124-
`relatedLayout` are **removed** (ADR-0085). For arrangements the relationship
125-
layer can't express — filtered splits (e.g. Open vs Closed tabs), a chart/report
126-
tab — assign the object a **custom record Page** and lay it out
127-
explicitly with `record:related_list` (or inline-editable `line_items`) blocks.
105+
**Read side — the record page.** Child relationships are related lists on the
106+
parent's record page by default; the flags, slots and open mode: [Record Page](./rules/record-page.md).
128107

129108
### Field Conditional Rules in Forms
130109

@@ -146,6 +125,7 @@ authoring non-trivial CEL.
146125
- [Navigation & Run Modes](./rules/navigation.md) — `App.create`, the three run modes, record presentation.
147126
- [Dashboards, Reports & Cubes](./rules/dashboards.md) — widgets, dataset binding, filters, `compareTo`, bucketing, `options`, drilldown, report config, cubes.
148127
- [Pages & Docs](./rules/pages.md) — page types, regions, components, print pages, the html/react source tiers, styling, package docs.
128+
- [Record Page](./rules/record-page.md) — declarations → page, relationship flags, slotted / full overrides, drawer vs `page`, the reader's own choices.
149129
- [Actions](./rules/actions.md) — `locations`, visibility, examples, `ctx`, new tab, params.
150130

151131
---
@@ -207,7 +187,7 @@ Use this CRM-style structure as the canonical UI assembly reference:
207187
| **Analytics dataset** | `src/datasets/*.dataset.ts` | One per object you want reportable — dashboards and reports bind a **declared** dataset by name, so an object without one has no analytics face |
208188
| Dashboards | `src/dashboards/*.dashboard.ts` | Combine KPI + chart + table widgets with shared `dateRange` and `globalFilters` |
209189
| Reports | `src/reports/*.report.ts` | Select `rows` (dimensions) + `values` (measures) from that dataset; tabular/summary/matrix/joined |
210-
| Record pages | `src/pages/*.page.ts` | Compose `regions` + components (`page:header`, `record:highlights`, related lists, tabs) |
190+
| Record pages | — (synthesized) | The object's declarations produce the page ([Record Page](./rules/record-page.md)); `src/pages/*.page.ts` only to override a `slotted` slot or for a `kind: 'full'` layout |
211191
| User actions | `src/actions/*.actions.ts` | Use `flow` for orchestration and `script` for parameterized bulk mutations |
212192

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

‎skills/objectstack-ui/evals/README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ view / page / dashboard / report metadata.
99
dashboard/report widgets: when a data need fits a `defineDataset`, when it
1010
must escalate to a stored field or app code, and when an ad-hoc
1111
in-page `<ObjectChart>` needs no dataset at all.
12+
- `record-page.json` — the record page as the object declares it: a child
13+
table placed first by `relatedList: 'primary'` (no page file), and
14+
`navigation: { mode: 'page' }` on the list view with the reader's own
15+
open choice left out of metadata.
1216
- `report-period-export.json` — a non-`joined` report's `dateRange` (one
1317
`type: 'date'` dataset dimension) and `exportOptions` (csv / xlsx under the
1418
base object's `allowExport` permission).
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"skill_name": "objectstack-ui",
3+
"evals": [
4+
{
5+
"id": 1,
6+
"prompt": "On the lead record page, put the tasks first: our reps want the open tasks table before anything else when they open a lead. The `task` object already has a `lead` master_detail field.",
7+
"expected_output": "No page file. One flag on the child's reference field — `relatedList: 'primary'` on `task.lead` — makes the tasks a full-width body table in the lead's details, after the first field section; `relatedListFilter: { status: { $ne: 'done' } }` keeps it to open tasks, and the lead's `relatedLists: ['task', ...]` puts it before the other lists. Not a `definePage` with `kind: 'full'`, and never a `slots.tabs`.",
8+
"files": [],
9+
"assertions": {
10+
"must_contain": [
11+
"relatedList: 'primary'",
12+
"relatedLists"
13+
],
14+
"must_not_contain": [
15+
"definePage",
16+
"slots.tabs",
17+
"kind: 'full'"
18+
]
19+
}
20+
},
21+
{
22+
"id": 2,
23+
"prompt": "Contracts should open as a full page, not the side drawer. Reps also keep asking for records to open beside the list — add that to the contract object too.",
24+
"expected_output": "One declaration: `navigation: { mode: 'page' }` on the contract list view. The pane beside the list is each reader's own choice in the list's Display menu (Open records: split, drawer or full page), kept in the browser and offered for drawer-mode objects only, so nothing is authored for it; `split`, `modal` and `popover` were removed in @objectstack/spec 18.3.0 and are refused.",
25+
"files": [],
26+
"assertions": {
27+
"must_contain": [
28+
"navigation: { mode: 'page' }",
29+
"Display menu"
30+
],
31+
"must_not_contain": [
32+
"mode: 'split'",
33+
"mode: 'modal'",
34+
"mode: 'popover'",
35+
"definePage"
36+
]
37+
}
38+
}
39+
]
40+
}

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

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -124,24 +124,19 @@ export const TaskWorkbenchPage = definePage({
124124

125125
A record's create / edit / detail **presents itself adaptively**. You do **not**
126126
author the surface, the overlay width, or the column count — all three are derived
127-
at runtime from how heavy the record is + the client viewport, because an author
127+
at runtime from the client viewport, because an author
128128
(especially an AI) cannot know the client's screen. **Write the data (fields,
129129
`fieldGroups`); let the platform lay it out.**
130130

131-
- **Surface.** A clicked record opens in a **drawer** that expands to the full
132-
page (on mobile, a **page**); create/edit opens in a **modal** for a
133-
field-heavy record, otherwise a drawer. Set `navigation.mode: 'page'` on the
134-
list view only for an object whose record should open as a full page — or,
135-
for bespoke layout, assign a record `Page`.
131+
- **Surface.** A clicked record opens in the **drawer**, which expands to the full
132+
page; `page` only by declaration ([Record Page](./record-page.md) §5–6). Create /
133+
edit opens in a **modal**, or the full-page route when the object declares `editMode: 'page'`.
136134
- **Field width.** **Omit it** (`auto` sizes by widget type × current columns —
137135
textarea / rich-text / file take the row). Both keys are valid: `colSpan`
138136
(1–4) is clamped to the current column count, grid-aligned at every width;
139137
`span: 'full'` resolves to the form grid's full column count, but the shipped
140138
renderer emits it at the widest tier only (a 3-column form: one cell of two
141139
in a modal).
142-
- **Overlay width.** Never author pixels. If you must nudge, use the **`size`**
143-
bucket (`sm` | `md` | `lg` | `xl` | `full`) on `navigation`; the pixel
144-
`width` / `drawerWidth` are deprecated.
145140
- **Column count.** Not authored. The form grid follows its **real rendered width**
146141
via container queries — the same form is 1 column in a narrow drawer and up to 4
147142
on a wide page. Author *grouping* with `fieldGroups` + `Field.group`.

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ five) or `dashboard` (`header`, `footer`; other regions are grid cells);
3737
`full-width` is retired (a second name for `default`). On a `kind: 'full'` page a region
3838
the template does not place, or a second one for a filled slot, is refused at
3939
its `name` — the left column is `sidebar`, the right `aside`.
40-
A `slotted` record page has no `tabs` slot: shape the strip with `relatedList` /
41-
`relatedList: 'primary'` on the child's reference field, or author `kind: 'full'`.
40+
A `slotted` record page has no `tabs` slot — see [Record Page](./record-page.md).
4241

4342
### Component Catalogue (selection)
4443

0 commit comments

Comments
 (0)