兩次修復同樣的缺陷是流程失敗

每個團隊都有一個反覆出現的缺陷。分頁中的差一錯誤。認證middleware中缺失的空值檢查。三個衝刺前有人「修復過」的結帳race condition。

你並不是三次寫了同一個缺陷。你寫了三個具有相同原因的不同缺陷。修復處理的是症狀。原因依然隱藏。

費根審查的設計初衷是在缺陷逃逸之前發現它們。大多數團隊止步於返工階段。作者修復記錄的問題,主持人驗證修復,然後所有人繼續前進。這是錯誤的。跟進階段才是因果分析該在的地方。跳過它,你就等於在為同樣的缺陷類型安排下一次審查。

因果分析的實際含義

因果分析不是根本原因分析。根本原因分析問的是「哪一行程式碼失敗了以及為什麼」。因果分析問的是「我們的流程中有什麼讓這類缺陷得以存在」。

空指標例外的根本原因是「我們忘了檢查空值」。因果分析的發現是「我們的審查清單沒有包含空值安全性,而且我們的lint規則允許未檢查的解參照」。一個修復了缺陷。另一個修復了工廠。

在費根審查中,因果分析發生在返工之後。主持人按類別對缺陷進行分組,並主持一個簡短的會議來識別流程層面的原因。輸出不是程式碼變更。而是流程變更:更新的檢查清單、新的lint規則、培訓缺口或修改後的准入條件。

操作機制:如何執行

從審查日誌開始。每個缺陷應該已經有四個欄位:位置、嚴重度、類型和描述。在因果分析期間添加第五個:流程原因。

主持人按類型對缺陷分組。如果十二個缺陷中有三個是邊界條件錯誤,那就是一個模式。如果兩個是來自同一模組的API誤用錯誤,那也是一個模式。模式是訊號。個別缺陷是雜訊。

對每個模式問三個問題:

  1. 審查之前我們本可以預防這個類別嗎?
  2. 為什麼現有的預防機制漏掉了它?
  3. 下次預防這個類別的最廉價變更是什麼?

第三個問題是大多數團隊出錯的地方。他們建議重寫架構。這是不切實際的幻想。目標是最小的流程變更,消除整個類別。

邊界條件模式可能意味著在審查檢查清單中添加「差一和邊界檢查」。API誤用模式可能意味著添加一條靜態分析規則。缺失錯誤處理的模式可能意味著更新完成的定義,要求包含錯誤路徑測試。

可用的因果分析追蹤器

以下是一個Python指令碼,它接收審查日誌,按類型對缺陷分組,並提示輸入流程層面的原因。

#!/usr/bin/env python3
"""
Run causal analysis on a Fagan inspection log.
Reads a JSON log, groups defects by type, and emits a causal analysis report.
"""

import json
import argparse
from collections import defaultdict
from dataclasses import dataclass, field
from typing import List, Dict


@dataclass
class Defect:
    location: str
    severity: str
    defect_type: str
    description: str


@dataclass
class CausalPattern:
    defect_type: str
    count: int
    locations: List[str] = field(default_factory=list)
    process_cause: str = ""
    proposed_fix: str = ""


def load_log(path: str) -> List[Defect]:
    with open(path, "r") as f:
        raw = json.load(f)
    return [Defect(**item) for item in raw]


def analyze_patterns(defects: List[Defect]) -> List[CausalPattern]:
    groups: Dict[str, List[Defect]] = defaultdict(list)
    for d in defects:
        groups[d.defect_type].append(d)

    patterns = []
    for dtype, items in groups.items():
        patterns.append(CausalPattern(
            defect_type=dtype,
            count=len(items),
            locations=[d.location for d in items],
        ))

    return sorted(patterns, key=lambda p: p.count, reverse=True)


def prompt_causal_input(patterns: List[CausalPattern]) -> List[CausalPattern]:
    print("=== CAUSAL ANALYSIS SESSION ===")
    print("For each pattern, identify the process cause and the cheapest fix.\n")

    for p in patterns:
        print(f"Pattern: {p.defect_type} ({p.count} occurrence(s))")
        print(f"Locations: {', '.join(p.locations)}")
        p.process_cause = input("Process cause: ").strip()
        p.proposed_fix = input("Cheapest prevention fix: ").strip()
        print()

    return patterns


def emit_report(patterns: List[CausalPattern], output_path: str):
    report = {
        "summary": {
            "total_patterns": len(patterns),
            "total_defects": sum(p.count for p in patterns),
        },
        "patterns": [
            {
                "type": p.defect_type,
                "count": p.count,
                "locations": p.locations,
                "process_cause": p.process_cause,
                "proposed_fix": p.proposed_fix,
            }
            for p in patterns
        ],
    }

    with open(output_path, "w") as f:
        json.dump(report, f, indent=2)

    print(f"Report written to {output_path}")


def main():
    parser = argparse.ArgumentParser(description="Causal analysis for Fagan inspections")
    parser.add_argument("log", help="Path to inspection log JSON")
    parser.add_argument("--output", default="causal_report.json", help="Output report path")
    args = parser.parse_args()

    defects = load_log(args.log)
    patterns = analyze_patterns(defects)

    if not patterns:
        print("No defects found. Nothing to analyze.")
        return

    patterns = prompt_causal_input(patterns)
    emit_report(patterns, args.output)


if __name__ == "__main__":
    main()

將審查日誌儲存為 inspection_log.json

[
  {"location": "src/auth.py:42", "severity": "major", "defect_type": "null-safety", "description": "Missing null check on user object"},
  {"location": "src/orders.py:88", "severity": "minor", "defect_type": "boundary", "description": "Off-by-one in pagination limit"},
  {"location": "src/auth.py:67", "severity": "major", "defect_type": "null-safety", "description": "Unchecked token decode result"}
]

執行 python causal_analysis.py inspection_log.json,指令碼會引導你識別流程原因。它產生的報告將成為你下次回顧或流程改善週期的輸入。

為什麼大多數團隊跳過這一步

因果分析為已經昂貴的流程增加了時間。250行的標準費根審查需要8到12人時。因果分析再增加30到60分鐘。

當你有積壓工作時,這額外的時間感覺像是浪費。但如果因果分析哪怕只阻止了一種類別的缺陷復發,在下次該類別不再出現時它就回本了。

更難的問題是誠實。因果分析常常揭示缺陷之所以存在,是因為團隊跳過了一個步驟。程式碼在審查前沒有經過測試。審查者沒有使用檢查清單。檢查清單本身就不完整。

這些發現可能令人不適。把因果分析當作追責的團隊將再也得不到誠實的答案。主持人必須將其框定為流程改善,而不是問責表演。

真正的權衡:速度對學習

費根審查後你有兩個選擇。透過驗證修復然後繼續前進來閉環。或者透過驗證修復並了解缺陷為什麼存在來閉環。

第一個選擇今天更快。第二個選擇在未來六個月內更快。

從費根審查中獲得最大價值的團隊把審查日誌當作資料集。缺陷模式是回饋。忽視它們就像執行測試套件卻從不看失敗結果。

並非每個缺陷類別都值得做因果分析。一次性的錯別字不需要流程變更。但如果某個類別在連續兩次審查中出現,你面對的就是一個偽裝成程式碼問題的流程問題。

從影響最大的模組開始

你不需要對每次審查都做因果分析。選擇產生最多生產事故或最多逃逸缺陷的模組。執行完整的費根審查,收集日誌,花三十分鐘做因果分析。

第一次做的時候,提出的修復方案會顯而易見。更新檢查清單。添加一條lint規則。寫一段關於這個模式的簡短團隊備忘。到第三次審查時,你應該能在已經分析過的類別中看到更少的缺陷。

如果數量沒有下降,說明你提出的修復方案太模糊了。「更小心一點」不是流程變更。「在CI中執行空值安全性linter」才是。

隨著時間推移追蹤每次審查的缺陷類別。如果因果分析起到了作用,反覆出現的類別就會消失。如果它們沒有消失,你要麼錯誤地識別了原因,要麼沒能實施修復。

FAQ

費根審查中的因果分析是什麼?

因果分析是一個審查後步驟,團隊在其中檢查發現的缺陷,按類別分組,並識別流程層面的原因。目標是透過改變檢查清單、工具或實踐來防止復發,而不僅僅是修復單個缺陷。

因果分析與根本原因分析有何不同?

根本原因分析確定單個缺陷發生的具體原因,例如缺失的空值檢查。因果分析確定為什麼流程允許寫下整個缺陷類別,例如缺失的檢查清單項或未執行的lint規則。

什麼時候應該執行因果分析?

在費根審查的返工和跟進階段之後執行,趁缺陷還新鮮的時候。關注出現在多個缺陷中或已在之前的審查中出現過的模式。一次性缺陷很少能證明流程變更的合理性。

如何判斷因果分析是否有效?

隨著時間推移追蹤跨審查的缺陷類別。如果你的因果分析有效,反覆出現的類別頻率應該下降。如果同樣的類別持續出現,你提出的修復方案要麼不正確,要麼沒有被實施。

在下一個審查日誌上執行它

找到你最近的審查日誌。按類型對缺陷分組。對於排名最高的類別,問問下次預防它的最廉價流程變更是什麼。

把那個變更寫下來。指定負責人。在下次審查中驗證它。這就是因果分析。其他一切都只是修缺陷。