$npx -y skills add UnpaidAttention/fable5-methodology --skill legacy-debuggingDebug code you didn't write and can't fully trust — unfamiliar, poorly documented, or untested subsystems — using history archaeology, boundary instrumentation, minimal harnesses, and history bisection, while resisting the urge to rewrite. Trigger this when a failure lives in cod
| 1 | # Legacy Debugging |
| 2 | |
| 3 | In legacy code the missing artifact isn't the fix — it's the INTENT. The code tells you what |
| 4 | happens; nothing tells you what was supposed to happen. This skill recovers intent from |
| 5 | history and runtime behavior before you change anything. |
| 6 | |
| 7 | ## Step 1: Reproduce first — no exceptions |
| 8 | |
| 9 | Same iron rule as all debugging: make the failure happen on demand with the shortest command, |
| 10 | capture exact output, shrink the reproduction. In legacy code this matters MORE, because you |
| 11 | cannot fall back on "I know what this should do" — the reproduction is your only oracle. |
| 12 | |
| 13 | ## Step 2: Archaeology — recover the intent |
| 14 | |
| 15 | 1. `git log --follow -- path/to/file` and `git blame` on the failing region. Commit messages, |
| 16 | linked issues, and PR descriptions are the documentation that was never written. The |
| 17 | commit that introduced a weird line usually says why. |
| 18 | 2. Grep for the failing function's callers. How it is USED is stronger evidence of intended |
| 19 | behavior than how it reads. |
| 20 | 3. Look for abandoned intent nearby: old tests (even skipped ones), doc comments that no |
| 21 | longer match the code (they describe an earlier intent — note WHEN they diverged via blame), |
| 22 | config flags nobody sets. |
| 23 | 4. **Chesterton's fence procedure:** before deleting or "fixing" code that looks wrong or |
| 24 | pointless, find the commit that added it. If the commit explains it — you just learned a |
| 25 | constraint. If it doesn't and no caller depends on it, note it as a candidate removal in |
| 26 | your report; don't remove it as part of the bug fix. |
| 27 | |
| 28 | ## Step 3: Instrument boundaries — trust nothing named |
| 29 | |
| 30 | 1. In unfamiliar code, names lie: `validated_input` may be unvalidated; `cache` may never |
| 31 | hit. Log ACTUAL values at the boundaries of the failing region — arguments in, return |
| 32 | values out, state before/after — and read what's really flowing. |
| 33 | 2. Instrument at the seams between components before instrumenting inside them: it tells you |
| 34 | WHICH component misbehaves before you spend hours in the wrong one. |
| 35 | 3. Keep a written record of confirmed facts as you go ("`parse_row` returns None for rows |
| 36 | with quoted commas — confirmed by log"). In legacy work the fact list IS your growing |
| 37 | documentation; it goes in your working notes and, distilled, in your final report. |
| 38 | |
| 39 | ## Step 4: Build a minimal harness |
| 40 | |
| 41 | If the failing unit can't be invoked directly, spend the 15 minutes writing a scratch harness |
| 42 | that calls it with controlled input (a scratch test file or REPL script). Debugging through |
| 43 | five layers of the full app multiplies every observation's cost; the harness pays for itself |
| 44 | by the third run. The harness later upgrades into the regression test. |
| 45 | |
| 46 | ## Step 5: Bisect history when it "used to work" |
| 47 | |
| 48 | If there is any version where the behavior was correct: `git bisect` with your reproduction |
| 49 | script as the oracle. Ten minutes of bisect beats hours of code reading — it hands you the |
| 50 | exact commit, whose message and diff explain both mechanism and intent at once. |
| 51 | |
| 52 | ## Step 6: Resist the rewrite |
| 53 | |
| 54 | The urge to rewrite unfamiliar code is strongest exactly when you understand it least — that |
| 55 | correlation is the warning. Rules: |
| 56 | 1. The fix is minimal and local. Match the file's existing style even if you dislike it — |
| 57 | a mixed-idiom file is worse than a consistently old-fashioned one. |
| 58 | 2. Every weirdness you don't understand and don't need to touch stays untouched, and goes in |
| 59 | the report's findings list instead. |
| 60 | 3. Rewrite only as an explicitly scoped separate task with the user's sign-off AND a |
| 61 | characterization-test net first (see safe-refactoring) — never as a side effect of a bug fix. |
| 62 | |
| 63 | ## Step 7: Fix, test, document what you learned |
| 64 | |
| 65 | Regression test via the Step 4 harness; run whatever suite exists. Your report includes the |
| 66 | recovered intent ("per commit a1b2c3, the retry exists because vendor API returns 503 during |
| 67 | rebalancing") — you are leaving the documentation you wished you'd found. |
| 68 | |
| 69 | ## Worked example |
| 70 | |
| 71 | Symptom: inherited invoice service occasionally emits duplicate invoices. No docs, no tests. |
| 72 | |
| 73 | 1. Reproduce: replaying Tuesday's queue snapshot against a local instance → 2 duplicates, |
| 74 | deterministic. Shrunk to one message replayed twice. |
| 75 | 2. Archaeology: blame on the dedup check → commit "temp: relax idempotency check for |
| 76 | migration (revert after June)" — from three years ago. |