diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 2d63839..efc598c 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -682,7 +682,8 @@ try { if (ranFilesCount > SUITE.length) { probs.push(`ran=${ranFilesCount} > files=${SUITE.length} —— 每个文件只该计一次,` + `超出来说明**累加块有不止一份**(搬动时只加没删)。` + - `修法:全文件搜 \`totalTests +=\`,累加只许出现在一处(红绿分支之外那一处)。`); + `修法:数 \`ranFilesCount\` 的**写**有几处(**不一定是 \`+=\`** —— 见下面的源码扫描,` + + `只按 \`+=\` 搜会漏掉 \`= ranFilesCount + 1\` 这种等价写法)。`); } if (totalTests < totalFail) { probs.push(`checks=${totalTests} < fail=${totalFail} —— fail 是 checks 的子集,这不可能成立`); @@ -697,28 +698,64 @@ try { * 要让它触发得需要 >25 个"绿且不自报"的文件,而套件只有 25 个文件。 * ⇒ **数学上成立、实际上永远抓不到任何东西的判据** —— * 这正是我们这一路在消的形状,而我是**在加它的时候**又造了一个。 - * (顺带更正上面那条 `ran > files`:它同样只在"几乎所有文件都双计"时才响, - * 也就是**那个 bug 正好发生时它才响** —— 侥幸有效,但机理不是我以为的那个。) * - * 所以改成查**源码里的增量处数**:四个计数器各只许出现一次。 + * ★ **但真原因比"同一次加两次"更一般,而且我上面对 `ran > files` 的"更正"是错的** + * (pi 2026-09-15 把我的更正驳回来了,算式我实测复现过)。设 + * N=文件数、P=自报成功的、U=N−P=没自报的、Gp=绿且自报的、Gu=绿且没自报的,有 bug 时: + * + * ranFilesCount = P + Gp unreportedFiles = U + Gu + * ran > N ⟺ Gp > U + * unreported > N ⟺ Gu > P ← 两者是**镜像** + * + * 正常情况下 U ≈ 0(每个文件都自报)⇒ **`ran > files` 只要 Gp ≥ 1 就响 —— + * 任何一个被双计的绿文件就够**。我先前写的"只在几乎所有文件都双计时才响"**不成立**。 + * 实测:只让**一个**绿文件多计一次 ⇒ `ran=26 > files=25` **当场响**。 + * 而同一组数下 `unreported = U + Gu` 几乎恒为 0 —— 那才是它**永不触发**的原因。 + * + * ⇒ **可复用的规则**:`counter ≤ files` 这类结构性上界的**区分力 = 计数器典型值与上界之间的 + * 余量(slack)**。 + * · `ran` 典型值 ≈ files(余量 0)⇒ **极尖**,+1 就响 ⇒ **这条可以信**; + * · `unreported` 典型值 0(余量 25)⇒ 要 `Gu > P` 才响 ⇒ 永不触发。 + * **"结构性上界"本身不分好坏,要看余量。** 我是先拿"结构性的就是好的"当结论, + * 于是把一条很尖的判据误判成侥幸 —— **同一把尺子量两个余量截然不同的计数器**。 + * + * 所以源码处数扫描仍然要(它管的是另一件事:**拼法**),`ran > files` 也留着。 * 诚实说清性质:这是**形态判据**(读自己的源码),不是行为判据 —— * 它能抓"搬动/新增时留了旧路径"(实际发生的那三次), * 抓不到"在某条路径上被条件性地加两次"。 * 后者要真跑,得把累加抽成一个纯函数(入参 = 解析结果,出参 = 增量),再拿合成输入喂它。 * **那才是能真跑的形状,本轮不做,但记在这里**。 * - * ⚠️ 计数必须**锚在语句位置**(行首缩进后即 `X +=`),否则**我这条报错文案自己**里 - * 出现的 `X +=` 会被数进去 —— 第一次跑就是 `totalTests += 出现 2 处`, - * 而真身只有一处(**扫描器咬到自己的文档**,与 `@ohos`/`toISOString` 那两次同族)。 + * ⚠️ 计数必须**锚在语句位置**,否则**我这条报错文案自己**里出现的写法会被数进去 —— + * 第一次跑就是 `totalTests += 出现 2 处`,而真身只有一处 + * (**扫描器咬到自己的文档**,与 `@ohos`/`toISOString` 那两次同族)。 * `stripComments` 只去注释、**不去字符串**,所以这里不能只靠它。 + * + * ⚠️ 而且**只认 `+=` 是个假绿方向的洞**(pi 2026-09-15 抓到,我实测复现): + * `totalTests = totalTests + ran;` 这样的等价写法**躲得过扫描而仍然双计**。 + * ⇒ 所以判的是**对这四个计数器的"写"**,不是 `+=` 的出现: + * `+=` / `= X + …` / `= X - …` / `++` / `--` 都算。 + * + * ⚠️⚠️ 锚**不能是行首** —— 我第一次写成 `^\s*X`,然后实测发现**注入的第二处照样漏**: + * 语句可以从**行中间**开始,`if (ran === null) { ranFilesCount += 0; }` 就是一行两条语句, + * 而"第二处"长得**正是这个样子**。⇒ 锚必须是**语句起点**:`(?:^|[;{])`。 + * (**这是我第二次栽在同一件事上**:扫描器的锚要按**语言结构**定,不是按**版面**定 —— + * 上一次是"扫描器咬到自己的文档",这次是"锚钉在行首而语句不在行首"。) + * 实测四种等价写法(`+=` / `= X + n` / `X++` / 行中间)**全部命中**后才收。 + * 仍不是根治(`x ||= y`、`x = (x) + 1` 括号式、反射赋值都能躲)—— 根治就是上面那个纯函数。 */ 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; + const c = counter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + // 语句起点 = 行首、或上一条语句的 `;` / 块开始的 `{`(**不是**行首 —— 见上) + const WRITE = new RegExp( + `(?:^|[;{])\\s*${c}\\s*(?:\\+=[^=]|=[^=]\\s*${c}\\s*[-+]|[+-]{2})`, 'gm'); + const n = (ownSrc.match(WRITE) || []).length; if (n !== 1) { - probs.push(`\`${counter} +=\`(语句位置)出现 **${n}** 处(只许 1 处)—— ` + + probs.push(`对 \`${counter}\` 的**写**(语句位置)出现 **${n}** 处(只许 1 处)—— ` + `"每个文件只该计一次"是对**整族**计数器都成立的性质,不是一个一个判的;\n` + - ` 这个形状已经发生过三次(搬动/新增时留了旧路径),而**两处都跑得通、都报得出数**。`); + ` 这个形状已经发生过三次(搬动/新增时留了旧路径),而**两处都跑得通、都报得出数**。\n` + + ` 注意判的是**写**:\`X += n\`、\`X = X + n\`、\`X++\` 都算(只认 \`+=\` 会漏)。`); } } if (probs.length) { @@ -726,8 +763,20 @@ try { for (const x of probs) console.error(` - ${x}`); selfCheckFailed = true; } else { + /* + * ★ 报文只许写**真的判过**的东西(pi 2026-09-15 抓到我把没判的写上去过): + * 原来这句是 `ran ≤ files,unreported ≤ files,fail 是 checks 的子集` —— + * 而**全文件没有任何地方断言 `unreported ≤ files`**(`grep "unreportedFiles >"` 零命中)。 + * 我测出那条上界是空的、换成了源码处数扫描,**却把半句留在成功报文里** ⇒ + * 报文在**替一条已经被我判定无效的判断作证**。 + * 形状:**"读数器替一件事作证"落在自检自己身上** —— 而这条自检存在的理由, + * 正是"`RESULT` 那行自作证"。**读数器也要被读数。** + * + * `unreported` 现在由下面那句源码处数判(判的是"写有几处",不是值上界)—— + * 所以报文里只提它由谁判,不再写一个没人算的不等式。 + */ console.log(`ok 5 - 总数行自洽(ran=${ranFilesCount} ≤ files=${SUITE.length},` + - `unreported=${unreportedFiles} ≤ files,fail 是 checks 的子集)`); + `fail 是 checks 的子集;四个计数器各只有一处"写",由源码扫描判)`); } } catch (e) { console.error(`✗ 套件自检 5 跑不了:${e.message || e}`);