<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>版本管理 on Tarragon</title><link>https://tarrragon.github.io/blog/tags/%E7%89%88%E6%9C%AC%E7%AE%A1%E7%90%86/</link><description>Recent content in 版本管理 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 08 Sep 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/%E7%89%88%E6%9C%AC%E7%AE%A1%E7%90%86/index.xml" rel="self" type="application/rss+xml"/><item><title>版號擋不住並行覆蓋，因為兩個作者會從同一基底取到同一個號</title><link>https://tarrragon.github.io/blog/report/version-number-cannot-detect-divergence/</link><pubDate>Tue, 08 Sep 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/report/version-number-cannot-detect-divergence/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>一天之內同一支共用 skill 在發佈庫上被覆蓋三次，牽涉兩個專案的三個 session。三次的結構相同而角色互換：本專案在其中兩次是被覆蓋的一方，一次是覆蓋別人的一方。&lt;/p>
&lt;p>三次都可在發佈庫的 git 歷史裡逐個 commit 查證，本卡的敘述以那份歷史為準，而不是以任何一方的回報為準——其中一次的當事人回報與實際歷史不符，成因寫在下面。&lt;/p>
&lt;p>&lt;strong>單一資產、單一工具、單日。&lt;/strong> 成立的是機制與修法，不是頻率。&lt;/p>
&lt;hr>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>版號由作者指派，而不是由內容推導，所以它證明不了誰比誰新。&lt;/strong> 兩個作者從同一個基底出發，各自取「下一個號」會取到同一個號；版號比對在這種分岔上恆真通過，推送成功那一行照樣印出來，先推的那一份被整檔蓋掉而沒有任何訊號。&lt;/p>
&lt;p>能偵測分岔的是&lt;strong>基底&lt;/strong>：我這一份從哪裡長出來、對方那一份從哪裡長出來、兩者的共同祖先是什麼。三方比對回答得了這個問題，版號大小回答不了——版號只有一個維度，而分岔是兩個。&lt;/p>
&lt;p>推論是：&lt;strong>版號適合用來告訴讀者「這一份跟上一份差在哪」，不適合用來當並行寫入的閘門。&lt;/strong> 把它當閘門，等於用一個由被檢查者自己填寫的欄位去檢查被檢查者。&lt;/p>
&lt;hr>
&lt;h2 id="三次覆蓋">三次覆蓋&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>次序&lt;/th>
 &lt;th>誰蓋掉誰&lt;/th>
 &lt;th>版號關係&lt;/th>
 &lt;th>怎麼被發現&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>一&lt;/td>
 &lt;td>B 的 1.8.0 蓋掉 A 的 1.8.0&lt;/td>
 &lt;td>相同&lt;/td>
 &lt;td>A 在下一輪推送前比對版號時發現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>二&lt;/td>
 &lt;td>B 的 1.8.1 蓋掉 A 的 1.9.0&lt;/td>
 &lt;td>倒退&lt;/td>
 &lt;td>B 自行發現，取回歷史版本為基底重推、讓號 1.9.1&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>三&lt;/td>
 &lt;td>A 的 1.10.0 蓋掉 B 的 1.9.1&lt;/td>
 &lt;td>遞增（而基底是舊的）&lt;/td>
 &lt;td>A 推完以 git clone 回讀時發現&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第一次是版號相同，比對恆真通過。第二次是版號倒退，工具本來會警告而推送端帶了旁路旗標。第三次的版號遞增且看起來完全正常——&lt;strong>A 手上的基底是舊的，於是它算出來的「下一個號」比對方已發佈的號還大，比對照樣放行&lt;/strong>。三種形態在版號這一個維度上分別是相等、變小、變大，而三次的實質都是同一件事：推送方沒有對方那一份。&lt;/p>
&lt;hr>
&lt;h2 id="同型一個信號承載兩種狀態而讀它的邏輯只認得一種">同型：一個信號承載兩種狀態，而讀它的邏輯只認得一種&lt;/h2>
&lt;p>版號不是唯一的載體。同一批事件裡另一個 session 回報了同構的一次：一筆比較並交換式的引用更新回傳退出碼 128，而該操作&lt;strong>唯一預期的失敗&lt;/strong>是並行移動造成的衝突，於是執行者依先驗機率讀成衝突、退避重試八次全部失敗；取標準錯誤才看到真因是守衛擋下。同一個退出碼承載兩種語意，而讀它的邏輯只認得其中一種——與版號相等時有兩種可能（真的是同一版／兩個作者各自從同一基底取號）而比對只認得前一種，是同一件事。&lt;/p>
&lt;p>這一例另給出一個可直接用的訊號：&lt;strong>為條件 A 設計的重試迴圈一次都沒有成功時，那個信號多半代表 B&lt;/strong>。重試的成功率是免費的診斷資料，而它要在放棄之前讀，不是之後。&lt;/p>
&lt;p>&lt;strong>這條訊號單獨給不夠，由同一位回報者的自陳補上。&lt;/strong> 他當時查過那個引用的移動頻率（約三十秒一次），算出八次重試只跨八秒、在碰撞假設下不該全滅——&lt;strong>矛盾算出來了，而接下來調整的是重試次數，不是假設&lt;/strong>。可調的參數就在手邊，換假設要先承認前一個是錯的，兩者的即時成本差很多，所以證據到手不等於假設會被換掉。處置要跟訊號綁在一起：重試全滅時先寫下&lt;strong>這個結果在原假設下的機率&lt;/strong>，那個數字低到不合理就停止調參數、去讀原始錯誤輸出。&lt;/p>
&lt;h2 id="為什麼旁路會變成常態">為什麼旁路會變成常態&lt;/h2>
&lt;p>推送工具本來有兩道閘門——portability 檢查與三方方向警告——而一個旗標同時關掉兩者，違規清單只印在標準錯誤。&lt;/p>
&lt;p>真正讓它變成常態的是&lt;strong>旁路的理由每一次都成立&lt;/strong>。那支 skill 帶著幾處教學用的票號示例，字面命中閘門的樣式而語意上正當，於是每一次推送都要靠旗標才過得去；旗標因此寫進了操作流程。&lt;strong>真違規混在正當豁免裡，就永遠不會被單獨看見。&lt;/strong>（這個觀察由對方的 session 提出，本方的紀錄裡原本只有「旗標有未文件化的副作用」這一半。）&lt;/p>
&lt;p>處置分兩步而順序不能顛倒：先把正當豁免消掉——那幾處票號示例改用不屬於任何專案的命名空間之後，閘門零違規通過，旗標當場不再需要——再把旗標從流程裡拿掉。只做第二步會讓每次推送都停在同一批已知無害的告警上，而人對重複出現的告警會停止閱讀。&lt;/p>
&lt;p>&lt;strong>有一種豁免消不掉，而它正好由修法產生。&lt;/strong> 那批票號改名之後，記錄這次改名的 changelog 為了說明改了什麼，必須把舊字面寫出來——於是裝有引用穩定性守衛的消費端拉到新版就提交不了，由該消費端回報才浮現。&lt;strong>汰除某個字面時，記錄那次汰除的文字是它最後一個合法住址&lt;/strong>，而任何以字面為判斷標準的檢查對這個住址與對真違規一視同仁。這一類要用行內豁免標記處理，並在標記裡寫出理由；判別是問這個字面在這一句裡是被使用的還是被談論的。&lt;strong>標記的類別要用執行者當下答得出來的維度命名&lt;/strong>——描述來源的類別（測試資料／示意／搬遷）要他知道這段文字打哪來，而那個判別句問的是它在這一句裡的用途，用途這一問才是他手上就有的資訊。這一條的一般形態（防護知識與需要它的時點錯位，分位置與維度兩軸）住在&lt;a href="../recorded-failures-recur-when-the-record-is-off-path/">已經記錄過的失效仍然重演&lt;/a>。&lt;/p>
&lt;hr>
&lt;h2 id="第三次的成因拿快取端點當權威狀態">第三次的成因：拿快取端點當權威狀態&lt;/h2>
&lt;p>A 判定自己的內容「第二次被覆蓋」時，讀的是發佈庫的原始檔案端點。那個端點有內容分發快取，回讀到的是數分鐘前的內容，而&lt;strong>它與真實狀態的落差沒有任何訊號&lt;/strong>——讀得到、格式正確、看起來就是答案。而 B 當時已經自行復原，也已經修好 A 打算修的那個問題。&lt;/p>
&lt;p>於是第三次覆蓋的成因與它想修的錯誤同型：A 據一份過期的觀測做了一次多餘的重寫，而那次重寫蓋掉了 B 已經寫好的修正與紀錄。&lt;/p>
&lt;p>查共用資產的實際狀態要用會取得完整歷史的方式（&lt;code>git clone&lt;/code>），或用推送工具自己印的版號比對行——那一行比對的對象是它自己的複本，不是快取端點。這一項是&lt;a href="../proxy-is-cheaper-than-the-thing-it-stands-for/">代理比本體便宜&lt;/a>的位置重合型實例：快取端點與來源在「讀得到內容」這個位置上重合，取用它與成本差無關，而重合正是它不現形的原因。&lt;/p>
&lt;hr>
&lt;h2 id="工具早就得出這個結論而讀的人沒有跟上">工具早就得出這個結論，而讀的人沒有跟上&lt;/h2>
&lt;p>這三次覆蓋發生在一支&lt;strong>已經停用版號當判斷標準&lt;/strong>的工具上。它的同步清單產生函式在原始碼裡明寫理由：版本字串不再進入同步決策，因為兩個獨立演化的分支可能巧合共用同一版本號而內容不同，semver 比較又預設線性演進、對分支式分歧失準；內容同一性改判雜湊。&lt;strong>那正是本卡的結論，而它比本卡早。&lt;/strong>&lt;/p>
&lt;p>所以三次覆蓋的成因落在讀的人身上：&lt;strong>工具的防線在，而讀的人仍然把一個已經被降級的顯示行當成閘門&lt;/strong>。降級發生在原始碼裡，而讀的人看到的還是同一行字——&lt;strong>一個欄位的語意被改掉時，它在畫面上的樣子不會跟著改&lt;/strong>，而舊語意會由它的位置、它的措辭、以及所有引用過它的文件繼續供應。本站的操作規範就曾把它寫成第一順位的判斷標準，等於把那個誤讀固定下來。&lt;/p>
&lt;p>&lt;strong>降級失敗還有第二個原因，由另一位當事人的自陳提供：被降級的那一行看起來比真正的閘門更像判斷標準。&lt;/strong> 他讀工具輸出時同時看到方向警告與版號對照兩行，選了版號那一行，理由是它是數字、看起來可比較。一個警告句要人判斷它適不適用，一個版號對照直接給出大小關係——&lt;strong>版面上更像判斷標準的那一個會贏，而它是被作廢的那一個&lt;/strong>。所以降級一個欄位時，光是改掉它在原始碼裡的角色不夠：接手的閘門要在輸出上比它更像判斷標準（給出可直接執行的結論，而不是一句要人自行判斷的描述），否則舊欄位會靠版面繼續當閘門用。&lt;/p>
&lt;p>&lt;strong>版面優勢還有第三層，由第三個來源指出：閘門在輸出裡的位置決定它過不過得了讀者的過濾。&lt;/strong> 方向警告印在開頭、成功訊息印在末尾，而讀長輸出的人用 &lt;code>tail&lt;/code> 取尾段、或用 &lt;code>grep&lt;/code> 撈「成功了沒」的關鍵字——&lt;strong>兩種過濾方式最容易保留的都是最後那行，最容易丟掉的是最前面那行&lt;/strong>。本卡作者在同一天的多次推送裡逐次用 &lt;code>grep&lt;/code> 過濾輸出，而過濾樣式裡有成功訊息與版號比對、沒有方向警告，直到後來才補進去。所以閘門要贏過被降級的欄位，還得贏過讀者的過濾器：印在成功訊息之後，或讓它在任何合理的過濾樣式下都留得住（例如把判定結果併進成功訊息那一行）。&lt;strong>這條修法落在輸出層而不是原始碼層，理由由回報方點出：改掉欄位在原始碼裡的角色屬於文件規範，輸出樣式才是預設行為，而預設行為勝過文件規範。&lt;/strong> 把降級只做在程式碼與說明文件裡，等於用一份需要人去讀、去記得的規範，去對抗一個每次執行都出現在眼前的版面。&lt;/p>
&lt;p>這一節與上面的退出碼是同一件事的兩種載體：&lt;strong>語意變更不改變介面，於是舊讀法繼續成立地執行下去&lt;/strong>。退出碼在守衛加入之後多承載一種語意而數字沒變，版號在降級之後少承載一種語意而那一行沒變；兩者都沒有任何一處會通知既有的讀法它已經失效。&lt;/p>
&lt;p>顯示行與檔案分家還有第二條路徑：抽取版號這件事本身會出錯。該工具的抽取邏輯對兩種寫法有不同的搜尋範圍（一種搜全文、一種只搜前置區塊），寫法互相覆蓋時取到的可能是別處那一個；而各專案自建的摘要工具在解析更新紀錄的版號行時，被破折號後緊接的反引號與日期括號截斷過（併行專案實測兩例，檔案本身正確而報告錯誤）。&lt;strong>要確定一份資產的版本，讀它的來源欄位，不讀任何報告字串。&lt;/strong>&lt;/p>
&lt;h2 id="修法">修法&lt;/h2>
&lt;p>&lt;strong>閘門是方向警告，不是版號比對。&lt;/strong> 不帶旁路旗標推送時，工具會印出兩行：一行是「兩側自上次同步後都改動過」的方向警告，由三方基底以內容雜湊算出，看到它就去逐段比對；另一行是本地與遠端的版號對照，&lt;strong>它是給人對照更新紀錄用的顯示，不是判定依據&lt;/strong>。本地版號高於遠端不構成推送的許可——三次覆蓋裡有一次的版號正是遞增而基底是舊的。&lt;/p>
&lt;p>&lt;strong>讓號規則：先發佈的一方保號，後發現的一方讓號。&lt;/strong> 理由是其他消費端可能已經拉走先發佈的那一版，改動已發佈的號會讓他們手上的內容與號對不起來。&lt;/p>
&lt;p>&lt;strong>推完立刻以權威來源回讀。&lt;/strong> 三次覆蓋裡有兩次是隔了一輪才發現的，而回讀的成本是一次 clone。回讀要驗的是雙方的內容各自還在，不只驗自己的。&lt;/p>
&lt;p>&lt;strong>合併時把對方的紀錄原文放回。&lt;/strong> 覆蓋事件最容易第二次遺失的是對方寫下的根因分析——它在 changelog 裡，而不在程式碼裡，所以任何以檔案為單位的比對都不會特別提醒它。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>原則&lt;/th>
 &lt;th>關係&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="../shared-asset-removal-reasons-across-the-boundary/">#338 廢除共用資產時，前提與效果都在自己看不到的那一側&lt;/a>&lt;/td>
 &lt;td>同一個共用資產家族的兩種操作。那張卡管刪除、本卡管更新；共同結構是本地證據對跨消費端的判斷不夠，而不夠的部分不會有訊號。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../proxy-is-cheaper-than-the-thing-it-stands-for/">#334 代理比本體便宜&lt;/a>&lt;/td>
 &lt;td>本卡第三次覆蓋的直接成因是它的位置重合型：快取端點代理來源狀態。本卡另補一個形態——版號代理「誰比較新」，而它是被檢查者自己填的欄位。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../broken-scan-reports-a-plausible-number/">#291 壞掉的掃描會回報一個看起來合理的數字&lt;/a>&lt;/td>
 &lt;td>同族的輸出形態：版號比對通過、推送成功、快取回讀成功，三者都完成了動作而量到的不是那件事。差別在那張卡的工具是壞的，本卡的工具是好的而量測對象選錯。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../compressed-label-cost-multiplies-with-citations/">#339 壓縮標籤省下的字數是一次，展開成本乘以引用點數&lt;/a>&lt;/td>
 &lt;td>錨點寫法家族的延伸：兩個 1.8.0 在版號這個標識符上長得一樣，與兩個同名異義的標籤在字串上長得一樣是同一件事——標識符承載不了它被要求承載的區分。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../written-value-replaces-the-readers-check/">#342 寫出特定實例的值，就取代了讀者本來要做的查證&lt;/a>&lt;/td>
 &lt;td>同一個處置形態的另一個載體：本卡是接手的閘門要在輸出上比被降級的欄位更像判斷標準，那張卡是不留可抄的東西。兩者都把正確行為做進結構，而不是交給人的記性——交給記性的代價由每一次遺忘承擔，做進結構的付一次。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../lint-scope-must-be-explicit-fact/">#221 檢查的作用域要寫成可查證的事實&lt;/a>&lt;/td>
 &lt;td>旁路那一段的上位：豁免由被檢查者持有時，正當豁免與真違規在輸出上同形。本卡補一個處置順序——先消掉正當豁免，再收緊閘門。&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>該做的事&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>推送流程裡寫死了旁路旗標&lt;/td>
 &lt;td>先查它每次旁路掉什麼；理由每次都成立時，先消掉那個理由&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>本地版號與遠端相同或較低&lt;/td>
 &lt;td>先拉取合併，不要靠「我這份比較新」的印象&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>推送成功，而沒有人回讀過發佈端&lt;/td>
 &lt;td>覆蓋在推送端沒有訊號，回讀的成本是一次 clone&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>兩個 session 在同一天動同一份共用資產&lt;/td>
 &lt;td>推送前互相要一個可比對的指紋，不要互相問「是不是同一份」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>合併時只比對程式碼與正文&lt;/td>
 &lt;td>對方的根因紀錄住在 changelog，逐檔比對不會特別提醒它&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心&lt;/strong>：版號回答「這一份與上一份差在哪」，回答不了「我這一份有沒有蓋掉別人的」。後一個問題只有基底答得出來。&lt;/p></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>一天之內同一支共用 skill 在發佈庫上被覆蓋三次，牽涉兩個專案的三個 session。三次的結構相同而角色互換：本專案在其中兩次是被覆蓋的一方，一次是覆蓋別人的一方。</p>
<p>三次都可在發佈庫的 git 歷史裡逐個 commit 查證，本卡的敘述以那份歷史為準，而不是以任何一方的回報為準——其中一次的當事人回報與實際歷史不符，成因寫在下面。</p>
<p><strong>單一資產、單一工具、單日。</strong> 成立的是機制與修法，不是頻率。</p>
<hr>
<h2 id="核心原則">核心原則</h2>
<p><strong>版號由作者指派，而不是由內容推導，所以它證明不了誰比誰新。</strong> 兩個作者從同一個基底出發，各自取「下一個號」會取到同一個號；版號比對在這種分岔上恆真通過，推送成功那一行照樣印出來，先推的那一份被整檔蓋掉而沒有任何訊號。</p>
<p>能偵測分岔的是<strong>基底</strong>：我這一份從哪裡長出來、對方那一份從哪裡長出來、兩者的共同祖先是什麼。三方比對回答得了這個問題，版號大小回答不了——版號只有一個維度，而分岔是兩個。</p>
<p>推論是：<strong>版號適合用來告訴讀者「這一份跟上一份差在哪」，不適合用來當並行寫入的閘門。</strong> 把它當閘門，等於用一個由被檢查者自己填寫的欄位去檢查被檢查者。</p>
<hr>
<h2 id="三次覆蓋">三次覆蓋</h2>
<table>
  <thead>
      <tr>
          <th>次序</th>
          <th>誰蓋掉誰</th>
          <th>版號關係</th>
          <th>怎麼被發現</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一</td>
          <td>B 的 1.8.0 蓋掉 A 的 1.8.0</td>
          <td>相同</td>
          <td>A 在下一輪推送前比對版號時發現</td>
      </tr>
      <tr>
          <td>二</td>
          <td>B 的 1.8.1 蓋掉 A 的 1.9.0</td>
          <td>倒退</td>
          <td>B 自行發現，取回歷史版本為基底重推、讓號 1.9.1</td>
      </tr>
      <tr>
          <td>三</td>
          <td>A 的 1.10.0 蓋掉 B 的 1.9.1</td>
          <td>遞增（而基底是舊的）</td>
          <td>A 推完以 git clone 回讀時發現</td>
      </tr>
  </tbody>
</table>
<p>第一次是版號相同，比對恆真通過。第二次是版號倒退，工具本來會警告而推送端帶了旁路旗標。第三次的版號遞增且看起來完全正常——<strong>A 手上的基底是舊的，於是它算出來的「下一個號」比對方已發佈的號還大，比對照樣放行</strong>。三種形態在版號這一個維度上分別是相等、變小、變大，而三次的實質都是同一件事：推送方沒有對方那一份。</p>
<hr>
<h2 id="同型一個信號承載兩種狀態而讀它的邏輯只認得一種">同型：一個信號承載兩種狀態，而讀它的邏輯只認得一種</h2>
<p>版號不是唯一的載體。同一批事件裡另一個 session 回報了同構的一次：一筆比較並交換式的引用更新回傳退出碼 128，而該操作<strong>唯一預期的失敗</strong>是並行移動造成的衝突，於是執行者依先驗機率讀成衝突、退避重試八次全部失敗；取標準錯誤才看到真因是守衛擋下。同一個退出碼承載兩種語意，而讀它的邏輯只認得其中一種——與版號相等時有兩種可能（真的是同一版／兩個作者各自從同一基底取號）而比對只認得前一種，是同一件事。</p>
<p>這一例另給出一個可直接用的訊號：<strong>為條件 A 設計的重試迴圈一次都沒有成功時，那個信號多半代表 B</strong>。重試的成功率是免費的診斷資料，而它要在放棄之前讀，不是之後。</p>
<p><strong>這條訊號單獨給不夠，由同一位回報者的自陳補上。</strong> 他當時查過那個引用的移動頻率（約三十秒一次），算出八次重試只跨八秒、在碰撞假設下不該全滅——<strong>矛盾算出來了，而接下來調整的是重試次數，不是假設</strong>。可調的參數就在手邊，換假設要先承認前一個是錯的，兩者的即時成本差很多，所以證據到手不等於假設會被換掉。處置要跟訊號綁在一起：重試全滅時先寫下<strong>這個結果在原假設下的機率</strong>，那個數字低到不合理就停止調參數、去讀原始錯誤輸出。</p>
<h2 id="為什麼旁路會變成常態">為什麼旁路會變成常態</h2>
<p>推送工具本來有兩道閘門——portability 檢查與三方方向警告——而一個旗標同時關掉兩者，違規清單只印在標準錯誤。</p>
<p>真正讓它變成常態的是<strong>旁路的理由每一次都成立</strong>。那支 skill 帶著幾處教學用的票號示例，字面命中閘門的樣式而語意上正當，於是每一次推送都要靠旗標才過得去；旗標因此寫進了操作流程。<strong>真違規混在正當豁免裡，就永遠不會被單獨看見。</strong>（這個觀察由對方的 session 提出，本方的紀錄裡原本只有「旗標有未文件化的副作用」這一半。）</p>
<p>處置分兩步而順序不能顛倒：先把正當豁免消掉——那幾處票號示例改用不屬於任何專案的命名空間之後，閘門零違規通過，旗標當場不再需要——再把旗標從流程裡拿掉。只做第二步會讓每次推送都停在同一批已知無害的告警上，而人對重複出現的告警會停止閱讀。</p>
<p><strong>有一種豁免消不掉，而它正好由修法產生。</strong> 那批票號改名之後，記錄這次改名的 changelog 為了說明改了什麼，必須把舊字面寫出來——於是裝有引用穩定性守衛的消費端拉到新版就提交不了，由該消費端回報才浮現。<strong>汰除某個字面時，記錄那次汰除的文字是它最後一個合法住址</strong>，而任何以字面為判斷標準的檢查對這個住址與對真違規一視同仁。這一類要用行內豁免標記處理，並在標記裡寫出理由；判別是問這個字面在這一句裡是被使用的還是被談論的。<strong>標記的類別要用執行者當下答得出來的維度命名</strong>——描述來源的類別（測試資料／示意／搬遷）要他知道這段文字打哪來，而那個判別句問的是它在這一句裡的用途，用途這一問才是他手上就有的資訊。這一條的一般形態（防護知識與需要它的時點錯位，分位置與維度兩軸）住在<a href="../recorded-failures-recur-when-the-record-is-off-path/">已經記錄過的失效仍然重演</a>。</p>
<hr>
<h2 id="第三次的成因拿快取端點當權威狀態">第三次的成因：拿快取端點當權威狀態</h2>
<p>A 判定自己的內容「第二次被覆蓋」時，讀的是發佈庫的原始檔案端點。那個端點有內容分發快取，回讀到的是數分鐘前的內容，而<strong>它與真實狀態的落差沒有任何訊號</strong>——讀得到、格式正確、看起來就是答案。而 B 當時已經自行復原，也已經修好 A 打算修的那個問題。</p>
<p>於是第三次覆蓋的成因與它想修的錯誤同型：A 據一份過期的觀測做了一次多餘的重寫，而那次重寫蓋掉了 B 已經寫好的修正與紀錄。</p>
<p>查共用資產的實際狀態要用會取得完整歷史的方式（<code>git clone</code>），或用推送工具自己印的版號比對行——那一行比對的對象是它自己的複本，不是快取端點。這一項是<a href="../proxy-is-cheaper-than-the-thing-it-stands-for/">代理比本體便宜</a>的位置重合型實例：快取端點與來源在「讀得到內容」這個位置上重合，取用它與成本差無關，而重合正是它不現形的原因。</p>
<hr>
<h2 id="工具早就得出這個結論而讀的人沒有跟上">工具早就得出這個結論，而讀的人沒有跟上</h2>
<p>這三次覆蓋發生在一支<strong>已經停用版號當判斷標準</strong>的工具上。它的同步清單產生函式在原始碼裡明寫理由：版本字串不再進入同步決策，因為兩個獨立演化的分支可能巧合共用同一版本號而內容不同，semver 比較又預設線性演進、對分支式分歧失準；內容同一性改判雜湊。<strong>那正是本卡的結論，而它比本卡早。</strong></p>
<p>所以三次覆蓋的成因落在讀的人身上：<strong>工具的防線在，而讀的人仍然把一個已經被降級的顯示行當成閘門</strong>。降級發生在原始碼裡，而讀的人看到的還是同一行字——<strong>一個欄位的語意被改掉時，它在畫面上的樣子不會跟著改</strong>，而舊語意會由它的位置、它的措辭、以及所有引用過它的文件繼續供應。本站的操作規範就曾把它寫成第一順位的判斷標準，等於把那個誤讀固定下來。</p>
<p><strong>降級失敗還有第二個原因，由另一位當事人的自陳提供：被降級的那一行看起來比真正的閘門更像判斷標準。</strong> 他讀工具輸出時同時看到方向警告與版號對照兩行，選了版號那一行，理由是它是數字、看起來可比較。一個警告句要人判斷它適不適用，一個版號對照直接給出大小關係——<strong>版面上更像判斷標準的那一個會贏，而它是被作廢的那一個</strong>。所以降級一個欄位時，光是改掉它在原始碼裡的角色不夠：接手的閘門要在輸出上比它更像判斷標準（給出可直接執行的結論，而不是一句要人自行判斷的描述），否則舊欄位會靠版面繼續當閘門用。</p>
<p><strong>版面優勢還有第三層，由第三個來源指出：閘門在輸出裡的位置決定它過不過得了讀者的過濾。</strong> 方向警告印在開頭、成功訊息印在末尾，而讀長輸出的人用 <code>tail</code> 取尾段、或用 <code>grep</code> 撈「成功了沒」的關鍵字——<strong>兩種過濾方式最容易保留的都是最後那行，最容易丟掉的是最前面那行</strong>。本卡作者在同一天的多次推送裡逐次用 <code>grep</code> 過濾輸出，而過濾樣式裡有成功訊息與版號比對、沒有方向警告，直到後來才補進去。所以閘門要贏過被降級的欄位，還得贏過讀者的過濾器：印在成功訊息之後，或讓它在任何合理的過濾樣式下都留得住（例如把判定結果併進成功訊息那一行）。<strong>這條修法落在輸出層而不是原始碼層，理由由回報方點出：改掉欄位在原始碼裡的角色屬於文件規範，輸出樣式才是預設行為，而預設行為勝過文件規範。</strong> 把降級只做在程式碼與說明文件裡，等於用一份需要人去讀、去記得的規範，去對抗一個每次執行都出現在眼前的版面。</p>
<p>這一節與上面的退出碼是同一件事的兩種載體：<strong>語意變更不改變介面，於是舊讀法繼續成立地執行下去</strong>。退出碼在守衛加入之後多承載一種語意而數字沒變，版號在降級之後少承載一種語意而那一行沒變；兩者都沒有任何一處會通知既有的讀法它已經失效。</p>
<p>顯示行與檔案分家還有第二條路徑：抽取版號這件事本身會出錯。該工具的抽取邏輯對兩種寫法有不同的搜尋範圍（一種搜全文、一種只搜前置區塊），寫法互相覆蓋時取到的可能是別處那一個；而各專案自建的摘要工具在解析更新紀錄的版號行時，被破折號後緊接的反引號與日期括號截斷過（併行專案實測兩例，檔案本身正確而報告錯誤）。<strong>要確定一份資產的版本，讀它的來源欄位，不讀任何報告字串。</strong></p>
<h2 id="修法">修法</h2>
<p><strong>閘門是方向警告，不是版號比對。</strong> 不帶旁路旗標推送時，工具會印出兩行：一行是「兩側自上次同步後都改動過」的方向警告，由三方基底以內容雜湊算出，看到它就去逐段比對；另一行是本地與遠端的版號對照，<strong>它是給人對照更新紀錄用的顯示，不是判定依據</strong>。本地版號高於遠端不構成推送的許可——三次覆蓋裡有一次的版號正是遞增而基底是舊的。</p>
<p><strong>讓號規則：先發佈的一方保號，後發現的一方讓號。</strong> 理由是其他消費端可能已經拉走先發佈的那一版，改動已發佈的號會讓他們手上的內容與號對不起來。</p>
<p><strong>推完立刻以權威來源回讀。</strong> 三次覆蓋裡有兩次是隔了一輪才發現的，而回讀的成本是一次 clone。回讀要驗的是雙方的內容各自還在，不只驗自己的。</p>
<p><strong>合併時把對方的紀錄原文放回。</strong> 覆蓋事件最容易第二次遺失的是對方寫下的根因分析——它在 changelog 裡，而不在程式碼裡，所以任何以檔案為單位的比對都不會特別提醒它。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../shared-asset-removal-reasons-across-the-boundary/">#338 廢除共用資產時，前提與效果都在自己看不到的那一側</a></td>
          <td>同一個共用資產家族的兩種操作。那張卡管刪除、本卡管更新；共同結構是本地證據對跨消費端的判斷不夠，而不夠的部分不會有訊號。</td>
      </tr>
      <tr>
          <td><a href="../proxy-is-cheaper-than-the-thing-it-stands-for/">#334 代理比本體便宜</a></td>
          <td>本卡第三次覆蓋的直接成因是它的位置重合型：快取端點代理來源狀態。本卡另補一個形態——版號代理「誰比較新」，而它是被檢查者自己填的欄位。</td>
      </tr>
      <tr>
          <td><a href="../broken-scan-reports-a-plausible-number/">#291 壞掉的掃描會回報一個看起來合理的數字</a></td>
          <td>同族的輸出形態：版號比對通過、推送成功、快取回讀成功，三者都完成了動作而量到的不是那件事。差別在那張卡的工具是壞的，本卡的工具是好的而量測對象選錯。</td>
      </tr>
      <tr>
          <td><a href="../compressed-label-cost-multiplies-with-citations/">#339 壓縮標籤省下的字數是一次，展開成本乘以引用點數</a></td>
          <td>錨點寫法家族的延伸：兩個 1.8.0 在版號這個標識符上長得一樣，與兩個同名異義的標籤在字串上長得一樣是同一件事——標識符承載不了它被要求承載的區分。</td>
      </tr>
      <tr>
          <td><a href="../written-value-replaces-the-readers-check/">#342 寫出特定實例的值，就取代了讀者本來要做的查證</a></td>
          <td>同一個處置形態的另一個載體：本卡是接手的閘門要在輸出上比被降級的欄位更像判斷標準，那張卡是不留可抄的東西。兩者都把正確行為做進結構，而不是交給人的記性——交給記性的代價由每一次遺忘承擔，做進結構的付一次。</td>
      </tr>
      <tr>
          <td><a href="../lint-scope-must-be-explicit-fact/">#221 檢查的作用域要寫成可查證的事實</a></td>
          <td>旁路那一段的上位：豁免由被檢查者持有時，正當豁免與真違規在輸出上同形。本卡補一個處置順序——先消掉正當豁免，再收緊閘門。</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>推送流程裡寫死了旁路旗標</td>
          <td>先查它每次旁路掉什麼；理由每次都成立時，先消掉那個理由</td>
      </tr>
      <tr>
          <td>本地版號與遠端相同或較低</td>
          <td>先拉取合併，不要靠「我這份比較新」的印象</td>
      </tr>
      <tr>
          <td>推送成功，而沒有人回讀過發佈端</td>
          <td>覆蓋在推送端沒有訊號，回讀的成本是一次 clone</td>
      </tr>
      <tr>
          <td>兩個 session 在同一天動同一份共用資產</td>
          <td>推送前互相要一個可比對的指紋，不要互相問「是不是同一份」</td>
      </tr>
      <tr>
          <td>合併時只比對程式碼與正文</td>
          <td>對方的根因紀錄住在 changelog，逐檔比對不會特別提醒它</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：版號回答「這一份與上一份差在哪」，回答不了「我這一份有沒有蓋掉別人的」。後一個問題只有基底答得出來。</p>
]]></content:encoded></item></channel></rss>