Skip to content

feat(spec): a joined report's block headings and a master-detail field's inline grid heading are translatable - #26757

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-2humd9
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-2humd9

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread
Fixes #26112
Fixes #26142

Clause-②: yes
Metadata-protocol: added
Skills-docs: follow-up owed #26759 — skills/objectstack-i18n/SKILL.md (the fields.{field_name} row and the reports sentence) misses inlineTitle and reports.{report}.blocks.{block}; skills/** is Tier H, so the two-line edit rides that card after this PR lands

维护者速读

  • 改了什么:翻译包新增两个槽位——联合报表每个区块的标题/说明(reports.REPORT.blocks.BLOCK.label / .description),以及主从字段的内嵌表格标题(objects.CHILD.fields.FIELD.inlineTitle);服务端翻译器按请求语言覆盖,os i18n extract / check 也开始统计这两个键。
  • 为什么改:中文用户打开联合报表时标题是中文、区块标题却是英文;新建表单上的内嵌子表标题(如「Line Items」)也一直是英文,因为翻译包根本没有地方放这些译文,覆盖率检查也看不到缺口。
  • 风险与回滚:只新增可选键,已有翻译包与元数据不受影响;组织自行改过的区块标题或内嵌标题仍保留原文(与报表标题同一规则)。回滚即撤销本 PR。
  • 先看:packages/spec/src/system/i18n-resolver.ts(translateReportBlocks 与 translateObject 字段处理中的 inlineTitle)、packages/spec/src/system/translation.zod.ts(两个新槽位)、packages/cli/src/utils/i18n-extract.ts(walkReports 与字段遍历)。
  • 两分钟验证:pnpm --filter @objectstack/example-showcase exec vitest run test/zh-cn-report-blocks-inline-titles.test.ts,预期 3 个用例通过:示例应用的联合报表区块显示「未完成任务 / 已完成任务」,发票与费用报销的内嵌表格标题显示「发票明细 / 费用明细」。
  • 需要你做的:无,席位落地。

What changes

  • Spec face (translation.zod.ts): FieldTranslationSchema gains inlineTitle; the reports group gains blocks, a record keyed by the joined block's required name, closed at label / description (aliases name / title / heading onto label).
  • Serving translators (i18n-resolver.ts): translateReport overlays each block through a new translateReportBlocks; translateObject's field pass overlays inlineTitle. Both follow the packaged-base rule (ADR-0029 D9.2a), judged per block (matched by name) and per field. A document whose blocks and fields the bundle does not address comes back as the same reference.
  • Landing point, measured: both labels already reach the console as the served document. The joined renderer (ui/packages/plugin-report/src/DatasetReportRenderer.tsx) draws block.label off GET /meta/report, and attachInlineSubforms (ui/packages/app-shell/src/providers/MetadataProvider.tsx) copies the child field's inlineTitle off GET /meta/object, both localized at the REST boundary. So the fix lands in the two translators, and neither ui file changes.
  • i18n: a master-detail field's inlineTitle has no translation slot — the inline child grids on a zh-CN create form read "Versions / Deviations / Payment Plan" #26142 fallback decision: no fallback to the child object's translated plural label. An authored inlineTitle with no entry keeps its authored text, because a bundle translates an attribute and never swaps another attribute in over authored text (the rule pages.PAGE.title follows). An absent inlineTitle already renders the child's translated label.
  • Extract / check (i18n-extract.ts): walkReports walks blocks[], the field walk emits inlineTitle, both via pushOptional (a block without a label, or a field without an inlineTitle, is recorded and never demanded). os i18n check counts them through the same entries: blocks in the report bucket, inlineTitle in the field bucket.
  • Examples: the showcase zh-CN bundle translates both blocks of showcase_task_overview and the invoice / expense-report inline headings; the CRM zh-CN bundle translates the opportunity's product grid heading. Each key is translated at birth, so check:i18n-coverage stays at its baseline (527, none new).
  • Ledgers and generated files: a reports.blocks liveness row with label / description children (translation 36 → 38 live), plus gen:api-surface, gen:export-origins, gen:docs, gen:strictness-ledger, gen:liveness-counts output.

Tests

  • packages/spec/src/system/i18n-report-blocks.test.ts (10): the slot at all three doors, the closed face, the title alias, the overlay, the BCP-47 ladder, reference identity, and the packaged-base rule per block.
  • packages/spec/src/system/i18n-field-inline-title.test.ts (7): the slot, a non-string refusal, the overlay through the type dispatch with no unrecognized_keys, no plural-label fallback, and the packaged-base rule.
  • packages/cli/test/i18n-block-inline-title-walk.test.ts (5): the extracted keys, unwritten strings recorded without being demanded, the bundle round trip, and coverage reporting both gaps and then clearing them.
  • examples/app-showcase/test/zh-cn-report-blocks-inline-titles.test.ts (3): the acceptance pin for both cards, on the app's own metadata and bundle.

Acceptance notes

  • validate-translation-references (lint) checks that reports.REPORT names a real report, but it does not check that blocks.BLOCK names a real block of that report, so a mistyped block key resolves nothing without an orphan finding. This is noted, not filed.
  • The joined renderer's resolveText reads an inline locale map's default only. That predates this change and is unrelated to the bundle path.

Verification

All readings were taken at bcbc83749 (after merging origin/main 5cc31c788 through os-regen-merge.sh) unless marked as taken at 595f62162 (the pre-merge head).

Tests and typecheck
  • pnpm --filter @objectstack/spec test (595f621): Test Files 733 passed, Tests 21938 passed / 1 todo.
  • pnpm --filter @objectstack/spec test:repo (595f621): Test Files 80 passed, Tests 1295 passed.
  • pnpm --filter @objectstack/cli exec vitest run --project unit (595f621): Test Files 340 passed, Tests 4928 passed. The integration tier is left to CI: no spawn entry or integration file is touched.
  • pnpm --filter @objectstack/runtime test (595f621): Test Files 459 passed, Tests 7443 passed / 19 skipped.
  • typecheck for spec, cli, example-showcase and example-crm (595f621): all exit 0.
  • After the merge (bcbc837): src/system/ + src/ui/report* spec tests 64 files / 2150 tests, the two cli i18n walk files 9 tests, and the showcase pin 3 tests, all passing.
  • The ui packages are untouched, so no ui test run is owed.
Ablations (one per behaviour; src-resolved suites, each restored byte-identical by blob hash)
  • translateReport with the block pass removed: i18n-report-blocks.test.ts 6 failed / 4 passed.
  • translateObject with the inlineTitle assignment removed: i18n-field-inline-title.test.ts 3 failed / 4 passed.
  • extract with the inlineTitle push deleted: the cli walk test 3 failed / 2 passed.
  • extract with the block label push deleted: the cli walk test 4 failed / 1 passed.
Gates
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 109 families (595f621; the derivation at bcbc837 is the same list). 107 exit 0. Two exit 3 (prerequisite: a whole-repo build), spec check:skill-examples and check:dual-build-cjs-loads, are NOT MEASURED locally and left to CI. --ran reconciliation: 109 derived, 107 run, 2 NOT-MEASURED, 0 UNRUN.
  • Re-run at bcbc837 after the merge: check:i18n, check:i18n-coverage, check:i18n-walk-parity, spec check:liveness, check:empty-state, check:strictness-ledger, check:api-surface, check:authorable-surface, check:generated, check:nul-bytes, all exit 0.
  • No full dogfood run: there is no ! in the title and no (narrowing) arm.

Generated by Claude Code

…inlineTitle

A joined report's block label/description and a master-detail field's
inlineTitle are authored, user-visible labels with no translation slot.
Add `reports.<r>.blocks.<b>.{label,description}` and
`objects.<o>.fields.<f>.inlineTitle`, overlay them in translateReport and
translateObject's field pass (packaged-base rule, ADR-0029 D9.2a), walk
them in i18n extract so `os i18n check` counts them, and translate the
example rows that ship them.

Claude-Session: https://claude.ai/code/session_01WGufziF7p6fS8zz6Gtt1gq
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/spec, touching 17 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/data-modeling/fields.mdx (via inlineTitle (symbol, a field of interface ObjectFieldLike), inlineTitle (literal, a string literal in collectExpectedEntries; a string literal in lookupObjectFieldAttr; a string literal in translateObject))
  • content/docs/deployment/validating-metadata.mdx (via globalFilters (literal, a string literal in packagedPart))
  • content/docs/protocol/objectui/actions.mdx (via bulkActionDefs (literal, a string literal in packagedPart))
  • content/docs/ui/actions.mdx (via bulkActionDefs (literal, a string literal in packagedPart))
  • content/docs/ui/dashboards.mdx (via globalFilters (literal, a string literal in packagedPart))
  • content/docs/ui/views.mdx (via bulkActionDefs (literal, a string literal in packagedPart))

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

  • content/docs/releases/v16.mdx (via globalFilters (literal, a string literal in packagedPart))
  • content/docs/releases/v17/17-6.mdx (via globalFilters (literal, a string literal in packagedPart))
  • content/docs/releases/v17/17-7.mdx (via ObjectFieldLike (symbol, a top-level interface), bulkActionDefs (literal, a string literal in packagedPart))

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
  • 3 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 — 154 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 88fb255cbac335cc97e9d148daadf164cfec2343 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from c761fd0abc317684fe1af8353272fe79b868772b — the merge of head bcbc83749b5adc7aff859f5d2351a0b75cc1e3e1 into base 88fb255cbac335cc97e9d148daadf164cfec2343, 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 c761fd0abc317684fe1af8353272fe79b868772b && git checkout c761fd0abc317684fe1af8353272fe79b868772b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 88fb255cbac335cc97e9d148daadf164cfec2343 bcbc83749b5adc7aff859f5d2351a0b75cc1e3e1 && git checkout -B drift-repro 88fb255cbac335cc97e9d148daadf164cfec2343 && git merge --no-ff bcbc83749b5adc7aff859f5d2351a0b75cc1e3e1

node scripts/docs-audit/affected-docs.mjs --json 88fb255cbac335cc97e9d148daadf164cfec2343

⚠️ 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 88fb255cbac335cc97e9d148daadf164cfec2343 → 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: bcbc83749b5adc7aff859f5d2351a0b75cc1e3e1
Local-runs: none
Scope: packages/spec/src/system/i18n-resolver.ts, packages/spec/src/system/translation.zod.ts, packages/cli/src/utils/i18n-extract.ts, packages/cli/src/utils/i18n-coverage.ts, packages/spec/api-surface/system.json, packages/spec/authorable-surface/system.json, packages/spec/export-origins/system.json, packages/spec/liveness/translation.json, packages/spec/liveness/state-counts/translation.md, docs/audits/2026-07-unknown-key-strictness-ledger.counts/system.md, examples/app-crm/src/translations/crm.translation.ts, examples/app-showcase/src/system/translations/index.ts, examples/app-showcase/test/zh-cn-report-blocks-inline-titles.test.ts, packages/cli/test/i18n-block-inline-title-walk.test.ts, packages/spec/src/system/i18n-field-inline-title.test.ts, packages/spec/src/system/i18n-report-blocks.test.ts, .changeset/26112-26142-block-inline-title-translations.md + packages/spec/src/ui/report.zod.ts (JoinedReportBlockSchema), packages/spec/src/data/field.zod.ts (inlineTitle), packages/rest/src/meta-item-read-gate.ts (translateMetaList / item twin, PACKAGED_BASE_ACCESSORS), packages/rest/src/form-routes.ts, ui/packages/plugin-report/src/DatasetReportRenderer.tsx, ui/packages/plugin-report/src/ReportRenderer.tsx, ui/packages/app-shell/src/views/ReportView.tsx, ui/packages/app-shell/src/providers/MetadataProvider.tsx (attachInlineSubforms, composeObjects, the meta.getItems read), ui/packages/app-shell/src/views/metadata-admin/previews/TranslationPreview.tsx, ui/packages/i18n/src/utils/spec-translations.ts, packages/lint/src/validate-translation-references.ts, scripts/check-i18n-walk-parity.mjs, scripts/i18n-coverage-baseline.json, examples/app-showcase/src/ui/reports/index.ts, examples/app-showcase/src/data/objects/invoice.object.ts, examples/app-showcase/src/data/objects/expense-report.object.ts, examples/app-crm/src/objects/opportunity-line-item.object.ts, skills/objectstack-i18n/SKILL.md, content/docs/ui/translations.mdx, content/docs/protocol/kernel/i18n-standard.mdx, docs/adr/0029 (D9.2a)

Inputs read: cards #26112 and #26142 with every comment (claim 6442845658, fold pointer 6442877409, os-dev-report 6444984537), PR #26757 body, file list (19 files, 645/27, equal to git diff merge-base 5cc31c78…head), every PR comment (one: the docs-drift bot), the net diff against main, follow-up card #26759, and the head's check-runs at 2026-10-27T14:53Z and 2026-10-27T15:01Z. The head is the same at both readings and at the time of this write. Nothing was built, run or re-run; one git fetch of the head into the local checkout.

① Derived judgments

  1. spec/i18n: a joined report's block labels have no translation key, so under zh-CN the showcase's "Task Overview" shows a translated title above English block headings ("Open Tasks", "Completed Tasks") #26112 acceptance holds end to end through the serving door. reports.REPORT.blocks.BLOCK.label / .description is declared on the one reports entry shape (translation.zod.ts:1183-1205) that appTranslationDataShape() feeds to all four doors (TranslationData, PlatformTranslationData, ServedTranslationData, TranslationItem; :1997, :2040, :2081, :2246), closed with strictObject and title / name / heading aliased onto label. The key is stable: JoinedReportBlockSchema.name is SnakeCaseIdentifierSchema with no .optional() (report.zod.ts:340). translateReport (i18n-resolver.ts:2345-2371) now runs translateReportBlocks (:2281-2310), matching each served block to its packaged counterpart by name through packagedPart(base, 'blocks', …) and applying valueOverridesPackagedBase per block, so a block an org re-authored keeps its text, a block the packaged report never declared is treated as authored ({} from packagedPart), and no base supplied means the catalog applies — the same three answers the dashboard-widget and view-bulk-action translators give. Identity: an unaddressed block is returned as the same reference and a report none of whose keys match comes back as the input (pinned). The REST boundary localizes both list and item reads (meta-item-read-gate.ts:2895-2926 and the item twin at :2976-2985) for every type in TRANSLATABLE_METADATA_TYPES, which is derived from the dispatch table that already carries report, with packagedBase resolved through PACKAGED_BASE_ACCESSORS (report → getPackagedReportBase, :2826). The joined renderer draws resolveText(block.label, block.name ?? …) (DatasetReportRenderer.tsx:1843-1845) on the report prop handed down by ReportRenderer from ReportView, which takes reports from useMetadata() (ReportView.tsx:56) — the GET /api/v1/meta/report list the provider fetches through meta.getItems — so the translated string is what it reads; a translated block label is a plain string, which resolveText returns as is. The showcase pin asserts [open_block, 未完成任务], [done_block, 已完成任务] under the translated title.

  2. i18n: a master-detail field's inlineTitle has no translation slot — the inline child grids on a zh-CN create form read "Versions / Deviations / Payment Plan" #26142 acceptance holds end to end through the serving door. FieldTranslationSchema.inlineTitle (translation.zod.ts:140) is a plain z.string().optional(), and FieldTranslationSchema is the one schema every door's fields record reuses (:282), so the per-object door and the translation metadata type gain it together; a locale-map value is refused as invalid_type (pinned). translateObject's field pass (i18n-resolver.ts:3957-3960) overlays it under the same valueOverridesPackagedBase(packagedObjectField(…), 'inlineTitle', def.inlineTitle) rule as description, and lookupObjectFieldAttr reads objects.OBJ.fields.FIELD.inlineTitle across the locale chain (:3334-3336). The console's attachInlineSubforms (MetadataProvider.tsx:599-623) copies d.inlineTitle as the subform title from the CHILD object's served field def; it runs inside composeObjects (:678) over the provider's stored object items, which are the GET /api/v1/meta/object list (meta.getItems('object', …), :1045) — the REST-localized document — not a bundled copy. The one unserved path is the metadata-admin preview (preview.list('object'), :1044), which is the Studio draft preview and not the create form the card measured. The hotclm symptom (clm_contract_version.fields.contract.inlineTitle) is exactly the shape the spec test pins with contract_version / contract. The showcase pin covers showcase_invoice_line.fields.invoice and showcase_expense_line.fields.expense_report.

  3. The no-plural-fallback rule is consistent with pages.title and the other title attributes. translatePageHeaderTitle resolves pages.NAME.title across the chain, else the header's AUTHORED title left as written (i18n-resolver.ts:2469-2488); translateFormTitle keeps the authored title on a missing key (:1264, :1276); dashboard widget title and the report's own label keep the authored string when unaddressed. In every case the bundle translates an attribute and never substitutes another attribute for authored text; the only "derive from another attribute" behaviour (a header with NO title) is the renderer's absent-value case, and the ui already draws the child's label when inlineTitle is absent (attachInlineSubforms only sets title when d.inlineTitle is truthy). The decision is stated in the schema TSDoc, the translator comment, the PR body, the changeset and skills/objectstack-i18n: the group table misses fields.{field}.inlineTitle and reports.{report}.blocks.{block}.{label,description} (follow-up owed by PR #26757) #26759, and pinned by i18n-field-inline-title.test.ts (amendment keeps Amended Versions).

  4. Extract and check count both keys. collectExpectedEntries pushes objects.OBJ.fields.FIELD.inlineTitle with pushOptional next to placeholder (i18n-extract.ts:1393), and walkReports walks report.blocks[] by the block's name, pushing label and description with pushOptional (:1900-1908), so an authored string is demanded and an unauthored one is recorded without an inline. computeI18nCoverage reaches the same entries, mapping field → i18n/missing-field and report → i18n/missing-report (i18n-coverage.ts:331-361); the cli test pins both the gap and its clearing. check:i18n-walk-parity is group-level by design (its header says a new leaf stays green there and the per-family pin tests carry it) — this PR adds that pin. The example rows are correct against the metadata: showcase_task_overview blocks are open_block / done_block with labels and no descriptions (reports/index.ts:47-59); showcase_invoice_line.fields.invoice.inlineTitle: 'Line Items' (invoice.object.ts:219); showcase_expense_line.fields.expense_report.inlineTitle: 'Expense Lines' (expense-report.object.ts:156); crm_opportunity_line_item.fields.opportunity.inlineTitle: 'Products' (opportunity-line-item.object.ts:34). Those four are the only authored inlineTitles and the only joined report in examples/, so every new demanded key is translated at birth and the coverage baseline (i18n-coverage-baseline.json: crm 101 + showcase 320 + todo 106 = 527) is correctly unchanged; Lint · repo & tooling gates (hosts check:i18n-coverage and check:i18n-walk-parity) is green on this head.

  5. ui consumers that enumerate translation keys are unaffected. TranslationPreview.tsx:75 types CATEGORY_DEFS on keyof TranslationDataSchema.shape — top-level groups only; this PR adds no group, so the precedent of the reports-group card does not recur. spec-translations.ts SPEC_TRANSLATION_GROUPS is likewise top-level, and transformSpecTranslations forwards field members it does not flatten. Type Check UI and Build & E2E UI are green on this head.

  6. Generated artifacts agree with the source. api-surface and export-origins gain ReportBlockLike (interface); authorable-surface gains system/FieldTranslation:inlineTitle (the reports entry shape is an anonymous inline strict object, so its blocks leaf has no row there, as label / description have none); the strictness ledger moves 383 → 384 for the one new strictObject site; the liveness ledger adds reports.blocks with label / description children whose evidence anchors #translateReportBlocks / #lookupReportBlockAttr exist in the cited file (Spec property liveness green). content/docs/references/** changes are gen:docs output.

  7. Check-runs on the head (the gate verdicts). At 2026-10-27T14:53Z: 17 success, 7 skipped, 19 in progress. At 2026-10-27T15:01Z: 40 success, 9 skipped (opt-in / matrix shells), 1 in progress — Test Core (6/8). Green at that reading, among others: Build Core (hosts pnpm check:dual-build-cjs-loads, ci.yml job build-core), Type Check · consumer gates (hosts pnpm --filter @objectstack/spec run check:skill-examples, lint.yml job typecheck-consumers) — the two gates the dev left to CI (local exit 3) — plus TypeScript Type Check, Type Check · source gates / workspace / debt ledger, Type Check UI, Spec property liveness, Check Changeset (both), Lint · ESLint, Lint · repo & tooling gates, Lint · docs & code guards, Lint · CI, release & engine gates, Lint · PM dispatch-gates, Governed Surface Queue Guard, Build & E2E UI, Test UI, Test Core 1-5 and 7-8. Test Core (6/8) was in progress at the last reading: that is a reading, not a pass, and landing waits for it.

② Semver level

  • Changeset .changeset/26112-26142-block-inline-title-translations.md: @objectstack/spec: minor, @objectstack/cli: minor, body carries Clause-②: yes and Metadata-protocol: added line-initial, and the PR body carries the same two lines. Right: the diff moves packages/spec/src and packages/cli/src, both published (private unset, 18.2.0, one fixed group); examples/* are private: true, content/docs and docs/audits publish nothing. The protocol kind added takes at least minor on every package the diff moves — both are minor. No breaking banner, no migration, no ADR-0087 marker is owed (nothing removed, renamed, refused or re-meant).
  • Clause-②: yes — right. The accepted set widens (two new optional authorable keys on a strict face) and the public surface grows (ReportBlockLike export, ReportLike.blocks, ObjectFieldLike.inlineTitle). The (widening) arm would also be accurate and is optional; yes without an arm is a legal spelling.
  • Metadata-protocol: added — right. Two new keys an author can write, no key narrowed, no meaning or default changed, so added is the one kind and the precedence rule does not bite.

③ Boundary flags

  1. Branch claude/project-thread-2humd9 instead of claude/issue-26112-slug — declared in the claim as the harness-pinned thread branch; Implemented-by below is that branch. Accepted.
  2. DatasetReportRenderer.tsx and MetadataProvider.tsx unchanged — verified against the served path (①1, ①2): both read the REST-localized list the provider fetches; the only unserved read is the Studio preview, out of both cards' scope. Accepted.
  3. Two derived gates NOT MEASURED locally (check:skill-examples, check:dual-build-cjs-loads, exit 3 for want of a whole-repo build) — their CI counterparts, Type Check · consumer gates and Build Core, are green on this head (①7). Answered.
  4. Derived families 90-104 ran under a concurrent build lock — the dev re-ran the ratchet / generated / i18n families clean at the head, and the CI jobs that host them (TypeScript Type Check, Lint · repo & tooling gates, Spec property liveness) are green. Answered.
  5. No reviewer request on the PR — this record is the contract-tier review the claim's --tier line owes; nothing further is needed from the dev. Answered.
  6. cli integration tier not run locally — Test Core shards 1-5, 7, 8 green; 6/8 in progress at the last reading. Open as a reading only; the seat lands when it completes.
  7. Skills-docs: follow-up owed #26759 — the card is right for what it names: skills/objectstack-i18n/SKILL.md:162 (fields.{field_name} row: label, help, placeholder, options) and :175 (reports, positions and permissions hold label + description) are the two sentences, both now incomplete, skills/** is Tier H, and the card's prescription (keys, the no-fallback rule, the pages.{page}.components.{id} analogy, one example row each) matches the shipped face. Incomplete in one respect, escalated to the owning seat: the hand-written author page content/docs/ui/translations.mdx enumerates the same keys — :71 (objects.NAME.fields.FIELD.label / help / placeholder) and :85 (reports.NAME.label / description, the row the reports group landed with) — and now misses inlineTitle and reports.NAME.blocks.BLOCK.label / description; content/docs/ui/ is hand-written and ungoverned, so no Tier H reason defers it, and no carrier names it (the docs-drift bot anchors on symbols and could not list it). Remedy, the seat's choice: push the two rows onto this PR (a doc-only change; a new head needs a fresh record) or add the file and lines to skills/objectstack-i18n: the group table misses fields.{field}.inlineTitle and reports.{report}.blocks.{block}.{label,description} (follow-up owed by PR #26757) #26759's body so the owed edit has a carrier. Not a contract defect — the Zod face, the served door and the extract/check count are complete — so it does not move the verdict.
  8. Dev out-of-scope findings — (a) validate-translation-references resolves reports.NAME but not blocks.BLOCK against that report's blocks: confirmed (validate-translation-references.ts:2161-2165; the rule's own header at :55-66 says leaf-attribute shape is Zod's job, and blocks is a strict object so a bad leaf is refused at parse while a bad block NAME resolves nothing silently). Noted in the PR, not filed — consistent with Prime Directive chore: version packages #10 (no repro of a user-visible defect, a lint gap). One more line is now stale in that file's header: :163 "Leaf keys below the name are the schema's (label / description, strict)" — ride it with skills/objectstack-i18n: the group table misses fields.{field}.inlineTitle and reports.{report}.blocks.{block}.{label,description} (follow-up owed by PR #26757) #26759 or the next touch of that file. (b) resolveText reads an inline locale map's default member only (DatasetReportRenderer.tsx:301-308) — pre-existing, orthogonal to the bundle path, and a translated block label is a string. Accepted as noted.
  9. Docs-drift bot — of the six hand-written pages it listed, content/docs/data-modeling/fields.mdx:213 authors inlineTitle: 'Lines' in an example and makes no claim about translation; the other five anchor on packagedPart's pre-existing string literals, not on anything this diff changed in meaning. No page is falsified; the gap that matters is ③7.
  10. Clause-② arm omitted — see ②; a legal spelling, no action.

Implemented-by: claude/project-thread-2humd9
Reviewed-by: session_01WGufzgbJhFoXYsLSe5WSPP

VERDICT: PASS

Rendered 2026-10-27T15:04Z · read-only review of head bcbc83749b5adc7aff859f5d2351a0b75cc1e3e1; landing still waits on the one in-progress check-run named in ①7 and on the seat's answer to ③7.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读 (席位终稿)

  • 改了什么:翻译包新增两个槽位,一个是联合报表每个区块的标题和说明(reports.REPORT.blocks.BLOCK.label / .description),另一个是主从字段内嵌表格的标题(objects.CHILD.fields.FIELD.inlineTitle)。服务端翻译器按请求语言覆盖这两处,os i18n extract / check 也开始统计这两个键。
  • 为什么改:中文用户看联合报表时,报表标题是中文,区块标题却是英文;新建表单里的内嵌子表标题(如「Line Items」)也一直是英文。原因是翻译包里没有地方放这些译文,覆盖率检查也就看不到缺口。
  • 风险与回滚:只新增可选键,已有翻译包和元数据不受影响。没有译文的内嵌标题保留作者原文,与报表标题同一规则。回滚即 revert 本 PR。
  • 先看:packages/spec/src/system/i18n-resolver.ts(translateReportBlocks,以及 translateObject 字段处理里的 inlineTitle)、packages/spec/src/system/translation.zod.ts、packages/cli/src/utils/i18n-extract.ts。
  • 两分钟验证:pnpm --filter @objectstack/example-showcase exec vitest run test/zh-cn-report-blocks-inline-titles.test.ts,预期 3 个用例通过。
  • 需要你做的:无,席位落地。技能文档和作者文档补两个键的工作挂在 skills/objectstack-i18n: the group table misses fields.{field}.inlineTitle and reports.{report}.blocks.{block}.{label,description} (follow-up owed by PR #26757) #26759(skills/** 需人工合入)。

席位意见:接受。契约复核 PASS(6445499937)。两个 ui 文件不改是实测结论:渲染器读的就是服务端已翻译的文档。

席位核对 · ACCEPT · domain:spec seat 3 · os-support-ai · session session_01WGufzgbJhFoXYsLSe5WSPP · 2026-10-27T15:07Z

Head: bcbc83749b5adc7aff859f5d2351a0b75cc1e3e1 · cards #26112 + #26142 (fold) · claim 6442845658, fold pointer 6442877409

  1. 形态: a draft PR to main whose body opens with Fixes #26112 and Fixes #26142 on separate lines. No closing keyword sits next to any other card. 19 paths, +645 / −27. check-governed-merges --pr 26757: NOT governed, 672 lines ≤ 3000, so the ordinary queue landing applies. The file list is inside the claim surface: spec translation face and resolver, cli extract/coverage, the generated surfaces, the example bundles and the changeset. Both ui files are untouched, as the claim allows.
  2. 前提实测: the premise holds on 43731031 (seat and thread). The landing point moved, as measured: both labels reach the console through the REST-boundary translators, so the fix lands in translateReport / translateObject. The review confirmed this through the serving door (record ①1, ①2). origin/main a485b5a3 touched none of the 19 paths since the branch's merge of 5cc31c788.
  3. 门禁 check-runs (on this head): 51 runs, 42 success, 9 skipped, none red or pending. Lint & Repo Gates, TypeScript Type Check, Build Core and Type Check · consumer gates are success. The last two are the CI counterparts of the two gates the dev could not run locally (exit 3, whole-repo build), and Test Core 1–8 is success. The contract-tier review is PASS, record 6445499937 (CONTRACT_REVIEW_TIER, schema face).
  4. 接受的偏差: (a) the landing point is the serving translators, not the ui readers, as measured and recorded in the claim. (b) inlineTitle has no plural-label fallback, the same rule as pages.PAGE.title. (c) Out-of-scope findings noted in the PR and not filed: validate-translation-references resolves reports.NAME but not a block name under it (a lint gap with no user-visible repro), and resolveText reads only the default member of an inline locale map (pre-existing and orthogonal).
  5. 范围外去向: Skills-docs: follow-up owed #26759. That card names skills/objectstack-i18n/SKILL.md:162 / :175. The seat added the ungoverned author page content/docs/ui/translations.mdx:71 / :85 and the stale header line packages/lint/src/validate-translation-references.ts:163 to it, following review ③7. These are missing sentences, not false ones, so they go with the follow-up and not on this head. Clause-②: yes and Metadata-protocol: added hold, and spec and cli go minor.
  6. 落地条款: the prose faces were checked sentence by sentence against the diff: the changeset and the four example bundle rows. The sibling i18n PRs (i18n: an app hook's business refusal has no translation key — a zh-CN user reads "A current clean version is required before signing." in English #26170, spec/metadata-protocol: a translation item named after its locale (zh-CN), as TranslationItemSchema.name says is conventional, is refused 400 INVALID_REQUEST by the item-name grammar at the save door #26128, spec: a single-word language name ("Chinese", "English", "Japanese") still passes as a translation locale — the #25822 fix refuses multi-word names only, though its changeset and refusal text say "a language name" is refused #26463) edit other regions of translation.zod.ts; whichever lands later merges main through os-regen-merge.sh. Ready and auto-merge go through the relay in this act, and the seat follows the PR to MERGED.

Generated by Claude Code

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