規則檔的修法逐輪製造新歧義:程序寫成有測試的工具,共用的概念只定義一次
論述基礎與限制
這張卡來自本站規則檔(給 agent 讀的 AGENTS.md、docs/rules/ 的八份分冊與 CLAUDE.md)的一次多輪審查。審查跑到第五輪時,用同一組題目、只餵入口檔的三份低階模型理解探針,回報的歧義從第四輪的 18 處升到 25 處;另一位審查者在拋棄式 git repo 裡實跑規則檔寫的每一條指令,第五輪找出 19 則問題,其中 5 則是照做會做錯。單一 repo、單一個工作階段、兩輪的量測,所以卡上的數字是這一次的比例,不是通則;卡要歸納的是成因的分類與修法的方向。之後的同檔重測顯示,三份探針的歧義總數在同一份檔案上就會差到 20 對 13,「從 18 升到 25」這個比較沒有鑑別力(見要理解探針列出「有兩種讀法的位置」是審查題:同一份檔案兩批的總數差得比輪與輪之間還大,改用封閉案例題加預期答案);成因的分類來自逐則歸因,不受影響。
核心原則
規則檔逐條修補不收斂,成因多半出在三種結構:規則檔裡寫了沒有測試的程式、同一個概念在幾條規則裡各定義一次、觸發條件寫成執行者查不到的事實。三種結構都讓「修一處」產生新的缺陷,所以下一輪的 finding 落在上一輪新寫的句子裡。收斂的方向是縮小可能出錯的文字面:程序搬進有測試的工具,共用的概念只在一個位置定義,觸發條件換成指令輸出、diff、路徑這類查得到的事實。
第五輪的 19 則實測問題與 14 處探針歧義,逐則歸到最根本的成因,分布如下:
| 成因 | 實測問題 | 探針歧義 |
|---|---|---|
| 規則檔裡寫了沒有測試的程式 | 10 | 4 |
| 同一件事寫在兩處(含摘要與轉述) | 6 | 3 |
| 邊界寫成封閉列舉或類別名 | 2 | 5 |
| 同一個句子交代兩種讀者各自怎麼做 | 0 | 2 |
規則檔裡的程式缺少放正對照的位置。一條 shell 管線寫進散文,作者讀起來是對的,審查者讀起來也是對的,只有實際跑才知道它在某個狀態下會失敗。這一次的例子:取新增行的管線讓每一行保留 diff 的 + 前綴,字句層 bank 裡以 ^ 錨定的 pattern 因此恆為零命中;補新檔用的 git add -N 寫的是共用的 index,別的工作階段持有 lock 時失敗;在 git worktree 裡照「先切到 main」做,git 回 already used by worktree;改名讓 staged 清單的比對失敗。四條都是前一輪修法新寫進去的。程式碼有測試、有 exit code,散文裡的程式兩者都沒有,而照做的 agent 會逐字照抄。
共用的概念各寫一份,例外就各自漂移。「小改動不用做 X」這個概念同時服務叫用寫作 skill、多輪審查、skill 版號三條規則,三條各寫了一份例外:第一份是「錯字的單一字元、標點、連結、對齊、空白」,第二份是「修錯字、補連結、格式調整」加上「只改措辭」,第三份是「修錯字、補連結、格式調整、改措辭而語意不變」。三份的成員不同,探針問「刪掉一整句算不算」「frontmatter 的數值算不算」時,三條規則各給出不同的答案。同一個機制也出在宣告全集的句子上:入口寫「要先問使用者的只有兩種情形」,同一次修改在分冊新增了一節「推上共用庫之前先問使用者」,那一句就默默失效。封閉的列舉只有在它是那類情形的唯一住址時才保持正確。
執行者查得到的是 context 與路徑,查不到自己的身分。入口用同一個句子交代 Claude Code(已經載入全部分冊)與 Codex(只自動讀入口)各自怎麼做,兩輪的探針都判斷不出自己屬於哪一種。執行者查得到的是自己的 context 裡有沒有分冊的全文,以及這次要改的檔案路徑;把觸發條件換成這兩件事,判斷就不依賴身分。
反模式
- 照 finding 逐條補句子,每一則都補得對,下一輪的 finding 換一批出現在新句子裡。
- 規則檔裡的指令寫完沒有跑過,或只在一種狀態下跑過(乾淨的 index、主 worktree、沒有改名)。
- 同一個例外在每條用到它的規則裡各寫一份,每一份都只列那條規則的作者想到的成員。
- 「只有 N 種」「不跑的只有兩種」寫在不是唯一住址的位置。
- 路由表或入口用自己的話轉述分冊的觸發條件,分冊改了之後轉述留在舊版。
- 觸發條件寫成「Claude Code 的話……Codex 的話……」,讀者要先判斷自己是誰。
修法
- 程序寫成工具:要照順序跑兩條以上指令、或依輸出走不同分支的程序,寫成工具並附測試,實測出錯過的每一種狀態寫成一個測試案例。規則檔改寫成「什麼時候跑、輸出怎麼讀、非零的 exit 怎麼處置」。這一次寫成四支:取新增行與判定改動級別、只提交列出的檔案、只回報新增的 lint 問題、推送並在推送範圍有別人的 commit 時停下。單一條、沒有分支的指令可以留在規則檔,寫之前跑一次正對照。
- 共用的概念先定義再引用:在一個位置定義並命名,其他規則寫名字。這一次把「小改動」定義成三個級別(排版級、措辭級、規則級),排版級由工具依 diff 判定;三條規則各自只寫「哪一級以上要做」。「要先問使用者的情形」收成一節,是那類情形的唯一住址,別處連過去。
- 觸發條件換成查得到的事實:讀者身分換成「context 裡有沒有分冊全文」,工作類型的轉述換成「這次要改的路徑」。
- 語意判斷留下痕跡:工具判不了的分界(措辭是否改到規則的條件)由執行者判,判定寫進 commit message:改了哪一條規則的哪一項,或「條件、判斷標準、處置都沒變」。
判讀徵兆
| 徵兆 | 判讀 |
|---|---|
| 下一輪的 finding 多數落在上一輪新寫的句子 | 修法在擴大可出錯的文字面,逐條補句子不會收斂 |
| 實跑規則檔的指令找到的問題,比讀規則檔找到的多 | 規則檔裡寫了沒經過測試的程式,該搬進工具 |
| 探針對同一種邊界情形,在不同的規則裡給出不同的答案 | 同一個概念在那幾條規則裡各有一份定義 |
| 「只有 N 種」的宣告被同一組檔案別處的一條規則推翻 | 列舉不在唯一住址 |
| 同一處歧義改寫過一次仍然出現在下一輪 | 觸發條件依賴讀者查不到的事實(身分、主觀狀態),措辭改不掉 |
跟其他抽象層原則的關係
- 要理解探針列出「有兩種讀法的位置」是審查題:同一份檔案兩批的總數差得比輪與輪之間還大,改用封閉案例題加預期答案:限定本卡起點那個比較的效力:歧義總數是取樣雜訊,本卡的成因分類由逐則歸因得出,仍然成立。
- 規則的修法逐句驗證會通過、套到案例上才互撞:改完才查得到的事實管不了動手之前的步驟,修完要拿任務重走:本卡的下一輪。本卡的結構調整做完之後,修改那些結構的動作本身又產生新的歧義;那張卡追出四種機制,並替本卡的「共用的概念先定義再引用」補上時點這一問。
- 判定改動是誰做的檢查:重播差異而不拿全文、證據綁定這個 session、比對要提交的那一份內容:補本卡「程序寫成有測試的工具」留下的一段:工具的測試由作者寫,只涵蓋作者想到的路徑;那張卡的工具測試全過,漏報路徑由審查在拋棄式 repo 裡實跑找出,再寫回失敗的測試。
- Single Source of Truth:值的住址只能有一處:本卡最接近的既有卡,那張講的是值(CSS token、量測值)的住址;本卡把同一條原則用在規則檔自己的概念上,並補上那張卡沒有處理的兩種副本:摘要與轉述,以及散在各條規則裡的同一個例外。
- 列舉與數字殘留在定義型文件會製造維護債務:那張卡處理的是寫作過程留下的冗餘列舉;本卡的封閉列舉宣告的是全集,讀者靠它知道清單已經完整,失效的方式是別處新增成員之後它不報錯。修法因此不同:那張卡刪掉列舉,本卡把列舉移到唯一住址。
- 要求執行者做判定的規則,要一併規定判定留下什麼痕跡:改動級別裡工具判不了的那一段(措辭級與規則級的分界)照那張卡的方式留下痕跡。
- 拆分產生的缺陷落在檔與檔的交界,每一份單獨讀都是對的:規則檔拆成入口與分冊之後,入口對分冊的轉述是交界上的副本,本卡的「摘要與轉述也是副本」是那一類缺陷在規則檔上的形態。
- 規範文字的效力由它在讀者程序裡的位置決定,逐字讀過也不保證綁定:那張卡說規則要放在讀者程序裡會經過的位置;程序寫成工具是同一個方向的極端:讀者不必讀到那段文字,只要跑那支工具,規則就在執行的路徑上。