多份文件必然漂移:同步期待要嘛有機制承接、要嘛明示降級
論述基礎與限制
這則檢討處理開發流程的文件鏈設計:一個功能從需求到實作,資訊會流經多份文件——proposal、spec、use case、設計文件、追溯表、測試、註解、工作日誌。Brooks 在《人月神話》(The Mythical Man-Month)處理過這個問題,結論是同時維護多份文件不可能持續:文件之間的進度必然產生落差,而有落差的文件最終完全沒有人更新;他給的方向是把文件盡可能併入程式自身(self-documenting programs)、減少獨立文件的數量。Brooks 的直接對象是程式的文件(散文描述與流程圖、對照原始程式);把同一條機制推廣到需求到實作的整條文件鏈,是本卡的延伸。
本卡把這個論點收斂成可操作的判準,論述基礎有三塊。站內的既有實測:一條規則的抽象卡片修過三次、執行端讀的操作文件停在第一版(原則層與操作層的單向漂移);一行宣稱約束的註解在測試已存在時是沒有保護力的副本;測試假後端檔頭的「已模擬行為」彙整清單,沒有任何機制守著它與實際處理邏輯同步、終將過期(測試註解與命名紀律)。一次文件鏈盤點:對一套 TDD 流程 skill 的文件模型逐份檢查,找到四個錯配(詳見「一次文件鏈盤點的四個錯配」段)。限制:盤點對象是一套 skill 的文件模型設計、不是在實際專案量測漂移發生率;「必然漂移」的必然性繼承自 Brooks 的論證與站內個案,樣本都是「有紀律的團隊仍然漂移」、對紀律極端嚴格的組織是否例外沒有資料。
核心原則
文件會不會漂移,由「同步期待」與「守護機制」是否匹配決定,不由撰寫紀律決定。 同步是一種對執行者當下任務沒有收益的工作——改程式的人當下的目標是讓程式對,回頭改另一份文件對這個目標零貢獻,收益全落在日後的讀者身上。收益結構決定了它不會自發發生;靠紀律撐的同步,撐到第一個 deadline 為止。
機制的判準是兩件事:觸發綁在不可跳過的動作點、且過期當下會被發現。 這條判準把兩個邊緣情境安放好——PR checklist 綁在合併 gate 上是弱機制:觸發對了、驗證仍靠人,效力取決於那格能不能被敷衍勾掉;每週被讀者消費的 roadmap 自帶弱機制:過期下週就被發現,同步對寫的人有當下收益、不落在「零收益」的前提裡——「靠習慣維持多年」的反例多半屬於這類,它們不是紀律戰勝了收益結構,是消費頻率把機制藏在裡面。
修法是把每份文件明確分到三級之一、讓期待與機制對齊——「更勤勞地同步」不在選項裡:
| 級別 | 同步期待 | 必要條件 | 例 |
|---|---|---|---|
| 活文件 | 永遠反映現狀 | 有機制守著——會紅的測試、編譯器、CI 比對腳本、lint | 測試、型別、有覆蓋檢核腳本的規格 |
| scaffold(鷹架,一次性支撐物) | 只在被消費的那一刻正確,消費後即過期 | 標記一次性身分與消費時點,過期後不得被當權威引用 | 設計骨架、需求種子包、交接摘要 |
| append-only 記錄 | 記錄寫下當時的事實,永不回改 | 標明時點,讀者知道它是史料不是現狀 | 決策記錄、工作日誌、事後檢討 |
漂移只發生在錯配格(同步期待與守護機制兩軸交出來的那一格):被期待最新、卻沒有任何機制守著的文件。三級都不落的文件就是 Brooks 說的那種——起初有人勤勞地同步、落差出現後放棄、最後留著一份看起來像權威的錯誤資訊。分級的動作本身就是修法:一份沒有機制的「活文件」,出路只有兩條——給它機制(寫比對腳本掛進 CI;或更徹底、改為從權威載體生成:副本變成 build 產物、同步義務直接消失,doctest 與 OpenAPI 文件生成正是 Brooks「併入程式」的直系後代),或誠實降級(標成 scaffold 或記錄)。
三級裡最容易被誤讀的是 append-only 記錄:它看起來沒有機制,其實是唯一不需要機制的一級——「永不回改」撐得住,因為回改一份日誌對任何人的當下任務同樣零收益,沒有力量推著它變動;時點標記讓讀者自帶折舊,讀兩年前的決策記錄不會期待它反映今天。它的陷阱在被當成現狀讀:工作日誌裡「目前架構是 X」寫下時為真、三次重構後字面依舊——這不是漂移,是讀者用錯了級別,修法是在入口標明「本目錄是時序史料」。
分級處理的是單份文件;跨文件的第二條原則跟著出來:每類資訊指定唯一的權威載體,其他位置引用、不複製。 複製出去的每一份都是一個新的同步義務,而沒有機制守著的義務不會被履行。行為的權威載體是測試——它是唯一改壞當下會發聲的那份,這正是 Brooks「把文件併入程式」在有測試文化下的現代形式;介面的權威載體是程式碼簽名;「為什麼」拆兩半——repo 內的決策脈絡在需求文件、repo 外的事實(法規、外部契約)在註解。
測試的權威帶兩個限定。它是現狀的權威、不是正確性的權威——意圖的權威在需求文件,測試可能鎖住錯誤的實作,兩個權威分歧時是需求裁決、不是文件同步問題,修測試還是修需求由人決定。且權威限於測試覆蓋的範圍——未覆蓋的行為,權威回到意圖文件,而那條界線要可見(覆蓋映射的 gap 標記就是界線)。
一次文件鏈盤點的四個錯配
對一套 TDD 流程(proposal → spec → use case → 種子包 → 設計文件 → 測試 → 追溯表)逐份套用分級,錯配長這樣:
- 同一份行為敘述存在四處:UC 場景步驟被完整複製進 ticket 的種子包(開工時塞進任務單的需求場景副本)、再進設計文件的場景區段、最後進測試。UC 修訂後、中間兩份沒有機制跟上。判定:種子包與設計文件的場景區段是 scaffold、只在階段交接那一刻正確,但沒有被標記,讀起來像活文件。
- 追溯表的狀態欄是宣稱:UC 場景與測試的映射表用人工把
gap改成covered,而既有的收尾掃描只查gap、不驗covered的真實性——假 covered 與真 covered 在表上不可區分。判定:被當活文件、機制只有半套;出路是比對腳本連 covered 一起驗,或把狀態欄降級為盤點當天的快照記錄。 - 設計文件沒有生命週期:規格設計階段產出的功能規格放在 ticket 目錄,實作完成後它長得像權威規格,實際上從實作階段的每個決策起就開始漂移。判定:它是 scaffold,消費者是測試設計階段;消費完成後要標記 archived,權威轉移到測試與程式碼。
- 回補機制是單向的:測試階段發現 UC 沒寫的邊界、回補進 UC——但已經複製出去的舊副本不在回補範圍,回補反而擴大了 UC 與副本的落差。判定:回補只該修權威載體;這正是「引用不複製」的理由——副本不存在,回補就不需要第二站。
沒這樣做的麻煩
割裂:讀者要跨多份文件才能拼出一個行為的全貌,而各份的版本不一致時,他不知道信哪份。重複:同一資訊多處出現、每處都被期待維護,漂移之後「哪份是對的」變成考古題。錯誤註解比沒有註解更糟:漂移過的文件仍然帶著權威的外觀,讀者依它行動——這與宣稱約束的註解讓 reviewer 以為有人在守是同一個機制,宣稱本身消滅了查證動機。
論述基礎第一項的那次漂移,走完整條路長這樣。抽出操作文件是為了讓執行有現成步驟可照——當初的理由成立;卡片開始修訂之後,兩份各自讀起來都完整,審卡片的人讀卡片、審稿件的人讀操作文件,沒有任何檢查把兩份並排,修正一次都沒傳到執行端。浮出水面時規則的套用率是零,第一診斷是「自審失效」、修法方向指向加強檢查——而 reviewer 拿的仍是舊文件,審查通過反而回頭確認「規則已落實」,讓漂移更難被發現。誤診的分辨動作與同步修法見 #245。
跟其他抽象層原則的關係
- #253 寫註解的動機是怕被改壞時,要處理的是那個約束:同構的上一層。#253 處理單行註解——不參與執行、改壞不發聲、防護需求要交給會發聲的機制;本卡把同一個判準抬到文件層:不被機制守著的文件、同步需求同樣送錯了窗口。「行為的權威載體是測試」直接沿用 #253 的落點選擇。
- #245 原則層與操作層是兩份會漂移的副本,而漂移只往一個方向:本卡三級分類的實測實例。抽象卡片與操作文件是兩份都被期待最新的活文件、之間沒有機制——正是錯配格;#245 的修法(反向核對、版本號對齊)是給既有雙副本補機制,本卡補上游選項:一開始就不要有第二份被期待最新的副本。
- #249 對當下段落沒有收益的標註不會自發發生:本卡「不由紀律決定」的機制來源。同步另一份文件對改程式的人當下零收益,收益結構決定它不會自發發生——分級是把「期待它發生」改成「不期待它發生、或讓機制代勞」。
- #221 檢查規則的作用域要顯式列舉:追溯表錯配的形態來源。狀態欄寫
covered與實際有測試是兩件事,宣稱與現狀在表上不可區分——把宣稱當成現狀讀,正是零 error 與未涵蓋同訊號的文件版。 - #268 要維持當期的內容,只能放在更新到得了讀者的載體上:本卡的適用邊界。三級分類預設載體可以被補機制(自家的文件、掛得上 CI 的東西),而載體改完之後到不到得了讀者是更前面的一問:印出來的書、發布後不再送達的文件、別人維護而你只能引用的內容,讀者手上那一份不因為出了新版而改變,「給機制」這條出路在那裡不存在。「誠實降級」在那裡也只解決一半——降級之後那個主題無人承接,讀者要的答案落地沒有著落,所以 #268 在降級之外多要求一步:把具體值路由到到得了讀者的載體。順序是先答那兩問,答案落在「可更新」那一格時才回來分級。
判讀徵兆
| 徵兆 | 該做的行動 |
|---|---|
| 流程新增一份文件產出、說明裡有「保持同步」「記得更新」字樣 | 問守護機制是什麼;指不出來就分級——給機制或降級 |
| 同一段敘述(場景、規格、清單)出現在兩份以上文件 | 指定權威載體、其他處改引用;scaffold 副本標記消費時點 |
| 映射表、狀態欄、覆蓋表由人工維護 | 寫比對腳本掛進 CI 或收尾階段;做不到就標成快照、註明盤點日期 |
| 一份階段性產出在流程結束後仍留在原地、沒有標記 | 補生命週期:誰消費它、消費完成後標 archived、權威轉移到哪 |
| 發現文件與現狀不符、第一反應是「補一條同步規則」 | 先分級——錯配格的解是機制或降級,加規則是在期待紀律做機制的工作 |
適用範圍與邊界
- 適用:開發流程內部的文件鏈(需求、規格、設計、測試、追溯、註解);審查一套流程或 skill 的文件模型。
- 邊界:
- 對外發布的文件(使用者手冊、公開 API 文件):讀者拿不到 repo 內的權威載體,副本無法避免;此時同步要當成發布流程的一個步驟來設計(發布時從權威載體生成),不適用「不複製」。
- 法規或稽核要求的副本:保存義務來自 repo 外,副本的存在本身是需求;標明版本與時點即可。
- 定期重編的文件(季度更新的 onboarding 指南):不是第四級,是組合——append-only 快照的序列、加上行事曆觸發的重編排程;排程就是它的機制,逾期未重編即降為史料。
- 沒有測試文化的專案:「行為的權威載體是測試」預設測試存在且自動跑;前提不成立時,文件可能就是唯一載體,先讓機制存在再談降級。