論述基礎與限制

一天之內同一支共用 skill 在發佈庫上被覆蓋三次,牽涉兩個專案的三個 session。三次的結構相同而角色互換:本專案在其中兩次是被覆蓋的一方,一次是覆蓋別人的一方。

三次都可在發佈庫的 git 歷史裡逐個 commit 查證,本卡的敘述以那份歷史為準,而不是以任何一方的回報為準——其中一次的當事人回報與實際歷史不符,成因寫在下面。

單一資產、單一工具、單日。 成立的是機制與修法,不是頻率。


核心原則

版號由作者指派,而不是由內容推導,所以它證明不了誰比誰新。 兩個作者從同一個基底出發,各自取「下一個號」會取到同一個號;版號比對在這種分岔上恆真通過,推送成功那一行照樣印出來,先推的那一份被整檔蓋掉而沒有任何訊號。

能偵測分岔的是基底:我這一份從哪裡長出來、對方那一份從哪裡長出來、兩者的共同祖先是什麼。三方比對回答得了這個問題,版號大小回答不了——版號只有一個維度,而分岔是兩個。

推論是:版號適合用來告訴讀者「這一份跟上一份差在哪」,不適合用來當並行寫入的閘門。 把它當閘門,等於用一個由被檢查者自己填寫的欄位去檢查被檢查者。


三次覆蓋

次序誰蓋掉誰版號關係怎麼被發現
一B 的 1.8.0 蓋掉 A 的 1.8.0相同A 在下一輪推送前比對版號時發現
二B 的 1.8.1 蓋掉 A 的 1.9.0倒退B 自行發現,取回歷史版本為基底重推、讓號 1.9.1
三A 的 1.10.0 蓋掉 B 的 1.9.1遞增(而基底是舊的)A 推完以 git clone 回讀時發現

第一次是版號相同,比對恆真通過。第二次是版號倒退,工具本來會警告而推送端帶了旁路旗標。第三次的版號遞增且看起來完全正常——A 手上的基底是舊的,於是它算出來的「下一個號」比對方已發佈的號還大,比對照樣放行。三種形態在版號這一個維度上分別是相等、變小、變大,而三次的實質都是同一件事:推送方沒有對方那一份。


同型:一個信號承載兩種狀態,而讀它的邏輯只認得一種

版號不是唯一的載體。同一批事件裡另一個 session 回報了同構的一次:一筆比較並交換式的引用更新回傳退出碼 128,而該操作唯一預期的失敗是並行移動造成的衝突,於是執行者依先驗機率讀成衝突、退避重試八次全部失敗;取標準錯誤才看到真因是守衛擋下。同一個退出碼承載兩種語意,而讀它的邏輯只認得其中一種——與版號相等時有兩種可能(真的是同一版/兩個作者各自從同一基底取號)而比對只認得前一種,是同一件事。

這一例另給出一個可直接用的訊號:為條件 A 設計的重試迴圈一次都沒有成功時,那個信號多半代表 B。重試的成功率是免費的診斷資料,而它要在放棄之前讀,不是之後。

這條訊號單獨給不夠,由同一位回報者的自陳補上。 他當時查過那個引用的移動頻率(約三十秒一次),算出八次重試只跨八秒、在碰撞假設下不該全滅——矛盾算出來了,而接下來調整的是重試次數,不是假設。可調的參數就在手邊,換假設要先承認前一個是錯的,兩者的即時成本差很多,所以證據到手不等於假設會被換掉。處置要跟訊號綁在一起:重試全滅時先寫下這個結果在原假設下的機率,那個數字低到不合理就停止調參數、去讀原始錯誤輸出。

為什麼旁路會變成常態

推送工具本來有兩道閘門——portability 檢查與三方方向警告——而一個旗標同時關掉兩者,違規清單只印在標準錯誤。

真正讓它變成常態的是旁路的理由每一次都成立。那支 skill 帶著幾處教學用的票號示例,字面命中閘門的樣式而語意上正當,於是每一次推送都要靠旗標才過得去;旗標因此寫進了操作流程。真違規混在正當豁免裡,就永遠不會被單獨看見。(這個觀察由對方的 session 提出,本方的紀錄裡原本只有「旗標有未文件化的副作用」這一半。)

處置分兩步而順序不能顛倒:先把正當豁免消掉——那幾處票號示例改用不屬於任何專案的命名空間之後,閘門零違規通過,旗標當場不再需要——再把旗標從流程裡拿掉。只做第二步會讓每次推送都停在同一批已知無害的告警上,而人對重複出現的告警會停止閱讀。

有一種豁免消不掉,而它正好由修法產生。 那批票號改名之後,記錄這次改名的 changelog 為了說明改了什麼,必須把舊字面寫出來——於是裝有引用穩定性守衛的消費端拉到新版就提交不了,由該消費端回報才浮現。汰除某個字面時,記錄那次汰除的文字是它最後一個合法住址,而任何以字面為判斷標準的檢查對這個住址與對真違規一視同仁。這一類要用行內豁免標記處理,並在標記裡寫出理由;判別是問這個字面在這一句裡是被使用的還是被談論的。標記的類別要用執行者當下答得出來的維度命名——描述來源的類別(測試資料/示意/搬遷)要他知道這段文字打哪來,而那個判別句問的是它在這一句裡的用途,用途這一問才是他手上就有的資訊。這一條的一般形態(防護知識與需要它的時點錯位,分位置與維度兩軸)住在已經記錄過的失效仍然重演。


第三次的成因:拿快取端點當權威狀態

A 判定自己的內容「第二次被覆蓋」時,讀的是發佈庫的原始檔案端點。那個端點有內容分發快取,回讀到的是數分鐘前的內容,而它與真實狀態的落差沒有任何訊號——讀得到、格式正確、看起來就是答案。而 B 當時已經自行復原,也已經修好 A 打算修的那個問題。

於是第三次覆蓋的成因與它想修的錯誤同型:A 據一份過期的觀測做了一次多餘的重寫,而那次重寫蓋掉了 B 已經寫好的修正與紀錄。

查共用資產的實際狀態要用會取得完整歷史的方式(git clone),或用推送工具自己印的版號比對行——那一行比對的對象是它自己的複本,不是快取端點。這一項是代理比本體便宜的位置重合型實例:快取端點與來源在「讀得到內容」這個位置上重合,取用它與成本差無關,而重合正是它不現形的原因。


工具早就得出這個結論,而讀的人沒有跟上

這三次覆蓋發生在一支已經停用版號當判斷標準的工具上。它的同步清單產生函式在原始碼裡明寫理由:版本字串不再進入同步決策,因為兩個獨立演化的分支可能巧合共用同一版本號而內容不同,semver 比較又預設線性演進、對分支式分歧失準;內容同一性改判雜湊。那正是本卡的結論,而它比本卡早。

所以三次覆蓋的成因落在讀的人身上:工具的防線在,而讀的人仍然把一個已經被降級的顯示行當成閘門。降級發生在原始碼裡,而讀的人看到的還是同一行字——一個欄位的語意被改掉時,它在畫面上的樣子不會跟著改,而舊語意會由它的位置、它的措辭、以及所有引用過它的文件繼續供應。本站的操作規範就曾把它寫成第一順位的判斷標準,等於把那個誤讀固定下來。

降級失敗還有第二個原因,由另一位當事人的自陳提供:被降級的那一行看起來比真正的閘門更像判斷標準。 他讀工具輸出時同時看到方向警告與版號對照兩行,選了版號那一行,理由是它是數字、看起來可比較。一個警告句要人判斷它適不適用,一個版號對照直接給出大小關係——版面上更像判斷標準的那一個會贏,而它是被作廢的那一個。所以降級一個欄位時,光是改掉它在原始碼裡的角色不夠:接手的閘門要在輸出上比它更像判斷標準(給出可直接執行的結論,而不是一句要人自行判斷的描述),否則舊欄位會靠版面繼續當閘門用。

版面優勢還有第三層,由第三個來源指出:閘門在輸出裡的位置決定它過不過得了讀者的過濾。 方向警告印在開頭、成功訊息印在末尾,而讀長輸出的人用 tail 取尾段、或用 grep 撈「成功了沒」的關鍵字——兩種過濾方式最容易保留的都是最後那行,最容易丟掉的是最前面那行。本卡作者在同一天的多次推送裡逐次用 grep 過濾輸出,而過濾樣式裡有成功訊息與版號比對、沒有方向警告,直到後來才補進去。所以閘門要贏過被降級的欄位,還得贏過讀者的過濾器:印在成功訊息之後,或讓它在任何合理的過濾樣式下都留得住(例如把判定結果併進成功訊息那一行)。這條修法落在輸出層而不是原始碼層,理由由回報方點出:改掉欄位在原始碼裡的角色屬於文件規範,輸出樣式才是預設行為,而預設行為勝過文件規範。 把降級只做在程式碼與說明文件裡,等於用一份需要人去讀、去記得的規範,去對抗一個每次執行都出現在眼前的版面。

這一節與上面的退出碼是同一件事的兩種載體:語意變更不改變介面,於是舊讀法繼續成立地執行下去。退出碼在守衛加入之後多承載一種語意而數字沒變,版號在降級之後少承載一種語意而那一行沒變;兩者都沒有任何一處會通知既有的讀法它已經失效。

顯示行與檔案分家還有第二條路徑:抽取版號這件事本身會出錯。該工具的抽取邏輯對兩種寫法有不同的搜尋範圍(一種搜全文、一種只搜前置區塊),寫法互相覆蓋時取到的可能是別處那一個;而各專案自建的摘要工具在解析更新紀錄的版號行時,被破折號後緊接的反引號與日期括號截斷過(併行專案實測兩例,檔案本身正確而報告錯誤)。要確定一份資產的版本,讀它的來源欄位,不讀任何報告字串。

修法

閘門是方向警告,不是版號比對。 不帶旁路旗標推送時,工具會印出兩行:一行是「兩側自上次同步後都改動過」的方向警告,由三方基底以內容雜湊算出,看到它就去逐段比對;另一行是本地與遠端的版號對照,它是給人對照更新紀錄用的顯示,不是判定依據。本地版號高於遠端不構成推送的許可——三次覆蓋裡有一次的版號正是遞增而基底是舊的。

讓號規則:先發佈的一方保號,後發現的一方讓號。 理由是其他消費端可能已經拉走先發佈的那一版,改動已發佈的號會讓他們手上的內容與號對不起來。

推完立刻以權威來源回讀。 三次覆蓋裡有兩次是隔了一輪才發現的,而回讀的成本是一次 clone。回讀要驗的是雙方的內容各自還在,不只驗自己的。

合併時把對方的紀錄原文放回。 覆蓋事件最容易第二次遺失的是對方寫下的根因分析——它在 changelog 裡,而不在程式碼裡,所以任何以檔案為單位的比對都不會特別提醒它。


跟其他抽象層原則的關係

原則關係
#338 廢除共用資產時,前提與效果都在自己看不到的那一側同一個共用資產家族的兩種操作。那張卡管刪除、本卡管更新;共同結構是本地證據對跨消費端的判斷不夠,而不夠的部分不會有訊號。
#334 代理比本體便宜本卡第三次覆蓋的直接成因是它的位置重合型:快取端點代理來源狀態。本卡另補一個形態——版號代理「誰比較新」,而它是被檢查者自己填的欄位。
#291 壞掉的掃描會回報一個看起來合理的數字同族的輸出形態:版號比對通過、推送成功、快取回讀成功,三者都完成了動作而量到的不是那件事。差別在那張卡的工具是壞的,本卡的工具是好的而量測對象選錯。
#339 壓縮標籤省下的字數是一次,展開成本乘以引用點數錨點寫法家族的延伸:兩個 1.8.0 在版號這個標識符上長得一樣,與兩個同名異義的標籤在字串上長得一樣是同一件事——標識符承載不了它被要求承載的區分。
#342 寫出特定實例的值,就取代了讀者本來要做的查證同一個處置形態的另一個載體:本卡是接手的閘門要在輸出上比被降級的欄位更像判斷標準,那張卡是不留可抄的東西。兩者都把正確行為做進結構,而不是交給人的記性——交給記性的代價由每一次遺忘承擔,做進結構的付一次。
#221 檢查的作用域要寫成可查證的事實旁路那一段的上位:豁免由被檢查者持有時,正當豁免與真違規在輸出上同形。本卡補一個處置順序——先消掉正當豁免,再收緊閘門。

判讀徵兆

訊號該做的事
推送流程裡寫死了旁路旗標先查它每次旁路掉什麼;理由每次都成立時,先消掉那個理由
本地版號與遠端相同或較低先拉取合併,不要靠「我這份比較新」的印象
推送成功,而沒有人回讀過發佈端覆蓋在推送端沒有訊號,回讀的成本是一次 clone
兩個 session 在同一天動同一份共用資產推送前互相要一個可比對的指紋,不要互相問「是不是同一份」
合併時只比對程式碼與正文對方的根因紀錄住在 changelog,逐檔比對不會特別提醒它

核心:版號回答「這一份與上一份差在哪」,回答不了「我這一份有沒有蓋掉別人的」。後一個問題只有基底答得出來。