Use this when you have passing tests and want to improve code quality — remove duplication, improve naming, simplify structure, extract abstractions — without changing behavior. This is the "Refactor" step of the Red-Green-Refactor cycle.
Process
Step 1: Gather context
Ask the engineer:
- What code do you want to refactor? (File path, function, class, or module.)
- What tests cover this code? (Test file path or test suite name.)
- What's bugging you about the current code? (Duplication, naming, complexity, long methods, unclear structure — or "just clean it up.")
Step 2: Verify the safety net
Before proposing any changes:
- Read the code to refactor and the covering tests.
- Confirm all tests pass. If any test fails, stop — fix the failing test first. Refactoring starts from green.
- Assess test coverage of the target code. Flag areas with weak or missing coverage — refactoring there is risky without additional tests.
Step 3: Propose refactoring steps
Analyze the code and propose a sequence of small, independent refactoring steps. Each step should be one of these moves:
- Rename — variable, function, class, or file for clarity
- Extract — pull a block into a named function or method
- Inline — replace a trivial abstraction with its contents
- Move — relocate code to a more logical home
- Simplify — reduce conditional complexity, flatten nesting
- Remove duplication — consolidate repeated patterns
- Improve types — tighten type signatures, remove
any
Present the steps as a numbered list with:
- What changes
- Why it improves the code
- Risk level (safe / low / medium)
Ask the engineer to approve, modify, or skip steps before proceeding.
Step 4: Apply one step at a time
For each approved step:
- Make the change.
- Run the tests.
- If all tests pass — move to the next step.
- If any test fails — revert the change immediately. Diagnose whether the test was too tightly coupled to implementation or whether the refactoring changed behavior. Report findings before proceeding.
Never combine multiple refactoring steps into a single change. One step, one test run.
Step 5: Summary
After all steps are applied (or the engineer says "that's enough"), present:
- What changed (list of refactoring moves applied)
- What didn't change (behavior, public API, test outcomes)
- Any remaining opportunities flagged for later
Output location
Apply changes directly to the codebase. Present the summary in the conversation.
Example Output
Input
- File to refactor:
src/pricing/discount.ts— acalculateDiscount()function and two helpers - Test suite:
tests/pricing/discount.test.ts— 14 passing tests covering all branches - What's bugging you: "There's a big nested if/else block that checks customer tier and order size, the same
0.01multiplier appears four times with no label, and there's a helper calleddoCalcthat nobody can explain"
Output (abbreviated)
TDD Refactor: src/pricing/discount.ts
Safety Net Check ✅
- All 14 tests passing before any changes
- Coverage: full branch coverage on
calculateDiscount(),applyTierRate(), anddoCalc() - No risky blind spots — safe to proceed
Proposed Refactoring Steps
| # | Move | What Changes | Why | Risk |
|---|---|---|---|---|
| 1 | Rename | doCalc → applyTierRate | doCalc communicates nothing; new name matches domain language | Safe |
| 2 | Extract | Pull magic number 0.01 into const PERCENT_MULTIPLIER = 0.01 | Four inline literals; one named constant clarifies intent and centralizes change | Safe |
| 3 | Remove duplication | Consolidate four near-identical tier * amount * 0.01 expressions into a single computeRawDiscount(tier, amount) call | Same arithmetic repeated per tier branch; extract removes drift risk | Low |
| 4 | Simplify | Replace nested if/else tier block with a lookup map TIER_RATES: Record<CustomerTier, number> | 34-line if/else becomes a 5-line map lookup; adding a tier no longer requires a new branch | Low |
| 5 | Improve types | Replace tier: string parameter with tier: CustomerTier (existing enum, not currently used here) | Prevents invalid tier values at compile time; removes a runtime guard that tests already exercise | Low |
Approve, skip, or modify steps before I proceed.
Step 1 Applied — Rename doCalc → applyTierRate
// Before
function doCalc(tier: number, amount: number): number {
// After
function applyTierRate(tier: number, amount: number): number {
✅ 14/14 tests pass. Proceeding.
Step 2 Applied — Extract PERCENT_MULTIPLIER
const PERCENT_MULTIPLIER = 0.01;
All four * 0.01 sites updated. ✅ 14/14 tests pass.
Step 3 Applied — Extract computeRawDiscount()
function computeRawDiscount(tierRate: number, orderAmount: number): number {
return tierRate * orderAmount * PERCENT_MULTIPLIER;
}
Replaced four duplicate expressions. ✅ 14/14 tests pass.
Step 4 Applied — Replace if/else with TIER_RATES map
const TIER_RATES: Record<CustomerTier, number> = {
bronze: 5,
silver: 10,
gold: 15,
platinum: 20,
};
export function calculateDiscount(tier: CustomerTier, orderAmount: number): number {
const rate = TIER_RATES[tier] ?? 0;
return computeRawDiscount(rate, orderAmount);
}
✅ 14/14 tests pass. 34-line block replaced with 7 lines.
Step 5 Applied — Tighten type signature
// Before
function calculateDiscount(tier: string, orderAmount: number)
// After
function calculateDiscount(tier: CustomerTier, orderAmount: number)
Removed runtime if (!validTiers.includes(tier)) guard — now caught at compile time. ✅ 14/14 tests pass.
Refactor Summary
What changed
doCalcrenamed toapplyTierRate- Magic number
0.01extracted toPERCENT_MULTIPLIER - Duplicate arithmetic consolidated into
computeRawDiscount() - 34-line if/else replaced with
TIER_RATESlookup map tierparameter tightened fromstringtoCustomerTierenum
What didn't change
- All 14 tests pass with identical outcomes
- Public API signature (callers already passed valid tier strings matching the enum values)
- Discount amounts for all tier/amount combinations
Remaining opportunities (flagged for later)
orderAmounthas no validation — negative or zero values return a discount silently; consider a guard or a dedicatedOrderAmountvalue typeTIER_RATESis module-level; if rates become configurable, extract to an injected dependency