feat(criteria): ⑤b 负向清单的**两处副本**都要有守 —— 关掉我在 4598095 里明确留下的那条尾巴

`4598095` 结尾我写了「§7 那条同类无守…不在这条提交里改」—— 这条把它关掉。

## 形状(pi `7ec0044a` 指出,我逐条验证)

「已装二进制 = 当前 HEAD —— 不覆盖: ①脏树构建 ②部署后手工替换」这句话有**两份副本**:
```
deploy/check-deploy-drift.mjs   `negative` 常量 + 自检格      ← **有守**
deploy/redeploy-gateway.sh:480  注释 + `ok` 文案(§7)        ← **无守**
```
实测:
```
grep '已装二进制 = 当前 HEAD' 全仓 ⇒ **只命中 redeploy-gateway.sh 那一行**
redeploy-gateway.sh 的 `--self-check` 出现次数 = **0**(参数只有 --skip-tests/--skip-web/--dry-run/--help)
⇒ 同一个动作、同一句"不覆盖"的声明,**一处有守、一处没有**
```
★ 而**无守的那份恰是部署时打印到屏幕上的那份** —— 读者看到的就是它。

## 判据

`criteria-hygiene` 新增:两份副本都必须同时点名同一对失败类(`脏树` / `手工替换`)。
**变异验证**(两侧都验):
```
shell 那份删掉"手工替换"(注释与 ok 文案都删)⇒ 该判据 **红**,点名
  `redeploy-gateway.sh 未点名「手工替换」` ✓
恢复 ⇒ 绿 ✓
```

固定 8 → **9**,已同步 `run-all.mjs`。

## 归族

与上一条(发现路径)**同族**: 都防「**声明的副本**没有守卫」。
区别: 上一条防"没人知道工具存在",这一条防"**声明漂了没人知道**"。
★ pi 的 ⑬″ 判法在这里的用法:先问"这句声明**在 R 内还是 R 外**"——
  「不覆盖②」属于 R 内(该判据确实回答不了内容替换)⇒ 它不是"划出宣称"就能了事,
  而是**必须说出来**,所以两处副本都要保住这句话。

验证: `criteria-hygiene` 9/9;`bash -n deploy/redeploy-gateway.sh` 未受影响(本提交没动它)。
This commit is contained in:
2026-09-25 07:16:26 +08:00
parent 0c6c506d7a
commit b1eb0ab0c9
2 changed files with 44 additions and 2 deletions

View File

@ -777,3 +777,45 @@ test('★ 非门禁工具(deploy/*.sh)必须有发现路径(至少一处
+ ' 会让工具从"强制被调用"掉到"零引用",而**上一条判据正好不再覆盖它** ——\n'
+ ' 改名这个合规动作本身制造了一个无判据的盲区。');
});
/*
* ★★ 同一断言在两处各有一份副本时,**两份都要有守**(pi `7ec0044a` 指出,我验证)。
*
* 背景:⑤b("已装二进制 = 当前 HEAD")这句话出现在**两个文件**里 ——
* · `deploy/check-deploy-drift.mjs` 的 `negative` 常量(+ 它的自检格)
* · `deploy/redeploy-gateway.sh` 的注释与 `ok` 文案(§7)
* 而**只有前者有自检**。实测:
* grep '已装二进制 = 当前 HEAD' 全仓 ⇒ **只命中 redeploy-gateway.sh 那一行**
* redeploy-gateway.sh 的 `--self-check` 出现次数 = **0**
* ⇒ 同一个动作、同一句"不覆盖"的声明,**一处有守、一处没有**。
*
* ⇒ 判据: **两份负向清单必须同时存在,且都点名同一对失败类(脏树 / 手工替换)**。
* 为什么不能只查一份: 删掉 shell 那一份、或它漂成只写一项,**没有任何东西会红** ——
* 而读者看到的 `ok` 行正是 shell 那份(那是部署时**打在屏幕上**的那份)。
* ⚠️ 这与上一条(发现路径)同族: 都防"**声明的副本**没有守卫"。
* 区别: 上一条防"没人知道工具存在",这一条防"**声明漂了没人知道**"。
*/
test('★ ⑤b 的负向清单在 JS 与 shell 两处都有副本,两份都必须点名同一对失败类', () => {
const JS = join(REPO_ROOT, 'deploy', 'check-deploy-drift.mjs');
const SH = join(REPO_ROOT, 'deploy', 'redeploy-gateway.sh');
// 每一份都必须同时点出这两类(正是它们**不被**该判据覆盖)
const CLASSES = ['脏树', '手工替换'];
const missing = [];
for (const [name, p] of [['check-deploy-drift.mjs', JS], ['redeploy-gateway.sh', SH]]) {
const src = prose(p);
for (const c of CLASSES) {
if (!src.includes(c)) missing.push(`${name} 未点名「${c}」`);
}
}
assert.deepEqual(missing, [],
`⑤b 的负向清单有副本没写全(**副本漂了,而它恰好是屏幕上看的那份**):\n`
+ ` ${missing.join('\n ')}\n`
+ ' ⇒ 这两类的**唯一**呈现处就是这两份文案;漏掉一类 ⇒ 读者会把绿读成"没问题":\n'
+ ' · 脏树构建(含未提交代码,只披露不判红)\n'
+ ' · 部署后被手工替换/修改(只看内嵌 revision)\n'
+ ' ⚠️ 为什么两处都要守: `check-deploy-drift.mjs` 有 `--self-check`,\n'
+ ' 而 `redeploy-gateway.sh` **没有** ⇒ 只守前者的话,删掉/写漂 shell 那份不会红,\n'
+ ' 而 shell 那份是**部署当时打印 `ok` 行**所用的文本。');
});

View File

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