From 3c53510b2c56cbc4c7967ff5dd95037a9d4b27b0 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 19 Sep 2026 10:07:50 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20**=E5=90=8C=E4=B8=80?= =?UTF-8?q?=E4=BC=98=E5=85=88=E7=BA=A7=E5=86=99=E4=BA=86=E4=B8=A4=E9=81=8D?= =?UTF-8?q?=20=E2=87=92=20=E4=B8=A4=E4=B8=AA=E9=A1=BA=E5=BA=8F**=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20`diag`=20=E4=B8=8E=20`rc`=20=E6=8E=92?= =?UTF-8?q?=E5=BA=8F=E7=9B=B8=E5=8F=8D=EF=BC=8C=E7=BB=84=E5=90=88=E6=80=81?= =?UTF-8?q?=E4=B8=8B=20`UPSTREAM=5FRC`=20=E4=B8=8D=E5=8F=98=E5=BC=8F?= =?UTF-8?q?=E4=B8=BA=E5=81=87=EF=BC=88pi=20=E5=AE=9E=E6=B5=8B=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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`。 --- client/electron/test/CRITERIA.md | 51 ++++++++++++++++++++ client/electron/test/mutants/summary.py | 62 ++++++++++++++++++------- client/electron/test/run-all.mjs | 37 +++++++++++++++ 3 files changed, 132 insertions(+), 18 deletions(-) diff --git a/client/electron/test/CRITERIA.md b/client/electron/test/CRITERIA.md index 7ae0d4d..4cc7c42 100644 --- a/client/electron/test/CRITERIA.md +++ b/client/electron/test/CRITERIA.md @@ -838,6 +838,57 @@ Go 在同一包内**按源文件顺序**跑测试 ⇒ 登记那条先跑就读 | "未列入清单" 在套件输出 | grep **0** | grep **1** | | `(summary.py)manifest-mismatch` 红 | **0** | **1** | +### 16.2 ★★ **同一优先级写了两遍 ⇒ 两个顺序**:不变式只在**对角线**上被验过(pi + dsh 2026-09-18) + +§16.1 讲"结论到不了决定颜色的那一格"。这一节是它的**镜像**: +结论**两个都到了**,但**互相矛盾** —— 报出来的"原因"与"严重度"说的不是同一件事。 + +**缝的形状**:`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`)**在其上为假**。 + +**实测**(dsh 复现,裁 `PATH`、root 可达):修前 `diag=manifest-mismatch`、`rc=2`、表值 `1` ⇒ 不一致。 +**修后**:`diag=baseline-unrunnable`、`rc=2` ✓ 一致。 + +★ **后果不是假绿**(`manifest-mismatch.blocksGreen=true` ⇒ 照样红、note 也转印), +而是**严重度被低估**:2 档的码被 1 档的码**盖住** ⇒ 读者以为"只要改清单", +而真相是"**连数都没读成**"。**rc 通道从此不可信。** + +★★ **为什么 12 个案例一个都没抓到**(这才是本节的要点): + +> 那 12 个案例**个个只动一维**(`unlisted` / `ghosts` / `blind` / `unreadable` / 各 `baseline-*`) +> ⇒ 不变式只在**对角线**上被验过,**组合(off-diagonal)无人可达**。 + +**修法(两件,缺一不可)**: + +1. **一条链推两个结果** —— 严重度**只有一个权威来源**:`summary.py` 里按同一顺序算出 + `(diag, rc_want)` **一对**,`RESULT` 行用它、`sys.exit` 也用它 ⇒ **排序不可能再漂移**。 + ★ 为什么**不是**"把两条链的顺序改成一致":那还是**两份**表, + 下次加一个条件两处又会各自漂移 —— 正是本仓反复消的"**一份事实两处实现**"。 +2. **显式走一遍 off-diagonal**(新组合案例:`unlisted` + 跑不了 `sha256sum` + ⇒ 期望 `diag=baseline-unrunnable` 且 `rc=2`,**2 档优先**)。 + ⚠️ 这条**不能**只靠"每跑必断不变式"代替:组合态下若 `diag` 又被低档码占住, + 那条断言就**永远验不到 2 这一档**(它只会验到手边那个码的期望值)。 + +**变异验证**(dsh):把单链的 2 档与 1 档**换序**(= 复现修前的两条链) +⇒ `--only-selftest=exitcode-selftest` **rc=1、^RED=2**,其中: +`组合:未列入清单 + 跑不了 sha256sum ⇒ … rc=1(期望 rc=2 且输出含 "diag=baseline-unrunnable"(真打出 diag=manifest-mismatch,UPSTREAM_RC=1 ✓))` +⇒ 排序本身被锁住;且它还**连带**抓出 `盲读` 那条(换序后盲读也走 `manifest-mismatch`)。 + +★ **通用规则**(与 §16.1 那条并列): + +> **凡"多条判定链各自挑一个代表"的地方,都要问: +> 它们挑的是不是同一个?** +> 只测**单维**(每次只动一个条件)永远证明不了这件事 —— +> **对角线上的绿,对组合态没有发言权。** + ## 17. 变异只证明「注入的样本被抓」,**不证明完备性**;判据的**标签必须等于断言范围** **规则**(pi 2026-09-14):一条判据通过变异验证之后,只能说"我注进去的那一条会被抓"。 diff --git a/client/electron/test/mutants/summary.py b/client/electron/test/mutants/summary.py index 3757caf..3b7715f 100644 --- a/client/electron/test/mutants/summary.py +++ b/client/electron/test/mutants/summary.py @@ -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__': diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 8f32798..c3062c2 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -1118,6 +1118,23 @@ function exitcodeSelfTest() { */ ['底本受跟踪却被删 ⇒ rc=1 且 diag=baseline-deleted', ['jobs-one.json'], ['jobs-one.json'], 1, 'diag=baseline-deleted', 'deleted'], + /* + * ★★ pi 2026-09-18 本封报的**组合(off-diagonal)**口子 —— 这条案例就是它的锚点。 + * + * 修前 `summary.py` 里有两份优先级表且**顺序相反**: + * `diag` 先看 `unlisted/ghosts`、`rc` 先看 `blind/unreadable/baseline-*` + * ⇒ 两类**同时**成立时(这里:`unlisted` + 跑不了 `sha256sum`), + * `diag=manifest-mismatch`(表里 **1**)而**真 rc=2** ⇒ + * 上面那条不变式**在其上为假**,而**12 个案例个个只动一维** ⇒ 只在**对角线**上验过。 + * + * ⇒ 修法是**一条链推 `(diag, rc)` 一对**(`summary.py` 里 `severity`), + * 并在这里**显式走一遍 off-diagonal**:期望 `diag=baseline-unrunnable` + `rc=2` + * (严重度**按 2 档取**,因为它更重 —— "连数都没读成"盖过"清单没登记")。 + * ⚠️ 这条**不能**只靠"每跑必断不变式"代替:组合态下若 `diag` 又被低档码占住, + * 那条断言就**永远验不到 2 这一档**(它只会验到手边那个码的期望值)。 + */ + ['组合:未列入清单 + 跑不了 sha256sum ⇒ diag=baseline-unrunnable(2 档优先)且 rc=2', + ['jobs-one.json'], ['jobs-one.json', 'jobs-extra.json'], 2, 'diag=baseline-unrunnable', 'no-sha256sum'], ]; let bad = 0; const seenDiags = []; // 反向覆盖用:本组案例**真跑出来**的码(不是声明的) @@ -1163,6 +1180,26 @@ function exitcodeSelfTest() { * ⇒ 改成从**脚本真打出来的 stdout** 里取码:这样"验的是它真说的",而不是"我以为它会说的", * 且对 `wantOut=null` 的案例同样有效。 */ + /* + * ★★ pi 2026-09-18 本封报的新口子:**组合(off-diagonal)没人可达**。 + * + * `summary.py` 里两条链的**排序不同**: + * `diag`(`:321-328`):`unlisted/ghosts` **排第一** + * `rc`(`:438-445`):`blind/unreadable/baseline-unrunnable|unknown` **排第一** + * ⇒ 两类**同时**成立时 `diag=manifest-mismatch`(表里 1)而**真 rc=2**, + * 于是上面这条不变式**在其上为假**。 + * 实测(`unlisted` + `baseline-unrunnable`,裁 PATH,root 可达): + * `diag=manifest-mismatch`、真 `rc=2`、`UPSTREAM_RC['manifest-mismatch']=1` ⇒ 不一致。 + * + * 根因不是"漏了一个案例",是**上面 12 个案例个个只动一维** ⇒ 不变式只在**对角线**上被验过。 + * ★ 所以修法**不能只是"再加一条固定案例"**(那只挪一格对角线)—— + * 要让它对**每一个真跑出来的码**都成立:把不变式**提出来对每次真跑都断言**。 + * ⇒ 这样以后 `summary.py` 新加任何组合,只要 `diag` 与 `rc` 不同步,这里立刻红。 + * + * ⚠️ 但**光靠"每跑必断"还不够**:真跑出来的 diag 若是"被盖住的那一个" + * (组合态下永远是 `manifest-mismatch`),这条断言就**永远验不到 2 那一档** + * ⇒ 必须**显式有一条组合案例**把 off-diagonal 走一遍(下面 `combos`)。 + */ let rcCheck = ''; const seenDiag = (got.out.match(/diag=([\w-]+)/) || [])[1]; if (seenDiag) seenDiags.push(seenDiag);