跨端: 自检 5 在 install 相位假红(期望值改用本相位实际会跑的文件数)+ 删掉 harmony-calendar 那份 stripStrings 兄弟副本(改用保行号的那份)

pi 2026-09-15 两条都成立,第一条**正好落在 `install.sh --check` 跑的那条命令上**。

## 一★★ 新判据在 install 相位**假红**:`records=23 ≠ SUITE.length=25`

```
$ AGENTMAIL_CRITERIA_PHASE=install AGENTMAIL_PROBE_DEVICE=none node test/run-all.mjs
✗ 套件自检 5:跑出的记录数 23 ≠ 套件文件数 25 —— …
    没有重复文件 ⇒ 是**漏了**或**多跑了**,不是 push 两次:检查 SUITE 与循环。
```

成因是那 2 条**构建相位判据被相位跳过**、进不了循环 ⇒ 不 push 记录。
★ 而报文给出的成因在这个情形下**恰好是错的**:什么都没漏、也没多跑。
**一条假红配一句假解释**,比单纯假红更坏 —— 读者会照着错的方向去查。

**修法**:期望值改成**本相位实际会跑的文件数**,而且和跳过判定用**同一个谓词**:

```js
const phaseWillRun = SUITE.map(([f]) => f)
  .filter(f => !(PHASE === 'install' && sideOf(f) === SIDE.SOURCE));
```

同一个事实两份实现就会漂移 —— 所以不给它第二份。

**两相位都验了**(不是只验改坏的那个):

| 相位 | 之前 | 现在 |
|---|---|---|
| build | `ok 5` | `ok 5`(`RESULT` 逐字节不变) |
| install | **`✗ 假红`** | **`ok 5`** |

注入"push 两次"复验**两相位都还抓得住**:build `50 ≠ 25`、install `46 ≠ 23`,都点名重复文件
(诊断方向这次是**对的**:"push 了两次"而非"漏了")。

★ 顺带又抓到**我自己造的同一形状**:改成 `phaseWillRun` 之后,`ok 5` 报文仍写着"== 文件数",
而 install 相位 `files=25` 里只有 23 条会跑 ⇒ **报文本身成了假话**。已改成
"== **本相位实际会跑的** 23 条(套件 25 条里本相位跳过 2 条)"。
**"改了实现没改说法"这是第四次** —— 这四次都是同一个动作,我把它当成本轮的主要账。

## 二★★ `stripStrings` 两份:删掉兄弟副本,改用**保行号**的那份

你说得对,而且我实测出**哪一份才是该活的**:

- `lib/read.mjs` 那份(经那五轮边界修、且**保留换行**)—— 是**死导出**;
- `harmony-calendar.test.mjs:18` 那份(正则近似)—— 是**活的**,而且**不保行数**。

实测 89 个 `.ets`/`.ts:

```
行数不守恒的: 3        (例:plugins/dsh-mail-bridge/src/index.ts 原 2248 → 2245)
抹除结果不同的: 75
```

★ 关键在**那条判据打印 `文件:行号`**(`hits.push(\`…:${i+1} …\`)`)——
行号是它**唯一**的定位手段,而行数一少,**跨行模板之后的报错行号整体前移**。
**这正是我两轮前为 `stripComments` 修过的同一条**("行号必须保持不变"):
那次我修在**主实现**里,**兄弟副本里没修** —— 而副本不知道。

**处置**:删掉本地那份,`harmony-calendar.test.mjs` 改用 `read.mjs` 那份(`import { code, stripStrings }`)。

**换之前先证明兼容**(不是"看起来一样就换"):两份实现在**这份判据真正用的那个性质**上
——89 个文件里"抹除后还有没有 `toISOString`"——**判定全部相同(0 处分歧)**。
⇒ 换实现**不改判据结论**,只是让行号可信。实测 `harmony-calendar` 23/23 通过。

★ 我认你那句"一个刚花五轮才证明脆弱的东西有两份实现,等于第六轮只会被修在其中一份里"。
这条已经在本仓发生过一次(`blurStyleFor`),这次是**兄弟副本**而不是死代码 ——
**死代码会被 grep 掉,兄弟副本不会**:它有自己的 import 链,看起来完全健康。

## 三、其余

- 两相位 `RESULT`:build `files=25 ran=25 checks=400 pass=399 fail=1 red=4 broken=0 unreported=0`;
  install `files=25 ran=23 checks=388 pass=388 fail=0 red=3 broken=0 unreported=0`。
- `criteria-hygiene` 6/6 绿。
- 红线条数没变(都不是我的)。
This commit is contained in:
2026-09-15 13:46:56 +08:00
parent cb54c53937
commit f4f9174865
2 changed files with 47 additions and 16 deletions

View File

@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { dirname, join } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { readdirSync } from 'node:fs';
import { code } from './lib/read.mjs';
import { code, stripStrings } from './lib/read.mjs';
/*
* `code()` 去的是**注释**,不是**字符串**(pi 2026-09-14 §2):任何只是"提到"被禁写法的
@ -11,16 +11,25 @@ import { code } from './lib/read.mjs';
*
* **口径现在就定下来**(两条路里选"只在表达式位置匹配"):先整段去掉字符串字面量,再匹配。
* 于是"提到"不再误红,而**真的用它取日期键**照样红。
* 副作用写在旁边,免得下一个人当成漏网:这是**正则近似、不是解析器** ——
* 转义引号、跨行模板、嵌套模板都处理不干净,它是采样而非完备性证明。
* **出现同族误红时不许"再改一次扫描口径"把判据往宽里挪。**
*
* ★★ 这里原来有**自己的一份** `stripStrings`(正则近似),现在改用 `lib/read.mjs` 那份
* (pi 2026-09-15 抓到"同一件东西两份实现")。三条理由,按重要性:
*
* ① **这份判据打印 `文件:行号`**(`hits.push(\`…:${i+1} …\`)`),行号是它**唯一**的定位手段。
* 旧那份不保证行数守恒 —— 实测 89 个 `.ets`/`.ts` 里 **3 个**抹除后行数变少
* (`plugins/dsh-mail-bridge/src/index.ts` 原 2248 → 2245),于是跨行模板之后的
* **报错行号整体前移**。**这正是我两轮前为 `stripComments` 修过的同一条**
* ("行号必须保持不变")—— 那次修在主实现里,**兄弟副本里没修**。
* ② `read.mjs` 那份是**逐字符**扫的(不是正则近似),**保留引号与换行** ⇒ 行数守恒
* (实测 89/89 不变,旧那份 3 个不对)。
* ③ 两份里**只有一份**经过那五轮边界修(注释/字符串/等价写法/锚/正则引号)——
* 留两份,第六轮只会被修在其中一份里。**不给自己留兄弟副本。**
*
* ★ 换之前我实测过兼容性:两份实现在**这份判据真正用的那个性质**上结论一致
* ——89 个文件里"抹除后还有没有 `toISOString`"**全部相同**(0 处分歧)。
* ⇒ 换实现**不改判据结论**,只是让行号可信。
*/
function stripStrings(src) {
return src
.replace(/`(?:[^`\\]|\\.)*`/g, '``')
.replace(/'(?:[^'\\\n]|\\.)*'/g, "''")
.replace(/"(?:[^"\\\n]|\\.)*"/g, '""');
}
/*
★ 日历的纯逻辑(P6 第 1 步的逻辑那一半)—— **不需要设备**,所以它不该进静态欠账。

View File

@ -236,6 +236,21 @@ const PHASE = process.env.AGENTMAIL_CRITERIA_PHASE === 'install' ? 'install' : '
function sideOf(file) { return SIDES[file] || SIDE.ARTIFACT; }
const brokens = [];
/*
* ★★ 本相位**实际会跑**的文件 —— 期望值必须由**同一个判据**算出来。
*
* pi 2026-09-15 抓到的假红:我上一版把"记录数"跟 `SUITE.length` 比,
* 而 install 相位有 2 条构建相位判据**进不了循环**(不 push 记录)⇒ `records=23 ≠ 25` ⇒ 报红,
* 报文还给出**恰好错误**的成因("漏了或多跑了")—— 什么都没漏、也没多跑。
* 而这条命令正是 `install.sh --check` 的前端门禁(`AGENTMAIL_CRITERIA_PHASE=install npm test`)
* ⇒ 一个**假红**会打出一行 `[FAIL]`,教读者"那行 `[FAIL]` 可以忽略"。
*
* 所以:期望值 = 在**本相位**下不被跳过的文件数,而不是套件总数。
* 并且它和下面的 skip 判定用**同一个谓词**(同一个 `sideOf`),免得两处各写一份条件而漂移 ——
* "同一个事实有两份实现"正是这一路反复出现的形状。
*/
const phaseWillRun = SUITE.map(([f]) => f)
.filter(f => !(PHASE === 'install' && sideOf(f) === SIDE.SOURCE));
const skippedByPhase = [];
for (const [file, flags, expected] of SUITE) {
if (PHASE === 'install' && sideOf(file) === SIDE.SOURCE) {
@ -741,18 +756,21 @@ try {
* 于是它**能被行为判据抓住**,而不是只能靠猜文本。
* **这是"文本判据 → 行为判据",不是"可能错 → 不可能错"。**
*/
if (records.length !== SUITE.length) {
if (records.length !== phaseWillRun.length) {
const seen = new Map();
for (const r of records) seen.set(r.file, (seen.get(r.file) || 0) + 1);
const dup = [...seen].filter(([, n]) => n > 1);
probs.push(`跑出的记录数 ${records.length} ≠ 套件文件数 ${SUITE.length} —— `
probs.push(`跑出的记录数 ${records.length} ≠ **本相位实际会跑**的文件数 ${phaseWillRun.length}`
+ `(套件共 ${SUITE.length} 条,本相位跳过 ${skippedByPhase.length} 条构建相位判据)—— `
+ `每个文件**恰好一条**记录(这是"每个文件只该计一次"的**行为**版本;`
+ `原来那条是扫源码文本的,已删,理由见上)。\n`
+ (dup.length > 0
? ` 重复的文件:${dup.map(([f, n]) => `${f}×${n}`).join('、')}\n`
? ` 重复的文件:${dup.map(([f, n]) => `${f}×${n}`).join('、')}`
+ `⇒ **push 了两次**(不是漏跑)。\n`
: ` 没有重复文件 ⇒ 是**漏了**或**多跑了**,不是 push 两次:检查 SUITE 与循环。\n`)
+ ` 修法:records.push 每个文件只该执行一次(它在循环体里、红绿分支**之外**)。`
+ `注意"搬动累加块"这个动作历史上做过三次,都把旧路径留在了原地。`);
+ ` ⚠️ 期望值用 \`phaseWillRun\`(本相位不被跳过的那些)而**不是** \`SUITE.length\`:`
+ `install 相位会跳过构建相位判据,拿套件总数比就是**假红**`
+ `(这条命令正是 \`install.sh --check\` 跑的那条)。`);
}
if (ranFilesCount > SUITE.length) {
probs.push(`ran=${ranFilesCount} > files=${SUITE.length} —— 自报条数的文件比文件总数还多,`
@ -781,12 +799,16 @@ try {
*
* ★ 本条的判据形状**在 pi 2026-09-15 的建议后换过**:原来是"扫自己的源码,
* 数四个计数器各有几处写"(文本判据,长出了五轮毛病);
* 现在是"**记录数 == 文件数**"(行为判据,见上面 `reportedRecords`)。
* 现在是"**记录数 == 本相位实际会跑的文件数**"(行为判据,见上面 `reportedRecords`)。
* 报文如实说它现在由谁判 —— 改了判据却留着旧文案,就是又一次"说法与实现不一致"。
* (**这句话我这轮又踩了一次**:改成 `phaseWillRun` 之后报文还写着"== 文件数",
* 而 install 相位 `files=25` 里只有 23 条会被跑 ⇒ 报文本身就成了假话。)
*/
console.log(`ok 5 - 总数行自洽(ran=${ranFilesCount} ≤ files=${SUITE.length},` +
`fail 是 checks 的子集;四个计数器由 ${records.length} 条记录**投影**而来 —— ` +
`判的是"记录数 == 文件数"这个**行为**,不是"源码里 \`+=\` 有几处")`);
`判的是"记录数 == **本相位实际会跑的** ${phaseWillRun.length} 条"`
+ `${skippedByPhase.length ? `(套件 ${SUITE.length} 条里本相位跳过 ${skippedByPhase.length} 条)` : ''}`
+ `这个**行为**,不是"源码里 \`+=\` 有几处")`);
}
} catch (e) {
console.error(`✗ 套件自检 5 跑不了:${e.message || e}`);