Fixing the Same Bug Twice Is a Process Failure

Every team has that one defect that keeps coming back. An off-by-one in pagination. A missing null check in the auth middleware. A race condition in checkout that someone “fixed” three sprints ago.

You did not write the same bug three times. You wrote three different bugs with the same cause. The fix addressed the symptom. The cause stayed hidden.

Fagan inspections were built to find defects before they escape. Most teams stop at the rework phase. The author fixes the logged issues, the moderator verifies the fixes, and everyone moves on. That is a mistake. The follow-up phase is where causal analysis belongs. Skip it, and you are scheduling the next inspection for the same defect types.

What Causal Analysis Actually Means

Causal analysis is not root cause analysis. Root cause analysis asks “what line of code failed and why.” Causal analysis asks “what about our process allowed this defect category to exist.”

A root cause of a null pointer exception is “we forgot to check for null.” The causal analysis finding is “our review checklist does not include null safety, and our linting rules allow unchecked dereferences.” One fixes the bug. The other fixes the factory.

In a Fagan inspection, causal analysis happens after rework. The moderator groups defects by category and leads a short session to identify process-level causes. The output is not code changes. It is process changes: updated checklists, new lint rules, training gaps, or modified entry criteria.

The Mechanics: How to Run It

Start with the inspection log. Every defect should already have four fields: location, severity, type, and description. Add a fifth during causal analysis: process cause.

The moderator groups defects by type. If three of twelve defects are boundary condition errors, that is a pattern. If two are API misuse errors from the same module, that is a pattern too. Patterns are the signal. Individual defects are noise.

For each pattern, ask three questions:

  1. Could we have prevented this category before the inspection?
  2. Why did our existing prevention mechanisms miss it?
  3. What is the cheapest change that would prevent this category next time?

The third question is where most teams go wrong. They suggest rewriting the architecture. That is wishful thinking. The goal is the smallest process change that eliminates the category.

A boundary condition pattern might mean adding “off-by-one and boundary checks” to the review checklist. An API misuse pattern might mean adding a static analysis rule. A missing error handling pattern might mean updating the definition of done to require error path tests.

A Working Causal Analysis Tracker

Here is a Python script that takes an inspection log, groups defects by type, and prompts for process-level causes.

#!/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()

Save an inspection log as 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"}
]

Run python causal_analysis.py inspection_log.json and the script walks you through identifying process causes. The report it produces becomes the input for your next retrospective or process improvement cycle.

Why Most Teams Skip This

Causal analysis adds time to an already expensive process. A standard Fagan inspection for 250 lines costs 8 to 12 person-hours. Causal analysis adds another 30 to 60 minutes.

That extra time feels wasteful when you have a backlog. But if causal analysis prevents even one recurrence of a defect category, it pays for itself the next time that category does not show up.

The harder problem is honesty. Causal analysis often reveals that the defect existed because the team skipped a step. The code was not tested before inspection. The reviewer did not use the checklist. The checklist itself is incomplete.

These findings can be uncomfortable. A team that treats causal analysis as blame assignment will stop getting honest answers. The moderator must frame it as process improvement, not accountability theater.

The Real Trade-Off: Speed vs. Learning

You have two options after a Fagan inspection. Close the loop by verifying fixes and moving on. Or close the loop by verifying fixes and learning why the defects existed.

The first option is faster today. The second option is faster over the next six months.

The teams that get the most value from Fagan inspections treat the inspection log as a dataset. Defect patterns are feedback. Ignoring them is like running a test suite and never looking at the failures.

Not every defect category deserves causal analysis. A one-off typo does not need a process change. But if a category shows up in two consecutive inspections, you have a process problem masquerading as a code problem.

Start with the Highest-Impact Module

You do not need to run causal analysis on every inspection. Pick the module that produces the most production incidents or the most escaped defects. Run a full Fagan inspection, collect the log, and spend thirty minutes on causal analysis.

The first time you do this, the proposed fixes will be obvious. Update the checklist. Add a lint rule. Write a short team note about the pattern. By the third inspection, you should see fewer defects in the categories you already analyzed.

If the counts are not dropping, your proposed fixes are too vague. “Be more careful” is not a process change. “Run the null safety linter in CI” is.

Track defect categories per inspection over time. If causal analysis is doing its job, the recurring categories disappear. If they do not, you are either misidentifying causes or failing to implement the fixes.

FAQ

What is causal analysis in a Fagan inspection?

Causal analysis is a post-inspection step where the team examines the defects found, groups them by category, and identifies process-level causes. The goal is to prevent recurrence by changing checklists, tools, or practices, not just fixing the individual defects.

How is causal analysis different from root cause analysis?

Root cause analysis identifies the specific reason a single defect occurred, such as a missing null check. Causal analysis identifies why the process allowed that entire category of defect to be written, such as a missing checklist item or an unenforced lint rule.

When should I run causal analysis?

Run it after the rework and follow-up phases of a Fagan inspection, while the defects are still fresh. Focus on patterns that appear in multiple defects or have shown up in previous inspections. One-off defects rarely justify process changes.

How do I know if causal analysis is working?

Track defect categories across inspections over time. If your causal analysis is effective, the frequency of recurring categories should drop. If the same categories keep appearing, your proposed fixes are either incorrect or not being implemented.

Run It on Your Next Inspection Log

Find your last inspection log. Group defects by type. For the top category, ask what the cheapest process change would be to prevent it next time.

Write that change down. Assign an owner. Verify it in the next inspection. That is causal analysis. Everything else is just fixing bugs.