論述基礎與限制

本卡從一次審查抽出。一份規範記載的偵測程序是「改完某張卡之後,拿它的 slug 去掃兩個目錄,任一命中就是有副本要同步」。實跑:rg 回兩個由同步工具自動產生、且已被 pre-commit 覆蓋的鏡像,find 零命中,而真正分岔的那一份副本一次都沒被列出來——因為它的檔名少了一個字尾。

同一份規範已經寫著「只跑 rg 比不跑更糟,它會給出已檢查的假訊號」。這一次的形態比那句話更尖銳:兩條指令都跑了,兩條都設計正確,命中的全部是不會出問題的那一半。

第一次的修法(反查對應表)在同一個失效模式上失效,而失效是實測出來的:對一張既非同名、也不在對應表裡的卡跑修正後的程序,仍然回報零命中,而那份副本就在磁碟上、標題與本體逐字相同、本體新了兩個月。全庫量測之後才看到規模——一百五十張副本裡三十六張在對應表的射程外,而副本落後於本體的有七十四張。

限制是一則個案,且對象是文件副本不是程式。同一形態在別種以名字為鍵的偵測(重複程式碼、設定漂移、資產引用)上是否成立沒有測過。推翻方式:找一個以名字為鍵的偵測程序,它的漏檢率與副本改名率無關;本卡對它不成立。


核心原則

以名字為鍵的偵測,射程等於「名字沒變」這個假設的射程。 拿一個字串去掃全庫,掃得到的是還叫這個名字的那些。副本改名的時候,它同時退出了偵測範圍,而改名這件事本身沒有留下訊號——掃描回來的是一個較短的清單,而清單變短這件事在畫面上與「本來就沒有」長得一樣。

漏檢率與分岔率是兩件事,不要把它們接起來。 本卡初稿寫過「改名的那一群正好是已經分岔的那一群」,全庫量測直接推翻了它:同名的一百三十九對裡本體較新的有六十四對,不同名的六對裡有三對,兩個比例都在五成上下、沒有差別。改名不預測分岔。

留下的是另一件,而它更基本:漏檢不產生訊號。 掃描回來是一份較短的清單,而清單短與「本來就沒有副本」在畫面上完全一樣。所以問題不在漏掉的那些特別可能出事,在於漏掉這件事本身沒有人會知道——被漏掉的那一份從此退出所有檢查,不論它有沒有分岔。

對應表可能已經存在,而它的涵蓋率由別的需求決定。 會產生不同名副本的流程多半有一個工具負責產生它們,那個工具必須知道對應關係,所以表就在那裡——但它收錄什麼由那個工具的作用域決定,不由偵測的需求決定。本案的同步工具只在有公開鏡像的那幾個 skill 上跑,所以只有那些 skill 的異名副本進了表;而查副本的需求涵蓋每一張卡,結果是三十幾張副本既非同名也不在表裡。接上對應表是對的方向,而接上去之前要先量它的涵蓋率。


反模式:把「兩條指令都跑了」當成覆蓋

程序寫著兩條指令、註明「缺一不可」,並解釋了各自補什麼。跑完兩條、看到有命中,於是判定這一項已經檢查過。

這個判定跳過了一個問題:命中的那幾個,是不是本來就不會出問題的那一群。本次的兩個命中都在 pre-commit 的覆蓋範圍內——它們不同步就 commit 不過去,所以掃到它們等於沒有新資訊。真正只靠人工同步的那一份不在清單上。

回報覆蓋之前先問一句:這次命中的東西,有沒有別的機制已經在守? 全部都有的話,這次掃描的增量是零。


修法

工具做完之後還有一層射程限制要寫下來。 本案的工具用 commit 時間判斷哪一邊比較新,所以它抓得到「副本比較舊」,抓不到「副本比較新而內容已經分岔」——實測命中一例,兩份的敘述確實不同而時間戳讓它判為不落後。工具照設計運作而結論是錯的,這正是本卡自己的形態,差別只在它現在寫在工具的輸出裡。

用多個鍵取聯集,不要換一個鍵。 本案的第一次修法是「反查對應表拿到別名再掃」,而它在同一個失效模式上失效——對表外那幾十張仍然回報零命中,只是把鍵從卡名換到了表名。可用的形態是把每一種對應各給一個鍵取聯集:檔名相同、標題相同(去專案化時常常只改檔名不改標題)、對應表登記、內容提及。四個鍵各自抓到不同的子集,本案四個驗收案例分別由其中三個不同的鍵命中。

換掉鍵而不是增加鍵,是這一類修法最容易犯的錯,因為新的鍵在手上那個案例上有效,而它自己的射程沒有被量過。

驗收案例要蓋到每一個鍵,而且不能有兩個案例共用同一種輸出。 三案例(同名 / 不同名 / 無副本)是最低限度,而它有一個陷阱:「真的沒有副本」與「有副本但沒抓到」的輸出都是零行,兩個案例分不開彼此。所以案例集要保證每一個鍵至少有一個案例只靠它才命中——少了那一種,換鍵造成的失效通不過驗收也發現不了。

驗收的觸發要綁在會讓鍵失效的那個檔案上,不是綁在工具上。 本案的鍵之一解析另一支腳本的內容,而那支腳本每新增一個異名副本都會被編輯——觸發若只寫「改動這支工具之後」,那種改動永遠不會叫起驗收。

兩側刻意不同名的時候,把理由寫在對應表旁邊。 去專案化與重新界定都是正當的改名理由,記下來之後,下一個人看到不同名不會以為是筆誤而「順手統一」,把偵測的依據抹掉。

能自動化就別留在程序裡。 產生副本的工具已經持有對應表,讓它多一個檢查模式比要求每個人記得跑一段 shell 便宜。這一步在本次沒有做,登記在 posts 的 Backlog 上。


判讀徵兆

徵兆為什麼是訊號下一個動作
偵測程序的鍵是一個會被改寫的名字(slug、檔名)改名的副本靜默退出射程,而退出不產生錯誤找產生副本的工具有沒有對應表,接上去
修法把鍵換掉而不是增加鍵新的鍵在手上那個案例有效,而它自己的射程沒有被量過改成多鍵聯集,每個鍵配一個只靠它命中的驗收案例
掃描的命中全部落在另一個機制已經覆蓋的範圍內這次掃描的增量是零,而它讀起來像已經檢查過列出命中,逐個問「這一個本來就會被擋嗎」
同一份內容的兩側刻意用不同名,而理由沒有寫下來下一個人會把不同名當筆誤統一掉,順手抹掉偵測的依據把理由記在對應表旁
程序自己寫著「缺一不可」而沒有寫驗收案例指令的正確性與程序的覆蓋率是兩件事,前者不保證後者補三個案例(同名 / 不同名 / 無副本)

跟其他抽象層原則的關係

與 #297 自審的偵測方法要對得上規則的類型 是同一條紀律的兩個層次。 那裡問的是偵測方法配不配得上規則的類型,這裡問的是偵測方法的鍵撐不撐得住對象的變形。共同的失效形態是方法看起來執行了、而它量的東西與該量的東西不是同一個。

補 #221 lint 的作用域要是明確的事實 的一個缺口。 那張卡處理的是規則涵蓋哪些目錄要有登記,這裡處理的是即使目錄涵蓋到了,鍵對不上仍然掃不到。未納管目錄的零 error 與合規目錄的零 error 訊號相同這句話在這裡多一個形態:鍵改過的檔案就在納管目錄裡,掃描照樣是零。

與 #309 探針讀得到定義那個詞的系統提示 同機制。 那裡是量測工具的輸入被外部條件污染,這裡是偵測工具的鍵與對象脫鉤;兩者都是工具照設計運作而結論是錯的,而錯誤不以失敗的形式出現,以一份較短的清單或一組較收斂的數字出現。

與 #240 路由的目的地要真的承接那個主題 的三層驗收同形。 那裡是「存在」不等於「承接」,這裡是「掃到」不等於「掃全」;兩者都要求在工具回報成功之後多做一個語意判定。