Skip to content

Commit a61ddc9

Browse files
docs(skills): objectstack-ui carries the view and action judgment of cloud's private corpus — a view added beside the list, the empty board over a new group field, a button whose body writes (#31155, port 3/3) (#31479)
Part of #31155 — card 1 of 4 of the objectstack-ai/cloud#3659 ruling-A split; this is the third and last per-domain port (`objectstack-ui`), after #31223 (`objectstack-data`) and #31393 (`objectstack-automation`). It closes the card's port work; the card stays open for the seat's close-out. Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:把云端私有搭建语料里「UI」这一块、schema 表达不了的四条判断搬进公开的 `objectstack-ui` skill:① 「加一个看板/日历/画廊/甘特视图」= 新建一个 `listViews` 条目(键名编码种类,如 `status_kanban`),默认 `list` 原样保留,只有用户明说「改成/换成」才原地改种类;② 按刚新建的字段分组的看板首次打开是空的——已有记录全落在最后的 `Uncategorized` 泳道,交付时要说明(并更正云端原文:SQL 数据源上字面量 `defaultValue` 会成为新列的 DEFAULT,已有行会读到它);③ 「标记完成/审批/提交」这类操作是 `action` 元数据不是视图,写按钮的 body 必须真的调用 `ctx.api.object(OBJ).update`(`language: 'js'` + `api.write`),只 return 新值什么都不会写;④ 用户点名的视图种类(画廊/甘特/看板)照建,不降级成表格。外加 3 条 eval 用例;顺手把 `rules/navigation.md`、`rules/record-page.md` 残留的中文改成英文。净增 65 行。 - 为什么改:执行 cloud#3659 裁决 A(一个知识源:本地开发与云端 AI 用同一套公开 skills)。第 ② 条云端原文「defaultValue 只对新记录生效、不影响已有记录」对照 `main` 的 SQL 驱动是错的(`applyDeclaredColumnDefault` 把字面量默认值写进 ADD COLUMN 的 DEFAULT),本 PR 按运行时真实行为搬入,并在卡上记了更正。 - 风险与回滚:只改文档与 eval 夹具,无运行时改动;四个 token 棘轮行按实测钉住(两行抬高、两行降低,依据 cloud#3659 A 与「先削重复,余额按实测提高」,同文件先删了重复)。回滚 = revert 本 PR。 - 先看:`skills/objectstack-ui/rules/list-views.md`(新段落 Adding a board…,尤其 (c) 那句对照 `packages/drivers/driver-sql/src/sql-driver-schema-sync.ts` `applyDeclaredColumnDefault` 与 `sql-driver-rotation.ts` 的 ADD COLUMN 路径)、`skills/objectstack-ui/SKILL.md`(§ Actions 新段,对照 `packages/runtime/src/sandbox/quickjs-runner.ts` 的 `api.write` 门与 `packages/lint/src/rule-explanations.ts` 的 `action-record-write-discarded`)、`scripts/check-skills-token-ratchet.mjs`(四行)。 - 两分钟验证:`node scripts/check-skills-token-ratchet.mjs`,预期全绿且 objectstack-ui 四行 headroom 0;`python3 -c "import json;json.load(open('skills/objectstack-ui/evals/views-apps-actions-pages.json'))"` 不报错。 - 需要你做的:Tier H 受管面(`skills/**`),需要你的 APPROVED review 后由席位落地;本 PR 保持 draft。 ## What changes Six files under `skills/objectstack-ui/`, one changeset, four token-ratchet rows. Every sentence was checked against `packages/spec` and the runtime on `main` (merged in at 708922f); the one private sentence that was false is ported corrected (below). - `rules/list-views.md` — a new section after the kanban config, **Adding a board, calendar, gallery or gantt beside the list** (add-view-not-convert): "add a kanban / calendar / gallery / gantt view" is a NEW `listViews` entry whose key encodes its kind (`status_kanban`, `timeline_gantt`: the key a nav item's `viewName` names, registered as `OBJECT.status_kanban`), the default `list` and every other view untouched; retyping `list` to satisfy an add converts it in place and nothing refuses it; a kind changes in place only when the request says to convert or replace THAT view. Beneath it, **A board over a field added in the same change opens empty** (19f): adding a field writes nothing into existing rows, so every pre-existing record lands in the trailing `Uncategorized` lane — the populated lane last, past the right edge of a narrow screen, under a non-zero count; the renderer's correct fallback, not lost data; the three-part hand-over (all in the last lane, scroll right; dragging a card out writes the field, or edit from the list; what a `defaultValue` does to existing rows); the same disclosure when a list is converted into such a board. Same-file pay-downs first: the kanban example's `// required on every list view` comment (the sentence above it says it), the `userFilters` bullet's "Need named presets on an object? Add a `listViews` entry instead" (the bullet before it), "the saved-views switcher already covers presets" (same bullet), the search example's `searchableFields: []` comment (the table row above it), the narrowing sentence's "refuses the request" clause (the ⛔ section below it), the two Data Source code comments (the blockquote under them), the `summary` comment's "footer aggregation" (the Column Features row). - `SKILL.md` § View Types — **Keep the kind the request names** (the PORT clause of auto-surfaced): a photo wall / card wall / catalogue of covers is `gallery` with an image, avatar or file field as `gallery.coverField`, a schedule or timeline over a start/end pair is `gantt`, a board is `kanban`; never downgrade a named kind to `grid` — the file parses and the user opens a table where they asked for a board; a pointer to the new list-views section. - `SKILL.md` § Actions — **An operation is an `action`, never a view** (13): "mark as done", "approve", "submit", "set status to X" is `defineAction` metadata, a view only displays; a record button carries `objectName` (what folds it into the object's record surfaces) and `locations` (with neither `locations` nor a view naming it in `rowActions` / `bulkActions` it renders nowhere — `action-no-placement`); **a write button must write**: `ctx.record` is a read-only snapshot and the body's return value is applied to nothing, so a body that only returns the new values or assigns to `ctx.record` changes nothing and still reports success (`action-record-write-discarded`); the shape that writes is `type: 'script'` with `body: { language: 'js', source: "await ctx.api.object('task').update({ id: ctx.recordId, status: 'done' });", capabilities: ['api.write'] }` — `js` because an `expression` body is a pure formula with no I/O, `api.write` because the sandbox throws on `ctx.api…update` / `insert` / `delete` without it, the option `value` never its label; a multi-step operation is `type: 'flow'` with `target` naming a flow you also author. Same-file pay-downs first: the intro paragraph restating the frontmatter description, the "Load objectstack-formula" sentence restating the frontmatter, Common Pitfalls 1–2 restating `rules/list-views.md` § Column Features (one pointer item now), the blueprint row's `slotted` / `full` clause restating the Detailed Rules line, the duplicated "canonical reference" sentence, the App-navigation row's gloss. - `rules/navigation.md`, `rules/record-page.md` — English only (below); each file's ceiling LOWERED to its measurement. - `evals/views-apps-actions-pages.json` — cases 13 (a board added beside the list over a just-added `priority` select, with the empty-board disclosure), 14 (a "Mark as done" button whose body calls `ctx.api.object('task').update` with `api.write`), 15 (a photo wall is `type: 'gallery'` with `gallery.coverField`); `evals/README.md` names them. `must_not_contain` entries are the shape a wrong answer writes (`defineView(`, `list: { type: 'kanban'`, `language: 'expression'`, `type: 'grid'`), the form `main` narrowed the fixtures to; no entry appears in its own `expected_output`. - `.changeset/31155-port-authoring-rules-ui.md` — `@objectstack/skills` patch, `Clause-②: no`, `Metadata-protocol: none`. - `scripts/check-skills-token-ratchet.mjs` — four rows (table below). **Language scope (approver os-zhuang on #31223, verbatim 「skills 对外发布的版本应该全部用英文」; the account owner's 「同意」):** every non-eval file this PR touches holds 0 CJK code points at the head (Python scan over the CJK Unified, kana, fullwidth and CJK-punctuation blocks), and so do the lines it adds to the ratchet script. The two pre-existing carriers in this skill are English now: `rules/navigation.md` ("a simplified screen for the business team", "workbench" in the interface-page signal list) and `rules/record-page.md` (the record page's second tab is `Activity`, the console's own label — `ui/packages/plugin-detail/src/RecordPageColumns.tsx`, `t('detail.timeline', { defaultValue: 'Activity' })`). Eval prompts keep their language under exemption (a); `objectstack-i18n` untouched (b). The catalog's remaining carriers (`objectstack-formula/SKILL.md`, `objectstack-upgrade/SKILL.md`, `objectui/guides/i18n.md`) are not this PR's. ### Line readings (`skills/**`, against `origin/main` 708922f, merged in) | reading | before | after | |:--|:--|:--| | `SKILL.md` | 286 | 299 | | `rules/list-views.md` | 308 | 329 | | `rules/navigation.md` | 157 | 156 | | `rules/record-page.md` | 65 | 65 | | `evals/views-apps-actions-pages.json` | 125 | 155 | | `evals/README.md` | 39 | 41 | | every `skills/*/SKILL.md` summed | 4553 | 4566 | | whole shipped `skills/` tree | 20328 | 20393 | Net **+65** lines against the dispatch's +250 budget (evals included). ### Token-ratchet rows (priced bytes; a SKILL.md's stamp line excluded) | row | `main` 708922f | head | ceil/4 | ceiling (was → now) | same-file pay-down shown | |:--|:--|:--|:--|:--|:--| | `SKILL.md` | 14151 (14131 priced) | 15276 (15256 priced) | 3814 | 3544 → 3814 (+270) | six deletions, listed above | | `rules/list-views.md` | 12503 | 13802 | 3451 | 3129 → 3451 (+322) | seven deletions, listed above | | `rules/navigation.md` | 8779 | 8734 | 2184 | 2196 → 2184 (−12, LOWERED) | the bullet's repeated objectui ADR-0055 citation (the heading carries it) | | `rules/record-page.md` | 3982 | 3961 | 991 | 996 → 991 (−5, LOWERED) | step 5's "(one page at two widths)" (step 1 says layout follows the surface's width) | All four pinned AT the measurement, headroom 0; `main` did not move any of the four files between the branch base and the merge (its touches in this skill are three eval fixtures). Bundle: ratcheted (authored) count 184328 → 184903 (+575), ceiling sum 192290 → 192865 (+575); eval fixtures measured, not ratcheted. Authority: the ruling of record that orders this text into the published skills, objectstack-ai/cloud#3659 letter A (ruling comment 6682024726, 2026-11-06: "How to write valid, working metadata lives in the public skills … The private corpus's rules of that kind move there"), via the standing 2026-10-27 ruling, verbatim and untranslated: 「A:先削重复,余额按实测提高」 (ruling record 6513306068). ## No-loss mapping — the `objectstack-ui` rows (v1 ledger 6711215678; corrections 6711710289, 6715112987; ledger note 3 6717932694; this PR's note 4 on the card) Classes as in the ledger: CLOUD (orchestration / hosted-only, stays), SPEC (restates `packages/spec`), E/R (parses clean, dead at runtime — the spec lane), STATED (the published skill already says it), PORT (judgment, ported here). Spec paths are under `packages/spec/src/`. | private rule id | clause | class | target → this PR | regression kept | |:--|:--|:--|:--|:--| | auto-surfaced | the tool creates the new object's list view and nav entry (`listViewCreated` / `addedToAppNav`); older apps' built-in default list exists in no metadata listing; author a view only for a non-default one the user asked for | CLOUD | hosted tool envelope; a local project's `defineView` `list` is the default | stays in cloud (`authoring-rules.test.ts` corpus integrity) | | auto-surfaced | the view kinds (list, form, kanban, calendar, gallery, gantt) | SPEC + STATED | `ui/view.zod.ts` `VisualizationTypeSchema` / the list-view `type` enum; SKILL.md § View Types rows | — | | auto-surfaced | "a request for a gallery is a `gallery` view, NOT a grid; never silently downgrade a named view kind to a plain list" | **PORT** | SKILL.md § View Types, "Keep the kind the request names" (`gallery.coverField` from `GalleryConfigSchema`, `ui/view.zod.ts`) | no cloud pin names it; eval 15 | | add-view-not-convert | "add a kanban / calendar / gallery / gantt view" = a NEW distinctly-named view that coexists; never retype `OBJECT.list`; convert only on an explicit ask | **PORT** | `rules/list-views.md` § Adding a board, calendar, gallery or gantt beside the list (the `listViews` key, `viewName`, the registered name — `list-views.md` § The `defineView` container and `app.zod.ts` `viewName`) | cloud `kanban-new-group-field-disclosure.test.ts` "reachable from the ADD A VIEW rule" → the 19f paragraph sits directly under this one and SKILL.md § View Types points at the section; eval 13 | | add-view-not-convert | the tools reject an in-place kind flip | CLOUD | hosted `update_metadata` / `apply_edit` refusal; in a project file nothing refuses it, which the ported text says | stays in cloud | | 19f | a board grouped by a field created in the same turn opens all-empty: adding a field never backfills; the `Uncategorized` lane is last and off a narrow screen; disclose (a) where the records are, (b) how to fill them, (c) that a `defaultValue` applies to NEW records only | **PORT, (c) corrected** — against `main`, `packages/drivers/driver-sql/src/sql-driver-schema-sync.ts` `applyDeclaredColumnDefault` emits a literal `defaultValue` as the column DEFAULT (case 4), and `sql-driver-rotation.ts`'s existing-table path adds a missing column through `createColumn`, which calls it — so on a SQL datasource the pre-existing rows READ a literal default and land in its lane; a runtime token (case 2), a CEL envelope (case 3) and an option-level `default: true` emit nothing and backfill nothing. The lane: `ui/packages/plugin-kanban/src/index.tsx` pushes `KANBAN_UNCOLUMNED_ID` last with the `Uncategorized` title; `ObjectKanban.tsx` writes the target lane's value on a drag out of it and refuses to persist a drop into it | `rules/list-views.md`, the paragraph under the new section | cloud `kanban-new-group-field-disclosure.test.ts` (7 cases) → eval 13 (`Uncategorized`, `groupByField: 'priority'`, `listViews` must_contain; `list: { type: 'kanban'`, "ready to use" must_not_contain); the cloud test stays pointing at the moved text; the cloud sentence (c) is for cloud to correct when card 4 (cloud#3714) rewires the corpus — carrier: cloud | | 13 | a button / operation is `action` metadata, not a view; `objectName` binding; `locations`; a body that only RETURNS the updates writes nothing; `language: 'js'` never `'expression'`; `capabilities: ['api.write']`; `ctx.recordId`; option VALUE never label; multi-step → `type: 'flow'` | SPEC + STATED + **PORT** | SPEC: `ui/action.zod.ts` (`objectName` "auto-merged into the object's actions array by defineStack()", `locations`, body `language` expression = L1 pure formula vs js = L2). STATED: `rules/actions.md` examples (`objectName`, `language: 'js'`, `capabilities: ['api.write']`, `ctx.api.object(...)`), § Action body context; SKILL.md blueprint row (`flow` for orchestration). PORT: SKILL.md § Actions, the new paragraph — verified on `main`: `packages/runtime/src/sandbox/body-runner.ts` `buildActionSandboxContext` (`recordId` from the action ctx or `record.id`) and `warnDiscardedRecordWrites` ("the writes never left the sandbox … call ctx.api.object(…).update({ id: ctx.recordId, … }) (needs the 'api.write' capability)"); `quickjs-runner.ts` `installApiMethod` gates `update` / `insert` / `delete` on `api.write` and throws a capability denial; `packages/lint/src/rule-explanations.ts` `action-record-write-discarded` ("the handler returns the body's value and applies nothing to the record") and `action-no-placement` | no cloud text pin; eval 14 (`defineAction`, `objectName: 'task'`, `ctx.api.object('task').update`, `ctx.recordId`, `api.write`, `status: 'done'`; never `defineView(`, `language: 'expression'`, `status: 'Done'`) | | 13 | `get_metadata_schema('action')` first; `verify_build` lints `action_*`; the three cloud lint ids | CLOUD | hosted tool loop; this repo's equivalents are named in the ported text where one exists (`action-no-placement` for a missing `locations`, `action-record-write-discarded` for a `ctx.record` assignment, the sandbox's capability denial at run time for a missing `api.write`); a missing `objectName` has no lint here (`objectName` is optional — an object-less action is a global action by contract) | stays in cloud | | 13-tier | the tier's action types | CLOUD | restriction profile (#31156 / cloud#3713) | stays in cloud | | 2 | kanban / gallery / gantt config shapes; a dashboard TOTAL needs a stored number; a group-by over a multi-value field is no chart axis | SPEC + STATED | `ui/view.zod.ts` `KanbanConfigSchema.groupByField` REQUIRED, `GalleryConfigSchema.coverField`, `GanttConfigSchema` required trio; `rules/list-views.md` kanban + gantt sections (and `gallery.coverField` named in SKILL.md now); `rules/dashboards.md` "Breakdown by a multi-value field" (`INVALID_FIELD` on `groupBy`, `DATASET_INVALID` at compile for a dataset dimension — so the ledger's E/R row 8 "verify" reads as enforced on `main`) and the stored-number measure rule; evals `analytics-inline-vs-dataset.json` 6–7 | no cloud pin | | dashboards-second-beat (solution_design) | dashboards only when asked; blueprint `aggregate` / `measure`; calendar `startDateField` | CLOUD + SPEC + STATED | unchanged from the v1 ledger | — | **Conflicts with spec on `main`:** none in the four ported clauses. One conflict with the runtime, resolved by porting the true statement (19f (c) above); the cloud corpus's own sentence is for cloud to correct (card 4 of the split rewires that corpus). **Ledger state after this PR:** every PORT row of the v1 ledger has landed — objectstack-data (#31223, merged), objectstack-automation (#31393, merged), objectstack-ui (this PR). What the ledger still carries is keep-in-cloud (CLOUD rows), SPEC / STATED mappings (no prose owed), and the enforce-or-remove rows routed to the spec lane (#31224 and the two halves ledger note 3 named for the seat). No rule is left un-ported. ## Tests Docs and eval fixtures only, no runtime change ⇒ no ablation owed. The eval file parses (`json.load`); ids 1–15 contiguous and unique; a per-case cross-check over cases 13–15 finds no `must_not_contain` string inside its own `expected_output` and every `must_contain` string inside it. Prerequisite builds under the shared verify lock, after the merge of `origin/main` (AGENTS.md §9 — `packages/spec`, `lint`, `objectql` moved on `main`): `pnpm --filter '@objectstack/lint...' --filter '@objectstack/objectql...' --filter '@objectstack/client-react...' build` — `os-verify-lock: VERDICT command-exit 0 · held the lock 311s` (round 0 before the merge: 96s + 266s). ## Acceptance notes - **Cloud corpus text that is false against this repo's runtime (carrier: cloud, card 4 cloud#3714 or a cloud card):** rule 19f's clause (c), quoted in the table. The hosted agent today tells the user a literal `defaultValue` changes nothing about existing rows; on a SQL datasource it does. - **Observation, not filed (class: none of (a)/(b)/(c) as measured here; carrier: none):** `applyDeclaredColumnDefault` makes a literal `defaultValue` behave differently per datasource on a field added to an existing table — SQL dialects fill the existing rows through the column DEFAULT, the memory and mongodb drivers do not — and the spec's `defaultValue` describe ("applied on INSERT") does not mention the DDL half. The driver's own docblock names exactly this split as the reason it does NOT emit an option-level `default: true`. Read from source only; no end-to-end probe was run, so it stays an observation for the seat. - **E/R row 8 of the v1 ledger** (dashboard `dimensions` over a multi-value field, "verify") reads as enforced on `main`: `rules/dashboards.md` states `INVALID_FIELD` for `groupBy` and `DATASET_INVALID` at compile for a dataset dimension over one. Recorded here; the ledger note on the card says so. - Eval `must_contain` tokens `groupByField: 'priority'`, `ctx.recordId`, `coverField: 'main_image'` are prompt-specific values, as `applicant_email` / `signed_copy` are in earlier cases; the root catalog has no taught-token oracle (that gate scans `ui/skills` only). - The dispatch said `rules/navigation.md` carried 13 CJK lines and `rules/record-page.md` 4; on the branch base both carried 2 (the counts above are code-point lines). Both are English now. - Not ported, by class: the CLOUD halves above; the `objectName` lint half of 13 (no equivalent here, by contract). ## Verification Head cited: `cc008cb1` (one wording commit on top of merge commit 5c42c30 of `origin/main` 708922f; the union below ran on it). Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` — 64 families (the merge added `check:skill-links`); every exit code captured to a file before any pipe (`COMMAND > LOG 2>&1; status=$?`); reconciled with `--ran`. - `✓ dispatch-gates --ran: 64 derived famil(ies) accounted for — 63 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).` - 63 families exit 0, among them: `node scripts/check-skills-token-ratchet.mjs :: exit 0` — `skills/objectstack-ui/SKILL.md is 3814 tokens (ceiling 3814; headroom 0)`, `rules/list-views.md is 3451 tokens (ceiling 3451; headroom 0)`, `rules/navigation.md is 2184 tokens (ceiling 2184; headroom 0)`, `rules/record-page.md is 991 tokens (ceiling 991; headroom 0)`, `evals/views-apps-actions-pages.json is 3223 tokens — eval fixture, measured but not ratcheted`; `--self-test :: exit 0` (97 cases); `pnpm check:role-word :: exit 0`; `pnpm --filter @objectstack/spec run check:skill-docs :: exit 0`; `pnpm check:skill-links :: exit 0`; `pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0`; `pnpm check:lean-entry-closure :: exit 0`; `pnpm check:dts-closure :: exit 0`; `pnpm check:published-files :: exit 0`; `pnpm check:pm-dispatch-gates :: exit 0` (detached, exit written to a file; `2205 cases pass`); `check:nul-bytes`, `check:doc-authoring`, `check:closing-keyword-parity`, `check:skill-frame-sync`, `check:skill-identifier-liveness`, `check:skill-compatibility`, `check:pm-skill-id-lint`, `check:ratchet-remedy-authority` and the rest exit 0. - NOT MEASURED locally: `pnpm check:dual-build-cjs-loads :: exit 3` (PREREQUISITE NOT MET — needs every package's dist, a full `pnpm build`; CI's `Lint & Repo Gates` reads it). - No `os:check` block was edited, so `check:skill-examples` is not owed beyond the derived list. - CJK scan (Python, the blocks named above) over every non-eval file in the three-dot diff and the lines this PR adds to the ratchet script: 0. Control-character scan (`grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'`) over every touched file: 0. - `test:repo` not run locally (no test file in the diff; CI `Test Core`). No `dogfood` battery: no `!` in the title and no `(narrowing)` arm. Governed `skills/**` ⇒ Tier H: draft kept; landing waits for an APPROVED review from `GOVERNED_APPROVERS`; no seat arms, queues or readies it. CI check-runs: read by the seat at hand-over, not awaited by the dev. --- _Generated by [Claude Code](https://claude.ai/code/session_012dn2AdARJtc4RgABPH7PHk)_ --------- Co-authored-by: os-bob <noreply@anthropic.com>
1 parent 507b53c commit a61ddc9

8 files changed

Lines changed: 160 additions & 44 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): objectstack-ui carries the view and action judgment the hosted build agent learned — a view added beside the list, the empty board over a just-added group field, an operation is an action whose body writes, the view kind the request names
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Card 1 of the cloud#3659 ruling A split, the `objectstack-ui` port (the last of three) — the judgment the schema cannot express and the published skill did not yet state. `rules/list-views.md`: "add a kanban / calendar / gallery / gantt view" is a NEW `listViews` entry whose key encodes its kind, the default `list` untouched; a view's kind changes in place only on an explicit ask; a board grouped by a field added in the same change opens with every existing record in the trailing `Uncategorized` lane, and the hand-over says so — with the correction that on a SQL datasource a literal `defaultValue` is emitted as the added column's DEFAULT, so there existing rows read it. `SKILL.md`: an operation on a record is `defineAction` metadata, never a view; a write button's body must call `ctx.api.object(OBJ).update` with `language: 'js'` and `api.write` — a body that only returns the new values writes nothing; a named view kind (gallery, gantt, kanban) is kept, never downgraded to `grid`. The two Chinese fragments left in `rules/navigation.md` and `rules/record-page.md` are English now: the shipped skill text is English-only. Three eval cases pin the teachings (views-apps-actions-pages 13–15).

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

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -704,7 +704,22 @@ export const CEILINGS = new Map([
704704
// `rules/record-page.md`; one Detailed Rules link and the CRM blueprint row reworded.
705705
// The file is 14194 bytes less its stamp line -> 3544 tokens; the ceiling follows it
706706
// down to the measurement, the ratchet's ordinary direction.
707-
['skills/objectstack-ui/SKILL.md', 3544],
707+
// 3544 -> 3814 (#31155, the objectstack-ui port of the cloud#3659 ruling-A split): two
708+
// teachings the hosted build agent carried privately — an operation on a record is `action`
709+
// metadata, never a view, and a write button's body must CALL `ctx.api…update` with
710+
// `language: 'js'` and `api.write` (the return value is applied to nothing); and the view
711+
// kind the request names is kept, never downgraded to `grid`. PAID DOWN FIRST by genuine
712+
// deletion in this file: the intro paragraph restating the frontmatter description, the
713+
// "load objectstack-formula" sentence restating the frontmatter, Common Pitfalls 1–2
714+
// restating `rules/list-views.md` § Column Features (one pointer line now), the blueprint
715+
// row's `slotted` / `full` clause restating Detailed Rules' Record Page line, the
716+
// "canonical reference" sentence said twice, the App-navigation row's gloss. `main` base
717+
// 14151 bytes (14131 priced) -> 15276 (15256 priced) -> 3814 tokens, pinned AT the
718+
// measurement, zero headroom. Authority: the ruling of record that orders this text into
719+
// the published skills, objectstack-ai/cloud#3659 letter A (ruling comment 6682024726), via
720+
// the standing 2026-10-27 ruling (record 6513306068), quoted verbatim in the PR body.
721+
// ceil(($(wc -c < skills/objectstack-ui/SKILL.md) - 20) / 4) == ceil(15256 / 4)
722+
['skills/objectstack-ui/SKILL.md', 3814],
708723
// 8333 -> 6193: RE-LOCK at the landed count after the #19715 split (decision
709724
// batch #213 item 1, maintainer 「1 2 4 同意」): the worked R1, the report
710725
// template and the v17-canonical shapes block moved VERBATIM into
@@ -982,12 +997,34 @@ export const CEILINGS = new Map([
982997
// the paragraph restating the allowed-set table above it, and the related-record search
983998
// prescription that objectstack-data → Search Fields carries whole — so the file nets -4
984999
// and the ceiling follows it down to the new measurement.
985-
['skills/objectstack-ui/rules/list-views.md', 3129],
1000+
// 3129 -> 3451 (#31155, the objectstack-ui port of the cloud#3659 ruling-A split): one new
1001+
// section beside the kanban config — "add a kanban / calendar / gallery / gantt view" is a
1002+
// NEW `listViews` entry whose key encodes its kind, the default `list` untouched, a kind
1003+
// changed in place only on an explicit ask; and the board grouped by a field added in the
1004+
// same change opens with every existing record in the trailing `Uncategorized` lane, with
1005+
// the three-part hand-over (and the SQL-datasource literal-`defaultValue` exception the
1006+
// driver's ADD COLUMN DEFAULT makes true). PAID DOWN FIRST by genuine deletion in this
1007+
// file: the kanban example's `// required on every list view` comment (the sentence above
1008+
// it), the `userFilters` bullet's "Need named presets? Add a listViews entry" restating the
1009+
// bullet before it, "the saved-views switcher already covers presets" (same bullet), the
1010+
// search example's `searchableFields: []` comment restating the table row above it, the
1011+
// narrowing sentence's "refuses the request" clause restating the ⛔ section below it, the
1012+
// two Data Source code comments restating the blockquote under them, the `summary`
1013+
// comment's "footer aggregation" restating the Column Features row. `main` base 12503
1014+
// bytes -> 13802 -> 3451 tokens, pinned AT the measurement, zero headroom. Authority as the
1015+
// SKILL.md row above: cloud#3659 letter A (6682024726) via record 6513306068, quoted
1016+
// verbatim in the PR body.
1017+
// ceil($(wc -c < skills/objectstack-ui/rules/list-views.md) / 4) == ceil(13802 / 4)
1018+
['skills/objectstack-ui/rules/list-views.md', 3451],
9861019
// 2273 -> 2196 (#27059): LOWERED. The Surface bullet points at `rules/record-page.md`,
9871020
// says create / edit is a modal or the `editMode: 'page'` route, and the Overlay-width
9881021
// bullet moved there whole (the `size` bucket, the deprecated pixel keys); the ceiling
9891022
// follows the file down to the measurement (8784 bytes).
990-
['skills/objectstack-ui/rules/navigation.md', 2196],
1023+
// 2196 -> 2184 (#31155): LOWERED. The two Chinese fragments in the interface-page signal
1024+
// list are English (the shipped skill text is English-only), and the bullet's repeated
1025+
// objectui ADR-0055 citation (the section heading carries it) is gone; the ceiling follows
1026+
// the file down to the measurement (8734 bytes).
1027+
['skills/objectstack-ui/rules/navigation.md', 2184],
9911028
// 5501 -> 5692 (#19715, card item 4): a 5-line table of contents at the top,
9921029
// anchors to the file's existing headings, nothing else changed. +191 tokens,
9931030
// zero headroom to absorb any of it, ceiling +191. Maintainer ruling
@@ -1012,7 +1049,11 @@ export const CEILINGS = new Map([
10121049
// landed at +39 (1876 -> 1915), so the package nets +592 tokens, every one of them
10131050
// teaching the surface `main` ships today and none of it restating a sibling file.
10141051
// ceil($(wc -c < skills/objectstack-ui/rules/record-page.md) / 4) == ceil(3981 / 4)
1015-
['skills/objectstack-ui/rules/record-page.md', 996],
1052+
// 996 -> 991 (#31155): LOWERED. The record page's second tab is named as the console
1053+
// labels it (Activity) in place of the Chinese word, and step 5's "(one page at two
1054+
// widths)" aside — step 1 already says layout follows the surface's width — is gone; the
1055+
// ceiling follows the file down to the measurement (3961 bytes).
1056+
['skills/objectstack-ui/rules/record-page.md', 991],
10161057

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

‎skills/objectstack-ui/SKILL.md‎

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@ metadata:
2525

2626
# UI Design — ObjectStack UI Protocol
2727

28-
Expert instructions for designing user interfaces using the ObjectStack
29-
specification. This skill covers Views (list, form, kanban, calendar, …),
30-
App navigation, Dashboards, Reports, and Actions.
31-
3228
---
3329

3430
## View Types
@@ -47,6 +43,14 @@ App navigation, Dashboards, Reports, and Actions.
4743
| `chart` | Aggregate visualisation over the object (mini chart view) |
4844
| `tree` | Self-referencing hierarchy (tree-grid) |
4945

46+
**Keep the kind the request names.** A photo wall, card wall or catalogue of
47+
covers is `gallery` (an image, avatar or file field as `gallery.coverField`),
48+
a schedule or timeline over a start/end date pair is `gantt`, a board is
49+
`kanban`; never downgrade a named kind to `grid`: the file parses and the user
50+
opens a table where they asked for a board. Adding one beside the list, and
51+
the empty board over a field added in the same change:
52+
[List Views](./rules/list-views.md#adding-a-board-calendar-gallery-or-gantt-beside-the-list).
53+
5054
### Form Views
5155

5256
| Type | When to Use |
@@ -114,8 +118,7 @@ ObjectUI forms consume them. Their semantics and server behaviour:
114118

115119
UI-side: inline master-detail grids evaluate these rules **row-by-row** against
116120
each child row. Use `requiredWhen` — the `conditionalRequired` alias was REMOVED
117-
in protocol 17 and is now a parse error. Load **objectstack-formula** when
118-
authoring non-trivial CEL.
121+
in protocol 17 and is now a parse error.
119122

120123
---
121124

@@ -183,15 +186,13 @@ Use this CRM-style structure as the canonical UI assembly reference:
183186
|:--|:--|:--|
184187
| Multi-view object UI | `src/views/*.view.ts` | Define default `list` + `form`, then named `listViews` / `formViews` for scenarios |
185188
| **Public form** | `src/views/*.view.ts` (formView with `sharing: { enabled: true, allowAnonymous: true, publicLink: 'slug' }`) | Web-to-Lead at `GET/POST /api/v1/forms/:slug` |
186-
| App navigation | `src/apps/*.app.ts` | Use grouped nav trees, `viewName` shortcuts, and `requiresObject` for capability-aware visibility |
189+
| App navigation | `src/apps/*.app.ts` | Grouped nav trees, `viewName` shortcuts, `requiresObject` |
187190
| **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 |
188191
| Dashboards | `src/dashboards/*.dashboard.ts` | Combine KPI + chart + table widgets with shared `dateRange` and `globalFilters` |
189192
| Reports | `src/reports/*.report.ts` | Select `rows` (dimensions) + `values` (measures) from that dataset; tabular/summary/matrix/joined |
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 |
193+
| Record pages | — (synthesized) | The object's declarations produce the page ([Record Page](./rules/record-page.md)) |
191194
| User actions | `src/actions/*.actions.ts` | Use `flow` for orchestration and `script` for parameterized bulk mutations |
192195

193-
This blueprint is the default for “build a complete metadata app UI” tasks.
194-
195196
---
196197

197198
## Date Macros — Filter Placeholders
@@ -233,19 +234,31 @@ Register them under `defineStack({ actions: [...] })`.
233234
| `api` | Call a registered API endpoint | `target` |
234235
| `form` | Open a FormView by name (routed to `/_console/forms/:name`) | `target` |
235236

237+
**An operation is an `action`, never a view.** "Mark as done", "approve",
238+
"submit", "set status to X" — anything that DOES something to a record — is
239+
`defineAction` metadata; a view only displays. A record button carries
240+
`objectName` (what folds it into that object's record surfaces) and
241+
`locations`; with neither `locations` nor a view naming it in `rowActions` /
242+
`bulkActions` it renders nowhere (`action-no-placement`). **A write button must
243+
write:** `ctx.record` is a read-only snapshot and the body's return value is
244+
applied to nothing, so a body that only returns the new values or assigns to
245+
`ctx.record` changes nothing and still reports success
246+
(`action-record-write-discarded`). Write with `type: 'script'` and
247+
`body: { language: 'js', source: "await ctx.api.object('task').update({ id: ctx.recordId, status: 'done' });", capabilities: ['api.write'] }`:
248+
`js`, because an `expression` body is a pure formula with no I/O; `api.write`,
249+
because the sandbox throws on `ctx.api…update` / `insert` / `delete` without
250+
it; the option `value`, never its label. A multi-step operation is
251+
`type: 'flow'` with `target` naming a flow you also author.
252+
236253
---
237254

238255
## Common Pitfalls
239256

240-
1. **Putting too many columns in a grid view.**
241-
Users rarely need more than 6–8 columns visible by default. Use `hidden`
242-
for secondary columns.
243-
244-
2. **Forgetting `link: true` on the primary column.**
245-
The first meaningful column (usually the name/subject) should be the
246-
navigation link to the record detail.
257+
1. **Too many default columns, or no `link: true` on the primary one.** 6–8
258+
visible columns is plenty (`hidden` the rest); the name/subject column is
259+
the record link ([Columns](./rules/list-views.md#column-features)).
247260

248-
3. **Putting widget grid placement in `position`.**
261+
2. **Putting widget grid placement in `position`.**
249262
The grid-placement field is `layout: { x, y, w, h }` — `position` is not a
250263
widget key and the closed schema REJECTS it by name (it was silently
251264
dropped before protocol 17). `layout` is optional: omit it and the widget

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ view / page / dashboard / report metadata.
1919
base object's `allowExport` permission).
2020
- `views-apps-actions-pages.json` — `defineView` containers, `App.create`
2121
navigation, `defineAction` surfaces, record / interface pages, package
22-
docs, wizard forms.
22+
docs, wizard forms, a view added beside the list (and the empty board over
23+
a field added with it), a write button whose body writes, the view kind
24+
the request names.
2325

2426
## Format
2527

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,36 @@
120120
"must_contain": ["listViews", "or: [", "{current_user_id}"],
121121
"must_not_contain": ["$or:", "logic:", "conditions:", "anyOf:"]
122122
}
123+
},
124+
{
125+
"id": 13,
126+
"prompt": "`task` already ships `defineView({ list, form })`. Reps want a board: add a `priority` select (A / B / C) to the object and a kanban grouped by it — the list must stay — and tell them what they will see when they first open the board.",
127+
"expected_output": "A new `listViews.priority_kanban` entry (`type: 'kanban'`, the required top-level `columns`, `kanban: { groupByField: 'priority' }`) beside the untouched default `list`, never the list retyped into a board. The hand-over discloses the empty board: every existing task has no `priority` yet, so all of them sit in the trailing `Uncategorized` lane (scroll right on a narrow screen) under one empty lane per option; dragging a card onto a real lane writes the field, or they edit from the list; a runtime-token or CEL `defaultValue` on the new field fills no existing row, and a literal one does only on a SQL datasource, where it becomes the column DEFAULT.",
128+
"files": [],
129+
"assertions": {
130+
"must_contain": ["listViews", "groupByField: 'priority'", "Uncategorized"],
131+
"must_not_contain": ["list: { type: 'kanban'", "ready to use"]
132+
}
133+
},
134+
{
135+
"id": 14,
136+
"prompt": "Add a 'Mark as done' button on `task` records that sets `status` to Done (the option is labelled Done, its value is `done`).",
137+
"expected_output": "A `defineAction`, not a view: `name`, `label: 'Mark as done'`, `objectName: 'task'`, `locations: ['record_header']`, `type: 'script'`, `body: { language: 'js', source: \"await ctx.api.object('task').update({ id: ctx.recordId, status: 'done' });\", capabilities: ['api.write'] }`, `refreshAfter: true`. The body CALLS the update with the option value: a body that returns `{ status: 'done' }` or assigns to `ctx.record` writes nothing, an `expression` body cannot write, and without `api.write` the sandbox refuses the call.",
138+
"files": [],
139+
"assertions": {
140+
"must_contain": ["defineAction", "objectName: 'task'", "ctx.api.object('task').update", "ctx.recordId", "api.write", "status: 'done'"],
141+
"must_not_contain": ["defineView(", "language: 'expression'", "status: 'Done'"]
142+
}
143+
},
144+
{
145+
"id": 15,
146+
"prompt": "Products need a photo wall: every product as a card with its `main_image` as the cover. Write the `listViews` entry.",
147+
"expected_output": "`catalog_gallery: { label: 'Catalogue', type: 'gallery', data, columns: ['name', 'price'], gallery: { coverField: 'main_image' } }` — the kind the request names, added beside the existing views; not a table with an image column, and not the default `list` retyped.",
148+
"files": [],
149+
"assertions": {
150+
"must_contain": ["type: 'gallery'", "coverField: 'main_image'"],
151+
"must_not_contain": ["type: 'grid'"]
152+
}
123153
}
124154
]
125155
}

0 commit comments

Comments
 (0)