修复: 条数登记校验**只在"绿"的那条路上** —— 文件越红,它的登记数越没人守(假绿方向)

设备在场时跑全套,顺手核了每个文件的『登记条数 vs 实际条数』,发现:

  narrow-layout    登记=64 实际=88 exit=0  绿 ⇒ 校验生效(报了)
  nav-merge        登记=8  实际=9  exit=0  绿 ⇒ 校验生效(报了)
  background       登记=43 实际=44 exit=0  绿 ⇒ 校验生效(报了)
  harmony-presets  登记=5  实际=6  exit=0  绿 ⇒ 校验生效(报了)
  harmony-admin    登记=22 实际=27 exit=1  ★ 红 ⇒ 校验**走不到**(grep 0 命中)

根因:这条校验原来写在**最后一个 `else`**("退出码 0"那条路)里,而
`r.status !== 0` 会**先在 `else if` 里 reds.push 并跳过它**。
⇒ **文件越红,它的条数登记越没人守** —— 假绿方向:
harmony-admin 那多出来的 5 条判据**不在"被删会红"的保护内**,
而它恰好是红的 ⇒ **只要它一直红,缺口就一直是隐形的**;
等它修绿那天校验才第一次生效,那时多出来的几条可能早被删了。
(同族:`unlisted`/`blind`/`baseline=` 的结论到不了 `verdict`,§16.1。)

★ 而且这个缺口**已经真的吃过一次**:`c523c21`("邮件详情与「我的」页 1:1 对齐")
给 harmony-admin **+5 条判据**(它自己的 commit message 就写着 "+5"),
**却没同步登记数**,而当时该文件是红的 ⇒ 5 条新判据至今裸奔。

修法:把条数校验移进 `else { … }`(红绿都跑;先 push 退出码红,再判条数)。
⚠️ broken(崩了/一条条数都没自报)**不在这里**判 —— `diedWithoutReporting` 已吃掉它,
再叠一条"没找到自报条数"只是噪音:**"判据没答 ≠ 判据答错了"**(§17)。
并做那个"显式、可复核的编辑":登记数 22 → 27(和 c523c21 欠下的那 5 条对齐)。

变异验证:
· 红的 harmony-admin 登记数改 99 ⇒ 报『自报 27 条 < 登记的 99 条』✓
· 改成 27(对齐)⇒ **不报条数**、只剩"退出码 1" ✓
· 修后全套:red=10(harmony-admin 那条从"走不到"变成"报出来"后 +
  对齐登记数又收回,净额 0),另 4 个绿文件的条数不符**照旧照报** ✓

`CRITERIA.md` 新增通用规则:**校验写在哪条分支上,决定它保护谁。**
凡"出错时要额外检查 X"的守卫,先问:**这条分支真红的时候,它还跑得到吗?**
This commit is contained in:
2026-09-19 12:51:29 +08:00
parent 176c90272b
commit 788d7ccb20
2 changed files with 60 additions and 4 deletions

View File

@ -487,6 +487,43 @@ console.log('主题:通过'); // 有输出
那正是要看见的事(顺手删两条判据、某条被跳过、`check` 实现被改坏)。
代价:**故意删判据时要同步改数字**(这是一次显式的、能被复核的编辑,可以接受)。
### 条数登记校验**曾经只在"绿"的那条路上**(dsh 2026-09-19 设备在场时发现)
上面那条校验原来写在**最后一个 `else`** 里("退出码 0"那条路)。而 `r.status !== 0`
会**先**在 `else if` 里 `reds.push(...)` 并**跳过**它 ⇒ **文件越红,它的条数登记越没人守。**
**实测**(同刻、设备在场、一次全套):
```
test/narrow-layout.test.mjs: 登记=64 实际=88 exit=0 绿 ⇒ 校验生效(报了)
test/nav-merge.test.mjs: 登记=8 实际=9 exit=0 绿 ⇒ 校验生效(报了)
test/background.test.mjs: 登记=43 实际=44 exit=0 绿 ⇒ 校验生效(报了)
test/harmony-presets.test.mjs: 登记=5 实际=6 exit=0 绿 ⇒ 校验生效(报了)
test/harmony-admin.test.mjs: 登记=22 实际=27 exit=1 ★ 红 ⇒ 校验**走不到**(0 命中)
```
⇒ ★ 这是本仓那条母规则的又一例:**"判据在,但走不到"**,
而且方向是**假绿**:`harmony-admin` 那多出来的 5 条判据**不在"被删会红"的保护内**,
而它恰好是红的 ⇒ **只要它一直红,这个缺口就一直是隐形的**;
等它被修绿那天校验才第一次生效,那时多出来的几条**可能早被删了**。
(同族:`unlisted`/`blind`/`baseline=` 的结论到不了 `verdict`,§16.1。)
**修法**:把条数校验移进 `else { … }`(红绿都跑;先 push 退出码红,再判条数)。
⚠️ 但 **broken(崩了/一条条数都没自报)不在这里判** —— `diedWithoutReporting`
已经把它归入 broken,再叠一条"没找到自报条数"只是噪音:
**"判据没答 ≠ 判据答错了"**(§17)。
**修后实测**:`harmony-admin` 那一段现在报
`自报 27 条 > 清单里登记的 22 条 —— 新加的那几条不在"被删会红"的保护内`,`red` 10→11。
**变异**:把红的 `harmony-admin` 登记数改成 `99` ⇒ 报 `自报 27 条 < 登记的 99 条` ✓;
改成 `27`(对齐)⇒ **不报条数**、只剩"退出码 1" ✓。
★ **通用规则**:
> **校验写在哪条分支上,决定它保护谁。**
> 凡"出错时要额外检查 X"的守卫,先问:**这条分支真红的时候,它还跑得到吗?**
> —— 把校验放在"一切正常"的那条路上,等于**只在没出事时守着**。
计数必须写在 **`check()` 内部**:写在调用点或靠扫源码,"实现被换空"就看不见了
—— 上面那个反例的 `pass` 会是 0,正是靠这一条才有分辨力。

View File

@ -109,7 +109,7 @@ const SUITE = [
['test/criteria-hygiene.test.mjs', [], 6],
// 用户管理页(P4c 同批):动作↔服务端调用同名 / 门禁只认严格 admin /
// 启停只发 status / 「受限」徽标口径 / 页面零写死色值 / 接线(纯逻辑真被调用)
['test/harmony-admin.test.mjs', ['--experimental-strip-types', '--no-warnings'], 22],
['test/harmony-admin.test.mjs', ['--experimental-strip-types', '--no-warnings'], 27],
// P4c 图片上传:阈值与两档策略 / 失败必带原因 / 退档判定只有一处 /
// release 都 await / 解码按目标尺寸 / multipart 字段名 / 上传后重新同步
['test/harmony-imageprep.test.mjs', ['--experimental-strip-types', '--no-warnings'], 30],
@ -513,8 +513,27 @@ for (const [file, flags, expected] of SUITE) {
'\n 常见成因:语法/标识符错(`X is not defined`)、import 写错、编译不过、' +
'\n **按相对路径读文件但 cwd 不对**(`ENOENT: … open \'src/index.css\'`)。' +
'\n 用于变体验证时:broken **不算这次变异有效**,要重做。');
} else if (r.status !== 0) reds.push(`${file}(退出码 ${r.status})`);
else {
} else {
if (r.status !== 0) reds.push(`${file}(退出码 ${r.status})`);
/*
* ★★ 「条数登记」校验必须在**这里**(红绿都跑),**不能留在下面那个 `else` 里**。
*
* 实测(dsh 2026-09-19,设备在场时跑全套发现的):
* `test/harmony-admin.test.mjs` **登记 22 条、实际 27 条**(差 5 条,
* 那 5 条**不在"被删会红"的保护内**),而**它当时是红的**(1 fail)⇒
* 走的是上面 `退出码 1` 那条分支 ⇒ **这条校验一次都没被评估**:
* `grep '自报.*harmony-admin' dev.log` ⇒ **0 命中**
* (同时刻另有 4 个**绿的**文件条数不符,它们**全都正常报了** ⇒
* 说明不是校验写错,是**它在红的那条路上到不了**。)
*
* ★ 这正是本仓反复消的那个形状:**"判据在,但走不到"** ——
* 而且方向是**假绿**:一个文件越红,它的登记数越没人守;
* 等到它被修绿那天,那条校验才第一次生效 —— 那时"多出来的几条"可能早被删了。
* (同族:`unlisted`/`blind`/`baseline=` 的结论到不了 `verdict`,§16.1。)
*
* ⚠️ 但 broken(崩了/没自报)**不在这里**判:`diedWithoutReporting` 已把它吃掉,
* 再叠一条"没找到自报条数"只是噪音 ——"判据没答 ≠ 判据答错了"(§17)。
*/
/*
* ★ 这里**只保留"没自报条数"的那条红**;**任何累加都不在这里做** ——
* 投影之后累加根本不作为语句存在,上面那条 push 出来的记录是唯一的信息来源。
@ -582,8 +601,8 @@ for (const [file, flags, expected] of SUITE) {
}
}
}
/*
* ─── 投影:把 `records` 算成那四个计数器(**这里是它们唯一的"来处"**)───
*
* ★ 为什么是这个形状(pi 2026-09-15 建议,我采纳并实测):