Why refactoring is the ideal case
Every difficulty this course has described comes from the same root: you cannot cheaply tell whether the output is right. Refactoring removes that difficulty entirely. The requirement is behave identically, the reference implementation is in git, and the test suite is the oracle.
So mechanical change is where generated code is safest, most useful, and least discussed. Renaming across a repository. Extracting a module. Converting callbacks to promises to async. Migrating a test framework. Replacing a deprecated API across four hundred call sites. Splitting a 3,000-line file. Introducing a type where there was none.
All of these were previously bounded by typing speed and tedium, which is exactly the constraint that lifted.
Deterministic tools first
Before reaching for generation, check whether the change is expressible as a pattern. If it is, a syntax-aware rewriting tool will do it perfectly, on every occurrence, reviewably, and in seconds.
# ast-grep: structural search and replace, language-aware
sg --pattern 'logger.warn($MSG)' --rewrite 'logger.warning($MSG)' -l py -U
# comby: works across many languages
comby 'requests.get(:[url])' 'http.get(:[url])' .py -iOther free options in the same family: jscodeshift for JavaScript and TypeScript, libcst and bowler for Python, OpenRewrite for Java, gofmt -r for Go, rustfmt alongside cargo fix for Rust.
The advantage is not speed. It is that a deterministic tool applies exactly one transformation and cannot invent a variation on occurrence 217. When you review a codemod diff you check the transformation once and the count; when you review a generated diff you check every occurrence, because they may differ.
The rule of thumb: if it is a pattern, use a pattern tool; if it needs judgement, use a model; if it needs judgement in four hundred places, do it in four hundred small steps or reconsider.
The discipline that makes it safe
One commit, behaviour-preserving, verified.
- Confirm the tests are green and meaningful before you start. A refactor verified by a weak suite is not verified. If the mutation score is low, this is the moment to add a few real tests, before the refactor, so they test the old behaviour.
- Make the change with no behaviour modification at all. Not one small improvement while you are in there. Not a fixed bug. Nothing.
- Run the suite. Run a differential check if you have one.
- Commit alone, with a message saying it is behaviour-preserving.
- Only then make the behaviour change, in its own tiny commit.
The payoff for the discipline arrives at review. A reviewer can read “pure refactor, tests unchanged and green” and skim, then read twenty lines of real change carefully. If the two are mixed, they must read everything carefully, which means in practice they will read nothing carefully.
The payoff arrives again when something breaks in a fortnight and git bisect lands on a commit that either changed behaviour or did not. That is a much better position than landing on a commit that did both.
Large migrations without a large diff
For changes too big for one review (a framework upgrade, an API replacement across a monolith) the strangler pattern still applies and works well here.
- Add the new implementation beside the old one.
- Route a small share of calls to it behind a flag.
- Compare outputs, in shadow mode if the operation is read-only.
- Move the share up.
- Delete the old one, and delete the flag.
Each step is small, reviewable and reversible. The whole migration might be twenty pull requests over two months, and every one of them is safe. This is slower than a single enormous change and it is the only approach with a good outcome distribution.
What to be careful about
A refactor that quietly changes behaviour is the failure mode, and it is subtle by definition. Common instances: changing the order of side effects, converting a stable sort to an unstable one, altering exception types, changing when something is evaluated from eager to lazy, and normalising a value “harmlessly” on the way through.
The defence is a differential test rather than a careful read, because a careful read is exactly what these defeat.
A refactor is not free even when it is correct. It rewrites history for git blame, invalidates people’s mental maps, and conflicts with every open branch touching those files. Doing it because it is now cheap, rather than because something needed it, is a real cost imposed on everyone else. Cheap to produce is not the same as free to absorb, and that sentence is most of what this course has been about.
Every lesson here is open. Your progress is recorded once you pass the AI at Work exam.
Check your understanding
A single pull request converts a module to async and fixes a long-standing off-by-one in the same commit. Tests pass. Why is this hard to review even though the change is correct?
Pick the one you would defend. Your answer stays private.
Stuck on something?Ask Larissa, your AI tutor
