补充: 给"条数登记校验"加锚点(自检 5b)—— ★ 而我第一版锚点自己写成了**空真**
`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∈∅` 的判据看起来和真判据一样绿;
② **修好一件事会同时消灭它自己的测试对象** ⇒ 锚点不许建立在"当前的错误状态"上,
要建立在**恒在的集合**上("谁自报了条数",而不是"谁条数不符")。
This commit is contained in:
@ -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,正是靠这一条才有分辨力。
|
||||
|
||||
@ -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(总数行的自洽性):');
|
||||
|
||||
Reference in New Issue
Block a user