$npx -y skills add UnpaidAttention/fable5-methodology --skill safe-refactoringExecute behavior-preserving code restructuring — establish a test safety net first, apply the smallest mechanical transformations in sequence, verify after each, and refuse refactors that lack the coverage to be done safely. Trigger this when the user asks to "refactor", "clean u
| 1 | # Safe Refactoring |
| 2 | |
| 3 | A refactor changes structure and preserves behavior — both halves are the definition. Without |
| 4 | a way to PROVE behavior is preserved, you are not refactoring; you are rewriting and hoping. |
| 5 | |
| 6 | ## Step 0: Define the behavior contract |
| 7 | |
| 8 | State in one or two lines what must be true before and after: "all existing tests pass; the |
| 9 | public API of `billing/` is unchanged; the CLI output is byte-identical." This contract is |
| 10 | what "safe" means for this refactor. If any behavior IS allowed to change, that part is not a |
| 11 | refactor — split it out (Step 4). |
| 12 | |
| 13 | ## Step 1: Establish the safety net BEFORE touching anything |
| 14 | |
| 15 | 1. Run the existing tests over the affected code. Record the baseline (green, or which |
| 16 | failures are pre-existing). |
| 17 | 2. Assess whether the tests actually cover the code you'll restructure: run coverage over the |
| 18 | affected files, or inspect — do the tests exercise the branches you're about to move? |
| 19 | 3. **Insufficient coverage → write characterization tests first.** These capture what the |
| 20 | code DOES today (not what it should do): feed representative inputs, assert the actual |
| 21 | current outputs. For messy code, golden-master style works: capture today's output for a |
| 22 | corpus of inputs into fixture files, assert future output matches byte-for-byte. |
| 23 | 4. Characterization tests are committed/verified BEFORE the first structural edit — they are |
| 24 | the net; a net installed after the fall is decoration. |
| 25 | |
| 26 | ## Step 2: The refuse/defer rule |
| 27 | |
| 28 | Refuse (or defer pending tests) when ALL of: the code has no meaningful coverage, AND its |
| 29 | behavior can't be cheaply characterized (nondeterminism, heavy I/O entanglement, no way to |
| 30 | run it), AND the restructuring is more than mechanical. Say exactly that: |
| 31 | > "This refactor isn't safe yet: `dispatch()` has zero coverage and side-effects I can't |
| 32 | > capture in a harness. I can (a) write characterization tests first (~N tests), or (b) limit |
| 33 | > to mechanical renames only. Which?" |
| 34 | |
| 35 | Refusing a risky refactor is a correct deliverable. A "successful" blind rewrite is a defect |
| 36 | with a delay on it. |
| 37 | |
| 38 | ## Step 3: Smallest transformations, verified individually |
| 39 | |
| 40 | 1. Decompose into named mechanical steps — rename; extract function/module; inline; move; |
| 41 | change signature (with all call sites); replace conditional with early return. One step at |
| 42 | a time. |
| 43 | 2. After EACH step: run the affected tests (the verification-loop rhythm). Green → next step; |
| 44 | red → the step itself is the suspect, fix or revert it before anything else. |
| 45 | 3. Prefer tool-assisted transformations (LSP rename, IDE extract) over hand edits — then grep |
| 46 | for what tools miss: string references, docs, templates, reflection, serialized names, |
| 47 | SQL, config keys. |
| 48 | 4. Order steps so the tree compiles between every pair — never a step that requires step N+1 |
| 49 | to build. |
| 50 | |
| 51 | ## Step 4: No mixed cargo |
| 52 | |
| 53 | If you discover an actual bug mid-refactor: do NOT fix it inside the refactor. Note it, finish |
| 54 | (or pause) the refactor to a verified state, then fix the bug as its own change with its own |
| 55 | regression test. A diff that both moves code and changes behavior cannot be reviewed for |
| 56 | either property. Same for improvements: new features never ride along in a refactor diff. |
| 57 | |
| 58 | ## Step 5: Prove the contract |
| 59 | |
| 60 | Full affected suite green; baseline failures unchanged; and check the contract from Step 0 |
| 61 | directly (API surface diff, byte-compare the golden outputs). Review the diff explicitly |
| 62 | asking one question per hunk: "could this hunk change behavior?" Any hunk answering "yes" |
| 63 | that isn't a pure signature-propagation gets scrutiny. |
| 64 | |
| 65 | ## Worked example |
| 66 | |
| 67 | Task: "Clean up `report.py` — 600 lines, one god function." |
| 68 | |
| 69 | - Contract: CLI output byte-identical for all supported flags; tests stay green. |
| 70 | - Safety net: only 2 tests exist, covering ~20%. Write a golden master: run the CLI over 8 |
| 71 | representative fixture inputs (incl. empty file, unicode, flag combinations), save outputs |
| 72 | to `tests/golden/`. Now every future step is checked against reality. |
| 73 | - Steps: (1) extract `parse_args` → golden green; (2) extract `load_rows` → green; |
| 74 | (3) extract `render_table` → RED: one golden diff — a `.strip()` was lost in the move; |
| 75 | restore it, green (the net just paid for itself); (4) move helpers to `report/lib.py` → |
| 76 | green; (5) found an actual bug (crash on zero rows) → NOTED in report, not fixed here. |
| 77 | - Deliver: structu |