Ticket 越開越大,驗收條件越來越模糊,code review 要重新推理背景——這是我們曾長期面對的問題。根源很簡單:我們沒有定義「一個 Ticket 應該是什麼」。

從一個問題出發

試想這樣一張 Ticket:「實作 ISBNScannerService 的 15 個測試」。

15 個不同的測試目標,掃描啟動、停止、ISBN-10 驗證、ISBN-13 驗證、離線快取、批次模式……每一項失敗都讓整張 Ticket 無法完成。負責人要同時記住 15 個目標才能判斷自己有沒有做完。

這不是一張 Ticket,是一份工作清單。

一張 Ticket,最少應該是什麼?

答案:一個動詞 + 一個目標


什麼是 Atomic Ticket

1Atomic Ticket = 動詞 + 單一目標

核心特徵:

  • 單一職責:只有一個修改原因
  • 獨立驗收:不需要等待其他任務
  • 不可再拆分:硬拆會產生循環依賴,表示這些部分本來就是一體的

四個原子性檢查

語義檢查

「這個 Ticket 能用動詞加單一目標描述嗎?」

符合:「實作 startScan() 方法」、「修復 ISBN 驗證邏輯」。

不符合:描述裡有「和」或「並」——「實作掃描功能和離線支援」通常是兩張 Ticket 擠在一起。

修改原因檢查

「只有一個原因會導致這個 Ticket 需要修改嗎?」

「實作 startScan()」只受掃描 API 規格影響,單一原因。「實作掃描功能和離線支援」則是 API 變更或儲存格式變更都會觸發修改——兩個原因,應拆成兩張。

驗收條件一致性

「所有驗收條件都指向同一個目標嗎?」

如果驗收條件同時涵蓋 startScan()、stopScan() 和離線快取,其實在同時驗收三件事,應該拆開。

依賴獨立性檢查

「如果拆成兩張,它們之間有循環依賴嗎?」

「實作掃描啟動邏輯」和「實作掃描狀態管理」互相依賴,硬拆反而讓兩張都無法獨立完成——這種情況維持為單一 Ticket 才對。

反過來,「實作 startScan()」和「實作 stopScan()」是單向依賴,可以安全拆分。


什麼不是判斷原子性的依據

常見的錯誤標準:時間、程式碼行數、檔案數量、測試數量。

這些都是結果,不是原因。一個單一職責的任務可能只需要 10 行,也可能跨越多個檔案改 200 行。判斷的唯一依據是職責是否單一。


行為分離:開發、測試、調整各自追蹤

1開發 (IMP) → 測試 (TST) → 調整 (ADJ)
2                  |
3               發現問題
4                  |
5           分析 (ANA) → 調整 (ADJ)

開發完成不代表測試通過,測試完成可能衍生調整。把這三種行為混在同一張 Ticket,就失去了「問題在哪個環節發生」的追蹤能力。

完整的追蹤鏈讓我們能回答:「這個修復是從哪一次測試失敗衍生的?那次測試又是為了驗證哪個開發任務?」

Ticket 類型定義

七種類型區分不同性質的任務:

  • IMP(Implementation):開發新功能
  • TST(Testing):執行測試驗證
  • ADJ(Adjustment):調整或修復問題
  • RES(Research):探索未知領域
  • ANA(Analysis):理解現狀和問題
  • INV(Investigation):深入追蹤問題根因
  • DOC(Documentation):記錄和傳承經驗

Ticket 關聯追蹤

每張 Ticket 記錄三個關聯欄位:

  • source_ticket:觸發此 Ticket 的來源
  • spawned_tickets:此 Ticket 衍生的後續 Ticket
  • dispatch_reason:派發原因和交接理由
1開發 Ticket (IMP) 完成
2    |
3    | 衍生
4    v
5測試 Ticket (TST)
6    |
7    | 測試失敗,衍生
8    v
9調整 Ticket (ADJ),dispatch_reason: "UC-01 測試失敗,需修復 ImportService"

Ticket ID 命名規範

1根任務:{Version}-W{Wave}-{Seq}
2子任務:{根ID}.{n}[.{n}...]

Wave 代表執行批次的依賴層級:W1 無依賴可並行,W2 依賴 W1,以此類推。

0.15.16-W1-001 是 v0.15.16 的 Wave 1 第一個任務。0.31.0-W3-002.1 是 0.31.0-W3-002 的第一個子任務。單看 ID 就能判斷它在版本工作流中的位置。


5W1H 驅動的欄位設計

每張 Ticket 的 YAML 欄位對應 5W1H:

  • Who:負責人與各 Phase 的歷史負責人
  • What:任務目標(動詞加單一目標)
  • When:觸發時機
  • Where:架構層級與影響的檔案清單
  • Why:需求依據
  • How:任務類型與實作策略

欄位設計讓 Ticket 本身就能完整描述任務全貌,交接時不需要額外解釋背景。


拆分範例

把功能清單拆成原子任務

原始需求:「實作 ISBNScannerService 的 15 個測試」

拆分後:

10.15.16-W1-001: 實作 startScan() 方法
20.15.16-W1-002: 實作 stopScan() 方法
30.15.16-W1-003: 實作 validateIsbn10() 驗證邏輯
40.15.16-W1-004: 實作 validateIsbn13() 驗證邏輯
50.15.16-W2-005: 實作離線掃描支援(依賴 W1)
60.15.16-W2-006: 實作批次掃描模式(依賴 W1)

W1 任務可並行,W2 等 W1 完成後進行。

知道什麼時候不該拆

需求:「實作 BookRepository.save() 方法」

有人可能拆成:Ticket A 實作簽名、Ticket B 實作邏輯、Ticket C 實作驗證。

這是錯的。save() 的簽名、邏輯、驗證三者循環依賴,硬拆後每張都無法獨立完成。正確做法是維持為單一 Ticket。


建立 Ticket 前的四個問題

  1. 能用「動詞 + 單一目標」描述嗎?
  2. 只有一個修改原因嗎?
  3. 所有驗收條件都指向同一個目標嗎?
  4. 如果拆開,不會產生循環依賴嗎?

常見的違反模式:「實作 X 和 Y」(兩個目標)、「修復所有 X 測試」(多個目標)、「重構 X 並優化 Y」(兩個行動)、「建立 X 的完整功能」(目標模糊)。