Files
MailUI4Agents/client
JianFeeeee 72f35ff9cd 修复: 条数**可以伪造** —— 打满 13 条空 ok 就绕过了层 6 的下界(层 7:锚点不能落在自述上)
pi 2026-09-18 报的层 7。**成立,我复现,读数与它逐字相同。**

## 一、缺陷:下界判据的输入是"自我报告"

层 6 我加的是"比条数下界",而条数是从**输出**里数的 ——
**数的是它"打了几条",不是它"验了几条"**。于是把 `exitcodeSelfTest` 的体换成
"循环打 13 条空 `ok`、`return 0`"(不打任何真断言):

```
基线:      checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0 verdict=red
假 ok×13:  checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0 verdict=red   ← 逐字相同
下界相关 RED: 0 条(13 ≥ 下界 12 ⇒ 相等,无从对比)
```

**最尖形式**:假 ok×13 **+** `UPSTREAM_RC['baseline-residue']=99`(= 它本该抓的那个)
⇒ 0 条 RED、0 次提及 UPSTREAM。
**对照**(不掏空、只改值)⇒ `red=9→10`,真报"与真跑出来的 rc=1 不符"(⇒ 掏空是唯一原因)。

★ 根因:**下界判据的输入是"自述"** —— 判据自己说它做了多少,而它可以撒谎。
**锚点不能落在"被守对象的自述"上,必须落在它管不着的东西上。**

## 二、三种"看起来更硬"的替代我都否掉了(理由:仍在自述这一侧)

· **黄金输出**(逐字节比对期望输出)= 又一份**手写数据**(本仓刚证过那族可被同步改);
· **扫源码找关键词**= "**提到 vs 就是**"(本仓反复证过它脆);
· **毒化环境变量**(该模式下每条自检必须报红)= 把"必须红"的断言写在**自检函数体内**
  ⇒ **假体不打它**。★ 我实测过:模拟毒化后假体 `red=9`、下界红 0 —— 与层 7 同一问题。

## 三、修法:**因果锚点** —— 改坏被守的生产符号,那条自检必须红

```
· 假体(空 `ok`)在符号被改坏时仍然打 ok ⇒ 它不红 ⇒ **红**(判据抓它);
· 真自检读了那个符号 ⇒ 符号坏 ⇒ 它报 RED ⇒ 绿。
⇒ 这是"自检真的读了那个符号"的**行为**证据,不是它的自述。
```

实现:
· 快入口 `--only-selftest=<名>`(**实测 52~696ms/条**,对比 `--X-selftest` 要跑整套 suite 的 4.3~4.9s);
· 隔离用 `mkdtempSync` + `cpSync(join(HERE), …)`(**只拷 `test/`,1.6MB / 6ms**);
· `CAUSAL` 表:`probe-selftest`→`probeIsDue`、`skip-selftest`→`parseSkips` 的正则、
  `verdict-selftest`→`verdictOf` 的返回式、`exitcode-selftest`→`UPSTREAM_RC['baseline-residue']`。

★ **关系表本身也被真跑锚住**(这是它与"下界数字"的关键区别):
表里每一对都必须实测"改坏了它真会红";把符号写错(写成它不读的)⇒
那条自检改坏后**不红** ⇒ **红**。⇒ **表自己也被因果判据守着。**

## 四、我实现时踩的两个坑(都记在注释里)

① **必须拷整个 `test/`**:run-all 的**自检 1/2** 要 `readdirSync(test)` 与 `SUITE` 对齐,
   缺文件会让它在跑自检**之前**就 `exit(1)` ⇒ **读到的是假红**。
   我第一版少拷东西时就这么读错过:`rc=1` 看着像"改坏生效",其实是清单自检停了整套。
   这与本仓"隔离要用**最小**夹具"不冲突 —— 最小**自洽**的夹具,不是最小字节数。
② **锚文本的唯一性必须在除去 `CAUSAL` 表本身**的源码里数:
   我第一版没排除 ⇒ `probe`/`verdict` 立刻报"锚文本不唯一",而真因是**表里那一行字面量**,
   不是生产里有两处。★ 又是那句:**"提到"与"就是"在文本上长得一样。**

## 五、变异验证(都已还原)

| 变异 | 结果 |
|---|---|
| **层 7** 假 ok×13(exitcodeSelfTest) | **红**:`把 \`exitcode-selftest\` 守的生产符号改坏之后,它**仍然全绿** ⇒ 它没在读那个符号(或它是个空壳)`,`red=9→11` |
| **层 7 最尖**:假 ok×13 + `UPSTREAM_RC`=99 | **红**(同上,修复前 0 条) |
| 回归 **层6** 掏空 | 1 条(`只打出 0 条断言`) |
| 回归 **层5** 删整条 `skipSelfTest` | 1 条(契约要求而 `SELFTESTS` 缺) |
| 回归 **M43 / M45 / M47 / M48 / 名字级未接线** | 各 1 条 |
| 干净树假红检查 | `^RED` **0 条**、因果红 **0 条** |
| `CAUSAL` 表 4 对逐条直验 | 改坏后每条 `--only-selftest` rc=1(未改坏 rc=0) |

## 六、⚠️ 残留(如实登记)

· **跨文件同步删 `CAUSAL` 表项 + 对应生产符号**仍可绕过(与层 5/6 同形:我把成本从
  一次编辑抬到两次,性质没变)⇒ **不声称层 7 封死**。
· 另外:`CRITERIA.md` 里那三名"下界"仍可被改小(层 6 的残留),
  但**现在它不再单独构成绕过** —— 改小下界后仍要过因果判据(假体不读符号 ⇒ 红)。

## 七、验证与状态

· 五个自检单独跑全 rc=0;默认跑 `checks=459 pass=454 fail=5 skip=0 red=10 broken=0 unreported=0 verdict=red`、
  `mutants=48 ran=47 skipped=1 on_new_criteria=35 diag=none baseline=7/7✓`;正常态 `^RED ` **0 条**。
· ⚠️ `fail=4→5`、`red=9→10` **不是**本提交造成的:新增的是 `test/harmony-nav.test.mjs` 的
  设备占用红(连续 **32** 轮被别的会话占着前台,`K=3` 上限后按设计报红)。
  **我用 stash 证实过**:在**不含本改动**的干净 HEAD 上同样是 `red=10` + 该文件 `# fail 1`。
· 本提交含 `client/electron/test/run-all.mjs`(快入口 + `CAUSAL` 因果判据)与
  `client/electron/test/CRITERIA.md`(新增 §6.1.1)。
· 提交前 `HEAD=3175ee7`。
2026-09-18 08:31:01 +08:00
..