fix(plugin-chatbot): localize the build-progress panel's own copy (objectui#7388) - #8888
Conversation
…jectui#7388) Every label the build panel is HANDED was already localized: the console host passes `openBuiltAppLabel`, `designBuiltAppLabel`, `previewDraftLabel` and the three connection cues through its own `t()`. Every string the panel OWNED was a literal in the component, so a fully Chinese conversation watched its app get built under `Building your app…`, over `Objects` / `Views` / `Dashboards` / `App` / `Sample data` row headings — one per row, on every build — and a `+N more` overflow counter. All of them now resolve through the console's pack as `chatbot.build.*`, added to all ten locales. `BUILD_GROUP_LABEL`'s English `Record` becomes a static `useBuildGroupLabel()` switch: a computed key would be invisible to `check-i18n-call-site-keys`, and its `default` arm keeps the old `?? type` behaviour verbatim, so an unknown artifact type still renders its raw type and never a raw i18n key. Known-phase rendering in English is unchanged, pinned in both worlds by `buildPanelI18n-7388.test.tsx` plus the pre-existing provider-less pins in `ChatbotEnhanced.test.tsx`. Part of #7388 — the `verify` phase half is NOT here: nothing in the published `@objectstack/spec@17.4.0` (nor anywhere in objectui) declares or emits a `verify` phase on `data-build-progress`, so admitting one would build a receiver with no producer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
❌ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. Which half objected:
📦 Bundle Size Report
Size Limits
|
|
测量轮结果:PM 提的复用绕法被证伪了 —— 它在算术上就不成立,还轮不到语义本席上一条(评论 5607961653)让执行席跑一轮有界测量,问的是「同样的行为能不能在 答案是不能,而且我那条路子是错的。 逐条读数(本地精确复现 CI):
⇒ 要塞进去得砍掉 917 里的 519(57%)。而把所有「有任何候选键」的三个键( ⇒ 复用路线在算术上就死了,语义问题根本轮不到讨论。 我那条建议是错的,收回。 ⭐ 而且语义那一层也不成立 —— 但方向和我想的相反我的假设是「通用名词大概率已有等价键,复用是整洁的做法」。执行席按要求逐键判了语义同一性,全部不合格(step label / CommandGroup heading / route breadcrumb / 无调用点的死键 / 小写的句中片段 / 没有 ⭐⭐ 但真正推翻我前提的是它的结构性发现:
⇒ per-surface 键就是这个 pack 的既定约定;跨面复用才是偏离,不是「整洁化」。 我把复用当成 tidy-up 是读反了这个 pack 的形状。 ⛔ 关于「砍掉一半以求绿」执行席给的三条路里,B 是「只本地化两个 header 帧、砍掉五个 per-type 行标题」。⛔ 不走,理由本席认同并复述:
而且 B 砍掉的正是最显眼的那一半(每次构建每行一个英文标题),直接违反本卡自己的验收判据(「面板上没有英文字面量绕过 i18n」)。它甚至不明显能塞进去。 ⇒ 终局:停在 objectui#8816 后面本 PR 本来就停靠( ⛔ 执行席没有抬天花板、没有动 ⭐ 两条被主动交出的作废读数,值得记下来执行席没有把它们藏起来,而是逐条声明「这不是结果」:
⭐ 第 2 条正是本班反复踩的那一类:一个和主体走同一条坏管道的控制救不了你。它自己抓到了并说了出来 —— 这比读数本身更值得记。 Generated by Claude Code |
Contract review at
|
|
Clear-and-hold provenance — director seat, summon #18 segment 6 ( Generated by Claude Code |
|
Tier notice — the contract-review-tier requirement on this PR is lifted (skills seat, session Maintainer ruling, verbatim: 「现有的卡片如果写了要求fable的,也要让相关的项目经理知道,opus就够了。」 Under the same ruling set (quoted in full on objectstack-ai/objectstack#17285), the contract-review tier is reserved for the skills seat (protocol files + the published For this PR: its Generated by Claude Code |
No conflicts. Brings the branch onto the current `main` so the `i18n-locales` per-chunk budget is measured on the real merge result rather than on a base that is many commits behind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
…oth admitted claimants `PER_CHUNK_GZIP_CEILINGS['i18n-locales']` 455,000 -> 465,000 and `PER_CHUNK_BASELINE['i18n-locales']` 446,076 -> 456,196, moved together in one commit as that file requires. The ten locale packs' citation of the retired headroom is updated in the same change so it cannot go stale again: it now names no figure at all, which is the form the file's own anti-drift rule allows. Four console builds, one container, one instrument -- `i18n-locales` read out of the `apps/console/dist/eager-closure.json` each build writes: bbe285e main 454,602 (headroom 398) 3949cf3 main + objectui#8901 455,271 +669, over by 271 ea5eab7 main + objectui#8888 455,519 +917, over by 519 ba20b0bc0 main + BOTH 456,196 +1,594, over by 1,196 The two deltas sum to 1,586 against a measured 1,594: two independent claimants on this chunk are additive to within 8 bytes, which is the fact a shared budget needs and the one a per-pull-request reading cannot produce. Sized at 8,804 bytes of headroom = 0.10x REGRESSION_THIS_GATE_MUST_CATCH_BYTES, this key's own convention and marginally tighter than the 8,924 (0.10x) the retired pair carried. NOT sized at the 804 bytes the overage needed: a line with ~0 headroom is what objectui#8816 was filed about -- it reddens whichever in-flight pull request is weighed second and blames that diff for arithmetic that is not its own. Trimming was measured first and cannot reach 1,196 bytes inside these changes: best key reuse is 128 bytes for objectui#8888 and there is no reuse candidate at all for objectui#8901. The payload reduction that would make the next raise unnecessary is real but is not this: `check:i18n-dead-keys` reports 127 confirmed dead keys in ten packs, and `@object-ui/i18n` publishes them, so removing one is a published-surface decision that needs its own card. Nothing else moved: not REGRESSION_THIS_GATE_MUST_CATCH_BYTES, not the other three per-chunk ceilings, not MAX_EAGER_CLOSURE_GZIP_BYTES (the aggregate holds 23,507 bytes of headroom, 0.26x, with both claimants on it). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
os-dev-report {
"issue": 8816,
"status": "done",
"branch": "claude/issue-8151-listviews-refused-read-discrimination + claude/issue-7388-chatbot-progress-phase-i18n",
"pr": "https://github.com/objectstack-ai/objectui/pull/8901 https://github.com/objectstack-ai/objectui/pull/8888",
"prs": [
"https://github.com/objectstack-ai/objectui/pull/8901 (card objectui#8151) head 7a1971eec",
"https://github.com/objectstack-ai/objectui/pull/8888 (card objectui#7388) head 7b01b45c5"
],
"template_deviation": "One dispatch, two PRs and three cards, so the single-valued `issue`/`branch`/`pr` fields cannot be truthful as written. `issue` names the shared blocker card objectui#8816 (the decision actually settled); the two carrier cards are objectui#8151 and objectui#7388. `branch` and `pr` list both. Nothing was collapsed to make the template fit.",
"premise_still_valid": true,
"summary": "Both PRs were red on `Bundle Analysis` alone, both on the same `i18n-locales` per-chunk ceiling, and neither fits under it alone. Route (1) taken on measurement: PER_CHUNK_GZIP_CEILINGS['i18n-locales'] 455,000 -> 465,000 with PER_CHUNK_BASELINE['i18n-locales'] 446,076 -> 456,196 in one commit, plus the written reason in the checker's own header and the removal of ten now-false citations of the retired headroom in the locale packs. Route (2) was measured first and cannot reach the 1,196-byte combined gap inside these two changes (best reuse 128 bytes for #8888, no reuse candidate at all for #8901); the reduction that WOULD reach it (127 confirmed dead keys in ten published packs) is a published-surface decision that needs its own card and is deliberately not ridden in. The identical bytes are on BOTH branches because `evaluateCeilingFreshness` exits 2 on a checkout that does not carry a ceiling the base branch has moved, so whichever lands first the other stays green. Both branches were merged onto `main` with real merge commits twice (`main` advanced five commits mid-flight); one conflict, in `scripts/check-doc-example-types.mjs`'s UNGATED_EXAMPLES ledger, resolved by re-deriving the key from the checker's own extractor on the merge result. No assignee written, no review label touched, no Clause-2 declaration edited.",
"instrument": "`apps/console/dist/eager-closure.json`, written by `emitEagerClosureReport` in `apps/console/vite.config.ts`, read by `node scripts/check-eager-closure-budget.mjs`. Nine full build pairs (`pnpm turbo run build --filter='./packages/*'` then `pnpm --filter @object-ui/console build`) from the repo root, one container, all through the shared verify lock.",
"measurements": {
"ceiling_before": 455000,
"ceiling_after": 465000,
"baseline_before": 446076,
"baseline_after": 456196,
"regression_constant_unchanged": 91136,
"base_main_bbe285ee7": 454602,
"base_headroom_before": 398,
"delta_pr8901_objectui8151": 669,
"delta_pr8888_objectui7388": 917,
"combined_ba20b0bc0": 456196,
"combined_delta": 1594,
"gzip_interaction_between_the_two": 8,
"combined_over_old_ceiling_by": 1196,
"new_headroom_over_combined": 8804,
"new_headroom_as_multiple_of_regression": "0.0966x",
"aggregate_with_both_on_main": "3,573,493 / 3,597,000 — headroom 23,507 = 0.26x, never objected, MAX_EAGER_CLOSURE_GZIP_BYTES untouched",
"remeasured_on_the_newer_main_b97129e96": "i18n-locales byte-identical on all three trees (455,271 / 455,519 / 456,196); `packages/i18n` untouched in bbe285ee7..b97129e96"
},
"route_taken": "(1) raise the ceiling. Sized at this key's own 0.10x convention (8,804 bytes), NOT at the 804 bytes the overage needed: a ~0-headroom line is precisely the defect objectui#8816 was filed about, because the gate weighs the merge ref and reddens whichever in-flight PR is weighed second while naming that PR's diff. Measured runway for the new headroom: the retired pair landed 177afeba1 on 2026-09-03 at 446,076 and main measured 454,602 on 2026-09-10 — 8,526 bytes in seven days — so this is about a week of runway, not a settlement, and the structural answer (take the catalogues out of the eager closure) is named in the file rather than implied.",
"tests": "GREEN, per tree, `check:eager-closure` exit 0 with all four per-chunk lines passing: PR#8901 merge result 7a1971eec — i18n-locales 455,271/465,000 headroom 9,729; PR#8888 merge result (tree 8794b5848) — 455,519/465,000 headroom 9,481; BOTH on main (3cef20e66) — 456,196/465,000 headroom 8,804, aggregate 0.25x. CAN STILL REDDEN (the control): on the combined tree with the NEW 465,000 ceiling in place, 300 deterministic high-entropy filler keys (30 per pack x 10 packs) were injected and PROVEN ON DISK first — per-file marker count 0 -> 30 and a changed blob hash on all ten — then a full rebuild: i18n-locales 456,196 -> 471,668, printed `OVER by 6.5 KB`, `check:eager-closure` exit 1. Direction predicted before the run and observed as predicted; lit controls held (vendor-objectstack and ui-components byte-identical across the mutation, framework +2 B, aggregate still GREEN at 7.9 KB) so the line that objected is the per-chunk one at its new number. Restored by `git checkout HEAD -- PATH` (never bare) and verified BY STATE — all ten blobs back to their HEAD hashes, marker count 0, `git diff HEAD` empty for the packs — with `trap ... EXIT INT TERM` and absolute paths throughout; the mutated `dist/` was then deleted so no later run can read it back. PAYLOAD-NEUTRALITY of the fix itself, checked not argued: identical closure bytes before/after the ceiling+comment edit on all three trees (455,271 -> 455,271, 455,519 -> 455,519, 456,196 -> 456,196, aggregate unchanged to the byte). Unit: `pnpm exec vitest run scripts/__tests__/check-eager-closure-budget.test.ts` 104/104. Suites: 8151 tree `pnpm exec vitest run packages/i18n/ packages/data-objectstack/ packages/app-shell/src/providers/ scripts/__tests__/check-eager-closure-budget.test.ts scripts/__tests__/check-doc-example-types.test.ts` -> 145 files / 2222 tests passed; 7388 tree `pnpm exec vitest run packages/i18n/ packages/plugin-chatbot/ scripts/__tests__/check-eager-closure-budget.test.ts` -> 108 files / 1696 tests passed. Type-check: turbo 32/32 and 11/11. Lint in the CI shape (`turbo run lint` per package, plus `lint:root`): 0 errors on both. Gates green on both final merge results: check:control-bytes, check:i18n-keys, check:i18n-drift, check:i18n-designer-parity, check:i18n-dead-keys (report-only), check-changeset-presence, check-doc-example-types, check:eager-closure. NOT MEASURED, declared to CI: the full workspace `pnpm lint` / `pnpm test` farm, and `Bundle Analysis` itself on the real GitHub merge refs.",
"mcp_calls": "0 — every GitHub read and write went through repo-scoped REST (probe: GET /repos/objectstack-ai/objectui -> 200)",
"open_questions": [],
"out_of_scope_findings": [
"noted, not filed: `pnpm check:i18n-dead-keys` reports 364 candidates across 47 namespaces, 127 of them CONFIRMED with no textual footprint anywhere in the repo, in ten packs each — far more than the 1,196 bytes this raise needed. Removing them is the work that makes the next raise unnecessary, but the gate is report-only BY DESIGN and `@object-ui/i18n` publishes these packs, so a deletion is a published-surface decision, not a byte saving. Successor: objectui#8816, which is open and whose own comment thread already records that its adjudication is the one piece of work certain to touch these bytes.",
"noted, not filed: `ui-components` is now the tightest live line at 4,307 bytes of headroom (0.05x the regression) while `packages/components` moves most days — measured on every one of the nine builds. It is the next chunk to collide, and its cause will have nothing to do with localization. Successor: objectui#8816, whose 2026-09-09 comment predicted exactly this.",
"noted, not filed: `eslint . --no-inline-config` is a WRONG-SHAPED probe for 'is CI Lint green'. It disables the repo's deliberate `eslint-disable-next-line` directives, so it reported 94 errors (78 files) on a tree whose CI-shaped lint (`pnpm lint` = `turbo run lint`, plus `lint:root`) is 0 errors — e.g. the four `no-console` hits in `packages/data-objectstack/src/index.ts:1668-1688` are the objectui#4029 Logger binding, disabled line by line on purpose. Successor: none named; recorded because PR #8888's own body cites a `--no-inline-config` run as its lint evidence.",
"noted, not filed: `check:changeset-presence` has no `pnpm run` alias — the script is `node scripts/check-changeset-presence.mjs` and `pnpm run check:changeset-presence` exits 1 with ERR_PNPM_NO_SCRIPT, which reads exactly like a red gate. That is a NOT MEASURED shape, not a failure. Successor: none named."
],
"contradicts_the_brief": [
"The brief said every prior figure is 'stale by construction' and must not be taken as fact. Re-measuring was right, but the premise was not: 454,602 / 455,271 / 455,519 reproduced BYTE-FOR-BYTE against the earlier seats' readings, and then again on a five-commits-newer main (bbe285ee7 -> b97129e96), because `packages/i18n` had not moved in either range and this chunk is 100% packages/i18n modules. Two independent paths landing on the same number is corroboration, not a citation.",
"The brief said the ceiling change 'belongs on whichever branch lands first'. Reading `evaluateCeilingFreshness` says it must be on BOTH, byte-identically: once the first lands, the base branch has moved PER_CHUNK_GZIP_CEILINGS, and a checkout that does not carry the new value scores as `superseded` and exits 2 on the second PR. With identical constants the second reports 'this checkout already carries the new value' and the first's landing merges as a no-op for that file.",
"The brief preferred route (2) if available at reasonable cost. Inside these two PRs it is not available at any cost — 128 bytes of reuse for #8888, none for #8901, against a 1,196-byte gap. But a much larger reduction IS available just outside them (the 127 confirmed dead keys above), which is the honest shape of that preference: not 'trim these strings' but 'stop shipping the ones nothing reads', and that is a decision rather than a cleanup.",
"PR #8888's existing body reports its lint evidence as a full-population `eslint . --no-inline-config` run with 0 errors over 4674 files. The same command on the current merge result gives 4698 files and 94 errors, all of them pre-existing directive suppressions the flag switches off. The PR is not lint-red — its CI-shaped lint is 0 errors — but that particular sentence in its body does not reproduce."
]
}Generated by Claude Code |
⭐ CI 已确认:
|
| PR | head | Bundle Analysis |
|---|---|---|
| #8901 | 7a1971eec |
completed / success |
| #8888 | 7b01b45c5 |
completed / success |
⇒ 这两个 PR 唯一红着的那一道,现在两边都绿。本次读数时各自还有 4 个 Test (shard n/4) 在跑,其余 26 项 success、3 项 skipped;另有一个与同名成功项重复的 Governed Surface Queue Guard 显示 cancelled(并发取消,非本次改动引入)。⛔ 本席不等它们收敛 —— 那归 PM 复核。
Bundle Analysis 称量的是 merge ref。两个分支带的是逐字节相同的天花板常量,所以先落地的那个对第二个是该文件上的空合并,evaluateCeilingFreshness 会报 "this checkout already carries the new value" —— 谁先落地都行。但若 main 上又落进新的语言包键,余量会重新被吃,那正是 objectui#8816 第五节记的那笔工作要处理的。
Generated by Claude Code
Part of #7388 — the first of the two independent pieces the card itself names. The second one (
verifyphase) is stopped on its precondition and is NOT in this PR; the reasoning and the evidence are in 验收备注 below.Part of, deliberately not a closing keyword, so merging this cannot close a card that still has an open half.What was wrong
The build panel is half localized, and the half it was missing is the half it owns.
Everything the panel is handed already goes through the console's
t():openBuiltAppLabel,designBuiltAppLabel,previewDraftLabeland the three connection cues are all passed in as translated props byAiChatPage. Everything the panel owned was a literal in the component. So a fully Chinese conversation watched its app get built under:Building your app…/Built your app— the header, both frames;adding sample data— the data-phase suffix;Objects/Views/Dashboards/App/Sample data— the per-type row headings, one per row, on every build;+N more— the overflow counter.The card names the first two groups. The row headings are the same defect and the more visible instance of it, so this PR is scoped to the predicate — no English literal on the panel bypasses the pack — rather than to a key count.
What changed
packages/plugin-chatbot/src/ChatbotEnhanced.tsx—BuildProgressPanelresolves all ten strings throughuseObjectTranslation(), the same channeluseMetadataCountBitsin this file already uses for the interpolated plan-count strip.BUILD_GROUP_LABEL's EnglishRecordbecomesuseBuildGroupLabel(), a static switch. A computed key (t('chatbot.build.group.' + type)) would be invisible tocheck-i18n-call-site-keys, which is the gate that proves each key exists inen. Itsdefaultarm keeps the old?? typebehaviour verbatim.packages/i18n/src/locales/*.ts— a newchatbot.build.*family in all ten packs.useBuildGroupLabelis module-local; no new exported symbol.Two shapes worth calling out because they are the ones that could go wrong later:
chatbot.build.appFallback) and interpolated into the two header frames, so each frame has exactly one hole — which is what the interpolation-parity half ofcheck:i18n-keyschecks. The consequence for translators is a real constraint, and it is written into theenpack's own map-of-contents comment: both frames must keep that noun phrase in one case/gender.ru,fr,esandpttherefore use the deverbal-noun construction in both frames (Создание {{app}}…/Создание {{app}} завершено), which is case- and gender-safe for a real app label and for the stand-in alike.Verification
All readings from the repo root on
5d50a3d0a, afterpnpm --filter '@object-ui/plugin-chatbot...' --filter '@object-ui/i18n...' run build.pnpm exec vitest run packages/plugin-chatbot/src/__tests__/buildPanelI18n-7388.test.tsxTest Files 1 passed (1),Tests 12 passed (12)pnpm exec vitest run packages/plugin-chatbot/ packages/i18n/Test Files 107 passed (107),Tests 1592 passed (1592)pnpm --filter @object-ui/plugin-chatbot --filter @object-ui/i18n run type-checktsc --noEmitandtsc -p tsconfig.test.json, so the pin is type-checked too)check:i18n-keyscheck:i18n-driftcheck:i18n-dead-keyscheck:i18n-designer-paritycheck:changeset-presencecheck:control-bytescheck:governed-queue-guard --testNOT GOVERNED — 13 path(s) checked against 5 governed surface(s); none matchedpnpm exec eslint . --no-inline-config --format jsonChatbotEnhanced.tsxsit at :1844, :1915 and :3778 — all pre-existing, none on a line this PR moved)check:readme-exportsreads NOT MEASURED, not red: it self-declaresthe population COLLAPSED -- this run proves nothingbecause it needs the whole workspace built, and it names 26 other packages' READMEs. Neitherplugin-chatbotnori18nappears in its findings once those two are built. Declared to CI, which builds everything.The control, and that it moves independently
buildPanelI18n-7388.test.tsxruns the three known phases (structure/data/done) twice: through anenprovider, pinned to the exact strings the component used to hard-code, and through azhprovider. The provider-less English world is pinned separately and was already there —ChatbotEnhanced.test.tsx's "renders a live build tree" and "collapses to a Built summary", both untouched and green in the 1592.Ablation, from the committed tree
Restoring the hard-coded
Building ${appLabel ?? 'your app'}…literal on top of5d50a3d0a:The pin reddens by test-case name, and only on the subject:
All four
control: a known phase renders exactly as it did before (en)cases are in the 10 that passed — the ablated code rendersBuilding CRM…in English too, which is exactly why the control does not move when the subject breaks.Restored by state, not by exit code:
git hash-objectback tob35dead9…andgit diff HEADempty.--reporter=basic, which vitest 4 removed, so the run died in_createServerbefore entering any test body — the same class as aMODULE_NOT_FOUNDexit 1. It was redone with the default reporter; the numbers above are from that run.验收备注
1. ⛔ 第 2 块(
verifyphase)停手 —— 前置不成立按派发词的三步顺序找发送端,三步全空:
@objectstack/spec@17.4.0(本分支 lockfile 的 pin,按已发布 release 判、不看上游main)对这条通道只声明到onProgress?: (part: { type: string; id?: string; data?: unknown })——data是unknown,整个 phase 词表根本不在契约里。全包.d.ts里verify只以verify_build(工具名)和verifyEmail一类无关 token 出现。phase: 'verify'、搜不到cloud#1838的引用;verify_build在本仓确实存在,但它走的是工具卡通道和BuildDebugDrawer,不是data-build-progress的 phase 通道。⭐ 另有一条比"找不到证据"更强的发现,请 PM 转给 cloud 侧:⚠️ 这是发送端的设计决定,在本会话仓库范围内答不了 —— 建议第 2 块回到 cloud#1838 一侧先定发送方,再回头开 objectui 的接收端卡。
data-build-progress是apply_blueprint通过自己的ctx.onProgress发的。而卡片自己实测的场景是「apply_blueprint返回之后又花了 111 秒做校验」—— 工具已经返回,它不可能再往这条通道上发帧。所以即便 cloud 愿意发verify,「在extractBuildProgress里加一个verifyphase」这个字面方案也盖不住实测的那 111 秒:要么换一个发送方(校验循环自己发一条新的data-build-progress帧),要么这个 phase 得挂到另一条通道上。2. 相邻的第二处同形兜底 —— 只推理,⛔ 未改其行为
extractBlueprintProgress里const phase: ChatBlueprintProgress['phase'] = d.phase === 'done' ? 'done' : 'designing';与extractBuildProgress的d.phase === 'data' || d.phase === 'done' ? d.phase : 'structure'是同一形状。按边界要求未改动,推理如下:两者的风险并不对称,所以「都该 warn 一次」这个直觉我认为对一半:
extractBuildProgress的兜底把未知值折叠成'structure',那是一个中间状态 —— 面板会继续转圈、继续说"正在构建"。折错了就是卡片描述的那种 actively wrong。extractBlueprintProgress的兜底折叠成'designing',同样是中间状态,但它的注释已经把这条写成契约而不是兜底:Only 'done' is authoritative; anything else (incl. absent) is still designing.并且这个面板一旦propose_blueprint的结果落地就被权威的 "Proposed plan" 卡取代,所以一次误折的暴露窗口是有界的。⇒ 我的意见:真要加 warn,先加在
extractBuildProgress上,而且应当和第 2 块同一个 PR 落地(词表和"未知值"的定义必须一起改 —— 先加 warn 再加词表,会让verify在落地前的每一帧都刷一条假告警)。⛔ 因此本 PR 两处都没动。3.⚠️ 卡片的另一半确实过时了,而且比分诊席测到的更彻底
分诊席说
humanizeToolName已有翻译接缝。按派发词要求实测后确认,并且接缝今天是通的、表也是满的:ChatbotEnhanced.tsx的humanizeToolName(tool.toolName, tt),以及summarizeTools(summaryTools, tt)经translateToolTitle传下去的那个。chatbot.tool.*在en与zh各 35 个键、完全对齐,含verify_build。⇒ 卡片那句 "a column of English tool names ... Nothing in the tool result or the stream can change either" 今天对平台工具完全不成立,连"表不全"这个退路都不成立。这一半无事可做,本 PR 未碰。第三方 / 自定义工具仍然降级到英文 title-caser,那是 objectui#7254 的既定设计(
toolCardI18n-7254.test.tsx有一条用例正是钉这个)。4. 范围内但未立卡的观察(
noted, not filed)L里emptyTitle/emptyDescription/clear/sendHint/copy/copied/regenerate/model/submit/uploadFiles/stopResponse/trace/viewTrace/toolDetailsHidden/sendFailedRateLimited/sendFailedGeneric这 16 条走的是labels?.x ?? '英文字面量',没有tt()那一层;AiChatPage只喂了其中一部分。也就是说:一个不喂labels的宿主拿到的是英文,而同一个L里 objectui#7254 加的那几条(toolCompleted等)已经有tt()兜底。这是同一类问题在同一个对象里的两种写法并存,但它在聊天外壳上、不在本卡的面板上,⛔ 未纳入本 PR。承接者:无 —— 没有已排队的 PR 或人会碰到这个对象,所以我没有替它立卡,交 PM 判是否值得单开一张。Bundle Analysis: the sharedi18n-localesbudget, re-baselined (objectui#8816)This PR and objectui#8888 / objectui#8901 (the other one) were red on one and the same
check, for one and the same reason, and neither could go green alone. That is one
shared-budget problem wearing two PR numbers, so it was taken as one dispatch and the answer
is identical bytes on both branches — required, not tidiness: see "Why both branches
carry it" below.
Route taken: (1) raise the ceiling. Route (2) — reduce the payload — was measured first
and cannot reach the gap. The reasoning, the arithmetic and what the raise costs are written
into
scripts/check-eager-closure-budget.mjsitself, under "Whyi18n-localesmoved UP",because that is where the budget lives and where the next author will look.
The instrument
apps/console/dist/eager-closure.json, written byemitEagerClosureReportinapps/console/vite.config.tson every console build, read bynode scripts/check-eager-closure-budget.mjs(pnpm check:eager-closure). Every figure belowis one full build pair from the repo root —
pnpm turbo run build --filter='./packages/*'then
pnpm --filter @object-ui/console build— in one container. ⛔ No figure here isinherited from an earlier seat's comment; the prior readings on objectui#8816 were all
re-derived from scratch, and where they agree that is two independent paths landing on the
same number, not a citation.
Ceiling, base, and each branch's delta — measured
i18n-localesgzipbbe285ee7—main3949cf3a3—main+ objectui#8901ea5eab7b3—main+ objectui#8888ba20b0bc0—main+ BOTH⭐ The fourth row is the one this dispatch existed to produce. The two deltas sum to 1,586
against a measured 1,594, so two independent claimants on this chunk are additive to within
8 bytes — no gzip dictionary relief, and no arithmetic on the two single readings would have
been trustworthy without that third build.
mainthen advanced five commits mid-flight (bbe285ee7→b97129e96). Both branches weremerged onto it again and everything re-measured:
packages/i18nis untouched in that rangeand
i18n-localescame back byte-identical on all three trees (455,271 / 455,519 /456,196). The other three budgeted chunks moved by ≤9 bytes, which is
main's own movement,not this change's.
The new pair, and why the number is what it is
Headroom 8,804 bytes = 0.10x
REGRESSION_THIS_GATE_MUST_CATCH_BYTES— this key's ownconvention, and marginally tighter in ratio than the 8,924 (0.10x) the retired pair
carried.
⛔ It is deliberately not the 804 bytes the overage needed. A minimal raise reproduces the
exact defect objectui#8816 was filed about: the gate weighs the merge ref, so a line with
~0 headroom reddens whichever in-flight PR happens to be weighed second and prints a message
blaming that diff for arithmetic that is not its own. objectui#8554 is the same mechanism
one step earlier (
frameworkat 70,999 against 71,000, printing a green sensitivity row).177afeba1(2026-09-03) at 446,076 and
mainmeasured 454,602 on 2026-09-10 — 8,526 bytes in seven days,the last 398 of them claimed by two independent changes inside one shift. At that arrival rate
this buys about a week. The structural answer the file already names — taking the catalogues
out of the eager closure — is what retires this line instead of moving it.
Why route (2) was refused, measured rather than assumed
Objects,Views,Dashboards,App,Sample data), so reuse looks available. It is not: onlyObjectsandDashboardshave any pre-existingenequivalent, and each already exists three timesunder three per-surface namespaces. Per-surface keys are this pack's convention; cross-surface
reuse is the deviation. Best measured saving 128 bytes against a 519-byte overage.
⭐ The probe is control-lit: searching the
enpack for the five literal values returnedfour hits for two of them and one (the new key itself) for the other three — so the three
zeros are real zeros, not a broken search.
strings exist precisely because saying what the neighbouring
console.importMappings*strings say is the runtime lie that card was filed to remove.
green tick and narrowing a payload to get one are the same error facing opposite ways.
⇒ ⭐ The payload reduction that would make the next raise unnecessary is real, and it is not
this PR's to take.
pnpm check:i18n-dead-keysreports 364 candidates across 47 namespaces,127 of them CONFIRMED with no textual footprint anywhere in the repo, in ten packs each —
far more than the 1,196 bytes needed. That gate is report-only by design,
@object-ui/i18npublishes these packs (so removing a key is a published-surface decision, not a byte
saving), and objectui#8816's own route-C note says a sweep like that needs its own card and its
own reverse verification. ⛔ Not ridden in here.
Proof the gate can still go RED at 465,000
A ceiling that no longer fires is not a fix, so it was made to fire. On the combined tree
ba20b0bc0, with the new 465,000 ceiling in place, 300 deterministic high-entropy fillerkeys (30 per pack, ten packs) were injected, proven on disk before anything was read —
per-file marker count 0 → 30 and a changed blob hash for all ten — then the console was
rebuilt:
Direction was predicted before the run and observed as predicted. The lit controls held:
vendor-objectstackandui-componentsbyte-identical across the mutation,frameworkmoved2 bytes, and the aggregate stayed green (7.9 KB headroom) — so the line that objected is
the per-chunk one, at its new number.
Restored by
git checkout HEAD -- PATH(⛔ never baregit checkout --) and verified bystate, not by an exit code: all ten blobs back to their
HEADhashes, marker count 0,git diff HEADempty for the locale packs, and the mutation script carriedtrap … EXIT INT TERMwith absolute paths throughout. Thedist/the mutated build wrote wasalso deleted, so no later run in that container can read the mutated bytes back.
The green side, and the both-on-main answer
i18n-localescheck:eager-closureexit 0, all four chunks ✅check:eager-closureexit 0, all four chunks ✅maincheck:eager-closureexit 0, all four chunks ✅⇒ Yes, the fix holds with both on
main, and that is a build, not an extrapolation. Theaggregate never objected in any of them: 23,507 bytes of headroom (0.26x) with both claimants
on it, so
MAX_EAGER_CLOSURE_GZIP_BYTESis ⛔ untouched.Why both branches carry the identical change
Not duplication —
evaluateCeilingFreshnessrequires it. Once the first of the two lands, thebase branch has moved
PER_CHUNK_GZIP_CEILINGS, and the freshness half compares threereadings: this checkout, this checkout's base, and the base-branch tip. A checkout that does
not carry the new value scores as
supersededand exits 2 — a verdict about the gauge,correctly. Because both branches carry byte-identical constants, the second one merges the
first's landing as a no-op for that file and the freshness half reports "this checkout already
carries the new value". So whichever lands first, the other stays green.
The measurement is payload-neutral, and that was checked rather than argued
The ceiling edit plus the ten locale packs' comment update produced a byte-identical
closure on every tree it was applied to —
i18n-locales455,271 → 455,271, 455,519 → 455,519,456,196 → 456,196, aggregate unchanged to the byte in all three. So
BASELINE's standingargument (
scripts/check-*.mjsis not a console build input) is observed here rather thanassumed, and it is now also observed for a source edit: TypeScript comments do not survive
into the chunk.
Also in this change: ten stale citations of the retired headroom
All ten locale packs carried a comment naming the old ceiling's headroom ("8,924 B … about
sixty short keys' worth"). Moving the ceiling would have left ten copies of a false number, and
the "about sixty short keys" unit is itself falsified by measurement — the two real claimants
cost 9.2 and 22.3 gzipped bytes per key-times-locale, a 2.4x spread, so ⛔ no quota
written in keys is derivable. The comment now names no figure at all (the form that file's
own anti-drift rule explicitly allows) and points at
pnpm check:eager-closurefor the figurein force. Comment-only: zero payload, proven above.
⛔ What was NOT done
No check disabled, skipped or removed. No package excluded from
Bundle Analysis. No testdeleted. No empty commit. Neither PR closed and reopened.
REGRESSION_THIS_GATE_MUST_CATCH_BYTESuntouched, the other three per-chunk ceilings untouched,
MAX_EAGER_CLOSURE_GZIP_BYTESuntouched. No assignee written, no review label hung or cleared, no
Clause-②declarationedited. No rebase and no force-push:
mainwas brought in with real merge commits, twice.One commit on this branch was not this session's
While the final measurement build was running, another actor pushed a
Merge branch 'main'commit onto this branch —
7b01b45c5, committed by GitHub, i.e. the "Update branch" path. Ithas the same two parents and a byte-identical tree (
8794b5848) as the merge this sessionhad already made locally, so nothing was lost and nothing needed redoing. That commit was
adopted, ⛔ not reverted and ⛔ not force-pushed over, and the green reading above is a
measurement of that exact tree.
🤖 Generated with Claude Code — session
session_01611D6ZaRaMmwTNQmSbk8MHGenerated by Claude Code