fix(criteria): 两处"排除"职责不同 —— pi 的诊断错在把 t 当成观察者;但它"按名字不够"那半成立,改按**身份**

pi `661416f3` §二 报: 「`:765` 用 `basename(p) === t`(按名字)而 `:745` 用 `resolve(...) !== SELF`
(按身份)⇒ 你今天把同一件事做了两遍、两种判据 ⇒ 建议 `:765` 也改成排除观察者」。

★ **它把两处 `continue` 认成了同一件事,而它们职责不同** —— 我先复现它的建议,结果是**回归**:

```
:t                  来自 `tools = readdirSync(deploy)` 的非门禁 *.sh ⇒ **是被测工具**,不是观察者
:745(SCAN 那层)    排除**观察者**(本判据自己)—— 防"描述缺陷"被当成"存在引用"
:765(内层)         排除**工具自己的文件** —— 每个工具头注释都写自己名字(实测 5/5 各 1 处),
                    不排除 ⇒ **"自名"被算成"发现路径"** ⇒ 每个孤儿自证可达 ⇒ 永久假绿
```

## 双向变异实测(同一棵树,只改这一行)

```
① 应用 pi 的建议(:765 → 排除观察者)+ 造真孤儿 `deploy/zz-orphan-probe.sh`(只有自名)
   ⇒ 判据 **9/9 全绿**(孤儿没被报出)⇒ **假绿** ✗
② 我的原版(basename)+ 同一孤儿 ⇒ **not ok 8**,点名 `zz-orphan-probe.sh` ✓
```

★ 但 pi 那封里**有一半成立**(我一开始也差点整条驳掉 —— 这是本仓记过的形状:
  "用一个真机制去驳掉整条建议"):
```
若别处出现与工具**同 basename** 的文件(如 `docs/archive-stale-sessions.sh`),
  `basename(p) === t` 会**把它也跳过** ⇒ 那处引用白算 ⇒ **假红**。
实测: 把 `archive-stale-sessions.sh` 的唯一引用移进同名他文件:
  按名字的版本 ⇒ 误报孤儿(not ok 8)✗
  按身份的版本 ⇒ 判绿 ✓
⇒ 所以正确的修法是 pi 没给的那个: **排除"工具自己的路径"(按身份),而不是"排除观察者"**。
  两边都保住: 自名仍被排除(不假绿)、同名他文件仍被计入(不假红)。
```

## 落地

- `:765` → `resolve(p) === resolve(join(DEPLOY, t))`
- 删掉因本次改动而成为**死 import** 的 `basename`(本仓唯一一处 import-未用)
- 把"两处排除职责不同 + 按名字不够"写进注释,并加**反空真断言**:
  `assert.notEqual(resolve(join(DEPLOY, tools[0])), SELF_FILE)`
  —— 防"两处排除被合并/退化成一个"(那样就等于删掉其中一个,而删哪个都会假绿)

验证: 变异①(真孤儿)红、变异②(同名他文件)绿;正常树 9/9;drift 自检 64/0;
`run-all.mjs:141` 注册条数 9 未变(本次是加断言+注释,未新增 test)。
This commit is contained in:
2026-09-25 07:44:06 +08:00
parent 3a75da3222
commit fc54a816e8

View File

@ -24,7 +24,7 @@
import assert from 'node:assert/strict';
import { readdirSync, unlinkSync, writeFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { basename, dirname, join, relative, resolve } from 'node:path';
import { dirname, join, relative, resolve } from 'node:path';
import { test } from 'node:test';
import { fileURLToPath } from 'node:url';
@ -756,13 +756,34 @@ test('★ 非门禁工具(deploy/*.sh)必须有发现路径(至少一处
* 判据红/绿的原因变成了**它自己怎么写这段说明**,而不是被测对象的状态。
* ⚠️ 泛化:**任何"扫全仓找引用"的判据都必须排除观察者本身**,
* 否则"描述缺陷"与"存在引用"不可区分(这条与 `stripComments` 那条同源)。
*
* ★★★ 还有**第二个**排除,它常被误认成同一个(pi `661416f3` 就认错了,我实测反驳):
* 这里有两处 `continue`,**职责不同**:
* · `SCAN` 那一层(上面 `.filter(resolve(p) !== …import.meta.url)`)排除的是**观察者**
* —— 本判据自己,理由见上。
* · 这一层(`resolve(join(DEPLOY, t))`)排除的是**被测工具自己的文件** ——
* 每个工具的头注释都写自己的名字(实测 5/5 各 1 处),若不排除,
* **"自名"就会被算成"发现路径"** ⇒ 每个孤儿自己证明自己可达 ⇒ 永久假绿。
* ⇒ ★ 所以这一处**不能**改成"排除观察者",那是**删掉**本排除(实测: 真孤儿立刻漏报)。
* ★ 但"按名字(basename)"确实不够 —— 那是 pi 那封里**唯一成立**的部分:
* 若别处出现与工具同 basename 的文件(如 `docs/prune-demo.sh`),
* `basename(p) === t` 会**把它也跳过** ⇒ 那处引用白算 ⇒ **假红**(实测: 唯一引用在
* 同名他文件里时,按名字的版本误报孤儿;按身份的版本判绿 ✓)。
* ⇒ 结论(比 pi 的建议严一格,且不丢本排除): **按身份比对被测工具自己的路径**。
*/
const SELF_FILE = resolve(fileURLToPath(import.meta.url));
/** 反空真: 两处排除必须真的指向不同对象,合并了就会退化成"删掉一个"。 */
assert.notEqual(
resolve(join(DEPLOY, tools[0])), SELF_FILE,
'两处排除指向了同一个文件 ⇒ 本判据的排除逻辑坏了(观察者 == 被测工具?)。',
);
const orphans = [];
for (const t of tools) {
let refs = 0;
for (const p of SCAN) {
if (basename(p) === t) continue; // 排除自身
// 排除**工具自己的文件**(按身份,不按名字 —— 按名字会漏掉同名他文件里的引用)
if (resolve(p) === resolve(join(DEPLOY, t))) continue;
if (prose(p).includes(t)) refs++;
}
if (refs === 0) orphans.push(t);