fix: post-merge audit 第二輪 —— 讓 --retroactive 的 gate 真的執行,以及 Codex 那批未修的 CRITICAL - #320
Conversation
跨模型(Codex)在 post-merge audit 指出四個 CRITICAL:`.idd` 可以是 symlink、breadcrumb 的 `>` 會跟隨 symlink、目的檔只用 `[ -f ]` 檢查、 `find` 只比對檔名。#319 修掉了路徑切割那個(`-print0`),這四個沒修。 共同根因是同一句沒被說出口的假設:**這些路徑都是一般檔案與一般目錄**。 腳本沒有檢查它,於是由 shell 自己「跟隨 symlink」的預設替使用者的資料 做決定。`--apply` 預設走遍整個 ~/Developer,所以「我 clone 下來的 repo」 就在威脅模型內,而上面每一種形狀都能被 commit 進 repo 裡。 - `.idd` 是 symlink → `mkdir -p` 靜默接受並回報成功,config 被搬出樹外 - 目的檔是目錄 → `mv` 把檔案搬**進去**,變成 `local.json/issue-…json`, 然後印「✓ migrated」 - 目的檔是懸空 symlink → `-e` 為 false,沒有任何警告 - 目的檔是指向樹外一般檔的 symlink → `-f` 跟隨它,稽核印出「both present, current wins」——一句在「current 其實不在這個 repo 裡」時為假的話 - breadcrumb 路徑是懸空 symlink → `-e` 看不見,`>` 直接**建立**目標 - `-name` 會撈到 doc sample / fixture,在別人的樹裡憑空造出 `.idd` `-f`、`-d`、`-e` 都會跟隨 symlink,所以順序不是風格問題:`-L` 必須先測, 否則一個連結是由它指向的東西受審。 `mv` 沒有原子的 no-clobber——檢查與 rename 是兩個 syscall,中間有窗口。 `ln` 在目的存在時原子失敗,所以 hardlink-then-unlink 給得出 `mv` 給不出的 保證,且中途被打斷時留下兩個 link 指向同一 inode(config 兩邊都讀得到, 不會兩邊都沒有)。`mv -n` 是另一個候選,但 BSD 上它的 exit status 分不出 「已搬移」與「因目的已存在而跳過」。 測試:hostile tree 自成一個 scan root、自己一次 `--apply`(拒絕算 failure, 混進 happy path 會讓「apply exits 0」變成空話)。7 種形狀 + 1 條回歸鎖 (`find -type f` 用 lstat,所以 legacy 路徑是 symlink 時本來就不會被跟隨; 釘住它是因為這個保護藏在一個 flag 裡、看不見,將來有人改成 `find -L` 會安靜地開始跟隨)。30 assertions,全 suite 47/47。 acid(逐條還原、確認 suite 會紅):7 個機制裡 6 個單獨可轉紅。`ln` 在預設 組態下被前面兩個守衛遮住,把它們一起關掉、只變動 ln↔mv,紅的數量從 4 變 6——它是活的縱深防禦、不是裝飾。**mkdir 之後的 .idd 複查(M2)沒有測試 重量**:它只窄化 TOCTOU 窗口,單一 process 的測試碰不到,如實記在這裡。
`rows` 用 `\t` / `\n` 兩字元跳脫存放、再用 `printf '%b'` 印出,而 `%b` 也會 展開**資料裡**的跳脫。所以路徑或 `github_repo` 裡的一個反斜線是被執行、 不是被印出。sanitizer 擋掉了控制字元與 bidi mark,從來沒碰過反斜線——當時 下游沒有東西會解讀它。 `\c` 最糟:`%b` 遇到它會停掉**全部**輸出。一個被構造的值就能安靜刪掉它後面 的每一列與 totals 行,而少了一半列的地圖,讀起來跟「這台機器本來就只有這些 repo」一模一樣。`\n` 造出獨立的偽列,`\t` 位移欄位。 **更正一項我在 audit 裡寫錯的話**:當時我說「`--json` 路徑是安全的(jq 會 正確跳脫)」。那句話只對 newline payload 成立。同一個 `%b` 也餵給 jq,所以 反斜線 payload 一樣會截斷 JSON 那一列(實測 `config_format` 變成 `null`)。 我測了我寫的那段(sanitizer),沒測它與 printf 相接的那個縫。 改法:列緩衝直接放真的 TAB / NEWLINE,三處 `%b` 改 `%s`。資料不可能再帶回 真的控制字元,因為 sanitize_field 會刪掉 tab、把 newline 折成空白。 `find` 的退出碼:process substitution 會丟掉它。這件事在別的掃描器只是少幾 列,在這裡是要害——這張地圖的全部工作就是回答「這一層在不在」,而一個讀不 到的目錄回答「這裡沒有 config」,跟真的沒有 config **無法區分**,那正是 #301 的錯誤向上解析。改成先導向暫存檔、檢查退出碼、不完整時明講。順帶把 `-print` 換成 `-print0`(同 #319 對 migrate 的處置,消掉含換行目錄名那一類)。 新增 `refute_grep_re`(+ 自身 3 條測試)。這個 helper 值得存在,是因為固定 字串的否定斷言是本 repo 最容易寫錯的一種:`o/FORGED-ROW` 合法地出現在它被 寫進去的那一列**之內**,`refute_grep` 會因為一個測試根本沒打算檢查的理由而 失敗。要錨定就需要 regex。 測試自身也修過一次:第一版用 printf 寫 payload,檔案裡是 `\c`——那不是合法 的 JSON 跳脫,jq 直接拒收,值根本沒進到列緩衝,於是**探針通過、卻什麼都沒 測到**。改用 quoted heredoc 讓磁碟上真的是 `\\`,並加一條 probe self-check 斷言 payload 確實活著到 jq 之後。 acid:`%s`→`%b`(含還原跳脫式列緩衝)紅 2 條;拿掉 find 退出碼檢查紅 1 條。 全 suite 47/47。
`idd-close/SKILL.md` 原本自己寫著:「本 gate 是散文,不是機械強制…機械判定
只存在於 check-closed-without-summary.sh,而本 skill **並未呼叫它**…未做」。
揭露是誠實的,但揭露不是緩解——**七輪 verify 的全部成果,要等 agent 剛好讀到
那張表才生效**,而它守的是一個不可逆動作(在可能已有 summary 的 issue 上再
貼一份)。
helper 新增 `--issue N` 單一 issue 模式,輸出一個 JSON 物件,並且**用退出碼
下判決**:
0 class == missing 且 comment 集合完整 → 唯一放行
1 其他分類(compliant / casing / present) → 拒絕
2 無法判定(未 CLOSED / 截斷 / 抓取或解析失敗)→ 拒絕
審計模式的「永遠 exit 0」契約原樣不動(有斷言釘住)。那條契約是對**人**報告
用的;gate 是不可逆動作的前置條件,一個永遠 exit 0 的 gate 不是 gate——呼叫端
只能去解讀散文,而那正是這七輪維持 advisory 的原因。
gate 模式另外**不走** `--json comments` 那條路:它用 REST `--paginate` 抓
comment。那條巢狀 connection 硬上限 100 則且回**最舊**的 100 則,而 closing
summary 依定義是**最新**一則。審計端是事後修補截斷(#319),gate 端根本不走。
fail-closed 的範圍寫死在 skill 裡,包含「helper 不在」——找不到 gate 等於沒有
gate,不是「那就跳過吧」。
prose-drift 測試補兩類斷言:(1) skill 必須真的**執行** helper(比對呼叫式與
退出碼分支),不是只「指向 normative source」——指向正是它散文化七輪期間一直
在做的事;(2) helper 必須真的有那個 flag,否則 skill 呼叫一個不存在的旗標會
以最壞的方式 fail open(未知旗標只警告不中止,等於把審計的 always-exit-0
搬到破壞性路徑上)。
順帶修掉 drift 測試的 canary:它往 repo 寫固定檔名、沒有 trap。中途被打斷就
留下一個檔案,之後每一次執行都紅、且會被 `git add -A` 帶進 commit;固定檔名
還讓兩個並行執行互刪對方的 canary,各自把對方的刪除讀成「掃描看不到植入的
字串」。改成唯一檔名 + trap。
測試自身修過兩次:
- `jq -r ".comments_complete // \"null\""` 把真正的 `false` 報成 "null"
(jq 的 `//` 視 false 為空),那條截斷斷言因此不可能失敗。改 `tostring`。
- 一個斷言字串裡的反引號被 shell 當成命令替換執行掉了。
acid:G1 截斷仍放行→紅 2;G2 非 missing 仍放行→紅 4;G3 未 CLOSED 不再拒絕
→紅 1;G5 skill 不再呼叫 gate→紅 1。G4(--issue 整數驗證)第一輪**沒有測試
重量**——拿掉它 `--issue abc` 仍然 exit 2,只是改由下游 jq 崩潰造成,退出碼
分不出兩者。補一條斷言檢查錯誤訊息確實來自驗證本身後才轉紅(那條驗證真正
要守的是 live 路徑:數字會被插進 API URL,中間沒有 jq 擋著)。
全 suite 47/47,classifier suite 114 assertions。
`lead_line` 找「讀者實際看到的第一行」時會跳過整行 HTML marker(本 plugin 要求
機器可定位的 marker 放第一行,不能讓它把一份逐字正確的 summary 降級)。
`lead_has_content` 掃後續行找內容時**沒有套同一條規則**,於是 HTML 註解被算成
summary。
## Closing Summary
<!-- TODO: write this up properly -->
判成 `compliant`——**印在任何區段都沒有**,而 `--retroactive` 也拒絕它。那正是
內容判定當初要關掉的靜音管道,在下一層又開了一扇門。非 canonical 大小寫的同一
形狀則落 `casing`,那一類是**不帶 ⚠ 印出、並宣稱「summary 在」**。
抽出 `invisible_line`,兩邊共用。對讀者來說空行與看不見的行是同一件事,在這裡
就該是同一件事。
**方向誠實話**:這整類在**便宜**的一側——錯的答案是把 issue 藏起來,不是授權
重複貼文。仍然修,是因為前一輪明講要關掉這個管道、卻只關了一扇。
只認**單行** HTML 註解,這是刻意的:跨行的需要第 1-4 輪移除掉的 fence/comment
狀態機,而殘留誤差同樣落在便宜那一側。
fixture #162 / #163 + 6 條斷言;acid:拿掉後續行的可見性過濾 → 紅 3。
全 suite 47/47,classifier 120 assertions。
normative source 把 predicate 拆成兩個,判準不是「哪個比較準」而是**過度偵測
會往哪個方向錯**:`present_re`(寬)回答「讀者看得到嗎」,多報一次只是不採取
破壞性動作;`lead_re`(嚴)回答「這則 comment 是不是以它開頭」,多報一次就是
講了一句假話。這條規則不只屬於 classifier,但有兩個散文 reader 站錯邊:
- **`idd-find`** 用寬鬆比對標 `📜 closing summary(可考古的結案紀錄)`。一則
只是引用模板來提問的 comment 會拿到一模一樣的標籤。改成 `📜 summary marker`
——**標籤的字面就是它能保證的全部**。比對維持寬鬆(搜尋結果漏報的誤導成本
更高),改的是它敢宣稱什麼。render 範例那行一起改:只改被點到的那行、留下
隔壁那行,正是這個 marker 五輪漂移的原樣。
- **`idd-update`** 的 phase 推斷同樣用寬鬆比對,而 phase 是正面斷言(「這個
階段發生過」)。一則引用 `> ## Closing Summary` 的討論就能把一張**還開著**的
issue 推成 `closed`。改成要求該 heading 是那則 comment 的首行、不帶 blockquote
前綴、縮排 ≤3 空格 —— 即 `lead_re`。
**附件 URL 抽取**:字元類只排除 `)` 與空白,也就是只涵蓋 markdown link。實測
另外三種真實 issue body 的包裹方式全部把包裹字元黏了回來:
<https://…/a.pdf> autolink → 尾巴多 `>`
<img src="https://…/b.png"> HTML 屬性 → 尾巴多 `">`
see https://…/c.pdf. 句尾 → 尾巴多 `.`
三種都會 404,而抓不下來的附件就是被忽略的來源 —— 本 plugin 把那當成違規而非
不便。`<` `>` `"` `'` 不可能未編碼地出現在 URL 裡,排除掉是零成本;句尾標點改
成事後去掉一個(`.` 在 URL 中段合法,而這些網址都以副檔名結尾)。
**jq 錯誤路徑**:註解寫著「strip control characters and bidi overrides」,程式
只做前者 —— `tr` 在 LC_ALL=C 下處理**位元組**,看不到多位元組的 U+202E / U+2028。
先量了這個縫**通不通**:三種 payload(U+202E / U+2028 / ESC)放在會讓 `.state`
撞上裸字串的位置,jq 1.7 回 `Cannot index string with string ("state")`,**完全
不回顯輸入值**。所以改的是註解不是程式碼,並如實記下這個乾淨的否定結果 ——
補一份 Unicode-aware scrub 等於多一份字元類副本,而「安全定義的副本各自漂移」
正是這個檔案最長的病史。哪天遇到會回顯的 jq,要改的就是那一行。
新增:process-attachments 的 `wrapped` gh-stub mode + 5 條斷言(RED 3 條);
prose-drift 補 3 條斷言釘住兩個 reader 的方向。全 suite 47/47。
#288 的 closing summary 說「每個 scratch path 現在都掛在 `mktemp -d` 底下」。 實際出貨的是一段**這樣寫的註記**,後面接著二十四處繼續 MUST 舊的固定路徑 —— 包含交給 reviewer agent 的 OUTPUT 指示,也就是真正決定 findings 落在哪裡的那 幾行。規則和違反它的例子相隔四行,共存了兩個版本。 **一份先寫規則、隔一行又自打嘴巴的規格,比什麼都沒寫更糟**:去查規則的人查得 到,照著抄命令的人抄到缺陷。 固定名稱不帶 repo 身分:同一個 issue 號在不同 repo 的兩個 session 共用檔名, 前一輪的殘檔會被當成這一輪的 findings 讀進來 —— 靜默,且方向最壞(把別的 repo 的判決併進這份報告)。 全部改掛 `$VERIFY_DIR`(Step 0 新增 `resolve_scratch_dir` task,在任何寫檔或 spawn 之前解析一次)。過程中另外撞到兩個**同一段迴圈的兩份副本**——egress body (`master.md` / `pointer.md` / `pointer_template.md`)也用固定名,而那是要被 **貼到別人 issue 上的文字**:共用路徑上的殘檔或半寫檔不會大聲失敗,它會發布錯的 留言。兩份都改了;第二份是機械掃描抓到的,不是我看到的。 新增 `scripts/tests/verify-scratch-paths/`(第 48 個 suite):掃固定 /tmp 路徑, 帶 positive control(唯一檔名 + trap)。 **scope 明講**:只掃 `idd-verify`。同一個 grep 也會打到 `idd-edit` 的 `/tmp/idd-edit-backup/` 與 `/tmp/idd-edit-repl-*` —— 那是另一回事、**未涵蓋**: backup 目錄是文件叫使用者去 `ls` 的復原位置,搬它是行為變更,且碰撞後果是看得見 的衝突而非被靜默併入的判決。這條寫進測試檔本身,免得有人把它的綠讀成對 idd-edit 的背書。 註記本身也不再逐字寫出被禁的舊路徑 —— 同 prose-drift 測試禁止複述 classifier regex 的理由:不存在的副本不會漂移,而寫進文件的違例字面正是機械檢查第一個踩到 的東西。 全 suite 48/48。
#294 依使用者原話把 `Question for you` 那一欄從描述式分析改成問句 —— > 「iddclarify我覺得你可以直接放對使用者的問題」 理由在讀者是誰:那一欄給的是**要做裁決的人**。寫成分析,他讀完還得先把分析 還原成一個可回答的問題。 出貨的是**規則**、一張 ❌/✅ 對照表示範規則 —— 然後四十行後的 worked example 第三欄還是分析(`分群變數 / distinguishing variable (per K-means context…)`)。 照規則做的人得到新行為,照範例抄的人得到舊行為,而**範例才是會被抄的那半**。 跟 #288 的暫存路徑同一類:先寫規則、再在自己的範例裡違反它。 改掉兩列,並新增 `scripts/tests/clarify-question-column/`(第 49 個 suite): 掃所有 `surfaced` 範例列的第三欄,沒有問號就紅(`...` 佔位、`passed`、 `deferred` 三種豁免——前者不是措辭範例,後兩者沒有東西可問)。帶 positive control(唯一檔名 + trap)。在修改前的樹上跑會紅,已驗。 全 suite 49/49。
#292 之後 **attended** 的 `/idd-all` 走 `/idd-plan`(`EnterPlanMode` 閘門住在 那裡、不在 `/idd-implement`)。`idd-all` 更新了,`idd-plan` 的「與 idd-all 的 整合」段沒有,而且它把舊規則寫成**全稱句**: > **idd-all 不該走 Plan path**…應該 fallback 走 Simple path 那句話的 unattended 半段是對的。錯在把一個分支的結論寫成無條件的 —— 以 idd-plan 為準的讀者,會得到與實際執行相反的結論。 改成兩欄分述(attended / unattended),並明講 normative source 是 `idd-all` 的 dispatch table。這正是 repo 自己那條規則說的:**能列舉的就列舉,不要先寫一句 總括判準再補例外** —— 總括與例外是兩份不會一起改的規格。 新增 `scripts/tests/plan-routing-consistency/`(第 50 個 suite):先對 `idd-all` 斷言 routing 本身仍成立(否則下面那些斷言只是在釘一份虛構),再對 `idd-plan` 斷言分述存在、全稱句消失。在修改前的樹上跑紅 4 條,已驗。 全 suite 50/50。
|
追加一個 commit:#317( 同一類缺陷,所以併進來而不是另開 PR:#292 之後 attended 的 改成 attended / unattended 兩欄分述。新增 Refs #317。Suites 49 → 50。 (上一則留言是我用雙引號傳 body,裡面的反引號被 shell 當成命令替換執行掉了 —— 本 session 第四次踩到同一類「探針/輸出被 shell 吃掉」的坑。以本則為準。)
採 #315 的選項 1:把 implement 自己記下的外部寫入清單塞進 reviewer context,兩個 backend 都給(給不同 context 會讓一個 finding 取決於當時解析到哪個 backend)。沒有紀錄時報 UNKNOWN 而非「沒有外部寫入」—— 漏跑的 sweep 與跑了沒找到的 sweep 痕跡一樣。 不做選項 2(machine-readable manifest):那是兩個 skill 之間的新契約,而整輪 audit 的結論正是「沒經過自己那輪 review 的新契約就是缺陷住的地方」。理由與殘餘風險寫進 CHANGELOG。 過程中又踩了一次同型缺陷並記在 commit 裡:第一版插入點鎖在只有 requirements prompt 才有的 attachment 行,五個 prompt 只改到一個。測試現在用數的:annotated 數必須等於 prompt 數,且 prompt 數 ≥4(防 0 == 0 的空洞相等)。 PR 現況:10 commits、25 files、+1112/−108、suites 47 → 51、全綠。涵蓋 audit 的全部剩餘 findings 加上 #317、#315。 這個 PR 沒有經過任何獨立驗證 —— 每個機制我都做了 acid(逐條還原確認 suite 轉紅),也自己抓到並修掉四個壞掉的探針,但那全是同一個人同一個模型。本輪的核心發現就是「沒被獨立看過的東西會出事」,所以這句話寫在這裡而不是留白。 |
`idd-verify` 的 scope 是 diff,但 `idd-implement` 的 sister sweep 與 cross-reference note **會寫到 diff 以外的表面**:別的 issue 的 comment、別的 repo 的新 issue。沒有 任何 lens 看得到它們。 實例(`macdoc#143`):實作筆記裡一個事實錯誤被原樣擴散到另一張 issue 的參照 note。 四個 lens 都只評了 diff 內的措辭,是 devil's advocate **越出自己的 scope** 去讀 Implementation Complete 的 blast radius 紀錄才抓到。**某個 reviewer 臨場越界不是 機制。** 把 implement 自己記下的外部寫入清單塞進 reviewer context,**兩個 backend 都給** —— pai 與 manual fan-out 拿到不同的 context,會讓一個 finding 取決於當時解析到哪個 backend,而 skill 自己的契約說 Step 3 之後兩者可互換。 **沒有紀錄時報 UNKNOWN,不報「沒有外部寫入」**:漏跑的 sister sweep 與跑了但沒找到 的 sister sweep,留下的痕跡一模一樣。 過程中自己踩到一次同型缺陷:第一版的插入點鎖在只有 requirements 那個 prompt 才有 的 attachment 行,於是**五個 prompt 只改到一個**、另外四個靜靜留著舊 context —— 正是這一輪一直在抓的「改了被點到的那行、留下隔壁」。改成鎖每個 prompt 都有的 `Diff path:` 行,並在測試裡**數**:annotated 數必須等於 prompt 數,且 prompt 數 ≥4(防 0 == 0 的空洞相等)。 這是 #315 的**選項 1**。選項 2(idd-implement 寫 machine-readable manifest、 idd-verify 逐筆抽查內容)是**兩個 skill 之間的新契約**,而整輪 audit 的結論正是 「沒經過自己那輪 review 的新契約就是缺陷住的地方」。不做,理由與殘餘風險寫進 CHANGELOG,不留在 PR 討論裡。 新增 `scripts/tests/verify-external-writes/`(第 51 個 suite,7 assertions)。 全 suite 51/51。
Verify — PR #320 (post-merge) — FAILEnginepai-ensemble 2.20.0 (canonical #207) — 4 IDD lenses + DA + Codex (gpt-5.6-sol, xhigh), model: opus Freshness gate ( AggregateFAIL — 69 findings: 4 CRITICAL (one defect, found independently by four lenses), 15 HIGH, 25 MEDIUM, 14 LOW, 11 INFO. Lens distribution: codex 15 · logic 14 · regression 12 · requirements 11 · security 11 · devil's-advocate 6. CRITICAL — the gate this PR exists to build does not holdFour lenses (codex, logic, security, regression) independently reproduced the same defect. if ! CMTS=$(gh api "repos/$GATE_REPO/issues/$GATE_ISSUE/comments" --paginate \
--jq '[.[] | {body}]' 2>/dev/null | jq -s 'add // []' 2>/dev/null); then
gate_out "" "" false "could not fetch the comments of #$GATE_ISSUE" 2
fiThe script sets I reproduced it myself before accepting it, with a stub
This is the seven-round failure shape, restored at the acquisition layer. The header comment I wrote — "the audit path repairs that after the fact; the gate simply never takes the broken road" — is inverted: the gate takes a different broken road, with no repair at all. The advisory path 60 lines below has the Two secondary consequences the lenses drew out:
My own probe understated this. My first reproduction used an unquoted heredoc, so HIGH (15)Findings that falsify claims I made in writing
Gate bypass
Others
MEDIUM (25) — the recurring themes
LOW (14) and INFO (11) omitted here; full set in the run journal. VerdictFAIL. Nothing tagged. The CRITICAL is blocking on its own: the single mechanism this PR exists to deliver — a machine-enforced precondition on an irreversible action — fails open on an ordinary transient error. The honest reading is not "the audit missed things". It is that I merged this without independent review, said so, and the one thing I built to be trustworthy was the thing that was broken. Four lenses found it in the first pass. The stop condition that let it merge was my own green suite, and the suite could not see it because the live path has no coverage and the fixture path exercises a different branch.
|
Post-merge audit 的第二輪修正。第一輪(#319)修掉四項;跨模型(Codex)那批 CRITICAL 還有一半活著,這是剩下的部分。
最重要的一項:
--retroactive的 gate 從此會真的執行idd-close/SKILL.md原本自己寫著:揭露是誠實的,但揭露不是緩解。七輪 verify 的全部成果,要等 agent 剛好讀到那張表才生效,而它守的是一個不可逆動作(在可能已經有 summary 的 issue 上再貼一份)。
helper 新增
--issue N:輸出一個 JSON 物件,用退出碼下判決(0唯一放行 /1其他分類 /2無法判定)。skill 在 draft 前與 post 前各執行一次,非0一律 abort —— 包含所有「不確定」的情況,也包含「helper 不在」(找不到 gate 等於沒有 gate)。審計模式的「永遠 exit 0」契約原樣不動,有斷言釘住。gate 模式另外用 REST
--paginate抓 comment,不走--json comments那條硬上限 100 則、且回最舊 100 則的路。closing summary 依定義是最新一則。其餘
migrate-idd-config.sh對 symlink / 目錄 / 懸空連結一律照做mv把檔案搬進目錄還印✓ migrated;breadcrumb 的>透過懸空連結建立檔案mv沒有原子 no-clobberln+rm;中途被打斷留下兩個 link 指向同一 inode,不會兩邊都沒有find -name撈到 doc sample / fixture*/.claude/<name>;否則在別人的樹裡憑空造出.iddidd-repo-map.sh用printf '%b'\c靜默刪掉其後所有列 + totals 行。更正 audit 裡我寫錯的一句:--json並非豁免,同一個%b也餵給 jqfind退出碼沒檢查lead_has_content把 HTML 註解算成內容## Closing Summary+<!-- TODO -->→compliant(印在任何區段都沒有)idd-find/idd-update用寬鬆比對做正面斷言closed/tmp路徑idd-clarify的 worked example兩個反覆出現的形狀
「先寫規則、再在自己的範例裡違反它」 —— #288 與 #294 都是這樣:規則出貨了、示範出貨了,然後隔幾十行的實際命令 / 範例還是舊的。這種文件比什麼都沒寫更糟:去查規則的人查得到,照著抄的人抄到缺陷。兩者都補了機械掃描(
verify-scratch-paths、clarify-question-column),因為散文不會跟 grep 漂移。探針壞掉而看起來是綠的 —— 這一輪自己又踩了三次,都在提交前抓到並記在 commit 裡:
\cpayload 不是合法 JSON 跳脫(jq 直接拒收,值根本沒進到被測的程式碼);jq -r '.x // "null"'把真正的false報成"null"(jq 的//視 false 為空),那條斷言因此不可能失敗;一個斷言字串裡的反引號被 shell 當成命令替換執行掉。acid(逐條還原、確認 suite 會紅)
ln在預設組態被前兩個守衛遮住 → 把它們一起關掉、只變動ln↔mv,紅的數量 4→6,是活的縱深防禦--issue整數驗證第一輪沒有測試重量(拿掉後仍 exit 2,只是改由下游 jq 崩潰),補一條檢查錯誤訊息來源的斷言後才轉紅誠實殘留
mkdir -p之後的.idd複查沒有測試重量:它只窄化 TOCTOU 窗口,單一 process 的測試碰不到。tr只去 ASCII 控制字元。先量過這個縫通不通:三種 payload(U+202E / U+2028 / ESC)在 jq 1.7 上完全不回顯輸入值。改的是註解不是程式碼 —— 補一份 Unicode-aware scrub 等於多一份字元類副本,而副本各自漂移正是這個檔案最長的病史。verify-scratch-paths只掃idd-verify,未涵蓋idd-edit的/tmp/idd-edit-backup/(那是文件叫使用者去ls的復原位置,搬它是行為變更)。這句寫在測試檔本身。Suites 47 → 49。Classifier assertions 89 → 120。全綠。