pi 指出的残余缺口:我上一轮加的自检 4 是**文本证据**(文件里有 `test(` / `check(` /
`process.exit(1)`),只能证明"**有能红的路径**",不能证明"**它跑过**"。反例很短:
```js
const check = () => {}; // 实现被换空(现实形态:合并冲突改坏实现)
check('a', false); // 存在、也执行了,但什么都不会红
console.log('主题:通过'); // 有输出
```
## 落地(pi 给的闭环形状)
1. 自定义 `check()` 的判据结尾打一行机器可读汇总 `RESULT pass=<条数> fail=<失败数>`
(`node:test` 的判据不用改,已有 `# pass N`);
2. `run-all.mjs` **只解析这个固定 marker**(不猜口语汇总——「窄屏布局:全部通过」里没有数字,
按数字猜会误报,这一点我上轮已经实测过);
3. 与清单里登记的**期望条数**比对,**低于 → 红**。
关键细节:**计数写在 `check()` 内部**(theme/background 原本就在内部 ++;
narrow-layout 只有 failed 计数,补了 passed;markdown-xss 按 payload 条数算)。
写在调用点或靠扫源码的话,"实现被换空"就看不见了。
棘轮"只增不减":加判据**不用**改那个数,只有"条数掉了"才红。期望值按**实测**回填
(9/52/8/30/42/15/28/5/23/5/3/2)。
附带的可见性收益:这几轮我一直用"13→14""19→28""34→42"当信号,现在它成了判据 ——
某次改动顺手删掉两条判据、或某条被跳过,会立刻红。
## 变异
- pi 那个反例(`check` 换成空函数)→ 红(`自报 0 条 < 登记的 30 条`);
- 删掉 5 条 `check(` 调用 → 红(`自报 47 条 < 登记的 52 条`)。
## 规范
§6.5 新增"涉及运行时行为的结论必须实测过才能写进规范/判据"——同一个错这轮犯了两次
(我从"报告 0 个测试、退出 0"推断"退出码被吞",实测是照传;pi 拿我这个结论又建了一个洞)。
规则:**一次观察只支撑你看到的那一层**。
§6.6 记闭环形状与代价(故意删判据要同步改数字,属于一次可复核的显式编辑)。
⚠️ 并且如实记下一次**我自己违反规范**的事:写 §3 那条"变异后别用 `git checkout` 还原"的人
(就是我)在这次变异验证里又用了 `git checkout -- <文件>`,把刚加、尚未提交的 marker 抹掉了。
规矩写下来不等于会遵守 —— 已把这条实例写进规范,让人知道它是活人踩的坑。
## 验证
`npm test` 退出码 0(12 个判据文件全绿 + vitest 258/258);`run-all` 单独跑也 exit 0。
171 lines
10 KiB
Markdown
171 lines
10 KiB
Markdown
# 判据规范(写判据前先读这份)
|
||
|
||
这份文件记的是**判据本身的写法**:什么样的判据能红、能红在对的地方、以及不会在"代码完全正确"时乱红。
|
||
不是"怎么用 node:test",而是这个仓库里踩出来的形状。
|
||
|
||
一句话版本:**判据要钉结构与行为,不要钉字面与邻接。**
|
||
|
||
---
|
||
|
||
## 1. 判结构必须配对/解析;看邻接或固定宽度都会被合法写法绕过
|
||
|
||
统一形式(这是同一个坑在本仓露头的**第三次**,所以升格成规则):
|
||
|
||
> **判结构要配对/解析(括号、块、AST);看"前一个字符"或"固定宽度窗口"都会被合法写法绕过。**
|
||
|
||
三次露头:
|
||
|
||
| # | 当时的写法 | 被什么合法写法绕过 |
|
||
|---|---|---|
|
||
| ① | 固定宽度窗口正则 | 往规则里加一行注释就把 `backdrop-filter` 挤出窗口 —— 断言变红而 CSS 完全正确(`background.test.mjs` 的注释里记着这件事) |
|
||
| ② | 括号配对取代窗口 | 修掉了 ①,方向对 |
|
||
| ③ | "调用点往前看一个字符是不是 `}`" | ArkUI 的**链式修饰符**:`Row(){…}.backgroundColor(x).backgroundBlurStyle(A)` 里调用点前面是 `)` 不是 `}` → 判据**静默失效**(拿到 null 就放过去了) |
|
||
|
||
③ 的修法:往回扫时**跳过成对的括号组**再找 `}`。它同时暴露了"叠"有两种形态这件事
|
||
(同一组件叠两次 / 套在另一层玻璃的子树里),所以 §2 的形状在这些规则里反复出现。
|
||
|
||
**推论**:同一条规则里出现 `[\s\S]{0,N}` 时先问一句"N 是怎么来的"。答不出数字来源的,
|
||
就是窗口式判据,换成"取出这条规则的 `{...}` 体再断言"。
|
||
|
||
**已知存量**(pi 2026-09-14 点名,**WebUI 侧归他**,别人不要顺手改):
|
||
`client/electron/test/background.test.mjs` 里还有一批 `\{[\s\S]{0,80}…`、`{0,160}` 窗口。
|
||
下次碰那个文件时按本节形状改成"取规则体",不要在那里再加一条注释算了。
|
||
|
||
## 2. 合成的清单 + 空 allow-list:条目要结构化
|
||
|
||
"清单从 SDK/参考实现生成 + allow-list 目前为空"是个好形状,但它**迟早被侵蚀**:
|
||
上游升版会往清单里加新条目 → 某天早上套件突然红,且红在与本次改动无关的代码上。
|
||
这时人的第一反应是把名字塞进 allow-list —— 而 allow-list 一旦这么用,
|
||
就不再是"研究过的例外",只是"红的止痛药"。
|
||
|
||
所以条目写成三元组,并断言后两项非空:
|
||
|
||
```js
|
||
const ALLOW = [ /* { name, replacement, why } */ ];
|
||
```
|
||
|
||
- `replacement` 必须非空:**"暂时不想改"不是放行理由**,"替代品要求的 API level 高于本机基线"才是;
|
||
- **不要断言"名单必须为空"**(那会挡住合理放行);要断言的是"有名字、没替代品 → 红"。
|
||
这样侵蚀发生时红的是**放行这件事本身**,而不是某天的新 SDK。
|
||
|
||
同一个形状的实例:`cross-client-theme.test.mjs` 的 `SELF_OWNED_COLORS`(手写色登记表,
|
||
每条带理由)、`GLASS_REGISTRY`(玻璃面登记,每条带"为什么这里要玻璃")。
|
||
|
||
## 3. 判据必须能红,而且红的地方要对
|
||
|
||
每条新判据配一次**变异验证**:把源码改成"错的样子",确认它红,并且红在那条上。
|
||
变异没红有两种可能,都要查清:一是判据没覆盖,二是**变异没真的生效**
|
||
(本仓真发生过:变异脚本的锚点不匹配、缩进不对,于是"变异后依然全绿"被当成判据有效)。
|
||
|
||
顺带:变异后**不要用 `git checkout` 还原**(会连同未提交的改动一起抹掉)。
|
||
先 `cp` 到备份,再从事先的备份还原。
|
||
|
||
### 变异红了,还要看**红在哪**(pi 2026-09-14 补的一档)
|
||
|
||
只报"红了"不算证据。反过来的那一半同样成立:**变异后红了,也可能是假红** ——
|
||
比如变异把文件写坏了语法,判据红在"模块加载失败/解析不到源码",看起来像判据生效,
|
||
实际那条断言根本没执行。所以:
|
||
|
||
- 报结果要能**指名红的是哪几条**(本仓的变异表就是按这个写的:
|
||
"红 2 条(B + 裸色值)"、"红 3 条(含品牌色防线)");
|
||
- **红在解析/加载失败上不算红** —— 先让变异"语法正确、语义错",再谈判据有没有生效;
|
||
- 同理,变异**作用于注释**(被剥掉的那部分)也不算:判据读的是剥注释后的源码,
|
||
文档里的"理由"断言才读原文(见 §4)。
|
||
|
||
### 判据自己不会跑:一个家族,六种宿主
|
||
|
||
这条家族在本仓已经露头六次,共同点都是"**看起来全绿**":
|
||
|
||
| # | 宿主 | 形态 |
|
||
|---|---|---|
|
||
| ① | `&&` 链 | 前面红一条,后面全部不跑("红"不可信) |
|
||
| ② | 判据文件 | 没接进 `SUITE`(写好了但隐身) |
|
||
| ③ | 清单名字 | 文件名写错 = 静默跳过一条判据 |
|
||
| ④ | 文件末尾 | 判据写在 `process.exit()` **之后**(并发写入总往末尾追加) |
|
||
| ⑤ | 验证手法 | 用 `node --test` 去验 runner,里面的 `process.exit(1)` 被吞(见 §6) |
|
||
| ⑥ | runner 内部 | 清单 flag 与判据写法配错 |
|
||
|
||
`run-all.mjs` 现在对 ②③④⑥ 都有静态自检,⑥ 的落地方式见下(**不要**照抄"解析 pass 计数"):
|
||
|
||
⚠️ **实测过两条真实样本,结论与直觉不同**:
|
||
- `node --test <自定义 check() 的判据>`:**退出码照样传出来**(文件 exit 1 → 命令行 exit 1),
|
||
并没有被 runner 吞掉;
|
||
- 但 `node --test <什么都不做的文件>` 会报 `# tests 1 / # pass 1` ——
|
||
**计数不是"检查跑过"的证据**。所以"数 pass、0 就判红"既抓不到空判据(它报 1),
|
||
又会在汇总行没有数字的判据上误报。
|
||
|
||
改用**结构证据**:每条判据文件里必须存在"能红"的路径(`test(` / `check(` / `process.exit(1)`),
|
||
外加"跑完必须有输出"。**先看真实输出再写规则**——这一条本身就是 §1 的推论。
|
||
|
||
## 4. 读源码断言的两种模式,别混用
|
||
|
||
- 断**代码行为**:读**剥掉注释**的源码(注释里出现的调用不是调用);
|
||
- 断**"理由写清了没"**:读**原文**(理由就在注释里)。
|
||
本仓真踩过:用剥离注释的读取去断"注释里写了为什么用 Canvas",永远红。
|
||
|
||
## 5. 取片段按行/按块,别用偏移算术两头夹
|
||
|
||
`src.indexOf('build() {')` 会撞上文件里更早的同名成员;偏移差一个字符会把最后一行拦腰截断
|
||
(现象是"这行只剩 57 个字符"这种看着像文案、其实像切片的怪事)。
|
||
取"某个成员的正文"要按行扫到下一个同级成员,并加一条自检断言切片没跨到别的成员上。
|
||
|
||
## 6. 判据要接线,扫描范围要有下限
|
||
|
||
- 新增判据文件必须进 `test/run-all.mjs` 的 `SUITE`(run-all 的**自检 2** 会直接红);
|
||
- 扫目录的判据要断言"**至少扫到 N 个文件**",并列出必须包含的子目录
|
||
(`pages/` `common/` `model/` `api/` …)—— 目录改名/只扫一个子目录会让判据变成
|
||
**空判据而依然全绿**。
|
||
|
||
- **验证要按真实入口跑**:`npm test` 走的是 `node test/run-all.mjs && vitest run`。
|
||
用 `node --test test/run-all.mjs` 去验时,runner 只报"0 个测试"、**把 runner 内部的
|
||
`process.exit(1)` 吞掉**,于是"变异后依然 exit 0"看起来像判据失效(本仓刚踩过这一次:
|
||
自检 3 其实是好的,是我用错了入口去验它)。**验判据要模拟用户/CI 真正跑的那一行。**
|
||
|
||
## 6.5 涉及**运行时行为**的结论,必须实测过才能写进规范/判据(pi 2026-09-14)
|
||
|
||
同一个错在本仓犯了两次,方向相反但错法相同:**从观察推断机制、没跑**。
|
||
|
||
- 我从"`node --test test/run-all.mjs` 报 0 个测试、退出 0"这个**观察**,推断出
|
||
"runner 吞掉了 `process.exit(1)`" —— 实测下来退出码照传(见 §6 末),我错了;
|
||
- pi 拿我那个结论直接往下建了一个洞("配错 flag = 绿")—— 他也错了。
|
||
|
||
规则:**一次观察只支撑你看到的那一层**。"报告里 0 个测试"是真的,"退出码被吞"是推断的。
|
||
凡涉及运行时行为的结论(退出码、计数、回调时机、事件触发),
|
||
**先贴真实样本再写规则** —— 这也是 §1 的推论。
|
||
|
||
## 6.6 闭环:自报条数 + 每文件期望条数(只增不减)
|
||
|
||
文本证据只能到"**存在**"为止:`test(` / `check(` / `process.exit(1)` 的存在性
|
||
证明"有能红的路径",**不证明它跑过**。反例(现实事故形态:合并冲突把 `check` 的实现改空):
|
||
|
||
```js
|
||
const check = () => {}; // 实现被换空
|
||
check('a', false); // 存在、也执行了,但什么都不会红
|
||
console.log('主题:通过'); // 有输出
|
||
```
|
||
|
||
所以 `run-all.mjs` 现在做三件事:
|
||
|
||
1. 自定义 `check()` 的判据**自报条数**:结尾打一行
|
||
`RESULT pass=<条数> fail=<失败数>`(`node:test` 的判据不用改,已有 `# pass N`);
|
||
2. runner **只解析这个固定 marker**(不猜口语汇总 —— 「窄屏布局:全部通过」里没有数字,
|
||
靠猜数字会误报);
|
||
3. 与清单里登记的**期望条数**比对,**低于 → 红**。
|
||
|
||
棘轮是"只增不减":**加判据不用改那个数**;只有"条数掉了"才红 ——
|
||
那正是要看见的事(顺手删两条判据、某条被跳过、`check` 实现被改坏)。
|
||
代价:**故意删判据时要同步改数字**(这是一次显式的、能被复核的编辑,可以接受)。
|
||
|
||
计数必须写在 **`check()` 内部**:写在调用点或靠扫源码,"实现被换空"就看不见了
|
||
—— 上面那个反例的 `pass` 会是 0,正是靠这一条才有分辨力。
|
||
|
||
> ⚠️ 本节的作者在写完之后**又踩了一次 §3 那条**:变异验证时用 `git checkout -- <文件>`
|
||
> 还原,把尚未提交的改动(刚加的 marker)一起抹掉了。**规矩写下来不等于会遵守**;
|
||
> 变异前先 `cp` 备份,从备份还原。
|
||
|
||
## 7. 判据要钉用户真正会点的那一层
|
||
|
||
(移交信里交代的头号纪律)判据通过了但用户点不到,等于没做。
|
||
所以断言尽量落在"用户会触发的那个入口/那条路径"上:
|
||
例如"点同意/拒绝后列表要变"要钉到那条链路上,而不是钉"某函数存在"。
|