模組三|腳本與確定性驗證器(Day 12–16)
模組三的最後一天。前四天講這條產線上活著的那些檢查:64 行的 bash、34 行的 Python、換一個模型審編輯線、36 行的狀態檔。
今天講它們共同的盲區。
我實地比對出七組跨文件落差,全部都在所有檢查的視野之外。
昨天講過這一組,今天把兩個 repo 的數字並排:
| 狀態檔宣稱 | 我實跑得到 | |
|---|---|---|
| 製作端 | PASS,7 事實列,46.9 KB | PASS,12 事實列,63,980 bytes |
| 內容後台 | PASS,11 事實列,40,477 bytes | PASS,16 事實列,47,915 bytes |
兩邊都還是 PASS。
驗證器只驗產物,不驗那份宣稱「我驗過了」的帳本是否還是當時那份。
帳本可以無聲地過期,而 exit code 永遠是 0。
同一份節目表,同時存在於製作端的某集目錄與內容後台的對應目錄。
215 行 vs 199 行,18 行差異。 其中一份多了一整個小節,而且有一列查核表的內容不同。
哪一份是對的?我不知道。沒有任何機制知道這兩份應該一致。
它們不是複本關係(那樣可以用 checksum 檢查),也不是父子關係(那樣可以定義誰是來源)。它們就是兩份長得很像的檔案,各自被編輯過。
這一組最尷尬。
兩個 repo 各有一個同集號的目錄,而它們是完全不同的內容:不同主題、不同來源 id。
而且兩邊的狀態檔各自宣稱「自己這個是 active episode」。
如果有任何一支腳本試圖跨 repo 對應這一集,它會拿到兩份無關的資料,而且不會發現有問題。
三份 AI 協作指引文件,全部寫著同一個指令:
python3 scaffold_episode.py --episode-number 45 ...
而那支腳本實際定義的參數是 --number。
Makefile 用的也是 --number。只有那三份文件是錯的。
照文件抄的指令會被 argparse 直接拒絕。
這一組特別值得講,因為它揭露了文件跟程式碼的一個結構性差異:程式碼會被執行,所以錯了會被發現;文件只會被閱讀,而閱讀不會報錯。
Day 12 講過:那支 bash 只存在於使用者全域目錄,而三份文件教專案相對路徑。
乾淨 clone 的人跑不了它。
跟第四組是同一個病,文件描述的世界,跟磁碟上的世界不一樣,而沒有任何東西在比對這兩者。
這條產線有多份 AI 協作指引,因為不同工具讀不同的檔案。它們是同一份內容的不同世代。
其中一份的發布流程有六個步驟,第六步是「產出直式影片」,並且附了一條素材鐵律:
用 final/正片,不是毛片、不是原始 wav
而另一份指引的同一節完全沒有這一步,第六步直接跳到下一件事。
也就是說:給某個工具讀的那份,缺了一條防止拿錯素材去算圖的規則。
而這種「同一份規則的多個副本逐漸分岔」的問題,在有 30 個以上 AI 工具設定目錄的 repo 裡(Day 28 會講這件事)是必然的。

這一組是全篇最好的。
素材庫本來有一項 parity 檢查:比對資料檔與前端頁面裡內嵌的資料是否同步。這是唯一一個真正在檢查「兩份東西一不一致」的機制。
我實際跑稽核的時候,它印出這個:
news.json / index.html parity: SKIPPED
(index.html loads news.json at runtime; embedded scriptData parity check skipped)
因為前端後來改成執行時去 fetch 資料檔,不再內嵌一份副本,所以這個檢查失去了對象,於是它跳過。
這個處理技術上完全正確。沒有東西可比,跳過是對的。
問題在於:它沒有失敗,它只是安靜地不跑了。
沒有警告、沒有 TODO、沒有一行「這個檢查已停用,因為架構改變」。就是在一堆通過訊息裡,多一行 SKIPPED。
而它是這條產線上唯一一個跨文件一致性檢查。它消失了,而上面那六組落差就是它消失之後的樣子。

把七組排在一起,共同點很清楚:
每一組都需要同時看兩個東西。
那支 64 行的 bash 讀一個檔案。那支 34 行的 Python 讀一個檔案。它們的設計前提就是單檔。Day 12 講過,那個「只讀一個檔、不連網路」的封閉性正是它們執行門檻低、所以一直有在跑的原因。
單檔驗證器的低成本,跟它的盲區,是同一件事的兩面。
而要做跨檔案檢查,你需要知道「哪些檔案應該互相對應」,而那個對應關係本身沒有被寫在任何地方。它在我腦子裡。
七組可以歸成三種:
| 型態 | 組別 | 特徵 |
|---|---|---|
| 記錄與現實脫節 | 一 | 有人抄過一次數字,之後現實變了 |
| 同一份東西有多個副本 | 二、六 | 複製的當下是對的,之後各自演化 |
| 描述與實作不符 | 四、五 | 文件寫的跟磁碟上的不一樣 |
| 對應關係只存在腦子裡 | 三 | 沒有任何地方寫著這兩個該對上 |
而第七組是元問題:唯一能抓這些的機制沒了,而它的消失沒有留下任何痕跡。
七組我一組都沒修。
寫這篇的時候我又確認了一次,全部還在。
而它們的修法難度差很多:第一組(帳本過期)Day 15 講過,讓驗證器自己寫記錄,幾行程式碼;第四、五組(文件教錯指令)是改三份文件的事;第二、三組(跨 repo 分叉)沒有便宜的解法——它需要先定義「這兩個東西應該一致」,而那件事沒有被寫下來過。
第二個代價,比較誠實的一個:這七組是我為了寫這個系列才去找的。
不是日常維護發現的,是我特地花時間比對兩個 repo 的檔案才浮出來的。也就是說,如果沒有寫這個系列,它們會一直在那裡。
而這正是這一類問題最難處理的地方:它們不會主動找上你。 壞掉的功能會被使用者踩到,錯字會被聽眾聽到,而兩份不一致的檔案可以安靜地共存好幾個月。
確定性驗證器只能驗它看得到的那個單位。超出那個單位,就沒有了。
Day 10 講過欄位之間會分岔,今天講檔案之間會分岔。同一個模式,不同尺度。
所以在導入任何檢查的時候,先問清楚:它的視野邊界在哪裡? 一個讀單檔的檢查,它的盲區就是「檔案之間」;一個讀單一 repo 的檢查,盲區就是「repo 之間」。
而那些盲區不會因為你有檢查就變小,有時候反而變大,因為你會覺得「有在檢查」。
第二條,這一天最該記住的:
檢查被停用的時候,要留下痕跡。
第七組那個 SKIPPED 是完全合理的技術決定,而它的問題不在決定本身,在於它沒有留下一句「為什麼」。
如果那行輸出是這樣:
parity: SKIPPED — 架構改為 runtime fetch,此檢查已無對象。
若日後恢復內嵌資料,需重新啟用。見 issue #NN
那我幾個月後看到它,就會知道有一個檢查不見了。而實際上我是在寫這一篇、逐行讀輸出的時候才發現的。
一個安靜消失的檢查,比一個從來不存在的檢查更危險——因為你以為它還在。
模組三到這裡結束。明天進剪輯管線,從一個 95 字元的提示詞開始。