Skip to content
Back to skills

Refactoring Safely

ASecurity

Change the structure of code that already exists without changing what it does: smells as triggers, the named transformations (extract/inline, move feature, organise data, simplify conditionals), and above all the workflow that makes it safe — characterization tests first, one transformation at a time, green between every step. Use when a file or function is already too large; when asked to "split this module", "extract this", "break up main.py", "clean up this legacy code", "reduce coupling ...

  • 154 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 8, 2026
ai-agentsgoexpressrefactoring

Security analysis

A100/100

Pro scans all 8 files and shows the line behind each finding

Scanned October 8, 2026

npx -y skills add AnastasiyaW/codex-claude-code-config --skill refactoring-safely --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Refactoring Safely?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Refactoring Safely
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/anastasiyaw-refactoring-safely/badge)](https://www.skillsdirectory.com/skills/anastasiyaw-refactoring-safely)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: refactoring-safely
description: >
  Change the structure of code that already exists without changing what it does:
  smells as triggers, the named transformations (extract/inline, move feature, organise
  data, simplify conditionals), and above all the workflow that makes it safe —
  characterization tests first, one transformation at a time, green between every step.
  Use when a file or function is already too large; when asked to "split this module",
  "extract this", "break up main.py", "clean up this legacy code", "reduce coupling
  here"; when a shape advisory fires on a grown file; or before any restructuring of
  code that has users. Do NOT use to decide the target layout of a NEW project (use
  architecture-first), for unit-level naming and function quality in code you are
  writing fresh (use code-complexity), for capacity or storage decisions (use
  system-and-data-design), or to strip over-engineering on request (use lean-code). This
  is the transformation with a net; deciding WHERE things should end up is a different
  question, and doing both at once is how refactors lose behaviour.
---

# Refactoring safely — changing shape without changing behaviour

Refactoring is a behaviour-preserving transformation. If behaviour changed, that was not
a refactor, it was a rewrite with optimistic branding — and the reason large restructures
fail is almost always that the two were done in one step.

## The rule that makes the rest work

**Never restructure and change behaviour in the same commit.** Alternate deliberately:
refactor (green → green, no behaviour change), then change behaviour (with a new test).
When both happen at once, a failing test cannot tell you which half broke it.

## Before touching anything: the net

Legacy code is code without tests, regardless of age. So:

1. **Characterization tests first.** Not tests of what it *should* do — tests of what it
   *does*, including behaviour you consider wrong. Their job is to detect change, not to
   judge it. Wrong-but-tested behaviour gets fixed later, on purpose, in its own commit.
2. **Cover the seams you are about to cut**, not the whole file. Coverage of the region
   under the knife is what matters.
3. **Confirm they fail when you break something.** A characterization test that passes
   against deliberately broken code is worse than none — run that check once.
4. **Know how to revert.** Small commits, one transformation each.

## Smells, and what each one actually indicates

| Smell | Underlying problem | Transformation |
|---|---|---|
| Long function | Several jobs in one scope | Extract function, split by level of abstraction |
| Large module | Several reasons to change in one file | Move features into modules that own their state |
| Long parameter list | A missing object, or a hidden mode flag | Introduce parameter object; split by behaviour |
| Feature envy | Method uses another object's data more than its own | Move method to where the data lives |
| Shotgun surgery | One decision spread across files | Gather it into one owner |
| Divergent change | One file changing for unrelated reasons | Split by reason to change |
| Primitive obsession | A concept represented as a string or dict everywhere | Introduce a type; put its rules with it |
| Duplicated *decision* | One rule expressed in several places | Extract and give it one home |

Duplicated *text* expressing unrelated decisions is not a smell. Merging it couples
things that had no reason to change together.

## The sequence for a large module

Order matters more than technique, and this order is what keeps each step small:

1. **Constants and pure helpers out first.** They have no state and no dependents to
   break, and they are what every future slice would otherwise import from the old file.
2. **Then the state nobody else touches.** Background machinery, caches and locks used
   by one area move with that area, taking their invariants along.
3. **Then whole vertical slices** — one feature's handlers plus the state it owns. A
   slice that takes its own state with it is a clean cut; a slice that leaves state
   behind produces "modules" that still need the old file.
4. **The long tail last.** Single functions with no clear home go where they are used,
   or into one honestly-named remainder. Splitting them first is motion without benefit.
5. **Delete an old path only after every identified consumer has migrated and
   proportionate consumer/integration proof shows the replacement carries the
   required behavior.** For a live externally served boundary, that proof may
   include traffic/canary evidence; an offline installer, private module, or
   staged component instead uses its applicable focused contract/integration
   receipt. Keep the old path until deletion is authorized, needed consumers
   are proven migrated, and no speculative early removal can erase a rollback
   or compatibility boundary.

At each step: green before, green after, one commit.

## Concurrency: the part that bites

Hand-rolled locks and module-level mutable state do not survive naive moves. Before
moving anything guarded:

- Write down the invariant the lock protects, in words, before the move.
- Move the state and its lock together, into the module that owns them. State in one
  module and its lock in another is a bug waiting for load.
- If two areas share one lock, that is the coupling — resolve it before splitting, not
  during.

## References — load on demand

- `references/refactoring-patterns/smell-catalog.md` — the full catalogue with triggers
- `references/refactoring-patterns/refactoring-workflow.md` — safe workflow and rollback
- `references/refactoring-patterns/composing-methods.md` — extract, inline, replace temp
- `references/refactoring-patterns/moving-features.md` — move method/field, hide delegate
- `references/refactoring-patterns/organizing-data.md` — replace primitive, encapsulate
- `references/refactoring-patterns/simplifying-conditionals.md` — guard clauses, polymorphism
- `*-original.md` — the source skill's own framework prose, kept verbatim

## Gotchas

- **The big-bang restructure.** A branch that reorganises everything cannot be reviewed,
  cannot be bisected, and conflicts with every parallel session. Small steps on main
  beat a heroic branch, every time.
- **Refactoring under a shared file.** If other sessions or teammates are editing the
  same file right now, the cut is a coordinated operation, not a background tidy. Check
  first; conflicts here lose work rather than just time.
- **"Modular" that still imports the old module.** If every new module imports the file
  you split, you moved text, not boundaries. Constants and shared state first — that is
  what step 1 and 2 are for.
- **Renaming during a move.** Move, commit, then rename. A diff that does both is
  unreviewable and hides accidental behaviour changes.
- **Treating traffic as universal proof.** Traffic/canary proof is meaningful for a
  live served boundary, not an offline or sealed module. Match the consumer or
  integration receipt to the boundary, while retaining its deletion authority
  and migration check.

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| Tests broke and you cannot tell why | Behaviour change smuggled into a structural commit | Revert; redo as two commits |
| Extracted function needs six parameters | The cut is in the wrong place | Cut along the data, not along the line count |
| Circular import after the split | Two new modules both own part of one concept | Extract the shared concept into a third |
| Race appears only after the move | State and its lock ended up in different modules | Move them together; re-state the invariant |
| New offline path has no traffic | The module is not a live served boundary | Identify its consumers and use the applicable focused contract/integration receipt before authorized deletion |

Files in this skill

  • SKILL.md7.8 KB
  • references/refactoring-patterns-original.md20.9 KB
  • references/refactoring-patterns/composing-methods.md11.6 KB
  • references/refactoring-patterns/moving-features.md12 KB
  • references/refactoring-patterns/organizing-data.md12.7 KB
  • references/refactoring-patterns/refactoring-workflow.md14 KB
  • references/refactoring-patterns/simplifying-conditionals.md12.8 KB
  • references/refactoring-patterns/smell-catalog.md13.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.