diff --git a/client/electron/test/CRITERIA.md b/client/electron/test/CRITERIA.md index 1b57578..9ae4c38 100644 --- a/client/electron/test/CRITERIA.md +++ b/client/electron/test/CRITERIA.md @@ -143,6 +143,46 @@ const ALLOW = [ /* { name, replacement, why } */ ]; `process.exit(1)` 吞掉**,于是"变异后依然 exit 0"看起来像判据失效(本仓刚踩过这一次: 自检 3 其实是好的,是我用错了入口去验它)。**验判据要模拟用户/CI 真正跑的那一行。** +### 6.1 判据的**存在性**也要有下限:把"必须存在哪几条"锚到判据自己的表之外(pi 2026-09-18) + +§6 那条只说了"扫目录要有下限"。**同一条规矩换个对象就漏了** —— 这条是它的镜像: + +`run-all.mjs` 的自检登记表 `SELFTESTS` 原来**没有任何下界**,也没有任何"必须存在哪几个"的 +外部清单。于是**把一整条自检(函数体 + 登记 + CLI guard)三处同步删掉是零痕迹的**: + +``` +基线: RESULT … checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0 verdict=red 自检段:5 个自检… +删掉: RESULT … checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0 verdict=red 自检段:4 个自检… +``` + +`red` 与基线**逐字相同**、相关 RED **0 条**,只有报文里 `5` 变成 `4`(而那句读起来像正常)。 +最尖的形式:删掉 `exitcodeSelfTest` —— **守着 `UPSTREAM_RC`"真脚本真跑"锚点的那个** —— 同样零痕迹。 +⇒ 我们花两轮建起来的真跑锚点,可以被一次编辑无声撤掉。 + +**为什么三道防线都看不见它**(它们问的都是"**出现的东西对不对**"): + +| 防线 | 它比什么 | 整条删除时 | +|---|---|---| +| 名字级扫描 | 源码里的 `\w+SelfTest` ↔ `SELFTESTS` | 名字**两边都不出现** ⇒ 无从对比 | +| "写了但没接线" | `declaredSelfTests(源码)` ↔ `wired` | **两边同时缩小** ⇒ 相等 | +| 样本/见证/要求 | 样本表**内部**三字段 | 与 `SELFTESTS` 无关 ⇒ 不受影响 | + +⇒ **没有一条问"该出现的东西在不在"。** 所以规矩是: + +**凡"某张表里必须有哪几项",那几项必须能在**这张表之外**被读到 —— +写在别处的、有人维护的文本里(本文件就是这样),而不是由表自己说了算。** + +本仓的落点是:`run-all.mjs` 读**本文件 §6.1**下方那段 ``, +要求它与 `SELFTESTS` 的键**恰好相等**(两个方向都判)。 +★ 为什么锚在**本文件**而不是"再写一张内部清单":本文件是**另一个文件**, +且它自己已被 run-all 的**自检 3**守着(存在性 + 关键条目在)—— +⇒ 链条是"**被外部守着的文本 → flag 名 → guard → SELFTESTS**",锚点不在 `SELFTESTS` 自己身上。 + +★ 自查句:**"我这条判据,把它的定义、登记、调用三处一起删掉,谁会红?"** +问不出来,就说明这条判据的**存在性**没有锚点(它只被"内容正确"类判据守着)。 + + + ## 6.5 涉及**运行时行为**的结论,必须实测过才能写进规范/判据(pi 2026-09-14) 同一个错在本仓犯了两次,方向相反但错法相同:**从观察推断机制、没跑**。 diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 0b05e94..ecc13bd 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -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` 的键 + * **恰好相等**(两个方向都判)。`CRITERIA.md` 自己已被**自检 3** 守着 + * (存在性 + 关键条目在)⇒ 链条:**被外部守着的文本 → flag 名 → guard → SELFTESTS**。 + * ★ 删掉一条自检 ⇒ 本文件的名单没跟着删 ⇒ **红**(而"名单也一起同步删"是**跨文件**编辑, + * 不再是"改我自己的一张内部表")。 + */ + /* + * ⚠️ 契约行必须**独占一行**且**只能有一条** —— 我第一次写这条判据时就踩了: + * 上面那段注释里**举例提到了**这个标记(行内),而当时的正则没锚行首行尾, + * 于是它**先命中了注释里那次**(捕获到 `…`)⇒ 干净树上立刻出现 2 条假红。 + * ★ 这正是本文件反复写的那个形状的又一例:**"提到"与"就是"在文本上长得一样。** + * ⇒ 修法:锚 `^…$`(`m` 标志)+ **要求恰好一条**(多出来也红)。 + */ + const SELFTEST_CONTRACT_RE = /^\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));