廢除一項共用資產時,它的前提與它的效果都在自己看不到的那一側
論述基礎與限制
由一次跨三個消費端的裁決往回追出來。事件是本專案刪掉共用 skill 底下的兩個 hook,兩週後另外兩個專案來問為什麼——它們都還在跑,各有四處註冊。三處判斷都在本地做過,三處都錯或都不完整。單一事件,三個消費端;同步工具的行為經原始碼確認,其餘工具鏈的傳播語意未量測。
核心原則
一項資產只要被別人裝過,廢除它的三個判斷就都跨過了自己的邊界:沒有人在用(前提)、刪掉之後別人就沒有了(效果)、這項資產在這裡沒有用(理由)。 三個判斷都做得出來,也都有本地的證據支持,而本地的證據對它們一個都不夠。
三者的共同形狀是本地檢查的輸出,與跨邊界的事實無關——它在兩種世界裡回報同一個結果。
前提:「沒有人在用」本地驗不到
刪除的理由裡寫著「任何 settings 都沒有註冊它們」。這句話對寫的人為真,而它讀起來像一個關於全體的事實。而另外兩個消費端各有四處註冊,其中一個在刪除前五天還修過那支腳本。
grep 自己的 settings 得到零,與沒有人在用,是同一個輸出。判別問句是這條指令跑在誰的機器上——答案是「我的」,那它證明的就只有「我沒在用」。
修法不必是去問遍所有人。把作用域寫進那句話就夠了:「本專案的 settings 未註冊」是一個本地驗得到的事實,而且它讓下游知道這句話沒有涵蓋他們。省略作用域的版本反而更貴,因為它讓三方各自去找一個不存在的東西。而作用域有兩個會漏掉的位置:判定(這個集合分成幾格)與措辭(每一格的宣稱有多寬),修好判定不會順便修好措辭,見非互動環境裡的確認關卡裡刪除清單那一段。
效果:刪除傳播不到已經裝了的人
同步工具有一個旗標讓刪除送到共用庫(--prune),而它管的是推送方向。拉取方向沒有對應的動作:拉取只覆蓋兩邊都有的檔、只新增遠端多的檔,本地獨有的檔一律保留(該工具的預覽把它們標成 local-only (preserved),而拉取的程式路徑裡沒有任何刪除呼叫)。
所以刪除的射程是「還沒裝的人」。已經裝了的人不但留著那個檔,還會在下一次拉取時收到宣告刪除的版本紀錄——於是他們同時持有「這個檔已刪除」的紀錄與這個檔本身,兩份互相否證而沒有任何訊號。三個消費端裡有兩個處在這個狀態,都是靠人工盤點才發現的。
這一層特別容易漏,因為它躲在一個已經想過的問題後面:知道「刪除要靠 --prune 傳播」的人,會認為自己已經處理過傳播這件事。
理由:「不適用」有兩種,遠看相同而處置相反
判定一項檢查在本地不適用時,看到的通常是同一個現象——它什麼都沒查到。而它底下有兩種完全不同的情形:
| 目標集合為空 | 作用域寫死而指向別處 | |
|---|---|---|
| 本地現象 | 什麼都沒查到 | 什麼都沒查到 |
| 實情 | 這裡沒有它要檢查的那種對象 | 這裡有,只是它掃的路徑不是這個 |
| 處置 | 可以廢除 | 改成可設定的路徑,不是廢除 |
上面那次事件裡兩支腳本各佔一種。一支檢查某種前端框架的事件處理函式,而本專案沒有那種原始碼——目標集合確實是空的。另一支檢查某個目錄底下表格內會讓工具崩潰的字元,而本專案同樣有表格、同樣會被那個工具讀,只是它寫死的目錄名不同——那是設定沒設對,不是不適用。第二支被判成不適用,判的人(本卡的作者)看的是路徑對不對得上,而路徑對不對得上是一個關於設定的事實。
判別問句只有一句:把作用域改對之後,這支檢查有沒有對象可查。 有,就是設定問題;沒有,才是不適用。這一問要在動手刪之前問,因為刪掉之後那個腳本要從版本歷史裡取回來才讀得到。
缺陷狀態:目標集合為空的東西不會失敗,而不失敗被讀成沒有缺陷
三個判斷之外還有第四個,它由前三個裡的「不適用」衍生出來。廢除意見寫了「兩支都沒有已知缺陷、還在跑的人不必停用」,而下游把其中一支搬到主題相符的位置時發現:它的報告生成函式用單引數呼叫一個要兩個參數的 logger,每一次走到報告路徑都會拋例外,同一個錯誤呼叫散在六個輔助函式裡。它在我們這裡從來沒有失敗過,因為目標集合是空的——沒有東西要報告,就永遠走不到那條路徑。
「沒有已知缺陷」在那句話裡與「在我的環境不會觸發」是同一個表述。不適用不只讓人判不出它該不該廢除,還讓人判不出它有沒有缺陷:判定缺陷狀態的前提是跑得到那條路徑,而不適用正是跑不到的那種情形。所以廢除紀錄三選一的第三項(寫出缺陷)要加一個限定——寫不出缺陷時要分開寫是「查過、沒有」還是「本環境無法觸發其主要路徑,缺陷狀態未知」,兩者在紀錄上同形而下游的處置相反。
修法
廢除紀錄的三選一。 指名接手的機制(並確認那個名字打得開)、寫出不適用的條件並標明作用域(別人可以拿自己的環境去對)、或寫出缺陷(還在跑它的人需要知道)。沒有指向的句子把判斷成本轉嫁給每一個之後撞到它的人,而他們手上的資訊比寫的人少。
共用資產的廢除,把通知寫進動作裡。 刪除既然傳播不到已安裝的消費端,「刪掉」這個動作本身就不完整;完整的版本是刪除加上一則說得出理由的紀錄,讓收到紀錄的人自己判斷要不要跟。
要恢復的話,先讓作用域可設定再恢復。 原樣恢復會把同一個寫死的路徑送回每一個消費端,而它們的目錄結構本來就不同。
跟其他原則的關係
- #221 檢查規則的作用域是一個獨立的事實 — 同一個機制換了一層邊界。那裡的作用域是路徑集合、漏掉的是自己專案裡的目錄;本卡的作用域是消費端集合、漏掉的是別人的機器。兩者的零回報都與合規同形,而本卡這一層連補掃都做不到——別人的 settings 不在自己的檔案系統上。
- #337 審查的射程只涵蓋被審查的對象 — 相鄰。那裡是工具落在射程外,這裡是別的消費端落在射程外;共同點是那個對象從一開始就不在問句的值域裡,所以加檢查補不到,要靠把它寫進清單或寫進紀錄。
- #334 代理比本體便宜 — 三個判斷各自取了一個代理:自己的 settings 代理全體的 settings、
--prune代理刪除的傳播、路徑對不對得上代理適用性。三者都便宜、都會給出一個乾淨的結果,而第三個的代理是本卡作者在回答別人的提問時當場取的。 - #325 散文裡指向一組成員的數字,在結構改動之後不會跟著動 — 同批的一次自傷示範:本卡的初稿把「兩週後」寫成另一個數字,處置是改成不自帶計數而不是把數字改對。
判讀徵兆
- 刪除的理由裡有一句關於全體的事實,而支持它的指令跑在自己的機器上。
- 版本紀錄說某個檔已刪除,而那個檔在工作區裡。
- 一項共用資產的廢除紀錄寫「已由其他機制覆蓋」,而問三個人都說不出那個機制是什麼。
- 判定某支檢查不適用的依據是它掃的路徑在這裡不存在。