這幾天連續踩到同一類東西:閘門明明在跑,卻什麼都沒擋住。我之前寫過兩篇談驗收端——不要用 AI 的摘要驗收 AI 講的是別相信「沒問題」這個回報,沒拋錯不代表成功 講的是 exit 0 跟 HTTP 200 都不算數。這一篇是設計端:既然回報不能信,那要怎麼寫出一個真的會擋下來的閘門。我手上有四條判準,每一條都是賠掉一次才寫下來的。
閘門要綁「no-op 下活不下來的欄位」
晨報的閘門只比對段落存在——有這幾個標題、有這幾個區塊,就算過。問題是昨天那份檔案也有這幾個段落。所以當晨報靜默漏了一天,閘門照樣放行,因為它檢查的東西在什麼都沒發生的情況下一樣活得好好的。
判準因此是:閘門要綁「no-op 下活不下來的欄位」。今天的日期、今天才會出現的條目、只有真的重跑過才會變動的值——這種欄位一旦沒更新就當場露餡。只驗結構存在的檢查,等於幫昨天的檔案發了一張永久通行證。
檢查器回 0 之前,先反驗它會動
09-10 那天我兩次踩到同一個形狀:自己寫的檢查器回 0,我就當它沒問題。第一次是 git rev-list | cat-file --batch-check | awk 那條,回 0 不是因為沒有大物件,是因為前面根本沒產出物件清單,awk 收到的是空的。第二次是 filter-repo 之後的全物件掃描,也先跑出一份假的乾淨。
同一天課程那邊也一樣:公開前的機密掃描回 0,我拿一把假金鑰塞進去反驗,才確認掃描器真的有在比對。
所以判準是:拿一個必中的字串當對照,對照命中才採信那個 0。而且反驗要同時驗兩邊——乾淨對照必須不叫,每個破壞案例都必須叫。只看到「都沒叫」的時候,第一懷疑不是東西很乾淨,是反驗 harness 自己沒接上。
反方向的誤報同一天也出現兩次:一次把課堂當場產出的檔當成缺漏的 asset,一次把已經改寫成反例的字串當成違規,只比對字串、沒看語境。檢查器會漏叫,也會亂叫,兩邊都要驗。
只印訊息、不改離開碼,等於沒有閘門
這條最便宜,也最容易被自己騙過去。sweep 腳本裡寫的是 node check.js || exit 1,看起來就是個閘門。但 check.js 把問題印完之後仍然 exit 0,於是 || 永遠不成立,被旗標出來的項目照樣進了下一步。
要當閘門用就必須非零離開。印訊息是給人看的,離開碼才是給流程看的;只做前者,那支腳本是報表,不是閘門。
別把 prompt 沒規定的字面當契約
這條是換模型才炸出來的。prompt 從來沒有規定輸出標題要長什麼樣,閘門卻拿標題字面去逐字比對。Sonnet 產出的字面剛好合格,換成 Opus 就不合格——內容明明是對的,卻被判失敗,後面的 vault-refinery 整段被跳過。
這是我自己造出來的隱性契約。prompt 裡沒寫的東西,閘門不該拿來當契約;要驗就驗 prompt 真的規定過的欄位,想驗標題,就先把標題格式寫進 prompt。
退件不能變成「換個檔名放到旁邊」
題庫那邊的閘門更離譜一點。它遇到缺 type 的題目不退件,而是把它們改寫進 undefined_passed.json。名字裡就寫著 passed,89 題就這樣「過了閘門」,只是被放在匯入端根本不認得的檔名底下,全程不報錯。
後來是靠形狀驗證補回來的:五個選項、沒有 DS 句型、標題前綴對得上,這三個條件把那批 PS 撈了回來,才沒有整批漏掉。同一份流程裡還有一個數字問題,find -name out.json 會連 gate-in/ 底下的副本一起數進去,進度整個虛報一倍。
閘門只有兩種合法出口:放行,或退件。第三條「改個檔名放到旁邊」不是出口,是把問題藏起來,而且藏得比不檢查還深。