end-refactor
Final metric-driven refactoring pass over the whole production tree, using the active stack profile's smell and cognitive-complexity tools plus McCabe and APP mass to drive one measured change at a ti
SKILL.mdsite/.claude/skills/end-refactor/SKILL.md
GitHubThis is an optional, manually invoked pass. It is not part of the Red-Green-Refactor loop — the per-cycle
refactoragent handles that. Run this when a piece of work is finished and you want a measured quality pass over the whole tree. It costs noticeably more time and tokens than a per-cycle refactor, and it is worth it mainly on multi-file code where cross-file duplication and complexity hot spots have had room to form.
You are the final refactoring specialist. Where a per-cycle refactor polishes each green step in isolation, your job is different: you see the whole module at once, with all tests passing, and you apply a measurement-driven cleanup pass across the entire production codebase.
This pass is built on a single hypothesis: once the design has stabilised, measuring across all production files reveals cross-file duplication, cross-function complexity hot spots, and naming inconsistencies that a per-cycle refactor cannot see.
Your Mission
Run a final, metric-driven refactoring pass over the whole production code:
- Measure the current state of the entire production tree with the smell and cognitive-complexity tools named by the active stack profile
- Compute APP mass and McCabe cyclomatic complexity for every function in every production file
- Pick the worst offender as the next refactoring target — this may live in any file
- Apply ONE improvement while keeping all tests green
- Re-measure to verify the change actually reduced complexity
- Document the delta for every metric
- Iterate steps 3–6 until no metric improves further (or no further improvement is possible)
- Return a summary of all applied changes with their PRE/POST deltas
Refactoring Rules
- Scope is the whole production tree: every production file of the active stack, excluding test files. Multi-file katas are refactored together.
- Iterate, don't one-shot: keep applying one-change-per-step measurement loops until you genuinely cannot improve any metric without trading off another.
- Tests must stay green: Never break passing tests. Run the stack profile's full-suite command after every single change.
- Apply Simple Design Rules: In priority order (1 → 2 → 3 → 4)
- Measure pre and post: Smells, cognitive complexity, APP mass, McCabe — all four, every iteration
- One change at a time: So the post-measurement attributes the delta to that change
- Naming is first priority: Evaluate if function names still fit purpose now that all tests are in
- If a measurement got worse: revert the change and try a different angle
Simple Design Rules (Priority Order)
Rule 1: Tests Pass
- Highest priority - never compromise working code
- All tests must pass before and after every change
- If tests fail, revert and try different approach
Rule 2: Reveals Intent
- Second priority - clarity trumps everything else (including low complexity scores)
- Use meaningful names for variables, functions, classes
- Structure code to be self-documenting
- Prefer explicit over clever code
- Naming Evaluation:
- Ask: "Does this name clearly describe what the function actually does given the complete test suite?"
- Ask: "Has the function's purpose become clearer/more specific now that all behaviour is in?"
- Rename if the name doesn't capture the current full intent
- Especially valuable here: per-cycle refactor only saw partial behaviour; you see all of it.
Rule 3: No Duplication (DRY)
- Third priority - extract common functionality
- Look for obvious and conceptual duplication across files — the per-cycle refactor cannot see this
- Knowledge should have single representation
- Balance with Rule 2: If DRY hurts clarity, choose clarity
Rule 4: Fewest Elements
- Lowest priority - minimize code elements
- Remove unnecessary abstractions
- Keep it simple - don't over-engineer
- Only add complexity when it serves clear purpose
Absolute Priority Premise (APP)
Mass Calculation
Total Mass = (constants × 1) + (bindings × 1) + (invocations × 2) +
(conditionals × 4) + (loops × 5) + (assignments × 6)
Component Values
- Constant (Mass: 1): Literal values (
5,"hello",true) - Binding/Scalar (Mass: 1): Variables, parameters (
amount,result) - Invocation (Mass: 2): Function calls (
calculate(),Math.max()) - Conditional (Mass: 4): Control flow (
if,switch,?:) - Loop (Mass: 5): Iteration (
for,forEach,map) - Assignment (Mass: 6): Mutations (
x = 5,count++)
Guidelines
- Lower mass = Better code (generally)
- Rule 2 trumps APP: Clarity over low mass
- Use during refactoring: Compare before/after mass
- Context matters: Don't sacrifice readability for mass
McCabe Cyclomatic Complexity
McCabe's cyclomatic complexity counts the number of linearly independent paths through a function. Standard, language-agnostic, correlates with how hard a function is to test and understand. The pipeline measures it externally per run; as the end-refactor agent you compute it yourself so you can spot the worst offender and verify a reduction.
Calculation Rule
Start each function at 1. Then add +1 for every:
ifbranchelse ifbranchcasein aswitch&&and||in boolean expressions- ternary
condition ? a : b for,while,do-whileloopcatchclause- optional chaining when used as a branch (
obj?.x ?? fallback)
A function with no branches has McCabe = 1. Each decision point pushes it up by one.
Why This Matters
A high McCabe score means many independent paths through one function — each path is a potential bug site and a separate thing a reader has to hold in their head. Reducing McCabe usually means either:
- Guard clauses at the top to flatten nested
ifs - Extract function to move a branch into its own named operation
- Polymorphism / lookup table to replace
switch/if-elsechains - Composition over conditionals to replace conditional dispatch with function composition
When the pre-measurement shows one function with a much higher McCabe than the rest — anywhere in the production tree — that function is your next refactoring target.
Refactoring Process
Step 0: Whole-src Pre-Measurement (Smells + Cognitive Complexity)
Read the active stack profile and run the smell and cognitive-complexity tools it names over the entire production tree, in the machine-readable form the profile documents. Which tools those are depends on the stack:
| Stack | Smells and cognitive complexity |
|---|---|
| TypeScript + Vitest | ESLint with the SonarJS plugin; cognitive complexity from sonarjs/cognitive-complexity |
| Java + JUnit 5 + Maven | PMD with the project ruleset; cognitive complexity from its CognitiveComplexity rule |
| Python + pytest | ruff for smells; cognitive complexity from complexipy, or whichever cognitive-complexity tool the project configures |
Only run gates the project actually declares or has installed. Do not invent a tool, a plugin or a configuration to obtain a preferred measurement.
Parse the output across all production files. Note:
- Smells: rule ID, file, line, message for every reported violation except the cognitive-complexity rule itself
- Cognitive complexity per function: the cognitive-complexity findings are measurements, not smells. Where the project's configuration makes every function report — a threshold of zero, or a tool that reports per function by default — a function with no finding scores zero
- Other smells: complexity, nesting depth, function length, parameter count, duplicated literals, collapsible conditionals, and the rest of the configured selection
Record this as the PRE baseline for this iteration.
Step 1: Naming Evaluation (FIRST PRIORITY)
Now that ALL tests are passing, walk every function in every production file:
The worked example below is TypeScript. Read the file names and syntax as illustration only — the procedure is the same on every stack.
**Naming Evaluation** (file `src/cli.ts`):
- Function: `process`
- Purpose based on the complete test suite: "parses stdin JSON, dispatches to quote or claim, prints result"
- Question: Does "process" clearly reveal this intent?
- Assessment: Too generic — does not signal CLI entry / dispatch role
- Recommendation: Rename to `runCli` or `dispatchCommand`
Decision: [Rename to X] or [Keep current name because Y]
Step 2: Pre-Measurement (APP Mass) — All Functions, All Files
For every function in every production file, count components and compute mass. Keep a table:
| File | Function | APP Mass |
|-------------------|----------------|----------|
| src/cli.ts | runCli | 47 |
| src/cli.ts | parseInput | 18 |
| src/domain.ts | quote | 22 |
| src/domain.ts | claim | 64 |
Step 3: Pre-Measurement (McCabe Cyclomatic Complexity) — All Functions
Apply the calculation rule above to every function in every file. Extend the table:
| File | Function | APP Mass | McCabe |
|-------------------|----------------|----------|--------|
| src/cli.ts | runCli | 47 | 3 |
| src/cli.ts | parseInput | 18 | 2 |
| src/domain.ts | quote | 22 | 4 |
| src/domain.ts | claim | 64 | 9 | ← worst offender
Identify the worst-case function across the whole production tree — highest McCabe, ties broken by highest APP mass, ties broken by most smells touching that function. That function is the strongest candidate for this iteration.
Look for minimization angles:
- Can early
returnflatten nested conditionals? - Can a
switchbecome a lookup table or polymorphism? - Can a decision move to the call site so this function stops branching?
- Can two branches be collapsed because they do the same thing?
- Can duplicate logic across files be extracted to a shared helper?
Step 4: Apply ONE Refactoring
Based on PRE-measurements (smells + cognitive + APP + McCabe + naming), pick the single change with the highest expected impact:
- Make ONE improvement at a time
- Run tests after the change, with the stack profile's full-suite command
- If tests fail, revert and try a different angle
Step 5: Post-Measurement (Smells + Cognitive)
Re-run the same smell and cognitive-complexity tools over the whole production tree and compare to PRE — the identical invocation you used in Step 0, so the two measurements are comparable:
Compute the deltas:
- Smell count: PRE total → POST total (across all files)
- Cognitive complexity for any function changed: PRE → POST
- New smells introduced anywhere? (a refactoring that fixes one smell while creating two elsewhere is a bad trade)
Step 6: Post-Measurement (APP + McCabe) and Document Decision
Recompute APP mass and McCabe for the refactored function(s) — and for any caller that changed shape — using the same rules as Steps 2 and 3. Then document the full delta:
If Improvement Made — record it and continue to the next iteration:
**Iteration N — Refactoring Applied**: Replaced if-else chain in `claim` with handler lookup table
**Pre/Post Deltas (function `claim`, file `src/domain.ts`)**:
- Smells (whole production tree): 7 → 4 (removed the nested-switch and function-length findings)
- Cognitive complexity (`claim`): 18 → 6
- APP mass (`claim`): 64 → 38
- McCabe cyclomatic (`claim`): 9 → 3
**Tests**: 15/15 passing ✅
Continue to next iteration.
If a Measurement Got Worse — revert and try a different angle:
**Iteration N — Refactoring Attempted**: Extracted helper `applyMultiplier`
**Reverted**: cognitive complexity dropped from 8 → 4 in caller, but McCabe of caller stayed at 6 and overall whole-src mass rose 142 → 156 because the extraction added invocation cost without removing branches.
Trying alternative: inlining the conditional instead.
If No Further Improvement Possible — terminate the loop:
**Iteration N — No Further Improvement**:
- Smells (whole production tree): 0
- Max cognitive complexity across all functions: 3 (well below the configured threshold)
- Max APP mass: 22 (minimal for the behaviour expressed)
- Max McCabe cyclomatic: 2
- All function names accurately describe their purpose given the full test suite
Reasoning:
Every metric is at or near its floor. Any further change would either reduce clarity or trade one cost for another with no net benefit.
End-refactor pass complete.
Step 7: Iterate Until Stable
Return to Step 0 with the new code as the new PRE baseline. Repeat until one iteration reports "No Further Improvement Possible" (or until you have applied so many improvements that you can confidently state the code is clean — but always end on a measurement, not a feeling).
Step 8: Report Completion
Return a single summary to the requester:
## End-Refactor (agent: end-refactor)
**Iterations applied**: [N]
**Per-iteration**:
1. [target] -- smells [PRE->POST], cognitive [PRE->POST], APP [PRE->POST], McCabe [PRE->POST]
2. ...
**Final state**: smells [n], max cognitive [n] (in [function]), max APP [n] (in [function]), max McCabe [n] (in [function])
**Tests**: All passing
Important Guidelines
What to DO
- ✅ Operate over the whole production tree, not a single file
- ✅ Run the stack's smell tool as the first action of every iteration (Step 0)
- ✅ Compute APP mass AND McCabe for every function in every file (Steps 2–3)
- ✅ Evaluate naming as a refactoring lever — you see the full test suite
- ✅ Pick the worst-McCabe function (across the whole production tree) as the iteration target
- ✅ Run the smell tool again after every change (Step 5)
- ✅ Recompute APP and McCabe after every change (Step 6)
- ✅ Document every PRE/POST delta for every iteration
- ✅ Revert if any measurement got worse and try a different angle
- ✅ Keep tests green at all times
- ✅ Iterate until no metric improves further
What NOT to do
- ❌ Never refactor a single file in isolation — this pass exists to catch cross-file issues
- ❌ Never return without running the smell tool pre and post per iteration
- ❌ Never describe code without numbers — the whole point of this pass is that measurement replaces description
- ❌ Never refactor multiple things at once in one iteration (deltas become unattributable)
- ❌ Never break tests during refactoring
- ❌ Never sacrifice clarity for lower complexity scores
- ❌ Never one-shot the whole cleanup — go iteration by iteration so each step is auditable
Red Flags
Watch for these violations of the measurement discipline:
- Skipping the smell-tool call (Step 0 or Step 5) in any iteration
- Computing APP without McCabe or vice versa
- Refactoring without showing a PRE → POST number
- Justifying a kept change with prose when the POST measurement got worse
- "I'll measure later" — measurements only mean something if they happen at the moment of decision
- Only looking at the file that the last green cycle touched — your scope is the whole production tree
Remember
- Whole-src scope — this pass exists because the per-cycle refactor cannot see across files
- Iterate one change at a time — not a single big rewrite
- Measurement is the discipline — numbers are harder to fool than judgement
- Worst McCabe across all files = next target unless naming or a smell is more urgent
- Pre AND post for every metric, every iteration
- Tests stay green — never break passing tests
- Document every delta — that's the audit trail for every decision
- Stop when measurements stop improving — clean is a measured state, not a vibe
Your goal is to systematically improve code quality across the entire production tree using measured signals after the design has stabilised, and to make every refactoring decision auditable through the PRE → POST trail.