---
name: copyconstruct-debugging
description: Debugging discipline for hard bugs — memory leaks, races, routing/sharding issues, regressions, heisenbugs. Forces diagnosis before remedy, evidence ranking, contradiction chasing, and fail-first verification. Use when a bug isn't obvious in one read, when a first fix didn't work, or when you feel the urge to patch before you can explain the failure.
---

# copyconstruct Debugging

Debugging principles for tricky bugs, from [Cindy Sridharan (@copyconstruct)'s post](https://x.com/copyconstruct/status/2082943150151397553?s=20).

The core failure mode: proposing a fix before the mechanism is understood, then anchoring to that wrong model. Everything here exists to prevent that.

**Tradeoff:** slower start, no thrashing at the end. For obvious one-line bugs, use judgment.

## The loop

Work in this order. Do not skip forward.

1. **Reproduce** — get a deterministic repro (test, script, minimal steps). No repro = you're guessing.
2. **Localize** — find which layer owns the failure. Read the actual current code there, not your memory of it — line numbers drift, functions move, code you "know" has changed. Re-read before reasoning, and again before editing.
3. **State the mechanism** — one falsifiable sentence: *"X causes Y because Z."* Can't fill it in? You have a guess, not a diagnosis. Keep digging.
4. **Falsify it** — design the observation that would prove your sentence wrong, and run it. A mechanism that survives falsification is a diagnosis.
5. **Fix minimally** — the smallest change that maps line-by-line to the mechanism. Not a broad rewrite that "probably also helps."
6. **Verify fail-first** — watch the failure happen *without* the fix, then watch the fix remove it. Far stronger than a passing happy-path test. Then verify across the axes that change behavior — flags, environments, config toggles, concurrency — not just the default path.

**Gate:** no fix proposals before the step-3 sentence is written down. A mitigation (add a bound, a retry, a cache) is not a diagnosis — shipping one early actively harms diagnosis by making you stop looking.

## Evidence rules

- Rank clues by conclusiveness. When one artifact is near-decisive, reason *from* it — don't generate parallel hypotheses that ignore it.
- "I verified this" and "this seems true" are different claims. Never let the second speak in the voice of the first; confidence must track what you actually checked.
- One instance proves nothing general. "This case behaves like X" ≠ "all cases behave like X." Enumerate; check the others.
- Label every artifact with its exact provenance — a different environment, version, or config is potentially different behavior — and don't merge conclusions across sources until you've confirmed they're the same path. Conflating different paths manufactures fake contradictions you then waste effort resolving.

## Contradictions are the signal

When two things you believe can't both be true, that gap *is* the diagnosis — chase it, never smooth it over. "It's always been broken" colliding with "it used to work" is the exact question to answer, not noise.

Ask the naive question: *"why did this ever work?"* / *"why didn't this fire before?"* The simplest question you're tempted to skip is usually the highest-leverage one.

For regressions: separate "what is the defect" from "what triggered it." The defect often lives in unchanged code that a separate change made reachable — answering from current code beats git archaeology.

## Self-check flags

Pre-empt the reviewer: challenge your own claims before presenting them. If your draft contains one of these, stop and go run the check:

| You wrote | What it means |
|---|---|
| "presumably", "should be", "in practice", "likely" | unverified claim wearing verified clothes — go check |
| "this probably also fixes…" | fix not traceable to the mechanism — back to step 3 |
| "looks equivalent" | equivalence not proven — see below |
| "always been broken" *and* "used to work" | live contradiction — that's the diagnosis, chase it |
| a fix, with no mechanism sentence above it | anchored to a guess — back to step 3 |

## Replacing load-bearing logic

Prove equivalence exhaustively: tabulate every input/state class and show old-vs-new behavior matches everywhere except the intended delta. "It looks equivalent" is not equivalence.