# 判据规范(写判据前先读这份) 这份文件记的是**判据本身的写法**:什么样的判据能红、能红在对的地方、以及不会在"代码完全正确"时乱红。 不是"怎么用 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.1 判据的**存在性**也要有下限:把"必须存在哪几条"锚到判据自己的表之外(pi 2026-09-18) §6 那条只说了"扫目录要有下限"。**同一条规矩换个对象就漏了** —— 这条是它的镜像: `run-all.mjs` 的自检登记表 `SELFTESTS` 原来**没有任何下界**,也没有任何"必须存在哪几个"的 外部清单。于是**把一整条自检(函数体 + 登记 + CLI guard)三处同步删掉是零痕迹的**: ``` 基线: RESULT … checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0 verdict=red 自检段:5 个自检… 删掉: RESULT … checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0 verdict=red 自检段:4 个自检… ``` `red` 与基线**逐字相同**、相关 RED **0 条**,只有报文里 `5` 变成 `4`(而那句读起来像正常)。 最尖的形式:删掉 `exitcodeSelfTest` —— **守着 `UPSTREAM_RC`"真脚本真跑"锚点的那个** —— 同样零痕迹。 ⇒ 我们花两轮建起来的真跑锚点,可以被一次编辑无声撤掉。 **为什么三道防线都看不见它**(它们问的都是"**出现的东西对不对**"): | 防线 | 它比什么 | 整条删除时 | |---|---|---| | 名字级扫描 | 源码里的 `\w+SelfTest` ↔ `SELFTESTS` | 名字**两边都不出现** ⇒ 无从对比 | | "写了但没接线" | `declaredSelfTests(源码)` ↔ `wired` | **两边同时缩小** ⇒ 相等 | | 样本/见证/要求 | 样本表**内部**三字段 | 与 `SELFTESTS` 无关 ⇒ 不受影响 | ⇒ **没有一条问"该出现的东西在不在"。** 所以规矩是: **凡"某张表里必须有哪几项",那几项必须能在**这张表之外**被读到 —— 写在别处的、有人维护的文本里(本文件就是这样),而不是由表自己说了算。** 本仓的落点是:`run-all.mjs` 读**本文件 §6.1**下方那段 ``, 要求它与 `SELFTESTS` 的键**恰好相等**(两个方向都判)。 ★ 为什么锚在**本文件**而不是"再写一张内部清单":本文件是**另一个文件**, 且它自己已被 run-all 的**自检 3**守着(存在性 + 关键条目在)—— ⇒ 链条是"**被外部守着的文本 → flag 名 → guard → SELFTESTS**",锚点不在 `SELFTESTS` 自己身上。 ★ 自查句:**"我这条判据,把它的定义、登记、调用三处一起删掉,谁会红?"** 问不出来,就说明这条判据的**存在性**没有锚点(它只被"内容正确"类判据守着)。 ★ 每项写成 `名字:下界`。**下界 = 这条自检至少该打出多少条断言**(`^(ok|RED) ` 行数)。 语义是 §6.6 那种**棘轮**:**只增不减** —— 加断言不用改数字,**掉下来才红**。 下界是**行为**读数(自检真跑出来的行数),不是"源码里那句 `console.log` 在不在"。 ★★ 为什么必须有它(**层 6**,pi 2026-09-18 报):上面的契约行只管**存在性**, 而存在性有**两种**失去方式 —— **名字没了**,和 **名字在、里面是空的**。后者零痕迹: ``` 把 `exitcodeSelfTest` 的函数体(14616 字节)换成 `{ return 0; }` (名字、登记、CLI guard、契约行一个都不动) ⇒ red 与基线逐字相同、^RED 0 条 最尖形式:掏空 + 把 `UPSTREAM_RC['baseline-residue']` 改成 99 ⇒ 0 行提到 UPSTREAM 对照(只改值、不掏空)⇒ red=10,真报「UPSTREAM_RC[baseline-residue]=99 与真跑出来的 rc=1 不符」 ``` ⇒ **所有防线问的都是"它在不在",没有一条问"它做了没有"。** ★ 所以下界必须写在**这张表之外**(就是本文件),不能由 `SELFTESTS` 自己说了算 —— 与 §6.1 同一条理由。**数字掉下来 = 那条自检要么被掏空、要么被改得不再检查东西。** ⚠️ 下界取的是**略低于实测值**的余量(实测 3/13/5/13/9),不是"恰好等于实测": 相等的下界会让**任何**一条断言的小改动都变成"必须同步改数字",而留余量只拦 "掉到明显不对"的那种(掏空 ⇒ 0、判据被删掉一半 ⇒ 腰斩)。 ★ 判据只在**该自检自称绿**时才比下界:`exitcodeSelfTest` 在**拿不到降权工具**的机器上 会 `continue` 掉两个案例(那是它**故意的**行为 —— 前提构造不出来就报红,不许静默跳过), 那种机器上条数本来就不同 ⇒ 不做这个区分就会在**别的机器上假红**。 ★ 层 5 与层 6 是**两根轴**:层 5 问"**它还在吗**",层 6 问"**它做了吗**"。 ★★ 而它们共同的教训是:**每根轴的"底"看起来都像整体的底。** "到底了"永远只对**当前那一根轴**成立 —— 这条我等了三轮才学会,写在这里给下一个人。 ### 6.1.1 条数下界**也**是自述 —— 必须再补一层**因果**判据(pi 2026-09-18,层 7) 上面那条下界判据(层 6)的输入是**输出行数**:**数的是它"打了几条",不是它"验了几条"。** 所以把函数体换成"循环打满 13 条空 `ok`、`return 0`"就**恰好绕过**它: ``` 基线: red=9 … verdict=red 假 ok×13: red=9 … verdict=red ← 逐字相同、下界相关 RED 0 条(13 ≥ 下界 12) 假 ok×13 + UPSTREAM_RC['baseline-residue']=99 ⇒ 0 条 RED、0 次提及 UPSTREAM 对照(只改值、不掏空)⇒ red=9→10,真报「与真跑出来的 rc=1 不符」 ``` ⇒ 根因一句话:**下界判据的输入是"自我报告"** —— 判据自己说它做了多少,而它可以撒谎。 **锚点不能落在"被守对象的自述"上,必须落在它管不着的东西上。** ★ 三种"看起来更硬"的替代都被否掉了,理由都是它们仍在自述这一侧: · **黄金输出**(逐字节比对期望输出)= 又一份**手写数据**(本文件刚证过那族可被同步改); · **扫源码找关键词**("源码里有那个断言")= "**提到 vs 就是**"(本文件反复证过它脆); · **毒化环境变量**(该模式下每条自检必须报红)= 把"必须红"的断言写在**自检函数体内** ⇒ **假体不打它**。实测:模拟毒化后假体 `red=9`、下界红 0 —— 与层 7 同一问题。 ★★ **本仓采用的锚点是"外部因果":改坏被守的生产符号 → 那条自检必须红。** ``` · 假体(空 `ok`)在符号被改坏时仍然打 ok ⇒ 它不红 ⇒ **红**(判据抓它); · 真自检读了那个符号 ⇒ 符号坏 ⇒ 它报 RED ⇒ 绿。 ⇒ 这是"自检真的读了那个符号"的**行为**证据,不是它的自述。 ``` 实现要点(`run-all.mjs` 的 `CAUSAL` 表 + 快入口): - 快入口 `--only-selftest=<名>`:实测 **52~696ms/条** (对比 `--X-selftest` 要跑整套 suite 的 4.3~4.9s); - 隔离用 `mkdtempSync` + `cpSync(join(HERE), …)`(**只拷 `test/`,1.6MB / 6ms**)—— 照本仓"隔离要用**最小**夹具"那条; ⚠️ 但**必须**拷整个 `test/`:run-all 的**自检 1/2** 要 `readdirSync(test)` 与 `SUITE` 对齐, 缺文件会让它在跑自检**之前**就 `exit(1)` ⇒ **读到的是假红** (我第一次少拷东西时就这么读错过:`rc=1` 看着像"改坏生效",其实是清单自检停了整套); - 关系表(自检 ↔ 它守的符号)也是手写数据,**但它被真跑锚住了**: 表里每一对都必须实测"改坏了它真会红";把符号写错(写成它不读的)⇒ 那条自检改坏后**不红** ⇒ 红。 ⇒ 表自己也被因果判据守着,这是它与"下界数字"的**关键区别**。 ⚠️ **这句话上面那半句是错的**,见 §6.1.2 —— 被守的只是"表里的锚文本唯一",**"表里该有几条"当时没人问**。 - ⚠️ 锚文本的唯一性必须在**除去 `CAUSAL` 表本身**的源码里数, 否则会**匹配到表里那一行字面量**(我第一版就踩了:probe/verdict 立刻报"不唯一", 真因是表自己,不是生产里有两处)。★ 又是那句:"提到"与"就是"在文本上长得一样。 ⇒ 可推广成一句:**任何"在源码里找自己写的那串字"的判据,都必须先把自己那串字挖掉** —— 否则它就是在给自己数数。 ### 6.1.2 **守着锚的那张表,自己没人守**(pi 2026-09-18,层 8) §6.1.1 我写"表本身也被真跑锚住了" —— **只对了一半**。被守的是"锚文本在源码里唯一", 而 **"表里该有几条"没有任何东西在问**。实测(`be49e07`): ``` CAUSAL = [](整表清空) ⇒ red=10,与基线**逐字相同**、因果 RED 0 条 只留 1 对(删掉另外 3 对) ⇒ red=10,那三条自检**从此失去因果锚**、无任何提示 CAUSAL = [] + 假 ok×13 ⇒ red=10、因果/空壳 RED **0 条** ⇒ **层 7 的攻击被完整放回来了,而代价从"改函数体"变成"删掉表里一行"。** ``` ⇒ 根因:**锚点不能落在"被守对象的自述"上**(层 7), 而这里是 **"守着锚的那张表自己的内容"没有锚** —— 上一轮是"判据自述",这一轮是"**锚表的自述**"。 ★ 新轴:**守护者自己进入了被守集合** —— 前几层问的都是"**被守的东西**怎样", 层 8 问的是"**守它的东西**怎样"。 **修法:覆盖面必须有一个不在表里的来源。** 这里用**两个**,都是已有的: - ① `contractEntries` —— `CRITERIA.md` 契约行(**外部文件**,与层 5 **同一个锚点**; 一个外部来源同时守两件事,不必再写第二张表); - ② `SELFTESTS` 的**键**(源码结构)—— 让"新加了一条自检却没配因果对"也能被抓。 ⚠️ **空真的坑**:只写"每个 `CAUSAL` 项都对应一个真自检"(反向)**挡不住 `[]`** —— `[]` 恰好满足那个空真("每个"在空集上恒真)。⇒ 必须**正向**要求覆盖。 ★ 与本仓那句同族:**"判据存在" vs "判据在路径上"**;这里是 **"表非空" vs "表够长"**。 ⚠️ **两个命名空间别混**(我第一版就混了,被自己刚写的判据当场抓住): `wired` 装的是**函数名**(`probeSelfTest`),`CAUSAL`/契约行装的是**旗标名**(`probe-selftest`)。 拿前者比后者 ⇒ **5 条全报"幽灵名"、另 5 条全报"没有因果锚"**(一次红 2 条、方向相反)。 ⇒ 判据没错,是我把两套名字当成了一套。**正确的来源是 `SELFTESTS` 的键**(旗标名)。 ★ 教训:**同一个东西在两个地方有两套名字时,"名字相等"不是可传递的**。 ### 6.1.3 **判断据的那个人,也可信吗**(pi 2026-09-18,层 9) §6.1.1 我为把因果判据的成本从 4.3s 压到 52~696ms 加了快入口 `--only-selftest`, 而它当时自带**第二张分发表 `T`**(`{'probe-selftest': probeSelfTest, …}`)。 我在上一封把它如实标成"**副作用、不是防线**" —— **层 9 就长在那个副作用上**。实测: ``` T 全换成 () => 1 桩 ⇒ red=9,与基线**逐字相同**、因果 RED **0 条** T 全桩 + 假 ok×13(层 7) ⇒ red=9、**红清单 diff 0 行**(层 7 的攻击完整放回) ``` 为什么因果判据看不见:它观察孩子**只经 `T` 的分发**,而它比的"孩子退出码" 正是 `T` 提供的 ⇒ **判据的观测通道被它自己要观测的对象控制着。** ⇒ 层 7 问"**被测者**的话可信吗",层 9 问"**测量者**的话可信吗"。 ★ 也是本仓老形状的又一次:`T` 与 `SELFTESTS` 是**同一事实两份实现** (`stripStrings` 兄弟副本、`blurStyleFor`、`parseSkips` 抄副本…… 这次是**分发映射表**)。 **修法一(消除"两份实现"本身):`T` 不该单独存在。** `SELFTESTS` 上移成**唯一的一份登记**,快入口从它派生 ⇒ "桩化 `T`"这个动作**没有落点**。 ⚠️ 五个自检都是 `function` 声明(**提升**)⇒ 引用它们没有 TDZ 问题; 但**调用**它们仍必须排在 `DIAG` 等 `const` 表之后(那个 `if` 留在原位)。 **修法二(更硬,堵形状而不堵那一个洞):给因果判据加对照 —— 未改坏时必须 rc=0。** 只问"改坏 ⇒ 必须 rc=1"的话,**任何恒 rc=1 的通道**(表、wrapper、环境变量…)都能让它绿。 ⇒ 加一问:**没改坏的时候它必须 rc=0**。通道坏了 ⇒ 这一问就红。实测: 把快入口改成恒 `exit 1` ⇒ **5 条 RED**(`观测通道本身有问题`); 而改坏生产符号那一半仍照常红。 ★ 这是"锚点必须落在被守对象管不着的地方"的**行为**版本: **我不需要知道通道长什么样,只要求它在两种输入下给出两种答案。** ⚠️ 代价:每对自检跑**两次**(默认跑 5.1s → **6.8s**,可接受)。 ★★ **一般化(我认为这是这条线上最值得记的一句)**: > **每加一条防线,要同时回答两问:** > ① 它守什么?(设计意图) > ② **它自己新增了哪些"必须被守"的东西?**(副作用) > > 层 9 就是第②问没问的产物 —— `--only-selftest` 是**性能上的净收益**, > 却往被守集合里加了一个**没人守**的成员。 ⇒ 这与"每根轴的底看起来都像整体的底"是同一现象的两种说法: **防线是会长出新的被守对象的**,所以你永远能看到一个新的"底"。 ★ 层级表:写法(4) → 存在性(5,它还在吗) → 行为(6,它做了吗) → 自述(7,被测者可信吗) → 锚表(8,锚还在吗) → **观测(9,判断据的那个人可信吗)**。 ### 6.1.4 **对照能被"按内容分流"的通道骗过** —— 红必须是"报出来的",不是"退出来的"(pi 2026-09-18,层 10) pi 报的层 10:上一版那条"对照"(未改坏必须 rc=0)能被一个**读自己源码**的通道骗过。 通道在快入口里加:读自己的源码、发现"我被改坏了"(源码里出现了改坏标记)就 `process.exit(1)`。 它在**两种输入下都答对**:原样走真实现 ⇒ 绿;被改坏 ⇒ 自己退 1 ⇒ **对照通过**, 而层 7 的假体照旧静默。实测:**红清单 diff 0 行**。 ★ 根因:**对照只问"输入→输出映射是否非平凡",没问"那个输出是不是被守对象产生的"。** 通道可以**自己**产生那个"红"(靠检测自己的源码),于是它既是仪器又是被测者 —— 层 9 是"仪器归被测者管",层 10 是"**仪器可以自己造假象,而它造得让对照满意**"。 **修法:把"红"从退出码改成可归因的报告。** · ① 干净孩子必须 **rc=0 且 0 条 `RED ` 行**(健康的自检不该喊红); · ② 改坏后的孩子必须 **rc=1 且 ≥1 条 `RED ` 行**(它得说出**为什么**)。 · ⇒ 只会 `process.exit(1)` 的通道**什么也没说** ⇒ ①过②不过 ⇒ **红**。 ★ 一般化:**"它红了吗"必须落在"它说了什么"上** —— 退出码是**任何人**都能给的(连崩了都给),而 `RED ` 行是被守对象**自己的判断**。 ⚠️ **已知残留(不声称层 10 封死)**:通道打一行**假 `RED `**(`console.log('RED …')`) ⇒ 同样两问都答对。实测 `red=9`、0 条因果红。**成本只高了一行 `console.log`。** ⇒ 这与 pi §二的根因完全同形:**通道能分辨两种输入,就答对两问。** · pi 的 ① 方向(改坏**孩子读的外部依赖**而非孩子自己的源码)能穿透这个分流 —— 但我实测:**只有 `exitcode-selftest` 读外部 `summary.py`**;另 4 条的生产符号 (`probeIsDue`、`parseSkips`、`verdictOf`、`summarizeMutants`)都在 `run-all.mjs` **自己里**。 ⇒ **① 不 generalize**,它只堵 1/5。 · pi 的 ②(加第三种输入"改坏无关符号")也测了:**定向通道**(只认自己那一个改坏形态) 在无关符号被改坏时仍走真实现 ⇒ rc=0 ⇒ **② 通过** —— 被绕过。 ⇒ ★ **这一串的底(层 5→10)**:每加一层,成本从"一处编辑"抬到更高, 但"多处同步删仍静默"和"通道能分辨输入就答对两问"**性质没变**。 **防线是会长出新的被守对象的,而观测通道最终都能分辨输入。** ⇒ pi 建议到此为止(层 10),我同意 —— 见 §6.1.5。 ★ 层级表:写法(4) → 存在性(5) → 行为(6) → 自述(7) → 锚表(8) → 观测(9) → **归因(10,"红"是报出来的还是退出来的)**。 ### 6.1.5 **这一串的定价:为何到层 10 为止**(pi 2026-09-18,dsh 同意) 层 5→10 六轮,每轮都真实、每轮读数都硬,但**边际收益在递减**,且有实测依据: | 层 | 攻击成本 | 修法后剩余 | |---|---|---| | 5/6 | 一处编辑 | 同步删多处仍静默 | | 7 | 写假体 | 锚点到表外 | | 8 | 删表 | 新自检漏配因果对被②抓 | | 9 | 桩化通道 | 通道恒 rc=1 被对照抓 | | 10 | 读自己源码的分流通道 | **打一行假 `RED ` 仍过**(成本 +1 行) | ★ 三个结论(都实测过): 1. **这一串的价值是校准口径**("锚点必须落在被守对象管不着的地方"), 这个价值在层 7~9 已拿满 —— 层 10 只是在**重复同一课**。 2. **真世界里没有攻击者会这样改代码**:它的威胁模型要求攻击者 同时改生产符号 + 通道 + 假体 + 假 RED 行 —— 这与"直接删契约行"的 真实风险等级相距太远。 3. **留下的东西是可复用的**(§6.1.4 的判据 + §6.1.3 的"每加一条防线回答两问"), **其余的建议停在这里**:不承认它封死,也不再往上爬。 ⇒ **同意 pi:这一串到层 10 为止,不再往上找。** 注意力转回到 到期那 6 条判据(`harmony-appearance`、`harmony-logic`、`cross-client-theme`、 `appearance-defaults`、`harmony-admin`、`harmony-imageprep`)的升级。 ### 6.1.6 ★★ **"孩子管不着的东西"在因果判据里不存在**(dsh 2026-09-18,比"边际递减"更硬的停止理由) pi 在复核层 10 时提出:层 10 的修法(要求 `RED ` 行)**推翻了层 7 的原则** ("锚点不能落在被守对象的自述上")—— 把锚点从"行为"搬回了"自述"。 **这条我核对成立**(`:2674` 的原则与 `:2774` 的修法同住一个文件)。停止理由因此从 "成本 > 收益"升级为**"层 11 的修法必须推翻层 7 或层 10,两者互斥"**。 ★ 但我把 pi 的**建议修法①也测了**,得到一个**比原则互斥更强**的结论: **pi 的 ① 不是逃逸口。** 它主张"改坏**孩子读的外部依赖**(而非孩子自己的源码) ⇒ 通道没有可检的信号"。实测(`exitcode-selftest` + 假体 + 通道): ``` 通道只盯外部 summary.py ⇒ 干净树 rc=0 ✓,改坏外部 ⇒ rc=1 ⇒ ① 有效 通道**同时**盯外部 summary.py + 自己源码 ⇒ 干净树 rc=0 ✓,改坏外部 ⇒ rc=1 ⇒ **① 被绕过**(同刻对照:red 与基线一致、diff 0 行) ``` ★★ 根因(这是这一整串的**真正的底**): > 因果判据的要求是:**改坏 X ⇒ 孩子的判词必须变**。 > ⇒ 这要求 X **因果上游**于孩子的判词 ⇒ **孩子必须读 X**。 > ⇒ 孩子读得到 X ⇒ **孩子能观察 X 被改坏**(同 uid、可读,`summary.py` 是 `-rw-r--r--`)。 > ⇒ **"孩子管不着的东西"被因果判据自身的定义排除掉了。** ⇒ 所以层 7 那句"锚点必须落在**它管不着**的东西上"在这个接口里**没有可满足的解**: · 要"因果有效",X 就必须被孩子读到; · 要"孩子管不着",X 就不能被孩子读到; · **二者不可同时成立** ⇒ 不是"还没爬到",是**接口本身没有解**。 ★ 唯一出路是**换接口**(父进程自己算、或进程外观测)—— 那不是"再修一条判据"。 实测本仓**进程外观测通道 grep 0 处**(`strace`/`inotify`/`atime` 差分全无), 且孩子的输出只有 stdout + 退出码,**两者都由孩子自己产生**。 ⇒ ★ 这与 pi 的"原则互斥"是同一结论的**两个证明**:pi 证明的是**两条原则不能共存**, 我证明的是**这个接口里不存在满足"管不着"又要"因果有效"的对象**。 **两者都指向"停",而后者不依赖"哪条原则优先"的判断。** ⚠️ 顺带记一条**我自己的量错**(与 pi §四 同族,我这一串第 N 次): 我用 `python3 heredoc` 拼通道时,**引号嵌套写出了 SyntaxError** ⇒ 通道**在干净树上就崩**, `rc=1`。我差点把它读成"通道生效"。**是 stderr 里的 `SyntaxError` 救了我** —— ⇒ **"量到 ≠ 量对了"**:任何构造出的攻击,**第一句读数必须是"它在干净树上无症状吗", 且必须看 stderr 而不是只看 rc。** ★ 层级表(**每层都是一根轴**):写法(4) → 存在性(5,"它还在吗") → 行为(6,"它做了吗") → **自述(7,"它说的话可信吗")**。 ## 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)**:写完一处"要提醒人的输出"之后, **去看一眼默认路径的输出** —— 不是看代码,是看它实际打出来的样子。 ### 16.1 ★★ 光进默认路径不够:**结论必须到得了"决定颜色的那一格"**(dsh + pi 2026-09-18) §16 管的是"**打印**有没有走默认路径"。这一条管的是它**下一跳**:输出打了, 但**决定套件颜色的那一格没读它** ⇒ 等于没打。**同一条缝在本仓已出现三次**, 每次位置都不同,所以必须分开记 —— 否则下一个人只会修好其中一例。 **缝的形状**(三次都一样): > `summary.py` 把结论说对了、退出码也说对了,**但没有一条通道把它送到 > "决定 `verdict` 的那一格"** ⇒ 读者看到的是一个**看着完全正常**的读数。 **三个落点**: | # | 落点 | 现象 | 修法 | |---|---|---|---| | 1 | **正则匹配就走不到 `status`** | 盲读时 `summary.py` 照样打 `RESULT mutants=0 …`(匹配)⇒ `status=2` 从没被读;`sp.stdout` 全文件只一处引用 ⇒ 那三行 `✗✗` 一个字都到不了读者 | `summary.py` 盲读改打 **`NO-READING`**(不匹配)+ run-all **先判 `status`** | | 2 | **部分可读**(`chmod 000` 单个 job) | `blind` 为假、数字是真算的 ⇒ 那行**匹配正则且看着正常** ⇒ 修法①**对它无效** | 只有**先判 `status`** 能兜住 ⇒ 两层修法**各治一例**,不是叠保险 | | 3 | **`unlisted`/`ghosts` 退 0** | 警告行**已算出来**,却被 `whyLines: status !== 0 ? whyLines : []` **丢掉** ⇒ 磁盘 13 个 job、套件报 48、"未列入清单"在套件输出 grep **0** 次 | 退出码按**性质**分:`unlisted`/`ghosts`⇒**1**(清单该改=数据);`blind`/`unreadable`⇒**2**(权限=环境);并放宽转印为 `(note \|\| status !== 0)` | ★ **第 3 例的正反两面**(这是它比前两例尖的地方): `unlisted`/`ghosts` **不是**环境问题,是**清单该改**。按 `env-defaults.sh:25` "失败要说清是环境问题…不要让它冒充代码缺陷" —— **反向也成立**: **别让"清单没跟上"冒充环境**。所以第 3 例归 **1** 不归 **2**。 ★★ **为什么会连着踩三次**:这三例的**上游判别都做对了** (`unlisted` 算出来了、`blind` 判出来了、`unreadable` 分出来了), 错的全在**最后一跳到 `verdict`**。⇒ 通用规则: > **每加一条"发现问题就报告"的逻辑,都要问一句: > 它的结论**最终喂给了哪一个决定颜色的变量**?** > 只 `print` 不算到达;要落到 `reds`/`brokens`/`dueFailed`/`selfCheckFailed` 之一, > 或者让退出码变成下一层会读的那个值。 ★ 已锁住:`--exitcode-selftest` 有真跑案例(`['未列入清单 ⇒ 1', …]`,`:1062`)、 `--verdict-selftest` 有判定案例(`['manifest-mismatch(status 1)⇒ 不许绿', …]`,`:1976`)。 **变异验证**(dsh):把 `unlisted or ghosts` 从 `return 1` 改回 `return 0` ⇒ `--only-selftest=exitcode-selftest` 立刻 **rc=1、2 条可归因红** (`未列入清单 ⇒ 1:rc=0(期望 rc=1 … UPSTREAM_RC[manifest-mismatch]=1 与真跑出来的 rc=0 不符)`)。 ⇒ 这条缝**有判据守着**,不靠下一个人再读一遍源码。 **端到端 A/B**(dsh,干净 worktree,同刻对照): | | A:12 文件(清单一致) | B:13 文件(1 个未列入清单) | |---|---|---| | `diag=` | `baseline-stale` | **`manifest-mismatch`** | | "未列入清单" 在套件输出 | grep **0** | grep **1** | | `(summary.py)manifest-mismatch` 红 | **0** | **1** | ### 16.2 ★★ **同一优先级写了两遍 ⇒ 两个顺序**:不变式只在**对角线**上被验过(pi + dsh 2026-09-18) §16.1 讲"结论到不了决定颜色的那一格"。这一节是它的**镜像**: 结论**两个都到了**,但**互相矛盾** —— 报出来的"原因"与"严重度"说的不是同一件事。 **缝的形状**:`summary.py` 里同一个文件有**两份**优先级表,且**顺序相反**: ``` diag(原选择处):unlisted/ghosts 排第一 ⇒ 组合态报 manifest-mismatch rc (原返回处):blind/unreadable/baseline-unrunnable|unknown 排第一 ⇒ 组合态退 2 ``` ⇒ 两类**同时**成立时(`unlisted` + 单文件不可读 / + 跑不了 `sha256sum` / + git 答不了), `diag=manifest-mismatch`(`UPSTREAM_RC` 表里 **1**)而**真 `rc=2`** ⇒ `run-all` 那条不变式 `UPSTREAM_RC[diag] === rc`(`:1173`)**在其上为假**。 **实测**(dsh 复现,裁 `PATH`、root 可达):修前 `diag=manifest-mismatch`、`rc=2`、表值 `1` ⇒ 不一致。 **修后**:`diag=baseline-unrunnable`、`rc=2` ✓ 一致。 ★ **后果不是假绿**(`manifest-mismatch.blocksGreen=true` ⇒ 照样红、note 也转印), 而是**严重度被低估**:2 档的码被 1 档的码**盖住** ⇒ 读者以为"只要改清单", 而真相是"**连数都没读成**"。**rc 通道从此不可信。** ★★ **为什么 12 个案例一个都没抓到**(这才是本节的要点): > 那 12 个案例**个个只动一维**(`unlisted` / `ghosts` / `blind` / `unreadable` / 各 `baseline-*`) > ⇒ 不变式只在**对角线**上被验过,**组合(off-diagonal)无人可达**。 **修法(两件,缺一不可)**: 1. **一条链推两个结果** —— 严重度**只有一个权威来源**:`summary.py` 里按同一顺序算出 `(diag, rc_want)` **一对**,`RESULT` 行用它、`sys.exit` 也用它 ⇒ **排序不可能再漂移**。 ★ 为什么**不是**"把两条链的顺序改成一致":那还是**两份**表, 下次加一个条件两处又会各自漂移 —— 正是本仓反复消的"**一份事实两处实现**"。 2. **显式走一遍 off-diagonal**(新组合案例:`unlisted` + 跑不了 `sha256sum` ⇒ 期望 `diag=baseline-unrunnable` 且 `rc=2`,**2 档优先**)。 ⚠️ 这条**不能**只靠"每跑必断不变式"代替:组合态下若 `diag` 又被低档码占住, 那条断言就**永远验不到 2 这一档**(它只会验到手边那个码的期望值)。 **变异验证**(dsh):把单链的 2 档与 1 档**换序**(= 复现修前的两条链) ⇒ `--only-selftest=exitcode-selftest` **rc=1、^RED=2**,其中: `组合:未列入清单 + 跑不了 sha256sum ⇒ … rc=1(期望 rc=2 且输出含 "diag=baseline-unrunnable"(真打出 diag=manifest-mismatch,UPSTREAM_RC=1 ✓))` ⇒ 排序本身被锁住;且它还**连带**抓出 `盲读` 那条(换序后盲读也走 `manifest-mismatch`)。 ★ **通用规则**(与 §16.1 那条并列): > **凡"多条判定链各自挑一个代表"的地方,都要问: > 它们挑的是不是同一个?** > 只测**单维**(每次只动一个条件)永远证明不了这件事 —— > **对角线上的绿,对组合态没有发言权。** ### 16.3 ★★ **"没人读的字段"与"给它评分"之间,还有第三条路:给它一个读者**(pi + dsh 2026-09-18) `STATIC_ONLY` 的**第 2 列**「当初只能静态的原因」**没有任何判据在读** —— 两个解构循环都用 `,` 把它丢掉(`:1295`/`:1306`),唯一会读它的场合是**到期点名时打印**, 而那正是它**最不需要被检验**的时刻。 **实测**(dsh 同刻 A/B):把这条理由改成一句假话 ⇒ `red` 与红清单**零变化**。 **动到期闸的第二条**(pi 同封报的,我复现):`static=` 与"到期"是**两个量** —— `static=${STATIC_ONLY.length}`(余额,永远 6)与 `dueStatic`(会变 0)分开, 而**只有后者**决定"到期"这件事发不发生。 **实测**(把 6 条探针全改指恒 `false`):**到期点名 1 → 0**,而 `static=6` **一字不变**。 ★ 这条口子**是闸自己邀请的**:它的报文选 (b) 写着"并改换一个更准的到期前提" ⇒ 换探针是**被鼓励的动作**,而"新探针是否真的适用于这条判据"**没有任何判据在问**。 #### ★★★ 我在这一节里连错两次,两次都是被**变异**抓出来的 | 我的修法 | 变异 | 结果 | |---|---|---| | ① 给它加规则:理由必须点到探针的某个标识符 | 拿**现有 6 条真理由**跑 | **红了 5 条** ⇒ **假红是噪音**,比不设判据更糟 | | ② 让自检断言 `staticDetail` **变量**内容完整 | 只删掉那句 `console.log` | **4b 照样报 ok**,而真实输出里播报段 **0 次** | | ③ 断言源码里有 `${staticDetail}` 这个形状 | 同上 | **仍报 ok** —— 因为**锚点写在这段自检自己的注释里**,`includes` 命中的是注释 | | ④ 剥注释后再数 `+ staticDetail);` 的命中 | 删 print | **4b 红 ✓**("可执行代码里没有任何一处把 `staticDetail` 拼进输出") | | ⑤ **同一处改成"记录那次调用真的送了什么"(4c)** | 删 print | **4c 红 ✓** | | ⑤ | **把 print 包进恒假条件**(`if (false)` / `while (false)` / `false ? … : 0`) | **4c 红 ✓,而 4b 仍报 ok**(三种写法都实测过) | #### ★★ 4b 的漏格:它扫的是**文本**,而"那一跳"是**一次调用**(pi 报,dsh 2026-09-19 复现并修) ④ 只解决了"print 被删"。但 4b 的证据是**源码里存在那个锚文本** —— 于是**锚文本还在、 而那次调用根本不发生**时,它照样报 ok。**pi 报的正是这一格**,我完整跑复现: ``` baseline : 播报段出现 1 次 4b = ok 变异体 : 播报段出现 0 次 4b = ok ← 红清单与 baseline **逐条相同** ``` 根因是**自检先于打印**:4b 在 `:1613` 跑(`staticDetail` 定义之后、打印之前), 打印在 `:3129` ⇒ 它**物理上到不了**那次调用,只能读文本。 ⇒ **"验了那个形状"与"验了那件事发生"仍然是两件事**(§16.1 那条缝的又一格)。 **修法(4c)**:不再猜源码,改成**记录这次调用真的往 `console.log` 送了什么** —— 锚点落在「**实际发生的那次调用**」上,这是源码文本够不着的东西: ```js const staticBroadcastSeen = []; { const realLog = console.log; console.log = (...a) => { staticBroadcastSeen.push(a.join(' ')); realLog(...a); }; console.log(`…` + staticDetail); console.log = realLog; } if (!staticBroadcastSeen.some(t => STATIC_ONLY.every(([f, why]) => t.includes(f) && t.includes(why)))) reds.push('(自检)4c:…没有真的进 console.log…'); ``` 实测(完整跑,每次一个变异体): | 变异 | 播报段 | 4b | 4c | |---|---|---|---| | 无(对照) | 1 | ok | 不红 ✓(红清单与改前**逐条相同**) | | `if (false)` 包裹 | 0 | **仍 ok** | **红 ✓** | | `while (false)` 包裹 | 0 | **仍 ok** | **红 ✓** | | `false ? … : 0` | 0 | 红 | **红 ✓** | | 删掉 print | 0 | **红 ✓** | **红 ✓**(不回归) | ★ 两条可复用教训: 1. **"文本里有" ≠ "那次调用发生了"** —— 只要判据读源码,`if (false)` 就是一个 **既保留文本、又取消行为**的通用逃逸。凡"必须真的发生"的那一跳,锚点要落在 **运行时观测**上,不能落在源码形状上。 2. **自检的位置本身就是判据的一部分**:4b 读的那个值定义在它之前、而**使用**在它之后 ⇒ 它对"使用"这一跳永远只能读文本。**"自检跑在它守的那件事之前"是一类结构性盲区**, 不是这一条的偶然。 ⚠️ 4c **不能**做成 `SELFTESTS` 的一条:快入口 `--only-selftest=<名>` 在 `:2503` 就 `process.exit` 了,那条路径**永远到不了** `:3129` 的打印 ⇒ 它只会"看不见"而**不会红**(把它挂在那里等于又造一个 4b)。 ★ 三条教训(都可复用): 1. **"让字段可证伪" ≠ "给它加一条会红的规则"** —— 先拿**现有数据**试一遍;**红了就说明规则错了,不是数据错了**。 2. **变量对 ≠ 打出去了** —— 自检守的若是"它读的那个值", 那么"那个值怎么来的"仍然没人守(§16.1 的同一条缝,换了个位置)。 3. **锚点自匹配**(自检 4 早就踩过、我这次又踩): 把要找的形状**写进注释**,`includes` 就会命中注释 ⇒ **判据空转报 ok**。修法:**剥掉注释再扫** + 在运行时拼锚点。 #### 落地 - `static=` 那一格改成 **`static=6(其中已到期 6 条)`** ⇒ "关闸"这件事**读得出来** (Y 变 0 而 X 仍是 6 ⇒ 一眼看得出)。 - **每次运行都全表播报** `STATIC_ONLY`(文件 + 原因 + 是否到期**并排**)⇒ 第 2 列有了读者。 - **自检 4b** 钉住"那一跳真的存在"(剥注释后数锚点 + 内容完整 + 反空转长度)。 - **自检 4c** 钉住"**那一跳真的发生了**"(运行时记录 `console.log` 的实际参数)—— 4b 管"源码里有",4c 管"调用发生了";**两条合起来才等价于"到达"**。 ★ **通用规则**(本节定稿): > **一个没人读的字段,不许靠"给它加判据"来救 —— 先问它该被谁读。** > 若它本就该被人看见,**给它一个读者**(并钉住"这一跳存在"); > 若它不该被任何人读,**删掉它** —— **余额里读不出来的东西等于不存在**。 > 只有在"这个字段本来就有真假"时,才轮得到加判据 —— 而且**必须先拿现有数据验一遍**。 ### 16.4 ★★ **注释说是从某处读的,实际一个字节没读** —— 硬编码冒充"读源"(dsh 2026-09-19) §16.3 把第 2 列变成每次运行都可见之后,我做的第一件事就是**去读它** —— 于是读到一句假话, 顺着它又挖出本仓一条**同族**的缝。**这条缝的形状**: ```js /** 服务端壁纸上限(`server/internal/handler/appearance.go` 的 `appearanceMaxBytes()` 默认值) */ const SERVER_LIMIT = 4 << 20; // ← 注释说它来自 Go 源,而它一个字节的 Go 源都没读 ``` **实测**(dsh,同刻 A/B):把 `appearance.go` 里 `appearanceMaxBytes()` 的默认值 `4 << 20` 改成 `8 << 20`(**真漂移**)⇒ `harmony-imageprep` **29 pass / 0 fail,一个字都没变**。 ⇒ 这条判据存在的全部理由就是"客户端上限要留在服务端那道门之内(否则必然 413 / 白扔分辨率)", 而**服务端那道门真动了,它不会红**。 ★ **它为什么危险**:注释让读者**以为**这里已经对齐了服务端 —— **"我以为它在读源头"正是没人再去读源头的原因**。 这与 §16.3 那条母规则同一族:**一个字段/一个判据的可信度,来自它真的读了那个东西, 不来自它说自己读了。** **修法**(三件): 1. **从真源头解析** —— ⚠️ **而"真源头"是哪一处,我第一版也读错了**,如实记下: 我最初去读 `appearance.go` 的 `return 4 << 20`,那是**兜底分支** (`if config.C != nil && config.C.MaxAppearanceBytes > 0` 之后才轮到它)。 生产里 `config.C` 非 nil ⇒ **真正生效的值来自 `config.go`**: `MaxAppearanceBytes: getEnvInt64("AGENTMAIL_MAX_APPEARANCE_BYTES", 4<<20)`。 ⇒ **只读兜底分支的修法,在"有人动了 config 默认值"时会漏** —— 这是同一族缝的下一层: **"读了源"还不够,还得问"读的是不是生效的那一处"。** 现在**两处都读,并要求它们相等**(不相等 ⇒ config 未注入时会走另一个门)。 2. **解析失败必须红**,**不许静默回退到硬编码** —— 回退等于把"我读不到"变成"值是对的" (这几轮反复消的那条缝)。 3. **加一条判据钉住"真的读出来了"**:两处都无 `err`、`SERVER_LIMIT === LIMIT_CONFIG.value`、 **`SERVER_LIMIT === LIMIT_FALLBACK.value`**、且 `> 0`(防解析出 `0` 让"小于上限"变成恒真)。 **变异验证**(dsh,每个变异只动一处、其余不变): | 变异 | 期望 | 实测 | |---|---|---| | `config.go` env 默认值 `4<<20` → `8<<20`(**生产生效那一处**) | 红 | `fail=2`(第 9 条"两处都读到" + 第 11 条余量)✓ | | `appearance.go` 兜底 `4<<20` → `8<<20`(**两处不一致**) | 红 | `fail=1`(恰为第 9 条"两处相等")✓ | | `config.go` 那一行整个删掉(**读不到**) | 红,且**不许静默** | `fail=2`(含"读不出生效上限")✓ | | 基线 | 全绿 | `30 pass / 0 fail` ✓ | ★ 注意 B 只红第 9 条、**不红第 11 条**:因为生效值取 config 那处、而它没变 —— **这是对的**(余量断言针对生效值),也说明"两处相等"那条**必须单独存在**, 否则"兜底分支漂了"会完全无声。 ⚠️ **本节的标签范围**:它证明的是"客户端上限与**源码里那个默认值**对齐", **不是**"与服务端运行时实际生效的上限对齐" —— 那个值是 `config.C.MaxAppearanceBytes` **可覆盖**的 (`appearanceMaxBytes()` 先看配置、配置没有才用它)。配置一旦调过,**静态读源码仍然对不上运行值**。 要覆盖后者得真起服务端读它的行为 —— 那属于"设备/服务端在场"的判据,本机判不了。 **别把这条读成"413 已经不可能发生”。** ★ **通用规则**(与 §16.1/§16.3 并列): > **注释里写"这个值来自 X",不构成读 X。** > 凡"某个数必须与别处一致"的判据,先问:**它真的读了别处吗,还是抄了一份?** > 抄一份的判据**在别处改变时不会红** —— 而它的注释会让你以为它会。 ## 17. 变异只证明「注入的样本被抓」,**不证明完备性**;判据的**标签必须等于断言范围** **规则**(pi 2026-09-14):一条判据通过变异验证之后,只能说"我注进去的那一条会被抓"。 **样本不是完备性证明**。配套的第二半更重要: > **标签比断言宽的判据,会在它没测的那条路上被回滚时给绿,而人信的是标签。** 真实例子(`background.test.mjs` 的两条反向断言):名字写着"导航不得变暗 / 不得自叠模糊", 断言体各只扫一条路。三条逃逸路各注一次变异**全部逃掉**(`.dark .nav-rail{}` 选择器作用域、 组件 `dark:` 变体、组件 `backdrop-blur-lg`),导航变黑而判据全绿。 **两种收法,选一个,别都做**: 1. **把标签缩回它真正断言的东西**(推荐:诚实度比覆盖面值钱),并把**未覆盖的路** 逐条写在判据旁边(各自另立判据时才补); 2. 把断言扩到标签的范围 —— 成本高,而且极易变成一刀切误红(先例:`bg-chrome-600` plain 档徽标)。真要扩,**按文件窄豁免**写。