Systematic Debugging
No code fix without an evidenced root cause for the specific behavior being changed.
The Iron Law
NO FIX WITHOUT A REPRODUCED FAILURE AND A FALSIFIABLE ROOT CAUSE
If you cannot state what is wrong, predict what evidence would prove you wrong, and reproduce the failure on demand, you are guessing -- and a guess that happens to make the symptom disappear is still a guess.
Context
Systematic debugging turns a bug report, failing test, or bad runtime behavior into a defensible fix.
See context and anti-pattern notes.
Inputs
I/O contract notes define required inputs and authority.
Process
Step 1: Read the Evidence You Already Have
Before forming any theory:
- Read the entire error message and the full stack trace, not the first line. Error text frequently names the exact cause, file, and line -- treating it as noise is the single most common debugging failure.
- Confirm you are observing the code you think you are: right branch, right environment, rebuilt artifacts, cleared caches, correct deploy target. If a print statement or deliberate syntax error at the failure site does not show up, stop -- you are debugging the wrong code.
- Check what changed recently:
git log/git diffaround the failure window, dependency bumps, config and environment changes.
Step 2: Pin the Failure Boundary
Write down concretely: what fails, where it fails, expected vs actual behavior, and how to trigger it. Classify whether the failure is local, integration-boundary, or release-boundary sensitive. If the symptom plausibly has lineage, invoke pc-bug-history-retrieval now -- use matches to narrow the search, never to skip the investigation.
Step 3: Reproduce, Then Minimize
Get a reproduction you can run on demand -- a failing automated test when possible, a deterministic manual repro otherwise. Then shrink it: smaller input, fewer components, shorter path to failure. A minimal repro is usually most of the diagnosis.
- Regression with a known-good past? Bisect the history (
git bisector manual halving). - Works in one environment, fails in another? Diff the two configurations and halve the differences.
- Failure is intermittent? Treat the flakiness itself as the bug. Do not rerun until green -- make the race, ordering, or state dependency deterministic first.
Concrete recipes are in references/techniques.md. Do not move to fixes while the reproduction is unstable.
Step 4: Run the Hypothesis Loop
One hypothesis at a time, cheapest test first:
- State a single falsifiable hypothesis: "X causes Y because Z."
- Predict what a specific observation will show if the hypothesis is true -- and what would disprove it.
- Test with the least invasive instrument: read the code path, then add targeted logging or assertions at component boundaries, capturing actual runtime values rather than reasoning from memory of the code.
- Record hypothesis, prediction, and result in a short debug journal before the next iteration. Change one variable per experiment.
If evidence disproves the hypothesis, discard it fully -- do not stack a patch for the old theory under a new one.
Step 5: Confirm Root Cause and Classify It
State the narrowest cause that explains all observed evidence, not just the headline symptom. Then classify:
- Local defect -- proceed to Step 6.
- Structural mismatch (requirements, architecture, or planning contradiction) -- stop patching, produce a
course-correction-note, and route upstream.
Escalation rule across failed fix attempts:
| Failed fix count | Required response |
|---|---|
| first | gather better evidence before changing more code |
| second | challenge the current hypothesis in writing and identify what evidence would falsify it |
| third or more | stop local patching, assume a structural mismatch until disproven, and prepare course-correction-note |
Distinguish outcomes precisely: the same failure persisting after a fix means the hypothesis was wrong; a new, different failure appearing often means the first fix was correct and a second bug is now exposed. The first resets your hypothesis; the second is progress -- start a fresh loop for the new failure instead of reverting blindly.
Step 6: Fix Once, Prove Causality Both Ways
Define the smallest safe change for the verified cause: what changes, what must not change, which brownfield seam or compatibility boundary stays protected, and whether this is a permanent fix or a declared workaround. Hand off to pc-tdd so a reproducing test exists before implementation expands.
Then prove causality in both directions:
- Fix applied -- the original reproduction passes, plus the surrounding test scope.
- Fix removed -- the original failure returns.
If removing the fix does not bring the failure back, you have not fixed the cause; something else moved. Return to Step 4.
Step 7: Record the Result
Produce a bug-fix-report: reproduction, root cause, confirming evidence, fix boundary, regression protection, and any follow-up debt if a workaround shipped. Keep the debug journal reference when the session had multiple attempts -- failed hypotheses are lineage for the next debugger.
Outputs
Produce only declared outputs at their documented quality boundary.
Quality Gate
- The full error output was read and the failure is reproducible on demand
- The observed code path is confirmed current (no stale build, cache, or wrong environment)
- Root cause is stated as a falsifiable explanation that accounts for all evidence, not a symptom label
- Historical matches were checked when the symptom plausibly had lineage
- Fix causality is proven both ways: repro passes with the fix, fails again without it
- Regression or characterization protection is defined before broader implementation proceeds
- Structural mismatches are escalated through
course-correction-noteinstead of patched around
Rationalization Prevention
When one of these thoughts appears, treat it as a stop signal:
| Excuse | Required response |
|---|---|
| "I'll try the obvious fix first and investigate if it fails" | Stop. Return to reproduction and evidence gathering before changing code. |
| "The error message is generic, no point reading the rest" | Read all of it, including the trace bottom and caused-by chain. It usually names the culprit. |
| "I saw this bug before, so I know the cause" | Use history to narrow the search, then prove the current root cause anyway. |
| "Rollback fixed it, so we know what happened" | Containment is not explanation; identify why the bad behavior existed. |
| "The repro is flaky, but I can still patch around it" | Stabilize the failure boundary or instrument it before writing the fix. |
| "Rerunning made it pass, so it's fine" | An unexplained pass is the same bug waiting; find the nondeterminism. |
| "This is probably just environment weirdness" | Name the environment difference and prove it explains the symptom. |
| "It's too hard to reproduce, I'll fix it by inspection" | Inspection produces hypotheses, not proof; instrument the boundary instead. |
| "I'll make the broad fix now and tighten it later" | Choose the smallest safe change for the evidenced root cause. |
| "This is the third try, but I think this version will work" | Stop local patching and prepare course-correction-note. |
| "The test passes now, so the fix works" | Also remove the fix and watch the failure return; otherwise causality is unproven. |
| "Another agent already investigated this" | Read the evidence yourself before accepting the conclusion. |
Red Flags -- Stop and Restart the Loop
- code changed before the failure was reproduced
- more than one variable changed per experiment
- a fix explanation contains "probably", "somehow", or "should"
- the same file is being patched for the third time this session
- each fix attempt reveals the same failure in a new costume
- instrumentation output was never actually read before the next change