修复: 三张手写表互相比对、没有外部锚点 —— 同步删「箭头函数」后 red 与基线**逐字相同**

pi 2026-09-18 报的。**成立,我复现,读数与它逐字相同**;而且我测出
**pi 建议的补法①也堵不住它**,所以换了锚点。

## 一、缺陷:同步修改 = 零痕迹

(a)把「箭头函数」从 `REQUIRED_FORMS` 与 `FORM_SAMPLES` **同步**删掉
⇒ `red=9`、`broken=0`、`verdict=red`,与基线**逐字相同**,样本表相关 RED **0 条**,
只有成功报文里 5 变成 4。

(b)更尖:连 `declaredSelfTests` 里 `const` 那条发现规则**一起**删(四字段全同步)
⇒ 同样 **0 条**。而后果是实的:那种写法的自检将来**既不跑也不报警**。

★ 根因(pi 的概括我认同):`REQUIRED_FORMS`/`FORM_SAMPLES`/见证**三张都是手写的、互相比对**
——三张同步改就一致地"对",**没有任何外部锚点**。
对照 `UPSTREAM_RC`:它当年止住共漂移,是因为它比对的是**真脚本真跑出来的 rc**,
不是另一张手写表。**"具名"这个形式救不了它** ——
`length >= 5` 与 `REQUIRED_FORMS` 在"被同步修改"这一点上**难度相同**(都是一次编辑)。

⚠️ 因此我 M45 注释里那句"具名清单让放弃支持变成**看得见的声明**"**被实测推翻**,
已在该处更正(保留原文与推翻它的读数,免得下一个人再信一次)。

## 二、为什么不用 pi 的补法①:它也堵不住

pi 建议"每条正则都必须有只满足它的样本"。我实测:**样本是规则的字段**,
删规则时它自己的样本一起消失 ⇒ 要求跟着消失 ⇒ 仍然 0 条红。
(我把它单独跑了一遍:`check(两条规则) = []`、`check(只剩 function) = []`,两行都是空。)

## 三、修法:**名字级锚点**(与写法无关,且不落到手写表上)

源码里出现的**任何** `\w+SelfTest` 标识符,都必须在 `SELFTESTS` 里 —— **不解析声明语法**。
于是它与 `function`/`const`/`async`/箭头/空格**全部无关**:
发现规则认不出某种写法时,那种写法的自检名**仍会被这个名字级判据看见**。

★ 唯一例外是样本内容用的假名(`fooSelfTest`),**显式登记**在 `SAMPLE_FAKE_NAMES`
(另加反向检查:登记的假名在源码里找不到 ⇒ 红,登记该清了)。

**这一跳的方向与 `UPSTREAM_RC` 一致**:锚点从"另一份手写数据"挪到**源码事实**
(`SELFTESTS` 里存的是真函数,`fn.name` 是运行时读数)。

## 四、变异验证(都已还原)

| 变异 | 结果 |
|---|---|
| **pi 的 (b)**:删 `const` 规则 + 四字段同步 + 未接线箭头自检 | **红**:`这些 *SelfTest 标识符出现在源码里却没接线(名字级扫描,与写法无关):silentArrowSelfTest`,`red=9→10`(**修复前 0 条 RED、一字不提**) |
| **写法无关性**:**两条规则全删** + 四种写法(箭头/async/名后空格/const=async)的未接线自检 | **红**:四条**全部**点名(`aSelfTest、bSelfTest、cSelfTest、dSelfTest`) |
| 回归 **M47** 标签撒谎 | 仍 **1 条红** |
| 干净树假红检查 | 名字级红 **0 条**、`red=9`(无假红) |

## 五、我踩的坑(第三次同一个)

加这个名字级块时又**用了声明在后面/不存在的变量**:
先 `ReferenceError: selfSrc is not defined`(我早前的编辑把 `selfSrc` 挪走了),
再是 `wired` 的 TDZ。与前两次同族:**新加的接线/判据必须排在它读的所有 `const` 之后**。
⇒ 顺手在这个块前显式定义 `selfSource`,并写清它是**本作用域**的读数。

## 六、验证与状态

· 五个自检单独跑仍全 rc=0;默认跑 `checks=459 pass=455 fail=4 skip=0 red=9 … verdict=red`、
  `mutants=48 ran=47 skipped=1 on_new_criteria=35 diag=none baseline=7/7✓`;正常态 `^RED ` **0 条**。
· 残留全清;`git status` 仅本文件;`HEAD=16cbc02`。
This commit is contained in:
2026-09-18 07:41:47 +08:00
parent 16cbc02421
commit dc261ad537

View File

@ -2364,11 +2364,11 @@ let selfCheckRuns = 0;
* ⇒ 样本与生产扫描调的是**同一个** `declaredSelfTests`(不是抄一份)。
*/
const FORM_SAMPLES = [
['function 声明', 'function fooSelfTest() {}\n', /^function fooSelfTest\(\)/],
['箭头函数', 'const fooSelfTest = () => {};\n', /^const fooSelfTest = \(\) =>/],
['async function', 'async function fooSelfTest() {}\n', /^async function fooSelfTest\(\)/],
['名与括号间有空格', 'function fooSelfTest (a) {}\n', /^function fooSelfTest \(/],
['const = async (…', 'const fooSelfTest = async (a) => {};\n', /^const fooSelfTest = async \(/],
['function 声明', 'function fooSelfTest() {}\n', /^function\s+\w+SelfTest\s*\(/],
['箭头函数', 'const fooSelfTest = () => {};\n', /^const\s+\w+SelfTest\s*=\s*\(\s*\)\s*=>/],
['async function', 'async function fooSelfTest() {}\n', /^async\s+function\s+\w+SelfTest\s*\(/],
['名与括号间有空格', 'function fooSelfTest (a) {}\n', /^function\s+\w+SelfTest\s+\(/],
['const = async (…', 'const fooSelfTest = async (a) => {};\n', /^const\s+\w+SelfTest\s*=\s*async\s*\(/],
];
/*
* ★★ **样本表本身也必须被钉住**(我自己变异出来的 M45)。
@ -2387,8 +2387,16 @@ let selfCheckRuns = 0;
* ① 每种**要求**的写法都必须有样本(⇒ 删样本 = 红,不再是静默);
* ② 每个样本都必须能被认出来(原有方向)。
* ⇒ 删样本现在会红,而且**指名**删的是哪一种。
* (为什么不是"断言 `length >= 5`":那个数**自己**就是可以随手改小的常量;
* 具名清单至少让"我要放弃支持箭头函数写法"变成一次**看得见的声明**。)
* (为什么不是"断言 `length >= 5`":那个数**自己**就是可以随手改小的常量。)
*
* ⚠️⚠️ **我原来在这里写的是"具名清单至少让'我要放弃支持箭头函数写法'变成一次
* 看得见的声明"—— 这句被 pi 实测推翻,我复现,读数逐字相同:**
* 把「箭头函数」从 `REQUIRED_FORMS` 与 `FORM_SAMPLES` **同步**删掉(合法地少支持一种)
* ⇒ `red=9` 与基线**逐字相同**、相关 RED **0 条**,只有成功报文里 5 变成 4。
* 连 `declaredSelfTests` 里 `const` 那条规则**一起**删 ⇒ 同样 **0 条**。
* ⇒ **"具名"这个形式救不了它**:`length >= 5` 与 `REQUIRED_FORMS` 在被同步修改
* 这一点上**难度相同**(都是一次编辑)。
* ★ 真正的锚点不是"具名",是**"要求必须落到一个非手写的锚点上"**(见下面的名字级扫描)。
*/
const REQUIRED_FORMS = ['function 声明', '箭头函数', 'async function', '名与括号间有空格', 'const = async (…'];
const sampleNames = FORM_SAMPLES.map(([w]) => w);
@ -2456,7 +2464,49 @@ let selfCheckRuns = 0;
console.log(`RED 发现规则收进了不以 \`SelfTest\` 结尾的名字:${badName.join('、')}`);
reds.push(`(自检)\`declaredSelfTests\` 收进了不符合命名约定的名字:${badName.join('、')}`);
}
const selfSource = stripComments(prose(fileURLToPath(import.meta.url)));
const wired = new Set(SELFTESTS.map(([, fn]) => fn.name));
/*
* ★★ **名字级锚点**(pi 2026-09-18 报的那条:三张**手写表**互相比对,没有外部锚点)。
*
* pi 的实验(我复现,读数逐字相同):把「箭头函数」从 `REQUIRED_FORMS` 与 `FORM_SAMPLES`
* **同步**删掉 ⇒ `red=9` 与基线**逐字相同**、相关 RED **0 条**,只有成功报文里
* 的数字从 5 变 4。更尖的形式:连 `declaredSelfTests` 里 `const` 那条规则**一起**删
* (四字段全同步)⇒ 同样 **0 条**,而"那种写法将来既不跑也不报警"的后果是实的。
*
* ★ 根因(pi 的概括我认同):上面这几张表**都是手写的、互相比对**——
* 名单 ↔ 样本 ↔ 见证,三张同步改就一致地"对",**没有任何外部锚点**。
* 对照 `UPSTREAM_RC`:它当年止住了共漂移,是因为它比对的是**真脚本真跑出来的 rc**,
* 不是另一张手写表。**"具名"这个形式救不了它** ——
* `length >= 5` 与 `REQUIRED_FORMS` 在被同步修改这一点上难度相同(都是一次编辑)。
* ⇒ 所以我 M45 注释里那句"具名清单让放弃支持变成**看得见的声明**"
* **被实测推翻**,这里改正:**只有当"要求"能落到一个非手写的锚点上,它才看得见。**
*
* ⇒ 锚点用**名字级扫描**:源码里出现的**任何** `\w+SelfTest` 标识符,
* 都必须已经接线(在 `SELFTESTS` 里)——**不解析声明语法**。
* 于是它与 `function`/`const`/`async`/箭头/空格**全部无关**:
* 发现规则认不出某种写法时,那种写法的自检名**仍然会被这个名字级判据看见**。
* 实测(我做过):删掉 `const` 规则 + 四字段同步 + 一个**未接线**的箭头自检
* ⇒ 旧的规则级扫描 **0 条 RED、一字不提**;名字级扫描**看得见** `silentArrowSelfTest`。
*
* ★ 唯一需要登记的例外:`FORM_SAMPLES` 里样本内容用的假名(`fooSelfTest`)。
* 它是一个**显式登记的**例外,不是"扫不到就算了"——这样它自己也看得见。
*/
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));
if (nameUnwired.length) {
console.log(`RED 这些 \`*SelfTest\` 标识符**出现在源码里却没接线**(名字级扫描,与写法无关):`
+ `${nameUnwired.join('、')} ⇒ 默认路径不会跑它们`);
reds.push(`(自检)**名字级扫描发现没接线**:${nameUnwired.join('、')}`
+ ' —— 这条判据不看声明语法,所以换写法也躲不掉');
}
const ghostSample = SAMPLE_FAKE_NAMES.filter(n => !allNames.includes(n));
if (ghostSample.length) {
console.log(`RED 登记的样本假名在源码里找不到:${ghostSample.join('、')}`);
reds.push(`(自检)\`SAMPLE_FAKE_NAMES\` 里的 ${ghostSample.join('、')} 在源码里找不到(登记该清了)`);
}
const unwired = declared.filter(n => !wired.has(n));
const phantom = [...wired].filter(n => !declared.includes(n));
if (unwired.length) {