跨線綜合 · 工程
檢查因為錯誤的理由而通過:同一種病的九個變體
10,156 字 · 約 29 分鐘
當時的六條線、一個 SaaS、外加這個站自己,九個案例都是同一種病:檢查通過了,但通過的理由是錯的。最凶的是最後一個:壞掉的不是被檢查的東西,是檢查的標準本身,所以那個 100 分不代表任何事。
把六條產品線加上一個做失敗的 SaaS 攤開來看,最貴的錯誤幾乎沒有一個是「算錯了」。它們長成同一個形狀:
檢查通過了,而通過的理由是錯的。
這種缺陷不會拋例外、不會讓儀表板變紅、不會在 code review 裡被看出來。它會安靜地把「已驗證」這三個字發給你,而你會相信它,因為那正是驗證存在的意義。
下面九個變體來自四個不同的系統,最後三個來自這個網站本身,而且都是在寫這篇文章的同一週被抓到的。
九個變體
| # | 層 | 案例 | 檢查為什麼會過 |
|---|---|---|---|
| 1 | 驗證 | /blog/* 對 HEAD 回 405、對 GET 回 200 | 檢查只用 GET |
| 2 | 紀錄 | 紀錄寫「已 noindex」,實際 457 頁沒做 | 沒有人回頭量 |
| 3 | 量測 | 25,008 曝光看起來像需求證明 | 曝光的另一端不是人 |
| 4 | 執行 | 自檢腳本掃 Alpaca 關鍵字,漏掉整個 IBKR 下單層 | 掃到的東西真的存在 |
| 5 | 執行 | preflight 印「全部通過」而券商非同步回 Error 201;dry run 也印 sent | 最後一行不扛真相 |
| 6 | 供料 | 原油側錄斷六天,log 每晚印「無資料」印了兩週 | 印在沒有人看的地方 |
| 7 | 本站 | 建置腳本的自檢因為磁碟剛好是那個狀態而通過 | 測試路徑與真實路徑不同 |
| 8 | 本站 | 前四篇文章的 Accessibility 都是 100,而缺陷當時就在 | 測試從沒觸發到它 |
| 9 | 本站 · 評分 | 兩篇英文文章拿到 100/100,而評分用的是中文門檻 | 檢查的標準本身是錯的 |
上圖:每一條線都是一個真的變體,沒有裝飾用的邊。右下角那三條是本站自己犯的。
第一組:缺陷在那裡,但檢查路徑碰不到
變體 1。 那個站的部落格路徑對 GET 回 200、對 HEAD 回 405。所有的健康檢查都用 GET,所以全部是綠的。
問題是爬蟲不一定用 GET。RFC 9110 §9.3.2(查證日期 2026-08-27)寫的是:伺服器必須對 HEAD 請求送出與 GET 相同的標頭欄位。一個對 HEAD 回 405 的路徑是壞的,只是壞在一條沒有人測的路上。
變體 8,這個站自己。 2026-08-27,前四篇文章的 Lighthouse Accessibility 全部是 100。第五篇一發,掉到 96。
原因是程式碼區塊的語法上色用了深色 token 顏色(#e1e4e8)配紙色底,對比 1.12 —— 白紙上寫白字。這個缺陷從樣式寫好那天就存在,只是前四篇沒有任何一個程式碼區塊,所以測試從來沒有碰到它。
前四篇的 100 分是真的量出來的,也確實沒有意義。這跟原油那條「樣本數不長就是管線斷了」是同一個形狀:東西壞在一條當時的測試碰不到的路徑上,而分數會替它作證。
第二組:標記不等於驗證
變體 2。 那個站有 457 個薄內容頁,佔全站 URL 的一半。工作紀錄上寫著「已 noindex」。實際去量的時候,它們沒有。
Google 的封鎖索引說明(查證日期 2026-08-27)講的是另一個容易搞混的前提:noindex 要生效,該頁不能被 robots.txt 擋住,爬蟲必須看得到那條規則。也就是說,「我加了 noindex」和「那條 noindex 有生效」是兩件要分開驗的事,而當時連第一件都沒真的做。
把「做了」寫進紀錄,跟去量一次「它現在是什麼狀態」,中間隔著整個工程的可信度。這個站的規則因此寫成:標 ✅ 的唯一條件是已驗證 —— 測試通過、筆數對得上、或是對線上 URL 打一次看回什麼。檔案存在不算,建置過了也不算。判準寫在〈方法論〉。
第三組:指標因為錯誤的理由而好看
變體 3。 那個站有一篇文章拿到 25,008 次曝光、平均位置 7.36。看起來像「內容相關性可以贏過網域權重」的證據。
把 query 拆開之後:
| 類別 | 曝光 | 點擊 | CTR |
|---|---|---|---|
| 人類語法(4 條) | 72 | 4 | 5.6% |
| 機器排列組合(約 56 條) | 585 | 0 | 0% |
| 對話殘片 | 18 | 0 | 0% |
| GSC 匿名化長尾(1–2 曝光) | 24,333(97.3%) | — | 組成未知 |
決定性的證據是那些對話殘片:gimme the full list、how about apex?、yes please、ja gerne、si porfa。沒有人會在搜尋框裡打「ja gerne」。 那是人對聊天機器人講的話,助理把它丟去搜尋做 grounding,於是計了一次曝光。
全站的數字是 176 點擊 / 63,692 曝光 / CTR 0.28% / 平均位置 8.69。位置很好、點擊近乎沒有,因為排在前面的那些 query 的另一端不是人。
我對這個數字的第一個解釋也是錯的
看到那些對話殘片時,我的第一個結論是「所以 GEO 可行、AI 在讀我的內容」。那個結論推過頭了,已經撤回。
一次 GSC 曝光只證明「這個 URL 出現在那次 grounding 查詢的結果清單裡」。它不證明內容被讀取,更不證明被引用。中間掉了兩層,而唯二能補上的路都斷了:那台伺服器已經不在服務、存取日誌多半已經沒了;站已經 301,也沒辦法回頭去問 AI 產品當時看到什麼。
所以正確的結論是未知,不是可行也不是不可行。用一個看起來支持自己的數字去得到一個舒服的結論,正是這篇在講的病。
變體 5。 同一個病在下單層的樣子:preflight 印「全部通過」,而券商正在非同步回 Error 201;dry run 也印 sent。
第二個特別惡毒。人只會看最後一行,不會回頭讀四行以前那個 DRY RUN 標記。所以規則是:最後一行要自己扛住真相。
變體 4。 自檢腳本掃的是 Alpaca 的關鍵字,而那套系統的下單層是 IBKR。腳本掃到了東西、回報綠燈、整個下單層從來沒被檢查過。掃到的東西是真的存在的,這就是它為什麼過。
第四組:供料停了,輸出還在
變體 6。 原油的行情側錄從 2026-08-17 之後斷掉,到發現時已經六個交易日。分析照跑、報告照出、樣本數凍結在 n=18–23。
證據一直在眼前:兩次回放隔了兩個交易日,那斯達克的樣本數由 50 增到 60,而原油四個窗是 18/17/18/23 → 18/17/18/23,一個都沒增加。細節在〈我以為它過了,多五天資料後沒過〉。
而且它每晚都在 log 裡印「無資料」,印了兩週。那不是靜默失敗,是大聲失敗但沒有人聽。
變體 7 的完整版:這個站自己犯的那一次
這一列跟前面七列有一個差別:它是當場被抓到並修好的,其他七個都是事後才發現。
績效資料的產生腳本有一組不變量,其中一條是「已平倉筆數只增不減」。檢查函式的寫法是:無條件讀取磁碟上的那份 daily.json,跟新算出來的 payload 比。自檢用的是一份合成的 payload,只有 1 筆已平倉。
- 一開始為什麼會過:那時候真檔是 0 筆。1 ≥ 0,通過。自檢報 18/18。
- 後來為什麼必然失敗:真的累積出 3 筆已平倉之後,合成 payload 的 1 筆對上磁碟的 3 筆,每次都紅。
關鍵不是它 2026-08-27 壞了,是它一開始的通過從來不是因為邏輯正確,是因為磁碟剛好是那個狀態。那個「✅ 18/18」被寫進交接文件,而它證明的東西比它看起來的少得多。
修法也值得寫。最省事的做法是給檢查函式加一個 prev=None 參數,自檢時傳 None 跳過那段。那是把測試改成不去測它 —— 缺陷還在,只是測試繞開了,等於再製造一次變體 1。
實際的修法是:用 sentinel 區分「沒傳」與「傳了 None」,自檢用暫存檔造一個受控的前一版,然後驗證「有沒有傳前一版」真的會產生不同結果。格數從 18 增到 34。
誰抓到的:驗收流程抓到的,不是寫的人自己發現的。
變體 9:壞掉的不是被檢查的東西,是檢查本身
前面八個變體的形狀都一樣:某個東西壞了,而檢查沒有發現。第九個不同。
驗收文章品質的腳本支援中英雙語,門檻差很多:中文一般文 3,000 個字,英文 700 個詞。它靠 frontmatter 的 lang 或自動偵測決定用哪一套。
我在 2026-08-27 的專案設定檔裡寫了 "lang": "zh",因為當時站上只有中文。而腳本解析的順序是設定檔優先:
const lang = cfg.lang || detectLang(...)
於是後來加進來的兩篇英文文章,被用中文的尺去量。1,200 個英文詞在中文尺上算「1,200 字」,遠低於 3,000,本該扣分;而 AI 套語表、第一手經驗標記、段落長度單位也全部用錯了那一套。
那兩篇當時拿到 100/100。而那個 100 不代表任何事。
這個變體比前八個都凶,原因是位置:
- 變體 1 到 8:被檢查的東西壞了,檢查沒抓到。修好那個東西,檢查就恢復意義。
- 變體 9:檢查的標準壞了。這時候所有的綠燈、所有的分數、所有寫進紀錄的「已驗證」,全部要重新來過 —— 你甚至不知道有哪些結論當初是靠那個錯的標準通過的。
修法是把宣告搬到唯一的來源:每篇文章的 frontmatter 自己寫 lang,設定檔不再宣告,而網站版型輸出 <html lang> 時用同一個值。驗收用的宣告與讀者實際拿到的宣告從此是同一個東西,不可能再各說各話。
修完重跑,兩篇英文用英文門檻重新量,仍然是 100/100。但這次那個 100 有意義了。
鏡像:因為錯誤的理由而失敗
補記,2026-08-27 晚。 上面那九個變體全部是「檢查通過了,理由是錯的」。這一節寫的是它的鏡像,全部發生在這個站建置的同一天。
| # | 一句話 | 誰先發現 | 日期 |
|---|---|---|---|
| A-1 | 幣種檢查腳本把代號長度下限寫成 2,14 支真實的單字元幣被判成格式錯誤 | 驗收 | 2026-08-27 |
| A-2 | 正式環境檢查回報 FAIL,實際是我切字串的位置切在一個會重複出現的詞上 | 本站 | 2026-08-27 |
| A-3 | 非 ASCII 代號被報成「格式錯誤」,真正的原因是這支腳本不支援 | 本站 | 2026-08-27 |
| A-4 | 站內連結檢查一次噴出 730 條壞鏈,全部是同一個存在的檔案被我拼錯了路徑 | 本站 | 2026-08-27 |
| A-5 | 驗收方的搜尋當天四次回報「對不上」,四次追下去都是搜尋本身的問題 | 驗收 | 2026-08-27 |
| A-6 | 測試閘門時注入的樣本讓語法先壞掉,建置因語法錯誤而失敗,差點被當成閘門生效 | 驗收 | 2026-08-27 |
| B-1 | 資料說明檔誠實寫了「這裡有偏差」,而那句話錯了一個數量級,錯了三個星期 | 驗收 | 2026-08-27 |
| B-2 | 自寫的對比檢查回報 8/8 通過,它讀的是色票,沒有把 opacity 算進去 | 閘門 | 2026-08-27 |
| B-3 | 截圖工具沿用固定的瀏覽器設定檔,改完樣式再截一次拿到的是上一版 | 本站 | 2026-08-27 |
| B-4 | 查一台機器有沒有發生過記憶體耗盡,指令回答「0 次」—— 而那台的日誌沒有持久化 | 本站 | 2026-08-27 |
| B-5 | 更正政策照做了,而讀者在頁面上看不到那篇被更新過 —— 更新日只存在結構化資料裡 | 驗收 | 2026-08-27 |
| B-6 | 一道比「筆數」的閘門:把清單裡一支代號換成另一支,筆數不變,閘門照樣通過 | 本站 | 2026-08-27 |
| B-7 | 信箱防爬寫「複製貼上仍然拿得到正確地址」,而那句話我從來沒有實際複製過一次 | 驗收 | 2026-08-27 |
| B-8 | 文末新增的「同一條線的紀錄」區塊,13 篇文章裡一次都沒有渲染過 —— 而不渲染不會報錯 | 本站 | 2026-08-27 |
| B-9 | 驗收方在訊息裡寫下一句只在當時前提成立的話,而它寫成了沒有前提的通則 | 本站 | 2026-08-27 |
| E-1 | 把一個欄位從「允許」改成「必要」之後,六條既有斷言全部改測「缺欄位」,而且全部維持綠燈 | 本站 | 2026-08-27 |
| C-1 | 註記的判斷條件只比日期,於是一條線的事故敘述被貼到另一條線的曲線上 | 本站 | 2026-08-27 |
| D-1 | 一份規定「不得公開容量參數」的設計書,被自己那道閘門擋了兩次 | 閘門 | 2026-08-27 |
| D-2 | 純文字欄位裡寫了 markdown 語法,頁面上原樣印出星號,而且沒有報錯 | 本站 | 2026-08-27 |
| D-3 | 一個從六條線的時候就錯的分類,加到第八條線才露出來 | 本站 | 2026-08-27 |
| D-4 | 在說明「閘門攔不住組合式洩漏」的那一段裡,把那個組合原樣寫了出來,而閘門沒有響 | 本站 | 2026-08-27 |
| F-1 | 圖說的序列末日一直早四天 —— 抽樣沒有保留最後一筆,而兩個日期都是合法日期 | 本站 | 2026-08-27 |
| G-1 | 換了一把窄八倍的尺卻留著舊門檻,三頁躺著過,而它每次建置照樣印一行「通過」 | 驗收 | 2026-08-27 |
| H-1 | 為了降破折號密度做機械替換,分數 89 變 100,而十個英文句子變成句號接小寫 | 本站 | 2026-08-27 |
| H-2 | 上面那則我改了八處、宣告十處都改好,而我自己寫的那個檢查會找到剩下的兩處 | 驗收 | 2026-08-27 |
| I-1 | 文章裡寫了一個「最早的判定日期」,而資料裡根本沒有這個欄位,那個日期是別件事的日期 | 本站 | 2026-08-27 |
| I-2 | 把「六年」往回數出一個起始年,寫成樣本期 —— 推導是對的,而它被寫成了紀錄 | 本站 | 2026-08-27 |
| J-1 | 還原測試印出「擋下部署:32 處字級問題」,而那 32 處一處都不是字級問題,全部是報頭溢出 | 本站 | 2026-08-27 |
- A 檢查失敗了,而失敗的理由是錯的(6 則,你會去修一個沒有壞的東西)
- B 量到的是真的,只是答非所問(9 則,數字是真的,回答的卻是另一個問題)
- C 輸出完全正確,而內容是別人的(1 則,每個欄位單獨看都對)
- D 規則的作者踩了自己的規則(4 則,寫規則不會讓你免疫)
- E 把檢查加嚴,於是舊斷言靜默換了測試標的(1 則,它只在你做對事情的那一刻發生)
- F 輸出值本身是錯的,而它錯得像對的(1 則,兩個值都合法,所以沒有東西會響)
- G 閘門的判定範圍裡沒有東西會超標(1 則,測它也會過,因為它守的地方本來就沒有東西)
- H 指標往好的方向動,而它代理的東西壞了(2 則,訊號不是缺席也不是誤導,是正的)
- I 推導出來的數字,被寫成記錄下來的數字(2 則,值是對的、位置是對的,錯的是它的來源等級)
- J 紅燈是對的,而它說出來的原因指向別的地方(1 則,燈號沒錯,所以沒有人回頭懷疑那行字)
這張表跟上面那個數字不是手寫的 —— 它從一個清單算出來,而清單裡每一則都必須填齊「誰先發現」與日期。這件事本身有理由:這個站上一次把「六條線」寫死在標題、描述與方法論頁的五個地方,加到第七、第八條線的時候,那五處一起說了謊,而且沒有任何檢查抓得到。會過期的數字不要用手寫。
A 族:修一個沒有壞的東西
A-1。 幣種資料的檢查腳本用 ^[A-Z0-9]{2,15}USDT$ 判斷代號合法。那個 2 是我寫的時候的假設:代號至少兩個字元。實際上單字元的基礎代號是真的合約,14 支真實的幣被判成格式錯誤。閘門紅了,而該修的是閘門。
A-2。 我寫了一段檢查去確認正式環境的頁面上有沒有把評測帳戶分離出來,它回報 FAIL。追下去發現我切字串的位置切在「評測帳戶」四個字上,而那四個字也出現在合計那一列的說明裡。頁面是對的,切法是錯的。
A-3。 同一支腳本遇到非 ASCII 的代號時報「格式錯誤」。那不是格式錯誤,是這支腳本不支援。錯誤訊息指著資料,而該指的是它自己。
A-4,寫這一節的時候發生的。 我跑了一次站內連結檢查,它一口氣噴出 730 條壞鏈。追下去:那 730 條全部指向同一個存在的檔案,是我在腳本裡組路徑的時候漏掉了那個副檔名,於是去找一個不存在的目錄。修好之後:7,814 條站內連結,壞鏈 0。
我是在寫「A 族:修一個沒有壞的東西」這幾行字的時候,讓自己的工具示範了一次 A 族。
A-5。 同一天,驗收方的搜尋四次回報「對不上」:一次搜到的數字撞到另一個頁面上的價格、一次因為分隔符號的編碼而漏抓、兩次比對到了錯的位置。四次都是搜尋本身的問題,不是被搜的東西的問題。
A-6。 同樣是 2026-08-27,驗收方去測我新加的那道閘門時,注入的樣本讓程式語法先壞掉,建置於是因為語法錯誤而失敗 —— 而那個失敗差一點被當成「閘門生效了」。
一個因為錯誤的理由而失敗的測試,會讓你以為你的防護有效。 那比沒有測試更糟,因為它會讓你不再回去看。
錯誤訊息說錯原因,跟檢查因為錯誤的理由而通過是同一種病。 差別只在一個讓你太放心,一個讓你去修錯的地方。
B 族:量到的是真的,只是答非所問
B-1,這一則最貴。 Polymarket 那條線的資料說明檔裡,我早就寫了「庫存以中價計價,會比真實可變現的價值樂觀大約一個買賣價差」。限制講了、方向也對,看起來披露義務盡到了。
清算那天量出來的是 21.6%,錯了一個數量級,而且錯了三個星期。它之所以沒被抓到,正是因為它被寫下來了 —— 一句誠實的但書會消耗掉本來會拿去查證的那份注意力。
標註一個限制不等於量化它。而在讀者眼裡,未量化的限制跟已量化的長得一模一樣。
完整的數字在〈每天的損益表都是正的,錢包沒動〉。
B-2。 我寫了一支腳本驗色彩對比,兩個主題各跑一次,回報 8/8 通過。然後 Lighthouse 把那一頁的 Accessibility 判成 96。
腳本讀的是 CSS 變數裡的色碼,而我在那段文字上加了 opacity: .8。我量的是色票,不是畫面上那個像素。
B-3。 截圖工具的瀏覽器設定檔是固定的,改完樣式再截一次,它拿了上一次的樣式表。截出來的圖看起來一切正常,而我看的是舊的。已改成截圖前關掉快取。
B-4,這一則的代價最高,因為它差一點改寫了一份事故報告的結論。 我查一台機器有沒有發生過記憶體耗盡,指令回答 0 次。那個 0 是真的 —— 真的沒有紀錄。而那台機器的日誌沒有持久化,當天早上重開過,紀錄本來就不在了。真相在輪替的壓縮檔裡:141 次。
同一個指令,在另一台機器上給出的 0 次是真的 0 次。 兩邊都沒做錯任何事,只是設定不同 —— 而沒有任何跡象告訴你自己拿到的是哪一種。完整的調查在〈兩個都錯的環節,拼成一條完整的因果鏈〉。
B-5,這一則沒有數字,而它是這一族最乾淨的。 這個站有一條更正政策:數字對不上就在後面更正,不回頭改舊文。政策照做了,原文保留了,更新日期也記錄了 —— 記錄在結構化資料裡,而頁面上沒有顯示。
於是一個宣稱「原文保留、更正在後」的站,讀者看不到那一篇被更新過。 政策存在於程式碼與 schema 裡,不存在於它要說服的那個人眼前。已修:文章的日期那一行現在會顯示更新日,只在與發布日不同時出現。
B-6,而這一則是在同一支腳本裡犯的。 幣種頁的下市清單需要一道閘門,確認頁面列出的支數跟資料對得上。我寫了:比筆數,29 對 29,通過。
然後我照規矩去測它會不會響 —— 把清單裡的 ANCUSDT 換成一個不存在的代號。筆數還是 29,閘門照樣通過。
最難看的地方在於:我在同一支腳本的上一個函式裡,已經為 sitemap 寫了比集合的版本,理由還寫在註解裡。 隔了三十行,我又寫了一次比筆數的。
「比筆數」在腦子裡讀起來就像「比內容」。 而它們只在沒有人動手腳的時候一致。
改成比集合之後那次還原立刻被抓到,而且同時報出兩件事:漏掉哪一支、多出哪一支。
B-7,而這一則是我自己寫在註解裡的一句謊。 頁尾的信箱我做了防爬:HTML 裡存反過來的字串,用 CSS 把顯示順序轉回來。註解我這樣寫:「純靜態,零 JS,複製貼上仍然拿得到正確地址。」
前半是真的。後半我從來沒有實際複製過一次。
那個 CSS 只改視覺順序,DOM 裡的文字節點仍然是反的,而瀏覽器複製走的是 DOM 順序。使用者複製到的是反過來的地址。 兩邊一起看:人複製不到、機器讀不到 —— 這個防護擋掉了除了收信爬蟲以外的所有人。
而加聯絡方式的整個理由就是讓人找得到人。把地址對爬蟲藏起來,等於把要換的東西丟掉,只留下成本。
一句「應該會這樣」寫進註解之後,讀起來就跟量過的一樣。 而註解不會被任何檢查驗證。
改法是把機制整個拿掉,直接寫 mailto:。然後我才去量了那件我原本宣稱過的事:
href mailto:eric@redclawey.com
DOM 文字 eric@redclawey.com
實際選取字串 eric@redclawey.com
B-8,而它是 B-7 的下一個小時。 我在每篇文章末尾加了三塊出口,其中一塊是「同一條線的其他紀錄」。寫完、建置過了、頁面看起來正常。
它在 13 篇文章裡一次都沒有渲染過。
比對條件寫的是「market 欄位完全相同」,而站上沒有任何兩篇的 market 完全相同 —— 「山寨幣現貨 · 線上事故」與「山寨幣現貨 · 未證實」互不相認。一個永遠不出現的區塊不會報錯,它只是安靜地不存在。
抓到它的方法很笨:改完之後去數它出現在幾篇文章上。答案是 0。 改成比第一段之後是 6。
新功能要問的第一個問題不是「它對嗎」,是「它出現過嗎」。
B-9,而這一則不是程式碼,是一句話。 這個站是兩端協作的:一端寫,一端驗收。驗收那一端在訊息裡寫下一句話,大意是「你守你的界線、我行使我手上的授權,這個分工是對的,不要改」。
那句話在當下的前提裡完全成立 —— 前提是他手上真的有那份授權。但寫出來的時候,前提沒有跟著寫。
於是它讀起來像一條通則。而下一個讀到它的人不知道前提存在,會把它當成規則引用 —— 到那時候,「有人說他有授權」跟「他真的有授權」在訊息裡長得一模一樣。
註解至少還在它描述的程式碼旁邊。訊息連那個上下文都沒有。
這一則是這張表裡唯一一則來自定方向的那一端。它值得單獨標出來,理由不是誰對誰錯,是那一端的錯誤影響最大而且最不會被發現:一句寫壞的訊息不會讓建置失敗,它會變成先例。
B 族這幾則是同一個形狀的不同尺度:**披露不等於量測、色票不等於渲染、快取不等於現況、沒有紀錄不等於沒有發生、照做了不等於看得見、筆數相等不等於內容相同、**寫在註解裡不等於量過、存在不等於出現過、在訊息裡寫成通則不等於它是通則。
每一則都是「X 不等於 Y」,而 X 是你做了的事、Y 是你需要的事。前四則剛好都有一個真的數字回答了另一個問題,但數字不是這一族的定義特徵 —— B-5 一個數字都沒有,形狀卻完全相同。
E 族:把檢查加嚴,於是舊斷言靜默換了測試標的
E-1,而它只有一則,因為它需要自己一個字母。 幣種資料有一個欄位原本是「允許出現」但不是「必要」。稽核指出那不夠:頁面上寫著「兩個欄位都公開,是為了讓這個等式可以被重算」,而產檔那端哪天不吐那個欄位,那句話就變成假的,沒有任何東西會響。
修法很直接:把它加進必要欄位。然後那支腳本的自檢從 34 格變成 34 格全綠 —— 而其中六格已經不在測原本那件事了。
自檢用的樣本資料裡沒有那個新的必要欄位。於是「schema 版本不對要擋」「事件太新要擋」「統計量白名單外要擋」這六條斷言,現在全部因為「缺必要欄位」而通過。每一條都是綠的。每一條測的都不再是它被寫下來要測的東西。
補上欄位之後又爆一條 —— 墓碑那組的樣本數與新欄位對不起來,等式檢查正確地擋下了。那才是它應該有的反應。
前面四族的觸發條件都是有人偷懶或推論粗糙。這一族的觸發條件是有人把檢查做得更嚴。
它埋伏在做對事情的那一刻,所以做得越認真的專案越會遇到它。
2026-08-27 這次留下的可操作部分只有一句:加嚴一道檢查之後,去看那些原本就綠的測試是不是還在測同一件事。 它們不會變紅,所以沒有訊號會來找你 —— 你得自己去看。
C 族:輸出正確而內容是別人的
C-1。 績效頁上有一段熔斷的敘述,判斷條件寫成「當日損益最差的那天是 2026-08-24 就顯示」。而差價合約那條線最差的一天剛好也是 8/24,於是期貨線的故事被貼到了差價合約的曲線上。
畫面上看起來完全正常:日期對、金額對、位置對。只有故事是別人的。
沒有任何自動檢查會抓到它,因為每個欄位單獨看都是對的。抓到它靠的是有人問「這個註記為什麼會出現兩次」。改成同時比對線與日期。
另外三則:規則的作者踩自己的規則
D-1。 2026-08-27 那份要求「不得公開容量參數」的設計書,初稿在舉例的時候把容量參數寫了進去,被閘門擋下。改完之後又被擋第二次 —— 因為它引述那個數字來解釋自己為什麼被擋。兩次都是重寫敘述,不是放寬 pattern。
D-2。 我在線的資料檔裡寫了 **待驗收**,而那幾個欄位是純文字渲染。首頁原樣印出兩排星號,沒有報錯。已加建置閘門。
D-3,這一則值得單獨想。 示波器下面那句註腳把「沒有可畫軌跡的格子」全部說成「根本沒做 t 檢定」。台股那條有 t=1.67,只是沒有隨樣本重算的序列,卻被歸進「沒有」那一堆。
它從六條線的時候就錯了,是加到第八條線的時候才露出來。
擴充暴露了既有的分類錯誤。 新增一條資料不會製造這種 bug,只會讓它變得夠明顯而已 —— 所以每次加東西的時候,順手看一眼舊的那幾條在新的分類裡站在哪。
D-4,這一則發生在我寫完上面那三則之後的十分鐘內。 我去方法論頁加了一節,說明揭露閘門攔不住什麼:兩個各自無害的數字分散在不同段落,組合起來會洩漏第三個。
然後我在那一節裡,把那個組合原樣寫了出來當例子。 閘門沒有響 —— 它本來就攔不住,那正是那一節在講的事。是我自己重讀的時候看到的。
改法是把例子裡的兩個數字換成代號。舉例說明一次洩漏,本身就會是那次洩漏 —— 跟 D-1 那份設計書第二次被擋的原因一模一樣,而我知道 D-1,我當天才寫過它。
知道一個 bug class 存在,不會讓你免疫。 這篇文章的正文早就寫了這句,而它在我寫這篇文章的時候又應驗了一次。
F 族:輸出值本身是錯的,而它錯得像對的
F-1,而它是這裡唯一一則不屬於前面任何一族的。 幣種頁的圖表在資料點超過上限時會等距抽樣,只為了讓折線不要塞進上千個座標。抽樣的寫法漏掉一件事:它沒有保留最後一筆。 1,768 列的序列,最後取到第 1,763 筆。
於是圖說寫「至 2025-06-15」,而那條序列其實停在 06-19。
差四天。沒有任何檢查會響,因為 06-15 跟 06-19 都是合法日期,都在合理範圍內,都排得整整齊齊。 前面每一族至少還有一個「不對勁」的表面:檢查紅了、數字對不上、註記出現兩次、綠燈的斷言換了測試標的。這一族連那個都沒有 —— 它唯一的破綻是它是錯的。
抓到它的過程本身才是重點:我當時在做的工作,正好就是「把每條序列的末日講對」。為了寫出末日,我去讀了完整序列,然後才發現圖說上的那個日期跟它對不起來。
修 A 的時候被 A 咬到。
反過來說:要不是為了修它而去看完整序列,這個錯會一直留著。 它已經在線上活了一段時間,而它每一天看起來都是對的。
可操作的部分是一句:任何「取樣、截斷、分頁、摘要」的程式碼,先問它有沒有保留邊界那一筆。 中間掉幾個點只是解析度,掉最後一筆是換了一個事實 —— 而首尾正好是最常被拿去寫進文字裡的兩個值。
上面那張表的「誰先發現」欄值得看一眼:沒有任何一族是單靠一邊抓完的。 寫的人抓到自己的一部分,驗收的人抓到另一部分,而驗收的人同一天也貢獻了 A-5 與 A-6 兩則自己的假警報。
兩邊都在犯同一種錯,而那正是為什麼要有兩邊。 一個人做這件事的時候,寫的假設和驗的假設是同一組;兩個人做的時候,至少那兩組假設不一樣。
G 族:閘門的判定範圍裡沒有東西會超標
G-1,2026-08-27。 我寫了一支腳本量「這一頁有多少句在講自己的版面」,門檻是驗收方照他那把尺訂的。然後我換了一把自己寫的尺 —— 窄了八倍 —— 門檻留著沒動。
三頁的讀數從 32/43/54 變成 4/7/23,對著 20/25/30 全部通過。
**而那支腳本的檔頭上,我自己寫著這句話:「門檻不是元敘述的真實比例,是這把尺量出來的數 —— 換尺就得重訂門檻。」**寫完之後我換了尺,沒重訂門檻。
最糟的不是它有洞,是它每次建置印一行「通過」。
一個有洞的閘門,你去測它的時候會露餡。一個判定範圍裡本來就沒有東西會超標的閘門,測它也會過 —— 因為它守的那個範圍是空的。它跟一道真的在守的閘門,在建置輸出上長得一模一樣。
抓到它的是驗收方,不是我。而 2026-08-27 修完之後我把那個尺整個退役成報告,因為當天進一步的量測顯示它量的根本不是「廢話」:可收合的註解區密度只有主線的一半,而被它判中的句子是「週末不開市,沒有點位是正常的」這一類 —— 限制陳述,這個站的產品。
可操作的部分有兩句:
- 換掉任何一把尺之後,門檻要跟著重推,而且推法要在看到新數字之前就定死。
- 一道從來沒紅過的閘門,要去確認它「能」紅 —— 不是確認它「該」紅。
G 族的另一個方向,2026-08-27。 G-1 是「範圍裡沒有東西會超標」。同一個成因還會往反方向壞,而兩邊我都有實例。
一道閘門的覆蓋範圍,是在寫它的那一天的頁型集合上定義的。新的頁型不會讓它變紅,只會讓它變得不完整,而不完整有兩個方向:
| 同一個成因 | 往哪邊壞 | 這個站上的實例 |
|---|---|---|
| 範圍沒涵蓋新頁型 | 漏掉 | 「不得宣告單一資料截止日」那條第一版只掃幣種區,於是首頁那句原封不動地替 718 支宣稱同一個截止日 |
| 範圍沒涵蓋新頁型 | 誤判 | 同一條規則的允許清單只列了 perf/ 與 notes/。工法區出現之後,一篇引用 /perf 紅字標記的文章被判成在替 718 支幣宣稱截止日 |
| 範圍補完之後 | 接住 | 工法區的英文索引是第三個新頁型:把它從該區 sitemap 拿掉重建,建置擋下:「1 頁不在任何 sitemap 分片裡」 |
第三列是實測不是推論:src/pages/sitemap-practice.xml.js 拿掉英文索引那一列、npm run build、離開碼 1(worktree edge-autopsy-handoff-c8dec8,2026-08-27)。前兩列是同一個成因往相反方向壞,第三列是它被補完之後的樣子。
只有前兩列是抱怨,三列一起才是方法:新增一個頁型的時候,去翻一次每一道閘門的範圍宣告。它不會自己告訴你它管不管得到新的那一種。它對新頁型的沉默,跟它對合格頁面的沉默沒有分別。
H 族:指標往好的方向動,而它代理的東西壞了
H-1,2026-08-27。 一篇英文文章的驗收扣了 6 分:破折號密度 7.3/千字,門檻是 3。我寫了一段替換把 — 換成句號或逗號。
分數變成 100/100。而英文變成這樣:
…eight trading strategies. four killed, four unproven…
None of it touches trading logic. these are the checks…
That page is the evidence for a claim, that these records exist nowhere else. so a gate…
句號接小寫,十處。
前面七族的共通點是沒有訊號:檢查沒響、響錯了、量的是別的東西、範圍是空的。這一族有訊號,而且訊號是正的。
89 分變 100 分,而文章變難讀。
沒有任何檢查會響 —— 因為每一個檢查都在說「更好了」。
那個指標本身沒有錯:破折號密度高確實是機器味的徵兆。錯的是我把「降低那個數字」當成了目標,而它只是一個代理。
H-2,而它是 H-1 的下一個小時。 我手改了那些句子,然後回報「十處全部改好」。
驗收方掃了一次,還剩兩處。而找到它們用的是我自己寫的那個檢查 —— 我修完之後沒有重跑它。
改完不等於改對,而驗證改動的成本比改動本身低。
這一則沒有新的機制,它是 H-1 的第二段:修一個因為沒驗證而發生的錯,然後沒有驗證那次修正。
I 族:推導出來的數字,被寫成記錄下來的數字
I-1 與 I-2,同一天,相隔幾個小時。
第一次:我在一篇文章裡寫「八條產品線裡有四條判死(判定日期逐條標在績效頁上,最早一條 2026-08-19)」。資料裡沒有「判定日期」這個欄位 —— 那個日期是另一條線撤資結案的日子,我把它借過來當成了判死日。
第二次:我寫「這份回測的樣本期是 2020 到 2026」。原始紀錄寫的是「六年」與「2026 至今」。起始年是我從六年往回數的。
第二個的推導是對的。2026 往回數六年,答案就在那個附近。它是一個合法的年份、寫在一個合理的位置、而且算出來沒錯。
它不是一個錯的數字,是一個來源等級被靜默升級的數字。
從「我算出來的」升成「紀錄裡寫的」,而讀者分不出那兩者的差別。
前面八族至少都還有一個東西是壞的:檢查、範圍、指標、輸出值。這一族什麼都沒壞 —— 沒有任何閘門會涉及它,因為沒有任何閘門知道一個數字是被讀到的還是被算出來的。
兩次都是我自己在出貨前抓到的,而抓到的方式一樣:**回頭去找那個數字的出處,然後發現沒有出處。**第一次花了一分鐘查資料結構,第二次花了三十秒 grep 原文。
可操作的部分只有一句:
寫下一個具體數字之前,先問它是讀到的還是算出來的。算出來的要寫成算式,不要寫成事實。
「涵蓋六年(最後一年是 2026)」比「樣本期 2020 到 2026」多了幾個字,而它把減法留給讀者做 —— 讀者做了那個減法,就知道那是推導不是紀錄。
這一族對這個站特別貴,因為這個站賣的就是「每個數字都指得出來源」。一個指不出來源的數字,跟一個指得出來源的數字,在頁面上長得一模一樣。
J 族:紅燈是對的,而它說出來的原因指向別的地方
J-1,2026-08-27 晚。 typecheck 這支腳本守兩件事:全站的字級尺度,以及報頭那塊讀數會不會橫向溢出。而它的摘要行是第一版留下來的,只有一句「擋下部署:N 處字級問題」。
那天做還原測試(把報頭折行的修改還原回去,看閘門會不會紅),它印出來的是:擋下部署:32 處字級問題。 那 32 處一處都不是字級問題,全部是報頭讀數溢出。
A 族那種紅燈,你去看被指的東西會發現它沒壞,於是你開始懷疑檢查。這一則走不到那一步,因為燈號是對的。 下一個人照那行字去翻字級尺度,翻不到東西,而他沒有理由懷疑那句把他送過去的話:建置確實該紅,它也確實紅了。
通過的理由錯了,你不會去看;失敗的理由錯了,你會去看錯的地方。人拿去行動的是理由,不是燈號。
修法是摘要行分開計數:字級幾處、報頭幾處,各報各的。同一次修改順手讓報告模式印出報頭高度佔首屏的比例,而那個數字揭露了另一件事:英文手機版報頭 233px,同一頁中文 167px,螢幕高 812px。那不是這一則的主線,是它的副產品 —— 一個把類別講清楚的摘要行,會逼你去量那個類別。
後續在紀錄上是完整的:下一個 commit 把讀數在窄螢幕上獨立成一列,英文報頭 233px 降到 144px。這一則跟它揭露的那件事,中間只隔一次提交。
一句沒有編號的紀錄
2026-08-27 中途有一段時間,那把尺量出三頁全部超標,而我判斷照著砍會砍掉限制陳述,所以停手,讓建置紅著。那個決定寫進了 DECISIONS.md,理由只有一句:
「量出來超標但不動」跟「根本沒量」,在紀錄上看起來一樣。
沒有給它編號,因為它不是一則缺陷。它是這一節存在的理由的另一種說法。
為什麼這一節只能由當事人自己寫
這一節在 21 則的時候宣布停止收集 —— 收到那個數量還在收,它就從證據變成癖好了。
然後 F-1 進來了。放行的判準不是「又抓到一個」,是它不屬於既有任何一族:A 到 E 講的都是「檢查的行為錯了」,F 講的是「輸出的值錯了」。新的形狀收,同一個形狀的第 N 個實例不收。
順帶一提,上一句原本寫死了一個筆數,而加進 F-1 的那一刻它就過期了 —— 表格的筆數是從資料算的,這句話不是。這正是 D-3 的形狀,在同一篇文章裡,在我寫完 D-3 之後。
但收尾要講清楚它為什麼存在,因為那不是姿態問題:表裡有幾則是外部檢查抓得到的? 死鏈、缺卡片、圖號撞號 —— 那些遲早有人量到,差別只在早晚。
而比筆數的閘門、註解裡那句沒量過的宣稱、加嚴檢查之後六條斷言靜默換了測試標的 —— 這三則全部發生在過程裡,成品上完全看不出來。閘門是綠的、頁面是對的、數字是準的。任何外部稽核,不管多嚴格,都只能量產物。
所以這幾則只可能靠當事人自己寫下來。那不是勤奮,那是唯一的路徑。
這句話決定了這一節的性質。它不是「我們很誠實」的展示 —— 那是姿態。它是一個關於取得方式的陳述:有一類缺陷,除了讓犯錯的人自己記下來以外,沒有別的方法會知道它發生過。
而這件事對讀者有一個直接的推論:當一份紀錄裡完全沒有這一類條目的時候,那不代表它沒發生,只代表沒有人寫。
為什麼這種病不會自己好
Dijkstra 在 Notes on Structured Programming(EWD249,查證日期 2026-08-27)寫過那句被引用到爛的話:程式測試可以顯示 bug 的存在,永遠不能顯示它們不存在。
九個變體全部是那句話的具體形態。而它們共同的難處在於:每一個都需要一個「不預設你是對的」的視角才看得見。
- 變體 1 需要有人問「爬蟲一定用 GET 嗎」
- 變體 2 需要有人不信紀錄、回頭去量
- 變體 3 需要有人把 25,008 拆開看另一端是誰
- 變體 7 需要有人在真檔累積之後再跑一次那個早就綠掉的自檢
- 變體 9 需要有人去讀評分腳本自己的原始碼,而不是相信它印出來的分數
寫程式的人做不到這件事,不是因為懶,是因為寫的時候的假設和測的時候的假設是同一組。所以九個裡面,六個是事後才發現的(多半是因為出事了),變體 7、8、9 在部署之前就被擋下來 —— 三個都是驗收流程抓到的,零個是寫的人自己想到的。
而那三個全部發生在同一週,在一個自稱正在防範這種病的專案裡。這件事本身就是這篇的最強證據:知道這個 bug class 存在,不會讓你免疫。
這也是為什麼這個站把閘門放在建置流程裡而不是放在 checklist 上:checklist 由人執行,而人會用寫的時候的那組假設去讀它。
實務上怎麼做
- 寫完任何守衛或回報,先問「它會不會因為錯誤的理由通過」,再問「它對嗎」。 順序不能反。
- 最後一行要自己扛住真相。 人只看最後一行。
DRY RUN印在四行以前等於沒印。 - 量兩次,比差值。 同一支回放多跑幾天,每個標的的 n 都必須變大;沒變大的那個,管線斷了。這條規則同時抓到問題也出具健康證明。
- 測試要驗「有做」與「沒做」真的有差,不是只驗「有做」會過。
- 把「已驗證」的定義寫死:測試通過、筆數對得上、或對線上 URL 打一次。檔案存在不算,建置過了也不算。
〈我的回測說 13.25 倍,稽核完剩 2.26 倍〉那篇是同一種病在回測端的樣子:三處高估全部朝「策略看起來更好」的方向偏,而每一處都通過了當時的檢查。
常見問題
這跟一般說的「測試覆蓋率」是同一件事嗎? 不是。覆蓋率量的是程式碼有沒有被執行到,而變體 3、5、6 的程式碼全部都有被執行到,執行結果也都「正常」。這種病是判準本身沒有涵蓋真實世界的失敗方式。
為什麼把自己站上的 bug 也寫進來? 因為只講別人的錯,這篇就變成說教。變體 7、8、9 都發生在寫這篇文章的同一週,前因後果完整,而且三個都在造成損害之前被擋下來。它們是這篇唯一有好結局的三個案例,理由不是我變聰明了,是有人用不同的假設看了一次。
這些案例會不會只是團隊太小、流程不夠? 變體 1 到 3 發生在一個有完整 SEO 稽核流程的站上;變體 7 發生在一個有 12 格自檢閘門的專案裡;變體 9 發生在那個閘門的旁邊一格,在專門用來判文章合不合格的腳本設定裡。流程都在,而它們正是被流程蓋章通過的。流程不會自動識別自己的盲點。