通過關卡不等於通過的是同一個程式
論述基礎與限制
本卡的證據來自一份公開且可重現的對照實驗:Robert C. Martin 的 negative-test-experiment(2026 年 8 月)。同一份需求(Greg Yob 的 Hunt the Wumpus)從空目錄寫八次,變因是測試紀律四選一(三法則 TDD、test-last、逐函式 bundling、完全不寫測試)乘上 CRAP 度量是否強制壓到低於 4;每一行寫完後封存該行的測試套件,再從空的用突變測試長出第二套。實驗的 policy、plan、八份 notebook 與結論都在 repo 裡。
實驗的資料:八行全部通過同一份 25 案例的驗收腳本、25/0,包含那個一個單元測試都沒有的版本。八棵原始碼樹沒有任何兩棵 checksum 相同,行數 331 到 603、函式數 33 到 118。作者標出一個真正的行為分岔:原作的危險檢查順序是 wumpus、pit、bats,而八行裡有四行照做(列 3、6、7、8),另有三行(列 1、4、5)依 notebook 逐行記載都是 bats、pit、wumpus,列 2 未分類——實際存在的是兩種順序。驗收從來沒有把兩個危險放進同一個房間,所以兩種都通過。abstract 的總結句是 Acceptance never distinguished any of this。
這份實驗由 AI agent 執行,policy 檔通篇是防止 agent 抄近路的條款(不准從先前的 runs 目錄複製、不准憑上一行的記憶開始下一行)。作者記錄了一次作廢:前一輪的網格產出的是兩個程式被複製成八份,整輪丟棄重跑。
限制有四層。單一產品、單一語言、單一組驗收腳本;八行的設計與可讀性評分由作者本人給出(1 到 5 的主觀量表);每一格只執行一次,紀律造成的差異與單次重寫的變異分不開;而那份驗收腳本由需求的作者本人所寫,因此等價類的大小是相對於那一份腳本,不是相對於「驗收測試」這個一般類別。可跨情境重用的是機制而不是那些數字。本卡的另一個來源是同一位作者在 2026 年 7 月的公開發言(不再逐行讀 agent 產出的程式碼、改用約束包圍),該發言未能直接讀取原頁、經由二手報導轉述——它在本卡裡的角色是問題的來歷,不是論證的承重件。
核心原則
一組關卡通過,證明的是產出落在該關卡切出的等價類裡,不是產出就是需求要的那一個。 關卡由若干條件構成,每條條件排除掉一部分可能的程式;沒有被任何條件排除的部分全部通過,而那個集合的大小由條件之間的交叉密度決定,不由條件的數量決定。
人對這個集合的大小沒有直覺。二十五條驗收案例看起來已經把功能交代完了,而它們只約束了「每一個危險單獨出現時的反應」,沒有一條讓兩個危險同時出現——兩種不同的危險判定順序因此都合法。判定順序不是實作自由度,它是需求的一部分,只是沒有人把它寫進條件裡。
這條原則跟「測試不夠多」是不同的診斷。同一維度上多加一條斷言,縮小等價類的幅度隨密度遞減;而對一個沒有任何條件碰過的維度,增量是零。 判定順序住在「兩個危險同時出現」這個交叉裡,而那個交叉一條條件都沒有——套件規模再大也不會走進去。八行的套件規模從零到 206 個範例,通過與否完全一致,正是這個結構的外顯。
沒有逐行讀產出時,等價類的大小就是驗證的實際精度。讀程式碼之所以曾經補得上這個缺口,在於閱讀的過程會撞見條件沒有規定的地方,而不在於人特別擅長找 bug。
修法
- 設計驗收條件時問「哪些不同的程式會通過這組條件」,不問「這個程式會不會通過」。 前者逼出等價類的邊界,後者只確認手上這一個樣本。實際的做法是把條件攤成維度表(狀態、事件、順序、併發、邊界、失敗),看哪些維度組合沒有任何一條條件同時涵蓋。
- 交叉的維度優先於單維度的密度。 覆蓋率與測試數量都只反映密度,換指標的做法見品質閘門的更替。 補條件時先補「兩件事同時發生」,同一件事的第十個邊界值排在它後面。判定順序、資源競爭、狀態機的非法轉換都住在交叉裡。
- 把通過的產出當成一個樣本,而不是答案。 同一組條件會被重跑時(agent 重寫、重構、換實作),要能說出重跑後的產出跟這一次是不是同一類。checksum 相同代表沒變,不同則什麼都還沒證明——那個實驗裡八棵樹的 checksum 全不同而全部通過同一組驗收。要指出差在哪個維度,得對關鍵維度各跑一次探測,checksum 只是最粗的一層。
- 不讀程式碼時,要指名這件事現在由誰負責。 指不出來的話,這個決定交換掉的是等價類的可見性,換來的並不只是省下閱讀時間。出處獨立的驗收條件是目前指得出來的來源之一:需求、驗收條件、實作、測試這條鏈上,只有驗收條件不由實作推導出來。另一個來源是判準寫不下來的那一區——親自操作產品的探究。站內目前只界定了這一區的存在與邊界,沒有它的操作方法;要那一層的實作走 Bach 與 Bolton 那本,位置在書單的驗證線。
- 區分自由度與未規定的需求。 等價類大本身不是缺陷——命名、模組切法、內部資料結構本來就該留給實作。要問的是集合裡的差異落在哪一側:換一個變數名是自由度,換一個判定順序是需求沒寫。
跟其他原則的關係
| 原則 | 關係 |
|---|---|
| #221 檢查規則的作用域要顯式列舉:零 error 可能是沒被檢查 | 同一形態在驗收條件上——#221 的「規則存在不等於規則涵蓋」在這裡是「條件通過不等於條件問過」;兩者的零錯誤訊號都與真正合規無法區分 |
| #222 約束要讓違反路徑走不通 | 下游——#222 要求約束要有執行層,本卡處理執行層裝上之後的下一個問題:會發聲的關卡仍然只在它問過的維度上發聲 |
| #253 寫註解的動機是怕被改壞時,要處理那個約束 | 補一個限制——#253 的判準是「存不存在一條會紅的斷言」,本卡補上「那條斷言涵蓋哪些維度」;斷言存在而維度沒交叉時,保護是名義上的 |
| #276 AI 生成的內容不虛構經驗與出處 | 相鄰的產出信任問題——#276 管生成內容裡的事實宣稱、本卡管生成程式碼的行為宣稱;兩者的共同結構是形態看起來完整而承重處未經查證 |
| #278 機械約束買到被量測的那個數字 | 同源實驗的另一軸——本卡談關卡放過了什麼,#278 談關卡逼出來的改動長什麼樣;兩者合起來是「通過」這個訊號的兩種空洞方式 |
這條原則在別的領域長什麼樣
驗收條件與程式是這條原則最容易看見的一組,而它的形狀跟載體無關:一組通過條件切出一個等價類,而人習慣把等價類當成單點。 認得出別的領域裡的同一形態,就不必每次重新推導。
| 領域 | 那裡的「關卡」 | 被放行的等價類長什麼樣 | 那裡的交叉維度是什麼 |
|---|---|---|---|
| 寫作規範 | lint 規則與 keyword bank | 全部合規而讀起來仍是同一個模具壓出來的稿件 | 規則各自檢查單句,跨句、跨篇、跨文體的組合沒有規則碰到 |
| 合規稽核 | 控制項清單逐條打勾 | 每一條控制都有證據,而流程串起來仍有一段無人負責 | 控制項按職能切分,交接點落在兩個職能之間 |
| SLO 設計 | 可用性與延遲的門檻 | 指標全綠而使用者體感很差 | 單一指標各自達標,指標之間的相關性(高延遲期間的錯誤率)沒有門檻 |
| 面試流程 | 每關的通過標準 | 每關都通過而入職後表現落差極大 | 各關獨立評估單一能力,能力之間的互動沒有任何一關在看 |
| 型別系統 | 編譯通過 | 型別全對而語意是錯的 | 型別檢查單一值的形狀,值與值之間的不變量不在射程內 |
這幾個領域的修法是同一個:先問這組條件放行了哪些彼此不同的產物,補的位置在交叉上。可操作的程序、維度表的攤法與補條件的順序在 agent 產出的程式碼怎麼驗,那裡也給了覆蓋率門檻為什麼救不了這件事(品質閘門的更替)。
這條原則什麼時候不成立
條件與產出是一對一時不成立——產出的空間本來就只有一個元素(例如一個純函式的完整真值表已被列舉)。等價類的概念在那裡是空轉的。
產出的差異全部落在自由度上時也不成立:條件刻意只約束外部行為、內部留給實作,那個等價類大是設計意圖而非缺口。判別的問題是這個差異消費者觀察不觀察得到;答「觀察不到」要指出它落在可觀察面(介面契約、時序、資源、錯誤形態、分佈)之外的哪一側,指不出來就當成需求缺口。