今天講一個很小的設計,小到只有一行檢查。但我認為它是整套工廠裡最聰明的一個。
它處理的問題是:規格和程式,會慢慢對不上。
工廠建好之後,理想的流程是:先寫規格 → 跑流程 → 產出程式和測試。
但真實世界不會這麼乾淨。
會有一些東西是不走規格的:一支啟動腳本、一個資料庫遷移檔、一層框架的組裝設定。這些不是業務規則,寫規格反而累贅。也會有一些臨時的:先做個東西驗證想法,想說之後再補規格。
然後——
「之後」不會來。
那個薪資工廠的紀錄裡就有這樣的痕跡。有幾次執行的備註寫著「基礎設施非 spec」:
持久化層(資料庫接線) ~12 min 基礎設施非 spec
框架組裝層(依賴注入設定) ~10 min 基礎設施非 spec
操作入口(命令列包裝) ~20 min 基礎設施非 spec
這些標記是誠實的、也是合理的。那些東西確實不該有業務規格。
但問題來了:當「不走規格」變成一個可以宣稱的選項,界線就會慢慢滑動。 今天是資料庫接線不走規格,明天是「這個功能很簡單,先做再補」,後天是「反正那份規格沒人看」。半年後你有三十個業務流程,二十份規格。而沒有人知道少的是哪十個。
薪資工廠某一次執行的備註,記了這樣一段:
後補 use-case 規格(覆蓋率 5/5)
+ 結構檢查新增「每個 use case 必有 spec」的勾稽閘門
消除「有 use case 無 spec」的漂移
拆開來看,這裡發生了三件事:
一、發現漂移。 有一個業務流程做出來了,但沒有對應的規格。
二、補上。 把那份規格補寫,而且跑了覆蓋率驗證。
三——這才是關鍵——把「不會再發生」寫成機器檢查。
第三步就是那一行閘門:掃描業務流程目錄,每一個都必須在規格目錄找到對應的檔案。找不到就擋。真的就是幾行:
#!/bin/bash
# 每個 use case 必有 spec
fail=0
[ -d .dev/use-cases ] || { echo "⛔ .dev/use-cases 不存在——這支檢查等於沒跑" >&2; exit 2; }
for uc in .dev/use-cases/*.md; do
[ -e "$uc" ] || continue
name=$(basename "$uc" .md)
[ -f ".dev/specs/${name}.json" ] || { echo "❌ 有 use case 無 spec:$name" >&2; fail=1; }
done
exit $fail
第二行那個 -d 檢查不能省。 目錄不存在的時候,for 迴圈一圈都不跑、fail 還是 0、腳本回報通過。那正是我在算帳那篇自承的「指向不存在的檔案,整段從不執行,連 PASS 都不印」。同一個坑,我在寫這支的時候又踩了一次。
注意它是單向的。 這一版只檢查「有 UC 沒 spec」,反過來的「有 spec 沒 UC」它抓不到。那是另一個方向的漂移,要另外寫一圈。我第一版只寫了一邊,而且沒發現。

因為它處理的是單向的失效。
一般的檢查是驗「這個東西對不對」。這個閘門驗的是「這個東西存不存在」。
而「不存在」是最難被發現的一類問題:
Day 06 講過 UI 的沉默約束,Day 08 講過開發規範的沉默約束。這是第三種:缺漏的沉默。
而且它會隨時間累積。每多一個沒補規格的流程,工廠的覆蓋面就少一塊,而那個缺口不會自己浮出來。用一個閘門把它變成每次都會被檢查的事,成本只有一行 grep。
我後來把這個 pattern 抽象成一句話:
凡是「A 存在就必須有對應的 B」的關係,都應該有一支腳本去掃。
同一個 pattern 可以套在很多地方:
| 關係 | 檢查 |
|---|---|
| 每個 use case 必有規格 | 掃目錄對應 |
| 每條規格的後置條件必有測試斷言 | 就是 Day 22 那支覆蓋率腳本 |
| 每條鐵律必有決策紀錄編號 | 掃編號是否存在 |
| 規範文件引用的路徑必須存在 | Day 22 的文件一致性檢查 |
| 每個決策紀錄編號只能被用一次 | 掃重號 |
| 每個修完的 bug 必有回歸測試 | Day 18 講的那條 |
這六條全部是同一個形狀:對應關係的完整性。
而它們全部可以用幾行腳本檢查,全部屬於第 3 層。
我建議你現在就可以拿這個 pattern 去掃自己的專案:**列出所有「A 必須有 B」的關係,然後看有幾條是靠人記得的。**我第一次做這個練習的時候,列出七條,其中六條沒有任何機器檢查。
上面那張表裡有一條是「每個決策紀錄編號只能被用一次」。這條規則看起來瑣碎到有點好笑。不就是編號嗎,會撞到又怎樣?我看過一個真實的坑:兩個人在不同分支上各自新增決策紀錄,都取了下一個流水號。合併之後,兩份不同的決策用同一個編號。聽起來是小麻煩,但後果比想像中大。因為決策紀錄是被引用的東西:
當編號指向兩個東西,這些引用全部失去意義。 而且失去意義的方式是安靜的。沒有人會收到錯誤訊息。解法很簡單:流水號只在一個索引檔登記,要開新的先去取號。而「有沒有重號」「有沒有孤兒(有編號沒檔案、有檔案沒登記)」,一支腳本掃。
這又是「規則單一來源」,只是這次的對象是編號。
最後講一個實務細節。
這類「完整性檢查」放在什麼時候跑,差別很大:
薪資工廠那支結構檢查是掛在寫檔之後的,但這條「每個 use case 必有規格」的勾稽被放在結構檢查的第六節。也就是說它跟著結構檢查跑,但那支腳本本身是在工作告一段落時執行完整版。
檢查的內容決定它該在哪個時機跑。 便宜又即時的(命名、目錄)放前面,需要「一段工作完成」才有意義的(完整性、一致性)放後面。放錯時機的檢查會一直誤報,而誤報三次之後就沒有人看了。