Multi-File Refactors Without Breaking Everything
Sequencing a large change so each step is verifiable, and using the type checker as your safety rail.
Read 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.
What it's actually good at, and what it isn't
Before sequencing anything, be honest about which side of the mechanical-versus- semantic line your change actually sits on. Handing over the wrong kind is the mistake underneath most of the others in this chapter.
Well suited to a model
Renaming a symbol across forty files. Converting a class component to a function component, mechanically. Splitting one large file into several along boundaries you specify. Updating every call site after you change a type.
The wrong thing to hand it
Redesigning a data model when the right shape depends on a product decision nobody has written down. A performance rewrite where correctness depends on an invariant that lives in someone’s head, not the code. Any refactor where “better” is a judgement call rather than a checkable property.
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.
- If you can't state what "done" means as something a test or type checker can verify, it's a judgement call — hand it to a person, not a model.
- 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.
Quick check
Answer these to unlock the next chapter — 3 of 4 to pass. You can retake it anytime.
Answer every question to check.
Make a free account to read on
Every chapter is free — an account is how your progress, XP, and streak follow you from your laptop to your phone, and how you show up on the leaderboard. No payment, no trial.