From d3140c213c74e773d0364834ad6ffe8e410782f1 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 19 Sep 2026 12:56:19 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85:=20=E7=BB=99"=E6=9D=A1?= =?UTF-8?q?=E6=95=B0=E7=99=BB=E8=AE=B0=E6=A0=A1=E9=AA=8C"=E5=8A=A0?= =?UTF-8?q?=E9=94=9A=E7=82=B9=EF=BC=88=E8=87=AA=E6=A3=80=205b=EF=BC=89?= =?UTF-8?q?=E2=80=94=E2=80=94=20=E2=98=85=20=E8=80=8C=E6=88=91=E7=AC=AC?= =?UTF-8?q?=E4=B8=80=E7=89=88=E9=94=9A=E7=82=B9=E8=87=AA=E5=B7=B1=E5=86=99?= =?UTF-8?q?=E6=88=90=E4=BA=86**=E7=A9=BA=E7=9C=9F**?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `788d7cc` 把条数校验挪进 `else { … }`(红绿都跑)之后,**它没有自检**: 下一个人完全可以再挪回 `else if` 后面,那时**什么都不会红** (红文件又收不到条数回执),而缺口**只在文件恰好红时隐形** —— 正是它上次潜伏到 `c523c21` 的原因。⇒ 补自检 5b。 做法(锚点落在**实际发生的比较**上,不许落在源码文本上,§16.3): 在 `else { … }` 里每次比较都 `countCheckRan.add(file)`, 5b 从 `records`(谁真的自报了条数)**独立重算**应当被评估的集合,再要求它被覆盖。 **不读 `reds`、不看那条校验自己的输出** —— 否则就是"读数器自作证"。 ★★ 而**我第一版 5b 是空真的**(自捉,如实记): 我原来比的是"凡**条数不符**的文件都必须被记录过" —— **而这条修复本身就把 harmony-admin 的登记数对齐了 ⇒ 那个集合恒空 ⇒ 断言恒真。** 变异测试当场抓到:把 `countCheckRan.add` 挪回"只绿才走",**5b 一声不响** —— 那一刻我才发现它不是"通过",是"**没有对象**"。 改成比 **"所有自报了条数、且没崩的文件"**(红绿都含):红文件必在其中 ⇒ 非空, 且"红文件被漏记"必被抓。另加**反空转**:集合为空而并非全部 broken ⇒ 自检自己报失效。 变异验证:挪回"只绿才走" ⇒ 5b 报出 6 个红文件名 (cross-client-theme / build-stamp / align-refs / harmony-push / criteria-hygiene / harmony-admin); 基线不报 ✓。修后全套(树内):files=31 ran=31 checks=487 pass=476 fail=11 red=10 broken=0 unreported=0。 `CRITERIA.md` 补两条可复用教训: ① **"拿现有数据试一遍"要试到"数据非空"** —— `∀x∈∅` 的判据看起来和真判据一样绿; ② **修好一件事会同时消灭它自己的测试对象** ⇒ 锚点不许建立在"当前的错误状态"上, 要建立在**恒在的集合**上("谁自报了条数",而不是"谁条数不符")。 --- client/electron/test/CRITERIA.md | 25 ++++++++++++++++ client/electron/test/run-all.mjs | 49 ++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/client/electron/test/CRITERIA.md b/client/electron/test/CRITERIA.md index 219d3e9..529f26f 100644 --- a/client/electron/test/CRITERIA.md +++ b/client/electron/test/CRITERIA.md @@ -518,11 +518,36 @@ test/harmony-admin.test.mjs: 登记=22 实际=27 exit=1 ★ 红 ⇒ 校验 **变异**:把红的 `harmony-admin` 登记数改成 `99` ⇒ 报 `自报 27 条 < 登记的 99 条` ✓; 改成 `27`(对齐)⇒ **不报条数**、只剩"退出码 1" ✓。 +#### 给这条修法加锚点时,**我第一版自己写成了空真**(dsh 自捉,如实记) + +修完要加个锚点钉住"校验真被评估过",否则下一个人挪回去**什么都不会红**。 +我第一版写的是:**"凡『条数不符』的文件都必须被记录过"** —— + +**而这条修复本身就把 `harmony-admin` 的登记数对齐了 ⇒ `mismatched` 恒为空 ⇒ 断言恒真。** +变异测试当场抓到:把 `countCheckRan.add(file)` 挪回"只绿才走"的位置, +**5b 一声不响**(这时我才发现它不是"通过",是"没有对象")。 + +⇒ 改成比 **"所有自报了条数、且没崩的文件"**(`shouldBeRecorded`,红绿都含): +红文件一定在里面 ⇒ 非空,且"红文件被漏记"必被抓。 +另加一条**反空转**:`shouldBeRecorded` 为空而并非全部 broken ⇒ 自检自己报失效。 + +**变异验证**(挪回"只绿才走"):5b 报出**6 个**红文件名 +(`cross-client-theme`、`build-stamp`、`align-refs`、`harmony-push`、`criteria-hygiene`、`harmony-admin`), +基线**不报** ✓。 + +★ 两条可复用的: + +1. **"我拿现有数据试一遍"要试到"数据非空"** —— `∀x∈∅` 的判据**看起来和真判据一样绿**, + 而它连"有没有对象"都没问过(本仓的"空真"陷阱,这已是第 N 次,第一次发生在我自己的新判据上)。 +2. **修好一件事会同时消灭它自己的测试对象**:我把登记数对齐 ⇒ "条数不符"这个集合就空了。 + ⇒ 锚点**不许建立在"当前的错误状态"上**,要建立在**恒在的集合**("谁自报了条数")上。 + ★ **通用规则**: > **校验写在哪条分支上,决定它保护谁。** > 凡"出错时要额外检查 X"的守卫,先问:**这条分支真红的时候,它还跑得到吗?** > —— 把校验放在"一切正常"的那条路上,等于**只在没出事时守着**。 +> 而给它加的锚点,**不许以"当前的错误状态"为对象**(那会被同一次修复清空)。 计数必须写在 **`check()` 内部**:写在调用点或靠扫源码,"实现被换空"就看不见了 —— 上面那个反例的 `pass` 会是 0,正是靠这一条才有分辨力。 diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index ec7ed35..4ec6cfd 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -248,6 +248,22 @@ const reds = []; * 它只累加**它自己实际跑过并解析到自报条数**的那些文件,不猜、不扫目录。 */ const records = []; +/* + * ★★ 「条数登记」校验的**行为锚点**(dsh 2026-09-19)。 + * + * 为什么需要它:那条校验原来写在**最后一个 `else`**("退出码 0")里, + * 而 `r.status !== 0` 会先在 `else if` 里 `reds.push` 并**跳过它** ⇒ + * **文件越红,它的登记数越没人守**(实测:`harmony-admin` 登记 22/实际 27 且为红 ⇒ + * 校验 0 命中;同刻 4 个绿文件的条数不符全都正常报了)。 + * 缺口**只在文件恰好是红的时候隐形**,所以它潜伏到了 `f4b8bc1` + * (该提交自己写着 "+5" 却没同步登记数)—— 平时根本看不出来。 + * + * ⇒ 只把校验挪进 `else { … }` 还不够:下一个人挪回去**什么都不会红**。 + * 所以加这个锚点,落在**实际发生的比较**上(不许落在源码文本上,§16.3): + * 凡"自报了条数却与登记不符"的文件,**无论红绿**,都必须在这张表里留一条记录; + * 汇总处断言 `countCheckRan ⊇ 所有条数不符的文件`。 + */ +const countCheckRan = new Set(); /** * 各计数器**不在这里累加** —— 它们由下面的 `records` **投影**出来(见 `records` 的说明)。 * @@ -515,6 +531,7 @@ for (const [file, flags, expected] of SUITE) { '\n 用于变体验证时:broken **不算这次变异有效**,要重做。'); } else { if (r.status !== 0) reds.push(`${file}(退出码 ${r.status})`); + countCheckRan.add(file); // ★ 行为锚点:这条校验**被评估过**(红绿都记,见 `countCheckRan` 的说明) /* * ★★ 「条数登记」校验必须在**这里**(红绿都跑),**不能留在下面那个 `else` 里**。 * @@ -1505,6 +1522,38 @@ try { if (totalTests < totalFail) { probs.push(`checks=${totalTests} < fail=${totalFail} —— fail 是 checks 的子集,这不可能成立`); } + /* + * ★★ 自检 5b(dsh 2026-09-19):**条数登记校验必须在红绿两条路上都被评估**。 + * + * 独立重算:从 `records`(谁自报了条数)与 `brokens`(谁没读起来)**直接算出** + * "应当被那条校验评估过"的文件集合 —— **不读** `reds`、**不看**那条校验的输出 + * (那会变成"读数器自作证")。 + * + * ⚠️ **第一版这里是空的**(dsh 自捉,如实记):我原来只比"**条数不符**的文件", + * 而这条修复本身就把 `harmony-admin` 的登记数对齐了 ⇒ `mismatched` 恒为空 + * ⇒ **`∀x∈∅` 恒真,5b 永远不会红**(正是本仓那条"空真"陷阱)。 + * 变异测试当场抓到:把记录挪回"只绿才走",5b **不响**。 + * ⇒ 改成比**"所有自报了条数且没坏的文件"**(`shouldBeRecorded`,红绿都含): + * 红文件(`harmony-admin` 等)**一定在里面** ⇒ 非空、且"红文件被漏记"必被抓。 + */ + { + const brokenFiles = new Set(brokens.map(b => String(b).split('(')[0].trim())); + const shouldBeRecorded = records + .filter(r => r.checks !== null && !brokenFiles.has(r.file)) + .map(r => r.file); + /* 反空转:除非"整个套件都跑不起来",否则这个集合必须非空 —— 空集合会让下面的断言恒真 */ + if (shouldBeRecorded.length === 0 && brokens.length < SUITE.length) { + probs.push('自检 5b 自身失效:`shouldBeRecorded` 为空(却并非所有文件都 broken)' + + ' ⇒ 下面的覆盖断言会**恒真**(`∀x∈∅`)。'); + } + const unguarded = shouldBeRecorded.filter(f => !countCheckRan.has(f)); + if (unguarded.length) { + probs.push(`这些文件**自报了条数**、却没被"条数登记校验"评估过:` + + `${unguarded.join('、')} —— 校验被挪到某条走不到的路径上了` + + `(典型:挪回"退出码 0"那条分支 ⇒ **文件越红越没人守**,方向是假绿)。` + + `修法:校验必须在 \`else { … }\`(红绿都跑)里,并保留 \`countCheckRan.add(file)\`。`); + } + } if (probs.length) { console.error('✗ 套件自检 5(总数行的自洽性):');