# 开发工具配置:为什么关掉 pi-lens 的自动改写 本仓库**明确关闭** pi-lens 的项目级自动格式化与 autofix。这份文档记录原因, 配置本身在 `.pi-lens.json`(根目录)。 ## 一句话 pi-lens 编辑任一文件后会「安全格式化」它,而它的默认格式化器与本仓库的手工排版 不兼容 —— 每次编辑都会产生与内容无关的大面积 diff,把真正的改动埋掉。 已经造成过两次**真实损失**。 ## ⚠️ `.pi-lens.json` 必须是严格 JSON pi-lens 的配置加载是: ```js // pi-lens dist/index.js PROJECT_CONFIG_BASENAMES = ['.pi-lens.json', 'pi-lens.json']; // 没有 .jsonc function parseConfigFile(configPath) { const text = fs.readFileSync(configPath, 'utf-8'); raw = JSON.parse(text); // ← 不做去注释处理 } // 解析失败 → 打一行警告,然后**整份忽略** ``` 所以**带 `//` 注释会让这份配置完全失效**,而失败方式是静默的: 进程照常跑,只有启动时一行 `[pi-lens] ignoring invalid project config`。 这个坑真的踩过:第一次写这份配置时把大段理由写成了 `//` 注释, 于是「已经关掉了」这个结论是假的 —— 防护从一开始就没生效。 现在理由放在本文件里,JSON 里只留一个 `$comment` 指针。 ## 为什么必须关 pi-lens 在编辑文件后会走 smart-default 回退选择格式化器(见它的 `FORMATTER_POLICY_BY_EXTENSION`): | 扩展名 | 默认格式化器 | |---|---| | `.ts` `.tsx` `.js` | biome(默认 tab 缩进 + 双引号) | | `.html` | prettier(双引号、`` 变小写、按 80 列折行) | 两套默认值都与本仓库的排版冲突。两次已发生的损失: 1. **`19a3161`**:`git add -A` 把约 7000 行 biome 重排扫进了功能提交, 那次提交无法审查(还掩盖了一处 Go 文件的删行)。 2. **`a404cba`**:prettier 改写了 `client/electron/index.html` —— 单引号变双引号、 DOCTYPE 变小写,直接打破 `test/theme.test.mjs` 的两条断言(该测试要求 同步内联脚本里是 `classList.add('dark')`,单引号)。 ## 为什么不是「把格式化器配成本仓库风格」 试过:把缩进、引号、lineWidth 全部对齐之后,`biome format --write` 仍然改动 17 个文件 —— 本仓库的注释按语义换行、数组与调用按可读性手工折行, 这些格式化器还原不了。 ## 谁在守着这个仓库的格式 不是格式化器,是这些: - `tsc --noEmit`(前端类型) - `go vet` / `gofmt -l`(Go) - tree-sitter / ast-grep(pi-lens 的结构规则与安全规则,**只读、不改写**) - 各包的测试套件(`npm test`、`go test ./...`、`node --test`) ## 相关 - `biome.jsonc`:只挡得住 biome,**挡不住 prettier** —— 两者是并列的候选格式化器, 各有各的配置。所以真正的开关是 `.pi-lens.json`,它两条改写路径一起关。 ## 部署残留清理(`deploy/prune-deploy-artifacts.sh`) 部署会留下两类持续增长的东西:网关旧二进制(每份 ~24MB)与插件快照(opencode/dsh 的 快照含 node_modules,一份几十 MB)。2026-09-14 实测 `/opt/agentmail` 累计到 **1.3GB**, 其中旧二进制 ~790MB、快照 ~540MB。 ```bash bash deploy/prune-deploy-artifacts.sh # 干跑,只报告 bash deploy/prune-deploy-artifacts.sh --apply # 真删(默认:网关留 3 份、每插件留 3 份快照) bash deploy/prune-deploy-artifacts.sh --self-check # 判据自检(16 项,两侧都验) ``` 三条硬规矩:**保留回滚窗口**(发布纪律要求有回滚目标,所以不是全清);**绝不删正在使用的 快照**(扫 /proc 的 cmdline 与 cwd,命中就跳过 —— 删掉它进程一重启就找不到自己的代码); **在线数据库永不入列**。 注意那个"在用"判断必须排除**本进程及其祖先链**:调用方常把路径写在命令行里, 不排除就会出现"永远判为在用"(与 `pkill -f` 杀掉自己那条命令同一个坑,已写进脚本注释)。 ### 四类残留与各自的窗口 | 类别 | 来源 | 窗口 | |---|---|---| | 网关旧二进制 `agentmail-gateway.bak-` | `redeploy-gateway.sh` | 保留最新 3 份 | | 插件快照 `/` | `redeploy-plugin.sh` | 每插件保留最新 3 份,`current` 永远保留 | | 数据库/附件**备份集** `backups/agentmail-.db` + `attachments-.tar.gz`、`data/agentmail.db.bak-` | `reset-demo.sh` 与早期手工留档 | 保留最新 1 集 | | `/tmp/agentmail-pre-deploy-*.db`、`/tmp/agentmail-pre-prune-*.db` | `redeploy-gateway.sh`、`prune-test-sessions.sh` | 各保留最新 2 份 | 同一个 `` 的库与附件包算**一个备份集**,一起进出窗口 —— 拆开留没有意义。排序按 **文件名里的时间戳**而不按 mtime:09-02 的两份备份被 09-08 的一次"打开看一眼"改了 mtime, 按 mtime 排会把最老的判成最新的。**没有时间戳的文件一律不碰**(判定不了就不删)。 `pre-prune` 那份是会话归档(不可逆操作)唯一的回滚点,所以它单独一条窗口、报告里也写明身份。 ### 三条判据,以及它们各自防的那个错 1. **在线库不入删除清单**(`del()` 里的 `readlink -f` 比对):备份删错能重建,在线库删错回不来。 2. **每个插件至少留一份"上一版"**:只有 `current` 时脚本报 `⚠ 无回滚目标`。它只报告、 **不造快照** —— 造不出来的东西不该假装有。(2026-09-14 那次事故就是这么暴露的: 四个插件各只剩 `current`,回滚目标没了。) 3. **`--self-check` 两侧都验**:干净样本该删的删、该留的留、在线库不动必须是绿的; 把窗口外的备份换成**指向在线库的符号链接**,脚本必须**拒跑**(退出码 1), 不是"删了才发现"。最后一条是"自检没有碰生产根":比对前后 `/opt/agentmail` 的清单指纹。 ### 事故记录:自检把生产当成了沙箱(2026-09-14) `--self-check` 第一版用 `ROOT="$t" … bash "$0"` 传假根。脚本读的是 `AGENTMAIL_ROOT`, 于是这个前缀赋值被静默忽略,自检的 `--apply` 打在了**生产根**上,删掉一批回滚备份 (3 个旧网关二进制、8 个插件快照、09-02 的两组备份集)。 **为什么没被发现**:`PRUNE_TMP_DIR` 那一路的变量名是对的,所以输出的 `/tmp` 段看着"确实是假根", 干跑那一轮的报告也像模像样 —— 半对的状态比全错更难认。是 `bash -x` 里那行 `ls -1t /opt/agentmail/…` 露的马脚。 **改法不是"下次小心"**:① 传对变量名;② 自检的根目录必须在临时区,否则拒跑; ③ "自检不碰生产"进判据(前后指纹比对)。这条与 `pkill -f` 杀自己同类: **安全装置自己出错时,产出的是一份看着正常的报告**。 ## 判据纪律:三种"看起来验过了"的失效形态(2026-09-14) 同一天里,三条自己写的判据各以一种方式失效 —— 它们**都绿着**,但都不再判别任何东西。 记在这里,因为这是**可迁移**的那部分;具体实证留在各自的头注释里(下面有索引)。 ### 一、钉装饰:断言落在注释上,不落在机制上 `env-guard.test.mjs` 曾对源码文本断言 `/ENOSPC/` 与 `/环境/`,而那段**解释性注释里 本来就有这两个词** ⇒ 把整段翻译逻辑删掉、只留注释,判据照样绿。 **改法**:能被反面样本喂的抽成纯函数(`translateEnvError`),断言**行为** (ENOSPC 要翻译;普通错误必须**原样返回同一个对象** —— "什么都翻译"比不翻译更坏)。 ### 二、分支退化:判据绑在一个会变的环境上 "空间不足 ⇒ exit 2"那条原本靠"本机 `/tmp` 恰好是满的"来验。机器一恢复健康 (`/tmp` 被清空),那条就自动跳过、**无声失效**。 **改法**:给被测物开一个**只为测试存在**的开关(`--inject-avail`),让两种机器状态 都验得了;两个方向都要验(只验"不足⇒2",一个恒报不足的坏守卫也能绿)。 ### 三、跑不到的分支:断言在,区分力不在 按"真实测量"分叉的那版写成了 `realAvail < MIN ? (不足分支) : (充足分支)`,而**本机真实可用就是 0** ⇒ 永远走 不足分支。于是把开关**整个忽略掉**,断言**照样绿**。 比"分支退化"更狠:退化至少留了一行自我声明("此条退化为弱检查"),**短路是无声的** —— 代码看起来两个方向都验了,实际只跑了一个。 **改法**:不与真实测量比,让**两个探针互为反面**(注入 1 字节必须 exit 2, 注入 128 MiB 必须放行),并断言**输出里的判定词**而不只是退出码 —— 退出码可能与真实状态巧合相同。论证对真实值**任意取值**都成立。 ### 变异纪律(做"删掉机制看判据红不红"时) 1. **先证明你能撤回来,再注入变异。** 2. **变异只对"已在 HEAD 里干净提交"的文件做**;还原只走 `git checkout HEAD -- `。 3. **还原路径不得依赖被测资源。** 一次把备份写进 `/tmp` —— 正是当时被占满的那个资源, 备份没写成而变异已覆盖源文件;是同一次"手写 `||` 兜底把失败吞掉"才让恢复变得不确定。 4. **判据不得用被测物证明自己**(第 3 条是它在"还原"上的投影)。 5. **别只看过滤后的输出。** `grep '^not ok'` 会丢掉"整份文件没跑起来"这个信息 (语法错时只报一条 `not ok 1 - test/xxx.test.mjs`)。退出码 + `# pass`/`# fail` 汇总行才是可靠信号 —— 也**不要**为此引入手抄的"期望用例数"常量:手抄常量会过期。 6. **判据的退出码不许经管道取值。** 反例(2026-09-14,我自己踩的): `bash deploy/check-shared-libs.sh 2>&1 | tail -25; echo "exit=$?"` —— `$?` 拿到的是 **`tail` 的**退出码,于是那条脚本的红(真值 1)被我报成了 0; 两个失败信息之所以还看得见,只是因为它们走 stderr 没进管道。 **用 `$PIPESTATUS[0]`,或先落文件再读** —— 管道会改写量纲,与上一条同族。 7. **"注入点"会把该抓的 bug 藏起来。** 2026-09-14 实例:`checkLayout` 的每个自检样本都 显式注入 `repoUnits`,于是那条判据的**真实默认值从没被任何样本走过** —— 而它当时恰好是错的(解析到不存在的 `/home/program/agentmail/systemd`), 判据因此**一个文件都没比过却报"一致"**,自检还 100% 绿。 ⇒ 默认值本身要有判据(导出常量 + 断言存在)、样本要留至少一条**不注入**的; ⇒ 同类还有**位置选择器**(`bad[0]`/`badBak[0]`):在函数前面插一条新检查就改变了 既有断言的语义 —— 断言要按**名字**锚定。 8. **"看起来在比、其实没比"要设成一条自查。** 同一轮里它出现了三次: 目录路径错(`../systemd`)、空目录被当成"一致"、依赖是**符号链接**而选目录时 只挑 `isDirectory()` ⇒ 三次都产出"通过"。 ⇒ 判据绿的时候**也要留下覆盖范围的证据**("比了 22 个文件"/"165 个包"): 空 note 无法区分"一致"和"没比过",而那正是这三次的样子。 ⇒ 一条判据如果**只能靠真文件系统喂**,它就没法被自检 —— 读写都要走可注入面。 9. **变异之前先提交。** 我在**未提交**状态下变异,然后用 `git checkout HEAD -- ` 还原,把自己的改动一起冲掉了(这条纪律我写过、还是踩了)。 顺序必须是:提交 → 变异 → 确认红 → `cp` 还原 → `cmp` 校验。 10. **改"布局/文件集"时,先在脑子里跑一遍"如果这一步被回滚,部署判据会怎么变"。** (pi 2026-09-14 提的,很准。)实例:把 `lib/user-question.js` 搬到 `test/lib/`, 收益是"快照更干净";但**回滚它会让快照立刻多出一处运行时漂移** —— 因为快照是"搬家后"的树,而仓库回到了"搬家前"。这类改动的收益账里没有这一项, 于是它不体现在任何判据里,只体现在回滚之后的红灯上。 11. **判据的退出码不许经管道取值**(与第 6 条同源,写清取法):临时命令里 `cmd | tail -25; echo $?` 拿到的是 `tail` 的退出码。**要取就读 `${PIPESTATUS[0]}`**。 脚本侧四个 deploy 脚本都有 `set -o pipefail`(`install.sh` 还带 `-e`), 所以脚本内的管道判定是对的 —— **但那是判据的一部分,不是风格**: `redeploy-plugin.sh` 的 `if ! node … | sed …; then` 与 `install.sh` 里同形状那处, 依赖 `pipefail` 才测的是被检程序的状态;谁重构时把 `set -o pipefail` 删了或挪了位置, 判定会静默变成"`sed` 成功即成功"。**动那几行要连着 pipefail 一起想。** 12. **判据的输出必须能自证"它比完了全部对象"。** 实例:`check-shared-libs.sh` 的 `show_diff` 里 `cmp … | head -3` 在 `set -euo pipefail` 下返回 1 ⇒ 独立调用触发 `set -e` ⇒ **脚本当场中止**:只报第一个分叉文件,后续对象与收尾汇总都不打印。 退出码**恰好还是 1**(判定是对的),所以光量退出码看不见它 —— 这正是"判定对、证据被截断"。修法 `|| true`,并在注释里写明它不是风格而是判据。 13. **自检样本必须"独立":每条样本都要说清它锚在哪一条检查、用的是哪一棵树。** 这一族我 2026-09-14 一天里踩了三次,合起来记比拆成三条好,因为**修法是同一个** (显式命名 + 显式复位)—— 三者都是"断言的语义被当前状态悄悄改掉": | 形态 | 实例 | |---|---| | **位置选择器** | 自检里 `bad[0]` / `badBak[0]`:前面插一条新检查之后,锚点指的就不是原来那条了 | | **共享夹具状态泄漏** | 新补的四条豁免样本第一版**全红**:它们继承了两棵树里既有的改动(`extra.mjs`、只存在于 `a` 的 `test/`),红的理由根本不是要测的那件事 | | **探针自身没有分辨力** | E 那次的探针按**文件名**判两侧,而两条路径 basename 相同(`deploy/service-failure-notify.mjs` vs `/opt/agentmail/bin/…`)⇒ 两侧读到同一个串、样本"通过"得毫无意义。与 `grep -c 用例名` 那个假数、"名字出现在 `# Subtest:` 头"同族不同形:**锚在"名字"而没有锚在"哪一侧"** | 配套一条(同一天踩到):**自检的夹具形状必须与生产形状一致**,否则夹具会把真 bug 藏住 —— `prune-deploy-artifacts.sh` 的构建暂存自检用 `: >` 造**普通文件**,而生产是 **有内容的目录**;正是这个差异让"`ls -1t` 对多个目录打 `路径:` 头 ⇒ 那段清理一直空转" 这个 bug 藏了很久(判据自己在用文件名判"删了没有",夹具认了错形状,于是它也认了)。 14. **退出码也有量纲。** `node deploy/check-deploy-drift.mjs --self-check` 的退出码 **不是**"自检的结论":自检本体 28/28 全过,但同一个进程接着跑了宿主判据、 于是整体 exit 1。报"自检失败"就是把两个量纲混成一个。 ⇒ 报结论时**分开说**:"自检本体 N/M 通过;整体退出码还包含 X"。 15. **"命令不在" ≠ "命令在但输出为空"。** 把两者压成同一个字符串就会产出假绿 —— 实例(pi 评审 2026-09-14,实测复现): `fc="$(journalctl … 2>/dev/null | grep -icE 'panic|fatal|SIGSEGV' || true)"`, journalctl 失败(无权限读日志 / unit 不存在 / dbus 不通)⇒ 错误被 `2>/dev/null` 吞掉 ⇒ grep 读空输入 ⇒ 输出 `0`、退出 1 ⇒ `|| true` ⇒ `fc="0"` ⇒ **打印"近 2 分钟无 panic/fatal"**。 下面那条 `sse` 同形,后果更坏:**把"工具缺失/读不到"归因成"插件没连上"**, 提示人去查密钥,而问题在日志读不到 —— 一条把人引向错误方向的假绿。 ⚠️ 顺带实测:**`PIPESTATUS` 分不开这两种情况**(命令不存在与"存在但无匹配"都给 `1`), 所以不能靠管道状态区分,必须**先把输出取出来、成功后再过滤**; "命令是否存在"另用 `command -v` 做前提检查(`AGENTMAIL_REQUIRE`)。 ⇒ 规矩:**"命令不在"走 2/红 + 人话;"命令在但输出为空"才是判定结果。** 同族放宽:环境自足不能只覆盖**变量**,也要覆盖**命令**(`journalctl`/`curl`/`systemctl`/ `git`/`go`/`npm` 都曾是被假设存在的那一类)。 16. **"原子替换"要指名哪一步原子 —— 而且要验 `install`/`mv` 到底做了什么。** 实例(pi 评审 2026-09-14,已用 strace + `ulimit -f` 实测): `redeploy-gateway.sh` 头部断言"`install(1)` 本质是 rename,是原子的 —— 要么完整换掉,要么原样不动",**而那是整节设计的理由**(为什么优先 `install`、 为什么失败即回滚)。实测 `strace … install -m 0755 /bin/true /tmp/t`: `unlinkat(t)` → `openat(t, O_CREAT|O_EXCL)` → 写入,**全程没有 rename**。 中途失败实证:`ulimit -f 1` 下安装 ⇒ 退出码 **153**(SIGXFSZ), 目标变成 **1024 字节的截断 ELF**,原 14 字节内容**已被销毁** —— 头部后半句自己写的风险("中途失败留下半截二进制")`install` **并不免疫**。 ⇒ 真原子 = **临时文件放在目标同目录**(同 fs)+ 最后一次 `mv`。 还要注意 **`/tmp` 与 `/opt` 常是不同文件系统**(实测设备号 40 vs 2049), 所以"改成 `mv` 就原子了"同样不成立 —— 跨 fs 的 `mv` 退化成 copy+unlink。 17. **环境前提表有第五列:同时性(并发)。** 变量/命令/空间/身份之外, 还有"同一时刻只能有一个"。`prune`/`redeploy`/`install` 都会写同一批路径, 而工作区是多 agent 共用的 ⇒ 需要 `flock`(**不是**"检查锁文件是否存在"——那本身有竞态)。 ⇒ 判据:"两个同时启动 ⇒ 第二个 exit 2 并点名"。 18. **同一份代码搬到另一个脚本里,"能引用的变量和函数"是不一样的。** 我给三个脚本加同一段锁,第一版照抄:`install.sh` 没有 `bad()`(用裸 `echo`)⇒ `bad: command not found`(127);`redeploy-plugin.sh` 没有 `$PREFIX`(它用 `$DEST_ROOT`)⇒ `PREFIX: unbound variable`(`set -u`);而 `install.sh` 的 `--check` 刻意允许无写权限运行 ⇒ 在那里建锁又变成 `Permission denied`。三处都是"复制粘贴的上下文假设"。 19. **新能力自带的新依赖,要回到那张登记表 —— 否则"表"与"被表的东西"不同步。** 实例(pi 评审 2026-09-14):我刚给部署加了"同时性"那一列(`flock` 部署锁), **却忘了把 `flock` 登记进三个脚本的 `AGENTMAIL_REQUIRE`**。后果(已实测): `flock` 不在机器上 ⇒ `command not found`(127)⇒ `! flock` 为真 ⇒ 打印"**另一个部署正在跑(锁被占用)**" —— 退出码事后是对的(2), 但**诊断是错的**,而照着它做的是"等另一个部署结束":**永远等不到**。 ⇒ 每次给部署路径加一个新命令,都要回去在 `AGENTMAIL_REQUIRE` 里加一个词。 20. **"服务停着而脚本死了"是独立于"文件半截"的一类风险 —— 环境前提的第六列候选(中断)。** `redeploy-gateway.sh` 在 `systemctl stop` 与 `systemctl start` 之间有窗口, 外部信号(Ctrl-C、宿主杀进程、会话被回收、OOM)会让脚本直接退出 ⇒ **服务留在停止状态而脚本什么都不说** ⇒ 后果是"邮件全停 + 无人告知", 比半截二进制更难发现。原子 `mv` 只修了后者。 修法:进窗口前 `trap … INT TERM HUP`、出窗口摘掉,trap 只在"确实还停着"时动手 (否则会多起一次服务)。判据可喂:用 stub `systemctl` + 探针脚本,给自己发 `SIGINT`, 断言"停着 ⇒ 调了 start、退出码 130"与"没停 ⇒ 不调 start"。 21. **注释里的数字无法被判据守住。** —— 而不是只落到你想到的那一个。** 实例(pi 评审 2026-09-14):我在 `env-defaults.sh` 的 `HOME` 上写了两条规则 ("`mkdir -p` 对已存在的不可写目录会返回成功 ⇒ 必须单独判 `-w`"、"判据落在能不能写、 不落在路径像不像"),**同一条规则没落到紧邻的 `TMPDIR` 上** —— 而 ENOSPC 正是这条链的 元老问题。两种失败形状(不可写 / 写满)都在**中间**炸,报错看起来像工程问题。 ⇒ 同族的"变量"与"命令"是同一张表的两列,写规则时要把表**列全**: 设了要判"可写"、判了可写还要判"有空间"(`0` 是**真的没有**,读不到才是"不知道")。 17. **注释里的数字无法被判据守住。** 同一文件里曾同时写"写点五处"和"共 6 处" (且它的式子 2+2+3 加起来是 7,实际 10 处)——**三处说法三个数**。 与"不要手抄期望用例数常量"同源:**两组矛盾的数字比没有数字更糟**, 因为它让读者以为有人数过。要判覆盖完整只能靠**机制**(整段 try/catch),不靠数数。 11. **一条新判据上线时,先找它可能与哪些既有不变量冲突。** 2026-09-14: 新加的"本平台不可达 ⇒ 搬去 `test/lib/`"与既有的"共用模块四方**逐字节同源、 连相对路径一起钉**"(`deploy/check-shared-libs.sh`)**方向相反** —— 我只看⻅了自己那条,于是"按规则推断出的正确动作"把共用判据打红两处, 还差点让下次部署**静默删掉**一个 dsh 桥的生产模块(`lib/user-question.js`)。 ⇒ 可达性只能当**报告**,不能当搬家判据;判据里读共用清单做豁免(不手抄)。 ### 两条同族的实现纪律 - **测试文件之间不许互相 `import`**(启用 `node --test` 时每个文件一个进程、 模块导入是进程内的)⇒ 被引的那个文件的用例会在**引用者那个进程里再注册一遍**。 实测:从测试文件取夹具,让巨行用例(单条往临时目录写 ~12 MiB)跑了**两次** (测试总数 475;修完 459)。夹具放**非测试模块**(`lib/session-fixtures.mjs`), 判据也进套件(`env-guard.test.mjs` 里那条扫描)。 - **提交前先看 `git status` 里有没有"不是我的"文件。** 这个工作区是**多 agent 共用的** —— 2026-09-14 20:02~20:04 就有另一条会话在改 `deploy/install.sh`、 `deploy/prune-deploy-artifacts.sh`、`deploy/redeploy-gateway.sh`(`-trimpath` 那组加固), 而当时我正在同一个仓库里连续提交。`git add -A` 会把**别人没写完、没审过的改动** 一起做进我的提交里,而且从 `git log` 上看不出来是谁的。 **规矩:显式列出要提交的路径,别用 `-A`/`-u` 图省事**;提交信息里也不要把 别人的改动算作自己的成果。(这次三次提交都是显式路径,事后 `git show --stat` 核对过。) - **写点要"一处覆盖全部",且覆盖范围不取决于入口。** 同一条"ENOSPC 被翻译成环境问题" 在这套代码里被漏过**三次**,而且是**三个不同的入口假设**(不是同一处错了三次): ① `test/lib/session-fixtures.mjs` 的 `writeSession`(第一版只包 `writeFileSync`, `mkdirSync` 在 try 之外); ② `deploy/check-deploy-drift.mjs` 的 `mk()`(`mkdtempSync` 也**是**一个写点,却在 try 之外); ③ 同一个文件的 `main()` —— 兜底层放在调用方,而**被兜的 `selfCheck()` 是导出的** ⇒ 任何绕过 `main()` 的调用者拿不到翻译。 三次都可以概括成一句话:**"我以为的入口/哪一行"决定了覆盖范围**。 - **判据过宽和过窄都是坏的。** "测试文件不许互相 import"那条判据自己同时踩过两边: 过窄(只匹配静态 from,漏掉动态 `import()`)、过宽("文件里出现别的测试文件名" 把**注释里的散文引用**也算违规,还被自己注释里的示例字面量点亮)。 最终形状只能是"解析真引用"。 - **因果特定的判据 + 因果无关的判据要配对。** "测试文件互相 import"只能发现**已知成因**; 同族的另一种成因它看不见 —— 实测**跨文件同名用例不会被 runner 拦** (两个文件各写一个同名用例 ⇒ `# tests 2 / # pass 2 / # fail 0`,零警告)。 所以补一条不挑成因的运行期判据:`test/lib/run-suite.mjs` 从**同一次运行的 TAP** 里数结果行,重名即红。 - **判据的锚点必须与结论一一对应。** 同一条重名检查,锚 `^(ok|not ok) - <名字>` (结果行)是对的;锚"名字出现过"是错的 —— TAP 里名字既出现在 `# Subtest:` 头、 又出现在结果行,**效应 2 倍、噪声也 2 倍且恰好同值**,于是"4"看起来还能解释; 若行种类是 3,就会把"两次"读成"三次"。**别让噪声与效应同阶。** - **`lib/` 与 `test/lib/` 的边界**(2026-09-14):部署脚本是 `cp -a "$SRC/." "$STAGING/"` 加一条 `rm -rf "$STAGING/test"`(**没有** `EXCLUDE_DIRS` 这种变量)⇒ **`lib/` 整份进快照**。 于是规则必须是可判定的:`lib/` = 从**生产入口**可达的模块;只被测试引用的放 `test/lib/`。 判据在 `test/lib/reach.mjs`(真走 import 闭包,含按路径 fork 的子进程入口) 与 `test/layout-boundaries.test.mjs`。 ★ 别写成"被 `src/` **直接** import":实测 22 个 `lib/` 模块里 4 个 `src` 直接引用数为 0 (`addressing.js` 被 `lib/inbox-format.js` 传递引用、`mail-session-id.js`、`crash-notify.mjs`、 `user-question.js`)—— **直接引用数不是可达性**,所以判据真走图。 ★★ 但这条例外更要紧:**`lib/` 首先是四桥共用命名空间,其次才是"本平台可达"**。 `user-question.js` 在 pi 侧只被测试引用、却在**四桥共用清单**上 (`deploy/check-shared-libs.sh` 的 `ALL_LIBS`,它是 **dsh 桥的生产代码**)。 按"不可达就搬走"处理它,会同时打红两处(共用模块缺失 + 共用测试已分叉), 而且**下一次部署会静默把它从生产快照里删掉**。 所以可达性只能当**报告**,不能当搬家判据 —— 判据里读共用清单做豁免。 (教训的完整形状:**一条新判据上线时,先找它可能与哪些既有不变量冲突** —— 这里两条不变量方向相反,而我只看见了自己那条。) ### 索引(实证在各自文件头注释里,此处不复述) - `plugins/pi-mail-bridge/test/lib/tmp-space.mjs` —— 测量层:`bavail × bsize`、 **`0` 是"真的没有"而不是"不知道"**(只有 `null` 才是不知道)。 - `plugins/pi-mail-bridge/test/lib/env-error.mjs` —— ENOSPC ⇒ 人话;为什么它是纯函数、 为什么**不与** `deploy/` 的实现合并(`deploy/` 的独立性比去重值钱)。 - `plugins/pi-mail-bridge/test/lib/reach.mjs` —— `lib/` 与 `test/lib/` 的边界判据。 - `deploy/check-deploy-drift.mjs` —— 现场比树(判据自己算,不靠手抄常量)+ 判据自检的两侧验证 + 为什么它自带兜底层。 - `deploy/check-deploy-drift.mjs` 的运行输出本身**就是**部署状态的判据来源; **不要**把它抄成一份哈希清单往外发 —— 抄出来的那一刻就开始过期 (2026-09-14 发给 `jianf` 的清单在两次提交后就作废了)。 ### 附:一条被自己的结论"半路纠正"的观察(留作提醒) 2026-09-14 排查时看到 `permission_requests` 里有一条 `result IS NULL` 的挂起请求 (11:56 创建,已挂 3h54m),当时的推断是"worker 被重启/超时杀掉 ⇒ 停在授权上没人解除 ⇒ 这是那个 bug 的同族缺口"。 **实际是正常状态**:网关的等待窗口是 `PermissionWaitWindow = 10 * time.Minute` (`server/internal/models/permission_mode.go`),请求 12:06 就失效了, 界面靠 `AttachPermissionDeadline` 推导出的**时刻**(不是布尔快照)自己判断过期。 **教训**:看到与已知 bug 形状相同的现象时,先把"它是否已经由另一层按设计处理掉了" 查完再下结论 —— 否则会把一个正常状态写成缺陷,而这类误判会以"我发现了新问题"的 语气传播出去,比沉默更贵。