修复: **同一优先级写了两遍 ⇒ 两个顺序** —— diag 与 rc 排序相反,组合态下 UPSTREAM_RC 不变式为假(pi 实测)
pi 顺着我这几轮新分的"1 还是 2"往下试,找到一条**两条判定链排序不一致**的口子 ——
它正好长在我刚分的那个岔路口上。**我复现了,是当前代码的真 bug。**
## 一、口子
`summary.py` 里同一个文件有**两份**优先级表,而且**顺序相反**:
```
diag(原选择处):unlisted/ghosts 排第一 ⇒ 组合态报 manifest-mismatch
rc (原返回处):blind/unreadable/baseline-unrunnable|unknown 排第一 ⇒ 组合态退 2
```
⇒ 两类**同时**成立时(`unlisted` + 单文件不可读 / + 跑不了 `sha256sum` / + git 答不了),
`diag=manifest-mismatch`(`UPSTREAM_RC` 表里 **1**)而**真 `rc=2`**
⇒ `run-all` 那条不变式 `UPSTREAM_RC[diag] === rc`(`:1173`)**在其上为假**。
**实测**(裁 `PATH`、root 可达):修前 `diag=manifest-mismatch`、`rc=2`、表值 `1` ⇒ 不一致。
★ 后果**不是假绿**(`manifest-mismatch.blocksGreen=true` ⇒ 照样红、note 也转印),
而是**严重度被低估**:2 档的码被 1 档的码**盖住** ⇒ 读者以为"只要改清单",
而真相是"**连数都没读成**"。**rc 通道从此不可信。**
## 二★★ 根因:12 个案例**个个只动一维** ⇒ 不变式只在**对角线**上验过
`exitcodeSelfTest` 里**确实**有那条不变式,但它只跑自己构造的案例,
而那些案例每次只动**一个**维度(`unlisted` / `ghosts` / `blind` / `unreadable` / 各 `baseline-*`)
⇒ **组合(off-diagonal)无人可达**。
## 三、修法(两件,缺一不可)
1. **一条链推两个结果**:`summary.py` 里按同一顺序算出 `(diag, rc_want)` **一对**,
`RESULT` 行用它、`sys.exit` 也用它 ⇒ 排序不可能再漂移。
★ 为什么**不是**"把两条链顺序改成一致":那还是**两份**表,下次加条件两处又会各自漂移
—— 正是本仓反复消的"**一份事实两处实现**"。
2. **显式走一遍 off-diagonal**(新组合案例):
`未列入清单 + 跑不了 sha256sum ⇒ diag=baseline-unrunnable(2 档优先)且 rc=2`。
⚠️ 这条**不能**只靠"每跑必断不变式"代替:组合态下若 `diag` 又被低档码占住,
那条断言就**永远验不到 2 这一档**。
## 四、验证
· **修后组合态**:`diag=baseline-unrunnable`、`rc=2` ⇒ 与 `UPSTREAM_RC` **一致** ✓。
· **新案例**:`ok 组合:未列入清单 + 跑不了 sha256sum ⇒ … rc=2(真打出 diag=baseline-unrunnable,UPSTREAM_RC=2 ✓)`;
`--only-selftest=exitcode-selftest` **rc=0、^RED=0、ok=15**。
· **变异**(把单链 2 档与 1 档换序 = 复现修前两条链)⇒ **rc=1、^RED=2**:
`组合:… rc=1(期望 rc=2 且输出含 "diag=baseline-unrunnable"(真打出 diag=manifest-mismatch,UPSTREAM_RC=1 ✓))`
⇒ 排序本身被锁住;且**连带**抓出 `盲读` 那条(换序后盲读也走 `manifest-mismatch`)。
· **全套隔离跑**(worktree,只带本笔三个文件):`^RED=0`、因果红 0、对照红 0。
(`fail=10`/`red=10` 是并发会话的跨端线与设备竞争,与本笔无关。)
## 五、文档
`CRITERIA.md` 新增 **§16.2**(§16.1 的镜像:那边是"结论到不了",这边是"两个都到了但互相矛盾"),
含那条通用规则:
> **凡"多条判定链各自挑一个代表"的地方,都要问:它们挑的是不是同一个?**
> 只测**单维**永远证明不了这件事 —— **对角线上的绿,对组合态没有发言权。**
★ 文件:`client/electron/test/mutants/summary.py`、`run-all.mjs`、`CRITERIA.md`。
This commit is contained in:
@ -312,20 +312,51 @@ def main():
|
||||
# ⇒ 按本仓反复立的那条口径分家:**"检查了、结论无害"**(`baseline-stale`)
|
||||
# 与 **"根本没检查"**(`baseline-absent`)**不该同格**;
|
||||
# 而"受跟踪却被删"更重一档 —— 它连"配置"都不是,是**缺文件**。
|
||||
# ★★ 第五类(pi 2026-09-18 本封报的**组合口子**):**两条链排序不同**。
|
||||
#
|
||||
# 修前同一个文件里有**两份**优先级表,而且顺序相反:
|
||||
# diag(原选择处):`unlisted/ghosts` **排第一** ⇒ 组合态报 `manifest-mismatch`
|
||||
# rc (原返回处):`blind/unreadable/baseline-unrunnable|unknown` **排第一** ⇒ 组合态退 2
|
||||
# ⇒ 两类**同时**成立时(`unlisted` + 单文件不可读 / + 跑不了 sha256sum / + git 答不了),
|
||||
# `diag` 说 `manifest-mismatch`(`UPSTREAM_RC` 里 **1**)而**真 rc=2** ⇒
|
||||
# `run-all` 那条不变式 `UPSTREAM_RC[diag] === rc`(`:1173`)**在其上为假**。
|
||||
# 实测(裁 PATH,root 可达):`diag=manifest-mismatch`、`rc=2`、表值 1 ⇒ 不一致。
|
||||
#
|
||||
# 后果**不是假绿**(`manifest-mismatch.blocksGreen=true` ⇒ 照样红、note 也转印),
|
||||
# 而是**严重度被低估**:`counts-unusable`(2) 与 `baseline-unrunnable/unknown`(2)
|
||||
# 被 `manifest-mismatch`(1) **盖住** ⇒ 读者以为"只要改清单",
|
||||
# 而真相是"**连数都没读成**"。rc 通道从此不可信。
|
||||
#
|
||||
# ⇒ 结构性修法(**一条链推两个结果**,而不是把两个顺序改成一致):
|
||||
# 严重度**只有一个权威来源** —— 下面这段按同一顺序算出 `(diag, rc)` **一对**,
|
||||
# `RESULT` 行用它、退出码也用它 ⇒ **排序不可能再漂移**。
|
||||
# ★ 为什么不是"把 diag 链的顺序也改一致":那还是**两份**优先级表,
|
||||
# 下次有人加一个条件,两处又会各自漂移(这正是本仓反复消的"一份事实两处实现")。
|
||||
#
|
||||
# 顺序按**严重度递减**:
|
||||
# 2 档(**没读数**,连是环境还是代码都判不了):blind / unreadable / baseline-unrunnable|unknown
|
||||
# 1 档(**数据/清单该修**):unlisted / ghosts / baseline-residue|deleted
|
||||
# 0 档:其余(`baseline-stale`/`baseline-absent` 仍带码,但**不必红**)
|
||||
if blind:
|
||||
severity = ('counts-unusable', 2)
|
||||
elif unreadable:
|
||||
severity = ('counts-unusable', 2)
|
||||
elif bl_diag in ('baseline-unrunnable', 'baseline-unknown'):
|
||||
severity = (bl_diag, 2)
|
||||
elif unlisted or ghosts:
|
||||
severity = ('manifest-mismatch', 1)
|
||||
elif bl_diag in ('baseline-residue', 'baseline-deleted'):
|
||||
severity = (bl_diag, 1)
|
||||
elif bl_diag != 'none':
|
||||
severity = (bl_diag, 0)
|
||||
else:
|
||||
severity = ('none', 0)
|
||||
diag, rc_want = severity
|
||||
if blind:
|
||||
print('RESULT mutants=NO-READING ran=NO-READING skipped=NO-READING '
|
||||
'on_new_criteria=NO-READING diag=counts-unusable'
|
||||
f'on_new_criteria=NO-READING diag={diag}'
|
||||
'(**没读数**:读不到 jobs/ 目录 ⇒ 上面的数字全部无效)')
|
||||
else:
|
||||
# 诊断码按"最该被看见的那一个"取(一个 RESULT 行只挂一个码,避免读者要看两处)
|
||||
if unlisted or ghosts:
|
||||
diag = 'manifest-mismatch'
|
||||
elif unreadable:
|
||||
diag = 'counts-unusable'
|
||||
elif bl_diag != 'none':
|
||||
diag = bl_diag
|
||||
else:
|
||||
diag = 'none'
|
||||
print(f'RESULT mutants={len(by)} ran={ran} skipped={skipped} '
|
||||
f'on_new_criteria={only_new} diag={diag}'
|
||||
f'(口径A=只挂新判据 {only_new} / 口径B=任一挂新判据 {any_new} / '
|
||||
@ -435,14 +466,9 @@ def main():
|
||||
# 但它**必须能被读者看见**,所以走 `diag=` 通道(`run-all` 会转印它的说明行)。
|
||||
# `baseline-unknown`(git 答不了 ⇒ 判不了残留还是过期)也归 **2**:
|
||||
# 与 `baseline-unrunnable` 同类 —— **没读数**,不是"有残留"。
|
||||
if blind or unreadable or bl_diag in ('baseline-unrunnable', 'baseline-unknown'):
|
||||
return 2
|
||||
# ★ `baseline-deleted` 归 **1**(pi 建议):`baseline.sha` 是**受版本控制**的,
|
||||
# "跟踪着却不在工作树里"就是 ` D` —— 该改的是**仓库/数据**(把它恢复),
|
||||
# 不是环境、也不是"没读数"。与 `manifest-mismatch` 同类(都是"清单/数据该修")。
|
||||
if unlisted or ghosts or bl_diag in ('baseline-residue', 'baseline-deleted'):
|
||||
return 1
|
||||
return 0
|
||||
# 严重度已在上面按**单链**算好(`(diag, rc_want)`),这里只把同一结果交回调用方;
|
||||
# `sys.exit(main())` 直接用它当退出码 ⇒ **不存在第二份优先级表**(这正是本笔修的口子)。
|
||||
return rc_want
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in New Issue
Block a user