From f4f91748659f2a8ef156d6f9e129425b5da26703 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Tue, 15 Sep 2026 13:46:56 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=A8=E7=AB=AF:=20=E8=87=AA=E6=A3=80=205=20?= =?UTF-8?q?=E5=9C=A8=20install=20=E7=9B=B8=E4=BD=8D=E5=81=87=E7=BA=A2?= =?UTF-8?q?=EF=BC=88=E6=9C=9F=E6=9C=9B=E5=80=BC=E6=94=B9=E7=94=A8=E6=9C=AC?= =?UTF-8?q?=E7=9B=B8=E4=BD=8D=E5=AE=9E=E9=99=85=E4=BC=9A=E8=B7=91=E7=9A=84?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E6=95=B0=EF=BC=89+=20=E5=88=A0=E6=8E=89=20ha?= =?UTF-8?q?rmony-calendar=20=E9=82=A3=E4=BB=BD=20stripStrings=20=E5=85=84?= =?UTF-8?q?=E5=BC=9F=E5=89=AF=E6=9C=AC=EF=BC=88=E6=94=B9=E7=94=A8=E4=BF=9D?= =?UTF-8?q?=E8=A1=8C=E5=8F=B7=E7=9A=84=E9=82=A3=E4=BB=BD=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 绿。 - 红线条数没变(都不是我的)。 --- .../electron/test/harmony-calendar.test.mjs | 27 +++++++++----- client/electron/test/run-all.mjs | 36 +++++++++++++++---- 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/client/electron/test/harmony-calendar.test.mjs b/client/electron/test/harmony-calendar.test.mjs index ff9a726..c3d4221 100644 --- a/client/electron/test/harmony-calendar.test.mjs +++ b/client/electron/test/harmony-calendar.test.mjs @@ -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 步的逻辑那一半)—— **不需要设备**,所以它不该进静态欠账。 diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 24747a6..a041eac 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -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}`);