修复: 一整条自检(函数体+登记+guard)三处同步删掉是**零痕迹** —— SELFTESTS 没有下界
pi 2026-09-18 报的层 5。**成立,我复现,读数与它逐字相同。**
## 一、缺陷:三道防线都在问"出现的东西对不对"
| 变异 | 结果 |
|---|---|
| **H1** 删整条 `skipSelfTest`(函数体 + 登记 + CLI guard 三处同步) | `red=9 … verdict=red` 与基线**逐字相同**、相关 RED **0 条**,只有报文里 `5 个自检`→`4 个自检` |
| **H1'** 最尖形式:删 `exitcodeSelfTest` —— **守着 `UPSTREAM_RC`"真脚本真跑"锚点的那个** | 同样零痕迹 ⇒ 两轮建起来的真跑锚点可被一次编辑无声撤掉 |
| **H2** 只删登记(函数与 guard 留着) | `red=11`(名字级扫描 + "写了没接线"两条防线抓住)⇒ **不静默** |
| **H3** 只删函数体 | `ReferenceError`、无 `RESULT` ⇒ **响的**,不是静默洞 |
★ 为什么三道防线都看不见 H1:
· **名字级扫描**:整条删掉时那个名字**两边都不出现** ⇒ 无从对比;
· **"写了没接线"**:`declaredSelfTests(源码)` 与 `wired` **两边同时缩小** ⇒ 相等;
· **样本/见证/要求**:样本表**内部**三字段,与 `SELFTESTS` 无关。
⇒ 根因一句话:**`SELFTESTS` 没有任何下界,也没有"必须存在哪几个"的外部清单;
所有防线问的都是"出现的东西对不对",没有一条问"该出现的东西在不在"。**
★ 这与本仓**自检 1/2** 是同一句话的不同对象:那两条判"清单里的文件必须真的存在"、
"`test/` 下每个 `*.test.mjs` 都要在清单里"—— **它们有外部依据(磁盘上的文件)**,
而 `SELFTESTS` 的"该有哪几个"**只由它自己说了算**。**同一句话,换个对象就漏了。**
## 二、修法:把"必须存在哪几条"锚到 `SELFTESTS` 之外
`CRITERIA.md` §6.1 写一段契约(本文件是**另一个文件**,且它自己已被本仓**自检 3** 守着:
存在性 + 关键条目在),并放一行 `<!-- selftests: … -->`;`run-all.mjs` 读它,
要求与 `SELFTESTS` 的键**恰好相等**(两个方向都判)。
⇒ 链条:**被外部守着的文本 → flag 名 → guard → SELFTESTS**,锚点不在表自己身上。
删掉一条自检 ⇒ 名单没跟着删 ⇒ **红**;"名单也一起删"变成**跨文件**编辑(见下 §四)。
## 三、★★ 我没能用 pi 建议的①,因为它的**前提是假的**
pi 建议"拿对外的 flag 契约当锚点",理由写的是"这五个 flag 名是**对外的**(有人在用、有文档)"。
**我实测:不是。** 全仓(排除 `.git`)搜这五个 flag ⇒ **外部消费者 0、文档 0**。
⇒ 所谓"对外契约"**不是已有的,是需要造出来的**;直接用会把锚点建在一个不存在的保证上。
★ 所以我把它落在 `CRITERIA.md`(**确实**是别的文件、**确实**有 4 个文件引用它、
且**确实**被自检 3 守着),而不是落在"它自己新写的一张内部清单"。
## 四、⚠️ 残留(如实登记):**跨文件的同步删仍然静默**
实测:**同时**删 `run-all.mjs` 的三处 **和** `CRITERIA.md` 的那一项 ⇒ `^RED ` **0 条**。
⇒ 本次修法把"改我自己的一张表"抬成了"**改两个文件**",
**难度上去了,但性质没变**(仍是可同步的、无真跑锚点的编辑)。
★ 按这条线的规律,我**不**声称这一层封死了 —— 只是把成本从一次编辑抬到两次编辑。
真要把存在性也锚到行为上,得让"删掉一条自检"在**真跑**里留下痕迹(例如对外契约由
`npm test` 的入口兑现),那是独立工作。
## 五、我写这条判据时踩的坑(同一形状的第 N 次)
契约行我第一版的正则没锚行首行尾,而 §6.1 的**正文里举例提到了**那个标记
⇒ 正则**先命中了注释里那次**(捕获到 `…`)⇒ 干净树上立刻 2 条**假红**。
★ 正是本文件反复写的形状:**"提到"与"就是"在文本上长得一样。**
⇒ 修法:锚 `^…$` + **要求恰好一条**(多出来也红)—— 现在契约行自己也有唯一性判据。
## 六、变异与回归(都已还原)
| 变异 | 结果 |
|---|---|
| **H1 / H1'** 三处同步删 | **红**:`契约里要求存在的自检在 SELFTESTS 里没有:skip-selftest / exitcode-selftest`,`red=9→10` |
| 契约里写一个不存在的自检(反方向) | **红**(`verdict-selftest`) |
| 契约行 0 条 / 2 条 | **红**(要求恰好 1 条) |
| 回归 **M43** 窄正则 | 1 条认不出 |
| 回归 **M45** 只删样本 | 1 条"没有样本"(★ 我第一次跑成 0 条,是**我的脚本**同时删了 `REQUIRED_FORMS` 的名字 ⇒ 那是共漂移不是 M45;改正后 1 条) |
| 回归 **M47** 标签撒谎 / **M48** 空见证 | 各 1 条 |
| 回归 名字级(未接线自检) | 1 条 |
## 七、验证与状态
· 五个自检单独跑仍全 rc=0;默认跑 `checks=459 pass=455 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/CRITERIA.md`(新增 §6.1 + 契约行)与 `client/electron/test/run-all.mjs`。
· `HEAD=dc261ad`(提交前)。
This commit is contained in:
@ -2492,6 +2492,55 @@ let selfCheckRuns = 0;
|
||||
* ★ 唯一需要登记的例外:`FORM_SAMPLES` 里样本内容用的假名(`fooSelfTest`)。
|
||||
* 它是一个**显式登记的**例外,不是"扫不到就算了"——这样它自己也看得见。
|
||||
*/
|
||||
/*
|
||||
* ★★ **存在性锚点**(pi 2026-09-18 报的层 5):`SELFTESTS` 原本**没有下界**。
|
||||
*
|
||||
* 实测(我复现,与 pi 读数逐字相同):把**一整条自检**(函数体 + `SELFTESTS` 登记 +
|
||||
* CLI guard)三处同步删掉 ⇒ `red=9 … verdict=red` 与基线**逐字相同**、相关 RED **0 条**,
|
||||
* 只有报文里 `5 个自检` 变 `4 个自检`。最尖的形式是删掉 `exitcodeSelfTest`
|
||||
* —— **守着 `UPSTREAM_RC`"真脚本真跑"锚点的那个** —— 同样零痕迹。
|
||||
*
|
||||
* ⇒ 根因:上面三道防线(名字级扫描、"写了没接线"、样本/见证/要求)
|
||||
* 问的都是"**出现的东西对不对**",**没有一条问"该出现的东西在不在"**。
|
||||
* 名字级扫描尤其说明问题:整条删掉时那个名字**两边都不出现** ⇒ 无从对比。
|
||||
*
|
||||
* ⇒ 锚点必须落在 `SELFTESTS` **之外**,且是**别的文件**(不是又一张内部清单):
|
||||
* 读 `CRITERIA.md` §6.1 下方那行 `<!-- selftests: … -->`,要求与 `SELFTESTS` 的键
|
||||
* **恰好相等**(两个方向都判)。`CRITERIA.md` 自己已被**自检 3** 守着
|
||||
* (存在性 + 关键条目在)⇒ 链条:**被外部守着的文本 → flag 名 → guard → SELFTESTS**。
|
||||
* ★ 删掉一条自检 ⇒ 本文件的名单没跟着删 ⇒ **红**(而"名单也一起同步删"是**跨文件**编辑,
|
||||
* 不再是"改我自己的一张内部表")。
|
||||
*/
|
||||
/*
|
||||
* ⚠️ 契约行必须**独占一行**且**只能有一条** —— 我第一次写这条判据时就踩了:
|
||||
* 上面那段注释里**举例提到了**这个标记(行内),而当时的正则没锚行首行尾,
|
||||
* 于是它**先命中了注释里那次**(捕获到 `…`)⇒ 干净树上立刻出现 2 条假红。
|
||||
* ★ 这正是本文件反复写的那个形状的又一例:**"提到"与"就是"在文本上长得一样。**
|
||||
* ⇒ 修法:锚 `^…$`(`m` 标志)+ **要求恰好一条**(多出来也红)。
|
||||
*/
|
||||
const SELFTEST_CONTRACT_RE = /^<!--\s*selftests:\s*([^>]*?)\s*-->\s*$/gm;
|
||||
const contractMs = [...criteriaDoc.matchAll(SELFTEST_CONTRACT_RE)];
|
||||
if (contractMs.length !== 1) {
|
||||
console.log(`RED \`CRITERIA.md\` 里的 \`selftests:\` 契约行有 ${contractMs.length} 条`
|
||||
+ '(要求**恰好 1 条**、独占一行)⇒ 自检的**存在性**没有唯一锚点');
|
||||
reds.push(`(自检)\`CRITERIA.md\` 的契约行应为 1 条,实为 ${contractMs.length} 条`);
|
||||
}
|
||||
if (contractMs.length === 1) {
|
||||
const contract = contractMs[0][1].split(/\s+/).filter(Boolean).sort();
|
||||
const keys = SELFTESTS.map(([n]) => n).sort();
|
||||
const missing = contract.filter(n => !keys.includes(n));
|
||||
const undeclared = keys.filter(n => !contract.includes(n));
|
||||
if (missing.length) {
|
||||
console.log(`RED 契约里要求存在的自检**在 \`SELFTESTS\` 里没有**:${missing.join('、')}`
|
||||
+ ' ⇒ 那条判据被删掉了(它的存在性有外部锚点,躲不掉)');
|
||||
reds.push(`(自检)契约要求而 \`SELFTESTS\` 缺:${missing.join('、')}`);
|
||||
}
|
||||
if (undeclared.length) {
|
||||
console.log(`RED \`SELFTESTS\` 里有契约没登记的自检:${undeclared.join('、')}`
|
||||
+ ' ⇒ 新增判据要同时写进 `CRITERIA.md` §6.1 的契约行');
|
||||
reds.push(`(自检)\`SELFTESTS\` 有而契约没登记:${undeclared.join('、')}`);
|
||||
}
|
||||
}
|
||||
const SAMPLE_FAKE_NAMES = ['fooSelfTest'];
|
||||
const allNames = [...new Set([...selfSource.matchAll(/\b(\w+SelfTest)\b/g)].map(m => m[1]))];
|
||||
const nameUnwired = allNames.filter(n => !wired.has(n) && !SAMPLE_FAKE_NAMES.includes(n));
|
||||
|
||||
Reference in New Issue
Block a user