修复: pi 报的两条 STATIC_ONLY 发现 —— 第 2 列**没有读者** + static= 与"到期"是**两个量**(闸可被静默关闭)
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`。
This commit is contained in:
@ -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):一条判据通过变异验证之后,只能说"我注进去的那一条会被抓"。
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user