ba5720573e0be0de0e6dd7cf2c17142b4e5cc35d
12 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 1c3335f0c9 |
修复: 第五个落点 —— 判据写对了,却**没有任何人执行它**(check-file-modes.sh 空转)
pi `4fc16f66` 报的是 rc=2 那半个缝(M18)—— 我复核后**已经在 `e99a657` 修好了**,
而它的**父提交正是 pi 报信时读的 `6ee9902`**。又是同一形状的竞态。
但照 pi §四 那句「**每条分支的前提本身也要能被构造**」做审计时,
我发现**第五个落点,而且这一格的判据本身写得完全正确**:
`deploy/check-file-modes.sh` = 源文件权限政策的**唯一**判据。
它甚至专门修过自己那份 `[ -x ]` 恒真的 root 陷阱(`b7dc9e9`),改成直接读权限位 —— 修得对。
**但它从来没有被任何东西执行过。** 全仓 `grep -rn "check-file-modes"` 只有:
· `check-deploy-drift.mjs` 3 处**注释**(谈分工)
· `run-all.mjs:951` **注释**(讲 root 陷阱)
· `summary.py:77` **注释**,且**说法与实现不符**
· `summary.py:431` 一句 `print(...)` 的**文案**
⇒ 唯一一处非注释提及是一句 print 的文案。没有 install.sh / redeploy-* / hook / CI / cron 调它。
**而它当时正红着**:2 个文件是 600 + `jobs/` 缺属主 x 位。
★ 为什么这一类**只能靠判据守**(实测):
`git status` 看不见、`git diff` 看不见、`git ls-files -s` **只记 100644/100755**
(组/其他读位**不进版本库**)⇒ 把受跟踪文件 `chmod 600 ↔ 644` 两次 `git status` 都空。
套件也看不见(`jobs/` 缺 x 位时实测 `diag=none`、`ran=48`,全绿)⇒ **没有第二条通道**。
0600 在"跑的人恰好是属主"时不炸,换身份就是 EACCES,而那串报错**看起来像代码问题**。
修法:
· 接进 `deploy/install.sh`(check-shared-libs 之后),**走 `--check` 累积通道**
(照既有 npm_rc/CHECK_GATE_RC)。⚠️ 我第一版**直接调**,而门禁红时它 `exit 1`、
install.sh 是 `set -e` ⇒ 当场吞掉后面所有诊断:**实测**接线后 `构建 Gateway`
在日志里命中 **0 次**(接线前 2 次)—— 正是 install.sh:141-157 刚修过的同一个毛病,
我自己又造了一遍。改累积后回到 2 次,门禁报 `[FAIL] 权限政策没过(退出码 1)`。
· 修那两个文件:600 → 644 ⇒ 权限政策现在绿(rc=0)。
· 新增判据 `criteria-hygiene` 第 7 条:政策门禁必须被入口脚本在**可执行位置**调用。
★★ 变异验证时**我自己先假绿了一次**(单记):第一版用 `code()` 剥注释,而
`lib/read.mjs` 的 `stripComments()` 只认 `//` 与 `/* */` —— 那是 **JS** 的注释,
**`install.sh` 是 shell,注释是 `#`** ⇒ 对 shell 文件**原样返回** ⇒ 判据读到了
**我写在它上面那段解释里的** `` # `check-file-modes.sh` 红时 `exit 1` ``。
变异①(接线整段删掉)⇒ 第一版 **rc=0(假绿)**;自剥 shell `#` 后 **rc=1** ✓
变异②(只在 echo 里提一句)⇒ rc=1 ✓
变异③(gates 恒空 ∀x∈∅)⇒ rc=1(`只找到 0 个`)✓
(③ 第一次 python 锚点没匹配上、变异没施加,我重测并**先打印"变异已施加"**才算数。)
★ 射程如实标出:只管 `deploy/check-*.sh`(政策门禁族);**不管** `check-deploy-drift.mjs`
这类**按需手动工具**(自带 `--self-check`、文档写明"事后自查")——要求它进入口是**错的**。
它实测同样 0 处调用,但**本判据不覆盖它,也没修它**。判"有没有接线",不判"接得对不对"。
★ 通用纪律(CRITERIA.md §16.1.2):**判据自己也要能"被证明它真的在看代码"。**
若判据读的**语言**与 stripComments 实现的**语言**不一致(shell vs JS、Python vs JS),
那"剥注释"是**假的** ⇒ 判据消费散文,而**变异验证是唯一能戳破它的东西**。
全套 checks=516→**517** pass=512 fail=5 red=5 broken=0 verdict=red(5 条红都是别的会话的);
mutants=48 ran=48 skipped=0 diag=none baseline=7/7✓;5 个自检各 rc=0。
|
|||
| 4f386199cf |
修复: 集合指纹**不是集合的函数**(顺序敏感 + 只含 name:size);"读不到"曾静默报成"全 0 且指纹正常";"设备忙"的跳过**加界**;criteria-hygiene 两处按字面量裁射程
pi 2026-09-18 报的两个洞我**都当场复现了**(不是我信了,是跑出来了),修完都验了阳性对照。
## 1. 集合指纹必须是集合的函数(pi 洞 1,两个都复现)
原来 `'\n'.join(f'{f}:{getsize(f)}' for f in listed)`。实测:
· **顺序敏感**:只把 `jobs.manifest.json` 反序(集合/内容/计数全不变)
⇒ `sha=02502771` → `3439e049`。于是"集合变了 ⇒ 一眼看得出"失效,
**每次清单整理都假变**。
· **只含 name:size**:字节级等长改写(`jobs-one.json` 的 `name: 'image'` → `'imoge'`,
265 字节不变)⇒ **指纹仍是 02502771**。更尖锐的是 file 路径改成等长的
`ApiClienX.ets`(指向不存在的文件)时 `ran/skipped/on_new_criteria` 全变而
**指纹不变** —— **"集合没变而数变了"恰恰是它声称要抓的情况,它抓不到。**
⇒ `sorted()` + 内容哈希。实测:原序/反序/排序三种**同一哈希**(`33ff3bea`);
等长改内容 ⇒ 变(`89b2aa41`)。
## 2. "读不到" ≠ "不存在"(pi 洞 2,复现 + 比 pi 说的更糟)
隔离副本 + `runuser -u nobody` + `chmod 644 jobs/` 实测:
```
nobody: mutants=0 ran=0 skipped=0 … 原始条目 0 sha=e3b0c442 rc=0
```
`e3b0c442` = **空字符串的 sha256**;而且退出码 **0**。根因:`exists()` 对
**不可进入目录里的文件**返回 **False**(实测),12 个全被跳过;而 `glob` 那一半
照样列得出 12 个 ⇒ `unlisted`/`ghosts` 全空 ⇒ **"清单与磁盘不一致"那条警告一声不响**。
**同一份权限,两个半边给出互相矛盾的结论。**
⚠️ 比 pi 说的更糟的一点:那行**同时**印着 `清单 12 个 job 文件` 和 `sha=e3b0c442`
—— 一句话里说"12 个"却一个都没读到。
⇒ ① 判目录可进入性(`access(R_OK|X_OK)`),读不到就 `✗✗` 明说数字**全部无效**、
并点明"这不是空集合";② `open` 接住 `OSError`,把读不到的文件**攒起来一次报全**
(原来会死在第一个文件上抛 `PermissionError`,让人以为"就这一个");
③ 退出码 **2**(本仓约定:2=环境)—— 读不到就是没读数,而**没读数不是成功**。
④ `unreadable` 与 `ghosts` **分开报**:修法完全不同(修权限 vs 删条目)。
★ 这里我先写了个**不可达的分支**:`unreadable` 先探后读,而真读时 `PermissionError`
会提前抛出 ⇒ 那段报告永远走不到("判据在,但走不到",这次长在报告分支上)。
自己查出来并改成"读的时候接住",才让它成为可达的真分支。
## 3. "不抢前台"的跳过**加界**(pi 2026-09-18 §3,我接受)
只跳过不设界,"设备忙"会变成到期判据的**永久灰区**:不算红不算绿 ⇒ 永远不必被升级
—— 到期机制要防的正是这个,只是入口换成了"设备忙"。⇒ 连续 K(默认 3)轮没跑成,
**跳过自己变红**并给出接管路径(K 轮内是礼貌,K 轮外是闹钟)。
边界:**只对"设备在、前台不是我们的"计数**;**设备不在不计数也不变红** ——
那是 `PROBES.device` 的既有裁定(没装 SDK 的机器不该天天假红),超出本模块职责。
账本 `.tmp/harmony-busy-skips.json`(已 gitignore):判"**这台机器上**连续多少轮没验成",
换机器不继承。实测 1→2→3 轮 skip、第 4 轮起 FAIL;跑成一次即清零、再从 1 重新数。
自检(不连设备)把 ①K 轮内礼貌 ②超了必红 ③跑成清零 ④上限可覆盖 都钉住,
变异验证 M7(over 恒 false)⇒自检②红、M8(noteRan 不清零)⇒自检③红。
## 4. 顺带修掉 criteria-hygiene 自己两处"按字面量裁射程"
我按纪律把账本读取从裸 `readFileSync` 改成 `prose()`(`criteria-hygiene` 立刻红,
**那条判据是对的、我错了**),接着暴露出该判据自身两个洞:
· **import 按写法硬匹配**:只认 `'./lib/read.mjs'`/`'../lib/read.mjs'`,
而 `harmony-device.mjs` **就在 `lib/` 里**、按惯例写 `'./read.mjs'` ⇒
假红"根本没 import"(其实 import 了、运行时完全正常)。改成**解析说明符后与
`SELF` 比**(本仓已用"解析后比较"解决过同一族:`abspath`、`join(HERE,…)`)。
· **用 `prose()`(原文)判"用没用"** ⇒ **注释里**写 `prose(…)` 就算用了
(实测该文件命中 3 处、只有 1 处是真调用)。改成 `code()`。这与它上面那条
"不许裸用 readFileSync"踩过的是同一个坑,我在那条上写了理由、**这条漏了**。
两处修法都做了承重验证:删真 import ⇒ 红;import 换成 `code as prose2`(别名)⇒ 红;
只在注释里写 `prose(` ⇒ **绿**(对照旧实现:同一份样本 ⇒ **红**)。
## 验证
· 全套件 `checks=459 pass=455 fail=4 skip=0 red=9` —— 红的仍是同样 9 条(都是并发
会话的"自报 > 清单"与 4 条 exit 1),我中途引入的两条(裸 readFileSync、import 假红)
已消。`mutants=48` 不变。
· 所有 chmod/清单反序/等长改写**都已还原**,`git status` 只剩本次 5 个文件。
|
|||
| cb54c53937 |
跨端: 补上 --check 的反向洞:CHECK_GATE_RC 真正 exit 出去(红门禁不再退 0);干跑不再重写 dist(BUILD_INFO_OUT 改道临时目录,build-stamp 的红不会被干跑刷掉)
pi 2026-09-15 两条都成立,而且第一条是我**自己上一个 commit 的注释在骗人**。
## 一★★ `CHECK_GATE_RC` 只被打印、**从来没被 `exit` 用** —— 我把它从末尾挪走时丢了退出
```
$ grep -n "exit.*CHECK_GATE_RC" deploy/install.sh
(无)
$ 干跑分支终点: exit 0 ← 无条件 0
```
而我上一版注释写的是"**最后用记下的码退出(见文件末尾的 `CHECK_GATE_RC`)**" ——
**文件末尾根本没有那一行**(那里是正式安装路径的 systemctl/curl,干跑走不到)。
我上一轮把那段从"文件末尾"挪到 `go vet` 之前时,**挪了打印、丢了 `exit`**。
⇒ 后果是我上一个 commit **把病修反了**:
| | 第一道红的行为 | 命令退出码 |
|---|---|---|
| 修之前 | 中止一切,诊断全丢 | **失败**(正确的那半) |
| 我那一版 | 诊断全跑完 | **0**(错的那半) |
而丢掉的这一半**正好是会被引用的那一半** —— 这条链上引用过 `install.sh --check` 的 `exit 0`
当绿读数。**"注释说的事和代码做的事不一致"这是我第三次犯**(前两次:`unreportedFiles` 说修了没修、
`ok 5` 报文认证了没人判的界)。
**修法**:干跑分支终点 `exit "${CHECK_GATE_RC:-0}"`。
## 二★★ "干跑"并不只读:它重写 `client/electron/dist`,能**静默刷掉 `build-stamp` 的红**
同一个脚本在别处很小心地不写 `node_modules`("干跑不写依赖"),却**照跑 `npm run build`**。
两个后果(你指出的):
1. **干跑改共享状态** —— 而它自己那句"干跑没有执行"的清单只列了**工作区外**的路径,`dist` 悄悄在射程内;
2. **它能把 `build-stamp` 从红刷成绿** —— 那条比的是 `dist/BUILD_INFO.json` 的 `gitRev`/`srcHash`
与当前源码,而它现在的红**恰恰就是"产物是在旧提交上构建的"**(实测 `gitRev=7b16fec` vs `HEAD=406bb34`)。
跑一次干跑 ⇒ 重构建 ⇒ 不匹配消失 ⇒ **一条红没了,而没有任何人决定过"重构建"**。
**修法**:干跑改成 `gen:bg` + **`vite build --outDir <临时目录>`** + `build-info.mjs`(新增 `BUILD_INFO_OUT`)。
**真装的 `npm run build` 一字不改**(仍写真正的 `dist`)。
**实测**(`dist` 内容指纹 sha256):
```
跑之前 ff97258fbcc1de49…
跑之后 ff97258fbcc1de49… ✓ 未动
BUILD_INFO.json: gitRev=7b16fec vs HEAD=406bb34 ⇒ **红还在**(没被干跑刷掉)
退出码=1(红门禁 ⇒ 非 0)
```
**退出码两侧都验了**(不是只看"非 0"就收):
- 真跑一次全脚本 ⇒ `bash -x` 追到 `+ exit 1`(**真的走到了我那一行**);
- 抽出那一行(逐字节)配三种前置状态 ⇒ `CHECK_GATE_RC=1` → 1;空 → 0;未设 → 0;
- 构造全绿情形(中和两处真门禁)⇒ **退出码 0**,`FAIL` 行数 0。
★ 顺带一条**我自己踩到的坑**:第一次构造"全绿"副本时我把脚本拷到 `/tmp` 跑,
而 `REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"` ⇒ `REPO=/tmp` ⇒
`env-defaults.sh` 找不到 ⇒ 退出 1,**看着像"绿路径也退 1"**。放到仓内(临时文件、跑完即删)才是对的。
**又一次"我的验证环境和生产不同"** —— 这次差异是脚本位置。
## 三、你指出的"同一现象两种政策":加了互相指向,并写明**为什么不能统一**
- `install.sh` 的 origin WARN 旁边写明:AGC 闹钟对同一现象判**红**,**有意如此**;
- `criteria-hygiene` 第 6 条的报错里加上镜像的一句。
理由(写进两处):那边问"**本机配置能不能用**" ⇒ 离线不是配置错 ⇒ WARN;
本条问"**凭证有没有进过远端历史**" ⇒ **查不了就答不出** ⇒ 红。
并且写明统一到任一边的代价:统一到 WARN ⇒ **泄漏告警在离线时静默变绿**;
统一到红 ⇒ 离线机器上一次干跑就报假红。
## 四、残余(照实写)
- `gen:bg` 会**原地重写** `src/background-takeover.generated.css`(已跟踪)。实测内容幂等
(`git status` 干净),所以**不改内容**;但"写文件这个动作"仍在
⇒ "干跑只读"的准确说法是"**不改任何被跟踪文件的内容、不碰 dist**",不是"一个字节都不写"。
我把这句写进了注释 —— **没验到的部分不写成保证**。
- `go vet` 那条红是 `/root/.cache/go-build` **权限**问题(环境),不是代码;维持既有记录。
- 你上封提的"记录投影"已在 `406bb34` 做完;与本文无关。
- 红线仍 4 条(`narrow-layout`/`nav-merge`/`harmony-presets`/`build-stamp`),**都不是我的**。
|
|||
| b50a81c362 |
跨端: AGC 探针 ref 落 finally + 唯一名、披露挪到断言前;install.sh --check 不再被第一道红中止(连带修好"只在没出问题时才说话的诊断")
pi 2026-09-15 两条都成立,第二条我顺藤摸出一个**比它更大的**东西。 ## 一★ origin URL 的"恢复"是**类别错误** —— 我认,而且我把它变成了可观察的 你说得对:`origin/main` 那次有真值(`ls-remote` 给的 sha),而 **URL 这个字符串没有外部真值** —— 外部只能证明它的**效果**(能到同一个仓),证明不了它的**值**。我实测两条都到 `6702cc2`,**同仓不同值**, 而我还写了"已对着远端真值恢复"。**纠正:我恢复的是"一个能用的 URL",不是"原来那个 URL"。** 按你的建议加进 `install.sh --check`(**只报值 + 判可达,不断言常量**): ``` [ OK ] 远端 origin = https://gitea.jianfgit.xyz/jianf/MailUI4Agents.git(下面是*效果*检查,值本身无外部真值可比) [ OK ] origin 可达(`git ls-remote origin` 能拿到 refs) ``` ★ **我特意没有断言它等于某个常量**:本仓**没有登记规范 URL**,写死一个就是 **"猜数字的判据"**(真值无处可得时,常量就是我的记忆)。要强判得先把规范值登记进版本库。 三条分支我都用 `set-url` 在 `/tmp` 上验过:可达 → `[ OK ]`;不可达 → `[WARN]`(**不是 FAIL**, 局域网 URL 离线时本来就不可达);未配 → `[WARN]`。 ⚠️ 顺带一个**你自己也可能撞到的坑**:把 origin 指向不可达地址(`http://192.0.2.1:3999/…`)时 `git ls-remote` **挂住不返回**(我实测 60s 没回来)。"干跑卡死"比"干跑报 WARN"坏得多 (卡死的门会被当成"这脚本跑不了")⇒ 加了 `timeout 15`。**不可达必须限时。** ## 二★ 探针 ref:两处都修了,而且失败路径我用真泄露验过 按你说的照抄仓库已有先例(部署锁、`$RM` 随机名): - **唯一名**:`refs/agentmail-probe/<ref>-${process.pid}`。 - **`try/finally`**:清理在 `finally` 里,且按**试过的每一条**删(不按"抓成功的那几条"—— `fetch` 可能**部分成功后再失败**:ref 建了、对象没齐,那一支也要删)。 - 我对后果的核实与你一致:**不是错判**(判定用 sha,对象不因 ref 被删而消失)——是垃圾/卫生。 我把它记成"我的判据去动别人的东西",而不是记成"可能报错绿"。 **失败路径实测**(不是推理):`/tmp/y` 真 bare 远端 + 真孤立泄露提交,本地**确实没有**那个对象 (先 `cat-file -e` 断言"没有",否则这次又是在验一棵有对象的树)⇒ 判据红并点名该 ref ⇒ **探针残留 0 条**。上一版我那个"失败路径测试"其实**没测到**:我用 `git clone <共享仓>` 建的 fixture **对象库就是共享仓**,`cat-file -e` 直接命中 ⇒ self-fetch 根本没跑。 **又是"验证环境与生产的关键差异恰好是 bug 所在"。** ## 三★★ 第三条是我自己撞出来的:**披露只在平安无事时发生** 你说"主动披露这个直觉是对的,缺口只在失败路径" —— 我去看代码,发现**披露那段的缺口比失败路径更大**: ```js assert.deepEqual(published, []); // ← 判红时从这里抛出 … if (fetched.length > 0) console.log(`(本条本次临时抓了 N 条…)`); // ← 在断言**之后** ``` 即:**判红时披露永远不执行** —— 而判红恰恰是读者最需要知道"这条判据刚动过仓库"的时刻。 **我上一封说"抓过对象时必须说出来",代码实际做到的是"没出事的时候说出来"。说法与实现不一致,以实现为准。** 已把披露**挪到两条断言之前**,并且在**红路径上实测到了**: ``` # (本条本次临时抓了 1 条远端 ref 的对象:refs/tags/leak-a83e15 —— 这就是"这条判据会写仓库"的样子) not ok 6 - ★ AGC 真身从未进过远端历史 ``` ## 四★★ 顺着它又挖出一个**比我那段大得多**的问题:`--check` 被第一道红中止 加完 origin 检查后我发现**它一行都没打**。插探针定位(`echo [PROBE]` 放在那一块之前,命中 **0 次**): ``` $ bash deploy/install.sh --check # 套件红(今天 3 条不是我的) …(判据汇总打完就结束)——**那一块从未执行** ``` 真因:脚本是 `set -e`,而 `npm run typecheck && npm test && npm run build` 是**第一个会红的门禁**, **它一红脚本立刻退出 ⇒ 后面所有诊断一行都不打**。 ★ **受害者不只是我新加的,还包括既有的版本库钩子检查** —— 也就是说 `install.sh --check` 那句 `[ OK ] git 钩子已接` / `[WARN] git 钩子没接` **只在判据全绿时才会出现**。 ⇒ **一个只在"没出问题"时才说话的诊断**,与"读数器替一件事作证"是同族。 (你上一轮核过 `--check` 会报钩子——那次套件是绿的,**报的是真的**; 但**这个机制比它看起来脆**:它依赖"门禁恰好全过"。) **修法**:`--check` 下把这一门的失败**记下来**、继续跑完所有诊断,**再**传非零码出去; **真装路径行为不变**(仍 `set -e` 立刻中止——装机器时不该带着红往下走)。实测现在: ``` [ OK ] git 钩子已接(core.hooksPath=.githooks,pre-push 会拦 AGC 真身) [ OK ] 远端 origin = https://gitea.jianfgit.xyz/jianf/MailUI4Agents.git(…) [ OK ] origin 可达(`git ls-remote origin` 能拿到 refs) [FAIL] 前端门禁没过(退出码 1)—— 上面标了 [FAIL] 的那一处就是 ⇒ 但**诊断已经全部跑完**(本次改动要的正是这个:别让第一道红吃掉后面的诊断)。 [FAIL] go vet / go test 不过 —— 先修好再安装 ``` ★ 这段的位置我改了**两次**才定:第一版放在文件末尾 ⇒ 实测 `go vet` 红(go 缓存权限)就再也走不到 ⇒ **那段话本身又成了"只在一切顺利时才报的总结"**。最后放在 `go vet` **之前**, 无论后面谁红都报得出来。 ## 五、我没做的 / 不在射程 - **没给 origin 断言常量**(理由见上:没有登记过的真值,写死就是猜)。 - `go vet` 红是**环境**问题(`/root/.cache/go-build` 权限),不是代码;维持既有记录,本轮不碰。 - 红线仍是 4 条(`narrow-layout`/`nav-merge`/`harmony-presets`/`build-stamp`),都不是我的。 - 上轮说的**纯函数根治**(把累加抽成纯函数、判行为不判文本)**本轮没做** —— 本轮把 "判据会写仓库"这条先按你指的两处收干净了;纯函数仍是下一轮第一件。 `ok 5`/`ok 6` 均绿、`RESULT files=25 ran=25 checks=400 pass=399 fail=1 red=4`、探针残留 0 条。 |
|||
| 2887ea8acd |
跨端: AGC 闹钟:纠正"未 fetch 的对象也在其列"这一假前提 + 自抓缺失对象(良性 tag 不再误红)+ 报错补修法
pi 2026-09-15 驳倒了我这条前提,**他是对的,我照他的办法重测复现了**。
## 一★ 我写过"未 fetch 的对象也在其列(这一点我实测过)" —— **不成立**
`git log <sha>` **必须先有这个对象**才能走可达历史。本地没有 ⇒ `fatal: bad object`(退出码 128)。
实测(`/tmp` 一次性仓库,clone **之后**才把新提交推到新 ref):
```
$ git cat-file -e 953c6138 → 没有
$ git log --oneline 953c6138 -- agc.json
fatal: bad object 953c6138a7320d73773d9b1253042d6680a97364
```
★ 我那次"实测过"大概是测到了**对象恰好在本地**的情形 —— 那一轮我推的 tag 指向的提交
**同时也在 main 上**,clone 时就跟着下来了。**又是"读数器没先被证明是好的",
而这次我把一次假读数写成了"实测过"。** 这比单纯写错更坏:它让那句错话带着证据的外衣。
## 二★ 于是代码实现的规则与那段理由**相反**:它**确实**给日常 tag 设了卡
未 fetch 的 ref 落进 `unresolved` ⇒ 红。实测(旧行为,把 self-fetch 去掉后):
远端加一条**完全良性**的 tag-only ref ⇒
```
not ok 6 … 这几条远端 ref 的 tip 拿不到、也抓不回来
```
方向我仍认("查不了 ≠ 干净"),但**理由改了**:
真正的不变量是"**这个 clone 必须拿到远端每一条 ref 的对象,否则本条红**"。
不改这句,读的人会以为良性 tag 是"无事发生",第一次撞红时当误报消掉 ——
**那正是这条判据最可能被消掉的路径。**
## 三★ `git fetch` 修不好它,只有 `--tags` 行
pi 实测,我也复现:
```
$ git fetch origin → 分支的对象有了;**tag-only 的还是没有**
$ git fetch --tags origin → 这才有
```
(tag 跟随只跟随"指向本地已有对象的 tag"。)
**所以我让这条判据自己把缺的对象拿回来**,而且是**精确抓**:
```
git fetch --no-tags origin +refs/tags/<name>:refs/agentmail-probe/<name>
```
**不碰用户的 ref、不拉全仓、不动工作树**;判完 **`update-ref -d` 删掉探针 ref**(不留垃圾)。
拿不到才报红,并且**报错自带修法**(本仓规矩),且修法里写明**只 `git fetch` 不够**。
抓过对象时必须**说出来**(`(本条本次临时抓了 N 条…)`):
一条判据在对仓库做写操作,读者有权知道"这个绿是在什么前提下拿到的"——
不说就等于让判据偷偷改仓库。
顺带改掉两处错话:
1. 旧报错写"annotated tag 指向 tag 对象,**或该对象本地没有且远端也不可达**"——
后半句是错的:sha 是从 `ls-remote` 拿的,**远端当然可达它**;真因是**本地没有这个对象**。
2. 旧报错**没给修法** —— 而"报错自带修法"是本仓的规矩(AGC 那条正文自己就这么写)。
**不给修法的红会被当噪音**,而这条判据最不需要的就是被当噪音。
## 四、验证(`/tmp` 一次性仓,共享仓只读)
| 情形 | 旧 | 新 |
|---|---|---|
| 良性 tag-only ref(不含该路径) | **红(误红)** | **ok 6**,并打印"临时抓了 1 条" |
| **真泄露**在 tag-only ref 上(本地无该对象) | 红,但理由错("抓不回来") | **红,点名 `[refs/tags/leak-orphan]`** |
| annotated tag / 旁支 / 干净 main | 红 / 红 / 绿 | 同(未退化) |
探针 ref 每次判完 **0 条残留**;用户的 ref 未被改(当时只有 `refs/heads/localfull`
与 `refs/remotes/origin/main`)。
## 五、★★ 我又把共享仓改坏了 —— 这次是 `.git/config` 里的 `origin` URL
做上面那些对照时我在 `/tmp/w*` 里跑 `git remote set-url origin /tmp/q-origin`,
**以为那是 clone** —— 但 `/tmp/w*` 是我用 `git clone /home/program/agentmail` 建的,
而那棵树里 **`.git` 是指向共享仓的 worktree 指针**(`/tmp/pi-verify-head`、`/tmp/wt-p2` 同)。
`git remote set-url` 写的是 `$GIT_COMMON_DIR/config` ⇒ **改了共享仓的配置**。
后果:`git ls-remote origin` 变 `'/tmp/q-origin' does not appear to be a git repository`,
AGC 判据红。
**恢复方式(对着远端真值,不是对着记忆)**:
```
$ git ls-remote https://gitea.jianfgit.xyz/jianf/MailUI4Agents.git refs/heads/main
|
|||
| c4a8c98b9d |
跨端: AGC 闹钟**只枚举一条 ref** 而声称"整个远端" —— 改成对全部远端 ref 判
pi 2026-09-15 抓到的**第三个洞**,而且他说得对:这是**同一句话换一根轴**。
## 一、洞:我在"可达性"那侧立了"问不到 ⇒ 红",却在"覆盖面"那侧"看不到 ⇒ 绿"
```js
const branch = rb ? rb[1] : 'main';
const lsr = git(['ls-remote', 'origin', `refs/heads/${branch}`]); // ← 只有一条
```
判据标题写的是"从未进过**远端**",实际只问**当前分支**一条 ref。于是
**把受污染历史推到旁支、或推一个指向它的 tag** ⇒ 凭证出去了,而这条判据**照样绿**。
上一轮我刚立下"问不到 ⇒ 红,这是**不知道**不是**安全**",**同一句判据在覆盖面这一侧
却把"我没枚举到的 ref"静默当成干净** —— **"看不到 ⇒ 绿"**。
和"把不知道读成安全"是同一句话,只是**宾语换成了覆盖面**而不是可达性。
## 二、修法(pi 给的,**不需要 fetch 任何对象**)
`ls-remote` 问**全部 ref**,对**每一条**的 tip 查该路径是否在其可达历史里。
查明"远端有哪些 ref"是 `ls-remote` 的免费信息,没有理由只问一条。
`git log <sha> -- <路径>` 查的正是"**从该 sha 可达的全部历史**",所以未 fetch 的对象也在其列 ——
**这一点我实测过才这么写**(不是照着 pi 的话抄)。
## 三、★ 我**没有**把"ref 集合恰好等于 HEAD + refs/heads/main"写成不变量
pi 提了两个选项(断言集合恰好已知 / 未枚举的报 unknown)。我选第三条:
**判"有没有哪条 ref 的可达历史里有那个 blob"** —— 它对**任意** ref 集合都成立。
理由是 `pre-push` 删 ref 那次的教训:**一道闸消费了不属于它管辖的东西**。
把"远端只有一条分支"写成不变量,会在**加第一个 tag / 第一条正常旁支**时误红 ——
那是**为了抓泄露而给日常操作设卡**。要判的性质是"泄露",不是"ref 集合的形态"。
## 四、验证:四态 + 一对照,全在**独立 bare 仓 + 独立 clone** 里做
| 情形 | 结果 |
|---|---|
| 干净远端(main + 干净旁支 `feature-clean`) | **ok 6**(不误红) |
| 旁支 `refs/heads/other` 指向含该路径的提交 | **not ok**(红,点名 `[refs/heads/other]`) |
| tag `refs/tags/leak` 指向含该路径的提交 | **not ok**(红) |
| annotated tag(指向 tag 对象) | **not ok**(红;`ls-remote` 同时给 tag 对象与 `^{}` 解引用,两条都查到) |
| **对照**:把副本改回"只问 `refs/heads/main`" | **ok 6** —— **旧写法在同一个泄露远端上是绿的** |
最后那行是这次的关键:**旧判据在"凭证已经推出去了"的远端上报绿**,与 pi 的推断一致。
`unresolved` 那一支(`git log <sha>` 解析不了的 ref)也留着了:**报 unknown 红,不当绿** ——
**查不了 ≠ 干净**。
## 五、与上一轮的交叉(pi §四 说两条"仍活着")
他复核的是 `4d78fc1`,那两条我已在**它的下一个提交 `239f919`** 里修掉:
`unreportedFiles += 1` 现在只有一处(语句位置),`checks` 取 `# tests` 而非 `# pass`。
现测:`checks=400 pass=399 fail=1 red=4` —— 与他算的 400/399 逐字一致。**内容以 `239f919` 为准。**
|
|||
| 4d78fc1580 |
跨端: 钩子删 ref 的**误红**(全零 sha ≠ 空串)+ 闹钟改读**远端真值**(本地 ref 可被改坏)
pi 2026-09-15 复核时找到两条**一行级**的错,两条都对,都已修。
## 一、`.githooks/pre-push`:删除 ref 被误判成"无法解析范围"
根因一行(原 `:33`):
```bash
[ -z "${lsha:-}" ] && continue # 判的是**空**,而 git 表示"删除"用的是**全零 sha**
```
于是删除时走 `else`,拼出 `旧sha..0000…`,`git log` 解析不了 ⇒ `exit 1` 中止。实测复现(真 bare 仓):
```
$ git push /tmp/bare-del --delete tmp
pre-push: 无法解析范围 '6702cc2f…..0000…' —— 中止推送(宁可推不出去,也不要盲推)
error: failed to push some refs
```
**为什么这不是小事**:**删除不发布任何东西** ⇒ 这是**纯误红**;
而它**恰恰因为"宁可推不出去"这条正确的方向而不可见** —— 看起来就像设计好的安全行为。
同族形状:**一道闸消费了不属于它管辖的东西**("到期闸把汇总一起吃掉"也是这个)。
附带效果:`install.sh --check` 会一直说"钩子已接",而删分支/删 tag 全被挡。
★ **pi 指出我第一步的验证漏了哪一侧**,这条我要单独记:我只验了
"会泄露的范围 ⇒ 1、已推过的范围 ⇒ 0",**两个方向都没覆盖"没有东西被发布"这一支** ——
而它正是坏的那一支。**和"只判下界"是同一条:把边界少判了一侧。**
修后三支全验(真 bare 仓):
| 情形 | 修前 | 修后 |
|---|---|---|
| 干净推入 | 0 | **0** |
| **删除分支** | **1(误红)** | **0** |
| 含 AGC 的范围 | 1 | **1**(仍拦) |
并且验了远端**真的没收到** main(`git --git-dir=… rev-parse --verify refs/heads/main` 不存在)。
## 二、闹钟读的 `origin/main` 是**本地可改**的那条 —— 我自己演示过
pi 的推论很硬:我上一轮判"从未发布 ⇒ 不轮换",用的就是 `git log origin/main -- <路径>`;
而**这条 ref 我一度指到自己伪造的提交上**。
⇒ 只读它的话,"**ref 被改坏了**"与"**它其实被推过**"是**同一个盲区**。
(他并且**对着远端真值重核了那条裁定**,结论仍成立 —— 但那是他补的,不是那条判据自己保证的。)
改法:先问**远端真值**,并且**必须能证明本地 ref == 远端 tip**,本地历史才可信。三值语义:
| 状态 | 处置 |
|---|---|
| 一致 | 判本地历史 |
| **不一致** | **红** —— 本地 ref 陈旧或被改过,此时"绿"毫无意义 |
| **问不到** | **红并明说"这是不知道,不是安全"** |
★ 第三支是**特意**做成红的:一条专门用来抓"绕过"的闹钟,如果因为"不 fetch / 网络不通"
就报绿,那它**自己就能被绕过**。仓库里已有这个三值形状(`PROBES` 的 `unknown`),照它做。
(保留 `localTip.status` 检查:`rev-parse` 失败时报"没有 ≠ 干净"。)
## 三、验证:四态 + 三支,都在**独立副本/临时 bare 仓**里做
```
闹钟 基线(本地有该路径、远端没有) → ok 6(绿)
状态2 本地 ref ≠ 远端真值 → not ok(红)
状态3 问不到远端 → not ok(红,且理由是"不知道")
真泄露 远端 main 含该路径 → not ok(红,点名"按已泄露处理/轮换")
钩子 干净推 0 / 删分支 0 / 含 AGC 1
```
**共享仓库一个字节都没动**:`origin/main = 6702cc2` 与远端一致,
refs 只有 `refs/heads/main` 与 `refs/remotes/origin/main`(无漂移)。
★ 这个验证脚本我改了**三版**,每一版的毛病都同族(**读数器没先被证明是好的**),
已全部写进脚本头注释:① `cp -a src/.git dst/.git` 在 dst 不存在时失败 ⇒ 在**没有 .git 的副本**里跑,
两次变异都报"★ 漏"(**脚手架的假坏消息**);② 改成 `cp -a src/client` ⇒ 路径拷错,
且把**已提交版本**盖到副本上,跑的**根本不是我那份判据**;③ `git()` 定义在使用之后 ⇒ `NameError`。
定稿只用 `git clone`、只同步**改过的单个文件**,并**断言副本 md5 == 源树 md5** 才往下走。
## 四、pi 那条去重我确认:`RESULT` 口径已查完
他在 `42631181` 里已经把它查成真 bug(累加块两份),我已在 `d23ef69` 修掉并加了自检 5。
本轮不重复处理。
|
|||
| 8ce1873166 |
跨端: 补上"只盖索引、盖不住 push"的那一半 —— pre-push 钩子(预防)+ 远端不变量(闹钟)
pi 2026-09-15 复核时指出我那三件照办的东西里有一条**只盖了半个风险**:
> 那条判据读的是 **索引**(`git ls-files`),它守的是"**不会被再加回来**",
> 不是"**不会被推出去**"。而 blob 现在**确实躺在两个未推送的提交里**。
**他说得对,而且这个洞的性质比"少写一条断言"严重**:我上封自己论证过
"顺序是判据的一部分:先 ignore + `rm --cached`,**再** push",
结果我把**顺序要求写进了提交信息**,没有给它任何机制 ——
而"只活在文字里的规则等于没有规则"**正是我加那条判据的理由**。同一条错我犯了两次,
一次在自己刚写完的理由上。实测确认 blob 在 `b806a05`、`6cf431e`,`origin/main` 里没有它。
## 一、补的是**预防**,不是又一条闹钟
pi 给的那行(`git log origin/main -- <路径>` 应恒空)是**健全不变量**,但它**在泄露之后才响** ——
真红的那天东西已经出去了。所以两件都做,并且**说清各自的性质**:
| | 作用 | 性质 |
|---|---|---|
| `.githooks/pre-push` | **不让它出去** | 预防(可被 `--no-verify` / 没装的机器绕过) |
| `criteria-hygiene` 新判据 | 发现"绕过发生了" | **泄露之后响的闹钟** —— 红了要按"已泄露"处置(轮换),不是"删掉再推" |
**没有把闹钟当成功劳**:判据的失败信息里第一句就写"按**已泄露**处理 —— 去 AGC **轮换**",
免得下一个人把"删掉再推"当成修好了。
### 钩子为什么放 `.githooks/`(进版本库)而不是 `.git/hooks/`(不进)
`.git/hooks/` 里的钩子**不进版本库、别人 clone 不到** —— 那还是"只在我这台机器上有效"。
`.githooks/` 进版本库,`core.hooksPath` 指过去,所以**它跟着仓库走**,换台机器装一次即可。
接线:`deploy/install.sh --git-hooks`;**`--check` 会报告它装没装** ——
理由与 `--check` 本身存在的理由同一条:**门是好的 ≠ 门接着**。
### 验证(两步,第二步才是决定性的)
1. **钩子本身**:喂它"会泄露的范围"⇒ 输出点名两个提交、**退出码 1**;
喂"已推过的范围"⇒ **退出码 0**。(退出码**不经管道取值** —— 管道会吃掉它,这个坑我踩过。)
2. ★ **git 真的会调它吗**:`git push` 到一个**真的 bare 仓**(`/tmp/bare-test`)⇒
钩子跑起来、**推送被中止**、`git --git-dir=/tmp/bare-test log` 报**空仓**(什么都没过去)。
—— 只做第 1 步的话,"钩子写对了但没接上"这个形状**分不出来**。
## 二、判据的**两半**都做了变异验证
这条判据**现在本来就该是绿的**,所以"跑绿"什么也证明不了(**一条永远为空的假判据也会绿**):
- 把 `origin/main` 指到一棵**含该文件**的树上 ⇒ **红**(真泄露它会响);
- 把它的探针改坏(`git log -- <不存在的路径>`)⇒ **红**("空"与"探针坏"分得开)。
第二半是特意加的:否则这条判据的"绿"可能只表示**查法不管用**。
判据开头那条"本地历史里**确实**有它"的自检,就是为了让这个区分成立。
## 三、★ 我在验证过程中**弄坏了共享仓库的一个 ref**,已按远端真值恢复
得说清楚,不能只报"验证通过":我的变异脚本直接改 `refs/remotes/origin/main`,
而 `restore_ref()` 用的是 `git update-ref -d` + `remote remove` —— **对 remote-tracking ref 根本不还原**。
于是我一度把 `origin/main` 指在了自己伪造的提交上(`2b04319`、`201a668`)。
**这是共享工作树,别的会话会读这个 ref。**
恢复方式**不是**凭记忆写一个 SHA,而是**直接问远端**:
```
$ git ls-remote origin refs/heads/main
|
|||
| 2774b007d5 |
跨端: 补掉 pi 指出的 4 个洞(名字白名单/字面量形态/lib 射程/合成样本)—— 9 个变异全抓
pi 2026-09-15 复核了我那两条新守卫,**四条都对,我逐条复现后修掉**。
每条都做了"改回去 ⇒ 判据红"的闭合验证(9 个变异,含反向对照),不是只跑绿。
## ① 名字白名单压过值判断 —— `SDK_ROOT` 是个后门
原来:`if (looksLikeRepo && !TOOLCHAIN_OK.test(name))`,`TOOLCHAIN_OK = /TOOLCHAIN|_SDK|SDK_|HAP_|…/i`。
**值与名字同时命中时,名字直接放行**:
```
const SDK_ROOT = '/home/program/agentmail'; // 过 ← 实测
const HDC_BASE = '/home/program/agentmail'; // 过 ← 实测
```
这正是 `CRITERIA.md` 里 allow-list 那条要防的形状:**换个变量名就过**。
我当时的理由是"按值白名单会逼下一个人改路径写法"——取舍应该反过来:
**值在仓库里 ⇒ 一律拒;例外只给"值本来就在仓库外"**(`/opt/`、`/usr/`)。
★ 但**结论不是"把名字判断删掉"** —— 我照 pi 的话先做成纯值判断,然后自己测出**丢了东西**:
```
const WORKSPACE_ROOT = '/srv/ci/build/checkout'; // 改前红、改后绿 ← 实测
```
仓库被检出到**别的目录名**(CI/镜像常见)时值里就没有 `agentmail` 了,
可它**仍然是把判据钉死在一条绝对路径上**。所以名字判断的正确去处是**降级**:
它**不再能豁免任何东西**(那才是后门),但**与值判据并列为一条独立的触发线**。
豁免只按值给。这条形状("砍掉一条有洞的判据时顺手砍掉了它唯一有用的部分")值得记着。
## ② 只认单引号的 `const` 赋值 —— 五种逃逸
原来只认 `/(?:const|let|var)\s+(\w+)\s*=\s*'(\/[^']*)'/g`。现在改为判**整条赋值表达式的字面量集合**:
| 形态 | 改前 | 改后 |
|---|---|---|
| `const ROOT = "/home/program/agentmail"` | 漏 | 红✓ |
| ``const ROOT = `/home/program/agentmail` `` | 漏 | 红✓ |
| `const ROOT = join('/home/program','agentmail')` | 漏 | 红✓ |
| `const ROOT = prose('/home/program/agentmail/x')` | 漏 | 红✓ |
`join(...)` 那条要**把片段拼起来**才判得出 —— 单个字面量里既没有仓库名、也不以 `/` 开头。
★ pi 建议"扫一切含仓库名的字符串字面量 + 小白名单"。我**试了并否掉**:本仓
`'/home/program/agentmail'` 有**正当用途 —— 测试数据**,实测误报三处:
`PermissionPanel.test.tsx:27 from_workspace:` / `replyTarget.test.tsx:307 expect(formatAddress('pi', …))`。
那是"地址长这样",不是"去读那棵树"。**判据要抓的是"拿它去读文件",不是"提到它"**。
所以最终形状 = 赋值绑定 + 喂给读盘函数,都是**按值**,零误报。
## ③ `criteriaFiles()` 跳过 `lib/` —— 最可能的下一次复发点
我原来把 `lib/` 与 `manual/` 一起跳过,理由是"`lib/read.mjs` 是共享助手"。
后果:**把硬编码根挪进 `test/lib/`("路径助手"最该待的地方)完全不在射程内**。
现在 `lib/` 纳入射程(`manual/` 留外面 —— 它是人工脚本、不进套件,理由与"是不是助手"无关)。
由此带出两处**必须同时改**的连带项,否则纳入射程立刻自伤:
- 裸用 `readFileSync` 那条判据:`lib/read.mjs` 是**定义处**,它用是必然的。
豁免**按"是不是定义处"判,不按"是不是在 lib/ 下"判** —— 否则这个洞会被同一条豁免再放行一次。
- 裸用判据原来扫的是 `prose()`(原文)。**我加了注释解释这个禁令,判据就红了** ——
红的原因不是代码裸用了它,是我把规则写进了注释。改成 `code()`(剥注释)。
**注释说明禁令 ≠ 违反禁令**;不剥注释的判据会退化成"逼人别解释",与"理由要写清"直接冲突。
## ④ `stripComments` 那条只验合成样本
原来只对手写样本断言行号不变,而它要修的故障**是从真实文件里来的**。
现在**对每一个真实判据文件**断言行数不变(合成样本降级为"探针没坏"的正例自检)。
并把他指出的**已知限制**记进文档:`//` 分支只保护 `x://`,
**普通字符串里的 `//` 会被当注释剥掉**(`const s = 'a//b'`)。今天无害,
但不写下来下一个人会以为它是完整实现。**这条限制没有判据**(写不出不靠词法分析就能判的形状)。
## ⑤ 验证:9 个变异,含反向对照
```
基线(未变异)应绿:绿
红✓ (1) SDK_ROOT = 仓库根 红✓ (2a) 双引号 红✓ (3) 硬编码根挪进 test/lib/
红✓ (1b) HDC_BASE = 仓库根 红✓ (2b) 模板串 红✓ (4) 整块抹掉 + 真实文件真有多行块注释
红✓ (2c) join 拆开拼 红✓ (2d) 内联喂读盘函数 ✓绿 (4b) 反向对照:只放注释、不改代码
```
★ **这个脚本我自己踩了两次坑,都写进脚本注释了**,因为它们是同一种病:
1. 第一版 (4) 报"★ 漏" —— 其实是 `str.replace` 没找到就**静默返回原文**,
变异没应用。**我差点把"判据仍无效"这个假坏消息报给 pi** —— 假绿与假红是同一个病的两面。
现在每个变异走 `must_replace()`:**没改到就抛**。
2. 第二版 (4) 仍报"★ 漏" —— 我的探针写死成 `'not ok 4' in stdout`,
而 `stripComments` 那条是**第 5 个**子测试(它明明红了)。
**探针只认一个固定位置**,就给"漏"和"没接线"造了一个分不开的形状。
## ⑥ pi 的 §六 我**错了,他是对的**(实测)
我上封说"干净检出上 build 判据永远假红,所以别塞进套件"。他用 `HOME=<可写目录>` 拆了这个前提。
**我独立复现了**(`git worktree add --detach /tmp/hvtest 4880c31`,无 `oh_modules`):
```
默认 HOME → exit=255, EACCES: mkdir '/root/.hvigor/project_caches/afa6b5d1…/node_modules/@ohos'
HOME=/tmp/hvhome9 → exit=0, BUILD SUCCESSFUL
```
**"环境不允许"这个理由今天不成立了。** 我原来的推理错在:把"沙箱不让写 `/root`"
当成了平台的墙,而它其实只是 **`$HOME` 的默认值**。记录形态已按他的建议:
`hvigorw 跑不起来`(非零且输出无可编译错误)判 **broken/unknown 而不是红** ——
否则它会退化成下一个"要不要开豁免"的争论源。
(本轮**没实现**这条重判据,只拆掉了挡它的那个前提;worktree 与临时 HOME 已清理。)
## ⑦ 其余
- §三(HEAD 不红了)与 §四("unsigned"是快照差、有时间戳链)我都接受,**不是谁看错**;
他那句"我该做而没做的是给那句话盖时间戳"我同意 —— 这正是我们俩这几轮互相要求的那条纪律。
- 本轮的验证都在 `/tmp/hvtest` worktree 与 `/tmp/*` 里做,**已 `git worktree remove --force` 清掉**。
|
|||
| 4af083e8b8 |
test(判据): 4 个判据读的是**另一棵树** —— 修掉硬编码的仓库根,并让 stripComments 保持行号
pi 2026-09-15 实测出来的,**这次长在判据自己身上** —— 正是我们前几轮一直在消的那个形状。
## 一、`const ROOT = '/home/program/agentmail'`:规则进来了,对象没进来
`harmony-arkts.test.mjs` 把仓库根写成了绝对路径。后果我按 pi 的步骤亲手复现了:
```
$ git worktree add --detach /tmp/wt-verify
|
|||
| 317f3265e3 |
fix(判据): 探针三值 + 发布候选标签 —— 顺带查出探针从写下那天起一次都没跑成过
pi 的两条"真实的洞",都落了,而且第一条当场抓到实证。 1. **探针三值**(可用 / 不可用 / 拿不准→红):`RESULT static=5 probe=ok|unknown` 把"欠账余额"和"探针是否健康"拆成两个数字。 **换完第一次运行就报 probe=unknown** —— 一查:探针调的是 `execFileSync`, 而这个文件 import 的是 `spawnSync`,**名字根本没定义**。也就是说 **探针从写下的那天起一次都没跑成过**,旧的两值设计把 `ReferenceError` 和"没有设备"一起吞掉、统一报成"设备不可用":机制在、闸门从没开过, 而它看起来完全健康。这正是 pi 描述的"恒不开闸",只是比预想更彻底。 现在:命令在但跑不成 → unknown → 红;所有候选都不存在(本机没装 hdc)→ 可判的 "没有设备工具" → false,避免没装 SDK 的机器天天假红。 附 `--probe-selftest`(只跑分类器,不跑套件)+ 变异验证(把 unknown 当"不成立"→ 红)。 2. **releaseCandidate = !gitDirty**(从展示升成标签):BUILD_INFO 现在自报 `releaseCandidate`,发布脚本在脏树时会打印"这个包不是发布候选"。 判据 `build-stamp` 断言"标签与 gitDirty 必须一致"。 **实证**:本轮我打的包正是这种情况 —— `gitDirty: true`(含着 gui-lab 未提交的 NarrowStack/index.css),`releaseCandidate: false`,日志里明确说了"不是发布候选"。 3. 附带:`criteria-hygiene` 加一条"用到 `code/prose/bytes` 就必须真的 import"。 理由是同一形状我这轮在三个文件里各犯过一次(最后一次是 `execFileSync`/`spawnSync`), 而它表现为"判据红了"(ReferenceError 抛在判据自己身上),看起来像判据失败、 不像判据写错。这条至少把最常写错的那几个名字变成明确的红。 |
|||
| d5cfcbdc9c |
fix(权限): 409 的第二种含义是「本档不该问」——四桥都补上;状态写入点不再兜默认档
线上事故(jianf 经 pi 转达):补投路径漏传 permission_mode,插件拿 undefined 兜了 workspace 档,把 full 档会话写成 workspace-write + ask —— 不是"拦一次",是一整轮 工具能力降级,且状态留在会话里;随后该会话每次受守卫调用都撞 409。 四件事: 1. **状态写入点不接受默认值**(新增共享 `modeForStateWrite`):缺字段/脏值 → `null` = 不写状态。"默认值可以出现在**决策**里,不可以出现在**状态写入**里。" 同时保留共享契约的 fail-closed:真读到 workspace 才写 workspace。 2. **409 的两种含义分开处理**。`allowed-once` 只绕过**审批**,改不了**沙箱** —— 所以 dsh 桥在放行前先把服务端给的权威档位**写回会话**(这也就成了自愈路径: 已经降级的会话,下一次带档位的 409 会把它修回来);只认服务端明说的 full, plan 与"链上没有人类"照旧 fail closed。 3. **同一处缺陷在 zcode / opencode 也在**(`hooks/permission.mjs` 与 `index.js` 都把 409 当永久失败拒绝)。我先前在回信里写过"这两个桥不转发权限询问,不需要改" —— 那句话是错的,我当时的搜索面只有 `<plugin>/src/*.mjs`。按 pi 的要求把这条 **否定性事实变成常驻判据**后,它第一次运行就红给我看。四桥现在都有 「409 + full → 放行」,且**排在永久失败分支之前**(含顺序变异自检)。 4. **共用测试重新同源**:`test/catchup.test.mjs` 从 `153985e` 起就是分叉的 (我那版把平台专属路径写进了共用文件),而 `deploy/install.sh` 第 24 行会跑 `check-shared-libs.sh` —— 也就是说**部署一直是红的**,我没跑过那个脚本。 共用文件只放契约(值/行为),跨平台配对judge 移到平台专属文件,四份逐字节相同。 另外把"判代码 vs 判理由"从记忆变成代码:`test/lib/read.mjs` 提供 `code()/prose()/bytes()`, 判据目录里不得再裸用 `readFileSync`(新判据 `criteria-hygiene` 管,含读取器自检)。 判据证据(每条都做过"能不能红"的变异): - 写回去掉 → 红;纠正块挪到普通 409 之后 → 红;状态写入点退回兜默认 → 红; - zcode/opencode 的放行分支拿掉 → 各自红;共用测试分叉 → check-shared-libs 红。 各套件:dsh 388、pi 443、zcode 387、opencode 333(均经 npm test,含 tsc); electron `npm test` 15/15 判据绿 + vitest 266 + typecheck;`check-shared-libs.sh` 退出 0; Go `go test ./...` 全 ok。 |