Day 20 的分數只管單筆。可是跑多輪之後,同一個症狀會被重複撿到,判錯的那幾筆也會一再回頭。今天處理兩件事:確認過的誤判記進名單別再報,同一個指紋認得出來就不開第二張單。
Day 20 的分數解決「哪一筆先看」,但它一次只看一筆。跑到第四輪你會發現另一個問題:同一個 bug 被撿到四次,四筆各自都拿到 high,分數看不出它們其實是同一件事。
本專案的索引現在是 18 張單,背後是 34 次觀察。不去重的話,issue tracker 收到的是 34 張,其中有 4 張都在講數量欄。開發者打開 tracker 第一件事不是修 bug,是先花時間判斷哪幾張是同一張。
誤報是另一個方向的浪費。有些症狀已經有人拍板過不是缺陷,例如未登入頁面打 /users/me 回 401。沒有名單,它每一輪都會被重新撿起來、重新算分、重新送到你面前,你每一輪都要重新判同一件事。
兩件事的目標是同一個:讓 tracker 裡的每一張單都值得打開。誤報率一旦上去,人就不再信任這個來源,後面所有的自動化都失去意義。
拿累積下來的真檔驗一次。先算指紋:把 cart id、時間戳、第幾次全部正規化掉,一筆「購物車列小計顯示 $00.00、頁尾總計正確」的觀察,算出 cart|line-item-total-shows-zero|view-cart。拿去查表:
grep -A3 'cart|line-item-total-shows-zero|view-cart' output/issues-index.yaml
- fingerprint: "cart|line-item-total-shows-zero|view-cart"
issue: https://github.com/vansleee/sdet-skills/issues/2
occurrences: 2
完全相同,併進舊單、occurrences 加一,不開新單。再盤點整份索引:
python3 -c "
import re
occ = [int(x) for x in re.findall(r'occurrences: (\d+)', open('output/issues-index.yaml').read())]
print('單數', len(occ), '觀察總數', sum(occ))
"
單數 18 觀察總數 34
34 次觀察壓成 18 張單,16 次重複沒有變成第二張單。
指紋的工作只有一句話:讓同一個 bug 的不同次觀察算出同一個字串,不同 bug 算出不同字串。格式固定三段:
<area>|<signature>|<trigger>
area 是行為區域,用路由或功能名,去掉 query string 與 id;signature 是錯誤簽章或一句穩定的現象描述,不含 stack trace 與行號;trigger 是觸發條件的「類別」,寫成動詞加名詞,描述做了什麼類型的事而不是具體值。全部小寫、以 - 連字。

正規化把「看起來不一樣」的合起來,簽章把「看起來很像」的分開。兩件事用同一組欄位。
算之前要先正規化:UUID 與 hash 換成 <id>、具體數字換成 <n>、時間戳與 session id 直接移除、大小寫統一。但分類性的值要保留,qty-0 與 qty-negative 是兩個不同的 bug,不能一起換成 qty-<n>。
有一份東西明令不准放進指紋:出現次數、時間戳、cart id、截圖路徑、瀏覽器版本、測試帳號、環境名稱。這些屬於觀察紀錄,掛在 issue 的證據上。放進去的後果是同一個 bug 每次算出來的指紋都不一樣,永遠去不了重。
算完拿去比對,四條規則分流:
| 比對結果 | 處置 |
|---|---|
| 完全相同 | 不開新單,舊單 occurrences 加一、新證據 append、必要時更新 confidence |
area 與 signature 相同、trigger 不同 |
標 related,不自動合併,列給人判 |
| 找不到 | 才開新單,並把指紋寫進索引 |
| 命中誤報名單 | 移進 suppressed,記下命中哪一條 |
鬆緊拿捏只有一個判準:只放「換一次觀察也不會變的本質」。 太鬆會把不同 bug 併成一張,開發者看不懂;太緊會讓去重完全失效。
去重與濾誤報是跨輪累積的能力,所以兩份檔案都放在 output/ 根目錄,不進單輪的 session 資料夾。切進單輪就失去去重與校準的能力。
output/issues-index.yaml 一筆一張單,欄位是指紋、issue 連結、本地報告路徑、occurrences、confidence、target、證據清單,疑似同根因的另外掛 related。它同時是「這個 bug 被撿到幾次」的計數器,Day 20 的複現次數配分就是讀它。
output/known-false-positives.yaml 一筆一條 pattern,欄位是 pattern、scope、reason、decided_by、decided_at。後兩個欄位不是形式,進這份清單等於有人拍板過。所以有一條硬規則,needs-spec 不准寫進來。沒有規格可比對只代表還沒人判,不代表它不是 bug,寫進去就是把一個未決的問題永久消音。
scope 那欄也不是裝飾。本專案第一條 FP 的 scope 是 anonymous-only,後來有一個登入後才發生、且以 API 回 200 為判準的候選,閘門明確記下「不在該條範圍」,沒有被誤壓。
一筆新觀察進來,先正規化掉會變動的部分,算出三段式指紋;拿指紋先過誤報名單,命中就壓下並記下命中哪一條;沒命中再查索引,完全相同就併進舊單、area 與 signature 相同但 trigger 不同就標 related 交人判、找不到才開新單並寫回索引。
一筆新觀察
│
▼
正規化(id / 時間戳 / 次數 移除)
│
▼
算指紋 area|signature|trigger
│
▼
查 known-false-positives.yaml
│ │
命中 未命中
▼ ▼
suppressed 查 issues-index.yaml
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
完全相同 area+signature 同 找不到
trigger 不同
▼ ▼ ▼
occurrences++ 標 related 開新單
證據 append 交人判 寫回索引
去重是把重複變成信心,不是把單子變多,同一個 bug 出現四次應該讓它的分數更高,而不是讓 tracker 多三張。誤報名單的門檻是有人拍板,decided_by 與 decided_at 缺一不可,needs-spec 一律不准進。而規則擋得掉機械性重複,擋不掉同根因,所以第二條規則寫的是列給人判,不是自動合併。
判斷力到今天湊齊了:Day 18 的 oracle 決定是不是 bug、Day 20 的分數決定值不值得往下花力氣、今天的指紋與誤報名單決定該不該再報一次。但這三塊還是得你一條一條下指令去觸發。明天把它們包成第一位代理人:給一張 charter,它自己探索、分類、判定、打分、去重、濾誤報,交回一份排好序的候選清單。只找,不開單。
<area>|<signature>|<trigger> 是同一個思路references/bug-fingerprint.md(指紋格式、正規化與比對四規則)、state-templates/known-false-positives.example.yaml 與 issues-index.example.yaml - 兩份狀態檔的欄位定義