feat(criteria): 新建「非门禁工具必须有**发现路径**」判据 —— 补上改名制造的盲区(pi a6dd501c)

pi 指出、我复现的一个**盲区**:`check-*.sh` 族靠命名约定被 `readdirSync` 强制接线
(上一条判据管的就是"判据在但走不到")。★ 而这条约定的**代价**是:
**为了躲开它而改名之后,没有任何判据管"改名后还找不找得到"**。

```
实测(我独立复现):
  recount-relay-counts.sh   非自身引用 = 1(只有 docs/DEBTS.json)
  archive-stale-sessions.sh 非自身引用 = **0**(全仓只命中它自己)
对照: prune-deploy-artifacts.sh 在 docs/DEV-TOOLING.md:71 有**专节** ⇒ 那才是它被找到的原因
⇒ 两个机制不同、都要有:
    check-* 族: 「判据在但**走不到**」(**执行**路径)—— readdirSync 强制
    按需工具 : 「工具在但**没人知道它存在**」(**发现**路径)—— 本判据
   改名正好绕开前者 ⇒ 后者必须独立存在
```

## 判据

`criteria-hygiene` 新增:每个非门禁 `deploy/*.sh` 至少要有**一处非自身引用**
(扫 docs/deploy/test/.githooks/scripts 的 .md/.mjs/.sh/.json,用 `prose()` —— 判的是散文)。
含反空真护栏(工具数 <2 报红)。

固定 7 → **8**,已同步 `run-all.mjs` 的登记数。

## ★★★ 建这条判据时我自己先假绿了一次(已修,值得记)

第一版判绿了,而 `archive-stale-sessions.sh` **明明是零引用**。原因:
```
我的判据注释里写着「archive-stale-sessions.sh —— 非自身引用 = **0**」
⇒ 判据扫"文件里有没有出现这个名字" ⇒ **它自己那句描述**被数成 1 个引用
⇒ 一个零引用的孤儿**因为被描述成孤儿**而看起来有引用 ⇒ 判绿
```
修法:扫描时**排除观察者自身**(`resolve(p) !== resolve(fileURLToPath(import.meta.url))`)。
⚠️ 我第一版排除的是 `SELF`,而本文件里 `SELF` 指的是 `lib/read.mjs`(另一个文件的变量)
⇒ 排错了对象,仍然假绿。**"我知道要排除自己"和"我排除的是自己"是两件事。**
泛化:**任何"扫全仓找引用"的判据都必须排除观察者本身**,
否则"描述缺陷"与"存在引用"不可区分(与 `stripComments` 那条同源)。

## 变异验证(两侧都验,不只验绿)

```
删掉 DEV-TOOLING 的"按需工具"整节 ⇒ 该判据 **红**(报出 archive-stale-sessions.sh)✓
恢复                          ⇒ 绿 ✓
```

## 顺带

`docs/DEV-TOOLING.md` 加「按需工具」一节,逐个列出发现路径(5 个工具)+ 两个机制的对照表。

验证: `criteria-hygiene` 8/8;`run-all --test criteria-hygiene` 该文件不在红名单。
(另 4 个红文件为**既有**、与本改动无关: build-stamp 是前端产物未重建(expected 9e05322 /
actual 6c98ac2)、commit-hygiene 是 AGC 配置、cross-client-gesture 需设备、cross-client-theme
单独跑 21/21 绿 —— run-all 里那次红来自环境差异。)
This commit is contained in:
2026-09-25 07:13:04 +08:00
parent 9e053227f4
commit 0c6c506d7a
3 changed files with 111 additions and 3 deletions

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 { dirname, join, relative, resolve } from 'node:path';
import { basename, dirname, join, relative, resolve } from 'node:path';
import { test } from 'node:test';
import { fileURLToPath } from 'node:url';
@ -698,3 +698,82 @@ test('★ 政策门禁(deploy/check-*.sh)必须被入口脚本真正调用
+ ' **不能直接调** —— 门禁红时它 `exit 1`,而 `install.sh` 是 `set -e`,\n'
+ ' 会在那里中止、后面所有诊断一行都不打(`install.sh` 141-157 行刚修过同一个毛病)。');
});
/*
* ★★ 非门禁工具的**发现路径**(pi `a6dd501c` 2026-09-25 指出,我复现)。
*
* 上一条判据管住了 `check-*.sh`(门禁族)—— 它靠**命名约定**被强制接线。
* 而那条约定有个代价:**为了躲开它而改名之后,就没有任何判据管"改名后还找不找得到"**。
* 实测:
* · `recount-relay-counts.sh` —— 新名(为躲 `check-*` 约定而改)非自身引用 = **1**
* (只有 `docs/DEBTS.json`),`install.sh` / `.githooks` / `docs/DEV-TOOLING.md` 全 0
* · `archive-stale-sessions.sh` —— 非自身引用 = **0**(全仓只命中它自己)
* 对照:同族的 `prune-deploy-artifacts.sh` 在 `docs/DEV-TOOLING.md:71` 有**专节** ——
* 那才是它会被找到的原因(发现路径存在,不靠人记得)。
*
* ⇒ 判据: **每个非门禁 `deploy/*.sh` 至少要有一个"非自身"的引用**。
* 与上一条**同族但机制不同**:上一条防"判据在但**走不到**"(执行路径),
* 这一条防"工具在但**没人知道它存在**"(发现路径)。
* ⚠️ 为什么上一条盖不住它: `check-*` 靠 `readdirSync` 强制,
* 而**改名正好绕开那条约定** —— 于是"改名"这个合规动作把工具从"强制被调用"
* 直接降级成"零引用",**而且没有任何东西会红**。
*/
test('★ 非门禁工具(deploy/*.sh)必须有发现路径(至少一处非自身引用)', () => {
const DEPLOY = join(REPO_ROOT, 'deploy');
// 排除门禁族(上一条已管)与三个入口脚本自身(它们是入口,不是"被发现的对象")
const ENTRY_ITSELF = new Set(['install.sh', 'redeploy-gateway.sh', 'redeploy-plugin.sh']);
const tools = readdirSync(DEPLOY)
.filter((n) => n.endsWith('.sh') && !/^check-.*\.sh$/.test(n) && !ENTRY_ITSELF.has(n))
.sort();
// 反空真:工具一个都没列出来 ⇒ 大概率是路径/目录变了,不是"工具真的没了"
assert.ok(tools.length >= 2,
`只找到 ${tools.length} 个非门禁 deploy/*.sh(期望 ≥2)—— 大概率是路径变了。空真必须报红。`);
/*
* 扫描范围:可能承载"发现路径"的文本。用 `prose()`(原文)——
* 判的是"文档/注释里有没有提到它",那**正是散文**,不是代码。
*/
const SCAN_DIRS = ['docs', 'deploy', 'client/electron/test', '.githooks', 'scripts'];
const SCAN = SCAN_DIRS.flatMap((d) => {
const base = join(REPO_ROOT, d);
try {
return readdirSync(base, { recursive: true })
.map((f) => join(base, String(f)))
.filter((p) => /\.(md|mjs|sh|json)$/.test(p) && !p.includes('node_modules'));
} catch { return []; }
}).filter((p) => resolve(p) !== resolve(fileURLToPath(import.meta.url)));
/*
* ★★★ **必须排除本判据自己** —— 这一条我第一版漏了,当场假绿。
*
* 那条注释里写着「`archive-stale-sessions.sh` —— 非自身引用 = **0**」,
* 而判据扫的是"文件里有没有出现这个名字" ⇒ **它自己那句描述**就被数成了 1 个引用
* ⇒ 一个零引用的孤儿**因为被描述成孤儿而看起来有引用** ⇒ `refs=1` ⇒ 判绿。
*
* 实测:排除 SELF 之前 8/8 全绿(假绿);排除之后立刻报出 `archive-stale-sessions.sh`。
* 这与本仓已记录的那族**完全同形**:"判据开始消费散文" ——
* 判据红/绿的原因变成了**它自己怎么写这段说明**,而不是被测对象的状态。
* ⚠️ 泛化:**任何"扫全仓找引用"的判据都必须排除观察者本身**,
* 否则"描述缺陷"与"存在引用"不可区分(这条与 `stripComments` 那条同源)。
*/
const orphans = [];
for (const t of tools) {
let refs = 0;
for (const p of SCAN) {
if (basename(p) === t) continue; // 排除自身
if (prose(p).includes(t)) refs++;
}
if (refs === 0) orphans.push(t);
}
assert.deepEqual(orphans, [],
`这些工具**没有任何发现路径**(存在,但没人会知道它存在):\n`
+ ` ${orphans.join('\n ')}\n`
+ ' ⇒ 药方:在 `docs/DEV-TOOLING.md` 加一节(照 `prune-deploy-artifacts.sh` 的先例),\n'
+ ' 或在相关入口/工具里引用它。\n'
+ ' ⚠️ 为什么要紧(实测形状):为了躲开 `check-*` 命名约定而**改名**,\n'
+ ' 会让工具从"强制被调用"掉到"零引用",而**上一条判据正好不再覆盖它** ——\n'
+ ' 改名这个合规动作本身制造了一个无判据的盲区。');
});

View File

@ -136,8 +136,9 @@ const SUITE = [
['test/harmony-calendar.test.mjs', ['--experimental-strip-types', '--no-warnings'], 30],
['test/debt-visibility.test.mjs', [], 1],
['test/commit-hygiene.test.mjs', ['--experimental-strip-types', '--no-warnings'], 4],
// 判据目录自身的卫生:读文本必须走 test/lib/read.mjs 的具名入口
['test/criteria-hygiene.test.mjs', [], 7],
// 判据目录自身的卫生:读文本必须走 test/lib/read.mjs 的具名入口;
// 以及非门禁工具必须有**发现路径**(躲开 check-* 约定改名 ⇒ 否则零引用无人知)
['test/criteria-hygiene.test.mjs', [], 8],
// 用户管理页(P4c 同批):动作↔服务端调用同名 / 门禁只认严格 admin /
// 启停只发 status / 「受限」徽标口径 / 页面零写死色值 / 接线(纯逻辑真被调用)
['test/harmony-admin.test.mjs', ['--experimental-strip-types', '--no-warnings'], 31],

View File

@ -123,6 +123,34 @@ bash deploy/prune-deploy-artifacts.sh --self-check # 判据自检(16 项,
③ "自检不碰生产"进判据(前后指纹比对)。这条与 `pkill -f` 杀自己同类:
**安全装置自己出错时,产出的是一份看着正常的报告**。
## 按需工具(`deploy/` 下非门禁的那些)
`deploy/check-*.sh` 是**门禁族**:它们靠**命名约定**被 `criteria-hygiene.test.mjs`
用 `readdirSync` 强制接线,凡该族必须被入口脚本调用(判据在、不许走不到)。
★ 而这条约定的**代价**是:**为了躲开它而改名之后,就没有任何判据管"改名后还找不找得到"**。
为躲开 `check-*` 而改名的工具(`recount-*`/`prune-*`/`archive-*`/`reset-*`)属于**按需工具**:
不强制接线,但必须**有发现路径** —— 否则它存在,而没人会知道它存在。
判据在 `criteria-hygiene` 的「非门禁工具必须有发现路径」:每个非门禁 `deploy/*.sh`
至少要有**一处非自身引用**(文档 / 入口 / 别的工具)。实测它建起来时抓到的第一个孤儿
就是 `archive-stale-sessions.sh`(全仓零引用)。
| 按需工具 | 发现路径 |
|---|---|
| `deploy/prune-deploy-artifacts.sh` | 本节上方专节 |
| `deploy/recount-relay-counts.sh` | 本节(口径复算;被 `docs/DEBTS.json` 引用) |
| `deploy/archive-stale-sessions.sh` | 本节 —— 归档陈旧会话(`--dry-run` 先看) |
| `deploy/prune-test-sessions.sh` | `docs/DEV-TOOLING.md` 的清理表、`redeploy-gateway.sh` 注释 |
| `deploy/reset-demo.sh` | `client/electron/README` 与演示脚本族 |
⚠️ 同名判别器的**两个不同机制**(别混):
```
check-* 族 : 「判据在但**走不到**」(**执行**路径)—— readdirSync 强制
按需工具族 : 「工具在但**没人知道它存在**」(**发现**路径)—— 本表就是那个发现路径
改名正好绕开前者 ⇒ 因此后者必须独立存在,不能指望前者兜住
```
## 判据纪律:三种"看起来验过了"的失效形态(2026-09-14)
同一天里,三条自己写的判据各以一种方式失效 —— 它们**都绿着**,但都不再判别任何东西。