501 lines
34 KiB
Markdown
501 lines
34 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}` 窗口。
|
||
下次碰那个文件时按本节形状改成"取规则体",不要在那里再加一条注释算了。
|
||
|
||
### 1.5 两张表各缺一半时,必须**按 id 联接**,不能按相邻关系配对(pi 2026-09-14)
|
||
|
||
读 SDK 的系统色时踩到的:`sysResource.js` 只有**名字→id**,`resources.txt` 只有 **id→值**,
|
||
而后者把名字与值排得并不总是紧邻(我第一遍按文本邻近关系读,直接串了行,
|
||
把 `ohos_id_color_background` 读成了 `ohos_id_color_background_dark` 的值)。
|
||
正确做法:名字查 id、id 查值,**两次联接都基于标识符**;顺便用一张表校验另一张表
|
||
(例如 `_dark` 结尾的名字应当拿到深色值),对不上就说明联接方式错了。
|
||
|
||
这与第 1 条是同一件事:**邻接不是结构**。它同样适用于"我自己写的判据"
|
||
(§6.6 里那条被变异测试抓住两次的窗口式正则就是反例)。
|
||
|
||
## 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. 判据必须能红,而且红的地方要对
|
||
|
||
每条新判据配一次**变异验证**:把源码改成"错的样子",确认它红,并且红在那条上。
|
||
变异没红有两种可能,都要查清:一是判据没覆盖,二是**变异没真的生效**
|
||
(本仓真发生过:变异脚本的锚点不匹配、缩进不对,于是"变异后依然全绿"被当成判据有效)。
|
||
|
||
**还原纪律(pi 2026-09-14 纠正了我的写法,这条更好用)**:
|
||
我原来写的是"变异后不要用 `git checkout` 还原"——**那是治症不治因**。
|
||
真因是:**被还原到的那个状态还没提交**,于是 `checkout` 把未提交的改动一起抹掉了
|
||
(我丢的是一个刚加、尚未提交的 marker)。
|
||
可执行的形状是改**操作顺序**,而不是记一条禁令:
|
||
|
||
> **任何破坏性还原,都要求"将被还原到的那个状态已经在某个提交里"。**
|
||
|
||
- 所以:**变异前先把基线提交掉**(哪怕是个很小的提交)。之后 `git checkout -- <文件>`
|
||
只可能还原到已提交状态,不会连带抹掉未提交的东西;
|
||
- 要更稳就 `git worktree add` 一个干净副本,在副本里变异,主工作区完全不碰;
|
||
- 备份(`cp` 到 `/tmp/*.bak`)仍然可以,但它靠的是"人记得备份"——顺序改对了则不依赖记性。
|
||
这跟"记得打 marker"改成"计数写在 `check()` 内部"是同一招。
|
||
|
||
### 变异红了,还要看**红在哪**(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)一起抹掉了。**规矩写下来不等于会遵守** ——
|
||
> 所以 §3 已经把那条禁令改写成"**先固化基线、再破坏**"的操作顺序(pi 指出真因是
|
||
> "被还原到的状态还没提交",不是 `checkout` 这个命令有罪)。
|
||
|
||
### 共享 helper:`test/lib/checks.mjs`(新判据请用它)
|
||
|
||
```js
|
||
import { check, finish } from './lib/checks.mjs';
|
||
|
||
check('这条判据的名字', 条件, '失败时给人看的细节');
|
||
finish('标签'); // 打汇总 + `RESULT pass=N fail=M`,有失败则退出码 1
|
||
```
|
||
|
||
计数只可能发生在这个模块内部,所以用它以后:
|
||
**漏打 marker** 与 **计数写错位置**(导致 `check` 实现被改空也看不见)这两类
|
||
在新判据文件上**不可能发生** —— 不用再靠记性。
|
||
`markdown-xss` / `narrow-layout` 已改用它;其余 10 条保持原写法(`run-all` 的 marker 检查已覆盖)。
|
||
|
||
### 失败信息要**自带修法**(pi 2026-09-14)
|
||
|
||
受众不只是读过这份规范的人:并发写 WebUI 的 agent 新加判据时**不会打开这份文件**,
|
||
看到套件红的第一反应很可能是"套件坏了" → 删自检或往清单里塞豁免。
|
||
所以 `run-all.mjs` 现在在"没找到自报条数""条数掉了"这两条错误里直接写出
|
||
**可照抄的修法**与样板文件路径。
|
||
|
||
> 原则:**red 是那个人一定会看到的东西,文档不一定被打开** —— 错误信息是最省成本的交接面。
|
||
> (验证方式:真删掉一条判据的 marker 行跑一遍,确认错误信息能照抄执行。)
|
||
|
||
## 6.7 判据的作用对象是**值/行为**时,不要退化成对源码形状的匹配
|
||
|
||
(pi 2026-09-14 用反例钉死这条,起因是我写的一条"取键函数必须把账号拼进键"的判据连改三版。)
|
||
|
||
那一族判据的形状是:*去源码里找某个写法*。它**永远差一个反例** —— 我实测过的三版:
|
||
|
||
| 版本 | 判什么 | 用什么骗过去 |
|
||
| --- | --- | --- |
|
||
| 一 | 整段(含签名)里同时出现 `accountId` 与常量 | `return STORAGE_KEY_PREFIX;` —— 参数表里的 `accountId` 就满足了 |
|
||
| 二 | 函数体里两者**离得近** | `return accountId ? PREFIX : PREFIX;` —— 提了一下没用 |
|
||
| 三 | 函数体里**同一个表达式**既含常量又拼接账号 | `const k = PREFIX + accountId; return PREFIX;` —— **拼了但没返回**(这一版我实测仍然全绿) |
|
||
|
||
三版都在判"源码里有没有那个形状",而缺陷是"**算出来的值对不对**"。闭合的形状是**行为判据**:
|
||
|
||
```ts
|
||
strictEqual(storageKey('a') !== storageKey('b'), true);
|
||
strictEqual(storageKey('a').includes('a'), true);
|
||
```
|
||
|
||
这两行对"拼了没用上""拼了又丢掉""换个名字的退化"都红,且不误伤合法重构;
|
||
正则那条的长期效果只剩"将来一次无害重构给你一个假红"。**所以:能跑的值就用跑的,
|
||
正则降级成顺带看一眼,或者直接删。** 删的时候把"为什么删"和反例写在原地,
|
||
否则下一个人会好心把它加回来。
|
||
|
||
**这条规则的适用边界(别过度推广)**:
|
||
- 判"**代码里有没有这个调用/这个来源**"(例如"写缓存只许用 `storageKey()`,不许写死键名")
|
||
—— 这是**来源**约束,不是值对不对,正则在这里是合适工具;
|
||
- 对象**跑不起来**时(`.ets` 在本机没有运行时:编译要 hvigorw、运行要设备,
|
||
而设备在这条链上不可用,见计划文档 §7.21、以及本文件 §6.8 的到期机制),
|
||
静态匹配是唯一可用的手段 ——
|
||
但要把"这只证明形状、不证明值"写进判据的说明里,别让它冒充行为验证;
|
||
- 与"自报条数 < 登记条数"同族:**静默放行**是这类判据最危险的失败方式。
|
||
|
||
#### 6.7.0 可机检的分流规则:**值 → 行为,来源 → 静态**(pi 2026-09-14 要求写硬)
|
||
|
||
上面那段说清了"结论",但没给下一个人一条**照着走就行**的规则 —— 于是仍然靠感觉。
|
||
按对象分流,只有两类:
|
||
|
||
| 你要约束的东西 | 只能用哪种判据 | **为什么只能是它**(不是风格问题,是覆盖问题) |
|
||
| --- | --- | --- |
|
||
| **值算得对不对**(键拼错没有、数字对不对、"点两下真的切换了吗") | **行为判据** | 行为判据只覆盖它**跑到的路径**,所以它**原理上判不了**"有没有别的路绕过去" |
|
||
| **有没有别的路绕过去 / 值必须来自某处**(不许自己写死键名、命中区必须引用共享常量) | **静态判据** | 它要的是**全程序可达性**,只有读代码能回答"还有没有第二个写入口" |
|
||
|
||
两条推论,写成一句话记住:
|
||
|
||
- **静态判据判"值"永远差一个反例** —— §6.7 那张表里我三版各被穿透一次,就是这个;
|
||
- **行为判据判"来源"永远差一条路径** —— 你跑的那条路对了,不等于没有第二条路;
|
||
- 所以两者**不是强弱关系,是分工**。缺任一条,那一对判据就是**假判据**(看着有,其实漏一半)。
|
||
|
||
**照这条规则回看本仓已有的一对样例**(P5 底栏命中区,正好是"值 + 来源"的完整配对):
|
||
|
||
| 判据 | 类型 | 内容 |
|
||
| --- | --- | --- |
|
||
| `③ 命中区 ≥44vp` | **值**判据 | 从 `NavItems.ts` **导入**数值判(不是正则猜源码) |
|
||
| `③ 命中区的应用点` | **来源**判据 | `.ets` 里**不许自己写 `32`**,必须引用 `NAV_ITEM_MIN_HIT` |
|
||
|
||
这两条合起来才闭合:只判值 → 组件自己写 `32` 照样"值是对的";只判来源 → 常量被改成 `20` 也没人管。
|
||
(写新判据时先问一句"我这一对齐全吗",比事后补更省事。)
|
||
|
||
### 6.8 静态判据是**欠账**,必须能到期(`UNBLOCK` + `RESULT static=N`)
|
||
|
||
pi 2026-09-14 指出:规范里的"**暂时**"不是一种状态,是一个**待办** —— 没有任何机制会回来读它,
|
||
于是它永远留在原地。改写成能自己到期的形状(已实现,见 `test/run-all.mjs`):
|
||
|
||
1. 只能验形态的判据登记在 `run-all.mjs` 的 `STATIC_ONLY` 里,每条**必填到期前提**
|
||
(`UNBLOCK`),且前提必须是**可机检的**(如"设备可用"这种探针),不是一句陈述
|
||
(写成 `'vibes'` 这类未知前提 → 套件当场红);
|
||
2. 汇总打 `RESULT static=N` —— 这是**欠账余额**,涨了要看得见;
|
||
3. **前提一旦为真,这些判据自动变红**(实测:把探针改成恒真 → 五条登记判据同时报"到期")。
|
||
这就是"暂时"的到期机制:设备可用的那天,欠账当场显形,不等人想起来。
|
||
|
||
它是"自报 0 条 < 登记条数"的**时间版本**:那条管"判据还在不在",这条管"它该升级了没有"。
|
||
|
||
### 6.7.1 附:**探测器**与**门**不是一回事
|
||
|
||
同一次讨论(pi 2026-09-14 §2)里还有一条:`dist` 比 `src` 新**只说明"src 改了而产物没跟上"**,
|
||
不说明"产物是从当前 src 构建的"。实测:构建失败但**已经碰过** `dist/index.html` 时,
|
||
那条判据**是绿的**(我跑过这个变异,它抓不到)。真正的门是**喂退出码**:
|
||
`set -euo pipefail` / 不接管道 / 看 `PIPESTATUS`。两者不互替 ——
|
||
门负责"失败就别产出",探测器负责"产出跟上了没有"。
|
||
本仓的门是 `scripts/release-linux.sh`(构建失败即停,判据注入失败构建验过),
|
||
探测器是 `test/build-stamp.test.mjs`。
|
||
|
||
## 7. 判据要钉用户真正会点的那一层
|
||
|
||
(移交信里交代的头号纪律)判据通过了但用户点不到,等于没做。
|
||
所以断言尽量落在"用户会触发的那个入口/那条路径"上:
|
||
例如"点同意/拒绝后列表要变"要钉到那条链路上,而不是钉"某函数存在"。
|
||
|
||
## 8. 在**共享工作区**上做归因:别动别人的未提交改动
|
||
|
||
(pi 2026-09-14 §4 指出,我当天正是这么干的。)
|
||
|
||
事情经过:`narrow-layout` 出现 3 条红,我用 `git stash push -- src/components/MailView.tsx`
|
||
把**并发写作者的未提交改动**暂时收起来,看红是否跟着消失,再 `stash pop` 放回去。
|
||
结论是对的,**方法不行**:
|
||
|
||
- `git stash` 会**写**共享工作区 —— 那不是我自己的文件。即便随后 `pop` 回来,
|
||
这中间任何一次崩溃、冲突、或对方恰好跑一次 `git add -A`,丢的是**别人的工作**;
|
||
- 它还只影响 **tracked** 文件:对方的 untracked 新文件照样留在原地,
|
||
于是"stash 之后干净了"本身是**假的干净**,归因结论也可能是假的;
|
||
- 同一工作区里同时有别的写入者时,任何"我来把树弄干净一下"的动作都在赌别人的东西不丢。
|
||
|
||
**该用的手段**(都只读、不碰工作区):
|
||
|
||
```bash
|
||
git show HEAD:client/electron/src/components/MailView.tsx > /tmp/mv.head.tsx # 拿基线对比
|
||
git worktree add /tmp/attr HEAD # 或者在独立工作树里复现
|
||
```
|
||
|
||
或者直接问写那半代码的人 —— 共享工作区里"谁改的"根本不是能从树上读出来的信息
|
||
(这也是为什么 `BUILD_INFO.json` 要记 `gitRev`/`gitDirty`:产物自带来源,比事后猜强)。
|
||
|
||
**一句话**:归因手段的选择标准是"**会不会让别人的东西处于危险里**",不是"哪条命令最快"。
|
||
|
||
## 8.1 共享树上不要用 `git stash` / `--autostash`(同一台机器,自动化了而已)
|
||
|
||
`git stash` 与 `git rebase --autostash` 是同一族:**把工作树里全部未提交改动收走,稍后再放回**。
|
||
在共享树上这意味着**别人未提交的改动**也被收走了 —— 冲突或中断时它们可能**留在 stash 里**
|
||
而不是回到工作树,而 ta 那边只会看到"我的改动不见了"(pi 2026-09-14 指出)。
|
||
|
||
纪律(照 §8 那条"stash 只影响 tracked 文件 ⇒ 树干净是**假干净**"升级):
|
||
|
||
1. **优先 `git worktree add`**:在一棵干净的工作树里做 rebase/改写,碰不到任何人;
|
||
2. 必须原地操作时:**先与并发写者约定窗口**("我会 rebase,请先停手"),或明确告知;
|
||
3. 事后**核对别人的文件还在**(`git status` 里该有的 WIP 还在原处),别只看自己的改动回来了。
|
||
|
||
## 9. 已知限制(登记处)
|
||
|
||
判据的**能力边界**必须写在这里,否则它的长期结局只有两个:被当成"它就是全能的"而误用,
|
||
或者被反复问"为什么它读不懂"。
|
||
|
||
**9.1 标识符只解析一层**(pi 2026-09-14;实现见 `appearance-defaults.test.mjs`)
|
||
|
||
需要比对"某个常量两边是否一致"时,判据会跟一层标识符(例如 `const R = TOKENS.radius`)。
|
||
**再深就不跟了**:那不是"读一个常量",而是"执行 Go/TS",判据会直接报"读不懂"并要求人看一眼。
|
||
后果:如果常量经过两层以上转发,判据会红(而不是误判为不一致)—— 这是有意的取向:
|
||
**宁可报读不懂,也不猜**。要覆盖两层,正解是把值写进一个显式的契约文件,而不是加强解析。
|
||
|
||
**9.2 静态判据的到期前提是"本工作区能装能点设备"**
|
||
|
||
5 条鸿蒙侧的判据只能验形态(`RESULT static=5`)。前提一旦成立,套件**自动变红**
|
||
(探针三值:可用 / 不可用 / 拿不准→红)。到期报文里会列出需要放行的目录(在工作区外)。
|
||
|
||
## 10. 缺省语义登记处(不是"默认值",是"缺了意味着什么 + 谁批准")
|
||
|
||
pi 2026-09-14 提的形状:**不是无条件 fail-closed** —— 缺字段可以是"宽"也可以是"窄",
|
||
**不可接受的只有"缺了却没人知道它意味着什么"**。所以这里登记的是缺省**语义与方向**。
|
||
理由要带上:反例来自 HomeAgent 内核自己的 `PluginManifest.Capabilities`,
|
||
它的注释写明"省略或为空 = **不受限**",因为 17 个存量清单都没有这个字段,
|
||
若把空声明当最小权限,它们会全部静默失去 IO 注入与记忆读写。
|
||
|
||
| **未知的预设 id**(`layersFor`/`normalizePreset`) | **静默替换为 `aurora`** | **宽(静默)** | **待批准**(不是"无人类批准"就收尾):这是实现时的默认行为(`model/Wallpaper.ts` 随该文件引入,`git log --diff-filter=A` 可查出处),本行是补登记。**已进欠账余额**:`docs/DEBTS.json` 的 `unknown-preset-approval` —— 到期前提是"有人追认或驳回『未知 id 显示 aurora 而不是空白』这个方向",**我作为实现者不能自己追认自己**(pi 2026-09-14:登记要落到"谁负责"或"什么时候还",不能停在"我们知道")。意图(写在这里,不冒充批准):背景不该因未知 id 变成空白。**这是"静默给别人的档",不是"给一个安全的空值"** —— 用户不会看到错误,但会看到**别人的档**;替换发生时会留下痕迹(`BackgroundPlan.presetSubstitutedFrom`,页面据此打日志,判据见 `harmony-presets.test.mjs`) |
|
||
| 只写不读的字段:鸿蒙 `bgBlur` | **写进去、存下来、同步它、映射函数也写好了,但没有任何消费点**(映射本身**已判**) | 无(当前无效果) | 出处:`model/Appearance.ts:39,52,88,104,133`(clamp + 合并)+`common/AppearanceStore.ets:142-195`(同步);**消费侧 0 处**。⇒ **这里的结论更正过一次**:我原先写"不存在映射表 ⇒ 钉映射判据是假判据",**那是错的** —— 映射表 `blurStyleFor(bgBlur)` **早就在 `model/Appearance.ts` 里**(文件注释还写着「判据可以直接跑它」),只是**没有任何调用点**。发现方式值得记下来:pi 要求把豁免从"按文件"改成"**文件 + 次数**",逐处核对出现次数时它才被翻出来。**现在的登记**:映射表存在且**已有行为判据**(分档边界 0/8/20、单调性、NaN 不许落到最厚档);"缺的是调用点"这件事由消费侧计数判据管(登记值 0),两个问题分开判 |
|
||
| 只写不读的字段:WebUI `LEGACY_BACKUP_KEY` | 删旧值前**另存一份**,没有任何代码读它 | 无(有意为之) | `backgroundStore.ts:200` 的注释写明目的(否则它会变成新的继承源)。**登记在此是为了审计能一次找全** |
|
||
| 邮件的 `permission_mode` | **不写、不改档**(不是写默认档) | 窄(fail-closed) | 本仓 2026-09-14:缺字段被 `\|\| 'workspace'` 兜成窄档,等于"一个 bug 以正常形态活着";守卫见 `plugins/*/lib/permission-mode.js` 的 `modeForStateWrite` |
|
||
| HomeAgent `plugin.json` 的 `sdk` | **内核不读**(字段只对人有效) | 无(不是语义,是文档) | 内核 `internal/plugin/manifest.go` 的结构体里没有该字段;`registry.go` 的 `loadOne` 只用 `NameZh/NameEn` |
|
||
| HomeAgent `plugin.json` 的 `capabilities` | **不受限** | 宽(fail-open) | 内核注释明写理由:17 个存量清单都没有它,空声明当最小权限会让它们**静默降级** |
|
||
|
||
## 11. 相位:门不许挂在它判不了的相位上
|
||
|
||
**规则(可机检)**:**部署门禁只允许读"待安装的产物 + 目标机状态",不许依赖"源码树是最新构建的"这个前提。**
|
||
一条门如果在某个相位**恒红或恒绿**,那不是门的问题,是**相位挂错了**。
|
||
|
||
两个相位问的是不同的问题:
|
||
|
||
| 相位 | 问的问题 | 判据 | 判法 |
|
||
| --- | --- | --- | --- |
|
||
| 构建 / 发布(`npm test`、`--release`) | 我要产出的东西,是不是从**当前源码**新鲜构建的? | `packaging`、`build-stamp`、`releaseCandidate` | **重新计算**(比对 dist/产物与源码树) |
|
||
| 安装(`deploy/install.sh`) | **别人已经产出的**这个东西,能不能装到这台机器上? | 产物自证(`gitRev`/`releaseCandidate`) | **读它自己说的**(不重算) |
|
||
|
||
实现:`run-all.mjs` 用 `AGENTMAIL_CRITERIA_PHASE=install` 切相位;每条判据在 `SIDES` 里登记它读的
|
||
哪一侧,`install` 相位里出现 `SOURCE` 侧的判据**直接红**;被跳过的判据会在汇总里**点名打印**
|
||
(不静默丢)。
|
||
|
||
三个真实实例(同一天撞到,都不是巧合):`check-shared-libs`(从 `153985e` 起在部署路径恒红)、
|
||
`packaging`(前端一改就卡死 —— 部署路径不重新打包)、`HOME: unbound variable`
|
||
(在**所有门禁跑完之后**才炸:最贵的位置)。
|
||
|
||
## 12. 「与 X 一致」的前置条件:X 侧那个值自己得有判据钉住
|
||
|
||
**规则**:凡判据说"本端的值与 X 端的值一致",先确认**X 端那个值在 X 端被判据钉着**。
|
||
否则它测的是"**我抄的那一份**",而不是"两边一致":X 端改一下,两边悄悄分叉,
|
||
而没有任何东西会红 —— 判据会一直绿着,因为它比较的是自己的副本。
|
||
|
||
这与"两张表各缺一半时必须按 id 联接,不能按相邻关系配对"是同一族:
|
||
**比较的对象必须是权威来源,不是手边可得的那个副本。**
|
||
|
||
真实例子(2026-09-14):鸿蒙 P6 的手势阈值(水平 ≥40px、≥1.5× 垂直、<600ms)
|
||
在 `client/electron/src/components/CalendarView.tsx:204` 是**裸字面量**,
|
||
没有任何判据引用它 ⇒ 当时写"与 WebUI 一致"是把**当下的巧合**当契约。
|
||
处理:先标"待两边对齐",再让两边各自钉住数值本身。
|
||
|
||
### 10.1 两类"看起来被接住了、实际没有落点"的东西必须登记在**同一处**
|
||
|
||
上面表里最后两行是同一类形状:**值被搬运/保存,但没有落点**(一个是鸿蒙侧的 `bgBlur`,
|
||
一个是 WebUI 侧的 `LEGACY_BACKUP_KEY`)。分居两处的话,将来审计容易**只找到一处就以为找全了**
|
||
(pi 2026-09-14 指出)。所以两类都登记在这张表里:
|
||
|
||
1. **缺省语义**:缺了意味着什么、方向是宽还是窄、谁批准;
|
||
2. **只写不读**:谁在写、为什么还留着、什么条件下必须补判据。
|
||
|
||
**判据形态(想做但还没做,写在这里避免当成已完成)**:凡登记为"只写不读"的字段,
|
||
写侧必须能指出"有意为之"的理由与批准人 —— 这样"搬运了但没人读"就不会以**静默**形态长期存在。
|
||
现在这一条还只是**文档级**约束(人工核对),没有机器判据去强制它;
|
||
要变成"做错会红",得先有一张字段清册可扫。
|
||
|
||
### 10.2 版本偏移下的可见后果:旧客户端遇到"新档"会静默显示 aurora
|
||
|
||
跨端耦合的常态是**两端不同时上线**:服务端/WebUI 先加了第 7 档,而鸿蒙还是旧构建 ——
|
||
旧鸿蒙的 `layersFor` 认不出新 id,按上面的缺省语义**静默显示 aurora**,
|
||
于是"用户以为自己选的是新档"。**这不是 bug,是这条缺省决策的可见后果**(已按 pi 2026-09-14
|
||
的建议同时记进 `docs/HARMONY-ALIGN-PLAN.md` 的差异表)—— 不记,将来会被当 bug 报,
|
||
而查到最后发现"这是我们批准的"。
|
||
|
||
## 13. 注释里可以放**指针**,不要放**断言**(尤其是关于另一端实现状态的断言)
|
||
|
||
**规则**:不要在 A 端的源码注释里断言 **B 端**的实现状态。写"B 端不消费这个字段"这类句子,
|
||
就是把**别处的观测**当成了**本机的契约** —— 它会在 B 端变化的那天变成**假事实**,
|
||
而且没有人会回来改(写的人不在那条链上,读的人无法判断它是否还成立)。
|
||
|
||
**该放在哪**:
|
||
|
||
- **断言留在"事实所属的那一端"**:例如"鸿蒙不消费 `bg_blur`"是**鸿蒙侧**的事实,
|
||
所以它写在鸿蒙侧(`model/Appearance.ts` / `AppearanceStore.ets` 一带)+登记进 §10;
|
||
- 另一侧如果要留痕,只放**指针**:例如"该字段的消费方状态由 <那一端的登记处> 说明"。
|
||
指针不会过期(它指向的地方会自己更新),断言会。
|
||
|
||
**同族**:这条与"两张表各缺一半时必须按 id 联接,不能按相邻关系配对"、
|
||
"'与 X 一致'必须先确认 X 侧有判据钉住"是同一族 —— **比较/引用的一端必须是权威来源,
|
||
不是手边那份可能过期的副本。**
|
||
|
||
## 14. 登记/清册类判据的报错,要同时写"正确修法"和"**最常见的错误修法**"
|
||
|
||
**规则**(pi 2026-09-14 提出):凡是"登记 / 清册 / 次数"类的判据,红的时候读的人**第一反应
|
||
通常是改那个数字**。所以报错信息必须把两件事都写上:
|
||
|
||
1. **正确修法**(该去补什么);
|
||
2. **最常见的错误修法**(别做什么,以及为什么那等于把判据废掉)。
|
||
|
||
例(`harmony-appearance.test.mjs` 的 `bgBlur` 那条):
|
||
|
||
> 停下:那时**必须**先补"px ↔ 材质档位"的映射判据(§10 / 计划文档 §7.12),
|
||
> **而不是把登记值从 0 改成 1**。
|
||
|
||
这与"清册条目的『允许』要给出一条能判的约束"(§11 族)是同一条纪律的两半:
|
||
前一半保证**判据有分辨力**,这一半保证**分辨力不会被"改数字"抹掉**。
|
||
|
||
## 15. 判据必须**自足**:不许通过共享可变状态在判据之间传递结论
|
||
|
||
**规则**:一条判据的结论必须由它自己测量出来。**判据之间不许共享可变状态**
|
||
(包级变量、临时文件、上一条判据留下的标记)。一旦共享,结果就取决于**执行顺序**,
|
||
而顺序依赖的显形方式永远是**假绿**(不是假红 —— 假红会被看见)。
|
||
|
||
真实例子:`debts` 的余额第一版由"SKIP 那一支"写入、由登记判据去读。
|
||
Go 在同一包内**按源文件顺序**跑测试 ⇒ 登记那条先跑就读到 `0` ⇒ 全绿而余额是错的。
|
||
修法:把测量抽成自足函数(`measureMailStatusDebt(t)`),**谁调用谁测**。
|
||
|
||
## 16. "要提醒人的"输出必须走**默认路径**(不能只活在 `-v` 后面)
|
||
|
||
**规则**:余额 / 自报 / 提醒类信息必须出现在**默认命令**的输出里;只有**诊断细节**
|
||
才可以藏在 `-v` 后面。
|
||
|
||
真实例子:我把欠账余额打在 `TestMain` 收尾 —— 而 `go test` 对**通过**的包
|
||
**不打印包的输出**(只在失败或 `-v` 时打),于是常态运行里一个字都看不见:
|
||
"可打印的余额"变成了"看不见的余额",与不显形等价。
|
||
修法:可见的那份挪进 electron 套件的 `RESULT` 行(默认路径),Go 侧只做同源比对。
|
||
|
||
**配套动作(pi 2026-09-14)**:写完一处"要提醒人的输出"之后,
|
||
**去看一眼默认路径的输出** —— 不是看代码,是看它实际打出来的样子。
|
||
|
||
## 17. 变异只证明「注入的样本被抓」,**不证明完备性**;判据的**标签必须等于断言范围**
|
||
|
||
**规则**(pi 2026-09-14):一条判据通过变异验证之后,只能说"我注进去的那一条会被抓"。
|
||
**样本不是完备性证明**。配套的第二半更重要:
|
||
|
||
> **标签比断言宽的判据,会在它没测的那条路上被回滚时给绿,而人信的是标签。**
|
||
|
||
真实例子(`background.test.mjs` 的两条反向断言):名字写着"导航不得变暗 / 不得自叠模糊",
|
||
断言体各只扫一条路。三条逃逸路各注一次变异**全部逃掉**(`.dark .nav-rail{}` 选择器作用域、
|
||
组件 `dark:` 变体、组件 `backdrop-blur-lg`),导航变黑而判据全绿。
|
||
|
||
**两种收法,选一个,别都做**:
|
||
|
||
1. **把标签缩回它真正断言的东西**(推荐:诚实度比覆盖面值钱),并把**未覆盖的路**
|
||
逐条写在判据旁边(各自另立判据时才补);
|
||
2. 把断言扩到标签的范围 —— 成本高,而且极易变成一刀切误红(先例:`bg-chrome-600`
|
||
plain 档徽标)。真要扩,**按文件窄豁免**写。
|