Skip to content

feat(i18n)!: the Console's language packs follow the platform locale table, keyed by its tags, and Traditional Chinese (zh-Hant) ships - #30902

Merged
objectstack-fleet[bot] merged 26 commits into
mainfrom
claude/project-thread-f2dhrw
Nov 6, 2026
Merged

objectstack-fleet[bot] merged 26 commits into
mainfrom
claude/project-thread-f2dhrw

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread
domain:ui#2 · os-tesla · session_01L9qibyWSiXh6B2sMVHfXYC

Part of #30511. Every item the card asks for is in this PR; the seat closes the card when it lands, and phase 4 (the locale menu) follows.
Supersedes #30595 (the same reviewed diff, resumed from claude/project-thread-xplvvb@9b15e9ad45 on this thread's own branch, then merged with main through #30640's lazy marketplace namespace and #30601's shared locale ladder).

Clause-②: yes (narrowing) — widens: a zh-Hant pack (the zhHant export of @object-ui/i18n/locales, the @object-ui/i18n/locales/zh-Hant module), the resolveBuiltInLanguage export of @object-ui/i18n, and every stored or browser code the platform locale table reads as one of its tags (zh-TW, zh-HK, zh-MO → zh-Hant; zh, zh-CN, zh-SG → zh-Hans; pt → pt-BR). Narrows: the Simplified Chinese and Portuguese packs are keyed zh-Hans and pt-BR, no longer zh and pt, in builtInLocales, BUILT_IN_LANGUAGE_CODES, getLoadedBuiltInLocales() and getAvailableLanguages(), and their modules @object-ui/i18n/locales/zh and @object-ui/i18n/locales/pt are @object-ui/i18n/locales/zh-Hans and @object-ui/i18n/locales/pt-BR.
Metadata-protocol: none
Skills-docs: follow-up owed — skills/objectui/guides/i18n.md

维护者速读

  • 改了什么:控制台的语言包改为跟随平台语言表,表里每个语言一个包,以表的标签为键(zh → zh-Hans,pt → pt-BR),新增繁体中文 zh-Hant,由简体包自动转换生成。刚合入的应用市场按需文案包同样按标签拆成 11 份,也有繁体。存值语言(sys_user.locale)挑选改用平台统一的匹配顺序,与服务端一致。
  • 为什么改:裁决 F([Decision] One locale list, one completeness rule — the long-term i18n regime across Console, platform metadata, cloud and mobile #29349)要求控制台的语言集合取自平台语言表;此前控制台自带十个包,表里的繁体中文缺席,测试照样全绿。台湾、香港用户此前看到的是简体。
  • 风险与回滚:只动前端与 i18n 门禁脚本,不动 spec 与服务端。旧存值 zh-CN、zh、pt 照常渲染;zh-TW、zh-HK 用户从简体变为繁体。仓外代码若按 builtInLocales.zh 或 @object-ui/i18n/locales/zh 取包会断,changeset 写了改法。首屏英文包字节不变。回滚即 revert 本 PR。
  • 先看:ui/packages/i18n/src/locales/registry.ts(按标签加载与 resolveBuiltInLanguage)、ui/packages/i18n/src/namespaces/marketplace/index.ts(应用市场文案按标签加载)、ui/packages/i18n/scripts/zh-hant.mjs(繁体转换规则)。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/i18n/,预期全部通过;再把 ui/packages/i18n/src/locales/index.ts 里 'zh-Hant': zhHant, 一行删掉重跑 all-locales-key-parity,预期转红并点名 zh-Hant。
  • 需要你做的:无。os-zhuang 已于 2026-11-06 14:26Z 批准本 PR(head c1f166cc),批准后的推送只有三次 merge main、两次 zh-Hant 重新生成、一行测试改写(changeset 的 FROM → TO)和三个测试文件的模块顶层预热导入(main ci(test-source-alias): follow a changed test into the source it loads, and red a cold literal import() paid there #30969 的新规则),契约复审 CONTRACT_REVIEW_TIER delta PASS 在案(评论 6693289236,Head-sha d9b3ddde571af4950702457bce4edb4de51ccce2)。席位落地。
  • 席位意见:ui 席 2 对照 head c1f166cc 的 diff 复核:152 个文件,语言包键随平台语言表改为标签(zh → zh-Hans、pt → pt-BR),新增由简体自动生成的 zh-Hant,应用市场按需文案包同样按标签拆分;旧存值 zh、zh-CN、pt 经表的读别名照常解析,首屏英文包字节不变、上限未动。属收窄(BREAKING,patch,changeset 带 FROM → TO 与 ADR-0087 标记),独立复审员按 CONTRACT_REVIEW_TIER 先判 FAIL(lint 入口守卫),修复后 delta 复审 PASS;CI 57 项全绿。复审提出的后续已立卡:A Traditional Chinese console reads and writes the Simplified entry of an inline label map #30910(繁体会话读写内联标签取到简体键)、skills/objectui i18n guide still teaches the zh and pt pack keys that #30511 retires #30911(skills i18n 指南仍教 zh/pt);服务端与 studio 命名空间缺繁体归 Platform locale table (ruling F of #29349): one list of 11 languages in spec, complete on every surface #29428。同意落地,待人批。

What changes

The pack set comes from the platform locale table. ui/packages/i18n no longer keeps its own list of ten languages. It reads PLATFORM_LOCALES from @objectstack/spec/system (phase 1, PR #30007).

  • Pack files. src/locales/zh.ts is now zh-Hans.ts, pt.ts is now pt-BR.ts, and zh-Hant.ts is new. builtInLocales is keyed by table tag, in table order, and satisfies Record<PlatformLocaleTag, unknown>. A tag the table gains without a pack, or a pack the table lacks, is a compile error.
  • registry.ts. LAZY_LOCALE_LOADERS is typed by the table's tags, and BUILT_IN_LANGUAGE_CODES is PLATFORM_LOCALE_TAGS.
    • The new export resolveBuiltInLanguage(code) names the pack that a stored or browser code reads as. It tries the table's tag or read alias first (platformLocaleTagOf, case-insensitive), then each shorter prefix (zh-Hant-TW → zh-Hant, en-GB → en).
    • loadBuiltInLocale and isBuiltInLanguage go through it, so every legacy code still loads its pack.
  • i18n.ts. fallbackLng is a map built from the table. Every tag falls back to the fallback language. Every read alias falls back to its tag, then to the fallback language. So an instance whose language is a legacy zh or zh-CN renders the zh-Hans resource.
  • provider.tsx. The boot effect and changeLanguage now write a built-in catalogue under its table tag, the bundle createI18n keyed it by.
    • Before, an instance in an alias (zh, zh-TW, pt) got the resident catalogue written a second time under the alias. That fired an added event, which re-rendered every reader and refetched every chart keyed on the resolver.
    • ObjectChart.refreshIndicatorLocale-10580 found it: its zh chart never left the skeleton.

zh-Hant is generated. scripts/zh-hant.mjs is the converter. It uses:

  • OpenCC s2twp, with opencc-js pinned at 1.3.1 as a devDependency;
  • the phrase overrides of the www.objectos.ai converter;
  • the locative 里 repairs.

scripts/gen-zh-hant.mjs writes src/locales/zh-Hant.ts from zh-Hans.ts (pnpm --filter @object-ui/i18n gen:zh-hant). It refuses to write when a pinned conversion case fails. There are two additions over the site's rules, both pinned as conversion cases:

  • STRADDLE_OVERRIDES: 并发布 → 並發布, 头发送 → 頭發送, 是只读 → 是只讀, 只影响 → 只影響. s2twp segments by its Simplified phrase dictionary before it converts. A phrase that straddles a word boundary (审阅「并发」布) therefore cannot be repaired at the Traditional phrase stage. The fix puts the Simplified phrase into the segmentation dictionary and at the head of the first group.
  • TWP_PHRASE_OVERRIDES gains 操作運行時 → 操作執行時.

Other details of the generated pack:

  • Gate-only keys. The six gate-only console.ai.* outbound keys are left out of zh-Hant, as they are from every non-gate pack. Comments replace them so line numbers stay aligned with zh-Hans.
  • Freshness test. src/__tests__/zh-hant-generated.test.ts reds when the committed pack is not byte-for-byte what the generator writes, and names the command that repairs it. It is a package test, not a check:* gate (ruling F adds none).
  • Merge-driver ledger. It records zh-Hant.ts as NOT_DRIVER_MANAGED (gen gen:zh-hant). The pack is generated whole, but a REGEN_ARTIFACTS row needs a check: script, and this phase adds none.
  • Type-check coverage. The converter's zh-hant.d.mts twin joins the package's tsconfig.test.json, so check:type-check-coverage reads it.
  • Lint-rule coverage. ui/scripts/check-lint-rule-coverage.mjs declares the same twin in UNREACHED_GROUPS, beside the two script twins it already declares. Its tests now accept an objectstack# card on a ledger row as well as an objectui# one.

The lazy marketplace namespace follows (#30640). src/namespaces/marketplace/zh.ts is now zh-Hans.ts, pt.ts is now pt-BR.ts, and zh-Hant.ts is new, generated by the same command.

  • MARKETPLACE_LOADERS is typed Record<PlatformLocaleTag, …>, eleven literal imports, so a table tag without a catalogue is a compile error.
  • resolveMarketplaceLanguage reads a stored code through resolveBuiltInLanguage, so zh-TW loads the Traditional catalogue and pt the pt-BR one. The loader stores the catalogue under the table tag.
  • The all-packs door (locales/index.ts) deep-merges each tag's catalogue into its pack, as main's complete() already did, and still throws on a leaf defined twice.
  • The converter gains generateZhHantNamespaceCatalogue and a frozen ZH_HANT_TARGETS list (the locales pack and the marketplace catalogue); gen:zh-hant writes both, and the freshness test walks the list.
  • apps/console/vite.config.ts names one i18n-ns-marketplace-<tag> chunk per table tag.
  • feat(console): a marketplace app's Install confirms what it adds before it installs #30840's eleven install.preview keys came in through the renames and are in zh-Hans, pt-BR and the regenerated zh-Hant.

Readers keyed by the old codes.

  • app-shell readers. LoadingScreen (the boot line), outboundAgentText and flowResponse read the loaded-pack snapshot by table tag, through resolveBuiltInLanguage.
  • app-shell pickRenderableLocale (the sys_user.locale → UI language pick) is now resolveLocaleAmong from @objectstack/spec/system (fix(spec,core): resolve locales through the platform table's read aliases, so a zh-TW / zh-HK reader gets zh-Hant #30601), the ladder the server's resolveLocale uses, instead of a copy of it. A stored zh-TW meets a list offering both scripts and renders zh-Hant; before, it took the first zh variant, zh-Hans. A variant the table reads as the language itself (es-ES for es) is taken ahead of the first offered variant, as the server takes it.
  • components page tabs. The Traditional rows of the exact-locale label dictionary now serve a zh-Hant session, and zh-HK / zh-MO sessions too. Before, every non-exact zh fell to the Simplified rows.
  • apps/console/vite.config.ts. One locale chunk per table tag.

Gates read the table.

  • check-i18n-en-drift.mjs takes its locales from PLATFORM_LOCALE_TAGS and merges each namespace's partial pack in (main's PACK_NAMESPACE_DIRS), throwing on a key defined twice. A follower pack that the base commit does not have (this PR's renames and zh-Hant) reads as empty at the base and is named in the summary. en missing at the base, or any pack missing at HEAD, still throws.
  • check-i18n-dead-keys.mjs strips map-chain tags by the table.
  • check-eager-locale-catalogues.mjs keeps its deliberate explicit list, now the eleven tags. Its test pins that list to the table and to the registry's loader table.

Paths. One doc link (ui/packages/plugin-gantt/docs/verification/README.md, which check-doc-links reads) and one test comment named zh.ts. Both now name zh-Hans.ts.

Tests

  • Suites that read the table. The parity, region-fallback, entry-shape and en-drift suites read the table.

    • all-locales-key-parity walks PLATFORM_LOCALE_TAGS, so a table language with no pack reds it by name.
    • region-locale-fallback-11326 covers both scripts: zh-CN and zh-SG read Simplified; zh-TW, zh-HK and zh-MO read Traditional.
  • zh-hant-generated.test.ts (new) covers:

    • byte-for-byte freshness and the generated header;
    • every pinned conversion case;
    • 保存 → 儲存 as the converter's answer;
    • the gate-only keys dropped, and nothing else;
    • the converter's declaration twin.
  • provider-load-rerender-10382.test.tsx. An instance booted in zh, zh-CN, zh-TW or pt reads its tag's resident catalogue, renders once, and keeps no bundle under the alias. A catalogue that lands after mount reaches a zh-TW reader.

  • i18n.test.ts. Main's CJK-Latin title spacing (Console and CLI polish from the real-user walkthrough #30126) is pinned for both zh-Hans and zh-Hant booted by tag.

  • check-i18n-en-drift.test.ts. Fixtures write a const named pack, because a tag is not an identifier. A new describe pins the base-absent follower rule, its CLI line, and the two cases that still throw.

  • git-paths-nested-layout.test.ts. It pinned that any pack missing at the base throws. Under the follower rule only en does, so the case now pins both halves: en missing at the base throws, and a follower the base lacks reads as empty and is named.

  • userLocale-10059.test.tsx. A new describe pins the table step:

    • zh-TW, zh-HK, zh-MO and zh-Hant-TW → zh-Hant;
    • zh, zh-CN and zh-SG → zh-Hans;
    • pt → pt-BR;
    • a deployment's own spelling;
    • the control, where no Traditional spelling is offered.

    The pin "first offered variant" moved from zh to es (es over en, es-MX, es-AR → es-MX), because the table reads a bare zh as Simplified. A new case pins the shared ladder's table step: es over en, es-MX, es-ES → es-ES.

  • Marketplace namespace suites (marketplace-namespace, marketplace-namespace-lazy, MarketplacePage.namespaceGate-30588) are keyed by tag, and pin zh-TW / zh-HK → zh-Hant and pt → pt-BR resolution plus a zh-TW reader's lazy load. zh-hant-generated runs each case over ZH_HANT_TARGETS, with a counter-probe on the list itself.

  • page-tabs-builtin-label-i18n-4645.test.tsx. zh-Hant, zh-HK and zh-MO sessions read 詳情 / 備註 / 概覽, and two of those are dict-only tokens. The zh-Hans / zh-SG control reads Simplified.

  • LocaleSwitcher suites (appLocales, persistence, unsavedEditsAsk-13254). They pick and expect the menu's names for the table's tags through localeLabel, and the fallback-menu cases expect one entry per tag. The component itself is not edited.

  • About 100 other suites hard-coded ten packs, a zh / pt key or a 'zh' lookup. They now read the table or the tag. For example, toHaveLength(10) became PLATFORM_LOCALE_TAGS.length, and where a table names every pack, a converted zh-Hant twin sits beside each retagged zh-Hans row.

Readings (head f7e9df2d, base 98f640d5)

Ablations, suites, type-check and gates

The heavy runs were taken on e94c13c8. f7e9df2d adds one merge of main (11 commits) and the regenerated zh-Hant; the i18n package, the en-drift test, the 32 test files those 11 commits added or changed, the i18n checks and the ADR-0087 and no-major checks were rerun on it.

  • Green on main, red by name here. On main before this branch, the table already named zh-Hans, zh-Hant and pt-BR and the Console had no pack keyed by any of them, yet all-locales-key-parity and region-locale-fallback-11326 were green (2 files, 58 tests; taken for feat(i18n)!: the Console's language packs follow the platform locale table, keyed by its tags, and Traditional Chinese (zh-Hant) ships #30595).
  • Ablations. Each was restored afterwards, with git diff HEAD empty.
  • Suites.
    • Full ui suite (pnpm --dir ui run test) on d0632482: 6631 files passed, 7 skipped; 21 files red. 19 of them read built dist / .d.ts output that the skip-dts build had not written; the other 2 named the old builtInLocales.zh key and were fixed in d0632482. After the full workspace build, the 21 files rerun: 21 / 21, 513 tests.
    • packages/i18n/, scripts/, packages/app-shell/src/hooks/ and the 36 test files main had added since the first merge, on e94c13c8: 401 files passed, 2 skipped; 7830 tests.
    • On f7e9df2d: packages/i18n/, the en-drift test and the 32 test files of the last merge: 131 files, 2014 tests, green.
    • Dogfood on e94c13c8, after the full workspace build: pnpm --filter @objectstack/dogfood test: 424 files passed, 3 skipped; 3538 tests.
  • Build and type-check.
    • The full workspace build with type declarations (turbo run build --filter='!@objectstack/docs'): 118 / 118. It caught one gap that the skip-dts runs had not: loadMarketplaceCatalogue indexed the tag-typed loader table with a string. e94c13c8 makes hasMarketplaceCatalogue a type guard.
    • turbo run type-check --filter='...[origin/main]': 101 / 101. ui type-check:coverage, :e2e, :scripts, :vitest-setup and :vitest-config: exit 0.
  • Gates.
    • The 76 derived dispatch gates exit 0 on e94c13c8. dispatch-gates --ran reads 76 derived, 76 run, 0 NOT-MEASURED, 0 UNRUN.
    • ui check:eager-closure, check:eager-locale-catalogues, check:i18n-keys, check:i18n-drift, check:i18n-dead-keys, check:spec-symbols, check:phantom-deps, check:unreferenced-sources, check:sdui-registration-pins and node scripts/check-sdui-manifest.mjs: exit 0.
    • After the Console's build, the i18n-locale-en chunk holds the single module en.ts, whose source is identical to main's; 57,625 gz bytes. No size ceiling is raised. zh-Hant is 62,336 gz bytes, and the marketplace zh-Hant catalogue 4,560, both loaded on demand.
    • check-adr-0087-registration --base origin/main and check-changeset-no-major --base origin/main: exit 0 on f7e9df2d, the latter also with this body as --event (level axis yes (narrowing) + none, every entry at patch).
    • eslint on the 144 changed ui sources: 0 errors.

Acceptance notes

  • Native read of zh-Hant. The conversion follows the site's rules, but a Traditional reader should still skim the pack. Renderings worth a look:

    • 儀表盤 (Taiwan usage is often 儀表板);
    • 只讀 (often 唯讀);
    • 釋出 and 發布 both appearing for "publish";
    • 後臺 / 啟動臺 (臺 vs 台);
    • 會話 for "session" (Taiwan usage is often 工作階段), and 退出登入 (often 登出);
    • the curly quotes “…” kept from Simplified (Taiwan usage is often 「…」).

    A rule change goes in scripts/zh-hant.mjs and in the site's converter, never in a hand edit of the pack.

  • Studio. The Studio namespace still ships en and zh. A zh-Hant or zh-TW reader sees Studio's Simplified catalogue (ruling app-shell: a published library that is 66% Console product surface — retire the 67 zero-reference exports now, decide the split, and fold the second i18n table into @object-ui/i18n #25922's namespace, untouched here).

  • The language menu (phase 4). LocaleSwitcher is not edited. Its hand-kept names are keyed zh and pt, so Intl.DisplayNames names the three tags they no longer match: 简体中文, 繁體中文 and Português (Brasil). The menu gains 繁體中文 as an eleventh entry.

  • Inline locale maps. pickLocalized is unchanged. Take a metadata label written { 'zh-CN': …, 'zh-TW': … }: a zh-Hant reader takes the first zh- key. A zh-TW-browser reader did the same before this PR, because that browser's language then resolved to zh. Teaching pickLocalized the table is a follow-up candidate, not done here.

  • One ladder. The Console's sys_user.locale pick and the server's resolveLocale now share resolveLocaleAmong, so the stored-locale ladder has one copy. The server's catalogues are its own and are not touched here.

  • ADR-0087. The changeset holds not-required (no-migration-prescription), and its marker names why.

    • The FROM → TO lines are source rewrites for consumers of the ./locales subpath, which is the class runtime-export-retired serves.
    • But that category reads only a package's root entry and refuses a subpath operand, and the root entry retires nothing here.
    • The seat may prefer to raise the gap.
  • Studio keys. fix(app-shell): word the flow closure refusal on the Problems list in the Console's language #30721's studio keys came in with the merge unchanged; Studio stays en + zh, so nothing in it is retagged.

  • Unchanged on purpose. The root ci.yml step is still named "followed by the nine translations", because the workflow is not edited. The eager-locale gate keeps its explicit list, now pinned to the table by its test.

Verification

Two minutes, from ui/:

  1. pnpm exec vitest run packages/i18n/: everything passes.
  2. Delete the 'zh-Hant': zhHant, line from packages/i18n/src/locales/index.ts and run pnpm exec vitest run packages/i18n/src/__tests__/all-locales-key-parity.test.ts: red, naming zh-Hant.
  3. pnpm --filter @object-ui/i18n gen:zh-hant: writes the same two zh-Hant.ts files (the pack and the marketplace catalogue) that are committed, so git diff stays empty.

A sample of the generated pack, with Taiwan vocabulary from s2twp:

zh-Hans.ts zh-Hant.ts
stop: '结束模拟登录' stop: '結束模擬登入'
manage_users: '管理用户' manage_users: '管理使用者'
manage_metadata: '管理元数据' manage_metadata: '管理中繼資料'
manage_platform_settings: '管理平台设置' manage_platform_settings: '管理平台設定'
manage_connector_connections: '管理连接器连接' manage_connector_connections: '管理聯結器連線'
save: '保存' save: '儲存'

claude added 17 commits November 5, 2026 14:35
…pack, key packs by table tag

The Console's language packs are now the platform locale table
(PLATFORM_LOCALES in @objectstack/spec/system): one pack per tag, keyed by
the tag (zh-Hans, zh-Hant, pt-BR). zh-Hant is generated from zh-Hans by
scripts/gen-zh-hant.mjs (OpenCC s2twp, pinned at 1.3.1, with the
www.objectos.ai overrides plus five straddle repairs found reading the pack).

Stored and browser codes keep resolving through the table's read aliases:
resolveBuiltInLanguage (new export) reads zh/zh-CN as zh-Hans, zh-TW/zh-HK
as zh-Hant, pt as pt-BR, then each shorter prefix; createI18n's fallbackLng
maps every alias to its tag. The app-shell splash, outbound agent text and
flow permission line read the snapshot by tag.

The parity, outbound, eager-catalogue and en-drift checks read the table;
en-drift reads a follower pack absent at the base as new.

Claude-Session: https://claude.ai/code/session_01L6bhgThgWPADcpU1xXCBsw
Co-authored-by: Claude <noreply@anthropic.com>
…pack, key packs by table tag

The Console's language packs are now the platform locale table
(PLATFORM_LOCALES in @objectstack/spec/system): one pack per tag, keyed by
the tag (zh-Hans, zh-Hant, pt-BR). zh-Hant is generated from zh-Hans by
scripts/gen-zh-hant.mjs (OpenCC s2twp, pinned at 1.3.1, with the
www.objectos.ai overrides, four straddle repairs and one phrase override
found reading the pack).

Stored and browser codes keep resolving through the table's read aliases:
resolveBuiltInLanguage (new export) reads zh/zh-CN as zh-Hans, zh-TW/zh-HK
as zh-Hant, pt as pt-BR, then each shorter prefix; createI18n's fallbackLng
maps every alias to its tag. The app-shell splash, outbound agent text and
flow permission line read the snapshot by tag.

The parity, outbound, eager-catalogue and en-drift checks read the table;
en-drift reads a follower pack absent at the base as new.

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

The suites outside ui/packages/i18n that hard-coded ten packs, a zh / pt
key, a 'zh' pack lookup or the menu's 中文 entry now read the platform
locale table or the tag.

Three readers keyed by the old codes are fixed with them. The provider
wrote a booted alias's catalogue (zh, zh-TW, pt) a second time under the
alias, an added event that re-rendered every reader and refetched every
chart for a catalogue already resident; it now writes under the tag. A
stored zh-TW locale took the first offered zh variant (zh-Hans);
pickRenderableLocale now takes the first offered code the table reads as
the same language first. The page-tab label dictionary served its
Simplified rows to every non-exact zh session; a zh-Hant, zh-HK or zh-MO
session reads its Traditional rows.

The en-drift fixtures write a const that is not named by the tag, and a
new describe pins the base-absent follower rule. The generated zh-Hant
pack is recorded as not driver-managed in the merge-driver ledger, and
the converter's declaration twin joins the package's test program. The
changeset declares the change.

Claude-Session: https://claude.ai/code/session_01L6bhgThgWPADcpU1xXCBsw
Co-authored-by: Claude <noreply@anthropic.com>
Main's pack edits to zh.ts and pt.ts land in zh-Hans.ts and pt-BR.ts;
zh-Hant.ts is regenerated from the merged zh-Hans.ts. The two conflicted
tests take main's CJK-Latin spacing cases under the zh-Hans tag, and a
case pins the spacing for both Chinese packs booted by their tags.

Claude-Session: https://claude.ai/code/session_01L6bhgThgWPADcpU1xXCBsw
Co-authored-by: Claude <noreply@anthropic.com>
The suite types its language by the keys of builtInLocales, which are now
the platform locale table's tags, so its zh cases name the zh-Hans pack.

Claude-Session: https://claude.ai/code/session_01L6bhgThgWPADcpU1xXCBsw
Co-authored-by: Claude <noreply@anthropic.com>
…er's twin

A doc link and a test comment named the old zh.ts path; they name
zh-Hans.ts. The nested-layout drift case pinned that a pack missing at
the base throws; under the follower rule only en does, and a follower the
base lacks reads as empty and is named, so the case pins both.

The lint-rule-coverage ledger declares the converter's declaration twin,
packages/i18n/scripts/zh-hant.d.mts, beside the two script twins it
already declares, and its tests accept an objectstack card on a row.

Claude-Session: https://claude.ai/code/session_01L6bhgThgWPADcpU1xXCBsw
Co-authored-by: Claude <noreply@anthropic.com>
34c9ccb is the first commit as pushed before its message was reworded
(c857810 has the same tree). Merged with the ours strategy so the branch
fast-forwards from what is already on the remote; no file changes.

Claude-Session: https://claude.ai/code/session_01L6bhgThgWPADcpU1xXCBsw
Co-authored-by: Claude <noreply@anthropic.com>
Merges main through the lazy marketplace namespace. Its catalogues move
to platform locale table tags with the packs (zh -> zh-Hans, pt -> pt-BR),
a zh-Hant catalogue is generated from zh-Hans by gen:zh-hant (now one list
of targets: the pack and each pack-shaped namespace), the namespace's
loaders are typed on the table, and the all-packs door merges each tag's
pack with its catalogue. The en-drift reader keeps both the follower-pack
excuse and the namespace partial packs.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibyWSiXh6B2sMVHfXYC
The marketplace shape, lazy-load and console gate suites name the
zh-Hans / zh-Hant / pt-BR catalogues and packs; a stored zh-TW reader is
pinned to the Traditional catalogue; the freshness suite holds every
zh-Hant target the generator writes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibyWSiXh6B2sMVHfXYC
pickRenderableLocale now calls resolveLocaleAmong from
@objectstack/spec/system, the ladder the server's resolveLocale runs,
instead of keeping a copy of it with its own table step. The table's read
aliases already name the script-subtag spellings, so a zh-Hant-TW row
still renders Traditional; a variant the table reads as the language
itself (es-ES) is now taken ahead of the first offered variant, as the
server takes it.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibyWSiXh6B2sMVHfXYC
The docs chrome and membership filter chip cases that main added read a
pack as builtInLocales.zh; they read zh-Hans (and the docs chrome also
zh-Hant) now. The chip cases keep booting the page in the stored zh
code, so they also pin that the alias still renders Simplified.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibyWSiXh6B2sMVHfXYC
…indexing its loader

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibyWSiXh6B2sMVHfXYC
@github-actions github-actions Bot added size/xl dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 6, 2026
… declaration, so importing the gate runs nothing

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

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibw7DekEe3wx5f3zphf

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f7e9df2dc7d58acf9fe82a92da1028ae691fc904
Local-runs: none
Scope: every path of the diff outside the prose faces (the changeset .changeset/30511-console-packs-platform-locale-tags.md is read for ②, not reviewed; no content/docs/** or apps/docs/** path is in the diff) — pnpm-lock.yaml, scripts/regen-artifacts.mjs, ui/apps/console/vite.config.ts, ui/packages/i18n/** (package.json, README.md, tsconfig.test.json, scripts/{gen-zh-hant.mjs,zh-hant.mjs,zh-hant.d.mts}, src/{i18n.ts,index.ts,provider.tsx}, src/locales/{index.ts,registry.ts,zh-Hans.ts,zh-Hant.ts,pt-BR.ts}, src/namespaces/marketplace/{index.ts,zh-Hans.ts,zh-Hant.ts,pt-BR.ts}, 70 src/__tests__/*), ui/packages/app-shell/src/{chrome/LoadingScreen.tsx,console/ai/outboundAgentText.ts,utils/flowResponse.ts,hooks/useUserLocale.ts}, ui/packages/components/src/renderers/layout/containers.tsx, ui/scripts/{check-eager-locale-catalogues,check-i18n-dead-keys,check-i18n-en-drift,check-lint-rule-coverage}.mjs and their __tests__, ui/packages/plugin-gantt/docs/verification/README.md, and the ~60 test files under ui/apps/console, ui/packages/{app-shell,components,fields,plugin-*} + cited: packages/spec/src/system/platform-locales.ts (the published table, PLATFORM_LOCALE_READ_ALIASES, platformLocaleTagOf), packages/spec/src/system/i18n-resolver.ts (resolveLocaleAmong), packages/platform-objects/src/identity/sys-user.object.ts (SYS_USER_LOCALE_TAG_PATTERN), ui/packages/app-shell/src/layout/LocaleSwitcher.tsx, ui/apps/console/src/pages/system/ProfilePage.tsx, ui/packages/i18n/src/namespaces/studio/index.ts, ui/packages/i18n/src/pickLocalized.ts, ui/packages/components/src/lib/date-fns-locale.ts, skills/objectui/guides/i18n.md. The lockfile and the two generated zh-Hant.ts catalogues are read for shape (header, line alignment, export default zhHant), not reviewed line by line.

① Derived judgments

Public surface, @object-ui/i18n (exports map unchanged: ., ./locales, ./locales/*, ./namespaces/studio, ./namespaces/marketplace):

  • . entry gains resolveBuiltInLanguage(code): string | null — RIGHT (widening; the one resolver every reader keyed by the old codes now goes through).
  • ./locales entry: builtInLocales keys are the table's tags in table order (zh → zh-Hans, pt → pt-BR, zh-Hant added), satisfies a Record keyed by PlatformLocaleTag; named exports zh/pt KEPT as identifiers (now the zh-Hans/pt-BR catalogues), zhHant added — RIGHT (narrowing of the key set, declared in the changeset with FROM → TO; the identifier exports are deliberately not renamed, which the header says).
  • ./locales/*: @object-ui/i18n/locales/zh and /pt no longer resolve (files renamed); /zh-Hans, /zh-Hant, /pt-BR new — RIGHT, declared, migration lines present.
  • BUILT_IN_LANGUAGE_CODES is [...PLATFORM_LOCALE_TAGS] (11 entries, zh/pt gone); type readonly string[] unchanged — RIGHT, declared.
  • getLoadedBuiltInLocales() keyed by tag; loadBuiltInLocale / isBuiltInLocaleLoaded / isBuiltInLanguage accept every table tag, every read alias, and any longer tag by prefix walk (zh-Hant-TW, fr-CA, pt-PT → pt-BR) — RIGHT: the accept set is a superset of main's (exact-then-base), and pt-PT→pt-BR preserves main's base-language behaviour; the spec's "pt-PT is not pt-BR" is a statement about platformLocaleTagOf, whose doc itself separates "means" from "serves", and the server's resolveLocaleAmong steps 4–5 serve pt-PT with pt-BR too, so Console and server agree.
  • matchLanguageTag unchanged; now documented as the generic matcher for non-table sets (provider seed, studio) — RIGHT.
  • pickInitialLanguage (exported) answers a TAG for a built-in match (zh-TW → zh-Hant, pt-PT → pt-BR) instead of the base language; an app resources key by base language still wins — RIGHT, and the only way a Taiwan browser reaches Traditional; it is a behavioural change on a public function and the changeset's "stored codes keep working" paragraph covers it only implicitly (see ② note).
  • createI18n fallbackLng becomes an object: each tag → [fallback], each read alias → [tag, fallback], plus default — RIGHT; i18next's object lookup (code, script part, formatted code, language part, default) reaches zh-Hant-TW → zh-Hant and pt-PT → pt → pt-BR as the docblock says; an instance in a stored alias renders its tag's bundle without a second copy.
  • I18nProvider boot effect and changeLanguage write a built-in catalogue under its TAG, never the alias (the double-write/added re-render fix, pinned in provider-load-rerender-10382) — RIGHT; the instance language, the document lang attribute and the stored value stay what the user had (zh-CN, zh-TW).
  • ./namespaces/marketplace: MARKETPLACE_LOADERS typed as a Record keyed by PlatformLocaleTag, MARKETPLACE_LANGUAGE_CODES are tags, resolveMarketplaceLanguage goes through resolveBuiltInLanguage, hasMarketplaceCatalogue is a type guard, catalogue stored under the tag — RIGHT, consistent with the packs.
  • ./namespaces/studio untouched: en + zh, matchLanguageTag base match; a zh-Hans/zh-Hant/zh-TW instance reaches zh through i18next's language-part step — RIGHT per ruling app-shell: a published library that is 66% Console product surface — retire the 67 zero-reference exports now, decide the split, and fold the second i18n table into @object-ui/i18n #25922 (Traditional readers get Simplified Studio strings; declared in the changeset).

@object-ui/app-shell: LoadingScreen, outboundAgentText (PACK_TAG zh → zh-Hans), flowResponse read the snapshot by resolveBuiltInLanguage — RIGHT. pickRenderableLocale = resolveLocaleAmong (spec), so the Console's sys_user.locale pick has the server's table step (zh-TW offered zh-Hans+zh-Hant → zh-Hant; es offered es-MX,es-ES → es-ES) — RIGHT, and it closes spec#3's note 6665575988. Exported surface of app-shell (hasSignInLanguagePick, saveSignInLanguagePick, LocaleSwitcher) unchanged. LocaleSwitcher itself is not edited: it enumerates offerableLanguages, so the menu now offers zh-Hans, zh-Hant, pt-BR, labelled by Intl.DisplayNames because the hand-kept zh/pt label rows no longer match — RIGHT for phase 2 (phase 4 owns the menu), see ④ S2.

@object-ui/components: translateLabel picks the Traditional dictionary rows for any locale resolveBuiltInLanguage reads as zh-Hant (zh-Hant, zh-HK, zh-MO), Simplified otherwise — RIGHT. date-fns-locale.ts (unchanged, read) resolves zh-Hans → zh-CN, zh-Hant → zh-TW, pt-BR exact through Intl.Locale.maximize() — no regression.

@object-ui/console: vite.config.ts one i18n-locale-TAG / i18n-ns-marketplace-TAG group per table tag — RIGHT (the eager gate reads these names).

Gates (edits to existing checks, no new check:*, per ruling F):

  • check-i18n-en-drift.mjs: LOCALES = [...PLATFORM_LOCALE_TAGS]; a FOLLOWER pack absent at the base reads as empty and is named; en absent at base, or any pack absent at HEAD, still throws — RIGHT (the renamed/new packs' keys are a key-set fact the parity test owns; the excuse is before-side only and pinned both ways in check-i18n-en-drift.test.ts and git-paths-nested-layout.test.ts). The en-first assertion at lines 194–196 is WRONG as spelled: a top-level if (LOCALES[0] !== 'en') { throw … } in a scripts/** module that exports bindings runs on import, which is exactly what ui/scripts/check-entry-guard.mjs refuses ("export bindings AND run on import"); the head's Lint UI job reds on it (④ M1). The assertion belongs inside a declaration (a function the LOCALES initialiser calls), not at module top.
  • check-i18n-dead-keys.mjs: map-chain tag stripping reads PLATFORM_LOCALE_TAGS (PACK_MAP_TAGS), PACK_EXPORT_NAMES gains zhHant — RIGHT (previously it stripped export identifiers as if they were tags; the two sets are now distinct).
  • check-eager-locale-catalogues.mjs: explicit 11-tag list kept on purpose; its test pins the list to the table AND to the registry's loader table, and pins that each loader imports the file named for its tag — RIGHT.
  • check-lint-rule-coverage.mjs: UNREACHED_GROUPS row for packages/i18n/scripts/zh-hant.d.mts, card objectstack#30511; tests accept an objectstack# card — RIGHT.
  • scripts/regen-artifacts.mjs: zh-Hant.ts as NOT_DRIVER_MANAGED with gen:zh-hant — RIGHT (no check: currency proof exists, so a REGEN_ARTIFACTS row would be wrong; the package test is the proof). Note the marketplace zh-Hant.ts catalogue is NOT listed there — minor, see ④ S5.

Tests that make the card's ask real: all-locales-key-parity pins Object.keys(builtInLocales) equal to PLATFORM_LOCALE_TAGS (a table tag with no pack reds by name); region-locale-fallback-11326 covers both scripts with a probe key spelled differently in each pack (保存/儲存) and fresh module graphs; zh-hant-generated is byte-for-byte over ZH_HANT_TARGETS with a counter-probe on the list; userLocale-10059 pins the table step. the satisfies and Record-keyed-by-PlatformLocaleTag typing makes a table drift a compile error in two places. RIGHT.

Generator: zh-hant.mjs ports the site's s2twp + TWP_PHRASE_OVERRIDES + LOCATIVE_LI, adds STRADDLE_OVERRIDES (segmentation + group 0) and one phrase override, refuses to write when a pinned CONVERSION_CASES entry fails, keeps the six GATE_ONLY_KEYS out of the pack as comments so line numbers stay aligned (5034 lines both files). opencc-js pinned 1.3.1 as a devDependency; the converter never ships. RIGHT.

Head check-runs on f7e9df2d (the gate verdicts, all completed): Lint UI — failure, step "Verify every ui scripts entry guard goes through one predicate": check:entry-guard names scripts/check-i18n-en-drift.mjs:194 (if () {} at module top in a file that exports bindings). Test UI (15/16) — cancelled ("a higher priority waiting request for ci-CI-pull_request-30902 exists": the PR received a newer push, 143ef31d, at 10:25Z and the concurrency group pre-empted this shard), so the aggregate Test UI reads failure and the CI workflow run reads cancelled; shard 15 is UNMEASURED on this head, not red. Everything else is green: Test UI shards 1–14 and 16, Test Core (1/8, 2/8, aggregate), TypeScript Type Check, Type Check UI, Type Check · workspace / consumer gates / source gates / debt ledger, Build Core, Build UI Deps, Build & E2E UI, Live E2E UI, Build Docs, Lint & Repo Gates, Lint · ESLint, every scoped Lint, Lint · PM dispatch-gates, Lint · docs & code guards, Check Changeset, Governed Surface Queue Guard, Check PR Size, PR-body guards, Validate Package Dependencies, Dogfood Regression Gate; the opt-in/scheduled jobs are skipped. One derived gate is red on this head, so the gate verdict is red.

② Semver level

Clause-②: yes (narrowing) on both the PR body and the changeset body, line-initial, with the widening and the narrowing each spelled out; Metadata-protocol: none — correct: no spec key, metadata type or authorable value changes. Level: patch on @object-ui/i18n, @object-ui/app-shell, @object-ui/components, @object-ui/console — the four released packages whose shipped code the diff changes (fields and the plugin-* packages carry test edits only). Per AGENTS.md "Add a changeset": kind none ⇒ patch, and a non-protocol breaking change ships patch with its BREAKING banner, FROM → TO migration (five lines: the two builtInLocales members, the two ./locales/* modules, and the code-comparison rewrite) and exactly one ADR-0087 marker — all present; marker not-required (no-migration-prescription) is a category the gate knows (check-adr-0087-registration.mjs) and the reason given (the rename is a source rewrite on a subpath that runtime-export-retired cannot take as operand; root entry retires nothing) is true of the tree. Check Changeset is green on the head. Should-note, not a defect: the changeset's "Stored codes keep working" paragraph does not name pickInitialLanguage's changed answer (zh → zh-Hans for a Chinese browser with no stored choice); it is implied by "a browser code … read through the table's aliases".

Clause-②: yes (narrowing) — the record agrees with the declaration; patch is the right level. The FAIL below is a gate finding (④ M1), not a semver one.

③ Boundary flags

os-dev-report 6682318861 carries no open_questions key and no dev flag; what it does raise:

Nothing escalated.

④ Design judgment

The narrowing against the published spec on main (packages/spec/src/system/platform-locales.ts): the table declares (a) one pack per tag on every surface, (b) stored values written before the table keep resolving through PLATFORM_LOCALE_READ_ALIASES, (c) new writes use the table's tags, (d) a closed alias list with no guessed region/script fallback in platformLocaleTagOf itself, and (e) a label per row for language menus. This diff makes (a) real for the Console (the parity suite and two Record-keyed-by-PlatformLocaleTag types turn a table/pack mismatch red), honours (b) at every reader found (registry, i18next fallbackLng, provider boot and switch, marketplace namespace, LoadingScreen, outboundAgentText, flowResponse, translateLabel, pickRenderableLocale), and does (c) at the two write sites (LocaleSwitcher → useLanguageSelection → sys_user.locale, and the device slot), where the server's locale_bcp47_shape rule (SYS_USER_LOCALE_TAG_PATTERN) accepts zh-Hans/zh-Hant/pt-BR and the server's resolveLocaleAmong table step serves a stored zh-Hans from its zh-CN catalogue. Nothing a consumer could do on main is taken away: builtInLocales.zh / .pt and the two module paths are the only removals, each with a one-line rewrite, and every code that used to load a pack still loads one (a strict superset). The Console's prefix walk beyond the closed alias list (pt-PT → pt-BR, fr-CA → fr) does not contradict (d): the spec separates "which tag a code MEANS" from "which key SERVES it", and the serving ladder it publishes (resolveLocaleAmong steps 4–5) answers pt-PT with pt-BR as well. Attempts to break it: a stored zh/zh-CN/pt row (alias → tag; pinned); a cached device choice zh (isStaticallyKnownLanguage → isBuiltInLanguage('zh') true, so it is kept and renders zh-Hans through fallbackLng); a zh-TW browser with no choice (pickInitialLanguage → zh-Hant, previously zh); a zh-Hant-HK browser (alias); a deployment offering zh-CN+zh-TW for a stored zh-HK (zh-TW, via the table step); an instance in zh-TW whose zh-Hant catalogue lands after mount (reaches it, pinned); the marketplace lazy loader for a zh-TW session (tag-typed, zh-Hant); a zh-Hans date picker (date-fns zh-CN via maximize()); Accept-Language: zh-Hant to a server with zh-CN only (variant step → Simplified, the declared Studio/server state). The narrowing itself holds; the one must-fix is a gate the head reds, not the design.

Must-fix (a FAIL on this head):

  • M1 ui/scripts/check-i18n-en-drift.mjs:194–196 — the en-first assertion is a top-level if … throw in a scripts/** module that exports LOCALES, FOLLOWERS, readPacks, analyze and more, so importing the gate for its exports (its own test does; check-entry-guard's rule is that a top-level process.exit/throw inside an importer is read as the importer's result) runs code on import. ui/scripts/check-entry-guard.mjs refuses it and the head's Lint UI check-run is red on exactly this line. Fix: move the assertion into a declaration — e.g. export const LOCALES = Object.freeze(enFirstTableTags(PLATFORM_LOCALE_TAGS)) with the throw inside enFirstTableTags — so the module's top level only declares. (The PR has since received 143ef31d, a one-file commit that does this; it is not the head this record names, and the seat re-renders the record there, with Test UI (15/16) measured.)

Should-fix (none blocks a corrected head):

  • S1 pickLocalized / localeWriteKey (ui/packages/i18n/src/pickLocalized.ts, unchanged): a zh-Hant session reading an inline map { 'zh-CN': …, 'zh-TW': … } takes the first zh-* key (Simplified) and an edit in that session writes back to that same zh-CN entry. Not a regression (a zh-TW browser resolved to zh on main and did the same; a stored zh-TW still exact-matches), but after this PR the chrome is Traditional while authored labels are Simplified. The dev named it "follow-up candidate, not filed": file the card (teach both limbs the table's reading) before phase 4 lands.
  • S2 LocaleSwitcher.tsx BUILT_IN_LABELS rows zh and pt are dead after this PR; 简体中文, 繁體中文, Português (Brasil) now come from Intl.DisplayNames, the ICU-dependent source the file's own header says it avoids. Phase 4 owns the menu; it should read PLATFORM_LOCALES[].label (the table carries exactly these names) and drop the hand map. ProfilePage.tsx already derives names and is unaffected.
  • S3 skills/objectui/guides/i18n.md teaches ten packs with zh and pt — after this lands it teaches a retired key and module path. Governed Tier H, so a separate card/PR; file it before landing so the follow-up is a tracked card, not a line in a changeset.
  • S4 Server catalogues (packages/core memory-i18n) and the studio namespace carry no zh-Hant; a Traditional Console shows Simplified server-translated labels and Studio strings. Both are declared (changeset, PR body, ruling app-shell: a published library that is 66% Console product surface — retire the 67 zero-reference exports now, decide the split, and fold the second i18n table into @object-ui/i18n #25922) and out of this card's lane; the tracking card Platform locale table (ruling F of #29349): one list of 11 languages in spec, complete on every surface #29428 should carry the server backfill as its own phase.
  • S5 scripts/regen-artifacts.mjs lists only ui/packages/i18n/src/locales/zh-Hant.ts as NOT_DRIVER_MANAGED; ui/packages/i18n/src/namespaces/marketplace/zh-Hant.ts is generated by the same command and belongs in the same ledger entry (or a second row), so the merge driver treats both alike.

Implemented-by: claude/project-thread-f2dhrw
Reviewed-by: session_01L9qibw7DekEe3wx5f3zphf

VERDICT: FAIL

…talogue beside the pack's

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

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

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

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

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

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

  • Dogfood Regression Gate (3/3) — 失败步骤: Boot example apps and exercise real user flows

    @objectstack/dogfood:test:  FAIL   isolated  test/admin-platform-admin-standing.dogfood.test.ts > #9482: what an ObjectStack platform admin gets from every /admin/ route > no /admin/ route refuses the
      ↳ 失败原因: @objectstack/dogfood:test: AssertionError: the stack serves /admin/ route(s) this file neither admits, refuses-by-design, nor classifies as answering for a non-authorization reason. A new admin route 
    

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

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

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

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 15 个失败构建(不含本次)。

分诊清单:

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibyWSiXh6B2sMVHfXYC

Merge-queue build 42258464763 failed in Dogfood Regression Gate (3/3): test/admin-platform-admin-standing.dogfood.test.ts reports that the stack serves an /admin/ route the file does not classify. That is a server route census. This PR adds no route and changes no server code: outside ui/** and the changeset, its diff is pnpm-lock.yaml (an opencc-js devDependency of @object-ui/i18n) and one ledger row in scripts/regen-artifacts.mjs. The same signature ejected the whole speculative stack (#30902, #30954, #30991, #30998, #31002), and the summary issue is #31012. The fix belongs to whichever PR adds that route, or to #31012, not to this PR. No re-queue is made from this side; the seat decides once #31012 is resolved or the stack ahead clears.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibw7DekEe3wx5f3zphf

重投 · 2026-11-06T17:26Z · head e0985fea7ac1c5771baf6dc1fc55c44bdc3eefed · stack victim

Queue build 42258464763 red on Dogfood Regression Gate (3/3): test/admin-platform-admin-standing.dogfood.test.ts, an unclassified /admin/ route in the served route census. This is a different signature from the AiChatPage timeout, which did not recur. The anchor issue #31012 reads it as 1 independent hit: root #30991 (S1 · root, two builds), with #30902, #30954, #30998 and #31002 inheriting it through speculative stacking. This PR adds no route and touches no server code; outside ui/** and the changeset its diff is pnpm-lock.yaml (opencc-js) and one regen-artifacts.mjs ledger row (dev comment 6691066708). The root #30991 left the queue at 17:15Z with auto-merge off, so the break is no longer in the stack. Re-queued on the same head.

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 6, 2026
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 6, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 6, 2026
…d tests

main's test-source-alias gate now follows a changed test into the source
it loads and reds a cold literal import() paid there. Three tests this
branch edits reach one: the installed-apps pins test (through the Setup
packages page into the CEL authoring helpers, @objectstack/formula and
@objectstack/lint) and two components tests (through the page renderer
into @object-ui/react-runtime). Import each at module top so the first
load is paid during collection, not inside a case's bounded wait.

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

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibw7DekEe3wx5f3zphf

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d9b3ddde571af4950702457bce4edb4de51ccce2
Local-runs: none
Scope: delta since e0985fea (PASS #3, comment 6688603790) — two commits: 99b8d9ee (merge of origin/main@7d9f1e97 into the branch, via os-regen-merge) and d9b3ddde (11 added lines, three test files: ui/packages/app-shell/src/console/marketplace/__tests__/MarketplaceRowActions.installedAppsPins.test.tsx +5, ui/packages/components/src/__tests__/page-tabs-builtin-label-i18n-4645.test.tsx +3, ui/packages/components/src/__tests__/renderer-copy-locale-25916.test.tsx +3). Carried forward by reference: the full review on f7e9df2d (comment 6682790803, FAIL, M1 closed in 143ef31d) and PASS #3 on e0985fea (comment 6688603790, which folds in the unposted #2 on 82771dc0). Cited this round: scripts/check-test-source-alias.mjs at main@7d9f1e97 (#30969, the source-reach rule: header "The source reach", sourceReachFindings, sourceReachFailure), .github/workflows/lint.yml:3710–3714 (job lint-ci-engine runs pnpm check:test-source-alias), ui/vitest.config.mts:525, ui/packages/app-shell/src/views/metadata-admin/celAuthoring.ts:252,281, ui/packages/components/src/renderers/layout/react-page.tsx:269, ui/packages/i18n/src/namespaces/studio/{en,zh}.ts, the ejected queue run 42267948651 (merge_group on d167fcaf, the queue's own merge of e0985fea with 7d9f1e97; job 128064683101 Lint · CI, release & engine gates red), and the 57 check-runs on d9b3ddde.

① Derived judgments

  1. The merge is git's own. git merge-tree --write-tree e0985fea 7d9f1e97 = 6515b3172d27593f9d5714456eb18cc05663481c = 99b8d9ee^{tree}, byte for byte. git diff --stat 99b8d9ee^2 99b8d9ee is 153 files, +7706/−825 — the same PR shape as at e0985fea. No hand resolution; no conflict was resolved by anyone.

  2. Nothing of the PR's own surface moved in the merge. ui/packages/i18n/src/locales/{zh-Hans,zh-Hant}.ts, ui/packages/i18n/src/namespaces/marketplace/{zh-Hans,zh-Hant}.ts, ui/packages/i18n/scripts/{gen-zh-hant,zh-hant}.mjs are blob-identical between e0985fea and d9b3ddde. Main's range 1b3e22f0..7d9f1e97 touches ui/packages/i18n in exactly two files, namespaces/studio/en.ts and namespaces/studio/zh.ts (+2 keys each, engine.packages.detail.disableMayBeRefused / disableRestricted, from fix(runtime)!: the package disable door refuses while an enabled package's validation rule reads a field the package adds #30825); both keys are present at head (studio/en.ts:1936–1937, studio/zh.ts:1748–1749). The studio namespace is the one this PR does not touch (S4, Platform locale table (ruling F of #29349): one list of 11 languages in spec, complete on every surface #29428), so the merge had nothing to reconcile there and the dev's "gen:zh-hant reports both targets current" is what the blobs say too: a regen with no change to make.

  3. zh-Hant is structurally the generator's output, both targets. Same Han-run-collapsed compare as rounds ✨ Set up Copilot instructions #2/Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #3, on the head blobs: console pack zh-Hans.ts 5066 lines vs zh-Hant.ts 5066, bodies 5057 vs 5057, zero non-Han differences with the six GATE_ONLY_KEYS rendered as the generator's comment line; marketplace pair bodies 234 vs 234, zero differences; both carry the // @generated by ui/packages/i18n/scripts/gen-zh-hant.mjs header. Unchanged conclusion, unchanged bytes.

  4. The PR's lockfile hunk is unchanged. git diff 7d9f1e97 d9b3ddde -- pnpm-lock.yaml (11 insertions, the opencc-js devDependency of @object-ui/i18n) is identical, after stripping @@ and index lines, to the PR's original hunk 98f640d5..c1f166cc. Main's lockfile movement is only context.

  5. No new retired-key consumer on main. The grep for builtInLocales.zh|pt, builtInLocales['zh'|'pt'], getLoadedBuiltInLocales().zh|pt, i18n/locales/zh|pt, ../locales/zh|pt, ./zh|pt, resident['zh'|'pt'] over ui/ and packages/ on main@7d9f1e97 returns the same single file it did at 1b3e22f0 (AuditLogPage.entryPage-30748.test.tsx, already rewritten in e0985fea); no file new in 1b3e22f0..7d9f1e97 matches; the head d9b3ddde has zero hits.

  6. The 11 lines are exactly what ci(test-source-alias): follow a changed test into the source it loads, and red a cold literal import() paid there #30969's rule prints as the remedy, and nothing else. The ejected queue job 128064683101 printed four findings from sourceReachFindings (one per test, specifier): MarketplaceRowActions.installedAppsPins.test.tsx:39 reaches import('@objectstack/formula') at celAuthoring.ts:252 and import('@objectstack/lint') at celAuthoring.ts:281 (and more), both through PackagesPage.tsx; page-tabs-builtin-label-i18n-4645.test.tsx:46 and renderer-copy-locale-25916.test.tsx:37 each reach import('@object-ui/react-runtime') at react-page.tsx:269 through renderers/index.ts → renderers/layout/index.ts. The remedy the gate prints is "Add a module-top side-effect import to THIS test file … import 'SPEC'; (SPEC being the specifier named) The source keeps its dynamic import". d9b3ddde adds precisely those four import '…'; lines (two in the marketplace test, one in each components test) plus seven comment lines explaining them; no source file, no vi.mock, no timeout, no config changes. All three specifiers are declared deps of their packages (app-shell/package.json:91–92, components/package.json:39). @object-ui/react-runtime is in addition aliased to source in ui/vitest.config.mts:525, so under the ui workspace runner the warmed transform is a source transform; the remedy is the same either way and costs the run nothing it was not already paying inside a case's window. On d9b3ddde the gate's own census line reads: "the source reach judged 237 test file(s) changed since 7d9f1e9 … OK".

  7. Why these three tests and not others. The rule judges only test files the change adds or edits, measured from the merge base. The three are PR-edited (they read builtInLocales['zh-Hans'] now); their collection-time reach into PackagesPage and ../renderers predates this PR. The finding is against the test as the rule defines it, not a regression this PR introduced in source, and the PR owes exactly the line the rule asks for — which is what it added.

② Semver level

Unchanged from the published record and PASS #3: .changeset/30511-console-packs-platform-locale-tags.md is blob-identical since c1f166cc; the delta adds no exported symbol, no protocol kind, no runtime behaviour — test-file imports and a clean merge of main. patch with the BREAKING banner, FROM→TO and one ADR-0087 marker stands.

Clause-②: yes (narrowing) — the published surface removed is the zh/pt pack keys on builtInLocales (replaced by table tags with read aliases); nothing in this delta widens or further narrows it.

③ Boundary flags

  • Dev flag "gen:zh-hant reports both targets current" — confirmed by blob identity (judgment 2) and the structural compare (judgment 3). No local run was made; none was needed.
  • Dev flag "11 lines, all module-top side-effect imports, no source or timeout change" — confirmed against the diff and against the gate's printed remedy (judgment 6).
  • Queue ejection 42267948651 — judged, not this PR's regression. The red is Lint · CI, release & engine gates on the queue's merge d167fcaf, from a rule (ci(test-source-alias): follow a changed test into the source it loads, and red a cold literal import() paid there #30969, acd190b2) that landed on main after PASS Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #3. The PR's head e0985fea could not have carried the lines before the rule existed. The remedy is in d9b3ddde and the same job is green on the head (job 128080920346, "ok Test-source alias gate").
  • PD14 / Tier H: the delta touches no governed surface (skills/**, docs/adr/** changes at head are main's own, arriving through the merge). S3 (the skills/objectui/guides/i18n.md follow-up card to be filed before landing) is still owed and is unchanged by this delta.
  • Should-fix list S1 (pickLocalized/localeWriteKey zh-Hant inline maps), S2 (LocaleSwitcher dead zh/pt label rows), S3 (Tier H guide card), S4 (server catalogues and studio namespace lack zh-Hant, Platform locale table (ruling F of #29349): one list of 11 languages in spec, complete on every surface #29428) carry forward unchanged; S5 closed in c1f166cc. None is a must-fix. Main's two new studio keys landing in studio/zh.ts (not zh-Hans.ts) is S4's shape, already recorded.
  • Earlier queue reds (Test UI (6/16) AiChatPage 4000 ms window, run 42251411272; Dogfood Regression Gate (3/3) admin-route census, run 42258464763, anchor Queue-flake anchor: test/admin-platform-admin-standing.dogfood.test.ts #31012) were judged in the thread as not this PR's; neither recurs on d9b3ddde's check-runs and this record adds nothing to them.

④ Design judgment

Unchanged from comment 6682790803 and PASS #3. The delta is maintenance: a byte-exact git merge of main, and the four warm imports a new main gate asks of PR-edited tests, placed where the gate says and explained in comments that name the chain. Warming a lazy chunk at a test's module top decides where the first transform is paid; it does not change what the source lazily loads or what the tests assert, so the narrowing (table tags in, zh/pt keys out, read aliases for stored codes) is exactly as reviewed.

CI on d9b3ddde: 57 check-runs, 50 success, 7 skipped (opt-in and schedule-gated lanes), 0 failure, 0 cancelled, 0 in progress — including Lint UI, Lint · CI, release & engine gates (the test-source alias gate), Test UI 16/16 shards, Type Check UI, Build & E2E UI, Live E2E UI, Check Changeset, Governed Surface Queue Guard. PR state: open, not draft, mergeable: true, mergeable_state: clean, base main, 153 files +7717/−825 vs merge base 7d9f1e97.

Implemented-by: claude/project-thread-f2dhrw
Reviewed-by: session_01L9qibw7DekEe3wx5f3zphf

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibw7DekEe3wx5f3zphf

席位核对 · ACCEPT · 2026-11-06T19:15Z · head d9b3ddde571af4950702457bce4edb4de51ccce2

Delta ACCEPT over 6688640149 (head e0985fea). Queue run 42267948651 ejected the PR at 18:24Z on the Test-source alias gate's new source-reach half (main #30969, acd190b2). That is a merge-result conflict, not a flake, and main's base was green on that lane.

  1. Shape: 99b8d9ee merges main@7d9f1e97, reproduced byte for byte by git merge-tree; both zh-Hant targets are still current, blob-identical to e0985fea. d9b3ddde adds 11 lines across three test files: module-top side-effect imports of @objectstack/formula and @objectstack/lint (MarketplaceRowActions.installedAppsPins.test.tsx) and of @object-ui/react-runtime (page-tabs-builtin-label-i18n-4645.test.tsx, renderer-copy-locale-25916.test.tsx). These are exactly the gate's four printed findings and its printed remedy. No source, mock, timeout or config change. 153 files, +7717/−825.
  2. Gates / check-runs: 57 check runs on this head, 50 success, 7 skipped, 0 red, including Lint · CI, release & engine gates (source reach judged 237 changed test files, OK). The local derived sweep ran 76 of 76 gates, all exit 0, --ran reconciled. mergeable_state: clean.
  3. Contract review: CONTRACT_REVIEW_TIER delta PASS 6693289236 on this head, the same reviewer as before.
  4. Landing: Tier H by size. The os-zhuang approval counts on any commit (governed guard: an authorized APPROVED review lifts the >5000-line SIZE limb, as it already lifts Tier H paths #20153, pm-dispatch ③). The seat relays automerge_enable. Part of #30511.

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

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants