Multi-File Refactors Without Breaking Everything
Sequencing a large change so each step is verifiable, and using the type checker as your safety rail.
Worth reading first: Small Diffs, Frequent Commits
A refactor is where AI assistance is most tempting and most dangerous: enormous mechanical effort, and a failure mode where everything still compiles but something subtle is now wrong. The trick is sequencing it so every step is verifiable.
Two kinds of large change
Before starting, work out which one you have — because they need opposite treatment.
- 1Mechanical
Renaming a symbol, changing an import path, moving files. The compiler or type checker can confirm the result. Safe to do all at once.
- 2Semantic
Changing what code does — splitting a component, altering a data shape, replacing a state approach. Nothing automatic confirms correctness. Must be done in steps.
People get hurt by treating a semantic refactor as if it were mechanical, because it looked mechanical while it was happening.
Make it verifiable first
The most important work happens before any refactoring. If you cannot tell whether behaviour changed, you cannot safely change it — with or without AI.
Before we refactor anything: write tests that capture thecurrent behaviour of src/lib/pricing.ts, including the edgecases. Do not change the implementation. These tests shouldpass against the code exactly as it is now.These are characterisation tests — they do not assert that the code is right, only that it does what it currently does. That is precisely what you need, because a refactor is by definition supposed to preserve behaviour.
Sequence it so each step stands alone
Ask for a plan before any edits, and require that each step compile and pass tests on its own:
I want to split the 600-line Dashboard component into smallerpieces. Plan this as a sequence of steps where each one: - changes as few files as possible- leaves the app compiling and tests passing- can be committed on its own Show me the plan. Do not edit anything yet.Then execute one step per prompt, running your checks and committing between each. This is the small-diffs principle applied to a change that superficially looks like it has to happen all at once.
Let the type checker do the reviewing
In a typed codebase there is a technique that turns a risky refactor into a guided one: change the type first and let the errors tell you where to go.
Change the User type so that 'name' becomes 'firstName' and'lastName'. Do not fix any of the resulting errors yet —just run the type checker and show me the full list ofplaces that now break.That list is an exhaustive, machine-generated inventory of every call site. It is more reliable than any search, and more reliable than the model’s own sense of completeness — which is the thing you should least want to depend on here.
What goes wrong, specifically
- 1Silent behaviour changes
It “simplifies” logic while moving it, and an edge case quietly disappears. Characterisation tests catch this; reading does not.
- 2Partial application
Eleven of thirteen call sites updated. Compiles if the types are loose. Ask explicitly whether any were missed, and verify with a search.
- 3Scope creep
Reformatting, renaming, and reordering mixed into the same diff, hiding the two lines that actually matter. State up front that unrelated changes are not wanted.
- 4Losing the thread
Step nine of a twelve-step refactor contradicts step three. This is the context window filling up — restart the conversation with the plan and current state.
The escape hatch
Do the whole refactor on a branch. If it goes wrong three steps in, you delete the branch rather than trying to reverse-engineer your way back:
$ git switch -c refactor/split-dashboard# ... work, committing after each verified step ...$ git switch main # if it went badly$ git branch -D refactor/split-dashboardAbandoning a refactor is a normal outcome, not a failure. The branch makes it cost nothing but the time already spent.
Key takeaways
- Decide whether the change is mechanical or semantic. Semantic changes must be stepped.
- Write characterisation tests first — they make “did behaviour change?” answerable.
- Ask for a plan where every step compiles, passes, and commits on its own.
- Change the type first and let the compiler produce an exhaustive list of call sites.
- Work on a branch, so abandoning it costs nothing.
- If step nine contradicts step three, the context is full. Restart with the plan.