Skip to content

feat(spec,rest,cli): a doc book's title and section titles are translatable — books.<name> in the translation bundle - #26819

Merged
objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/project-thread-g5q3wk
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/project-thread-g5q3wk

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #26690

Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: #26820. The docs are updated here.

维护者速读

  • 改了什么: 翻译包新增 books 分组,书名、书的描述和每个分组标题(按分组 key)都能翻译;GET /meta/book(列表和单项)和 GET /meta/book/:name/tree 按请求语言返回,控制台 /apps/<包>/docs 侧栏随之显示译文。os i18n extract 会生成这些键,os i18n check 报缺失为 i18n/missing-book。
  • 为什么改: 文档正文能按语言出,但书名和侧栏分组名只能写一种语言,中文用户看到半中半英的手册(HotCRM 使用手册)。
  • 风险与回滚: 只加不删:英文读者拿到的是原样字符串(同一引用);书的 translations 内联写法仍被拒绝,只是提示改为指向新键。回滚即还原本 PR。
  • 先看: packages/spec/src/system/i18n-resolver.ts(translateBook)、packages/rest/src/meta-item-read-gate.ts(树接口翻译这一步)、packages/spec/src/system/translation.zod.ts(books 分组形状)。
  • 两分钟验证: pnpm --filter @objectstack/runtime exec vitest run src/domains/meta-list-projection-parity.test.ts -t 26690,预期 zh-CN 拿到「帮助中心 / 开始」,en-US 拿到「Help Centre / Start」,两个传输层一致。
  • 需要你做的: 无,席位落地。

What changes

One translation path for a doc book's spine — the translation bundle, the same path an app's label and navigation labels take. No inline locale map comes back on the book (the retired 17.0.0 map stays a tombstone; sibling groups such as apps.<name>.navigation.<id>.label and pages.<name>.label use the bundle, not inline maps).

  • @objectstack/spec — system/translation.zod.ts: a books group on the shared per-app shape, so the bundle entry (TranslationData), the registered translation item and the platform bundle all accept it. Keyed by the book name: label, description, and groups.GROUP_KEY.label (by the group's stable key, as a navigation node is keyed by its id). Strict at every level; name / title point at label, sections / chapters / toc point at groups, the singular book points at books. The group counts in the settings refusal prose and the shape docblocks move fourteen → fifteen (and fifteen → sixteen for the platform face).
  • @objectstack/spec — system/i18n-resolver.ts: translateBook, registered in METADATA_DOCUMENT_TRANSLATORS. TRANSLATABLE_METADATA_TYPES is derived from that table and @objectstack/rest reads it, so GET /meta/book (list and item, both transports) is served in the request locale with no further wiring. A key the bundle does not carry keeps the authored string; a document with nothing to translate comes back as the same reference.
  • @objectstack/spec — system/book.zod.ts: the retired inline-translations prescription now names the bundle keys to move the strings to (it used to say the tree and portal render verbatim, which this PR makes untrue). Book label and group label describes name their bundle keys.
  • @objectstack/rest — createMetaBookTreeAnswer: step 5 translates the resolved book through translateMetaDocument (the same table) before rendering the spine, so the tree and the list the sidebar is built from agree. Translated after the §6.7 audience gate; a label is not an access input. MetaBookTreeSources gains resolveI18nService(); RestServer's route supplies it.
  • @objectstack/runtime — dispatcher book-tree route: spreads metaTranslationSources, so it answers the localized tree RestServer answers.
  • @objectstack/cli: walkBooks in os i18n extract emits books.NAME.label / .description (pushOptional) and books.NAME.groups.KEY.label (pushEntry, the label is required); coverage reports a gap as i18n/missing-book.
  • Liveness: translation/books seeded live with its reader, drilled to label, description, groups and groups.label.
  • Console sidebar: no code change. The docs portal builds its sidebar from GET /meta/book (use-book-data.ts), the client sends Accept-Language on every request, and the console already remounts the metadata subtree on a language switch. A console test pins the chain.
  • examples/app-showcase: zh-CN strings for showcase_manual (title, description, three section titles), so the new walk does not grow check:i18n-coverage.
  • Docs: ui/doc-pages.mdx gains a "Translating the spine" section (the inline-map callout now points at it); ui/translations.mdx and protocol/kernel/i18n-standard.mdx list the new group.
  • @objectstack/lint — translation-target-unknown: resolves books.NAME against the declared books (a sibling package's included) and the implicit per-package book (the package id, its one all group read off deriveImplicitPackageBook), and books.NAME.groups.KEY against that book's group keys. A mistyped book name or group key is an error with the nearest declared name. The rule's header and its explainRule paragraph cover the new arm.
  • @object-ui/i18n — SPEC_TRANSLATION_GROUPS: gains books, so the console's language loader walks the served group (the existing loadLanguage pin requires it). The Studio translation-preview coverage pin moves to 2/15 (13%) with the books category key.

Why

A package that ships its docs in two languages showed a half-translated manual. Each doc's body already followed the reader's locale (doc.translations, NAME.LOCALE.md), but the book title and every sidebar section title stayed in the authoring language: the book's inline translations map was retired in 17.0.0 because nothing read it, and the translation bundle had no key for a book. This gives the spine a translation slot on the one path navigation items and pages already use.

Tests

  • packages/spec/src/system/i18n-book.test.ts (new, 11): the three doors accept books; the face is closed; the aliases; the tombstone names the new keys; a zh-CN reader gets title, description and every section title; the translated book renders a translated spine through resolveBookTree; an en-US reader gets the same reference; per-key fallback; BCP-47 ladder and fallback chain; no mutation.
  • packages/cli/test/i18n-book-walk.test.ts (new, 4): the walk, seeding, a bundle written at the extracted paths parses, and the book coverage bucket.
  • packages/runtime/src/domains/meta-list-projection-parity.test.ts: the census fixture bundle gains a books entry, so every list, item and tree cell compares both transports under Accept-Language: zh-CN / ?locale=zh-CN as well. New reference pins: the zh-CN book list (and en-US unchanged), the zh-CN book item, and the tree on BOTH transports — zh-CN title and section title, en-US authored, the doc entry still localized by doc.translations.
  • ui/apps/console/src/pages/docs-portal.bookLocale-26690.test.tsx (new, 2): the real /apps/:appName/docs routes, layout, reader and sidebar over an adapter that answers GET /meta/book by running the authored book through the spec's translator with a bundle (bundle → served book → sidebar): zh-CN shows the translated title (aria-label) and section titles; en-US is unchanged.
  • packages/lint/src/validate-translation-references.test.ts (5 new): a declared book and its group keys are clean; an unknown book name; unknown group keys, a group label among them; a sibling package's book; the implicit per-package book and an orphan group under it. Removing the books loop reds three of them.

Acceptance notes

  • MetaBookTreeSources.resolveI18nService is a required member. Both in-repo transports supply it. A third-party implementer of this exported port would need to add it; making it optional would let a transport silently serve untranslated labels. Flagged for the contract review: the seat may prefer it optional.
  • The synthetic Uncategorized group stays English. resolveBookTree hardcodes its label; it is not an authored group and has no bundle key. Pre-existing, unchanged by this PR.
  • The implicit per-package book (no authored book; name = package id) also resolves books.PACKAGE_ID.label if a bundle carries it — an effect of translating whatever book the tree resolves, not a declared feature.
  • translation-target-unknown resolves books.NAME and books.NAME.groups.KEY against the declared books (a sibling package's included) and the implicit per-package book (the package id, one all group read off deriveImplicitPackageBook): a mistyped book name or group key is an error with the nearest declared name. Added in the review's patch round (claim amendment 6449487635).
  • No ! in the title and no (narrowing) arm, so no full dogfood run is owed.

Verification

Head 1f17d1834 for the suites below. The patch round at 4b8fb9a1b (the books lint arm and the two UI fixes) re-ran: @objectstack/lint test 7435 passed, typecheck exit 0; the UI loadLanguage, @object-ui/i18n and app-shell preview suites, 235 files passed; check:generated exit 0; the 31 lint, changeset, i18n and UI gate families from dispatch-gates, every exit 0 (the derived set is still 123). Every build and test ran through scripts/pm/os-verify-lock.sh on a shared box.

Build and package suites
  • pnpm build: 116/116 tasks, exit 0 (the first run found the Studio translation preview's exhaustive group table missing books; fixed in this PR).
  • @objectstack/spec test: 735 files, 22021 passed; typecheck exit 0.
  • @objectstack/rest test: 6498 passed; typecheck exit 0.
  • @objectstack/runtime test: 7448 passed; typecheck exit 0.
  • @objectstack/cli unit: 4938 passed; typecheck exit 0.
  • UI: vitest run of the console book-locale test and the Studio translation-preview test: 2 files, 23 passed. type-check of @object-ui/app-shell and @object-ui/console: exit 0.
  • eslint over the 16 touched TS/TSX files: 0 errors, 0 warnings.
Ablations (each red, then restored; git diff HEAD empty)
  • Drop book: translateBook from METADATA_DOCUMENT_TRANSLATORS: i18n-book.test.ts 2 of 11 fail; 3 runtime parity tests fail (list, item, tree).
  • Replace the tree's translate step with the untranslated book: the tree test fails on both transports.
  • Drop walkBooks(config, out): i18n-book-walk.test.ts 4 of 4 fail.
  • Console test, adapter returns the authored book instead of the translated one: the zh-CN sidebar test fails. (The console test reads the built spec, so a source-only ablation of the translator does not reach it; the chain it pins is served book → sidebar.)
Derived gates
  • dispatch-gates --commands: 123 commands; dispatch-gates --ran: 123 derived, 123 run, 0 NOT-MEASURED, 0 UNRUN; every exit 0. The three build-output gates (check:skill-examples, check:dual-build-cjs-loads, check:i18n-coverage) and check:test-source-alias, check:i18n, check:i18n-walk-parity were re-run after the full build.
  • The 12 artifact-roster gates whose roster sits in a touched directory (check-published-list-mirrors, check:meta-url-spelling, check:authz-resolver, check:changeset-fixed, check:engine-double-contract, check:error-code-casing, check:filter-alias-parity, check:i18n-stale-fill, check:published-readme-exports, check:route-ledger-census, check-dts-references, check:stack-collection-maps): all exit 0.
  • check:generated exit 0; check:liveness exit 0 (translation state counts 38 → 41).

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (translateBook joins the existing translators in system/i18n-resolver.ts, the table the readers already dispatch through.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes (eslint over the touched files, above).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (apart from the maintainer quick-read block, which the dispatch procedure writes in Chinese).
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzf4dTFmjsttVz7Pm8j

…rough the bundle's books group (wip)

Claude-Session: https://claude.ai/code/session_01WGufzf4dTFmjsttVz7Pm8j
Co-Authored-By: Claude <noreply@anthropic.com>
…cs, changeset, showcase zh-CN book strings

Claude-Session: https://claude.ai/code/session_01WGufzf4dTFmjsttVz7Pm8j
Co-Authored-By: Claude <noreply@anthropic.com>
…synthetic orphan group

Claude-Session: https://claude.ai/code/session_01WGufzf4dTFmjsttVz7Pm8j
Co-Authored-By: Claude <noreply@anthropic.com>
The wide layout wraps the book sidebar in an unlabelled nav, so filtering
navigations by link count picked the wrapper too. Query the navigation by
its name (the served book title) and pin that the authored title is absent
for a zh-CN reader.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzf4dTFmjsttVz7Pm8j
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/cli, @objectstack/lint, @objectstack/rest, @objectstack/runtime, @objectstack/spec, touching 34 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/authorable-surface/system.json, packages/spec/export-origins/system.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via getBookTree (sdk, the bare tail of client method meta.getBookTree, bound to GET /api/v1/meta/book/:name/tree), meta.getBookTree (sdk, the route ledger binds it to GET /api/v1/meta/book/:name/tree, selected by route anchor /meta/book/:name/tree))
  • content/docs/permissions/system-context.mdx (via handleMetadataRequest (symbol, a top-level function))
  • content/docs/protocol/kernel/i18n-standard.mdx (via TranslationDataSchema (symbol, a top-level const), globalActions (literal, a string literal in PlatformTranslationDataSchema; a string literal in TranslationDataSchema; a string literal in TranslationItemSchema), metadataForms (literal, a string literal in PER_APP_SETTINGS_PLATFORM_ONLY), settingsCommon (literal, a string literal in PER_APP_SETTINGS_PLATFORM_ONLY))
  • content/docs/protocol/objectui/actions.mdx (via globalActions (literal, a string literal in PlatformTranslationDataSchema; a string literal in TranslationDataSchema; a string literal in TranslationItemSchema))
  • content/docs/ui/apps.mdx (via /book/:name/tree (route, bridged from symbol createMetaBookTreeAnswer — its route source's handler names it), /meta/book/:name/tree (route, a path literal in a comment in appTranslationDataShape; a path literal in a comment on a changed line))
  • content/docs/ui/translations.mdx (via globalActions (literal, a string literal in PlatformTranslationDataSchema; a string literal in TranslationDataSchema; a string literal in TranslationItemSchema), metadataForms (literal, a string literal in PER_APP_SETTINGS_PLATFORM_ONLY), settingsCommon (literal, a string literal in PER_APP_SETTINGS_PLATFORM_ONLY))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx (via /book/:name/tree (route, bridged from symbol createMetaBookTreeAnswer — its route source's handler names it), /meta/book/:name/tree (route, a path literal in a comment in appTranslationDataShape; a path literal in a comment on a changed line))
  • content/docs/releases/v16.mdx (via globalActions (literal, a string literal in PlatformTranslationDataSchema; a string literal in TranslationDataSchema; a string literal in TranslationItemSchema))
  • content/docs/releases/v17/17-5.mdx (via /book/:name/tree (route, bridged from symbol createMetaBookTreeAnswer — its route source's handler names it), /meta/book/:name/tree (route, a path literal in a comment in appTranslationDataShape; a path literal in a comment on a changed line))
  • content/docs/releases/v17/17-7.mdx (via globalActions (literal, a string literal in PlatformTranslationDataSchema; a string literal in TranslationDataSchema; a string literal in TranslationItemSchema))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/authorable-surface/system.json, packages/spec/export-origins/system.json, …) — pages documenting those are invisible to this run
  • 7 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 55 of 207 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 159 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 96e7a4335c0aa611a706664da6df339f672ec1ab → packageMentionDocs.

Which tree this was computed on

This run read content/docs from e39bcb2b433a12d81fade431f99ab14ca48f07d7 — the merge of head 525ae464723b8994fec778d41e303a0049573e84 into base 96e7a4335c0aa611a706664da6df339f672ec1ab, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e39bcb2b433a12d81fade431f99ab14ca48f07d7 && git checkout e39bcb2b433a12d81fade431f99ab14ca48f07d7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 96e7a4335c0aa611a706664da6df339f672ec1ab 525ae464723b8994fec778d41e303a0049573e84 && git checkout -B drift-repro 96e7a4335c0aa611a706664da6df339f672ec1ab && git merge --no-ff 525ae464723b8994fec778d41e303a0049573e84

node scripts/docs-audit/affected-docs.mjs --json 96e7a4335c0aa611a706664da6df339f672ec1ab

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 96e7a4335c0aa611a706664da6df339f672ec1ab → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1f17d1834b91871decdcee2631dd145b9c979773
Local-runs: none
Scope: packages/spec/src/system/book.zod.ts, packages/spec/src/system/i18n-resolver.ts, packages/spec/src/system/translation.zod.ts + packages/rest/src/meta-item-read-gate.ts, packages/rest/src/meta-item-read-routes.ts, packages/rest/src/index.ts, packages/runtime/src/domains/meta.ts, packages/cli/src/utils/i18n-extract.ts, packages/cli/src/utils/i18n-coverage.ts, packages/lint/src/validate-translation-references.ts, scripts/fixtures/i18n-walk-parity/every-group.stack.json, ui/packages/app-shell/src/views/metadata-admin/previews/TranslationPreview.tsx, ui/apps/console/src/pages/use-book-data.ts, ui/apps/console/src/pages/book-nav.ts, .changeset/26690-translation-books.md

Inputs read: card #26690 (body, triage 6443918209, claim 6446870967, os-dev-report 6449088056), PR #26819 body and file list, the net diff origin/main...1f17d1834 (30 files), and the check-runs on the head. Check-runs at review time: 39 runs, 16 success, 5 skipped (opt-in lanes), 18 in_progress (Build Core/UI Deps/Docs, Test Core 1-8, Type Check workspace/debt/consumer, Lint scoped/repo/PM/CI gates), 0 failure. Those conclusions are the gate verdicts; nothing was built or run here. The seat's landing waits for the required jobs to conclude success (review-checklist: Lint & Repo Gates and TypeScript Type Check pinned to this head).

The rest of the diff (docs under content/docs/**, the showcase bundle, liveness, api-surface/authorable-surface/export-origins, the console and app-shell tests, the runtime parity test) is the lane seat's ACCEPT at TIER_DEFAULT; it was read to judge the hits, not reviewed.

① Derived judgments

Accept-set:

  • books opens on all three doors through the shared appTranslationDataShape() — TranslationDataSchema, PlatformTranslationDataSchema, TranslationItemSchema — right: one hunk, three faces, the way reports (feat(spec,cli): reports are translatable — reports in the translation bundle #25826) and positions/permissions (feat(spec,cli): positions and permission sets are translatable — positions and permissions in the translation bundle #25701) landed. The spec test parses the same bundle at all three.
  • Strict at both levels (strictObject with surface / history / aliases, the pages and permissions idiom); groups.KEY.label is REQUIRED while the book's label / description are optional — right: a group entry with no label overlays nothing, and the extractor demands it (pushEntry) for the same reason. Unknown keys refuse with unrecognized_keys (pinned).
  • Aliases name/title → label, sections/chapters/toc → groups, nested name/title → label, and the singular book → books on all three top-level alias maps — right; every target exists on the shape (the alias-integrity rule), and book collides with no sibling key.
  • The settings refusal prose and the shape docblocks move fourteen → fifteen (sixteen on the platform face), and books sits between permissions and metadataForms both in the shape and in the "in the order it declares them" list — right.
  • BookSchema: no accept-set change. The retired translations key still refuses (retiredKey, pinned); only its prescription moved from "delete, use doc.translations" to "delete and move the strings to books.NAME.*" — right, the old text would be false after this diff.

Public surface:

  • @objectstack/spec: translateBook, BookLike, BookGroupLike exported; api-surface and export-origins regenerated to match — right (the ReportLike precedent).
  • TRANSLATABLE_METADATA_TYPES gains book by derivation from METADATA_DOCUMENT_TRANSLATORS — right; isTranslatableMetaType in the rest gate reads the derived set, so GET /meta/book list and item translate with no further wiring (parity test pins both transports).
  • @objectstack/rest: MetaBookTreeSources gains the REQUIRED member resolveI18nService() (via Pick<MetaListTranslationSources, 'resolveI18nService'>); the type is exported from packages/rest/src/index.ts — judged in ③, right as shipped.
  • @objectstack/runtime: the dispatcher's book-tree route spreads metaTranslationSources(...) (which supplies resolveI18nService, resolveProtocol, requestLocale) after metaItemReadGateSources; the earlier explicit requestLocale line is subsumed, later spread wins — right. The gate then overrides resolveProtocol with async () => undefined because PACKAGED_BASE_ACCESSORS has no book row — right, no protocol call is wasted.
  • @objectstack/cli: walkBooks emits books.NAME.label / .description (pushOptional, optional on BookSchema) and books.NAME.groups.KEY.label (pushEntry, required) with source book; coverage maps book → i18n/missing-book with noun and surface rows — right. The walk-parity fixture authors a books[] member so check-i18n-walk-parity's declared == walked holds without a ledger entry — right.
  • Liveness: translation/books and its children seeded live with the resolver and the two rest readers as evidence; state counts 38 → 41 — right.
  • ui/packages/app-shell TranslationPreview.tsx: CATEGORY_DEFS satisfies Record<keyof TranslationDataSchema.shape, …> — the books row and its en/zh strings are a NECESSARY consequence of the schema change, not scope creep (the build reds without them). @object-ui/app-shell is private: true, so no changeset is owed for it.
  • Console: no code change, correctly. use-book-data.ts reads client.meta.getItems('book') and book-nav.ts renders book.label / group.label as served, so the translated list IS the sidebar; the console test pins bundle → served book → sidebar.
  • Generated docs (content/docs/references/system/book.mdx, translation.mdx, api/protocol.mdx) match the new .describe() text byte-for-byte in the hunks read — right, regenerated not hand-edited.

Nothing derived is judged wrong.

② Semver level

Changeset .changeset/26690-translation-books.md: @objectstack/spec minor, @objectstack/rest minor, @objectstack/runtime minor, @objectstack/cli minor. Right:

  • Metadata-protocol: added (new authorable keys books.NAME.label / .description / .groups.KEY.label) takes at least minor on the package the diff moves — spec. Right.
  • rest/runtime/cli carry new behaviour (a translated tree, a new port member, new extract keys and a new coverage bucket) — minor is the honest level; none is a bug fix.
  • @object-ui/app-shell and the console are private; examples/app-showcase is private; @objectstack/lint is untouched. No changeset missing.
  • Both declaration lines are present and identical in the PR body and the changeset body; no BREAKING banner is claimed, so no ADR-0087 marker is owed.

Clause-②: yes (widening). Right — nothing an author could write before is refused now; the only prose that changed on a refusal (the retired translations tombstone) still refuses the same key.
Metadata-protocol: added. Right — a new key family, no key removed, no default or meaning changed.

③ Boundary flags

  1. MetaBookTreeSources.resolveI18nService required or optional? — Answer: A, required, as shipped. Weighed by public-surface cost: the interface IS exported (export type { MetaBookTreeSources } in packages/rest/src/index.ts) and it is a port a transport CONSTRUCTS (an object literal handed to createMetaBookTreeAnswer), so a required member added to it is a compile-time break for any external transport on upgrade. Three things outweigh that: (a) the port's own contract is "each transport supplies only its I/O" and every sibling read already requires resolveI18nService on MetaListTranslationSources; the book tree inheriting that requirement is the port becoming consistent, not a new kind of demand; (b) an optional member converts a forgotten line into a tree served in the authoring language with nothing logged — the silent-degradation shape this repo's rules reject; a type error is the loud, migratable form; (c) no external implementer exists in-tree (rest-server, meta-item-read-routes, runtime meta.ts are the only three) and the changeset names the member for anyone outside. Level: the port is not an authorable surface, so Clause-② is judged on the metadata face (widening), and the TS-level requirement ships under minor with the member named in the changeset body — adequate.
  2. Synthetic Uncategorized group stays English — accepted as pre-existing (resolveBookTree hardcodes label: 'Uncategorized'; not an authored group, no bundle key). Not in this card's done-when (authored book label and group labels). Carrier note for a follow-up: a zh-CN reader with orphan docs still sees one English section title.
  3. Implicit per-package book resolves books.PACKAGE_ID.label — an effect of translating whatever book the tree resolves; harmless, and arguably useful. No action.
  4. Pinned spine-node labels (groups[].pages[] entries of the { doc, label } / { href, label } form) are not translated by translateBook — flagged here, not by the dev. resolveBookTree renders node.label ?? d?.label, so an authored node label already overrides the doc's own doc.translations label today; the gap is pre-existing in kind and outside the card's done-when. Escalated as a follow-up note for the seat, not a finding on this diff.
  5. translation-target-unknown has no books arm (dev's class-c finding) — judged below in ④ as a should-fix; routing in the summary.
  6. Skills-docs: follow-up owed — skills/objectstack-i18n/SKILL.md, evals/bundles-locales-coverage.json — a permitted form under pm-dispatch; the seat files the owed card and rewrites the line to #N before ACCEPT (review-checklist). A seat action, not a review finding.
  7. Check-runs — 18 still in_progress on the head at review time, 0 red. The seat pins the required jobs' conclusion: success to 1f17d1834 before landing; this record does not pre-empt them.

④ Design judgment

packages/spec/src/system/translation.zod.ts — holds. Tried to break: an author writing books.b.groups.g.title is refused with the label alias (pinned); books.b.icon refused unrecognized_keys (pinned); the singular book is refused with a pointer at books on each door (pinned). The group label being required where siblings' top-level label is optional is the correct asymmetry: a book's title may be absent (the schema allows it), a group's section title cannot be. The prose counts and the declared-order list agree with the shape. No finding.

packages/spec/src/system/i18n-resolver.ts — holds. translateBook follows translateReport's skeleton: name guard, per-attribute localeChain × pickData lookup (so the BCP-47 ladder, the deployment default-locale rule and the declared fallbackChain apply unchanged — pinned), an empty-string translation treated as absent (sibling rule), the SAME REFERENCE when nothing moves (pinned for en-US and for no bundle), per-key fallback to the authored string (pinned), and no mutation (pinned). It takes ResolveOptions and ignores packagedBase — right, because PACKAGED_BASE_ACCESSORS carries no book row (books are not org-overridable at the protocol), the translateApp precedent, so the gate's resolveProtocol: async () => undefined is consistent rather than a shortcut. A group without a string key passes through untouched; group order and every other group field ride through the spread. Registration in METADATA_DOCUMENT_TRANSLATORS is the whole REST wiring (pinned via TRANSLATABLE_METADATA_TYPES.has('book') and the runtime parity test on both transports). The description is translated on the list/item documents; ResolvedBookSchema carries no description, so the tree drops it — the tree's pre-existing shape, not a loss introduced here. No finding on the file.

packages/spec/src/system/book.zod.ts — holds. Prose only: the tombstone's prescription now names the bundle keys and os i18n extract, and the label describes name their keys; the old "the tree endpoint and the portal render label/description verbatim" sentence is removed because this diff makes it false. The retired key still refuses with retiredKey (pinned: the refusal text contains books.<book>.groups.<group_key>.label). The generated reference doc carries the same text. No finding.

Cited code read, as one change with the rule:

  • packages/rest/src/meta-item-read-gate.ts createMetaBookTreeAnswer step 5: translation runs AFTER audience.admitsBook(book) on the stored book — right, a label is not an access input and the audience must be judged on what was authored. served as typeof book is safe: the gate already returned on an undefined book. The route (meta-item-read-routes.ts) and the dispatcher (runtime/src/domains/meta.ts) both supply resolveI18nService, and the parity test pins the zh-CN/en-US tree on both transports with the doc entry still localized by doc.translations. One observation, pre-existing and not introduced here: the doc collapse uses sources.requestLocale() with no i18n service while the book translation uses requestLocale(i18n), so a deployment default-locale fallback can reach the book and not the docs; no change asked of this PR.
  • packages/cli/src/utils/i18n-extract.ts / i18n-coverage.ts: the walk is keyed by name and key, never by position; the new cli test pins the key set, the optional/required split, that a bundle written at the extracted paths parses under TranslationDataSchema, and the book bucket. Holds.
  • packages/lint/src/validate-translation-references.ts (the dev's class-c): the rule resolves objects, views, pages, globalActions, apps, dashboards, flows, picklists, and — through namedItemGroups — positions, permissions, reports. There is no books arm, and the header's "deliberately does NOT check" ledger does not list it, so books.crm_manaul.label parses clean, lints clean, and GET /meta/book serves the authored string in silence. Should-fix, not a must-fix: (i) the schema accept-set is identical to every sibling named group — a record keyed by name — so no authorable key is refused or accepted differently by the lint's absence; (ii) datasets sits in the same unledgered state on main today, so this is not a hole unique to this diff; (iii) the claim's file surface excluded the lint and the dev stopped and reported as the dispatch ordered. Against it, and why it is owed promptly: the two most recent named groups (positions/permissions in feat(spec,cli): positions and permission sets are translatable — positions and permissions in the translation bundle #25701, reports in feat(spec,cli): reports are translatable — reports in the translation bundle #25826) each landed their arm IN THE SAME PR as the group, and the rule's own header says an orphan key is "actively misleading", not inert. Routing: a one-commit patch round on this PR is small (one namedItemGroups row with namesOf('books'), plus a groups.KEY sub-leg against the book's declared group keys, plus a header line) and preferred; failing that, a filed card referenced from the PR before ACCEPT — ⛔ not a note.

Must-fix: none.
Should-fix: S1 — translation-target-unknown gains a books.NAME arm and a books.NAME.groups.KEY sub-leg (same-PR patch round preferred; else a filed follow-up card before ACCEPT).

Implemented-by: claude/project-thread-g5q3wk
Reviewed-by: session_01WGufzfhobWEgUUrjz4rrbh

VERDICT: PASS

…onsole loader knows `books`

translation-target-unknown gains a books arm: books.NAME resolves against
the declared books (a sibling package's and a package id's implicit book
included) and books.NAME.groups.KEY against that book's group keys.

The console's language loader lists `books` among the spec translation
groups, and the Studio translation-preview test counts the fifteenth
category.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzf4dTFmjsttVz7Pm8j
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4b8fb9a1b366c289bfa57251875f25336a1c3971
Local-runs: none
Scope: packages/spec/src/system/book.zod.ts, packages/spec/src/system/i18n-resolver.ts, packages/spec/src/system/translation.zod.ts + packages/lint/src/validate-translation-references.ts, packages/lint/src/validate-translation-references.test.ts, packages/lint/src/rule-explanations.ts, packages/lint/src/object-graph.ts, packages/rest/src/meta-item-read-gate.ts, packages/rest/src/meta-item-read-routes.ts, packages/spec/src/stack.zod.ts, packages/spec/src/api/protocol.zod.ts, ui/packages/i18n/src/utils/spec-translations.ts, ui/apps/console/src/loadLanguage.test.tsx, ui/packages/app-shell/src/views/metadata-admin/previews/TranslationPreview.tsx, ui/packages/app-shell/src/views/metadata-admin/previews/adminPreviews.i18n-10862-s2.test.tsx, .changeset/26690-translation-books.md, .changeset/config.json

DELTA record over the full review at 1f17d1834 (PR comment 6449452917, VERDICT PASS, should-fix S1). It judges the one commit 1f17d183..4b8fb9a1 (fix(lint,i18n): unknown doc-book translation keys are reported; the console loader knows books, 6 files, +149/−4), which the seat's claim amendment 6449487635 asked for, and re-affirms or corrects each judgment of the full record at the new head. Everything the full record found on the 29 files the delta does not touch stands unchanged; only what is named below moves.

Inputs read: card #26690 (body; triage 6443918209, claim 6446870967, os-dev-report 6449088056, claim amendment 6449487635), PR #26819 body, its 35-file list and the net diff against main plus the delta diff 1f17d183..4b8fb9a1, and the check-runs on 4b8fb9a1 at 2026-10-27T18:18Z: 31 runs, 7 success (Governed Surface Queue Guard, PR-body guards, Check PR Size, Check Documentation Links, Dogfood Regression Gate, Auto Label, filter), 5 skipped (opt-in lanes), 19 in_progress (Build Core/UI Deps/Docs, Plan Test Core shards, every Lint and Type Check job, Spec property liveness, Check Changeset, Flag docs), 0 failure. The Test Core shards and the UI test lane had not spawned yet. Those conclusions are the gate verdicts; nothing was built, run or re-run here. The seat pins the required jobs' conclusion: success to 4b8fb9a1 before landing; this record does not pre-empt them.

① Derived judgments

S1 is closed. The full record asked for a books.NAME arm, a books.NAME.groups.KEY sub-leg and a header line; the delta carries all three, with a fourth thing (the implicit-book admission) judged below.

Accept-set (what os lint now refuses, relative to main):

  • books.NAME keyed to no declared book → translation-target-unknown, error, where = locale "L" · book "NAME", nearest-name hint through the shared suggest → right; the reports arm's exact shape (namedItemGroups), and the concat disposition it relies on is real: COMPOSE_KEY_DISPOSITIONS.books === 'concat' (packages/spec/src/stack.zod.ts), so artifactProvidedRecords(stack, 'books') reading a sibling package's manifest.books is the right universe (pinned by the sibling-package test).
  • books.NAME.groups.KEY keyed to no group key of that book → error, hint "Match the key to a group's key (not its label)" → right; the leg resolves against Book.groups[].key, which is the key translateBook overlays by (packages/spec/src/system/i18n-resolver.ts), so a bundle keyed by a group's LABEL (Pipeline) is refused as the test pins. Only the groups map is walked; label / description stay the schema's (strict), consistent with the rule's ledger on leaf names.
  • The implicit per-package book: a package id (the stack's own manifest.id and every packages[].manifest.id) is admitted as a book with the single group key read off deriveImplicitPackageBook('_').groups[0].key (all), unless a book of that name is authored → right, and it matches the server. createMetaBookTreeAnswer resolves audience.bookNamed(name) = books.find(name) ?? deriveImplicitPackageBook(name, name) (packages/rest/src/meta-item-read-gate.ts), so GET /meta/book/PACKAGE_ID/tree really serves a book named by the package id with one all group, and translateMetaDocument then reads books.PACKAGE_ID.* off it (the full record's ③-3). The authored-wins rule (!books.has(id)) is the server's ??. The group key is read off the spec's own synthesis, not retyped, so it cannot drift.
  • Can the implicit rule let a real typo through? No. The server synthesizes a book for ANY unknown name (bookNamed never refuses), so the server side is looser than the lint; the lint admits only names that are declared books or declared package ids, which is the STRICTER side — crm_manaul is still reported (pinned). A typo can pass only by coinciding with another declared book or package id, which is the same residue every sibling arm carries. One admitted-but-idle key exists: books.PACKAGE_ID.label for a package that ships no docs lints clean and nothing renders it (the console synthesizes implicit books only for packages with docs, use-book-data.ts). That is not a typo and it is what the tree endpoint serves if asked; no action.
  • Nothing else in the rule visits rawData.books (no generic unknown-group walk exists), so no key is reported twice; no sibling arm changes.

Public surface:

  • @objectstack/lint: no new export; TRANSLATION_TARGET_UNKNOWN gains a new emitting site. The rule explanation (rule-explanations.ts) names books.BOOK, the package-id reading and the .groups.KEY "not its label" rule — right, and it matches the code.
  • packages/lint/src/validate-translation-references.ts header: a "Doc books" section (the S1 header line) and books appended to the ADR-0130 D4 compose-carrier list — right, books is concat.
  • The mechanism differs from the amendment's parenthetical (a namedItemGroups row with namesOf('books')): the dev built a Map from book name to its Set of group keys (universe.books) and its own loop, because the sub-leg needs each book's group keys and a row holding one Set of names cannot carry them. Right choice; the aim of the amendment (arm + sub-leg + header) is met.
  • @object-ui/i18n SPEC_TRANSLATION_GROUPS gains books — a REAL consequence, not a bent pin: ServedTranslationDataSchema is appTranslationDataShape() minus metadataForms, so the served document now carries books; ui/apps/console/src/loadLanguage.test.tsx walks GetTranslationsResponseSchema.shape.translations.shape and requires a payload carrying { books: {} } alone to take the transform branch. Without the entry isSpecTranslationData answers false and that pre-existing test reds; no test was edited for it. Runtime consequence: a served document carrying only books is namespaced under app instead of landing at the root. Nothing on the console reads app.books (the sidebar reads the already-translated GET /meta/book), and the changeset claims no more than namespacing — right.
  • adminPreviews.i18n-10862-s2.test.tsx 2/14 (14%) → 2/15 (13%) and the category.books site — a real consequence, not a bent pin: CATEGORY_DEFS is satisfies-checked as a Record over every key of TranslationDataSchema.shape (TranslationGroup) and CATEGORIES = Object.entries(CATEGORY_DEFS), so the coverage denominator IS the schema's group count (15 at this head) and the new site sits where the shape puts it (between permissions and metadataForms); 2/15 = 13.3% → 13%. The pin was red since 1f17d183 added the books row; the dev's "2 files, 23 passed" at that head ran TranslationPreview.categories-11765, not this file, which is how it was missed. The fix measures the rendered output, not a hand-edited constant.

Five new lint tests, read for vacuity:

  • (2) unknown book, (3) unknown group keys (two, one a LABEL), (5b) unknown group under the implicit book each assert a non-empty finding list with exact rule/severity/path, so removing the books loop reds all three — the dev's "3 of 5" claim agrees with the code as read. (5a) asserts [] for books.com.example.crm.groups.all with only manifest.id declared, so removing the package-id block reds it too; the implicit rule has its own positive. (1) and (4) are the false-positive guards on the same fixture and the sibling-package universe; they are vacuous alone and load-bearing beside (2)/(3), the way every sibling arm's "clean" test is.
  • The hints asserted are reachable by reading nearestName: crm_manaul→crm_manual is Levenshtein 2 under budget max(2, floor(10/3)) = 3; strat→start is 2 under budget 2. The test's stack shape (translations: [{ en }], packages[].manifest, manifest.id) is the shape the rule reads (stack.translations, packagesOf, stack.manifest).

Re-affirmed from the full record at this head, unchanged by the delta: every accept-set line on translation.zod.ts, i18n-resolver.ts and book.zod.ts; the three doors; the aliases; TRANSLATABLE_METADATA_TYPES; MetaBookTreeSources.resolveI18nService required; the runtime spread; the cli walk and coverage bucket; liveness; the console's no-code-change; the generated docs. Nothing derived is judged wrong.

② Semver level

Changeset .changeset/26690-translation-books.md now: @objectstack/spec, @objectstack/rest, @objectstack/runtime, @objectstack/cli, @objectstack/lint, @object-ui/i18n, all minor. Right:

  • @objectstack/lint minor — a bundle that linted clean now errors (new behaviour, not a bug fix); the reports precedent (.changeset/25788-translation-reports.md, PR feat(spec,cli): reports are translatable — reports in the translation bundle #25826) shipped its arm at minor. The full record's "@objectstack/lint is untouched" is superseded: it is touched and the bump is present.
  • @object-ui/i18n minor — the package publishes (publishConfig.access: public, no private), so a changeset is owed; the same precedent bumped it minor for the same kind of change. Right.
  • Both bullets in the changeset body describe the delta accurately (the implicit-book reading is stated; "namespaced like any other" claims no reader).
  • Correction to the full record (S2, should-fix): it said @object-ui/app-shell is private: true so no changeset is owed. It IS private, and by the AGENTS.md rule ("a changeset for anything that publishes") none is owed. But .changeset/config.json carries "privatePackages": { "version": true, "tag": false } — private packages are versioned and their CHANGELOG.md compiled from changesets — and the reports precedent listed '@object-ui/app-shell': minor for exactly this change (a new category in the translation designer's preview). One frontmatter line, '@object-ui/app-shell': minor, keeps the app-shell changelog's record of the Books category; the Check Changeset job (check-changeset-no-major.mjs) does not demand it, so it is a should-fix, not a gate matter.

Clause-②: yes (widening). Still right, judged against main: books did not exist there, so every bundle main accepted is accepted and lints the same now; the only new refusal (translation-target-unknown on a books.* key) can fire only on keys main's schema refused as unrecognized_keys. Nothing an author could write before is refused now.
Metadata-protocol: added. Still right — the delta adds no key and changes no default or meaning; a lint finding is not a metadata-protocol narrowing.

③ Boundary flags

  1. MetaBookTreeSources.resolveI18nService required — the full record's answer A stands; the delta does not touch the port.
  2. Synthetic Uncategorized group stays English — stands as a follow-up note; untouched.
  3. Implicit per-package book resolves books.PACKAGE_ID.label — the full record said "no action"; the delta now ADMITS it in the lint, which is the consistent half: what the tree serves, the lint resolves. Answered; right.
  4. Pinned spine-node labels not translated — stands as a follow-up note; untouched.
  5. translation-target-unknown has no books arm (the dev's class-c finding, the full record's S1) — closed by this delta. The os-dev-report's out_of_scope_findings entry is now stale as a finding; the seat's amendment already re-scoped it into this PR.
  6. Skills-docs — the PR body now reads Skills-docs: #26820. The docs are updated here.; the seat's rewrite is done. Nothing open.
  7. Check-runs — 19 in_progress on 4b8fb9a1 at 2026-10-27T18:18Z, 0 red; the Test Core shards and the UI lane not yet spawned. The seat pins conclusion: success on the required jobs to this head before landing. The two UI fixes in the delta are the ones the UI lane will measure; they were judged by reading above.
  8. PR body is stale (the dev never edits it; the seat does). Two sentences are now false and two sections omit the delta:
    • Acceptance notes, the sentence "translation-target-unknown does not resolve books.NAME keys yet (outside the claim's file surface): a typo in a book name or group key under books parses clean and translates nothing, silently. Reported to the seat as a finding, not fixed here." → replace with: "translation-target-unknown resolves books.NAME and books.NAME.groups.KEY against the declared books (a sibling package's included) and the implicit per-package book (the package id, one all group read off deriveImplicitPackageBook): a mistyped book name or group key is an error with the nearest declared name. Added in the review's patch round (claim amendment 6449487635)."
    • Verification, "Head 1f17d1834. Every build and test ran through scripts/pm/os-verify-lock.sh on a shared box." → replace with: "Head 1f17d1834 for the suites below; the patch round at 4b8fb9a1b added five validate-translation-references tests for books (removing the books loop reds three of them), the @object-ui/i18n group entry the console's loadLanguage pin requires, and the translation-preview coverage pin 2/15." (the seat fills the delta's measured figures from the dev's report).
    • What changes: add the two bullets the changeset already carries for @objectstack/lint and @object-ui/i18n.
    • Tests: add "packages/lint/src/validate-translation-references.test.ts (5 new): a declared book and its group keys are clean; an unknown book name; unknown group keys, a group LABEL among them; a sibling package's book; the implicit per-package book and an orphan group under it."

④ Design judgment

The three hits files are untouched by the delta; the full record's ④ on translation.zod.ts, i18n-resolver.ts and book.zod.ts stands at this head.

packages/lint/src/validate-translation-references.ts (the cited code the full record's S1 named, read as one change with the rule) — holds. Tried to break: books.crm_manaul.label → reported with Did you mean "crm_manual"?; books.crm_manual.groups.Pipeline (the label, cased) → reported, pipeline suggested by containment; books.com.example.crm.groups.start with no authored book of that name → reported (all is the only key); a package that authors a book named exactly its own id → the authored keys govern, the implicit all is not admitted (the server's ?? order). A books value that is not a record → the name is resolved and the leg skipped (if (!isRec(rawBook)) continue), consistent with flows. stack.books in record form → recordsOf reads the key as name, the sibling arms' reading. The universe is built once per run; the loop's cost is one Map per book. The orphan message says what the reader does with the key ("the book keeps its source-locale title" / "the docs sidebar keeps the section's source-locale title"), which is what translateBook and the tree do. No finding.

Must-fix: none.
Should-fix: S2 — add '@object-ui/app-shell': minor to the changeset frontmatter (private, versioned under privatePackages.version: true; the reports precedent listed it for the same preview change); S3 — the seat rewrites the stale PR-body sentences named in ③-8.

Implemented-by: claude/project-thread-g5q3wk
Reviewed-by: session_01WGufzfhobWEgUUrjz4rrbh

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-27T19:59Z

Head 525ae464723b8994fec778d41e303a0049573e84. Card #26690, claim 6446870967 with amendment 6449487635 (the books lint arm), dev report 6449088056.

  • Contract review: owed at CONTRACT_REVIEW_TIER for translation.zod.ts, i18n-resolver.ts and book.zod.ts.
    • The full review (6449452917) at 1f17d183 was a PASS. Its one should-fix, S1, was the missing books arm in translation-target-unknown.
    • The delta review (6450257839) at 4b8fb9a1 was a PASS. S1 is closed by a books.NAME arm and a books.NAME.groups.KEY sub-leg. The implicit per-package book is admitted with its group key read off deriveImplicitPackageBook, which matches what createMetaBookTreeAnswer serves. The two Test UI fixes measure the new group: the translation-preview denominator is the schema's group count (2/15), and SPEC_TRANSLATION_GROUPS gains books. They are not bent pins.
  • Should-fixes since then, all closed:
    • S2: adfc3060 adds '@object-ui/app-shell': minor to the changeset.
    • S3: the seat rewrote the stale body sentences.
  • Main merge: d99a631f merges main (no rebase), bringing in a20824f3 (fix(ui): the action key inventory lists disabledReason #26859). 525ae464 regenerates two artifacts on the merged tree. The books group, book: translateBook and feat(spec): a greyed action says why — disabledReason beside disabled #26786's disabledReason hunks all survive the merge.
  • Skills-docs: #26820.
  • Shape: Fixes #26690, 35 files. @objectstack/spec, @objectstack/rest, @objectstack/runtime, @objectstack/cli, @objectstack/lint, @object-ui/i18n and @object-ui/app-shell are minor. Clause-②: yes (widening) and Metadata-protocol: added. check-governed-merges --pr 26819: NOT governed.
  • Gates: check-runs on this head are 57 success, 5 skipped and 0 red. mergeable_state is clean.
  • Landing: Tier S. pr_ready + automerge_enable in this act.

Merged via the queue into main with commit 031d97a Oct 27, 2026
63 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-g5q3wk branch October 27, 2026 20:38
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 40576208471 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test UI (10/16) — 失败步骤: Run the ui tests (shard 10/16)

    × still gates the submit button on the same validation (the rule did not move) 92ms
    FAIL   dom  packages/app-shell/src/views/metadata-admin/PackageFormDialog.validationTiming.test.tsx > PackageFormDialog — validation timing (objectui#5416 defect 1) > still gates the submit button on 
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • packages/app-shell/src/views/metadata-admin/PackageFormDialog.validationTiming.test.tsx — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 12 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs: a book's label and its group labels cannot be translated, so a zh-CN reader sees the book and every sidebar section in the authoring language

2 participants