跨端: 修 ran/checks 被算两遍(搬动时只加没删)+ 补 red= 格 + 自检 5 钉住这类回归

pi 2026-09-15 把我上封 §⑦ 提的"口径对不上"**查成了真 bug**,不是已知口径。
他要的"对齐"我去做了,结果是**我错、他对**:总数行不只是"缺一格",其中**两格是虚高的**。

## 一、`ran` / `checks` / `pass` 被**算了两遍**

累加块(`totalTests += ran` / `ranFilesCount += 1` / `totalFail += …`)**存在两份**:

- 一份在红绿分支**之外**(`if (ran !== null) { … }`,注释还写着"必须在红/绿分支之外")——
  这是我**修另一半 bug 时搬过去**的:原先它在 `else` 里 ⇒ 红文件一条都不计入,`checks` 静默少算;
- **另一份还留在绿色 `else` 分支里**,搬动那一步**只加了新的、没删旧的**。

后果:**每个 exit 0 的文件都被计两遍**,只有退出码非 0 的(当时是 `build-stamp`)计一遍。

**修前 → 修后**(同一棵树、同一批判据):

```
修前  RESULT files=25 ran=49  checks=792 pass=791 fail=1 red=? broken=0 unreported=0
修后  RESULT files=25 ran=25  checks=399 pass=398 fail=1 red=4 broken=0 unreported=0
```

`ran: 49 → 25`(**每个文件恰好一次**,与 `files=25` 相等);`checks: 792 → 399`。
验算与 pi 的预测吻合:`24 个 exit 0 文件各多算一次` ⇒ `790 = 399 + 391`,
`ran = 24×2 + 1 = 49`。

★ **它是自洽的,所以读数检查不出来**:`pass + fail == checks`(791+1=792)
**两边一起虚高,恒等式照样成立** —— "自洽的读数不等于正确的读数"。
而 `RESULT files=…` 这行当初**就是为"总数要有权威来源"才加的**,结果**它自己就是错的**:
新机制进来时把旧路径留在原地。**与我们这一路在消的形状同族,只是这次长在总数行上。**

修法:`else` 分支里**只留 `if (ran === null) { … }`**("没找到自报条数"那条红),
累加删掉。顺带修掉一个还没露面的同类:`unreportedFiles += 1` 原来也在两处,
在"绿且没自报"时会加两次(现在 `unreported=0`,所以没人看见)。

## 二、`fail=1` vs 红 4 个:不是 bug,但**行里缺一格**

`fail` 累加的是**文件自报的失败条数**;那 3 条"自报条数 ≠ 登记数"的红是**运行器自己**推的
(`reds.push`),**不经过文件** ⇒ 进不了 `fail`。两格单位不同却只有一格,于是
`fail=1` 被读成"只有一处坏了"。**已补 `red=${reds.length}`**,并在那行上方写清三格的区别。

## 三、★ 自检 5:这类回归原来**没有任何东西拦着**

这个 bug 的形状是"**搬动一段代码时只加不删**",两处都跑得通、都报得出数,
而**没有任何东西会红**。所以加了一条结构性判据:

```
ran ≤ files      —— 一个文件最多贡献一次 ranFilesCount
fail ≤ checks    —— fail 是 checks 的子集
```

**为什么只判这两条**:`ran ≤ files` 是**结构性的上限**,不需要知道任何自报条数就能判。
而 `checks` **没有**这种上限(条数是文件自己报的)—— 给它编一个上限就是**猜数字的判据**,
所以不编;`checks` 的正确值只能逐文件核(这正是这个 bug 难发现的原因)。

**变异验证**(不是只跑绿):把双计重新注入 `else` 分支 ⇒
`✗ 套件自检 5:ran=49 > files=25 —— 每个文件只该计一次,超出来说明累加块有不止一份`;
还原 ⇒ `ok 5 - 总数行自洽(ran=25 ≤ files=25)`。

## 四、格式改动的影响面我核了

`RESULT files=…` 加了 `red=` 一节 ⇒ 先查**有没有别的解析者**:
全仓(`*.mjs`/`*.sh`/`*.md`/`*.go`)搜 `RESULT files=` 与 `checks=`/`ran=`,
**除 `run-all.mjs` 自己外没有任何消费者**;`mutants/` 那套工具**不共用**这一行
(它自己打 `RESULT mutants=48 ran=48 …`,口径 A = 36 不变)。所以加格是安全的。

## 五、未做

- 到期那 7 条(`STATIC_ONLY`)仍未动。
- 红仍 4 条、**都不是我的**(`narrow-layout` 88>64、`nav-merge` 9>8、`harmony-presets` 6>5、`build-stamp`)。
- `mutants` 的 `ran=48`/`mutants=48` 与本次无关(另一套计数),**没动**。
This commit is contained in:
2026-09-15 12:44:13 +08:00
parent 8ce1873166
commit d23ef69c88

View File

@ -305,20 +305,31 @@ for (const [file, flags, expected] of SUITE) {
} else if (r.status !== 0) reds.push(`${file}(退出码 ${r.status})`);
else {
/*
* 自报条数(闭环):自定义 check() 打 `RESULT pass=N fail=M`,node:test 打 `# pass N`。
* 只解析**固定 marker**,不去猜口语汇总(「窄屏布局:全部通过」里没有数字,
* 靠猜数字的写法会误报 —— pi 提过,我也先贴过真实样本)。
* ★ 这里**只保留"没自报条数"的那条红**;累加**不能**在这里做。
*
* 这段曾经是累加块(`totalTests += ran` / `ranFilesCount += 1` / `totalFail += …`)**唯一的**
* 位置,而它在 `else` 里 ⇒ **红文件一条都不计入**,`checks` 静默少算
* (实测:`checks=379` 而逐文件手算 387,差的正是红文件里的)。
* 后来把累加搬到了红绿**分支之外**(上面 `if (ran !== null) { … }`),
* **但没删掉这一份** —— 于是变成:
*
* `totalTests`/`ranFilesCount`/`totalFail` 对**每个 exit 0 的文件计两遍**,
* 只有退出码非 0 的文件(当时是 `build-stamp`)计一遍。
* 实测吻合:25 个 section、24 个 exit 0、1 个非 0
* ⇒ `ran = 24×2 + 1 = 49`(汇总行正是 `ran=49`)
* ⇒ `checks ≈ 逐文件单次合计 × 2`(`checks=792`)
*
* ★ 它是**自洽**的,所以读数检查不出来:`pass + fail == checks`
* (791+1=792)**两边一起虚高,恒等式照样成立** ——
* "自洽的读数不等于正确的读数"。而 `RESULT files=…` 这行本是为
* "总数要有权威来源"才加的,结果**它自己就是错的**:
* 新机制进来时把旧路径留在原地。这和我们这一路在消的形状同族,
* 只是这次长在总数行上(pi 2026-09-15 找到的)。
*
* ⇒ 教训写在这儿:**搬动一段代码时,删旧的那一步要和加新的那一步一起提交**;
* 只加不删的话,两处都跑得通、都报得出数,而**没有任何东西会红**。
*/
const marker = /RESULT pass=(\d+) fail=(\d+)/.exec(out);
const nodeTest = /^# pass (\d+)/m.exec(out);
const nodeFail = /^# fail (\d+)/m.exec(out);
const ran = marker ? Number(marker[1]) : (nodeTest ? Number(nodeTest[1]) : null);
// 累计总数(见汇总末尾的 `RESULT files=…`)—— 解析成功就加,无论该文件红绿
if (ran !== null) {
totalTests += ran;
ranFilesCount += 1;
totalFail += marker ? Number(marker[2]) : (nodeFail ? Number(nodeFail[1]) : 0);
} if (ran === null) {
if (ran === null) {
unreportedFiles += 1;
/*
* 报错**自带修法**(pi 2026-09-14):这条契约的受众不只是读过规范的人 ——
@ -602,6 +613,43 @@ try {
selfCheckFailed = true;
}
/*
* 自检 5(pi 2026-09-15 找到的总数行 bug):**`ran` 不许超过 `files`** —— 每个文件只该计一次。
*
* 起因(真发生过):累加块在红绿分支**之外**和绿分支 `else` 里**各有一份**,
* 于是每个 exit 0 的文件被计**两遍** ⇒ `ran=49`(而 files=25)、`checks` 约 2 倍。
* 它**自洽**,所以读数检查不出来:`pass + fail == checks`(791+1=792)两边一起虚高,恒等式照样成立。
* 而 `RESULT files=…` 这行本是为"总数要有权威来源"才加的 —— **它自己就是错的**。
*
* 判据形状:`ran <= files` 是**结构性的上限**(一个文件最多贡献一个 `ranFilesCount`),
* 不需要知道任何自报条数就能判。`checks` 没有这种上限(条数是文件自己报的),
* 所以别在这里给 `checks` 编一个 —— 那会变成"用猜数字的判据"。
*
* ⚠️ 这条自检管的是**总数的自洽性**,不是"条数对不对":`checks` 的正确值只有
* 逐文件核得出(这正是这个 bug 难发现的原因)。它拦的是"又一次搬动只加不删"。
*/
try {
const probs = [];
if (ranFilesCount > SUITE.length) {
probs.push(`ran=${ranFilesCount} > files=${SUITE.length} —— 每个文件只该计一次,` +
`超出来说明**累加块有不止一份**(搬动时只加没删)。` +
`修法:全文件搜 \`totalTests +=\`,累加只许出现在一处(红绿分支之外那一处)。`);
}
if (totalTests < totalFail) {
probs.push(`checks=${totalTests} < fail=${totalFail} —— fail 是 checks 的子集,这不可能成立`);
}
if (probs.length) {
console.error('✗ 套件自检 5(总数行的自洽性):');
for (const x of probs) console.error(` - ${x}`);
selfCheckFailed = true;
} else {
console.log(`ok 5 - 总数行自洽(ran=${ranFilesCount} ≤ files=${SUITE.length},fail 是 checks 的子集)`);
}
} catch (e) {
console.error(`✗ 套件自检 5 跑不了:${e.message || e}`);
selfCheckFailed = true;
}
/*
* pi 2026-09-15:这块原先在报文末尾 `process.exit(1)`,而**它在汇总之前** ⇒
* 只要到期闸红了,`RESULT` 行、欠账余额、条数不符、以及**所有其它红**都不会打出来。
@ -738,9 +786,20 @@ console.log(`\n========== 判据汇总 ==========`);
* 那个口径只跑标准 `test/*.test.mjs`,**不跑套件里的构建相位**,也不含本次相位跳过。
* `pass + fail == checks` 恒成立(fail 是那些文件里的红条数)。
*/
/*
* ★ 这一行的**三个"坏"计数单位不同**,别混着读(pi 2026-09-15 指出缺一格):
*
* · `fail=N` —— **文件自报的失败条数**(`# fail N` / `RESULT fail=N` 累加)。
* · `red=N` —— **红的判据文件数**(`reds`,含**运行器自己**推的那些,见下)。
* · `broken=N` —— 跑不起来(exit 非 0 / 无输出)的文件数。
*
* 为什么必须把 `red` 显式打出来:那 3 条"自报条数 ≠ 登记数"的判据是**运行器自己**推的
* (`reds.push`),根本不经过文件 ⇒ **进不了 `fail`**。实测过 `fail=1` 而"红的判据(4/25)",
* 于是 `fail=1` 被读成"只有一处坏了"。两格都在,读的人才不用自己猜单位。
*/
const totalsLine = (verdict) => `RESULT files=${SUITE.length} ran=${ranFilesCount} ` +
`checks=${totalTests} pass=${totalTests - totalFail} fail=${totalFail} ` +
`broken=${brokens.length} unreported=${unreportedFiles} verdict=${verdict}`;
`red=${reds.length} broken=${brokens.length} unreported=${unreportedFiles} verdict=${verdict}`;
if (reds.length === 0 && brokens.length === 0 && !dueFailed && !selfCheckFailed) {
// 报**本相位实际跑了**几条,不报清单长度 —— 上面刚写完"不能声称自己做过的事",
// 这里如果打 SUITE.length 就是在同一行里犯同一个错(2 条构建相位判据被跳过了)。