From e5181e94ff2059d33221edfd493e685c3e9c488c Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:56:01 +0800 Subject: [PATCH] doc: record the coverage uncovered-location reporter decision --- ...-06-coverage-uncovered-locations.i18n.yaml | 6 +++ ...2026-08-06-coverage-uncovered-locations.md | 42 +++++++++++++++++++ ...6-08-06-coverage-uncovered-locations.zh.md | 42 +++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 .agents/notes/implemented/process/2026-08-06-coverage-uncovered-locations.i18n.yaml create mode 100644 .agents/notes/implemented/process/2026-08-06-coverage-uncovered-locations.md create mode 100644 .agents/notes/implemented/process/2026-08-06-coverage-uncovered-locations.zh.md diff --git a/.agents/notes/implemented/process/2026-08-06-coverage-uncovered-locations.i18n.yaml b/.agents/notes/implemented/process/2026-08-06-coverage-uncovered-locations.i18n.yaml new file mode 100644 index 0000000000..7f9a5eee29 --- /dev/null +++ b/.agents/notes/implemented/process/2026-08-06-coverage-uncovered-locations.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-06-coverage-uncovered-locations.md +2026-08-06-coverage-uncovered-locations.md: 27c466e7ae505a2d318f8b21d442c31619610f59 +2026-08-06-coverage-uncovered-locations.zh.md: 93eeb863e51679c60d9e0ccfc742156c098aeca4 diff --git a/.agents/notes/implemented/process/2026-08-06-coverage-uncovered-locations.md b/.agents/notes/implemented/process/2026-08-06-coverage-uncovered-locations.md new file mode 100644 index 0000000000..27c466e7ae --- /dev/null +++ b/.agents/notes/implemented/process/2026-08-06-coverage-uncovered-locations.md @@ -0,0 +1,42 @@ +# Agent Note: Exact uncovered locations on coverage failure + +Status: implemented + +English | [中文](2026-08-06-coverage-uncovered-locations.zh.md) + +## Problem + +When the per-file 100% coverage gate fails, vitest emits only file-level error lines (`ERROR: Coverage for lines (…) does not meet global threshold (100%) for `) — you learn which file fell short, not which lines. The built-in `text` report does have an Uncovered Line #s column, but it is one giant table over hundreds of files repo-wide: the column truncates at the table width, carries line numbers but no column numbers, does not distinguish statements from branches from functions, and passing files occupy rows all the same. The net effect is that a red coverage run on CI is not directly actionable; the only way to locate the specific gap is to rerun the html report locally. + +## Decision + +`scripts/coverage-uncovered-locations.cjs` is a custom istanbul reporter (a `ReportBase` subclass): for every file below 100%, it emits one self-contained single-line record per uncovered statement, untaken branch path, and uncalled function — `:: uncovered …` — directly clickable in terminals and CI logs, and easy to grep. When every file passes, it prints nothing. istanbul report generation runs before threshold validation, so the records land exactly above the existing ERROR lines. + +The wiring is a single point: the coverage block in the root `vitest.config.ts` is the repo's only coverage configuration, shared by the CI lane (`run-gates ci-coverage`), local `test:coverage`, and focused runs (`--coverage.include`). The reporter joins both the CI and local reporter arrays by absolute path (`fileURLToPath`) — istanbul-reports' `create()` falls back to a bare `require(name)` for non-built-in names, and a relative path would resolve against istanbul's own package directory. + +Output conventions: + +- istanbul's 0-based column numbers are converted to 1-based (the convention editors and terminal links expect). +- v8 reports `end.column = Infinity` for whole-line statements: a span crossing lines degrades to a `(to )` suffix carrying only the line number, and a single-line span omits the suffix. +- An implicit branch arm (such as a missing else) may carry no location; the reporter falls back to the branch's own span so the record stays clickable; branch records are annotated with the branch type and `path k/n`. +- Records within a file are sorted by line, then column; there is no cap on the count. + +Two companion changes: the root `package.json` adds `istanbul-lib-report` as a devDependency (under pnpm's strict layout, `scripts/` cannot reach nested dependencies); the root workspace's entry/project globs in `knip.json` gain `scripts/**/*.cjs`, making the file and its dependencies visible to the hygiene gate. + +CJS is a forced shape, and a justified exception to the ESM-everywhere discipline: istanbul loads custom reporters via a bare `require()` outside the tsx/Vite pipeline, where TypeScript cannot participate; the namespace object `require(esm)` returns also fails its `new Cons(cfg)` construction — CommonJS is the only reliable shape. + +## Alternatives considered + +- **Rely on the built-in `text` report's Uncovered Line #s column.** This is precisely the problem as found: one repo-wide table, column-width truncation, line numbers only, no kind distinction, passing files in the same column — not actionable in CI logs. +- **Add a `json` reporter plus a separate wrapper script that reads `coverage-final.json` for post-processing after a failure.** Feasible in pure ESM/TS, but the wrapper would have to wrap both entry points — `package.json`'s `test:coverage` and the run-gates gate — changing their command shapes; the custom-reporter route touches one piece of configuration and takes effect at both entry points automatically. +- **Write the reporter in TypeScript/ESM.** istanbul's loading mechanism (a bare `require` outside the pipeline) rules this out, as above; swapping out the loading mechanism for the sake of one report file is out of proportion. + +## Verification + +Local matrix: with a deliberately induced failure, all three record kinds appear and their locations match the planted gaps; a mixed run emits records only for the failing files (files at 100% within the same run stay silent); an all-green run produces zero output and exit code 0. CI evidence: after temporarily planting one unreachable statement/branch/function in `clampTimeout`, the coverage lane — under the isolated condition of all tests passing (632 files / 10326 cases) with only the threshold failing — printed the 4 records above the ERROR lines (evidence links in the PR #1716 description); the demonstration commit was removed from the branch before merge. + +## Consequences + +- A red coverage run is self-sufficient: the log gives exact line and column numbers plus the kind of each gap, and rerunning the html report locally to pinpoint it is no longer needed. +- The cost is one CJS-file discipline exception and one root devDependency; all-green runs produce zero output and add no log noise. +- A file with zero coverage yields output on the order of its statement count (deliberately uncapped): the gate demands zero gaps, so the full listing is the action list, and vitest's own ERROR lines already provide the per-file summary as a backstop. diff --git a/.agents/notes/implemented/process/2026-08-06-coverage-uncovered-locations.zh.md b/.agents/notes/implemented/process/2026-08-06-coverage-uncovered-locations.zh.md new file mode 100644 index 0000000000..93eeb863e5 --- /dev/null +++ b/.agents/notes/implemented/process/2026-08-06-coverage-uncovered-locations.zh.md @@ -0,0 +1,42 @@ +# Agent Note: 覆盖率未达标时输出精确未覆盖位置 + +Status: implemented + +[English](2026-08-06-coverage-uncovered-locations.md) | 中文 + +## Problem + +per-file 100% 覆盖率门禁失败时,vitest 只输出文件级错误行(`ERROR: Coverage for lines (…) does not meet global threshold (100%) for `)——知道哪个文件没达标,不知道差在哪几行。内置 `text` 报表虽有 Uncovered Line #s 列,但它是全仓几百个文件的大表:该列按表宽截断、只有行号没有列号、不区分语句/分支/函数,且达标文件同样占行。结果是 CI 上的覆盖率红报不可直接行动,定位具体缺口只能本地重跑一遍 html 报表。 + +## Decision + +`scripts/coverage-uncovered-locations.cjs` 是一个自定义 istanbul reporter(`ReportBase` 子类):对每个低于 100% 的文件,按未覆盖语句、未走的分支路径、未调用函数各输出一条自含的单行记录 `:: uncovered …`——terminal 与 CI 日志中可直接点击跳转,也便于 grep。全部文件达标时零输出。istanbul 报表生成先于 threshold 校验,因此记录恰好落在既有 ERROR 行上方。 + +接线是单点的:根 `vitest.config.ts` 的 coverage 块是全仓唯一覆盖率配置,CI lane(`run-gates ci-coverage`)、本地 `test:coverage` 与聚焦跑(`--coverage.include`)共用它。该 reporter 以绝对路径(`fileURLToPath`)加入 CI 与本地两个 reporter 数组——istanbul-reports 的 `create()` 对非内置名回退为裸 `require(name)`,相对路径会按 istanbul 自己的包目录解析。 + +输出约定: + +- istanbul 的 0 基列号转为 1 基(编辑器与终端链接的约定)。 +- v8 对整行语句给出 `end.column = Infinity`:跨行时降级为只带行号的 `(to )` 后缀,单行时省略后缀。 +- 隐式分支臂(如缺省 else)可能不带位置,回退到分支自身的 span,保证记录仍可点击;分支记录标注类型与 `path k/n`。 +- 同文件内记录按行、列排序;不设条数上限。 + +配套两处:根 `package.json` 增补 devDependency `istanbul-lib-report`(pnpm 严格布局下 `scripts/` 摸不到嵌套依赖);`knip.json` 根 workspace 的 entry/project 通配增加 `scripts/**/*.cjs`,使该文件及其依赖对 hygiene 门禁可见。 + +CJS 是被迫的形态,也是 ESM-everywhere 纪律的一个有据例外:istanbul 在 tsx/Vite 管线之外用裸 `require()` 装载自定义 reporter,TypeScript 无法参与;`require(esm)` 返回的命名空间对象也过不了它的 `new Cons(cfg)` 构造,CommonJS 是唯一可靠形态。 + +## Alternatives considered + +- **依赖内置 `text` 报表的 Uncovered Line #s 列。** 正是问题现状:全仓大表、列宽截断、只有行号、不分种类、达标文件同列——CI 日志里不可行动。 +- **加 `json` reporter,另写 wrapper 脚本失败后读 `coverage-final.json` 后处理。** 纯 ESM/TS 可行,但 wrapper 必须同时包住 `package.json` 的 `test:coverage` 与 run-gates 的 gate 两个入口,命令形状随之改变;自定义 reporter 路线只动一处配置,两个入口自动生效。 +- **用 TypeScript/ESM 写 reporter。** istanbul 的装载机制(管线外裸 `require`)决定了不可行,见上;为一个报表文件把装载机制换掉不成比例。 + +## Verification + +本地矩阵:故意制造未达标时三类记录齐全、位置与埋点一致;混合现场只输出未达标文件(同跑内 100% 的文件静默);全绿跑零输出、退出码 0。CI 实证:临时在 `clampTimeout` 埋入一处不可达语句/分支/函数后,coverage lane 在全部测试通过(632 文件 / 10326 用例)、仅 threshold 失败的隔离条件下,把 4 条记录打印在 ERROR 行上方(证据链接见 PR #1716 描述);演示提交在合并前已从分支撤除。 + +## Consequences + +- 覆盖率红报自足:日志直接给出精确行列号与缺口种类,不再需要本地重跑 html 报表定位。 +- 代价是一个 CJS 文件的纪律例外与一个根 devDependency;全绿运行零输出,不增加日志噪音。 +- 整文件零覆盖时输出条数与该文件语句数同阶(刻意不设上限):门禁要求零缺口,全量列出即是行动清单,vitest 自身的 ERROR 行已按文件汇总兜底。