Skip to content

fix(spec): one label order on an overridden item, for all eight document translators - #30236

Queued
objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/project-thread-yfsy7x
Queued

objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/project-thread-yfsy7x

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Fixes #30193

Requested by Yibo Wang · project thread

Clause-②: yes (widening)
Metadata-protocol: none
Skills-docs: follow-up owed — skills/objectstack-platform/SKILL.md (the ADR-0146 "Customizing an installed package" paragraph, :128, says nothing of an override's label order) and skills/objectstack-i18n/SKILL.md (no line on an overridden item); owed once the serving half passes pairBundle, so a reader can see the order; the skills lane is paused
Prior rulings read: packagedbase,translatepage,override label → 0 hits; none; thread: none; repo: objectstack-ai/objectstack

维护者速读

  • 改了什么:.custom 配对包里覆盖基础包的条目(视图、仪表盘、报表、对象、页面、数据集、应用、文档手册八类),每个标签按同一顺序取文字:配对包自带的译文 → 覆盖项自己写的文字 → 基础包的译文;覆盖项没改动的标签照旧用基础包译文。
  • 为什么改:页面、数据集、应用、手册四类此前不比对基础包,覆盖项改过的标签会被基础包译文盖回;另外四类虽保住了覆盖文字,却读不到配对包自带的译文,因为解析器拿到的是一份合并后的词典,分不清译文出自哪个包。
  • 风险与回滚:只新增一个可选项 pairBundle;不传时输出与现在完全相同(已用既有 1024 个测试强制空配对验证),服务端也还没有传它,所以合并后用户可见行为不变;回滚即还原本 PR。
  • 先看:① packages/spec/src/system/i18n-resolver.ts 的 labelCatalogs / catalogFor(规则的唯一出处);② 同文件 translatePage 的页头标题判定;③ i18n-resolver.test.ts 末尾的八类型表驱动测试。
  • 两分钟验证:cd packages/spec && pnpm exec vitest run --project local src/system/i18n-resolver.test.ts -t "label precedence on an overridden item",预期 75 个测试全部通过。
  • 需要你做的:无,席位落地。

What changes

  • One new option, TranslateDocumentOptions.pairBundle: the translations the served item's paired .custom package ships itself, locale-keyed as bundle is.
  • One rule, one helper. labelCatalogs(bundle, opts) builds two catalogs per document: changed (the pair's own translations, nothing else) and unchanged (locale by locale, the pair's translation laid over the deployment catalog's). catalogFor(catalogs, changed) picks one from the comparison every translator already makes, valueOverridesPackagedBase against packagedBase. Every former "skip the catalog when the value diverged" gate is now "read the catalog this string earns", through these two functions, in all eight translators and every shared nested helper (bulk-action defs, form sections and title, the thank-you panel, global filters, report blocks, object list views, dataset members, page tab items, interface tabs).
  • translatePage, translateDataset, translateApp and translateBook read packagedBase (their options type widens from ResolveOptions to TranslateDocumentOptions), each string judged against the base's counterpart: page components by id and THE page's header, dataset members by name, navigation nodes by id, book groups by key. A page header title the override left as the base wrote it translates as the base's label.
  • No pairBundle, or one that answers none of the locale chain's codes: the catalogs are exactly the ones without it (same references). No packagedBase: nothing counts as changed. So no caller's output moves today: the server passes packagedBase only for object, dashboard, view and report (PACKAGED_BASE_ACCESSORS, form-routes.ts), and passes no pairBundle at all.

Precedence, per type (before and after)

"Changed" = the served string differs from the base's (valueOverridesPackagedBase). The catalog the server passes today is one merged dictionary per locale (see the design below), so "the merged catalog" means every package's code bundle and every authored translation row, deep-merged, with no way to tell the pair's from the base's.

Type Strings judged Before: a string the override changed Before: one it left alone After: changed After: left alone
view label, description, bulk-action defs (label, confirm text and button, params), form sections, form title, thank-you panel its own text (the pair's translation is never read) the merged catalog the pair's translation along the locale chain, else its own text per locale: the pair's translation, then the base's
dashboard label, description, widget title and description, global filter label and options its own text (pair never read) the merged catalog pair, else its own text pair, then base, per locale
report label, description, joined block label and description its own text (pair never read) the merged catalog pair, else its own text pair, then base, per locale
object label, pluralLabel, description, field label, help, option labels, list views its own text (pair never read) the merged catalog pair, else its own text pair, then base, per locale
page label, description, header title and subtitle, component copy, page:tabs items, interface tabs, form thank-you panel the base's translation (packagedBase not read) the merged catalog pair, else its own text pair, then base, per locale
dataset label, description, dimension and measure labels the base's translation (packagedBase not read) the merged catalog pair, else its own text pair, then base, per locale
app label, description, navigation node labels (any depth) the base's translation (packagedBase not read) the merged catalog pair, else its own text pair, then base, per locale
book label, description, group labels the base's translation (packagedBase not read) the merged catalog pair, else its own text pair, then base, per locale

Pair-provenance design

Measured at dfafae8: the resolver cannot tell a pair translation from the base's.

  • packages/rest/src/meta-item-read-gate.ts:2887 translationBundleOf builds { locale: i18n.getTranslations(locale) }.
  • packages/services/service-i18n/src/file-i18n-adapter.ts:189 getTranslations returns deepMerge(static, authored):
    • static = every package's code bundle, deep-merged in load order (loadTranslations, :199);
    • authored = every active translation sys_metadata row across ALL packages, deep-merged by item name, _packageId stripped (packages/core/src/fallbacks/authored-translation-sync.ts readAuthoredTranslationLayer).
  • One merged catalog per locale; provenance is lost twice. So the resolver needs a new input.

The option: TranslateDocumentOptions.pairBundle?: TranslationBundle (the same locale-keyed shape as bundle): the translations the served item's paired .custom package itself ships, and nothing else.

The string Read No hit
changed by the override (valueOverridesPackagedBase true) pairBundle only, along localeChain the override's own value (the base catalog is never read)
not changed, or no packagedBase per chain locale: pairBundle, then the deployment catalog the authored value
  • Interleaved per locale (pair zh-CN, catalog zh-CN, pair en, catalog en) for an unchanged string, so a pair that ships only en never displaces the base's zh-CN on an untouched label.
  • A pair that answers none of the chain's locales short-circuits to the catalogs without it, so an empty pair keeps even the no-bundle reference identity.

Accepted deviations

  • An object's validations[].message stays on the plain merged bundle, and inline object actions (translateAction) stay as today (seat ruling on the design, item 2). Reason: the objectql rule evaluator (packages/objectql/src/validation/rule-validator.ts:3785) answers the same message from i18n.t() with no pair input, and the pinned cross-path invariant is refusal text = served text. action is not one of the eight types and has no packagedBase.
  • What the validations deviation means for a user (contract review 6648581045, should-fix 2): on an overridden object, a validations[].message the override reworded never takes the pair's translation while the object's labels do, and a message it left alone reads the merged catalog, where the pair's row can lose to the base's by merge order. Lifting it needs a pair input on the objectql evaluator and the resolver together: An overridden object's validation message never takes its .custom pair's translation, while the object's labels do #30245.

Server-half contract for #30062

For a served item that is a declared .custom override, on item, list and container reads alike, for all eight types:

  • packagedBase = the BASE package's item (pre-override: the document the override replaces), never the override itself;
  • pairBundle = { locale: data } built from that .custom package's own translations only: its code-shipped translation bundle and its authored translation rows, both of which are the pair's own;
  • keep passing packagedBase for object, dashboard, view and report exactly as today, on every served item (PACKAGED_BASE_ACCESSORS, form-routes.ts :385 :421): the object reads it for extension-folded field labels too (ADR-0029 D9.2a, i18n: a tenant's rewrite of an objectExtensions-contributed validation rule message still loses to that package's translation, on the refusal and on /meta #27559), so it is not override-only. Add the same accessor for page, dataset, app and book;
  • omit pairBundle for an item that is not a declared override.

Seat correction, body r1 (contract review 6648581045, should-fix 1): the first version said "omit both for an item that is not an override", which would have regressed the four types the server serves with packagedBase today.

Carried

Tests

  • New: one top-level describe in packages/spec/src/system/i18n-resolver.test.ts, "label precedence on an overridden item", table-driven over all eight types through the dispatcher (translateMetadataDocument). Per type, a top-level label and one or two nested ones (bulk-action label, widget title, block label, field and option label, component copy and tab item, measure and dimension, nested nav node, group), each row of the table above: the pair wins; with no pair translation (no pair, and a pair shipping nothing for the item) the override's own label beats the base's translation in zh-CN and in the source locale; an unchanged label keeps the base's translation beside a changed one; an untouched label takes the pair's translation; the per-locale case (pair ships only en, an untouched label keeps the base's zh-CN); no packagedBase = nothing changed; an empty pair is the same answer as none, byte for byte; no input mutated. Plus the page header restatement case. 75 tests.
  • pnpm exec vitest run --project local --maxWorkers=2 over the 25 spec test files importing the resolver, under os-verify-lock: Test Files 25 passed (25), Tests 1099 passed (1099), VERDICT command-exit 0.
  • The byte-identical claim, run on every existing fixture: with an in-chain empty pair forced onto every call (pairBundle ?? { each chain code: {} }), the 25 files gave 1023/1024; the one red was a reference-identity assert with no bundle (equal value, new object), which the short-circuit above closes.
  • Ablations (each restored, verified by git diff --stat): the pair never read → 41 of the 75 red (5 rows × 8 types, plus the page header pair case); the interleave dropped → 8 red, exactly the per-locale case once per type; changed strings reading the merged catalog → 16 red.
  • pnpm --filter @objectstack/spec typecheck (build, scripts and test layers) exit 0.

Verification

  • pnpm --filter @objectstack/spec build under the lock: exit 0. pnpm check:generated: all 14 generated artifacts up to date (api-surface included), exit 0.
  • Dispatch gates: node scripts/pm/dispatch-gates.mjs --commands derived 78 commands at 8476878; all 78 ran and exited 0. Four first exited 3 (prerequisite not met, nothing measured) and were re-run green after git fetch --depth=1 of the teardown self-test's pinned fixture commit and a pnpm exec turbo run build: check-plugin-teardown-shape --self-test, check:doc-formula-expressions, check:dual-build-cjs-loads, check:lean-entry-closure. --ran reconciliation: 78 derived, 78 run, 0 NOT-MEASURED, 0 UNRUN.
  • GITHUB_EVENT_NAME=pull_request node scripts/check-changeset-no-major.mjs --base origin/main --event with this body: exit 0; the level axis reads Clause-②: yes (widening) and Metadata-protocol: none, a non-protocol change at patch.

Generated by Claude Code

…e eight document translators

A translation the paired .custom package ships wins, then the override's
own text, then the base's translation (ADR-0146 D2). page, dataset, app
and book now take TranslateDocumentOptions and judge every string against
packagedBase, as view, dashboard, report and object already did. The new
optional pairBundle option carries the pair's own translations; without
it every translator answers as before.

Claude-Session: https://claude.ai/code/session_01WGufzhytXa3Yi3uzE5pvuv
Co-authored-by: Claude <noreply@anthropic.com>
…slators

One table-driven block in i18n-resolver.test.ts, the same rows for view,
dashboard, report, object, page, dataset, app and book: the pair's
translation wins, with none the override's own label beats the base's
translation, an unchanged label keeps the base's translation beside a
changed one (nested labels included), and a pair that ships only `en`
leaves an untouched label on the base's zh-CN. An empty pairBundle is
pinned to give the same answer as none.

labelCatalogs now hands back the catalogs it would without a pair when
the pair answers none of the chain's locales, so an empty pair keeps the
no-bundle reference identity too.

Claude-Session: https://claude.ai/code/session_01WGufzhytXa3Yi3uzE5pvuv
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation protocol:system tests tooling labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 40 documentable anchor(s).

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

  • content/docs/automation/flows.mdx (via inlineHelpText (literal, a string literal in translateObject))
  • content/docs/data-modeling/field-types.mdx (via inlineHelpText (literal, a string literal in translateObject))
  • content/docs/data-modeling/fields.mdx (via inlineHelpText (literal, a string literal in translateObject), inlineTitle (literal, a string literal in translateObject))
  • content/docs/data-modeling/object-extensions.mdx (via inlineHelpText (literal, a string literal in translateObject))
  • content/docs/deployment/validating-metadata.mdx (via globalFilters (literal, a string literal in packagedPart))
  • content/docs/frontend/plugins/plugin-grid.mdx (via bulkActionDefs (literal, a string literal in packagedPart))
  • content/docs/frontend/plugins/plugin-view.mdx (via bulkActionDefs (literal, a string literal in packagedPart))
  • content/docs/getting-started/quick-reference.mdx (via inlineHelpText (literal, a string literal in translateObject))
  • 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/translations.mdx (via inlineHelpText (literal, a string literal in translateObject))
  • content/docs/ui/views.mdx (via bulkActionDefs (literal, a string literal in packagedPart))

⛔ 6 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-1.mdx (via relatedListTitle (literal, a string literal in translateObject))
  • content/docs/releases/v17/17-5.mdx (via inlineHelpText (literal, a string literal in translateObject))
  • content/docs/releases/v17/17-6.mdx (via translatePage (symbol, a top-level function), globalFilters (literal, a string literal in packagedPart))
  • content/docs/releases/v17/17-7.mdx (via bulkActionDefs (literal, a string literal in packagedPart), inlineHelpText (literal, a string literal in translateObject))
  • content/docs/releases/v18/18-2.mdx (via inlineHelpText (literal, a string literal in translateObject))

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
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 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; 103 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 — 170 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 afc761c35fa475d1a1a48cda5982be994a7f079f → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json afc761c35fa475d1a1a48cda5982be994a7f079f

⚠️ 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 afc761c35fa475d1a1a48cda5982be994a7f079f → 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

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Contract-review-claim: session_01WGufzfgvGexKn9vLHsy89K · head 8476878 · 2026-11-05T02:55Z

Full review of the first head at CONTRACT_REVIEW_TIER (a widening: the new pairBundle option and the pair rule across the eight translators).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8476878165579f66320292abdddcdbaa156d6149
Local-runs: none
Scope: packages/spec/src/system/i18n-resolver.ts (full diff vs merge-base dfafae8, plus the head's helper bodies: pickData :546, localeChain :578, packagedPart :2114, packagedObjectField :2143, valueOverridesPackagedBase :4522, translateObjectValidations :4668, every lookup* the eight translators call), packages/spec/src/system/i18n-resolver.test.ts (the new describe, 75 tests), .changeset/30193-override-label-pair-precedence.md; at origin/main: packages/rest/src/meta-item-read-gate.ts :2887 :2954 :3040-3115 :3165-3175, packages/rest/src/form-routes.ts :385 :421, packages/services/service-i18n/src/file-i18n-adapter.ts :189, packages/objectql/src/validation/rule-validator.ts :3785, ui/packages/i18n/src/useObjectLabel.ts :482 :489, ui/packages/app-shell (AppHeader.tsx :750, appReportIndex.ts :324, useRecentItemLabel.ts :122, servedNavText.ts), docs/adr/0146 D2/D3, scripts/check-changeset-no-major.mjs :880-1004, skills/objectstack-platform/SKILL.md :128, skills/objectstack-i18n/SKILL.md, AGENTS.md; card #30193 and its 4 comments, card #30062 body and ruling 6631250894, PR #30236 body and 2 comments, check-runs on the head.

① Derived judgments

  1. Rule fidelity: holds. One helper pair, labelCatalogs (i18n-resolver.ts :83) and catalogFor (:107), and every string of the eight translators reads catalogFor(catalogs, valueOverridesPackagedBase(the matching base node, attr, value)): view :333-342 and bulk-action defs :172, form sections :227, entries :258, form title :279, thank-you panel :300 (through translateFormSubmitBehavior :1469); dashboard :530-535, global filter :495 and options :508, widgets :555-566; report :750-756, blocks :723-730; object scalars :1122 (scalarOverridesPackagedBase, the same predicate :4501), field label :1148-1155, help :1184-1193, inlineTitle :1199, relatedListTitle :1211, options :1223-1228, listViews :1096-1103; page label/description :927-938, component copy key by key :963-972, tab items :836, thank-you :998-1000, header title/subtitle :1009-1014, interface tabs :1066; dataset :641-648 and members :598; app :450-455 and nav nodes :463-464; book :684-689 and groups :697. The only raw-bundle reads left inside the eight are the two accepted deviations: translateObjectValidations (diff :1239) and the inline-action translateAction (:1728-1753). View columns and report columns were never translated at the base, so nothing was skipped. Every nested node is matched by its stable key (name, id, field, value, key), never by index: dataset members by name :597, book groups by key :696, app nav by id with a depth-first first-wins map :398-412, page components by id through the same walk and collision arbitration the translator reads the served page with :869-884, tab items by value :834, interface tabs by name :1065.
  2. Changed detection: valueOverridesPackagedBase (:4522) is false with no base, false for a non-string or empty value, else base[key] !== value; applied per string against the matching base node. An item the override added gets {} from packagedPart/packagedObjectField/packagedNavNodes/packagedPageParts (base known, no counterpart), so every non-empty string on it counts as changed: pair only, else its own text. An item the base has and the override dropped is absent from the served document and is never touched. An inline locale map is never changed, so it keeps the catalog reading it has today.
  3. Byte identity: with no pairBundle, labelCatalogs returns { unchanged: bundle, changed: undefined } (:88, the same reference), and every lookup answers undefined on an undefined catalog (pickData :549 and the added if (!bundle) guards :190 :206 :315 :804), so a changed string falls back to its own value exactly as the former ? value : lookup arms did; I traced view, dashboard, report, object (label, the help slot restructure, options) and the page header (resolvePageHeaderTitle :774-791 with the dropped early return) and found no divergence. The same holds for packagedBase without a pair on the four former types. The no-bundle short-circuit (:94, a pair answering no chain locale) returns the same references and is pinned (test.diff :286-291). No caller moves today: the server passes packagedBase only for object, dashboard, view and report (PACKAGED_BASE_ACCESSORS meta-item-read-gate.ts :2954, read through packagedObjectBaseOf :2976 on item :3109, list :3050 and view-container :3173 reads; form-routes.ts :385 :421) and no caller anywhere in packages/, ui/ or cli passes pairBundle; page, dataset, app and book receive packagedBase: undefined.
  4. Locale chain: unchanged is keyed by every chain code with overlay(pickData(bundle, code), pickData(pair, code)) ?? {} (:99-103), so a lookup's chain walk reads pair zh-CN, catalog zh-CN, pair en, catalog en; changed is the pair itself, walked by the same localeChain and BCP-47 ladder (pickData :546), so a pair shipping only en answers a zh-CN viewer's changed string with its en text when en is in the declared chain, and with the override's own text otherwise (default-locale readings skip the chain, [Decision] the default locale's text — is the authored label the default-locale source (a default-locale request stops there), or must every supported locale ship a bundle? os i18n check says the former, localeChain does the latter; the resolver's literal ['en'] default rides on the answer #15711, unchanged). The {} for a code neither bundle answers stops pickData's variant expansion from re-resolving against another chain code, which keeps the unchanged catalog exactly the merged bundle's reading.
  5. Pins: per type (test.diff :198-303) the pair wins top-level and nested :228, the override's own label beats the base's translation with no pair and with an empty pair in zh-CN and en :233, an unchanged label keeps the base's translation beside a changed one :243, an untouched label takes the pair's :252, the per-locale interleave :257, no packagedBase :265, byte-identity with an empty and an off-chain pair with and without a base plus reference identity :272, no input mutated :295; nested slots per type cover bulk action, widget, block, field and option, component copy and tab item, measure and dimension, nested nav node, group; page header restatement :305-333. The three ablations named (pair never read, interleave dropped, changed strings reading the merged catalog) each have rows that can only pass through the new code; the counts (41, 8, 16) are consistent with the rows per type.
  6. Deviations: both real and stated in the body and the changeset. The validations one leaves a visible inconsistency the body does not name (should-fix 2). Studio: the Console header finding is accurate at origin/main (useObjectLabel.ts :482 pageLabel and :489 reportLabel resolve the catalog key first; callers AppHeader.tsx :750, appReportIndex.ts :324, useRecentItemLabel.ts :122), so card acceptance 3 is not met by this PR (should-fix 3).
  7. Body vs code: the precedence tables, the pair-provenance design (translationBundleOf :2887, getTranslations :189, readAuthoredTranslationLayer, rule-validator :3785) and the Studio file:lines all check out; the Skills-docs owed line names the right paragraph (objectstack-platform SKILL.md :128) and the i18n skill has no line on overridden items; the 维护者速读 matches the code and the report. Check-runs on the head at review time: 26 success, 9 skipped, 26 in_progress, 0 failed.

② Semver level

@objectstack/spec patch is right under Ruling A (check-changeset-no-major.mjs :901-931, AGENTS.md Post-Task step 3): the level follows the Metadata-protocol: line alone, and none is correct (no authorable key, value, default or meaning moves). Clause-②: yes (widening) is the right meaning: a new optional member on the exported TranslateDocumentOptions and four third-parameter types widened from ResolveOptions to its subtype, additive for every caller; api-surface/system.json lists exports only, so no regeneration is owed. ADR-0087 is correctly not needed: nothing is removed, renamed or re-shaped. Check Changeset is green on the head.

③ Boundary flags

④ Design judgment

The design is sound: one comparison, two catalogs built once per document, the interleave at the right grain, and the page header restatement reasoned through (a title the override left as the base wrote it translates as the base's label, test.diff :320). Three should-fix items, no must-fix:

Implemented-by: claude/project-thread-yfsy7x
Reviewed-by: session_01WGufzfgvGexKn9vLHsy89K

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

维护者速读

席位意见:fable 契约复审一轮 PASS,没有必须修的。三条建议都已处理:

席位核对 — ACCEPT · domain:spec#3 · head 8476878165579f66320292abdddcdbaa156d6149

This branch has not been deployed

No deployments
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/xl tests tooling

Projects

None yet

1 participant