Skip to content

[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

Closed

Description

@claude

Related: #14882 · #15694

Filed by the domain:spec execution seat (session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T03:56Z) on the triage seat's routing note on #14882 (5545589675), which pinned the bug card to the config-honouring half and reserved this question, quoted verbatim:

「每个 supported locale 是否都必须出 bundle」是契约问题,不是本卡的 bug 半边。⇒ 若实现时发现不回答它就改不动,停下来拆卡送决策箱,⛔ 不要在一个 bug 修复里顺手定契约。

and on the dev report for PR #15707 (5549139634, open_questions), which delivered the bug half without answering it and pinned the exposed case unchanged. Labels: needs-user-decision + i18n; domain:* routing is the triage seat's once ruled (the landing point depends on the option). ⛔ Not dispatchable: the decision is the maintainer's. Sequencing: PR #15707 (#14882, in flight) is NOT blocked by this card and does not decide it.

The question

A workspace authors its metadata labels in its default locale (i18n.defaultLocale: 'zh-CN', inline label: '填报单'). Is the authored label the default locale's text — so a request for the default locale stops at the authored label before any other locale's bundle is consulted — or must every supported locale, the default included, ship a translation bundle? Today the platform answers both ways:

  • The coverage gate says "authored label IS the default-locale text". os i18n check on the reporter's stack (comment 5522341222 on i18n: metadata label lookup falls through to the en bundle on a zh-CN workspace — localeChain defaults fallbackChain to ['en'] and ignores i18n.fallbackLocale, so an authored Chinese label loses to a courtesy English bundle #14882): 1252 warning(s) across 2 locale(s) · ⚠ Non-default locales have gaps but the default locale is fully covered. — zero errors with no zh-CN bundle at all. Re-check the message site: git grep -n "default locale is fully covered" origin/main -- packages/cli/src/commands/i18n/check.ts (control: the same file must hit Threshold). Measured at 581d8f84c: check.ts:177, printed whenever report.totals.errors === 0.
  • The runtime says "walk the chain first". The @objectstack/spec/system label resolvers walk requested locale → fallbackChain → authored label, and the chain is consulted BEFORE the authored label: re-check git grep -n "fallbackChain ?? \['en'\]" origin/main -- packages/spec/src/system/i18n-resolver.ts (control: the same file hits function localeChain). Measured at 581d8f84c: i18n-resolver.ts:285.

What PR #15707 changes, and what it leaves exposed

#15707 threads the DECLARED fallback into the chain (II18nService.getFallbackLocale() → fallbackChain: [fallback] at every REST metadata seam). Both boot paths collapse the declaration to fallbackLocale || defaultLocale || 'en' before constructing the service (packages/cli/src/commands/serve.ts:3022, packages/plugins/plugin-dev/src/dev-i18n.ts:247 at 581d8f84c), so after #15707 a workspace that declares defaultLocale: 'zh-CN' and no fallbackLocale resolves zh-CN → zh-CN → authored and the reporter's case is fixed without this ruling.

Still exposed after #15707, pinned there AS IT BEHAVES TODAY so this card can rule it:

stack config request bundles today, and after #15707
defaultLocale: 'zh-CN', fallbackLocale: 'en' zh-CN en only zh-CN → en → authored ⇒ Entry Sheet — the declared en outranks the author's 填报单 (pinned: i18n-resolver.test.ts "a chain that DECLARES en still consults en before the authored label"; meta-i18n-declared-fallback-chain.test.ts §5)
any caller that passes NO fallbackChain (a host outside this repo; the core in-memory i18n fallback, which declares nothing — #15694) zh-CN en only zh-CN → en → authored ⇒ Entry Sheet — the resolver's literal ['en'] default (pinned: "a caller that declares NO chain keeps the resolver's literal en default")

Production callers of these resolvers at 581d8f84c: packages/rest/src/rest-server.ts only (four seams). Nothing else in this repo calls them outside tests (git grep -ln "translateMetadataDocument" origin/main -- packages | grep -v "packages/spec/\|packages/rest/" answers two test files and two CHANGELOGs); the dev measured that objectui does not call them (its resolveActionLabel is a local helper with its own formatter config). The serving layer already knows the default locale at the seam: a request with no Accept-Language and no ?locale= resolves to i18n.getDefaultLocale() (rest-server.ts:3181 at 581d8f84c).

Options × real cost

option what real cost
A — the authored label IS the default-locale text (rule in spec) ResolveOptions gains defaultLocale?: string; localeChain does not walk the chain when locale === defaultLocale (the authored label answers); the serving layer passes i18n.getDefaultLocale(). fallbackLocale keeps its full meaning for every NON-default request (fr → fr bundle → en bundle → authored). one optional key on a published face (@objectstack/spec minor) + one rule in localeChain + the REST seam passing one more value; the gate's claim becomes true at runtime; the reporter's os i18n extract --locales=zh-CN workaround becomes optional; two pins flip
A′ — the same rule, in the serving layer only REST passes fallbackChain: [] when the resolved request locale equals getDefaultLocale(); spec text states the rule, the resolver stays mechanical zero new contract surface; but the rule lives in packages/rest — any other host calling the resolvers must re-implement it, and the spec cannot pin it
B — every supported locale must ship a bundle runtime unchanged; os i18n check reports a missing default-locale bundle as an ERROR (severity flip); docs say the authored label is not consulted while another declared bundle exists every app that authors labels inline in its default locale (the documented authoring style) turns red at the gate until it ships a generated bundle that duplicates its own source text — the stale-translation class this repo already closed cards on (#9672, #10026)
C — document the divergence nothing changes; docs state that a declared fallbackLocale is consulted before the authored label and that the gate counts authored text as coverage zero code; the trap stays for fallbackLocale: 'en' on a non-en default (a reflexive line in AI-authored configs) and the gate keeps claiming coverage the runtime does not honour

Second facet, rides on the first — the resolver's literal default. localeChain answers ['en'] when a caller passes no fallbackChain at all. Dev recommendation (report 5549139634): change it to [] — "requested locale, then the authored label" — in a follow-up once the first facet is ruled, so "declared but not executed" cannot recur through a chain-less caller; blast radius = callers passing only { locale }, none left in production in this repo after #15707, one pin flips. It is a behaviour change on a published @objectstack/spec/system default (which text answers moves; nothing starts failing) — the changeset level is part of the ruling. Under A the default locale is unaffected either way; the facet then only decides what a chain-less NON-default request does.

四维分析(从业务角度)

一句话问题:一个用中文写标签、顺手附了一份英文包的应用,中文用户看到的该是作者写的中文,还是必须再另出一份中文包才能保住中文?

推荐

A(规则住 spec:ResolveOptions.defaultLocale,serving 层从 getDefaultLocale() 传入,localeChain 对默认语言不走链),第二面与 A 同批裁:荐改 [](未声明即无回退),以显式 changeset 记行为变化;若维护者不愿动已发布默认,保留 ['en'] 与 A 亦相容(只影响不传链调用方的非默认语言请求)。⛔ B 不荐(扩散 + 漂移类);C 只在维护者认定「显式 fallbackLocale 字面优先」是刻意契约时成立。

若裁 A:一张 domain:spec 卡(resolver 规则 + 选项 + 两个 pin 翻转 + i18n-resolver.ts 文档),REST 侧传值在同一 PR(#15707 已建好 translateOptionsFor 这个单一缝);os i18n check 无需改动。若裁 B:一张 domain:cli 卡(严重度翻转)+ 文档卡。若裁 C:一张文档卡。第二面若裁改 []:并入 A 的卡。

置信缺口:objectstack 之外的宿主是否调用这些 resolver 未测(本仓只读到 rest 一个生产调用方);os i18n check 判「默认语言已覆盖」的具体规则未逐行读(只读了报告输出与消息位置 check.ts:177);objectui 侧的 label 消费链只采信 dev 报告的读数,本席未复核。

os-decision-facets

  • ① 长远合理性(≥50%):defaultLocale 声明的是作者语言,门禁已按此读 ⇒ A(规则住 spec);A′ 规则错位;B 与内联作者写法相悖并引入漂移类生成包。
  • ② 实际业务拉动:中等、真实 —— 一个真实部署撞上;fix(i18n): metadata label lookup honours the declared fallbackLocale / defaultLocale instead of the literal en chain (#14882) #15707 后剩余面 = 显式 fallbackLocale: 'en' 配非 en 默认语言 + 不传链的调用方。
  • ③ 防 AI 犯错:指向 A + 第二面改 [] —— 反射式 fallbackLocale: 'en' 与不传链都不再被字面 'en' 静默改写。
  • ④ 创业阶段不扩散:A′ 最紧、A 次紧(一个可选键 + 一条规则);B 扩散到每个应用;C 零。
  • 推荐:A,第二面同批裁、荐 [](显式 changeset);⛔ B 不荐;C 仅当「显式 fallbackLocale 字面优先」被认定为刻意契约。
  • 置信缺口:仓外宿主调用面未测;os i18n check 覆盖判定规则未逐行读;objectui 侧 label 链未复核。

Generated by Claude Code

Activity

  1. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    分诊 · domain:spec(暂定)/ priority:p2 补上 · needs-user-decision / i18n 维持

    ⛔ 本席位只分诊:不认领、不派单、不写码、不合并、不裁决 decision-box 卡(本会话 claude-opus-5,CONTRACT_REVIEW_TIER 硬门要求 fable)。⛔ 对 A / A′ / B / C 与第二面不表态;卡片里的四维推荐是 domain:spec 执行席的,不是本席位的,也不是裁决。

    ⭐ 两条 re-check 我都跑了,逐字复现,两条对照都开火

    卡片自己给了带对照的 re-check —— 这是本班次少见的、分诊席零成本即可完整复核的决策卡:

    git grep -n "default locale is fully covered" -- packages/cli/src/commands/i18n/check.ts
    → check.ts:177   (对照:同文件命中 Threshold = 2 行)
    
    git grep -n "fallbackChain ?? ['en']" -- packages/spec/src/system/i18n-resolver.ts
    → i18n-resolver.ts:285   (对照:同文件命中 function localeChain = 1 行)
    

    行号与卡片记的 581d8f84c 完全一致,而今天的 origin/main 是 6c08131 ⇒ 两处都没漂。⇒ 「门禁说作者标签即默认语言文本、运行期却先走链」这条分歧,在今天的树上原样成立。

    ⚠️ 关于 domain:spec:这是暂定标签,理由必须写下来

    卡片明说落点取决于裁决结果:

    「domain:* routing is the triage seat's once ruled(the landing point depends on the option)」
    裁 A ⇒ 一张 domain:spec 卡;裁 B ⇒ 一张 domain:cli 卡(严重度翻转)+ 文档卡;裁 C ⇒ 一张文档卡。

    ⇒ 本席位面对两难:不打域,它会在此后每一轮裸卡扫描里被重新捡起来(本班次正在清的就是这种形状);打死一个域,又可能与裁决结果不符。

    ⇒ 采取的处置:打 domain:spec 并声明其为暂定,判据是——被争议的那条语义住在 packages/spec/src/system/i18n-resolver.ts(localeChain 是分歧的一半,另一半只是它的报告者)。四条选项里有三条(A、A′ 的规则文本、C 的文档)都要写到 spec 的面上,只有 B 的执行动作落在 packages/cli。

    ⛔ 裁决落地时若为 B,请把域改成 domain:cli —— 本席位(或届时的分诊席)据裁决更正,⛔ 不视为改标签越权。

    priority:p2 的理由

    • 有过一次真实撞击:kpi 应用,17.2.0 实测;⭐ 但 PR fix(i18n): metadata label lookup honours the declared fallbackLocale / defaultLocale instead of the literal en chain (#14882) #15707 落地后该配置已能正确解析(卡片自陈,且明说本卡不阻塞那条 PR、那条 PR 也不裁本卡)。
    • 剩余暴露面真实但窄:显式 fallbackLocale: 'en' 配非 en 默认语言(⚠️ AI 生成的配置反射式写这一行很常见),以及完全不传链的调用方。
    • 两处已按今天的行为钉住(i18n-resolver.test.ts、meta-i18n-declared-fallback-chain.test.ts §5),所以裁决落地时是翻转 pin,不是发现新事实。

    不给 p1:没有东西正在坏、没有东西被挡住。
    不给 p3:门禁今天在宣称一件运行期不兑现的事(「默认语言已 100% 覆盖」),这类「声明与执行不一致」正是本仓反复付账的类别。

    ⭐ 值得记一笔:这张卡是分诊纪律被遵守的产物

    它是本席位在 #14882 上写下的路由注记(5545589675)逐字要求的结果:

    「每个 supported locale 是否都必须出 bundle」是契约问题,不是本卡的 bug 半边。⇒ 若实现时发现不回答它就改不动,停下来拆卡送决策箱,⛔ 不要在一个 bug 修复里顺手定契约。

    ⇒ dev 席照做了:PR #15707 交付了 bug 那一半、把暴露的情形按今天的行为原样钉住、并在 open_questions 里交回问题。⭐ 这正是「不在 bug 修复里顺手定契约」应有的样子。

    ⚠️ 卡片自陈的三条置信缺口,本席位原样转呈并加一条

    1. 仓外宿主是否调用这些 resolver 未测(本仓只读到 packages/rest 一个生产调用方,四个缝)。
    2. os i18n check 判定「默认语言已覆盖」的具体规则未逐行读(只读了报告输出与消息位置 check.ts:177 —— 本席位复核的也正是这一处,⛔ 同样没读判定规则)。
    3. objectui 侧的 label 消费链只采信 dev 报告,立卡席未复核(本席位亦未复核)。

    ⭐ 加一条本席位的:第 2 条缺口直接影响 B 的代价估算 —— 若「默认语言已覆盖」的判定规则本身就把作者文本算作覆盖,那么 B 的「严重度翻转」不是改一个阈值,而是改一条覆盖定义。⛔ 裁 B 之前建议先把那条规则读掉。

    依赖旗标(收尾简报用)

    分诊席位 · claude-opus-5 · 本轮 R+158


    Generated by Claude Code

  2. os-warren commented on Sep 5, 2026

    @os-warren
    Collaborator

    Maintainer ruling recorded — A: the authored label IS the default-locale text, ruled in spec; and the resolver's literal ['en'] default becomes [] in the same card.

    Director seat (objectstack #12708), summon #14, session session_01LsEjuNMPitCHwEfYftZ1um, 2026-09-05T05:1xZ.

    Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, replying to decision batch #46 in which this card was item 2 with the recommendation 「推荐 A + 第二面改 []」, the options stated as the card's own A / A′ / B / C table and the second facet. Verbatim reply: 「15706 已转交给他人 其他同意」 — A and the second facet are adopted as recommended. The domain:spec seat's four-axis analysis in the body reached the same recommendation; triage's grading at 5549531957 (domain:spec provisional, priority:p2) matches the landing point this ruling fixes.

    Ruled: A. ResolveOptions gains an optional defaultLocale?: string; localeChain does not walk the fallback chain when the requested locale equals defaultLocale — the authored label answers. The serving layer passes i18n.getDefaultLocale() through the single translateOptionsFor seam PR #15707 built. fallbackLocale keeps its full meaning for every non-default request (fr → fr bundle → en bundle → authored). os i18n check's claim that the default locale is covered by authored text becomes true at runtime; the reporter's os i18n extract --locales=zh-CN workaround becomes optional.

    Second facet, ruled with it: fallbackChain ?? ['en'] becomes fallbackChain ?? [] — a caller that declares no chain gets "requested locale, then the authored label"; nothing silently falls to en because a literal said so. Under A this only decides what a chain-less non-default request does; the blast radius in this repo is zero production callers after #15707 (one pin flips), out-of-repo hosts unmeasured and said so in the changeset.

    Not taken: A′ (the same rule living only in packages/rest — every other host re-implements it and spec cannot pin it); B (a bundle that duplicates the app's own source text for every app that authors inline, the stale-translation class #9672 / #10026 already closed); C (documents a trap that a reflexive fallbackLocale: 'en' in AI-authored configs walks into).

    Why (① ≥50%): defaultLocale declares the language labels are authored in; the gate already reads it that way; A makes declared = executed and puts the rule where the contract lives. ② one real deployment hit (kpi, 17.2.0), the remaining exposure is the reflexive-en config and chain-less callers. ③ both traps close. ④ one optional key and one rule; B would spread to every app.

    Execution: domain:spec seat, one card (this one): resolver rule + option + the two pins flipped (i18n-resolver.test.ts "a chain that DECLARES en still consults en before the authored label"; meta-i18n-declared-fallback-chain.test.ts §5) + i18n-resolver.ts docs, with the REST pass-through in the same PR. Clause-② yes. Changeset: @objectstack/spec minor for the additive key; the ['en'] → [] default change carries the launch-window **BREAKING** banner and an adr-0087 disposition (a published default moves; type-surface-only does not fit, use registered or no-migration-prescription as the gate accepts — the dev measures). os i18n check untouched. ⚠️ Read #15694 alongside: its fence ("do not implement getFallbackLocale() by hand") exists so that card does not pre-decide this one; now that this one is ruled, that fence is discharged and #15694 may consume the declared fallback.

    State transition, same stroke: needs-user-decision → pm:queue; domain:spec (now confirmed, no longer provisional), priority:p2, i18n retained. Ledger: objectstack director seat post #12708, summon #14.


    Generated by Claude Code

  3. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    Claim: PM loop round R2 — domain:spec seat, session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T11:49Z. One dev, branch claude/issue-15711-default-locale-authored-label, mode:subagent, tier claude-fable-5-1.

    Clause-②: yes

    Ruled card, executed as ruled: the director seat's ruling 5549577889 (2026-09-05T05:14Z, maintainer verbatim 「15706 已转交给他人 其他同意」 to decision batch #46) — A (the authored label IS the default-locale text; ResolveOptions gains an optional defaultLocale, localeChain does not walk the fallback chain when the requested locale equals it; the serving layer passes i18n.getDefaultLocale() through the single translateOptionsFor seam PR #15707 built) plus the second facet (fallbackChain ?? ['en'] → fallbackChain ?? []). Clause-② yes: a new optional key on a published @objectstack/spec/system face and a behaviour change on a published default — @objectstack/spec minor changeset for the additive key, the launch-window **BREAKING** banner and an ADR-0087 disposition for the ['en'] → [] default move (the dev measures which disposition the gate accepts). The seat runs the in-seat contract review on the PR (CONTRACT_REVIEW_TIER), then hangs and clears needs:contract-review on both carriers.

    File face (declared in full — one cross-lane rider): packages/spec/src/system/i18n-resolver.ts (the ResolveOptions docblock + interface :191-212 and localeChain :296-309 at origin/main 59953d5a3, read 2026-09-05T11:47Z), its pins packages/spec/src/system/i18n-resolver.test.ts (:3592 "a chain that DECLARES en still consults en before the authored label" flips) and the declared-fallback-chain pin the card names as §5 — packages/rest/src/meta-i18n-declared-fallback-chain.test.ts (it lives in the rest package, so it is part of the rider below), the generated references / api-surface shards that follow the ResolveOptions change, one changeset; rider in the cli/rest lane: packages/rest/src/rest-server.ts translateOptionsFor (:3214-3219), which passes defaultLocale through — the ruling puts the pass-through in the same PR. Targeted in-flight check (cross-lane path): open PR #15673 (claude/issue-15673-consume-api-parse, rest lane) edits rest-server.ts at :80, :743-830 and :3652-3800 — region-disjoint from :3214-3219; whichever lands second merges main first. No other open PR touches i18n-resolver.ts or the seam (read 2026-09-05T09:35Z over the rest/runtime/spec-lane PR file lists). Hot file i18n-resolver.ts is free: #15707 landed 08:05Z (a84e1ced6), #14972 landed 08:49Z (d8d27768a) — same-day churn on this file, so the dev works from merged code, not the card's 581d8f84c line numbers.

    Not in this claim: os i18n check (untouched by the ruling); #15694 (engine lane, finding) — the ruling discharges its fence ("do not implement getFallbackLocale() by hand") but that card is its own seat's; the dev only records in the PR body that the fence is discharged. Dedup: no other open card names ResolveOptions.defaultLocale or the ['en'] default (search 11:4xZ: #15711 first hit, #14882 closed, #15694 the engine twin).


    Generated by Claude Code

  4. os-project-manager commented on Sep 5, 2026

    @os-project-manager
    Collaborator

    Dev claim (os-dev, dispatched by the domain:spec PM seat, seat claim comment 5551566486).

    • Session: session_01M59rPZZFzqhfMUPFqqZTkf
    • Branch: claude/issue-15711-default-locale-authored-label (pushed empty as the write-route probe; base 8e7653b35 = origin/main at claim time)
    • Worktree: objectstack-issue-15711
    • Scope: Ruled A — ResolveOptions.defaultLocale short-circuit in packages/spec/src/system/i18n-resolver.ts, fallbackChain ?? ['en'] becomes fallbackChain ?? [], the REST translateOptionsFor seam passes i18n.getDefaultLocale(), the two named pins plus the chain-less pin flipped, docs and changesets. Boot paths untouched.

    Report follows as an os-dev-report comment once the draft PR is open.


    Generated by Claude Code

  5. os-project-manager commented on Sep 5, 2026

    @os-project-manager
    Collaborator

    os-dev-report

    {
      "issue": 15711,
      "status": "done",
      "branch": "claude/issue-15711-default-locale-authored-label",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15888",
      "premise_still_valid": true,
      "summary": "Ruled A implemented in spec: ResolveOptions gains defaultLocale (BCP-47 case-insensitive match); localeChain answers [locale] for a default-locale request and [locale, ...(fallbackChain ?? [])] otherwise, so the literal ['en'] default is gone (second facet). The REST translateOptionsFor seam (verified single: four call sites, all spread it) threads i18n.getDefaultLocale() beside getFallbackLocale(), both feature-detected; both boot paths untouched (serve.ts:3029, dev-i18n.ts:247). Pins flipped and bearing weight (spec: five #15711 cases replacing the two named pins, plus two older key-by-key pins re-spelled to declare the en chain they were using implicitly; rest: section 4 and section 5 at the seam incl. list/types/header-less reads and a no-getDefaultLocale control), docs updated (translations.mdx, i18n-standard.mdx; system-context.mdx anchors re-fixed by the census gate's own --fix after the seam docblock moved five rest-server.ts sites by 18 lines), changesets: spec minor with BREAKING banner and the measured ADR-0087 disposition not-required (no-migration-prescription) (gate exit 0; registered is not available, no registry entry would be true for a resolver default), rest patch, out-of-repo caveat quoted verbatim. Card assignee was set by the seat; needs:contract-review carrier untouched; no skip-changeset label (changesets present). Draft PR #15888 open; CI convergence is the PM's.",
      "tests": "Closure build under the verify lock (slot issue-15711): turbo build --filter='@objectstack/rest^...' --concurrency=2, 24 tasks, 5m29s, VERDICT command-exit 0 (waited 435s behind a sibling spec vitest). spec i18n-resolver.test.ts final: Tests 248 passed (248) (first run at acbfe264b: 2 failed/246 passed, the translatePage #6080 and translateFlow #11287 key-by-key pins received the AUTHORED texts under the new [] default, re-spelled to fallbackChain: ['en']). rest whole package: Test Files 183 passed (183), Tests 3122 passed (3122); seam file 19/19. Only two other resolver-calling test files in the repo (grep -rln over packages examples): service-i18n authored-translations 16 passed, plugin-approvals approval-status-vocabulary 6 passed. Typecheck: rest exit 0 (tsc + check:test-typecheck OK, 0 debt), spec exit 0 (tsc, scripts, check:test-typecheck 54/261/145 held, unchanged), spec check:test-typecheck re-run after the last test commit: same. Ablation (rest resolves @objectstack/spec/system through exports to dist, no alias): localeChain mutated back to fallbackChain ?? ['en'] without the short-circuit plus a used string marker; on-disk proof injected=1 removed_left=0; spec build emitted ESM/CJS then its DTS step failed TS6133 'isDefaultLocale' never read (mutation orphans the helper; JS artifact already written); ablation-dist-preflight: marker present in 4 built files; mutated spec file 4 failed/244 passed (the four rule-dependent #15711 pins; fr control green by design), mutated rest seam file 6 failed/13 passed (section 4 legacy/undefined; section 5 single, app/list/types, header-less, shipped-bundle omitted key; controls green by design). Restore: git checkout HEAD -- ABSOLUTE_PATH under an EXIT/INT/TERM trap, git diff HEAD empty, hash-object equals HEAD blob 4c95369c on both legs, git status --porcelain 0 lines; rebuild exit 0; preflight --absent: marker absent from all 217 built files; spec 248/248, rest 19/19. Direction red as predicted, no reversal. Narrowing declared: turbo ls --affected = 75 packages (a spec source edit reaches nearly all); locally run spec resolver file + whole rest suite + the two other resolver test files + spec/rest typecheck; spec full suite and the other affected packages declared to CI. Lint as a proven narrowing: eslint --no-inline-config --format json over the 5 touched TS files (the full TS set of git diff --name-only 8e7653b35..HEAD): 5 files, 0 errors, 0 warnings; population from eslint.config.mjs (files **/*.{ts,...} and packages/** blocks, parserOptions ecmaVersion/sourceType only); invariance: the config declares no parserOptions.project and no typed rules, so the diff moves no untouched file's verdict. Gates (dispatch-gates --commands: 97 families; --ran verdict: 79 run, 18 UNRUN = all --self-test variants of gate scripts this diff does not edit, declared to CI): green incl. check-adr-0087-registration / check-changeset-no-major / check-empty-changeset (run with --base 8e7653b35, the recorded base), spec check:generated at e22c25b9e and at the final commit b7312986c ('All 15 generated artifacts are up to date', so M7: no shard moved), spec source audits exported-any / dual-source-exports / entry-nameability / browser-reachable-entries green on the restored build, system-context-census green after --fix, nul-bytes green, control-byte self-scan of touched files: none. NOT MEASURED (PREREQUISITE NOT MET by their own verdict lines): check:type-check-debt (33 closure deps unbuilt), check:dual-build-cjs-loads (44 packages without dist), spec check:skill-examples (client-react not built). mcp calls listed below.",
      "mcp_calls": "5 — dev claim comment, create_pull_request, this report comment, pull_request_read (body read-back), issue_read get_comments (this comment's read-back); every card/comment read went through the zero-quota payload channel (both comments were within the first 15 timeline items), gh is not installed in this container so no REST probe was possible",
      "open_questions": [
        {
          "question": "The ruling says the chain is skipped when the requested locale EQUALS defaultLocale. Implemented as exact-tag equality, case-insensitive per BCP-47 (zh-cn names zh-CN, pinned). A base-language request (zh on a zh-CN default) is treated as a different locale: its own variant expansion still reaches the zh-CN bundle entry, but with no zh-CN bundle it walks the declared chain (a declared en answers). Not pinned, because pinning it would decide something the ruling does not spell out.",
          "options": [
            "A — keep exact-tag (case-insensitive) equality as implemented; treat a base-language default match as a separate card if a real config ever hits it",
            "B — widen the default-locale match to the resolveBundleLocale ladder (base-language and variant expansion), so zh on a zh-CN default is also 'the default'"
          ],
          "recommendation": "A, because it is the literal ruling ('equals'), it keeps the rule keyed on the declaration rather than on tag arithmetic (axis 3: nothing is inferred on the author's behalf), and no measured deployment sends a bare base tag while declaring a regional default (axis 1); B is a contract widening that belongs on its own card if pull appears (axis 4)"
        }
      ],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  6. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    Review — ACCEPT · Clause-② contract review PASS (domain:spec seat at CONTRACT_REVIEW_TIER, session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T12:55Z; report 5551935080, PR #15888 at head b7312986c; dev claim 5551597326).

    Contract limb — the diff is exactly the ruled shape (5549577889), read hunk by hunk at 12:50Z:

    • packages/spec/src/system/i18n-resolver.ts: ResolveOptions gains the optional defaultLocale?: string with a docblock that states the ruled rule; localeChain answers [locale] when the requested locale names the default (BCP-47 case-insensitive via isDefaultLocale, the same rule resolveBundleLocale applies) and [locale, ...(fallbackChain ?? [])] otherwise — the literal ['en'] default is gone (second facet). The fallbackChain docblock and the file header say the new semantics; two key-by-key docblocks stop naming en as the implicit next locale.
    • packages/spec/src/contracts/i18n-service.ts: docblock-only on getDefaultLocale?() and getFallbackLocale?() — no signature moves; the serving-layer threading is documented on the accessor.
    • packages/rest/src/rest-server.ts translateOptionsFor (the single seam — the dev verified four call sites, all spread it): threads defaultLocale from getDefaultLocale() beside the chain, both feature-detected, never answering 'en' on a provider's behalf; the docblock's former "⛔ Not derived from getDefaultLocale()" prohibition is rewritten to the ruled rule. Region :3187-3236, disjoint from rest-lane PR fix(rest): consume the parsed api sub-config so RestApiConfigSchema owns its defaults #15673's hunks.
    • No Zod schema, authorable key or accept set moves. Changesets: @objectstack/spec minor with the launch-window **BREAKING** banner for the ['en'] → [] default and the measured ADR-0087 disposition not-required (no-migration-prescription) (gate exit 0 — registered is not a truthful entry for a resolver default, as the ruling allowed "as the gate accepts"); the out-of-repo-host caveat quoted; @objectstack/rest patch. Governed predicate: 0 of 10 paths (run 12:50Z).

    Pins bear weight (read, not trusted): spec — five #15711 cases replace the two named pins: a declared en chain no longer outranks the authored label for a default-locale request (control: the same options without defaultLocale still walk the chain); a non-default fr request still walks fr → en → authored; a shipped zh-CN bundle still wins and an omitted key resolves authored; zh-cn names zh-CN; a chain-less caller gets authored on both a default and a non-default request, with en and declared-en controls. Two older key-by-key pins (translatePage, translateFlow) now declare the en chain they used implicitly — a required consequence of the [] default, recorded honestly. Rest — §4 rewritten (no chain ⇒ no en invented; a provider without getDefaultLocale is the control: the declared en chain is walked exactly as #14882 pinned) and §5 flipped to the authored answer across the item / app / list / types reads and the header-less request, with fr and en controls and a shipped-bundle case.

    Docs follow the face: content/docs/ui/translations.mdx ("How a locale is chosen") and content/docs/protocol/kernel/i18n-standard.mdx state the declared chain, the default-locale rule and the [] default; content/docs/permissions/system-context.mdx is the generated census page re-anchored by its own gate after the seam docblock moved five rest-server.ts sites.

    Verification accepted with the declared narrowing: resolver file 248/248 (two pins re-spelled after an honest first red), whole @objectstack/rest 183 files / 3122 tests, seam file 19/19, the two other resolver-calling suites green, spec + rest typecheck exit 0; ablation dist-resolved (marker in 4 built files; 4 spec + 6 rest pins red as predicted, controls green; restore proven by git diff HEAD empty and blob-hash equality; --absent on 217 built files) — direction red, no reversal; 79 gate families run green incl. check-adr-0087-registration, check:generated "15 artifacts up to date" (M7: no shard moved), system-context-census; 18 unrun are self-test variants of untouched gate scripts, three NOT MEASURED are dist-prerequisite gates — CI runs the spec full suite and the 75 affected packages.

    Open question ruled by the seat: A — exact-tag (case-insensitive) equality is the ruling's literal "equals"; a base-language request (zh on a zh-CN default) stays a different locale and walks the declared chain. Widening to the resolveBundleLocale ladder is a contract widening for its own card if a real config ever sends a bare base tag against a regional default. Not pinned, correctly.

    Landing path: GitHub reads the PR dirty on the generated content/docs/permissions/system-context.mdx (a textual conflict with origin/main af29b662b; the repo's merge driver regenerates it locally, GitHub does not) and therefore created no merge ref — zero check runs on b7312986c. One regen lap through the dev (scripts/pm/os-regen-merge.sh, no rebase) follows; the seat re-reads the delta at the new head, then hangs and clears needs:contract-review on both carriers (PR + this card), runs the pair check, and flips to ready + auto-merge (squash) once every check is green.


    Generated by Claude Code

  7. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    Review addendum — head move b7312986c → 9ab41a285 (PR #15888), re-read 2026-09-05T15:11Z. Delta via REST compare: 7 commits = 6 of origin/main's own (through af29b662b) + the merge commit 9ab41a285 (the regen lap; the dev's addendum never posted — its run was killed by the account's Fable model limit at ~13:00Z right after the push). The PR's own file list against its base is unchanged — the same ten files with the same line counts — so the contract surface reviewed in the verdict (5551957148) is byte-identical; the only regenerated file is content/docs/permissions/system-context.mdx (census page re-derived from the merged tree). Verdict unchanged — accepted. CI on 9ab41a285: 32 success, 2 skipped, 0 red. Landing path: GitHub reads the PR dirty again on the same generated page (origin/main moved on to cee396175 during the outage; local merge-tree names only that file), so a second regen lap runs through the revived dev (15:12Z); at the next head the seat re-reads the delta, cycles needs:contract-review on both carriers, runs the pair check and flips on green.


    Generated by Claude Code

  8. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    Review addendum — head move 9ab41a285 → 2c453f0b2 (PR #15888), re-read 2026-09-05T15:14Z. Delta via REST compare: 21 commits = 20 of origin/main's own (through ea03c7c16, including #15877's cron row) + the merge commit 2c453f0b2 (regen lap 2, census page re-derived from the merged tree; the revived dev's lap). The PR's own file list against its base is unchanged — the same ten files with the same line counts as at b7312986c — so the contract surface reviewed in the verdict (5551957148) is byte-identical. Verdict unchanged — accepted. mergeable_state no longer dirty (mergeable true at 15:14Z); CI on 2c453f0b2 running (11 green, 18 in progress at 15:14Z). The seat now hangs and clears needs:contract-review on both carriers (PR #15888 + this card) at this head, runs the pair check, and flips on green.


    Generated by Claude Code

  9. os-project-manager commented on Sep 5, 2026

    @os-project-manager
    Collaborator

    Regen-lap addendum for PR #15888 (os-dev, session_01M59rPZZFzqhfMUPFqqZTkf) — two laps, no code change; the PR's own files are byte-identical to the accepted head.

    Heads: b7312986c (accepted, Clause-② PASS) → 9ab41a285 (lap 1, merge of origin/main af29b662b, pushed 2026-09-05T13:0xZ, CI 32 success / 2 skipped per the seat) → 2c453f0b2 (lap 2, merge of origin/main ea03c7c16, pushed 15:1xZ). Both via bash scripts/pm/os-regen-merge.sh in a fresh worktree on the branch: no rebase, no hand gen:schema in a MERGE state, no force-push.

    What each merge commit contains (git show --stat; the regeneration happened INSIDE each merge commit, as the driver's own notice for a MIXED page instructs — there is no separate regen commit):

    Pre-push regen check: both pushes went through .githooks/pre-push → check-regen-pending.mjs --pre-push (the deferral marker named the census page; the page was already current, so the push proceeded; PUSH_EXIT=0 both laps).

    Tests on the merged tree (lap 2, 2c453f0b2, closure rebuilt under the verify lock: turbo build --filter='@objectstack/rest^...' --concurrency=2, 24 tasks, 4m33s, VERDICT command-exit 0; each test's exit captured separately, both 0):

    • pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/system/i18n-resolver.test.ts → Test Files 1 passed (1) · Tests 248 passed (248)
    • pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/meta-i18n-declared-fallback-chain.test.ts → Test Files 1 passed (1) · Tests 19 passed (19)

    (Lap 1 at 9ab41a285 was cut off by the account's model limit after the push and before these two runs; the seat's CI reading on that head stands as its evidence.) needs:contract-review untouched on both carriers. Worktree cleaned up after this comment.


    Generated by Claude Code

  10. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    Provenance — flip + auto-merge (domain:spec seat, session_01M59rPZZFzqhfMUPFqqZTkf): PR #15888 at head 2c453f0b2 (regen lap 2) — verdict 5551957148 (ACCEPT + Clause-② PASS at b7312986c), delta addenda 5552734026 (→ 9ab41a285) and 5552743900 (→ 2c453f0b2), verdict unchanged at each head; needs:contract-review hung and cleared on both carriers at this head 15:15Z, pair check ✓; NOT governed (predicate 0/10). Every check on 2c453f0b2 concluded by 15:44Z (34 success, 7 skipped). Ready 2026-09-05T15:44:21Z, auto-merge (squash) 2026-09-05T15:44:33Z, added_to_merge_queue 2026-09-05T15:45:34Z (read back at 15:45:49Z; mergeable_state: clean) (queue refs at 15:44:52Z: pr-15908-7b20ae227aea7ae9c6b0ea678cce645eabbe4307, pr-15923-a2051fa4e92441b2ae6b79959ccced1dfb148057, pr-15924-14f80bc89bd86d671d33f7eb5bf797ae0dd09104, pr-15933-d4c2cb196e84bebd5aebb5ec0f9872614fa6a307, pr-15934-97afcb5ccc061eb271cb37fda3bc468459eac7c2). On merge: pm:dispatched stripped, assignee cleared with a Release: line, landing note; the engine lane's #15694 has its fence discharged by this ruling (the dev's PR body says so). Landing watch armed.


    Generated by Claude Code

  11. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    Release: session_01M59rPZZFzqhfMUPFqqZTkf (domain:spec seat) — released 2026-09-05T16:44Z, assignee cleared in this stroke; reason: landed — PR #15888 merged through the merge queue at 2026-09-05T16:15:46Z (46803fa69), the card closed by the PR; destination: closed. The stroke runs at 16:44Z because the seat's landing watch fired into a session blocked by the account's 5-hour usage limit (~16:05Z → 16:40Z).


    Generated by Claude Code

  12. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    Landed — PR #15888 merged through the merge queue at 2026-09-05T16:15:46Z, merge commit 46803fa69 (squash of head 2c453f0b2; enqueued 15:45:34Z). Probe on origin/main at 2026-09-05T16:43Z (tip 0cf086759): git grep -c defaultLocale origin/main -- packages/spec/src/system/i18n-resolver.ts → 14, and fallbackChain ?? [] at :345 inside localeChain's isDefaultLocale short-circuit; control on the pre-merge tip ea03c7c16: the literal fallbackChain ?? ['en'] still present (1 hit) and no defaultLocale option. Card state, one write and read back: pm:dispatched stripped, assignee cleared (Release: line above). Lane inventory diff: only #15711 closes. #15694 (engine lane, finding) keeps its pm:queue; its fence ("do not implement getFallbackLocale() by hand") is discharged by this ruling — the engine seat consumes the declared fallback in its own card (recorded in the PR body). #14882 stays closed (PR #15707). Review record: verdict 5551957148 (ACCEPT + Clause-② PASS at b7312986c), delta addenda 5552734026 / 5552743900, carriers cycled at 2c453f0b2, pair check ✓, provenance 5552923533; the dev's lap report 5552790313.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions