test(criteria): 判据规范 CRITERIA.md(邻接不是结构·第三次露头)+ 废弃 API allow-list 结构化 + 扫描范围自检

pi 的两条增量,都不需要他再确认。

## 1(pi 建议):生成清单 + 空 allow-list 的结构性风险

他的推演:SDK 升版会往清单里加新条目 → 某天早上套件**突然红**,且红在与本次改动无关的
代码上;这时人的第一反应是把名字塞进 allow-list —— 而 allow-list 一旦这么用,
就不再是"研究过的例外",只是"红的止痛药"。所以条目结构化:

```js
const ALLOW = [ /* { name, replacement, why } */ ];
```

`replacement` 非空是硬断言("暂时不想改"不是放行理由,"替代品要求的 API level 高于基线"才是)。
**刻意不断言"名单必须为空"** —— 那会挡住合理放行;断言的是"有名字、没替代品 → 红",
于是侵蚀发生时红的是**放行这件事本身**,而不是某天的新 SDK。
变异:塞一条 `{ name:'px2vp', replacement:'', why:'暂时不想改' }` → 红。

## 2(pi 建议):扫目录的判据要防"空判据"

他问废弃 API 判据扫哪些目录(怕只扫 `pages/`,`common/` 里的旧写法逃掉)。
答案:扫的是**整个 ets 目录递归**(实测 25 个文件,含 `pages/ common/ model/ api/ entryability/`)。
顺手加了防退化的自检:文件数 ≥ 20,且 `pages/ common/ model/ api/` 四个目录都必须扫到
—— 目录改名/只扫一个子目录会让这条变成空判据而依然全绿。
变异:把扫描范围改成只扫 `pages/` → 红。

## 3(pi 建议):把"邻接不是结构"写进判据规范

同一个坑在本仓露头三次:① 窗口式正则被一行注释挤爆(原注释自嘲过);
② 括号配对取代窗口;③ 链式修饰符让"看前一个字符是不是 `}`"静默失效。
共同形式值得升格成规则,于是新建 `client/electron/test/CRITERIA.md`(七条),
并在 `run-all.mjs` 加**自检 3**:规范文件必须在、且必须点到关键条目。
WebUI 侧 `background.test.mjs` 的窗口式存量按 pi 的说明**记着不动**(那是他的地盘)。

规范里另外两条是本仓自己踩出来的:剥注释读代码 vs 读原文读理由(混用必红);
以及**验证要按真实入口跑** —— 我用 `node --test test/run-all.mjs` 验自检 3 时它"依然绿",
其实是 runner 把内部的 `process.exit(1)` 吞了;换成 `npm test` 走的那一行就红对了。
变异:删掉规范里的一条关键规则 → `npm test` 那条路 exit 1。

## 验证

`npm test` 退出码 0(11 个判据文件全绿 + vitest 258/258);`hvigorw assembleHap` 未受影响。
This commit is contained in:
2026-09-14 14:51:30 +08:00
parent c4ee5f3ebe
commit 0ce8b29546
4 changed files with 165 additions and 2 deletions

View File

@ -248,11 +248,29 @@ test('废弃 API清单**从 SDK 生成**,源码里不得调用(新增一
// 全局 showToast 不是 `declare function`(它是命名空间成员),由 harmony-logic 那条单钉
/**
* 允许的例外:**每条都要写理由**。
* 空名单就是"一处都不许"—— 想加就得在这里写明为什么非用不可。
* 允许的例外:**每条都要写"替代品"和"为什么现在不能换"**。
*
* pi 指出这套组合(生成的清单 + 空 allow-list有个结构性风险
* SDK 升版会往清单里加新条目 → 某天早上套件**突然红**,且红在与本次改动无关的代码上;
* 这时第一反应是把名字塞进 allow-list而 allow-list 一旦这么用,
* 就不再是"研究过的例外",只是"红的止痛药"。
*
* 所以条目结构化成 `{ name, replacement, why }`,并断言 `replacement` **非空**
* "暂时不想改"不是放行理由,"替代品要求的 API level 高于本机基线"才是。
* **刻意不断言"名单必须为空"** —— 那会挡住合理放行;但"有名字、没替代品"必须红,
* 这样侵蚀发生时红的是**放行这件事本身**,而不是某天的新 SDK。
*/
const ALLOW = [];
// 名单形状自检:每条都必须有替代品与理由(防止以后有人只塞个名字进来)
for (const entry of ALLOW) {
assert.ok(typeof entry.name === 'string' && entry.name.length > 0, 'allow-list 条目要有 name');
assert.ok(typeof entry.replacement === 'string' && entry.replacement.length > 0,
`allow-list 里 ${entry.name} 没写"替代品" —— 没有替代品的放行不是例外,是止痛药`);
assert.ok(typeof entry.why === 'string' && entry.why.length > 0,
`allow-list 里 ${entry.name} 没写"为什么现在不能换"`);
}
const walk = (dir, acc = []) => {
for (const e of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, e.name);
@ -261,7 +279,22 @@ test('废弃 API清单**从 SDK 生成**,源码里不得调用(新增一
}
return acc;
};
/*
* 扫描范围 = **整个 ets 目录递归**`pages/` `common/` `model/` `api/` `entryability/` …),
* 不是只扫 `pages/` —— pi 点过这条:只扫页面的话 `common/` 里的旧写法会逃掉,
* 而"新代码照抄旧模块"这条路径最常发生在 `common/`。
*
* "扫到的文件数 ≥ N"这条自检是防它**悄悄退化**(目录改名、遍历写错、
* 只扫了一个子目录都会让这条判据变成空判据而依然全绿)——
* 与 `cross-client-theme` 里给 pages 加的那条同形。
*/
const files = walk(ETS_DIR);
const scannedDirs = new Set(files.map(f => f.slice(ETS_DIR.length + 1).split('/')[0]));
assert.ok(files.length >= 20,
`这条判据要扫到整个 ets 目录(至少 20 个文件),实际 ${files.length} 个 —— 扫描范围退化了`);
for (const dir of ['pages', 'common', 'model', 'api']) {
assert.ok(scannedDirs.has(dir), `扫描范围要包含 ${dir}/(否则那里的旧写法会逃掉)`);
}
const hits = [];
for (const f of files) {
const src = readFileSync(f, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');