Blog
浪費半小時修了一個不存在的 bug
那個錯誤畫面是真的
改了三次程式碼、重新截了四次圖,每一次都看到同樣的錯誤。
那個錯誤畫面是真的。錯的是從那個畫面推出來的結論。
起因是要檢查一個網站在手機上會不會破版。用瀏覽器的無頭模式截了一張圖, 指定寬度 390 像素——大約一支手機的寬度:
chrome --headless --window-size=390,14000 --screenshot=shot.png
拿到一張 390 像素寬的圖。圖裡的文字在右邊被切斷,最後幾個字消失在邊界外。
結論很明顯:這個網站在手機上會破版。開始修。
調整排版設定,重新截圖,還是被切斷。又改了一個地方,再截,還是被切斷。
半小時後,換另一種方式去量這個網站,得到三個數字:
- 內容實際寬度:342
- 容器右邊界:366
- 可視範圍:390
每一個都在範圍內。網站從頭到尾都是好的。
被切斷的是那張圖。
瀏覽器做了什麼
瀏覽器有一個最小視窗寬度。我要求 390,它內部用了一個更寬的版面去排版。
然後照著要求,輸出一張 390 像素寬的圖片。多出來的部分直接裁掉。
瀏覽器內部實際排版的寬度
├──────────────────────────────────────┤
你拿到的圖片
├──────────────────────┤ 390px
└──────────────┘
這一段被裁掉了,
而圖上沒有任何痕跡
要檢查的「右邊界有沒有超出去」,答案就在被裁掉的那一段裡。
這張圖有三個特徵:
- 寬度精確是 390,要求什麼就給什麼
- 沒有任何訊息說「版面其實比較寬,裁掉了一部分」
- 被裁掉的那一塊,正好是要檢查的東西
第一點最麻煩。尺寸完全正確,於是沒有任何理由懷疑它。
給一張 500 像素寬的圖,馬上會發現不對。印一行「已裁切」,三秒鐘就懂了。 它精確滿足了要求,於是我把「要求被滿足」當成了「畫面是完整的」。
這兩件事沒有關係,在螢幕上卻長得一樣。
換個場景說一次
假設你請一位助理幫你整理收件匣,他有一條規則:「系統自動發的通知沒有價值,直接刪掉。」
這條規則平常幫你省很多時間。廣告、電子報、自動回覆,刪得乾乾淨淨。
直到某天你要找「密碼重設信」。那封信正好是系統自動發的。
打開收件匣,信件整整齊齊,沒有一行寫著「已刪除 3 封」。於是你得到一個結論: 系統根本沒寄給我。 然後去重寄了一次。
助理沒有做錯事。他照規則辦事,而規則在絕大多數時候是對的。
問題出在你們對「有價值」的定義,在那一刻剛好不同。
兩天前,撞過一模一樣的事
那次是 git。
剛把一個 PR merge 進 main,想確認它真的合併進去了。跑 git log -5,畫面吐出五行:
bf27894 7baa375 c7ec6a6 63704e2 60be3f1
裡面沒有剛做的那次 merge。第一個念頭是「merge 沒成功」。
還好多跑了一個指令:
git rev-parse HEAD
→ 169ec6c
169ec6c 就是那顆 merge commit。它一直都在,merge 從頭到尾都是成功的。
同一台電腦、同一秒鐘、同一個 repo,兩個指令對「最新的 commit 是哪一顆」給了不同答案。
為什麼 git log 少了一行
那台機器裝了一個叫 rtk 的工具,用途是省 token:輸入 git log,它攔下來,
換成自己的版本去跑,把輸出裁短再交還。
它的裁法之一是把 merge commit 濾掉。
這條規則多數時候是對的。merge commit 通常長這樣:
Merge pull request #3 from some-branch
沒有程式碼、沒有內容。回顧「這週改了什麼」的時候,它就是雜訊。
但當下要問的是「那次合併有沒有發生」。在這個問題底下,merge commit 是唯一有答案的那一顆。
工具沒有壞掉。明講 git log --merges,它會乖乖照做。它只是有一個我不知道的預設值,
而那個預設值剛好對準我要找的東西。
和截圖那次完全相同的地方
我說「給我 5 筆」。它做的順序是:
- 先把 merge commit 丟掉
- 從剩下的裡面數 5 筆
於是拿到完整的 5 筆。沒有缺口、沒有空行、行數也對。
跟那張 390 像素寬的圖一樣:要求的規格被精確滿足了,而「規格被滿足」 沒有告訴你任何關於完整性的事。
這兩次可以合成一句話
當一個工具替你做了取捨,它通常會先取捨,再來滿足你的規格。
兩條路徑的終點都寫著「5 筆」。你檢查的是數量,而數量在兩種情況下都正確。
那個「剛剛好」沒有任何資訊量。它只證明工具聽懂了你的話。
這種錯誤還有一個討厭的性質:它會壓抑你的懷疑。 明顯缺漏的清單會讓你去查, 看起來完整的清單會讓你直接下結論。
可以帶走的兩條做法
一、判斷「有沒有發生」時,問會回傳識別碼的指令
清單經過整理才交到你手上,整理的過程可能丟掉東西。識別碼指向一個明確的目標, 沒有整理的空間。
| 你想知道 | 容易被騙的問法 | 比較可靠的問法 |
|---|---|---|
| merge / push 成功了嗎 | git log | git rev-parse HEAD |
| 遠端跟本機同步了嗎 | 看 log 前幾行 | git ls-remote origin <branch> 比對 SHA |
| 這個版面在手機上正常嗎 | 指定寬度截圖 | 直接量元素的 scrollWidth 與 clientWidth |
| 這個字串有沒有出現在檔案裡 | 看搜尋結果幾筆 | 先確認搜尋工具的預設排除清單 |
二、問兩次,用不同的工具
那天真正救了我的,是兩個工具給出矛盾的答案:
git rev-parse HEAD → 169ec6c
git log -1 → bf27894
兩個都宣稱自己指的是最新的 commit,其中一個在說謊。只用其中一個的話, 這個錯誤永遠不會浮上來。
截圖那次也一樣。盯著同一張圖看半小時都不會發現問題,因為問題在那張圖本身。 換一種量法,三個數字就把答案給了。
這一課該放在哪一層
git log 那個坑,兩天前早就寫成筆記了。寫完兩天後,還是在截圖上踩了同一個形狀的坑。
筆記躺在硬碟裡,一次也沒有在需要的那一刻出現。
一條知識可以放三個地方,差別在誰負責執行它:
| 層 | 誰執行 | 什麼時候失效 |
|---|---|---|
| 筆記 | 未來的你 | 得先想到「我好像寫過這個」才會去查。而想不到的時候,正是最需要它的時候 |
常駐規則(CLAUDE.md 之類) | AI 助手,每個對話都讀 | 長對話會稀釋它,當下的任務會壓過它 |
| Hook | 工具本身,指令送出前 | 只有寫錯才失效 |
由上往下,可靠度遞增,寫起來也遞增麻煩。所以不必每一課都做到底。
判準只有一句:同一個坑踩第二次,就往下搬一層。
罕見的那一條,放常駐規則就好
截圖那個坑只踩過一次,貼一段文字進 CLAUDE.md 就夠了:
## 判斷「某件事有沒有發生」時的取證規則
要確認 merge、push、部署、同步這類「發生了沒」的問題,
用會回傳識別碼的指令,避開會回傳清單或畫面的指令。
- merge / push → `git rev-parse HEAD`、`git ls-remote origin <branch>` 比對 SHA
- 版面有沒有溢出 → 量元素的 `scrollWidth` 與 `clientWidth`,避免用截圖判讀
- 檔案裡有沒有某字串 → 先確認搜尋工具的預設排除清單
理由:任何「幫你省事」的中間層都可能先做取捨、再滿足你指定的規格。
於是輸出會精確符合你的要求,而「符合要求」不包含任何關於完整性的資訊。
只有清單或畫面時,換第二個獨立工具再問一次。
兩個答案不一致時,相信會回傳識別碼的那一個。
踩第二次的那一條,降到 hook
git log 這條踩過兩次,所以它不該再靠任何人記得。我寫了一支 hook:
偵測到 git log 要執行,就先印一行提醒。不擋,而且一個對話只講一次——
會擋住正常工作、或是一直跳出來的提醒,最後一定會被關掉。
寫完之後跑了單元測試,九種情況全過。接著在真實環境下了一次 git log,
它跳出來了;再下一次,它又跳了一次——「只提醒一次」失效。
查下去發現 hook 是對的,錯的是測試指令。我寫成 rm -f marker && git log,
而 hook 在指令執行之前就跑完了。真正的順序是:hook 建立 marker →
我的 rm 把它刪掉 → git log 執行。
我的量測動作把要量測的狀態清掉了。
這篇文章講的東西,在驗證這篇文章的結論時又發生了一次。
最後
現在多了一個習慣。要判斷一件事「有沒有發生」的時候,會先問自己一句:
手上這個答案,是原始的,還是有人幫我整理過的?
整理過的話,就再問一次——用一個不會整理的工具。