Skip to content

Commit 857cd3c

Browse files
objectstack-fleet[bot]os-steveclaude
authored
docs(skills): objectstack-i18n teaches the books translation group, with an eval for a two-language doc book (#27024)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi96SYQWBK8w8rA6L8gYD1gU7)_ Fixes #26820 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:`objectstack-i18n` 技能的顶层分组清单补上 `books`(文档手册的书名、简介和各章节标题按读者语言显示),给出键的拼法;评测文件新增第 9 题(双语手册),并删去 5 句与技能正文重复的评测句子来付 token 账,余额按实测把该评测文件的上限从 1642 提到 1790。 - 为什么改:spec 已落地 `books` 翻译分组,但技能不教它,照技能写 zh-CN 包的 agent 会让文档侧栏停在英文,或把键错放到 `pages` 下。 - 风险与回滚:只改技能文字、评测 JSON、棘轮一行和一条 changeset,无代码和 schema 变更;回滚即还原本 PR。 - 先看:`skills/objectstack-i18n/SKILL.md`(第 169–181 行那一段)、`skills/objectstack-i18n/evals/bundles-locales-coverage.json`(第 9 题与 5 处删句)、`scripts/check-skills-token-ratchet.mjs`(该评测行的算式注释)。 - 两分钟验证:仓根执行 `node scripts/check-skills-token-ratchet.mjs`,预期打印 82 个文件均在上限内、退出 0。 - 需要你做的:批准本 PR(`skills/**` 为 Tier H,席位不会 ready、入队或开自动合并)。 ## What changes - `skills/objectstack-i18n/SKILL.md`: the "Top-level groups alongside `objects`" paragraph now names `books`, the group the spec added so a doc book's title and section titles follow the reader's locale. The entry spells the three keys the spec declares, read off `packages/spec/src/system/translation.zod.ts` (the `books` record) and `system/book.zod.ts` on `origin/main`: `books.{book}.label` / `.description` by `Book.name`, and `books.{book}.groups.{group_key}.label` by each group's stable `key`, never its position, with one spelled example (`books.crm_manual.groups.getting_started.label`). It says the pages themselves localize through `doc.translations` and that the inline `translations` map a book once carried stays retired (the `BOOK_TRANSLATIONS_RETIRED` prescription). The existing sentence "`os i18n extract --no-objects-only` scaffolds every group above but `messages` and `settingsCommon`" now covers `books` too, which is what `walkBooks` in `packages/cli/src/utils/i18n-extract.ts` does. - `skills/objectstack-i18n/evals/bundles-locales-coverage.json`: case 9, a two-language `crm_manual` book whose sidebar is still English. The ideal answer spells the book label and the `getting_started` group label by key and says `os i18n extract --no-objects-only` seeds them; `must_contain` pins `books`, `groups`, `getting_started`, `doc.translations`; `must_not_contain` pins `pages.crm_manual` (a key invented under `pages`) and `sections.getting_started` (the `sections` alias the spec points at `groups`). - The eval file sat at its token ceiling (1642, headroom 0). Paid under the standing ruling recorded beside that row, quoted verbatim and untranslated: 「A:先削重复,余额按实测提高」. - **Cut first (227 bytes, 57 units, 6567 → 6340 bytes, 1642 → 1585):** five sentences that restated a rule the reader already reads elsewhere, each with its surviving home: 1. case 1, "there is no ICU plural/select" (31 B) → `SKILL.md` "No ICU MessageFormat" section; the case's own `{count, plural` pin stays. 2. case 3, "the retired `o.*` dialect is rejected on this door too" (56 B) → `SKILL.md` "The Retired `o.*` Shape" section; the case's own `o: {` pin stays. 3. case 5, "`extract --check` is the separate freshness gate" (50 B) → `SKILL.md` "`os i18n extract --check` — freshness, not coverage" section. 4. case 6, "inherited by every field referencing the picklist" (51 B) → `SKILL.md`'s `picklists` entry in the top-level group list ("every field referencing the picklist inherits them"). 5. case 8, "Neither is a `pages.*.components` key." (39 B) → the case's own `components.open_block` pin. - **Then the remainder:** case 9 adds 817 bytes = 205 units (6340 → 7157 bytes, 1585 → 1790). The row is pinned AT the measurement, `ceil(7157 / 4) = 1790`, zero headroom; the row comment in `scripts/check-skills-token-ratchet.mjs` carries the same arithmetic. - `SKILL.md` had headroom: 5485 → 5571 tokens against its ceiling of 6333 (stamp line excluded), so its row is unchanged. - `.changeset/26820-i18n-skill-books-group.md`: `@objectstack/skills` patch (the package's dist is built from `skills/**`). No forced test edits: nothing but the eval JSON and the ratchet row changed outside `SKILL.md`. ## Tests - Per-case cross-check over every case in the eval file, 9 cases: every `must_contain` string present in its own `expected_output`, no `must_not_contain` string inside it — 0 violations. `JSON.parse` of the file succeeds. - `node scripts/check-skills-token-ratchet.mjs`: `✓ check-skills-token-ratchet: 82 authored bundle file(s) within their ceilings; 10 generator-owned file(s) measured, not ratcheted.` (exit 0); `--self-test`: `✓ check-skills-token-ratchet self-test: 79 cases pass.` (exit 0). - No package test or typecheck is owed: the diff touches no package source (`skills/**`, one ratchet row, one changeset), and no behaviour a test pins changed, so no ablation. ## Acceptance notes - Skills readings (lines, then tokens as `ceil(utf8 bytes / 4)`, the sibling gate's convention): `SKILL.md` 521 → 524 lines, 5485 → 5571 tokens (ceiling 6333); `evals/bundles-locales-coverage.json` 85 → 95 lines, 1642 → 1790 tokens (ceiling 1642 → 1790); whole package, all 11 `SKILL.md` files: 4610 → 4613 lines, 56462 → 56549 tokens; whole `skills/` tree 19271 → 19284 lines. - Not carried (noted, not filed): no shipped example bundle (`app-crm`, `app-showcase`) declares a `books` group yet, so the `SKILL.md` example is a spelled key rather than a line quoted from a shipped bundle, as the `inlineTitle` and report-block lines are. - `check:skill-docs` and `check:skill-refs` are `@objectstack/spec` scripts (`pnpm --filter @objectstack/spec run check:skill-docs`), not root scripts; the derived list names the former. ## Verification All at head `cd559297`, on `origin/main` `7eb613c3` (the branch base; main had not moved when the list was derived). - Derived gate list: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` printed 63 commands for this change set (4 paths, 63 changed lines, under the 3000 human-merge threshold). Every one ran locally (the 8 dist-reading ones after `turbo run build --filter='./packages/**'` under `os-verify-lock`, VERDICT command-exit 0, 7m26s), each exit code captured before any pipe: **63 of 63 exit 0**. - Reconciliation: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE` → `✓ dispatch-gates --ran: 63 derived famil(ies) accounted for — 63 run, 0 NOT-MEASURED (a DERIVED zero — all 63 recorded an exit code and none of them is 3).` - Verdict lines worth reading: `✓ check-skills-token-ratchet: 82 authored bundle file(s) within their ceilings`; `✓ check-skills-token-ratchet self-test: 79 cases pass.`; `✓ check-skill-id-lint: 104 file(s) clean`; `check-nul-bytes: OK (scanned 21762 text file(s) …)`; `✓ dispatch-gates self-test (fast tier): 1935 cases pass; 7 slow section(s) deferred.` - `dispatch-gates --tier`: both `skills/**` paths hit clause ① (MANDATORY CONTRACT_REVIEW_TIER, no one-line exit), as the claim records. - CI: the PR's own check-runs are the reading for every family CI runs; nothing was re-run locally to stand in for them. <details><summary>The 63 commands and their exit codes (the `--ran` ledger)</summary> ```text node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-declaration-mirrors.mjs :: exit 0 node scripts/check-declaration-mirrors.mjs --self-test :: exit 0 node scripts/check-doc-route-spelling.mjs --advisory :: exit 0 node scripts/check-doc-route-spelling.mjs --self-test :: exit 0 node scripts/check-dts-emitted.mjs --self-test :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-self-test-wired.mjs :: exit 0 node scripts/check-self-test-wired.mjs --self-test :: exit 0 node scripts/check-self-test-workflow-commands.mjs :: exit 0 node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0 node scripts/check-skills-token-ratchet.mjs :: exit 0 node scripts/check-skills-token-ratchet.mjs --self-test :: exit 0 node scripts/check-whole-set-label-write.mjs :: exit 0 node scripts/check-whole-set-label-write.mjs --self-test :: exit 0 node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0 node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.mjs --self-test :: exit 0 pnpm check:agent-test-spelling :: exit 0 pnpm check:bash32-floor :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:cli-command-ids :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:corpus-claim-drift :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:merge-driver :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:parse-guard :: exit 0 pnpm check:pm-changeset-deadline-census :: exit 0 pnpm check:pm-dispatch-gates :: exit 0 pnpm check:pm-governed-merges :: exit 0 pnpm check:pm-skill-id-lint :: exit 0 pnpm check:pnpm-filter-targets :: exit 0 pnpm check:pnpm-flag-spellings :: exit 0 pnpm check:ratchet-remedy-authority :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:role-word :: exit 0 pnpm check:scripts-symbol-anchors :: exit 0 pnpm check:skill-compatibility :: exit 0 pnpm check:skill-frame-sync :: exit 0 pnpm check:skill-identifier-liveness :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0 pnpm --filter @objectstack/spec run check:skill-docs :: exit 0 pnpm check:dts-closure :: exit 0 pnpm check:dual-build-cjs-loads :: exit 0 pnpm check:entry-guard :: exit 0 pnpm check:lean-entry-closure :: exit 0 pnpm check:published-files :: exit 0 pnpm check:sourcemap-no-sources-content :: exit 0 ``` </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01ADHkmPLgCH4kXgfdFGF4Z2 --- _Generated by [Claude Code](https://claude.ai/code/session_01ADHkmPLgCH4kXgfdFGF4Z2)_ Co-authored-by: os-steve <steve@objectstack.ai> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8be3b43 commit 857cd3c

4 files changed

Lines changed: 53 additions & 10 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-i18n teaches the `books` translation group, with an eval for a two-language doc book
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** The top-level group list in `skills/objectstack-i18n/SKILL.md` named fourteen of the bundle's fifteen groups and left out `books`, the group the spec added so a doc book's title and section titles follow the reader's locale. An agent writing a zh-CN bundle from the skill left the docs sidebar in one language or invented a key under `pages`.
11+
- **Now.** The list names `books` with its three keys — `books.{book}.label` / `.description` by `Book.name` and `books.{book}.groups.{group_key}.label` by each group's stable `key` — says the pages themselves localize through `doc.translations` and that a book's inline `translations` map stays retired; `os i18n extract --no-objects-only` seeds the group like every other. Eval 9 in `evals/bundles-locales-coverage.json` checks that an answer for a two-language manual spells the book label and a group label by key and never a `pages.*` key. Five eval sentences that restated a rule the skill already teaches were cut to pay for it.
12+
- No schema, key or behaviour changes.

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -989,7 +989,25 @@ export const CEILINGS = new Map([
989989
// pin duplicated case 5's (77 bytes cut, 20 tokens); the remainder is the new
990990
// case; pinned AT the measurement, zero headroom. The raising PR's body carries
991991
// the same arithmetic.
992-
['skills/objectstack-i18n/evals/bundles-locales-coverage.json', 1642],
992+
// 1642 -> 1790 (2026-10-28, the owed Skills-docs follow-up of the spec PR that
993+
// added the `books` translation group): one eval prompt for a doc book's
994+
// spine copy — `books.<book>.label` / `.description` and
995+
// `books.<book>.groups.<key>.label`. Same ruling as the rows above, tap label
996+
// verbatim and untranslated: 「A:先削重复,余额按实测提高」. PAID DOWN FIRST: five
997+
// sentences restated a rule whose home the reader already reads — case 1's
998+
// "no ICU plural/select" (SKILL.md "No ICU MessageFormat" + its own
999+
// `{count, plural` pin), case 3's "retired o.* dialect" (SKILL.md "The
1000+
// Retired o.* Shape" + its own `o: {` pin), case 5's "extract --check is the
1001+
// freshness gate" (SKILL.md "os i18n extract --check — freshness, not
1002+
// coverage"), case 6's "inherited by every field referencing the picklist"
1003+
// (SKILL.md's `picklists` entry in the top-level group list), case 8's
1004+
// "neither is a pages.*.components key" (its own `components.open_block`
1005+
// pin): 227 bytes = 57 units (6567 -> 6340 bytes, 1642 -> 1585); the new case
1006+
// adds 817 bytes = 205 units (6340 -> 7157 bytes, 1585 -> 1790):
1007+
// ceil($(wc -c < skills/objectstack-i18n/evals/bundles-locales-coverage.json) / 4) == ceil(7157 / 4)
1008+
// pinned AT the measurement, zero headroom. The raising PR's body carries the
1009+
// same arithmetic.
1010+
['skills/objectstack-i18n/evals/bundles-locales-coverage.json', 1790],
9931011
// 1211 -> 1458 (card #26302): one eval for a demo whose seed names people —
9941012
// expected: both in `demoStaff` with a declared `position` /
9951013
// `permissionSets`, created by `os dev`; NO `role:` alias and NO platform

‎skills/objectstack-i18n/SKILL.md‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,13 @@ Top-level groups alongside `objects`: `apps` (label, description, navigation),
171171
an **error** whose message names the `objects.{owner}._actions.{action_name}` key to write
172172
instead), `picklists` (`label`, `options` by stored value — every field referencing the
173173
picklist inherits them), `dashboards`, `datasets`, `reports`, `pages`, `flows`, `positions`,
174-
`permissions` (permission sets, spelled as the stack's `permissions[]`), `metadataForms`,
175-
`settingsCommon`; `reports`, `positions` and `permissions` hold `label` + `description` by name,
176-
and a joined report's block headings sit at `reports.{report}.blocks.{block_name}.{label,description}`,
177-
keyed by the block's required `name`.
174+
`permissions` (permission sets, spelled as the stack's `permissions[]`), `books` (a doc book's
175+
spine, by `Book.name`: `books.{book}.label` / `.description`, and `groups.{group_key}.label` by
176+
each group's stable `key`, never its position — `books.crm_manual.groups.getting_started.label`;
177+
the pages themselves localize through `doc.translations`, and the inline `translations` map a
178+
book once carried stays retired), `metadataForms`, `settingsCommon`; `reports`, `positions` and
179+
`permissions` hold `label` + `description` by name, and a joined report's block headings sit at
180+
`reports.{report}.blocks.{block_name}.{label,description}`, keyed by the block's required `name`.
178181
Two keys sit deeper than their group's `label`: a `page:tabs` item at
179182
`pages.{page}.components.{tabs_id}.items.{value}.label` (by the item's `value`; an item without
180183
one has no key) and an approval step at `flows.{flow}.approvals.{node_id}.label`.

‎skills/objectstack-i18n/evals/bundles-locales-coverage.json‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
{
55
"id": 1,
66
"prompt": "Add Simplified Chinese for the `support_case` object: its label and plural, the `status` field with options New / Open / Closed, the `close` action, and a welcome message that greets the user by name.",
7-
"expected_output": "A `TranslationData` per locale under `objects.support_case` — `label`, `pluralLabel`, `fields.status.label`, `fields.status.options` keyed by the STORED option value (`new`, `open`, `closed`, never the display label), `_actions.close.label` — assembled with `defineTranslationBundle({ en, 'zh-CN': zhCN })` and registered under `defineStack({ translations })`. The message uses double-brace interpolation, `'welcome.user': '欢迎,{{userName}}!'`; there is no ICU plural/select.",
7+
"expected_output": "A `TranslationData` per locale under `objects.support_case` — `label`, `pluralLabel`, `fields.status.label`, `fields.status.options` keyed by the STORED option value (`new`, `open`, `closed`, never the display label), `_actions.close.label` — assembled with `defineTranslationBundle({ en, 'zh-CN': zhCN })` and registered under `defineStack({ translations })`. The message uses double-brace interpolation, `'welcome.user': '欢迎,{{userName}}!'`.",
88
"files": [],
99
"assertions": {
1010
"must_contain": ["defineTranslationBundle", "objects", "pluralLabel", "options", "_actions", "{{"],
@@ -24,7 +24,7 @@
2424
{
2525
"id": 3,
2626
"prompt": "A customer admin wants to relabel `account` as 客户 in their Studio without waiting for a redeploy.",
27-
"expected_output": "A `translation` metadata item (author with `defineTranslation`, shape `TranslationItemSchema`) carrying `locale: 'zh-CN'` and the same `objects.account.label` group as a file bundle; published items layer over the shipped bundles at boot and on every publish, and deleting the item restores the shipped value. One locale per item; the retired `o.*` dialect is rejected on this door too.",
27+
"expected_output": "A `translation` metadata item (author with `defineTranslation`, shape `TranslationItemSchema`) carrying `locale: 'zh-CN'` and the same `objects.account.label` group as a file bundle; published items layer over the shipped bundles at boot and on every publish, and deleting the item restores the shipped value. One locale per item.",
2828
"files": [],
2929
"assertions": {
3030
"must_contain": ["translation", "locale", "objects", "defineTranslation"],
@@ -44,7 +44,7 @@
4444
{
4545
"id": 5,
4646
"prompt": "`os i18n check` reports zh-CN is missing app, navigation and dashboard keys even though we just ran `os i18n extract`. What is wrong?",
47-
"expected_output": "Diagnoses that `--objects-only` is on by default, so extract wrote only the `objects` / `globalActions` subtree while check also demands `apps`, `navigation`, `dashboard`, `widget`, `page` and `flow` keys. Re-runs `os i18n extract --no-objects-only` (or hand-authors those groups), re-checks with `os i18n check --locales=zh-CN`, and adds `--strict` / `--threshold` in CI; `extract --check` is the separate freshness gate.",
47+
"expected_output": "Diagnoses that `--objects-only` is on by default, so extract wrote only the `objects` / `globalActions` subtree while check also demands `apps`, `navigation`, `dashboard`, `widget`, `page` and `flow` keys. Re-runs `os i18n extract --no-objects-only` (or hand-authors those groups), re-checks with `os i18n check --locales=zh-CN`, and adds `--strict` / `--threshold` in CI.",
4848
"files": [],
4949
"assertions": {
5050
"must_contain": ["--no-objects-only", "os i18n check", "apps", "--strict"],
@@ -54,7 +54,7 @@
5454
{
5555
"id": 6,
5656
"prompt": "Add zh-CN for the `pipeline_by_stage` report, the `sales_manager` permission set, the `regional_lead` position and the `deal_stage` picklist's option labels.",
57-
"expected_output": "Top-level groups keyed by name: `reports.pipeline_by_stage` and `positions.regional_lead` (`label`, `description`), `permissions.sales_manager` (the same, spelled as the stack's `permissions[]`, not the camelCase name), `picklists.deal_stage.options` by stored value, inherited by every field referencing the picklist.",
57+
"expected_output": "Top-level groups keyed by name: `reports.pipeline_by_stage` and `positions.regional_lead` (`label`, `description`), `permissions.sales_manager` (the same, spelled as the stack's `permissions[]`, not the camelCase name), `picklists.deal_stage.options` by stored value.",
5858
"files": [],
5959
"assertions": {
6060
"must_contain": ["reports", "permissions", "positions", "picklists"],
@@ -74,12 +74,22 @@
7474
{
7575
"id": 8,
7676
"prompt": "In zh-CN the `Products` grid heading on the `crm_opportunity` form and the `Open Tasks` block heading of the joined `showcase_task_overview` report are still English.",
77-
"expected_output": "`objects.crm_opportunity_line_item.fields.opportunity.inlineTitle` — the field lives on the child object, so the key is the child's, and an untranslated `inlineTitle` keeps its authored text (no fallback to the child's plural label); `reports.showcase_task_overview.blocks.open_block.label`, keyed by the block's required `name`. Neither is a `pages.*.components` key.",
77+
"expected_output": "`objects.crm_opportunity_line_item.fields.opportunity.inlineTitle` — the field lives on the child object, so the key is the child's, and an untranslated `inlineTitle` keeps its authored text (no fallback to the child's plural label); `reports.showcase_task_overview.blocks.open_block.label`, keyed by the block's required `name`.",
7878
"files": [],
7979
"assertions": {
8080
"must_contain": ["inlineTitle", "crm_opportunity_line_item", "blocks", "open_block"],
8181
"must_not_contain": ["fields.opportunity_line_item", "components.open_block"]
8282
}
83+
},
84+
{
85+
"id": 9,
86+
"prompt": "Our `crm_manual` doc book ships in English and zh-CN, but the docs sidebar still shows the book title and its `getting_started` section title in English.",
87+
"expected_output": "The `books` group, keyed by `Book.name`: `books.crm_manual.label` / `.description` and `books.crm_manual.groups.getting_started.label`, by the group's stable `key` (never its position); `os i18n extract --no-objects-only` seeds all three. The pages' own titles and bodies localize through `doc.translations`, and the inline `translations` map a book once carried stays retired.",
88+
"files": [],
89+
"assertions": {
90+
"must_contain": ["books", "groups", "getting_started", "doc.translations"],
91+
"must_not_contain": ["pages.crm_manual", "sections.getting_started"]
92+
}
8393
}
8494
]
8595
}

0 commit comments

Comments
 (0)