From 8a3d66a7c684061098286973273dd5d3d3014fc8 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 19 Sep 2026 11:53:04 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20pi=20=E6=8A=A5=E7=9A=84?= =?UTF-8?q?=E4=B8=A4=E6=9D=A1=20`STATIC=5FONLY`=20=E5=8F=91=E7=8E=B0=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20=E7=AC=AC=202=20=E5=88=97**=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E8=AF=BB=E8=80=85**=20+=20`static=3D`=20=E4=B8=8E"?= =?UTF-8?q?=E5=88=B0=E6=9C=9F"=E6=98=AF**=E4=B8=A4=E4=B8=AA=E9=87=8F**?= =?UTF-8?q?=EF=BC=88=E9=97=B8=E5=8F=AF=E8=A2=AB=E9=9D=99=E9=BB=98=E5=85=B3?= =?UTF-8?q?=E9=97=AD=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pi 单独发来它答应我的那两条(在**当前 HEAD** 上重测),我逐条复现、修掉, 并在过程中**自己连错两次**(都被变异抓出来,已写进 `CRITERIA.md §16.3`)。 ## 一、复现(我跑的,同刻 A/B) **发现 1**:第 2 列「当初只能静态的原因」**没有任何判据在读** —— 两个解构循环都用 `,` 把它丢掉(`:1295`/`:1306`),唯一读者是**到期点名时打印** (= 它最不需要被检验的时刻)。把它改成假话 ⇒ `red` 与红清单**零变化**。 **发现 2**:把 6 条探针全改指恒 `false`(`run: () => false`)⇒ **到期点名 1 → 0**,而 **`static=6` 一字不变**。 根因:`:2987` 的 `static=${STATIC_ONLY.length}`(余额)与 `:1605` 的 `if (dueStatic.length > 0)`(到期)是**两个量**,只有后者决定"到期"发不发生。 ★ 这条口子**是闸自己邀请的** —— 它的选 (b) 写着"并改换一个更准的到期前提", 换探针是**被鼓励的动作**,而"新探针是否真的适用于这条判据"**没有任何判据在问**。 ## 二、修法 1. `static=` 改成 **`static=6(其中已到期 6 条)`** ⇒ 关闸读得出来(Y→0 而 X 仍 6)。 2. **每次运行都全表播报** `STATIC_ONLY`(文件 + 原因 + 是否到期**并排**)⇒ 第 2 列有读者。 3. **自检 4b** 钉住"那一跳真的存在"。 ## 三、我自己的两次错修(都被变异抓出) | 修法 | 变异 | 结果 | |---|---|---| | ① 理由必须点到探针的某个标识符 | 拿**现有 6 条真理由**跑 | **红了 5 条** ⇒ 假红是噪音,已撤 | | ② 断言 `staticDetail` **变量**内容完整 | 只删那句 `console.log` | **4b 仍报 ok**,真实输出播报段 **0 次** | | ③ 断言源码含 `${staticDetail}` 形状 | 同上 | **仍 ok** —— 锚点写在这段自检**自己的注释**里,`includes` 命中注释 | | ④ 剥离注释后再数 `+ staticDetail);` | 删 print | **4b 红 ✓** | ## 四、验证 · `AGENTMAIL_PROBE_DEVICE=ok` ⇒ `static=6(其中已到期 6 条)`、⏰ 标记 6 个、4b ok; `=none` ⇒ `已到期 0 条`。 · 变异 2(改指恒 false):基线 `已到期 6 条`/点名 1 → 变异 `已到期 0 条`/点名 0 ⇒ **关闸可见**。 · 变异 4(删 print):4b **红**,红句"可执行代码里没有任何一处把 `staticDetail` 拼进输出"。 · 全套隔离跑:`^RED=0`、自检 4b 打印 1 次、播报段 1 次。 · 全仓 grep:**没有**任何下游解析 `static=` 的旧形状 ⇒ 格式改动安全。 ## 五、文档 `CRITERIA.md` 新增 **§16.3**,含那张"我连错四次"的对照表与三条可复用教训: ① 让字段可证伪 ≠ 给它加一条会红的规则(先拿现有数据试); ② 变量对 ≠ 打出去了; ③ 锚点自匹配要靠**剥注释**治。以及通用规则: **一个没人读的字段,先问它该被谁读 —— 给它读者;不该被读就删掉。** ★ 文件:`client/electron/test/run-all.mjs`、`CRITERIA.md`。 --- client/electron/test/CRITERIA.md | 47 ++++++++++ client/electron/test/run-all.mjs | 148 ++++++++++++++++++++++++++++++- 2 files changed, 193 insertions(+), 2 deletions(-) diff --git a/client/electron/test/CRITERIA.md b/client/electron/test/CRITERIA.md index 4cc7c42..e12343a 100644 --- a/client/electron/test/CRITERIA.md +++ b/client/electron/test/CRITERIA.md @@ -889,6 +889,53 @@ rc (原返回处):blind/unreadable/baseline-unrunnable|unknown 排第一 > 只测**单维**(每次只动一个条件)永远证明不了这件事 —— > **对角线上的绿,对组合态没有发言权。** +### 16.3 ★★ **"没人读的字段"与"给它评分"之间,还有第三条路:给它一个读者**(pi + dsh 2026-09-18) + +`STATIC_ONLY` 的**第 2 列**「当初只能静态的原因」**没有任何判据在读** —— +两个解构循环都用 `,` 把它丢掉(`:1295`/`:1306`),唯一会读它的场合是**到期点名时打印**, +而那正是它**最不需要被检验**的时刻。 +**实测**(dsh 同刻 A/B):把这条理由改成一句假话 ⇒ `red` 与红清单**零变化**。 + +**动到期闸的第二条**(pi 同封报的,我复现):`static=` 与"到期"是**两个量** —— +`static=${STATIC_ONLY.length}`(余额,永远 6)与 `dueStatic`(会变 0)分开, +而**只有后者**决定"到期"这件事发不发生。 +**实测**(把 6 条探针全改指恒 `false`):**到期点名 1 → 0**,而 `static=6` **一字不变**。 +★ 这条口子**是闸自己邀请的**:它的报文选 (b) 写着"并改换一个更准的到期前提" +⇒ 换探针是**被鼓励的动作**,而"新探针是否真的适用于这条判据"**没有任何判据在问**。 + +#### ★★★ 我在这一节里连错两次,两次都是被**变异**抓出来的 + +| 我的修法 | 变异 | 结果 | +|---|---|---| +| ① 给它加规则:理由必须点到探针的某个标识符 | 拿**现有 6 条真理由**跑 | **红了 5 条** ⇒ **假红是噪音**,比不设判据更糟 | +| ② 让自检断言 `staticDetail` **变量**内容完整 | 只删掉那句 `console.log` | **4b 照样报 ok**,而真实输出里播报段 **0 次** | +| ③ 断言源码里有 `${staticDetail}` 这个形状 | 同上 | **仍报 ok** —— 因为**锚点写在这段自检自己的注释里**,`includes` 命中的是注释 | +| ④ 剥注释后再数 `+ staticDetail);` 的命中 | 删 print | **4b 红 ✓**("可执行代码里没有任何一处把 `staticDetail` 拼进输出") | + +★ 三条教训(都可复用): + +1. **"让字段可证伪" ≠ "给它加一条会红的规则"** —— + 先拿**现有数据**试一遍;**红了就说明规则错了,不是数据错了**。 +2. **变量对 ≠ 打出去了** —— 自检守的若是"它读的那个值", + 那么"那个值怎么来的"仍然没人守(§16.1 的同一条缝,换了个位置)。 +3. **锚点自匹配**(自检 4 早就踩过、我这次又踩): + 把要找的形状**写进注释**,`includes` 就会命中注释 ⇒ + **判据空转报 ok**。修法:**剥掉注释再扫** + 在运行时拼锚点。 + +#### 落地 + +- `static=` 那一格改成 **`static=6(其中已到期 6 条)`** ⇒ "关闸"这件事**读得出来** + (Y 变 0 而 X 仍是 6 ⇒ 一眼看得出)。 +- **每次运行都全表播报** `STATIC_ONLY`(文件 + 原因 + 是否到期**并排**)⇒ 第 2 列有了读者。 +- **自检 4b** 钉住"那一跳真的存在"(剥注释后数锚点 + 内容完整 + 反空转长度)。 + +★ **通用规则**(本节定稿): + +> **一个没人读的字段,不许靠"给它加判据"来救 —— 先问它该被谁读。** +> 若它本就该被人看见,**给它一个读者**(并钉住"这一跳存在"); +> 若它不该被任何人读,**删掉它** —— **余额里读不出来的东西等于不存在**。 +> 只有在"这个字段本来就有真假"时,才轮得到加判据 —— 而且**必须先拿现有数据验一遍**。 + ## 17. 变异只证明「注入的样本被抓」,**不证明完备性**;判据的**标签必须等于断言范围** **规则**(pi 2026-09-14):一条判据通过变异验证之后,只能说"我注进去的那一条会被抓"。 diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index c3062c2..45f5c50 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -1292,7 +1292,7 @@ const STATIC_ONLY = [ ['test/harmony-imageprep.test.mjs', '图片上传链:要 `@ohos.multimedia.image` + 相册 + 服务端,三样本机都没有', 'device'] ]; -for (const [file, , probe] of STATIC_ONLY) { +for (const [file, why, probe] of STATIC_ONLY) { if (!SUITE.some(([f]) => f === file)) { console.error(`✗ 静态判据登记里的 ${file} 不在套件清单里(登记要跟着套件走)`); process.exit(1); @@ -1301,6 +1301,23 @@ for (const [file, , probe] of STATIC_ONLY) { console.error(`✗ ${file} 的到期前提 \`${probe}\` 不是已知探针(UNBLOCK 必须是可机检的前提,不是一句陈述)`); process.exit(1); } + /* + * ★★ pi 2026-09-18 本封的【发现 1】:第 2 列「当初只能静态的原因」**没有任何判据在读**。 + * 我同刻 A/B 复现了它:把这一列改成一句假话,`red` 与红清单**没有一处**因它而变; + * 它唯一被读到的场合是**到期闸点名时打印** —— 而那正是它最不需要被检验的时刻。 + * + * ⚠️ 我第一版修法**是错的,已撤**:我要求这一列必须点到探针 `desc`/`need` 里的某个 + * ASCII 标识符(如 `hdc`/`hvigorw`)。实测**6 条里红了 5 条** —— + * 而它们的理由都是**真话**("观感与运行期换肤要设备"这种中文描述本来就不含标识符)。 + * ⇒ 那是一条**在正确数据上误报**的判据:**假红是噪音**,比不设判据更糟。 + * ★ 教训:**"让字段可证伪"不等于"给它加一条会红的规则"** —— + * 先拿**现有数据**试一遍,红了就说明规则错了,不是数据错了。 + * + * ⇒ 正确的修法(见下方 `STATIC_ONLY` 逐条播报 + 钉住它的自检 4b): + * **给它一个读者** —— 每次运行都把它播报出来,并加一条自检**断言它真的被播报**。 + * 这样"没人读"这件事消失,而**不需要**给"理由是否为真"造一条会误报的判据。 + * (这与本仓那条老纪律一致:结论要**到达**读者,而不是"更严格地评判它"。) + */ } const probeResults = {}; for (const [, , probe] of STATIC_ONLY) { @@ -1531,6 +1548,115 @@ try { */ let dueFailed = false; const dueStatic = STATIC_ONLY.filter(([, , probe]) => probeIsDue(probeResults[probe])); + +/* + * ★★ pi 2026-09-18 报的【发现 1】的正确修法:**给第 2 列一个读者**。 + * + * 「当初只能静态的原因」原来**没有任何判据在读** —— 它只在**到期点名**时被打印, + * 也就是**只有在它已经不重要时**才被读到。我同刻 A/B 复现:把这条理由改成一句假话, + * `red` 与红清单**零变化**。 + * + * ⚠️ 我第一版修法是"给它加一条会红的规则"(理由必须点到探针的某个标识符)—— + * **实测在 6 条真理由上红了 5 条 ⇒ 已撤**。**假红是噪音**: + * 在正确数据上误报的判据,比不设判据更糟。 + * ⇒ 正确的修法不是"更严格地评判它",而是**让它到达读者**(本仓的老纪律): + * 每次运行都把它**全表播报**出来(含"是否到期"并排), + * 并由**自检 4b** 钉住"每条都真的被播报"。 + * + * ★ 定义放在这里(`dueStatic` 之后、自检之前)是**必须的**: + * 自检 4b 要读 `staticDetail`;若它定义在自检之后,会 `Cannot access before initialization` + * 当场抛错(我第一版就是这样,实跑撞到)。 + */ +const staticDetail = STATIC_ONLY.map(([file, why, probe]) => { + const due = dueStatic.some(([f]) => f === file); + return ` ${due ? '⏰到期' : ' —— '} ${file}\n` + + ` 当初只能静态的原因:${why}\n` + + ` 到期前提(${probe}):${PROBES[probe].desc}`; +}).join('\n'); + +/* + * ★★ 自检 4b(pi 2026-09-18 报的【发现 1】:第 2 列是**死重**): + * **"当初只能静态的原因"必须真的被播报出来。** + * + * 起因:这一列**没有任何判据在读** —— 它唯一被读到的场合是到期点名时打印, + * 而那正是它**最不需要被检验**的时刻。我把 6 条之一的理由改成一句假话, + * `red` 与红清单**零变化**(同刻 A/B 实测)。 + * + * ⚠️ 我第一版修法是"给它加一条会红的规则"(理由必须点到探针的某个标识符)—— + * **实测在 6 条真理由上红了 5 条 ⇒ 已撤**。**假红是噪音**: + * 一条在正确数据上误报的判据,比不设判据更糟。 + * ⇒ 正确的修法不是"更严格地评判它",而是**让它到达读者**(本仓的老纪律)。 + * + * ⚠️★ 我**第二版修法也是错的,也当场被变异抓出来**:那一版断言的是 + * **`staticDetail` 这个变量的内容**(每条文件名与理由都在里面)。 + * 实测变异:**只把那条 `console.log` 删掉**(变量仍完整)⇒ 4b **照样报 ok**, + * 而真实输出里播报段 **0 次** ⇒ 一个"理由根本没到达读者"的世界,被判成绿的。 + * ⇒ 这正是本仓反复消的那条:**自检守的是"它读的那个值",不是"那个值怎么来的"。** + * 变量对 ≠ 打出去了。**唯一能证明"到达"的,是读者真正收到的那份东西。** + * + * ⇒ 现在这条判据读的是**真实 stdout 里那一行**(本进程自己已经打过的话会被 + * `spawnSync` 的父进程输出捕获,但自检在打印**之前**跑 —— 所以这里改读 + * **源码里那句 `console.log` 是否真的带着 `staticDetail`**,并**再加一条**: + * "不能只有变量、没有打印"。两条合起来才等价于"到达"。 + */ +try { + const probs = []; + /* + * 判据形状(两段,缺一不可): + * ① **结构**:源码里必须存在一句把 `staticDetail` 打出去的 `console.log`。 + * 这是"值 → 读者"的那一跳;第 ②段单独存在时它正是漏掉的那一跳。 + * ② **内容**:那个变量必须**完整**(每条的文件名与理由都在、长度合理)。 + * ★ 变异对照(我实测): + * · 只删 `console.log` ⇒ ①红(第 ②段仍绿)—— 这就是第二版漏掉的那格; + * · 把 `staticDetail` 定义成空串 ⇒ ②红。 + */ + const selfSource = prose(fileURLToPath(import.meta.url)); + /* + * ① 「变量真的被打出去」——**锚点不许自匹配**(自检 4 的老教训,我这次又踩了): + * 第一版把锚点写成 `'${' + 'staticDetail}'`,而**这句拼法本身就出现在这段自检的注释里** + * ("这里断言:… `${staticDetail}` …")⇒ `includes` 命中的是**自检自己的注释**, + * 于是"删掉真正的 console.log"那个变异**照样报 ok**(实测 0 次播报 + 4b ok)。 + * ⇒ 修法:① 注释里**不写出那个字面形状**;② 判据改成**扫描代码区**(`stripComments`), + * 注释被剥掉后就不会自匹配;③ 再要求"出现次数 ≥ 1"。 + * ★ 这也顺带把判据的**范围**说清了:它验的是**可执行代码里**有那一跳,不是注释里提过。 + */ + const codeOnly = stripComments(selfSource); + // ★ 锚点取"真实的打印拼接",并**在运行时拼**以免在注释里自匹配; + // 剥掉注释后再数,命中 0 ⇒ 那一跳不在可执行代码里。 + const printAnchor = '+ static' + 'Detail);'; + const printHits = codeOnly.split(printAnchor).length - 1; + if (printHits === 0) { + probs.push('可执行代码里没有任何一处把 `staticDetail` 拼进输出 ⇒ ' + + '第 2 列**没有读者**(变量算出来了,但没打出去)'); + } + /* + * ② 「内容完整」——`staticDetail` 由 `STATIC_ONLY` 逐条 map ⇒ 每条都要在里面。 + */ + const missing = STATIC_ONLY.filter(([file, why]) => !staticDetail.includes(file) || !staticDetail.includes(why)); + if (missing.length) { + probs.push(`${missing.length} 条没进播报文本:${missing.map((m) => m[0]).join('、')}`); + } + /* + * 反空转:`staticDetail` 不许短于"所有理由之和"(空了也会让上面两条都命中)。 + */ + const whyTotal = STATIC_ONLY.reduce((n, [, why]) => n + (why || '').length, 0); + if (staticDetail.length <= whyTotal) { + probs.push(`播报文本长度 ${staticDetail.length} ≤ 所有理由长度之和 ${whyTotal} ⇒ 疑似空转/截断`); + } + if (probs.length) { + console.error('✗ 套件自检 4b(只能静态的原因必须被播报):'); + for (const x of probs) console.error(` - ${x}`); + selfCheckFailed = true; + } else { + console.log(`ok 4b - ${STATIC_ONLY.length} 条「只能静态」的原因**每条都被播报**` + + '(且源码里真有那句把它打出去的 `console.log`)'); + } +} catch (e) { + console.error(`✗ 套件自检 4b 跑不了:${e.message || e}`); + selfCheckFailed = true; +} + + const unknownProbes = Object.entries(probeResults).filter(([, v]) => v === 'unknown').map(([k]) => k); /* * ★★ 闸 (ii):「**移出 STATIC_ONLY**」必须要求「行为层**真跑过一次**」(pi 2026-09-18)。 @@ -2984,7 +3110,25 @@ try { + '变异体这一族**根本没被读数**,不是"没有问题"'); } -console.log(`RESULT phase=${PHASE} static=${STATIC_ONLY.length}${debtsLine} probe=${probeSummary}` + +/* + * ★★ pi 2026-09-18 本封报的**两条 `STATIC_ONLY` 发现**(我在当前 HEAD 上重测复现)。 + * + * 【发现 2】`static=` 与"到期"是**两个量** ⇒ 探针一改指,闸**静默关闭**: + * `static=${STATIC_ONLY.length}`(余额,永远 6)与 `dueStatic`(到期,会变 0)分开报, + * 而**只有后者**决定"到期"这件事发不发生。实测(同刻 A/B,把 6 条探针全改指恒 `false`): + * 到期点名 1 → **0**,而 `static=6` **一字不变**、数字那一格看着完全正常。 + * ⇒ 于是"**还欠着 6 条**"这句话仍然成立、而"**它们到期了**"这句话消失了。 + * ★ 这条口子**是闸自己邀请的**:它的报文选 (b) 写着"并改换一个更准的到期前提" + * ⇒ 换探针是被鼓励的动作,而"新探针是否真的适用于这条判据"**没有任何判据在问**。 + * ⇒ 修法:`static=` 那一格**同时**报两个数 —— "欠 X 条、其中 Y 条已到期"。 + * 这样"关闸"这件事**读得出来**(Y 从 6 变 0 时,X 仍是 6 ⇒ 一眼看得出)。 + */ +const staticDebtLine = `static=${STATIC_ONLY.length}(其中已到期 ${dueStatic.length} 条)`; +// ★ 第 2 列的**读者**(【发现 1】的修法):每次运行都全表播报。 +// 构造在 `dueStatic` 之后(自检 4b 要读它),这里只负责打印。 +console.log(`\n---------- 只能静态验形态的判据(每次运行都列全,第 2 列不再是死重)----------\n` + + staticDetail); +console.log(`RESULT phase=${PHASE} ${staticDebtLine}${debtsLine} probe=${probeSummary}` + (unknownProbes.length ? `(探针跑不了:${unknownProbes.join('、')} —— 已按到期处理)` : '(只能验形态的判据:到期前提成立就自动变红)') + mutantsLine);