模組三|腳本與確定性驗證器(Day 12–16)
模組二講素材庫,收在一顆壞掉的按鈕。今天開始講這條產線上唯一一個真正活下來的規則。
它是一支 64 行的 bash。
口播稿是產出的。而「產出的東西對不對」有兩種驗法:
第一種聽起來聰明多了。它能理解語意、能給建議、能處理我沒想到的情況。
而我選了第二種,理由只有一個:我要一個布林值。
模型會給我一段話。那段話可能是「大致完整,但建議加強某段」,然後我要判斷「大致」算不算通過。這個判斷會漂。今天我心情好就過了,明天我趕時間也過了。
程式給我 0 或 1。它不會因為我趕時間就變寬鬆。
這就是「確定性驗證器」的意思:不是它比較準,是它不會變。
腳本開頭的註解原文大意:
純 ASCII 原始碼。只對已存檔的 HTML 操作(封閉式,不連網路)。
三個限制,每一個都是刻意的:
純 ASCII → 這支腳本在任何 locale、任何終端機都能跑,不會因為編碼問題掛掉。
只讀一個檔 → 它不需要知道專案結構、不需要設定檔、不需要環境變數。
不連網路 → 它的結果只跟輸入有關。同一個檔案,今天跑跟明天跑結果一樣。
這三條加起來的效果是:它的執行門檻低到不會讓人懶得跑。
這一點比它檢查什麼還重要。Day 7 講過模板驗證器、Day 8 講過可信度稽核,兩支都寫好了、都沒被自動執行、落實率一個 8% 一個 31%。而它們跟這支的差別是:那兩支要先讀資料庫、要解析 JSON、要跑幾秒;這支貼一個路徑就跑完。
一個要人記得跑的檢查,它的成敗取決於跑起來有多麻煩。
大部分腳本的退出碼只有兩種:0 成功,非 0 失敗。這支分三種:
0 通過。並印出事實列數與檔案位元組數。
1 結構檢查失敗。逐條印出 FAIL 行「for the generator」。
2 目錄或 HTML 檔根本不存在。
把 2 跟 1 分開,是這支腳本最值得抄的一點。
「我沒東西可驗」跟「我驗了但不合格」是兩件完全不同的事:
1 → 稿子有問題,回去改稿2 → 流程有問題,可能是路徑錯了、可能是上一步根本沒產出檔案如果這兩者都回 1,我會拿著一份不存在的檔案在那裡改稿。
而多數人(包括以前的我)寫檢查腳本時,會把「找不到檔案」直接 exit 1,然後在錯誤訊息裡寫清楚。錯誤訊息要人讀,退出碼給程式讀。 當這支腳本被寫進自動流程的停止條件時,那個流程只看得到數字。

退出 1 的時候,它會逐條印出失敗項,而註解裡寫著這些訊息是 for the generator。
不是給我看的,是給產出這份稿子的東西看的。
這改變了訊息的寫法。給人看的錯誤訊息可以寫「結構不完整」;給產出者看的必須寫「缺少 X 段落」,它要能直接被當成下一輪的修改指令。
這個設計讓「驗證器」變成了「回饋迴路的一環」,而不是一個終點的閘門:
產出 → 驗證 → 失敗訊息 → 產出(帶著訊息重來)→ 驗證 → ...
而這個迴路能收斂,前提是失敗訊息夠具體。如果它只說「不合格」,迴路就會空轉。

講完優點,講它壞掉的地方。
這支腳本不在專案裡。 它只存在於我的使用者全域目錄。
而專案裡有三份說明文件,全部教人用專案相對路徑去跑它:
bash .claude/skills/episode-prep/verify_episode.sh "<集數目錄>"
那個路徑在專案裡不存在。任何人乾淨 clone 這個 repo,照文件抄的指令會直接失敗。
包括未來的我,換一台機器就跑不了。
這件事很諷刺:一支刻意做成「無外部依賴、任何環境都能跑」的腳本,它本身就是那個外部依賴。
而它為什麼會這樣?因為它是在一個「skill」目錄裡寫的,那是給 AI 工具用的全域設定區。它一開始就沒有被當成專案的一部分。
一個檢查如果不在被檢查的東西旁邊,它遲早會走失。
我把這支腳本對所有集數目錄跑了一遍:
PASS 2
FAIL 1
沒有可驗的檔案 27
覆蓋率 3/30 = 10%。
看起來很糟,但這個數字要拆開理解:那 27 個目錄不是「驗失敗」,是它們裡面根本沒有這支腳本要驗的那個檔案,因為它們是更早期的集數,那時候還沒有這個口播稿格式。
所以正確的說法不是「它只驗到 10%」,是「這個格式只覆蓋了 10% 的集數」。
它只宣稱驗一件事,而它把那件事驗得很好。這是它跟 Day 7 那個 8% 落實率的模板規格最大的差別:模板宣稱管全庫,實際只管到 8%;這支只宣稱管一個檔案,而它每次都管到。
一個誠實的小範圍檢查,比一個宣稱涵蓋全部卻沒在跑的規格有用。
最實際的代價:那支腳本到現在還沒搬進專案。
搬進去是複製一個檔案加改三份文件的路徑,十分鐘。而我從發現到今天寫這篇,沒做。
第二個代價比較根本:確定性驗證器只能驗它能表達的東西。
它驗得了「有沒有這個段落」,驗不了「這個段落寫得對不對」。Day 14 講的那個「換一個模型審編輯線」就是為了補這一半,而那一半到現在還是靠人記得跑。
第三個,也是我寫這篇才想到的:這支腳本的 15 條檢查,是硬編在裡面的字串。 只要稿子的段落標記改個名字,對應的檢查會靜默失效:它 grep 不到就當作不存在,而「不存在」會被判成失敗……
不對。我重新看了一次程式碼:如果段落標記改名,它會判失敗,因為它檢查的是「必須含某字串」。所以這個情況會噴錯,不會靜默通過。
這是我在寫這篇的時候現場修正的一個判斷。 留在這裡是因為它示範了一件事:即使是自己寫的 64 行 bash,隔了幾個月我對它行為的記憶也是錯的。
要一個布林值,不要一段評語。
當你需要的是「能不能進下一步」,就不要問一個會給你「大致上可以」的東西。判斷會隨心情漂,退出碼不會。
第二條,可以今天就抄的:
把「沒東西可驗」跟「驗了不合格」分成不同的退出碼。 前者是流程壞了,後者是內容壞了,處理方式完全不同。多數檢查腳本把這兩者混成一個 exit 1,然後在錯誤訊息裡區分,而訊息給人讀,退出碼給程式讀。
第三條,關於什麼樣的檢查會被執行:
執行門檻決定成敗,不是檢查品質。 我有三支檢查器,最簡陋的那支是唯一一直在跑的,因為它貼個路徑就完事。另外兩支寫得更完整、涵蓋更廣,而它們的實際落實率是 8% 跟 31%。
明天講那 15 條檢查的內容,以及怎麼看出哪幾條是被事故逼出來的。