修复: **判断据的那个人也归判据管** —— T 全桩后与基线逐字相同、层 7 攻击完整放回(层 9)

pi 2026-09-18 报的层 9。**成立,我复现,读数与它逐字相同。**

## 一、缺陷:因果判据的**观测通道**住在它自己那张可编辑的表里

层 7 我为了把因果判据的成本从 4.3s 压到 52~696ms,加了快入口 `--only-selftest`,
而它当时自带**第二张分发表 `T`**(`{'probe-selftest': probeSelfTest, …}`,`:2300`)。
我在上一封把它如实标成"**副作用、不是防线**" —— **层 9 就长在那个副作用上。** 实测:

```
T 全换成 () => 1 桩        ⇒ red=9,与基线**逐字相同**、因果 RED **0 条**
T 全桩 + 假 ok×13(层 7)  ⇒ red=9、**红清单 diff 0 行**
  ⇒ 层 7 的攻击被完整放回,而代价只是"把 T 里 5 个值换成 () => 1"。
```

为什么因果判据看不见:它观察孩子**只经 `T` 的分发**,而它比的"孩子退出码"
正是 `T` 提供的 ⇒ **判据的观测通道被它自己要观测的对象控制着。**
⇒ 层 7 问"**被测者**的话可信吗",层 9 问"**测量者**的话可信吗"。
★ 也是本仓老形状的又一次:`T` 与 `SELFTESTS` 是**同一事实两份实现**。
(pi grep 证实:`T` ↔ `SELFTESTS` 的交叉约束 **0 处**。)

## 二、修法一:消除"两份实现"本身 —— `T` 不该单独存在

`SELFTESTS` **上移**成唯一的一份登记(原来它在块内、且在快入口**之后**),
快入口从它 `find` 派生 ⇒ **"桩化 `T`"这个动作没有落点**。
⚠️ 五个自检都是 `function` 声明(**提升**)⇒ 引用它们没有 TDZ 问题;
而**调用**它们仍必须排在 `DIAG` 等 `const` 表之后 ⇒ 那个 `if` **留在原位**
(这正是它位置的理由,见原地那段"const 不提升"的注释)。
★ 验证:`grep -c "const T = {"` ⇒ **0**;5 个 `--only-selftest` 全 rc=0;默认跑不变。

## 三、★★ 修法二(我加的,比 pi 的建议更硬):给因果判据加**对照**

pi 的 ② 是"若保留两份则加交叉约束(键相等 + **值必须是同一个函数对象**)"——
那是对的,但它**只堵 `T` 这一个形状**。我顺着想了一层:

> **只要判据只有"改坏 ⇒ 必须 rc=1"这一问,任何"恒 rc=1 的通道"都能让它绿。**
> 不管是表、是 wrapper、还是环境变量。

⇒ 加一问:**没改坏的时候,它必须 rc=0。** 实测把快入口改成恒 `exit 1`(模拟被桩化的通道)
⇒ **5 条 RED**(`观测通道本身有问题(观测到的"红"不能归因给改坏)`);
而"改坏生产符号"那一半仍照常红。

★ 这是"锚点必须落在被守对象管不着的地方"的**行为**版本:
**我不需要知道通道长什么样,只要求它在两种输入下给出两种答案。**
★★ 因此它比交叉约束**更强**:我把**重新引入一张 `T2` 并桩化**的版本也测了
⇒ 同样 **5 条对照红** —— **它堵的是"形状",不是"那一个洞"。**
⚠️ 代价:每对自检跑**两次**(默认跑 5.1s → **6.8s**,可接受)。

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

| 变异 | 结果 |
|---|---|
| **层 9(a)** `T` 全桩(修复前) | `red=9`、逐字相同、因果红 0 —— **复现** |
| **层 9(b) 最尖**:`T` 全桩 + 假 ok×13(修复前) | `red=9`、红清单 diff **0 行** —— **复现** |
| **层 9(b) 重放(修复后)**:假 ok×13 | **红**:`把 exitcode-selftest 守的生产符号改坏之后,它仍然全绿` |
| **通道恒 rc=1**(模拟桩化) | **红 5 条**:`观测通道本身有问题` |
| **重新引入 `T2` 并桩化** | **红 5 条**(同上 ⇒ 堵形状,不只堵那个洞) |
| 回归 **层8** `CAUSAL=[]` / **层6** 掏空 / **层5** 删整条 / 契约行删掉 | 各 1 条 |
| 回归 M43 / M45 / M47 / M48 | 各 1 条 |
| **`CAUSAL` 5 对逐条直验**(含新对照) | 改坏后**每条** rc=1 |
| 干净树假红检查 | `^RED` **0 条**、对照红 0 |

## 五、★ 一条我认为该长期留着的判据(pi 建议,我采纳并写进 `CRITERIA.md`)

> **每加一条防线,要同时回答两问:**
> ① 它守什么?(设计意图)
> ② **它自己新增了哪些"必须被守"的东西?**(副作用)
>
> 层 9 就是第②问没问的产物 —— `--only-selftest` 在**性能上是净收益**
> (4.3s → 52~696ms),却往被守集合里加了一个**没人守**的成员。

⇒ 与"每根轴的底看起来都像整体的底"是同一现象的两种说法:
**防线是会长出新的被守对象的。**

## 六、⚠️ 残留(不声称层 9 封死)

· **多处同步删仍可静默**(层 5~8 的老残留,本轮未动):删 `CAUSAL` 对 + `SELFTESTS` 登记
  + CLI guard + `--only-selftest` 分支 + 函数体 + 契约项 ⇒ 仍静默。
· **对照本身也可能被绕过**:如果有人把对照那一段源码删掉,它同样没人守 ——
  这与层 8 同形。我**没有**给对照再套一层(会无限递归),只把这条如实写在这里。
  ★ 现在的**净收益**是:绕过它需要改**两处**(删对照 + 桩化通道),而不是一处。

## 七、验证与状态

· 五个自检单独跑全 rc=0;默认跑 `checks=462 pass=458 fail=4 skip=0 red=9 broken=0 unreported=0 verdict=red`、
  `mutants=48 ran=47 skipped=1 on_new_criteria=35 diag=none baseline=7/7✓`;正常态 `^RED ` **0 条**。
· 本提交含 `client/electron/test/run-all.mjs`(`T` 消除 + `SELFTESTS` 上移 + 因果对照)与
  `client/electron/test/CRITERIA.md`(新增 §6.1.3)。
· 提交前 `HEAD=a023286`。
This commit is contained in:
2026-09-18 08:55:38 +08:00
parent a023286158
commit 5afb2c1550
2 changed files with 102 additions and 13 deletions

View File

@ -295,6 +295,53 @@ CAUSAL = [] + 假 ok×13 ⇒ red=10、因果/空壳 RED **0 条**
⇒ 判据没错,是我把两套名字当成了一套。**正确的来源是 `SELFTESTS` 的键**(旗标名)。
★ 教训:**同一个东西在两个地方有两套名字时,"名字相等"不是可传递的**。
### 6.1.3 **判断据的那个人,也可信吗**(pi 2026-09-18,层 9)
§6.1.1 我为把因果判据的成本从 4.3s 压到 52~696ms 加了快入口 `--only-selftest`,
而它当时自带**第二张分发表 `T`**(`{'probe-selftest': probeSelfTest, …}`)。
我在上一封把它如实标成"**副作用、不是防线**" —— **层 9 就长在那个副作用上**。实测:
```
T 全换成 () => 1 桩 ⇒ red=9,与基线**逐字相同**、因果 RED **0 条**
T 全桩 + 假 ok×13(层 7) ⇒ red=9、**红清单 diff 0 行**(层 7 的攻击完整放回)
```
为什么因果判据看不见:它观察孩子**只经 `T` 的分发**,而它比的"孩子退出码"
正是 `T` 提供的 ⇒ **判据的观测通道被它自己要观测的对象控制着。**
⇒ 层 7 问"**被测者**的话可信吗",层 9 问"**测量者**的话可信吗"。
★ 也是本仓老形状的又一次:`T` 与 `SELFTESTS` 是**同一事实两份实现**
(`stripStrings` 兄弟副本、`blurStyleFor`、`parseSkips` 抄副本…… 这次是**分发映射表**)。
**修法一(消除"两份实现"本身):`T` 不该单独存在。**
`SELFTESTS` 上移成**唯一的一份登记**,快入口从它派生 ⇒ "桩化 `T`"这个动作**没有落点**。
⚠️ 五个自检都是 `function` 声明(**提升**)⇒ 引用它们没有 TDZ 问题;
但**调用**它们仍必须排在 `DIAG` 等 `const` 表之后(那个 `if` 留在原位)。
**修法二(更硬,堵形状而不堵那一个洞):给因果判据加对照 —— 未改坏时必须 rc=0。**
只问"改坏 ⇒ 必须 rc=1"的话,**任何恒 rc=1 的通道**(表、wrapper、环境变量…)都能让它绿。
⇒ 加一问:**没改坏的时候它必须 rc=0**。通道坏了 ⇒ 这一问就红。实测:
把快入口改成恒 `exit 1` ⇒ **5 条 RED**(`观测通道本身有问题`);
而改坏生产符号那一半仍照常红。
★ 这是"锚点必须落在被守对象管不着的地方"的**行为**版本:
**我不需要知道通道长什么样,只要求它在两种输入下给出两种答案。**
⚠️ 代价:每对自检跑**两次**(默认跑 5.1s → **6.8s**,可接受)。
★★ **一般化(我认为这是这条线上最值得记的一句)**:
> **每加一条防线,要同时回答两问:**
> ① 它守什么?(设计意图)
> ② **它自己新增了哪些"必须被守"的东西?**(副作用)
>
> 层 9 就是第②问没问的产物 —— `--only-selftest` 是**性能上的净收益**,
> 却往被守集合里加了一个**没人守**的成员。
⇒ 这与"每根轴的底看起来都像整体的底"是同一现象的两种说法:
**防线是会长出新的被守对象的**,所以你永远能看到一个新的"底"。
★ 层级表:写法(4) → 存在性(5,它还在吗) → 行为(6,它做了吗) → 自述(7,被测者可信吗)
→ 锚表(8,锚还在吗) → **观测(9,判断据的那个人可信吗)**。
★ 层级表(**每层都是一根轴**):写法(4) → 存在性(5,"它还在吗") → 行为(6,"它做了吗")
→ **自述(7,"它说的话可信吗")**。

View File

@ -2296,23 +2296,45 @@ if (process.argv.includes('--mutants-line-selftest')) process.exit(mutantsLineSe
* ⇒ 任何"调用自检"的代码都必须排在**所有** `const` 表与自检函数定义之后。
* (所以这里紧贴生产区段入口:`let mutantsLine` 之前。)
*/
/*
* ★★★ **唯一的一份登记**(pi 2026-09-18 报的层 9)。
*
* 层 7 我为了把因果判据的成本从 4.3s 压到 52~696ms,加了快入口 `--only-selftest`,
* 而它当时自带**第二张分发表 `T`**(`{'probe-selftest': probeSelfTest, …}`)。
* 我在上一封自己把那个入口标成"副作用、不是防线"—— **层 9 就长在那个副作用上**:
*
* · `T` 全换成 `() => 1` 桩 ⇒ `red=9`,与基线**逐字相同**、因果 RED **0 条**;
* · `T` 全桩 **+ 假 ok×13**(层 7 的攻击)⇒ `red=9`、**红清单 diff 0 行**。
* ⇒ **层 7 的攻击被完整放回,而代价只是"把 `T` 里 5 个值换成 `() => 1`"。**
*
* 为什么因果判据看不见:它观察孩子**只经 `T` 的分发**,而它比的"孩子退出码"
* 正是 `T` 提供的 ⇒ **判据的观测通道被它自己要观测的对象控制着。**
* ⇒ 层 7 是"**被测者**的话可信吗",层 9 是"**测量者**的话可信吗"。
*
* ⇒ 修法(pi ①,我采用):**让 `T` 不存在** —— 一处定义(本表)、一处派生。
* 那么"桩化 `T`"这个动作**没有落点**(改本表会被层 5/8 的判据抓住)。
* ⚠️ 本表因此**上移到 `--only-selftest` 之前**(原来是块内的 `const`,在它之后)。
* 五个自检都是 `function` 声明(**提升**),所以引用它们没有 TDZ 问题;
* 而**调用**它们仍必须排在 `DIAG` 等 `const` 表之后 —— 下面那个 `if` 就在原位,
* 这正是它位置的理由(见上面那段"const 不提升"的注释)。
* ★ 一般化(pi 建议写进 `CRITERIA.md`):
* **每加一条防线,要同时回答两问:① 它守什么?② 它新增了哪些"必须被守"的东西?**
* 层 9 就是第②问没问的产物。
*/
const SELFTESTS = [
['probe-selftest', probeSelfTest],
['exitcode-selftest', exitcodeSelfTest],
['skip-selftest', skipSelfTest],
['verdict-selftest', verdictSelfTest],
['mutants-line-selftest', mutantsLineSelfTest],
];
if (ONLY_SELFTEST) {
const T = { 'probe-selftest': probeSelfTest, 'exitcode-selftest': exitcodeSelfTest,
'skip-selftest': skipSelfTest, 'verdict-selftest': verdictSelfTest,
'mutants-line-selftest': mutantsLineSelfTest };
const fn = T[ONLY_SELFTEST];
if (!fn) { console.error(`✗ 未知自检 ${ONLY_SELFTEST}`); process.exit(2); }
process.exit(fn() > 0 ? 1 : 0);
const entry = SELFTESTS.find(([n]) => n === ONLY_SELFTEST);
if (!entry) { console.error(`✗ 未知自检 ${ONLY_SELFTEST}`); process.exit(2); }
process.exit(entry[1]() > 0 ? 1 : 0);
}
let selfCheckRuns = 0;
{
const SELFTESTS = [
['probe-selftest', probeSelfTest],
['exitcode-selftest', exitcodeSelfTest],
['skip-selftest', skipSelfTest],
['verdict-selftest', verdictSelfTest],
['mutants-line-selftest', mutantsLineSelfTest],
];
/*
* 自检的输出**不是**这个阶段的正常输出(各打几十行 `ok …`)⇒ 吞掉 stdout,
* 只在红时把它自己的 RED 行带进报告。
@ -2710,6 +2732,26 @@ let selfCheckRuns = 0;
reds.push(`(自检)因果锚文本不唯一:${name}`);
continue;
}
/*
* ★★★ **对照:未改坏时必须 rc=0**(层 9 的另一半,也是更硬的那一半)。
*
* pi 报的层 9 是"**观测通道与被测样本住在同一张可编辑的表里**"(我那张 `T`)。
* 我把 `T` 消掉了(唯一登记),但那只堵住了**那一个形状**:
* 只要判据**只有"改坏 ⇒ 必须 rc=1"这一问**,任何"恒 rc=1 的通道"
* (不管是表、是 wrapper、还是环境变量)都能让它绿。
* ⇒ 加一问:**没改坏的时候,它必须 rc=0。** 通道坏了 ⇒ 这一问就红。
* ★ 这就是"锚点必须落在被守对象管不着的地方"的**行为**版本:
* 我不需要知道通道长什么样,只要求它在**两种输入下给出两种答案**。
* ⚠️ 代价:每对一个自检跑**两次**(实测量级 52~696ms/次 ⇒ 可接受)。
*/
const base = spawnSync(process.execPath, [target, `--only-selftest=${name}`],
{ encoding: 'utf8', cwd: ROOT });
if (base.status !== 0) {
console.log(`RED 因果判据的**对照**不过:\`${name}\` 在**没改坏**时退出码是 ${base.status}`
+ '(应为 0)⇒ **观测通道本身有问题**(观测到的"红"不能归因给改坏)');
reds.push(`(自检)因果对照失败:\`${name}\` 未改坏时 rc=${base.status}(通道坏了)`);
continue;
}
writeFileSync(target, src.replace(from, to));
const got = spawnSync(process.execPath, [target, `--only-selftest=${name}`],
{ encoding: 'utf8', cwd: ROOT });