跨端: unreported 双计(我上轮说修了、代码没)+ checks/pass 单位取错(少算失败条数)

pi 2026-09-15 复核后指出总数行上**还有两个活的**,两条都对,其中一条是**我上一封说修了、
代码里没修**。

## 一、`unreportedFiles += 1` 仍在两处 —— 我说修了,没修

```
317  else { unreportedFiles += 1; }        ← 分支之外(位置对)
354  if (ran === null) { unreportedFiles += 1; … }   ← 绿色 else 里,**没删掉**
```

一个"exit 0 且不自报条数"的文件:`ran === null` ⇒ 第一块走 `else`(+1),接着进绿色
`else` ⇒ 再走 `if (ran === null)`(+1)⇒ **计两次**。

★ **上次我删掉了那份 `totalTests`/`ranFilesCount`/`totalFail`,却把紧跟的
`unreportedFiles += 1` 留在原地** —— 然后我在信里写"顺带修掉了"。
**"说修了"与"修了"是两件事,而只有后者可复核**。这是同一个形状的**第三次**
(搬动/新增时留了旧路径),而这次它长在**修那个形状的提交里**。

## 二、`checks`/`pass` 的单位是 `# pass`,不是 `# tests`

node:test 打三行 —— `# tests N` / `# pass N` / `# fail N`,且 `# tests = # pass + # fail`。
而原代码取的是 `/^# pass (\d+)/`:

```
Σ 自报 pass = 399、Σ 自报 fail = 1  ⇒ 真实总条数 400
汇总行:      checks=399  pass=398     ← 两格都少算
```

- `checks` = Σ`# pass` ⇒ **少了失败的那几条**;
- 接着 `pass = checks - fail` = 399 − 1 = 398,**在一个从未包含失败的和上又减了一次失败**
  (真实通过数是 399)。

修法:**总条数与失败条数各取各的** —— 自定义文件取 `pass+fail`,node:test 取 `# tests`
(缺失时回退 `# pass + # fail`,**不是** `# pass`)。

修后:`checks=400 pass=399 fail=1`,与 pi 用我的输出算出的数**逐字一致**。

★ 这一步的真正价值不是那两个数字,而是:**`pass + fail == checks` 从"定义式"变成了真不变量**。
原来 `pass` 是 `checks − fail` 派生的 ⇒ 那个等式**按构造恒成立、永远抓不到任何东西**
(上一轮"两边一起虚高时它照样成立"就是同一个病:**恒等式两边同源就没有检验力**)。
**这条我早就在 `511982ac` §五 自己写过**("单位应该是 `# tests`"),它没落进代码 ——
"已知但未落"比新引入的更该先修,因为共识已经有了,只差一次编辑。

## 三、★ 我加的第一版自检是**空的**,自己测出来了

pi 建议给 `unreportedFiles` 补一条 `unreported ≤ files` 上界。我先照做了,然后测了一下:

> 一次运行里一个文件只被处理一次,而 bug 的效果是**同一次处理里加两次** ⇒
> 只有 1 个这种文件时 `unreported=2`,而 `2 > 25` **为假** ⇒ **永不触发**。
> 要让它触发得需要 >25 个"绿且不自报"的文件,而套件只有 25 个文件。
> ⇒ **数学上成立、实际上永远抓不到任何东西的判据** —— 而我是**在加它的时候**又造了一个。

(顺带更正上面那条 `ran > files`:它同样只在"几乎所有文件都双计"时才响 ——
也就是**那个 bug 正好发生时它才响**。侥幸有效,但机理不是我以为的那个。)

改成查**源码里的增量处数**:四个按文件累加的计数器(`totalTests`/`ranFilesCount`/
`totalFail`/`unreportedFiles`)各只许出现一次。**形态判据**(读自己的源码),不是行为判据 ——
它能抓"搬动/新增时留了旧路径"(实际发生的那三次),抓不到"某条路径上被条件性地加两次"。
后者要真跑,得把累加抽成**纯函数**(入参 = 解析结果,出参 = 增量)再喂合成输入 ——
**那才是能真跑的形状,本轮不做,已在注释里写明它是唯一能真跑的形状。**

计数还得**锚在语句位置**(`^\s*X +=`):第一版用 `\bX +=`,把**我自己报错文案里**的
`X +=` 也数了进去 ⇒ 报 `totalTests += 出现 2 处`,而真身只有一处
(**扫描器咬到自己的文档**,与 `@ohos`/`toISOString` 那两次同族)。

**变异验证**:`unreportedFiles` 两处 ⇒ 红;`ranFilesCount` 两处 ⇒ 红;还原 ⇒ 绿。
单位那条的观测面也验了:改回 `# pass` ⇒ `checks=399 pass=398`(少算 1 条 = 失败条数)。

## 四、顺带修掉一个我自己埋的崩:一个读不到的文件会把整条判据吃掉

`checks` 单位一改对,`commit-hygiene` 立刻**红了**(`fail=2 red=5`)——
看着像是我改了计数引出的,其实是一条**真 bug 被暴露出来**:那条 AGC 判据裸用
`prose(join(ROOT, f))` 遍历**所有 tracked 文件**,而 `server/internal/repo/suggest_paths_test.go`
(另一个会话的未跟踪文件)此刻**读不到** ⇒ `ENOENT` ⇒ **整条判据崩掉**,
报出来的是"跑不起来",而**真正该报的"谁夹带了真值"一条都没报**。
⇒ **一道判据自己变成它要抓的东西的噪声。**

改:逐个文件 try/catch,读不到的单列 `unreadable` 并**报红**(**不许静默跳过** ——
静默跳过等于给"把真值放进一个读不到的文件"让路)。修后 4/4 绿,总数行回到
`pass=399 fail=1 red=4`。

## 五、修后状态

```
RESULT files=25 ran=25 checks=400 pass=399 fail=1 red=4 broken=0 unreported=0 verdict=red
ok 5 - 总数行自洽(ran=25 ≤ files=25,unreported=0 ≤ files,fail 是 checks 的子集)
```

红仍 4 条、**都不是我的**(`narrow-layout` 88>64、`nav-merge` 9>8、`harmony-presets` 6>5、
`build-stamp`)。到期那 7 条仍未动。
This commit is contained in:
2026-09-15 12:55:25 +08:00
parent dfb4b8537e
commit 239f919b71
2 changed files with 123 additions and 15 deletions

View File

@ -206,14 +206,35 @@ test('★ 版本库里不许跟踪 AGC 配置真身(gitignore 挡不住 `git a
{ what: 'AGC api_key 信封', re: /"api_key"\s*:\s*"\[!/ }
];
const offenders = [];
/*
* ★ **一个读不到的文件不许把整条判据吃掉**(我自己撞上的):
* 原来这里是裸 `prose(join(ROOT, f))`。只要**任何一个** tracked 文件此刻读不到
* (并发写入者正在写、刚 `git add` 还没落盘、被 `rm` 了),`prose()` 就抛 `ENOENT`,
* 整条判据**崩掉** ⇒ 报出来的是"跑不起来",而**真正该报的"谁夹带了真值"一条都没报**。
* 实测:`server/internal/repo/suggest_paths_test.go` 被并发会话删掉的那一瞬间,
* 这条判据以 `ENOENT` 红,而它本该报的是 AGC 形状。
* ⇒ **一道判据自己变成它要抓的东西的噪声**。所以逐个文件 try/catch,读不到的单独记账,
* 并且**读不到就明说**(不许静默跳过 —— 静默跳过等于给"把真值放进一个读不到的文件"让路)。
*/
const unreadable = [];
for (const f of files) {
// example 是**故意**带这些键名的(结构留、值全打掉)——按内容判,不按文件名豁免
const src = prose(join(ROOT, f));
let src;
try {
src = prose(join(ROOT, f));
} catch (e) {
unreadable.push(`${f}(${(e && e.code) || 'ERR'})`);
continue;
}
const hit = SHAPE.filter(s => s.re.test(src)).map(s => s.what);
// 只声明了键名但没有真值(值全是 `<…>`)的不算违规
const onlyPlaceholders = !/\[!00/.test(src) && !/"[0-9A-F]{32,}"/.test(src);
if (hit.length > 0 && !onlyPlaceholders) offenders.push(`${f}(${hit.join('、')})`);
}
assert.deepEqual(unreadable, [],
'这些 **tracked** 文件此刻读不到 ——\n' +
' 这条判据要**逐个读 tracked 文件**才能判"谁夹带了 AGC 真值",读不到就判不了。\n' +
' ★ 不静默跳过:静默跳过等于给"把真值放进一个读不到的文件"让路。\n' +
` 常见成因:并发会话正在写/删(本仓是共享工作树)。重跑一次通常就好。\n ${unreadable.join('\n ')}`);
assert.deepEqual(offenders, [],
'这些 tracked 文件里有 **AGC 配置的真身**(不是 example)——\n' +
' 它是客户端凭证,而本仓的镜像是**匿名可 clone 的公开项目**(docs/GITCODE-MIRROR.md):\n' +

View File

@ -189,7 +189,19 @@ const reds = [];
* 它只累加**它自己实际跑过并解析到自报条数**的那些文件,不猜、不扫目录。
*/
let totalTests = 0;
/** 逐文件自报的**失败**条数之和(`fail=` 那一格)。`pass + fail == checks` 是恒等式 */
/**
* 逐文件自报的**失败**条数之和(`fail=` 那一格)。
*
* ⚠️ 别再把这行写成"`pass + fail == checks` 是恒等式"就算完了 ——
* 汇总行里的 `pass` 是 `totalTests - totalFail` **派生**出来的,所以那个等式
* **按构造恒成立、永远抓不到任何东西**(上一轮"两边一起虚高时它照样成立"就是同一个病:
* **恒等式两边同源就没有检验力**)。
*
* 现在**两边各自独立测得**:`checks` 取文件的**总条数**
* (自定义文件 `pass+fail`、node:test 取 `# tests`),`fail` 取文件的失败条数。
* 所以 `pass + fail == checks` 从"定义式"变成了**真不变量** ——
* 单位取错(例如用 `# pass` 当总条数)现在会**直接破坏它**,而不是被它掩盖。
*/
let totalFail = 0;
/**
* **实际跑了但没能自报条数**的文件数 —— 这个数必须显式打出来。
@ -280,19 +292,41 @@ for (const [file, flags, expected] of SUITE) {
* `checks` **静默少算**。实测:`checks=379` 而逐文件手算 387 —— 差的那些正是红文件里的。
* **"红"只说明判据说"不成立",不说明它没跑**;跑过的条数一样要算进来。
*
* 单位是自报的条数本身(`RESULT pass=N fail=M` 的 N+M,或 `# tests N`),
* 两种文件的"条"不是同一种条,所以汇总里那一格叫 `checks` 而不叫 `tests`。
* ★ **单位必须是"总条数",不是"通过条数"**(pi 2026-09-15 指出,我 511982ac §五
* 自己就写过这条、但**没落进代码**):
*
* node:test 会打三行 —— `# tests N` / `# pass N` / `# fail N`,而 `# tests = # pass + # fail`。
* 原来取的是 `/^# pass (\d+)/` ⇒ `checks` = Σ`# pass` = **399**,
* 比真实总条数(400)**少了失败的那几条**;接着 `pass = totalTests - totalFail`
* = 399 − 1 = **398**,**在一个从未包含失败的和上又减了一次失败**(真实通过数是 399)。
* 实测(唯一有失败的文件 `build-stamp`:`# tests 7 / # pass 6 / # fail 1`):
* Σ 自报 pass = 399、Σ 自报 fail = 1 ⇒ 真实总条数 **400**
* 汇总行却是 `checks=399 pass=398` —— 两格都少算。
*
* ⇒ 所以:**总条数与失败条数各取各的**,`pass` 只作为**派生量**打印。
* 这样 `pass + fail == checks` 才从**定义式**(`pass := checks - fail`,按构造恒成立、
* 永远抓不到任何东西)变成**真不变量**(两边各自独立测得)。
* 上一轮那条"虚高时恒等式照样成立"是同一个病 —— **恒等式两边同源就没有检验力**。
*/
const marker = /RESULT pass=(\d+) fail=(\d+)/.exec(out);
const nodeTest = /^# pass (\d+)/m.exec(out);
const marker = /RESULT pass=(\d+) fail=(\d+)/.exec(out); // 自定义 check():pass/fail 自报
const nodeTests = /^# tests (\d+)/m.exec(out); // node:test:总条数
const nodeFail = /^# fail (\d+)/m.exec(out);
const ran = marker ? Number(marker[1]) : (nodeTest ? Number(nodeTest[1]) : null);
/*
* `# tests` 缺失时**回退**到 `# pass + # fail`(不是 `# pass`)——
* 老版本 node 或输出被截断时,和才等于总条数;单取 `# pass` 正是上面那个 bug。
* 两者都没有 ⇒ null("没自报条数",走 unreported 那条红)。
*/
const nodePass = /^# pass (\d+)/m.exec(out);
const nodeTotal = nodeTests ? Number(nodeTests[1])
: (nodePass ? Number(nodePass[1]) + (nodeFail ? Number(nodeFail[1]) : 0) : null);
const ran = marker ? Number(marker[1]) + Number(marker[2]) : nodeTotal;
const failed = marker ? Number(marker[2]) : (nodeFail ? Number(nodeFail[1]) : 0);
if (ran !== null) {
totalTests += ran;
ranFilesCount += 1;
totalFail += marker ? Number(marker[2]) : (nodeFail ? Number(nodeFail[1]) : 0);
totalFail += failed;
} else {
unreportedFiles += 1;
unreportedFiles += 1; // ★ 唯一的增量处(另一份在绿色 else 里,已删,见下)
}
// 不 break:后面每条都要跑出来,否则"红了几条"这个信息本身是假的
if (crashed || empty) {
@ -328,9 +362,16 @@ for (const [file, flags, expected] of SUITE) {
*
* ⇒ 教训写在这儿:**搬动一段代码时,删旧的那一步要和加新的那一步一起提交**;
* 只加不删的话,两处都跑得通、都报得出数,而**没有任何东西会红**。
*
* ★ 而**我自己就是这条教训的下一个实例**(pi 2026-09-15 复查抓到):
* 我在上一次修这里时,删掉了那份 `totalTests`/`ranFilesCount`/`totalFail`,
* **却把紧跟的 `unreportedFiles += 1` 留在了原地** —— 于是它**仍然计两次**
* (`:317` 的 `else` 一次、这里又一次)。我在信里写"顺带修掉了",**代码里没有**。
* "说修了"与"修了"是两件事,而**只有后者可复核** —— 这正是本仓反复立的规矩。
* ⇒ 这一行现在删掉了,并且由自检 5 的 `unreportedFiles ≤ files` 钉着
* (它今天不可见:`unreported=0`,没有文件触发 —— **看不见正是它需要结构性上界的理由**)。
*/
if (ran === null) {
unreportedFiles += 1;
/*
* 报错**自带修法**(pi 2026-09-14):这条契约的受众不只是读过规范的人 ——
* 并发写 WebUI 的 agent 新加判据时不会打开 CRITERIA.md,看到红的第一反应
@ -625,6 +666,14 @@ try {
* 不需要知道任何自报条数就能判。`checks` 没有这种上限(条数是文件自己报的),
* 所以别在这里给 `checks` 编一个 —— 那会变成"用猜数字的判据"。
*
* ★ **这个上界对"每一个按文件累加的计数器"都成立**(pi 2026-09-15 指出我只判了两个):
* 它们一共三个 —— `ranFilesCount` / `totalFail` / `unreportedFiles`。
* 我漏了 `unreportedFiles`,而它当时**真的仍然计两次**(我上一次"修好了"的那个提交里,
* 删了 `totalTests` 那三行却把紧跟的 `unreportedFiles += 1` 留在原地)。
* ⇒ 教训:**"每个文件只该计一次"是一条对整族计数器都成立的性质,不是对某一个**;
* 只给其中两个判上界,第三个就永远没人管。
* (`totalFail <= checks` 只用 "fail 是 checks 的子集",比 `<= files` 更紧且同样结构性。)
*
* ⚠️ 这条自检管的是**总数的自洽性**,不是"条数对不对":`checks` 的正确值只有
* 逐文件核得出(这正是这个 bug 难发现的原因)。它拦的是"又一次搬动只加不删"。
*/
@ -638,12 +687,47 @@ try {
if (totalTests < totalFail) {
probs.push(`checks=${totalTests} < fail=${totalFail} —— fail 是 checks 的子集,这不可能成立`);
}
/*
* ★ **"每个文件计一次"必须查"增量语句有几处",不能查数值上界**
* (pi 2026-09-15 指出我漏了 `unreportedFiles` 之后,**我自己测出来的更正**):
*
* 我原打算给 `unreportedFiles` 补一条 `unreported ≤ files` 的上界 —— **它是空的**:
* 一次运行里一个文件只被处理一次,而 bug 的效果是**同一次处理里加两次** ⇒
* 只有 1 个这种文件时 `unreported=2`,而 `2 > 25` 为假 ⇒ **永不触发**。
* 要让它触发得需要 >25 个"绿且不自报"的文件,而套件只有 25 个文件。
* ⇒ **数学上成立、实际上永远抓不到任何东西的判据** ——
* 这正是我们这一路在消的形状,而我是**在加它的时候**又造了一个。
* (顺带更正上面那条 `ran > files`:它同样只在"几乎所有文件都双计"时才响,
* 也就是**那个 bug 正好发生时它才响** —— 侥幸有效,但机理不是我以为的那个。)
*
* 所以改成查**源码里的增量处数**:四个计数器各只许出现一次。
* 诚实说清性质:这是**形态判据**(读自己的源码),不是行为判据 ——
* 它能抓"搬动/新增时留了旧路径"(实际发生的那三次),
* 抓不到"在某条路径上被条件性地加两次"。
* 后者要真跑,得把累加抽成一个纯函数(入参 = 解析结果,出参 = 增量),再拿合成输入喂它。
* **那才是能真跑的形状,本轮不做,但记在这里**。
*
* ⚠️ 计数必须**锚在语句位置**(行首缩进后即 `X +=`),否则**我这条报错文案自己**里
* 出现的 `X +=` 会被数进去 —— 第一次跑就是 `totalTests += 出现 2 处`,
* 而真身只有一处(**扫描器咬到自己的文档**,与 `@ohos`/`toISOString` 那两次同族)。
* `stripComments` 只去注释、**不去字符串**,所以这里不能只靠它。
*/
const ownSrc = stripComments(prose(fileURLToPath(import.meta.url)));
for (const counter of ['totalTests', 'ranFilesCount', 'totalFail', 'unreportedFiles']) {
const n = (ownSrc.match(new RegExp(`^\\s*${counter}\\s*\\+=`, 'gm')) || []).length;
if (n !== 1) {
probs.push(`\`${counter} +=\`(语句位置)出现 **${n}** 处(只许 1 处)—— ` +
`"每个文件只该计一次"是对**整族**计数器都成立的性质,不是一个一个判的;\n` +
` 这个形状已经发生过三次(搬动/新增时留了旧路径),而**两处都跑得通、都报得出数**。`);
}
}
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 的子集)`);
console.log(`ok 5 - 总数行自洽(ran=${ranFilesCount} ≤ files=${SUITE.length},` +
`unreported=${unreportedFiles} ≤ files,fail 是 checks 的子集)`);
}
} catch (e) {
console.error(`✗ 套件自检 5 跑不了:${e.message || e}`);
@ -780,11 +864,14 @@ console.log(`\n========== 判据汇总 ==========`);
*
* ⚠️ 单位要写清:`checks` 是**逐文件自报条数之和** —— 也就是套件在每跑完一个文件时
* 真正解析过、并据此判"有没有少跑"的那个数。它**混两种单位**:
* `check()` 文件自报 `RESULT pass=N`(N 条判据),`node:test` 文件自报 `# pass N`
* (N 个 test 用例)。两者都是"条",但**不是同一种条**,所以这里叫 `checks` 而不叫
* `check()` 文件自报 `RESULT pass=N fail=M`(取 N+M 为总条数),
* `node:test` 文件自报 `# tests N`(总条数)。
* 两者都是"条",但**不是同一种条**,所以这里叫 `checks` 而不叫
* `tests`,免得下一个人把它跟 `node --test test/*.test.mjs` 的 glob 口径直接比 ——
* 那个口径只跑标准 `test/*.test.mjs`,**不跑套件里的构建相位**,也不含本次相位跳过。
* `pass + fail == checks` 恒成立(fail 是那些文件里的红条数)。
* `pass + fail == checks` 现在是**真不变量**(两边各自独立测得,见 `totalFail` 的注释)——
* 它此前是定义式(`pass` 由 `checks - fail` 派生),所以**形同虚设**:
* 单位取成 `# pass` 时 `checks` 与 `pass` 一起少算,那个等式照样成立。
*/
/*
* ★ 这一行的**三个"坏"计数单位不同**,别混着读(pi 2026-09-15 指出缺一格):