Files
MailUI4Agents/client/electron/test/CRITERIA.md
JianFeeeee 456ae5a66d 跨端: pi 五条评审落地 —— 产物自证替代时间戳代理、静态判据可到期、判据能读一层标识符、值/来源配对规则、归因不许动别人的树
pi 2026-09-14 的评审(`9839f8a9`)五条,逐条落地;其中 §6 的两条是**核对后已成立**,不重复劳动。

## 1 产物自证:`dist/BUILD_INFO.json`(pi §1)

原来那条判据是"`dist` 比 `src` 新"——**代理变量**,pi 指出两层都靠不住:看不见"构建是否
成功"(实测过),而"`dist` 比 `src` 新"也不等于"dist 是从这份 src 构建的"(`checkout`/`cp`/时钟
都骗得过 mtime;我确实用 checkout 造过一次假红)。现在改成**内容自证**:
`scripts/build-info.mjs` 在构建最后一步写 `{gitRev, gitDirty, srcHash, srcFiles, buildCmd, builtAt}`,
判据重算当前指纹再**精确比对**(`test/build-stamp.test.mjs`)——"代理"两个字没有了,
报错能直接读出两边指纹。`release-linux.sh` 打完包把同一份信息打进日志(一个包自带
"它对应哪个源码状态")。`gitDirty` **只展示不判定**:共享工作区常年是脏的,拿它当红/绿依据
会天天误报。

实测三变异:改 src 内容不重建 → 红(两个指纹都打出来);BUILD_INFO 记成别的提交 → 红;
**`touch`(只动 mtime)→ 绿** —— 旧判据在这里是**假红**,新判据不误伤,这是它严格更好的地方。

## 2 静态判据的**欠账**与到期(pi §5)

"暂时"不是状态、是待办,规范里写下的"暂时"没有任何机制回来读它。改成可机检的形状:
`run-all.mjs` 登记 `STATIC_ONLY`(5 条:`.ets` 只能验形态)+ **必填到期前提**(探针,真跑
`hdc list targets`);汇总打 `RESULT static=N`(**欠账余额**);**前提一旦为真,这些判据当场
变红**并要求"改成行为判据或换更准的前提"。实测:探针恒真 → 5 条同时报"到期";把前提写成
`'vibes'`(未知探针/陈述)→ 套件红。这是"自报条数 < 登记条数"的**时间版本**。

## 3 判据能解析一层标识符(pi §3)

Go 默认值判据原来"值不是字面量 → 判据读不懂 → 红",长期结局是有人做一次无害重构
(`BgDim: defaultDim`)就把判据逼宽、再下一步少核一个字段。现在:字面量直接用;
标识符在**同一文件**查 `NAME = <字面量>`;查不到(跨包/计算/iota)才报"读不懂"。
实测:`BgDim: defaultDim` + `const defaultDim = 12` → **绿**(无害重构不再误伤);
`const defaultDim = baseDim + 0` → 红且报文说"读不懂"。**只解析一层**:再深就是"执行 Go"了。

## 4 §6.7 的可机检分流规则(pi §2)

新增 §6.7.0:**值 → 行为判据,来源 → 静态判据**,理由写成覆盖问题(行为判据只覆盖它跑到的
路径 ⇒ 原理上判不了"有没有别的路绕过去";来源约束要的是全程序可达性 ⇒ 只有读代码能答)。
两条推论:静态判据判值永远差一个反例、行为判据判来源永远差一条路径;**不是强弱,是分工**,
缺任一条那一对就是假判据。附本仓已有的完整样例(P5 命中区:值判据"≥44"+ 来源判据
"应用点必须引用常量"),新增 §6.8 记欠账机制、§8 记共享树归因纪律。

## 5 值/来源配对补齐 + 让位派生(pi §6 的两条)

- 命中区原来只有值那一半:补**来源**判据(`.ets` 里出现 `minHeight: 44` 这类裸数字 → 红,
  报文点出"值判据管数字够不够大、来源判据管用的是不是同一个数字")。变异:写死 44 → 红。
- "`76` 应从条高派生":**核对后已成立**(`NAV_CONTENT_RESERVE = NAV_BAR_HEIGHT +
  NAV_BAR_BOTTOM + 8`,判据也按常量算)。顺手把裸的 `8` 起名 `NAV_CONTENT_GAP`
  (这一族里唯一还需要人判断的数),并加判据钉住**派生关系**:写回字面量 `76` → 红。

## 6 `legacy.bak` 的生命周期(pi §4):**有意永久残留**,并说清代价

pi 质疑成立:判据钉死"没有任何代码读它"⇒ 也没有任何代码能删它。**否决了"点重置外观时删"**:
最可能点重置的人正是外观被接管的那个人,而这份备份是他唯一的旧值,那时删等于把恢复数据
毁在最需要的时刻。所以定为"有意永久残留"(没有代码路径能判定何时安全删除——那取决于人),
并把代价量出来写进 §7.12:值受 `MAX_DATA_URL_BYTES`(2.4MB)约束,**最坏是一张用户原图的
整份副本**,共享机器上即一份别人的外观;要清就手工 `removeItem`。判据钉住这行**同时**给出
"为什么不删"与"怎么删"(变异:删掉删除方法 → 红)。

## 7 归因方法(pi §6 第三条):认错并写成纪律

我当天归因 `narrow-layout` 的 3 条红时用了 `git stash push -- MailView.tsx`,动的是**并发写
作者的未提交改动**。结论对、方法不行:它写共享工作区(别人崩溃/`git add -A` 就丢他的活),
而且只影响 tracked 文件(untracked 的 WIP 还在 ⇒ "干净了"是假干净,结论也可能假)。
纪律写进 CRITERIA.md §8:只读手段(`git show HEAD:path > /tmp/...`、`git worktree add`)或直接问作者。

## 验证

`npm test` 全绿(14 个判据文件 + vitest 266 + typecheck),`RESULT static=5`;
`hvigorw assembleHap` BUILD SUCCESSFUL;`scripts/release-linux.sh` 重打 deb(日志带产物指纹)。
提交前两道**真红**按预期挡了我:改了 src 未重建 → build-stamp 红;重建了 dist 未重打包 →
packaging 红。**未验**(照旧不写成已完成):鸿蒙侧视觉/交互观感、运行期换肤重算(静态判据,
到期前提见 `RESULT static=5`)。

注:本次 `dist`/deb 是在**共享工作区**上构建的,树里含并发写作者未提交的
`CalendarView.tsx`/`index.css` —— 产物里的 `gitDirty: true` + `gitRev` 正是为此留的,
复核时先看那一行。
2026-09-14 16:00:34 +08:00

327 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 判据规范(写判据前先读这份)
这份文件记的是**判据本身的写法**:什么样的判据能红、能红在对的地方、以及不会在"代码完全正确"时乱红。
不是"怎么用 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`产物自带来源比事后猜强)。
**一句话**归因手段的选择标准是"**会不会让别人的东西处于危险里**"不是"哪条命令最快"。