Skip to content

fix(tooling): check-i18n-coverage 逐 config 收集失败,第一个失败不再吃掉其余十一个 (#6033) - #6310

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-6033-i18n-coverage-collect-errors
Aug 7, 2026
Merged

fix(tooling): check-i18n-coverage 逐 config 收集失败,第一个失败不再吃掉其余十一个 (#6033)#6310
hotlong merged 1 commit into
mainfrom
claude/issue-6033-i18n-coverage-collect-errors

Conversation

@hotlong

@hotlong hotlong commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #6033

前提复核(行号漂移核对)

分诊评论(2026-08-07T06:56Z)的落点是按 ede5a8e 给的行号。在本 PR 的基点
6131d90 上按内容重新定位,四处全部命中,且行号几乎没有漂移:

分诊所指 本 worktree 实际 结论
:178 无输出裸 throw :178 if (!raw.trim()) throw … 一致
:183 JSON 不可解析裸 throw :183 一致
:185 report.error 裸 throw :185 一致
:367-369 per-config 循环无 try/catch :367-370(const current = {} 占 367) 一致,差一行是起算点不同
:380-411 errors[] 收集器 :380-409 一致

premise 成立,并且是实测复现的,不是读代码推断的。在一棵 pnpm install
之后只跑了 pnpm exec turbo run build --filter=@objectstack/cli 的树上,跑
origin/main 版本(逐字节原样,存放在 worktree 之外):

Error: os lint failed for examples/app-showcase/objectstack.config.ts: Cannot find module
'/…/examples/app-showcase/node_modules/@objectstack/connector-mcp/dist/index.mjs'
imported from /…/examples/app-showcase/objectstack.config.bundled_yqbr4ytyonb.mjs
    at countI18nIssues (…/check-i18n-coverage.mjs:185:29)
    at …/check-i18n-coverage.mjs:369:25
Node.js v22.22.2

注意它死在第 2 个 config:第 1 个(app-crm)已经量到了数,第 3 到第 12 个
从未被尝试。这正是本单要修的形状 —— 诊断没说谎,但它吃掉了其余十一个。

改动叙述(单文件:scripts/check-i18n-coverage.mjs)

  1. countI18nIssues 改名 measureI18nIssues,返回 {count}{failure},
    不再为单个 config 抛异常。原来三处裸 throw 各自变成一条带成因的失败:
    无输出(附 stderr 读数 —— 此前 stderr 只喂给签名网,其余丢弃)、JSON 不可
    解析(附截断后的载荷头)、report.error(交给分类器)。改名是因为职责变了;
    头注释里保留了含旧函数名的那段栈,grep 旧名仍能落到本文件。
  2. 新增纯分类器 explainConfigFailure:把 CLI 原话变成「结论 + 一句修法」。
    唯一值得做的判别是模块解析 —— 指向某个包 dist/Cannot find module
    ⇒ 「已安装但没有构建产物」+ pnpm build;只点名包 ⇒ pnpm install && pnpm build;
    其余一律保留 CLI 原话并给出手动重跑命令。真正坏掉的 config 不会被判成
    「没构建」
    ——那会是 check-i18n-coverage 也是「声明了但没强制」的构建前置 —— 未构建时抛未捕获异常,并把成因指向 examples/app-crm 的配置 #5862 那个「自信却指向无辜处」的缺陷换个层级重演。
  3. 新增 measureAllConfigs(configPaths, measure):跑完整轮、收集失败;measure
    是注入的,所以 --self-test 能在没有 CLI、没有构建的情况下证明「一个 config
    失败不中止整轮」——而这恰恰是 CI 永远观察不到的行为。
  4. 新增 reportUnmeasuredConfigs:整轮结束后一次性报告,按结论去重
    check-i18n-bundles 在工作区未构建时把「CLI 没 build」报成 9 个包各自的 bundle 问题 #5217 的规矩(一个原因不要报成 N 个结果)在这里必须继续成立:十二个 config
    因同一个包没构建而失败,是同一个事实说了十二遍,只能算一条成因、列出它覆盖的
    config。反方向(本单)则是 N 个不同成因各占一条,不被第一个吃掉。
  5. 失败时抢在 --update 写盘与比对之前退出。这一条不是省事,是安全性:
    半轮没有结论可给 —— 没测到的 config 与被删掉的 config 无法区分,DOWN 方向会
    叫读者去 --update,而 --update 跑在任何比对之前,会冻结幸存者、悄悄丢掉
    其余,把真实债务从 baseline 里棘轮出去。与 reportPrerequisiteNotMet 同一条
    不变量:没测就不写。
  6. CLI 未构建那条前置仍然在第一个 config 就中止 —— 有意保留:每个后续 config
    都会因同一个环境原因失败,继续跑只会把一个事实印十二遍。

用户可见面为零(仓内工具链,不发布任何包)⇒ 无 changeset,PR 打 skip-changeset

反向验证(先申报预期,后执行)

声明 A(回归面)—— 申报

完整构建后(等价 lint.yml 的 Build workspace packages),origin/main 版与本版
在同一棵树上跑,stdout/stderr 与退出码逐字节一致

实测:一致。

sha256(A-main.out) = ebccfd34d946968f7647affbbc72c559707a306981e14e992f1c715538a99b34
sha256(A-mine.out) = ebccfd34d946968f7647affbbc72c559707a306981e14e992f1c715538a99b34   ← 同
stderr 双方均为 0 字节(e3b0c442…855 = 空串)
MAIN EXIT=0        MINE EXIT=0
输出:check-i18n-coverage: OK (12 config(s), 660 baselined untranslated string(s), none new).

--update 写盘路径同样未变:完整构建后跑 --update,baseline 的 md5 与提交态相同。

唯一的合理差异(申报在此,不算违反 A):--self-test 的成功摘要行按惯例列出
它证明了哪些分类器,本 PR 新增了一个,所以那一行文字变了:

  • 之前:… the missing-CLI-build and i18n-rule classifiers both go red, and stay distinct.
  • 之后:… the missing-CLI-build, i18n-rule and per-config-failure classifiers all go red, stay distinct, and a failing config does not end the round.

声明 B(目标面)—— 申报

树的状态:pnpm install 后仅 pnpm exec turbo run build --filter=@objectstack/cli
(55 个 task;packages/connectors/* 四个包确认无 dist)。N = 12 个 config
(3 个 example + 9 个 package extract config),先数出来再跑。

预期:① 输出中不出现 node 栈;② 12 个 config 全部被尝试;③ 恰好 1 个失败 ——
examples/app-showcase,成因点名 @objectstack/connector-mcp「已安装但没有构建
产物」,修法 pnpm build,其余 11 个量到数;④ 末尾一次性报告,退出码 1;
⑤ baseline 不被改写。

(为什么只预期 1 个:app-crm / app-todo 只 import @objectstack/spec(已构建),
9 个 extract config 只 import ../src/* 与 spec,唯有 app-showcase import 了四个
未构建的 connector。这是数出来的,不是猜的。)

实测:五条全中。

EXIT=1

check-i18n-coverage: COULD NOT MEASURE — 1 of 12 config(s) failed to lint (1 distinct cause)

  Cause 1 of 1 — 1 config(s):
    examples/app-showcase/objectstack.config.ts

    why:  `os lint` could not load the config: `@objectstack/connector-mcp` is installed but has no build output in this worktree
    saw:  Cannot find module '/…/examples/app-showcase/node_modules/@objectstack/connector-mcp/dist/index.mjs' imported from /…/objectstac…
    fix:  pnpm build

  Every config was attempted, so the list above is EVERY one that failed — not
  merely the first. …
  Nothing was compared: 11 config(s) did lint, but a partial round cannot judge
  the ratchet — … the baseline was left exactly as committed (`--update` included).

md5sum -c 确认 baseline 未被改写;同状态下再跑 --update,同样是这条判词、
EXIT=1、baseline 未动(改动第 5 点的安全性,实测过,不是只写在注释里)。

声明 B2(多 config / 共有成因)—— 申报过、结果与预期不符,如实记录

申报:把 packages/spec/dist 移走,12 个 config 应全部以同一个成因失败,
报告应显示 1 条成因覆盖 12 个 config(而不是 12 条判词)。

实测:不符,原因是探针选错了,不是实现不符预期。 移走 spec 的 dist 会让
CLI 自己跑不起来,于是 #5862 的签名网先一步命中,整轮在第 1 个 config 就以
PREREQUISITE NOT MET — the built CLI cannot resolve the command this gate runs
中止 —— 这正是那条前置应有的行为,顺带成了「本 PR 没弄坏 #5862 那条路径」的
回归证据。

第二次尝试:改为移走三个叶子包的 dist(observability / trigger-record-change
/ plugin-sharing),预期这些 extract config 会各自失败。实测仍只有 1 个失败
—— 因为 9 个 extract config 只 import ../src/ 下的对象与译文定义文件,那些文件
只依赖 @objectstack/spec,压根不碰这些运行时包。同一棵树上跑 origin/main 版,
它照旧只报 app-showcase 一条然后带栈死掉。

结论(如实申报,不改预期迁就结果):今天这棵仓库的拓扑下,不动 examples/**
就构造不出「多个 config 因不同成因失败」的自然状态
—— 唯一能这样失败的是
app-showcase。所以「N 个成因不被第一个吃掉」这条性质由 --self-test 里的注入式
measure 确定性地钉住(4 个 config、2 个不同成因、其中一个还是 throw 出来的),
而不是靠一次凑出来的实跑。这些断言不是摆设,已用变异体验证会红:

变异体(仅在 worktree 之外的副本上) 结果
去掉 measureAllConfigs 的 try/catch 断言未及执行,注入的异常直接逃逸整轮 → EXIT=1
分组键从「结论」换成「读数」 #6033 one cause is stated once — got 3 cause(s) for one missing package 等 2 条红 → EXIT=1

门禁 EXIT 表(均在 git add 之后跑)

命令 EXIT
NUL / 控制字节 pnpm check:nul-bytes 0(自测 56 断言 + 扫描 5961 个已跟踪文本文件)
语法 node --check scripts/check-i18n-coverage.mjs 0
ESLint pnpm exec eslint scripts/check-i18n-coverage.mjs --no-inline-config 0
本门自身 pnpm check:i18n-coverage(self-test && 实跑) 0
同族邻门(共享模块未动的回归) pnpm check:i18n 0
自扫控制字节(超出门禁盲区) grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' 无命中

package.json 未改(check:i18n-coverage 早已接好 --self-test,新断言直接进
既有自测,不需要新文件、不需要新接线)。

不在本 PR 里


Generated by Claude Code

#6032 让「CLI 未构建」这一个前置有了结论式回答,但 example 自身 workspace
依赖未构建那条路径原样保留:`countI18nIssues` 里三处裸 throw,per-config
循环没有 try/catch,于是第一个 lint 不了的 config 以一段 node 栈中止全程,
后面十个根本没被尝试过 —— 它们各自的成因(未必与第一个相同)也就从未出现。
`errors[]` 收集器在这条路径上永远走不到。

改为:`measureI18nIssues` 返回 `{count}` 或 `{failure}`,永不为单个 config
抛异常;`measureAllConfigs` 跑完整轮并收集失败;整轮结束后
`reportUnmeasuredConfigs` 一次性报告 **每个不同成因**(按结论去重),每条带
它覆盖的 config、CLI 原话读数与一句可行动修法。#5217 立的规矩(一个原因不
要报成 N 个结果)因此在反方向上同样成立:N 个不同原因不被第一个吃掉,而一个
共有原因也只说一次。

失败时抢在 `--update` 写盘与比对之前退出:半轮测不出结论 —— 没测到的 config
与被删掉的 config 无法区分,`--update` 会冻结幸存者、悄悄丢掉其余,把真实债务
从 baseline 里棘轮出去。与 `reportPrerequisiteNotMet` 同一条不变量:没测就不写。

CLI 未构建那条前置仍然在第一个 config 就中止 —— 那是有意的,每个后续 config
都会因同一个环境原因失败,继续跑只会把一个事实印十二遍。

`--self-test` 增加第三组断言:真实语料(仅构建 CLI 的树上录得的 report.error)
须点名缺失包并给出 `pnpm build`;真正坏掉的 config 不得被判成「没构建」;无法
识别的成因仍须产出完整判词;注入式 `measure` 证明一个 config 失败不中止整轮,
且不同成因分开、共有成因合一。CI 永远走不到这条路径(lint.yml 先构建),所以
这些断言只可能钉在这里。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 7, 2026 1:34pm

Request Review

@github-actions github-actions Bot added the size/m label Aug 7, 2026
@hotlong hotlong added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/m labels Aug 7, 2026 — with Claude
@github-actions github-actions Bot added the size/m label Aug 7, 2026
@hotlong
hotlong marked this pull request as ready for review August 7, 2026 13:50
@hotlong
hotlong added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 76f6186 Aug 7, 2026
30 of 31 checks passed
@hotlong
hotlong deleted the claude/issue-6033-i18n-coverage-collect-errors branch August 7, 2026 14:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check-i18n-coverage:example 自身依赖未构建时仍抛裸异常 + node 栈(诊断准确,但不是结论)

2 participants