論述基礎與限制

本卡從一套自建流量統計的資料判讀抽出,同一個錯誤在兩天內發生兩次、形狀完全相同。

第一次:原本的資料模型是每次瀏覽產生一列。判斷「這個 session 只看了一頁」的分類公式寫成 COUNTIFS(session欄, 當列session) = 1。後來為了量停留時間,加入離開事件,每次瀏覽變成一進一離兩列。公式沒有報錯,但 = 1 這個條件從此永遠不成立——每次瀏覽至少兩列。「單頁即走」這個分類從那一刻起恆為零,而零看起來就像「沒有這種流量」。

第二次:修正版公式開頭加了「非進入事件的列一律留白」,避免同一次瀏覽被計數兩次。這個修正在「每次瀏覽都有進入事件」的前提下正確。真實資料裡出現了第三種形狀:某些訪客只有離開事件、沒有進入事件(接收端的執行紀錄證實那個時間點沒收到任何請求,成因未確認)。這類列被開頭那個條件整批留白,一整類流量在統計裡完全不存在。

兩次都不是語法錯誤,也不是資料錯誤。公式合法、輸入正確、輸出是合法的分類標籤,只有分佈變了。

證據強度要說清楚,因為它與本卡的主張層級不對稱。觀測是兩次、來自同一個系統、同一位作者、同一週之內——這個規模不足以支持任何比例宣稱,也不足以支持「這類錯誤有多常見」。而核心原則不靠這兩次觀測成立:條件式編碼了未宣告的資料形狀假設,這是從條件式的性質推導出來的,兩次事件是它的例示而非樣本。分辨這兩層很重要,否則讀者會誤以為原則的可信度等於 n=2。

限制:本卡談的是資料形狀增加——新增事件型別、新增來源、新增狀態。欄位改名或型別變更在有型別檢查的環境會產生錯誤而自己現形,因此不在此列;在試算表這類沒有型別的環境它們同樣是靜默的(改欄位標題不會讓依範圍參照的公式報錯,數字被存成文字會讓求和悄悄少算),那些情形適用本卡的同一套修法。


核心原則

分析邏輯裡的每一個條件式都編碼了一個對資料形狀的假設,而那個假設沒有住址。 資料多出一種形狀時,條件式仍然合法執行、仍然回傳值,只是它回答的問題已經和寫下它時不是同一個。

不可見有三個成分。

假設不寫在任何地方。 COUNTIFS(...) = 1 這個式子裡的「1」承載的是「一次瀏覽產生一列」這個資料模型事實。這個事實不在公式旁邊、不在欄位定義裡、不在文件裡——它只存在於寫下公式那一刻的心智模型中,而心智模型不會隨資料模型一起更新。

錯誤的輸出與正確的輸出外觀相同。 分類邏輯回傳的仍然是合法標籤,沒有錯誤值、沒有型別衝突、沒有任何執行期的異常。要察覺它算錯,得先有一個獨立來源知道「這裡本來應該有多少」——而分析邏輯存在的理由,正是因為沒有那個獨立來源。

變更的動機讓副作用更難聯想。 新增資料形狀通常是為了收集更多資訊,動機是「知道更多」;副作用卻是某些既有統計悄悄歸零或翻倍。歸零最危險——它與「這一類根本不存在」在數字上完全不可區分,這是 #221 描述的同一種訊號同構,只是發生在分析層而非檢查層。


修法

資料模型新增一種形狀時,把下游分析邏輯的重算當成同一次變更的一部分。 分析邏輯是資料模型的下游依賴,和欄位定義、寫入程式碼屬於同一次變更的範圍。延後處理會失去對照:之後回頭看時,看到的數字已經是新公式算出來的,沒有東西可以比對。

讓未涵蓋的資料可見,而非留白。 分類邏輯加一個 catch-all 分支,把沒被任何條件接住的列標成「未分類」。留白與「不適用」在視覺上不可區分,「未分類」則是一個會被看見的標籤——它不需要有人事先知道漏了什麼,只需要有人看一眼分佈。

用總量守恆檢查涵蓋。 分類後各類數量的總和與原始列數的差額應該為零,差額不為零代表有形狀沒被涵蓋。這個檢查的成本是一條公式,抓的卻是所有「沒想到還有這種資料」的情形——它不要求預先列舉漏掉了什麼,這正是它比逐條檢視有效的地方。

條件式裡的常數要寫下它的來源。= 1 旁註明「假設每次瀏覽一列」。這讓資料模型變更時的反向搜尋成為可能:改事件模型的人可以搜尋「每次瀏覽」,找到所有依賴這個事實的地方。沒有這行註記時,那些依賴分散在各個條件式裡,只能靠記憶列舉。


判讀徵兆

某個分類的計數恆為零,且沒有人記得它上次非零是什麼時候——先檢查它的條件式是否還與當前資料形狀相符,再檢查現實中是否真的沒有那一類資料。

資料模型變更後,總量沒變但各分類的比例大幅移動——移動可能是真實的,也可能是某條件式換了語意,兩者的區別要靠對照舊公式在舊資料上的結果。

新增了一種事件、一種來源或一種狀態,而變更範圍裡沒有任何一處提到報表或公式——這代表下游依賴尚未被盤點,不代表下游沒有依賴。


跟其他原則的關係

  • #221 檢查規則的作用域要顯式列舉:兩者共享「零與未涵蓋不可區分」這個核心結構,作用層不同。#221 說的是檢查工具的納管範圍(未納管目錄的零 error 與合規目錄的零 error 訊號相同),本卡說的是分析邏輯的涵蓋範圍(未被條件接住的資料與不存在的資料計數相同)。處置方向也同構——都是把隱含的範圍變成顯式的列舉。
  • #232 自審 sweep 的偵測方法要對齊規則類型:#232 講偵測方法要看得見違反形態才可信,本卡是它在資料層的對應——分析邏輯要涵蓋得到某種資料形狀,它的計數才可信。兩者都在回答「這個 clean / 這個零,是通過還是沒被看到」。
  • #228 等比縮放不管空間分配:#228 指出工具的保證只涵蓋它工作的那一層,問題發生在別層時工具的靜默是「沒看到」而非「沒有」。本卡把同一件事推到分析邏輯:公式的保證只涵蓋它被寫下時的資料形狀,新形狀出現時它的合法輸出是「沒看到」而非「沒有」。
  • #239 宣告的組合不等於執行的組合:兩者都描述「存在」與「作用」之間的落差。#239 的落差在規則層(規則寫下了但沒被執行),本卡的落差在邏輯層(條件式執行了但已經不對應原本的問題)。本卡多一個條件——落差是由另一次正確的變更引入的,因此在變更當下沒有任何人做錯事。
  • #252 配額耗盡的症狀落在申請最頻繁的元件、成因在持有最久的那個:同屬「錯誤產物與正確產物外觀相同」的家族,本卡沒有涵蓋解釋這一層。本卡的對象是分析邏輯的輸出(合法標籤、無錯誤值),#252 的對象是診斷者當場串出來的解釋——它的每個組成事實都經得起查證,因此自洽度無法作為正確性的證據,與正確的解釋在讀起來的感受上不可區分。兩者的處置方向相同:引入一個獨立於產物本身的來源來驗,本卡是總量守恆檢查,#252 是系統保存的持有者紀錄。
  • #243 要求執行者做判定的規則,要一併規定判定留下什麼痕跡:本卡的「條件式常數要寫下來源」是它在自動化判定上的形態。#243 處理人做的判定沒有痕跡就不可複驗,本卡處理程式做的判定沒有記下前提就不可追溯——兩者都要求把判定所依賴的東西留在產物裡,而不是留在做判定的人腦中。
  • #153 漏抓先分 design gap 與 execution gap:發現統計算錯時的第一個分流。本卡描述的是設計落差(分析邏輯的涵蓋範圍沒跟上資料模型),而它容易被誤診成執行落差(「這次忘了更新公式」)——誤診的代價是修好這一次、下一次新增形狀時重演。

跟三個相鄰工程機制的差別

這張卡描述的現象與三個既有機制相鄰,界線值得說清楚,否則它讀起來像是它們的重述。

型別系統處理的是型別層的形狀,而本卡的形狀在語意層。一個列舉多出一個成員、一個事件型別欄多出一個值,型別完全沒變(都是字串),而依賴「這一欄只有兩種值」的計數邏輯已經失效。任何型別系統都接不住這一類。

Schema migration 覆蓋寫入端——建表、加欄、回填。慣例上它不覆蓋衍生的分析邏輯,因為那些散落在報表、儀表板與臨時查詢裡,不屬於 schema 的定義範圍。本卡的貢獻正是指出這條 checklist 停在 write path。

API 版本相容性最接近,兩者都在處理「生產者變了、消費者沒跟上」。差別在於 API 有一個明確的介面可以掛版號、有兩方各自的發布節奏、有 deprecation 期。本卡的情境是生產者與消費者是同一個人、隔幾天、沒有任何介面——沒有版號可掛,也沒有第二方會抱怨。