Part 4 of 7 · Chapter 5 of 5

Multi-File Refactors Without Breaking Everything

Sequencing a large change so each step is verifiable, and using the type checker as your safety rail.

Advanced14 min read

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.

  1. 1
    Mechanical

    Renaming a symbol, changing an import path, moving files. The compiler or type checker can confirm the result. Safe to do all at once.

  2. 2
    Semantic

    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.

Step zero
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:

Prompt
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.

Prompt
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

  1. 1
    Silent behaviour changes

    It “simplifies” logic while moving it, and an edge case quietly disappears. Characterisation tests catch this; reading does not.

  2. 2
    Partial application

    Eleven of thirteen call sites updated. Compiles if the types are loose. Ask explicitly whether any were missed, and verify with a search.

  3. 3
    Scope 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.

  4. 4
    Losing 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:

Terminal
$ git switch -c refactor/split-dashboard# ... work, committing after each verified step ...$ git switch main        # if it went badly$ git branch -D refactor/split-dashboard

Abandoning 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.