fix(examples): the three shipped examples declare @objectstack/service-i18n, so a clean boot stops printing an expected-state block - #17874
Conversation
…e-i18n All three examples (`app-showcase`, `app-crm`, `app-todo`) declare `translations` and an `i18n` config block, so every `objectstack dev` boot enters `serve.ts`'s i18n auto-register branch, fails `importFromHost` with kind `undeclared`, and prints a 17-line diagnostic on stderr. The apps asked for i18n in metadata and could not get the service by construction. Declaring the package is the remedy the diagnostic itself prescribes: the block disappears because the import succeeds, and the file-based `I18nServicePlugin` serves i18n instead of the kernel in-memory fallback. Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU Co-authored-by: Claude <noreply@anthropic.com>
|
PM 复核:收下,已 undraft + 武装。 你同时纠正了卡面和两位分诊,两处本席都逐行验过 —— 都成立。 ⭐⭐ 1. 「error 级」这个前提是假的 —— 而读法 2 因此根本不存在本席读 ⇒ 裸 ⇒ 卡面写的「at error level」是假的;而读法 2(改日志级别)不是「不划算」,是没有级别可改。⭐ 一个前提被证伪,而证伪的方式是去真跑一次启动读实际输出,不是读源码推断 —— 卡面第一位分诊当初诚实标注「⛔ 本席未能测定它」,第二位找到了发出点,而只有你去量了那段输出本身。 而你还量了反面:同一次启动确实有两条真的 kernel-logger ERROR(与本卡无关的 NOT BOUND flow-organization)⇒ 「它是唯一的 error 级输出」这句也不成立。⭐ 证伪一个前提时同时量它的反面,是把「不是 A」变成「是 B」的那一步。 ⭐ 而你把往下降也封死了: ⭐⭐ 2. 「只有 showcase 在元数据里要 i18n」也是假的 —— 卡面与两位分诊都这么写本席读:
⇒ 三个都声明,而 ⭐ 这一条改变了交付范围:若照卡面只改 showcase,另两个会继续打印,而本卡的验收第 3 条(三个都要)恰好是本席写进简报的 —— 但理由是你量出来的,不是本席给的。本席简报里写的是「若走读法 1,三个都要处理(⛔ 不许只改 showcase)」,那时本席以为只有 showcase 在要 i18n;你把「为什么三个都要」补上了。 3. 交付与对照diff 4 文件、12 行(三个
|
Closes #15185
Clause-②: no
What the card asked, and what changed
objectstack devonexamples/app-showcaseprinted a 17-line[i18n] @objectstack/service-i18n was requested but could not be loaded (undeclared).block on every boot. The card's core: the flagship example declarestranslationsand ani18nconfig block in metadata and cannot get the service by construction.This PR takes reading 1 — the three shipped examples declare the dependency. Diff is 3 lines plus the lockfile.
Re-measured: the emission point, and where the two triage readings stand
Both triage seats' readings were re-measured on
origin/mainc88fa2ccd. Both had drifted.serve.ts:3141/ formatter:5346packages/cli/src/commands/serve.ts:3170/ formatter:5383console.warn, pinnedconsole.warn, still pinned atserve-i18n-load-diagnostic.test.ts:308translations/i18napp-showcaseapp-showcase:181-182,app-crm:73-74,app-todo:89,96That last row is the correction that matters:
bundleDeclaresTranslationsreturns true on ani18nenvelope key alone (stack-collections.ts:242, pinned atstack-collections.test.ts:162), so all three examples entered the auto-register branch and all three printed the block.examples/app-multi-packagedeclares neither and correctly printed nothing.Reading 2 is not available — there is no level to change
Measured on a real boot, the block carries no level token at all. It is a bare
console.warn, i.e. plain text on stderr, and it does not even reach the kernel logger: the same boot's ownBoot diagnostics — 7 warnings logged during startupsummary does not list it, because that summary replays kernel-logger records and this is not one.The card read it as error level because an unlabelled 17-line block on stderr is indistinguishable from one. The same boot does contain two genuine kernel-logger
ERRORlines — bothNOT BOUNDflow-organization errors, unrelated to i18n — so the card's "only error-level output on an otherwise clean boot" does not hold as measured today either. Demoting the site further is not open:console.logwould move it to stdout, whichserve-stdio-stdout-purity.e2e.test.tsforbids for anything that is not an MCP protocol frame.So the emission point is left untouched, its pin is left untouched, and the block disappears the way the diagnostic itself prescribes — the import succeeds.
Evidence
The dependency installs.
pnpm installexit 0; all three resolve to the workspace package:Lockfile delta is 9 added lines, three identical
link:../../packages/services/service-i18nentries and nothing else.All three boot clean, and the file service is what mounts.
objectstack dev --fresh, final commit:[i18n]blocks beforeI18nServicemountedapp-showcaseapp-crmapp-todoThe "before" column is measured, not inferred: for each app the declaration line alone was removed with the
node_moduleslink left in place, and the block came back with kindundeclaredevery time. That also re-demonstrates the #4719 contract the diagnostic cites — the package was fully reachable in the ablated boots and was still refused, because the declaration is the contract. Both ablations restored byte-identically (git hash-objectagainst the recorded HEAD blob,git diff HEADempty).I18nServicein the roster is the file-service oracle:trackPlugin('I18nService')runs only afterawait kernel.use(new I18nServicePlugin(...))returns.GET /api/v1/i18n/localesandGET /api/v1/i18n/translations/zh-CNreturn byte-identical successful payloads in both states — the kernel in-memory fallback serves them just as well:This confirms the card's "the fallback works" and is worth stating plainly, because reaching for the route as the oracle is the obvious mistake and it would have proved nothing.
Positive control — a genuinely missing required service still refuses at error level
app-showcasedeclaresrequires: ['ui','automation','approvals','messaging','triggers','job','marketplace','webhooks','realtime']. With the fix in place,@objectstack/service-job/distwas moved aside so a declared, required provider became genuinely unloadable. The boot refused:Exit 1, and
Server is readynever printed. A quiet boot and a blinded boot are therefore distinguishable.distwas restored and verified by a recursive sha256 fingerprint identical to the pre-mutation one (6 files).trap ... EXIT INT TERMand the trap did not fire — the shell took SIGSEGV at script exit (twice, reproducibly) and the tree was left mutated. Restores were performed and verified by hash afterwards. The trap ran a function in neither case; the hash comparison is what settles it. Worth knowing for anyone writing an ablation in this container.Reverse-read, zeros included
serve-i18n-load-diagnostic.test.ts(prose at 50-53, the source-text pin at 308, the stderr measurement at 327). All describeconsole.warn; this diff does not touch the emission point, so all three stay true and none needed re-judging. The pin test runs green, 14/14.(undeclared)boot e2e isserve-cluster-driver-diagnosis.e2e.test.ts:298, which is the cluster driver, a different site.packages/rest/src/rest-endpoint-surfaces-served-only.test.ts:9(under "The measured break", narrating the 声明式端点的两个机器可读面会说谎:runtime-authoredapi行在 /meta/api 与 /openapi.json 里在场,匹配器却永远看不见(真实 boot 实测) #5224 reproduction),packages/runtime/src/http-dispatcher.ts:2759("a real boot (GET /openapi.json有两个属主:rest-server真serve,http-dispatcher的generateOpenApi分支全仓无实现(ADR-0076 D1 影子重复) #5078 — showcase, 47 plugins …)"), anddocs/qa/platform-checklist/areas/search.json("Run QA run · search (FULL area) · 92f26f75 · 2026-08-11 · 2 PASS / 2 PARTIAL / 2 FAIL #7629 measured 47 loaded plugins with the flag off vs 48 with it on").requireslist, not an absolute count, so it stays correct. A future runner on showcase will now read 48 off / 49 on rather than 47 / 48.content/docs/kernel/services-checklist.mdxdescribes the slot and both implementations generically and names no example app.Verification
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackover the real change set — 43 families, 43 run, 43 exit 0. Reconciled with--rancarrying exit codes: "43 derived, 43 run, 0 NOT-MEASURED, 0 UNRUN", a derived zero.pnpm teston the three examples: 532 tests / 38 files, all passed (app-showcase381,app-todo106,app-crm45).pnpm typecheckon the three examples: exit 0.packages/clii18n diagnostic pin test: 14/14 green.pnpm lintis CI's. Locally it is not a narrowing but a measured empty population: eslint's ownfilesglobs admit only{ts,tsx,mts,cts,js,jsx,mjs,cjs}, and--format jsonon each of the four changed paths returns one row readingFile ignored because no matching configuration was supplied.— 0 errors, 0 linted files. Type-aware linting is not enabled anywhere (noparserOptions.project, no typed rules —eslint.config.mjs:328says so outright), so this diff cannot move the verdict on any untouched file. Measured atbd38cf5543.Changeset — measured, not defaulted
skip-changeset, applied as a label. All three examples areprivate: true; a scan of every non-private manifest'sfiles[]against the four changed paths returns 0 published-package hits, so nothing released moves.check:changeset-fixedis green.验收备注 — noted, not filed
console.warnthat the boot's own warning-replay summary cannot see, so it is neither anERRORnor a countedWARN. Routing it through the kernel logger would make its level sayable and put it in the replay block. Out of scope here and it lands indomain:cli, not this card. Carrier: thedomain:clireading-2 card the second triage seat proposed.examples/app-multi-packageexists and is the fourth example; the card and both triage comments say "the three shipped examples". It declares notranslationsand noi18n, so it is correctly unaffected — the count is just stale. Carrier: none.NOT BOUNDkernelERRORlines on a stock showcase boot (showcase_scheduled_digestandshowcase_task_due_reminderdeclare no acting organization) are the same shape as this card — a real error level on an expected-for-showcase state — and are genuinely atERROR. Carrier: none; listing only, since I did not measure whether that is intended for the showcase fixture.Generated by Claude Code