Large Mechanical Changes
Some changes are too large to read site by site and too uniform to need it: a library's new major version renames an option used in 47 places, or the team renames a concept across five modules. Asking the agent to edit every file is the slowest and least checkable way to do them. Asking it to write the transformation, then reviewing the transformation once and the result by count, sample and tests, is how a 47-site change gets reviewed in twenty minutes.
The shift is from reviewing edits to reviewing a rule. Forty-seven hand edits are 47 separate decisions, each of which could differ from the others. One script is one decision applied 47 times. The second can be checked in a way the first cannot, and this page is about how.
The Cragmoor Upgrade
The booking service talks to two outside services, the payment provider and the email sender, through one HTTP client library. Its new major version renames the timeout option and changes its unit from seconds to milliseconds. The service calls the client in 47 places across five modules. The rename is the easy part; the type checker or the first test run will find a missed one. The unit change is the trap. A site that is renamed but not converted keeps its old number, now read as milliseconds, and waits a thousand times shorter: a 10-second payment timeout becomes 10 milliseconds, and every slow response from the provider becomes a failed refund.
--- a/refunds.py +++ b/refunds.py @@ -4,4 +4,4 @@ def send_refund(refund): - return http.post(PAYMENTS_URL, json=refund, timeout=10) + return http.post(PAYMENTS_URL, json=refund, timeout_ms=10_000)
--- a/refunds.ts
+++ b/refunds.ts
@@ -3,5 +3,5 @@
const PAYMENTS_URL = "https://payments.example/refunds";
export async function sendRefund(refund: { cents: number }) {
- return http.post(PAYMENTS_URL, refund, { timeout: 10 });
+ return http.post(PAYMENTS_URL, refund, { timeoutMs: 10_000 });
}--- a/Refunds.java
+++ b/Refunds.java
@@ -2,7 +2,7 @@
static final String PAYMENTS_URL = "https://payments.example/refunds";
public static void sendRefund(Refund refund) {
- Http.post(PAYMENTS_URL, refund, Http.Options.timeout(10));
+ Http.post(PAYMENTS_URL, refund, Http.Options.timeoutMillis(10_000));
}
public record Refund(long cents) {}Each site needs two changes that must travel together: the new option name and a value multiplied by 1,000. Miss the first and the code fails loudly. Miss the second and it fails only under load, in production, at the moment the provider is slow.
Three Ways to Do It
There are three ways to make the change, and they differ less in effort than in what you can review afterwards.
For this upgrade the script wins. Hand edits are the worst choice for exactly the reason Chapter 4 gave: the session fills with 47 similar diffs, attention to the early instructions fades, and somewhere around site thirty an edit renames the option and forgets to multiply. The toolchain would be ideal for the rename alone, but the rename is not the risk.
Reviewing the Script, Not the Sites
Ask the agent for a transformation script, not for the edits. At Cragmoor it is under 30 lines, readable in full. It states the rule in one place: where the option is passed a literal number of seconds, rename it and multiply the number by 1,000. It also states what it will not do: any other mention of the old option is printed for a person to handle, not guessed at. Review the script as code, with the same care as any production change, because it is about to write 47 of them.
Then run it on two files, read those two diffs completely, and only then run it everywhere. Two files are enough to catch a wrong rule; the remaining 45 are the same rule again. This is the asymmetry the whole page rests on. Reading the rule and two of its outputs is a complete review of a script. Reading two of 47 hand edits is a review of two edits.
A script has one more advantage on a codebase that does not stand still. While the upgrade branch waits for review, booking and waitlist keep changing on the main branch, at 64 and 41 commits a year. After a rebase, new call sites may have appeared. Rerun the script and they are converted by the same rule, with the same count check afterwards. Hand edits would have to be repeated by hand, by someone who has to remember to look.
Count, Sample, Search
The generated diff is checked three ways, and each catches a different failure. The count: before the upgrade the service had 47 call sites; the script rewrote 43 and listed 4 it could not handle, which adds up. A different number would mean the script missed sites or matched something that was not a call site. The sample: read five sites chosen at random in full, plus every site the script flagged as unusual. The search: after the hand fixes, a search for the old option name as a whole word must return nothing. Then the type checker and the full test suite run, as Chapter 8 arranged.
$ git log --oneline -3
25654f9 upgrade: hand fixes for the four sites the script listed
6aab596 upgrade: run the timeout script
4721456 upgrade: script that renames the timeout option and converts to ms
$ git show --shortstat --format= 6aab596
19 files changed, 43 insertions(+), 43 deletions(-)
$ git show --shortstat --format= 25654f9
4 files changed, 7 insertions(+), 7 deletions(-)
$ git grep -ci "http.post" -- src | awk -F: '{ n += $2 } END { print n }'
47
$ git grep -nw timeout -- src; echo "exit $?"
exit 1
Read the output as a checklist: 43 one-line changes from the script, 4 sites fixed by hand, 47 call sites in total, and a search for the old name that finds nothing and exits with status 1, which is how a search tool reports zero matches. Each number is something you can state in the pull request and a reviewer can rerun.
The Sites That Don't Fit
A script handles the sites that look like the rule and nothing else, which is its strength and its limit. Call sites that pass the timeout through a variable, a wrapper function or a configuration file are where a mechanical rule is wrong. So the agent is asked for a list of every site it could not transform mechanically, and every site on that list is read in full.
At Cragmoor there are four. One builds a dictionary of options and passes it through. One is a small retry wrapper that takes the timeout as a parameter and passes it on. One computes the timeout from whether the email is urgent. And one reads the timeout from configuration, where it is stored in seconds. That last one is where a careless fix does the most harm: multiplying at the call site would work until someone else reads the same setting. The conversion belongs in one place, the configuration value itself, renamed so that its unit is in its name.
Commits That Show What Was Mechanical
The change lands as three commits, visible in the log above: the script, the script's output and the hand fixes. The reviewer reads the first and the third in full. The second, 19 files and 43 lines, is checked by count and sample, because the first commit already showed the rule that produced it. A single mixed commit would force the reviewer to treat all 47 sites as hand-written, since nothing would say which ones were. The same habit from Chapter 9 applies with more force here: history that says which changes were mechanical is history a reviewer can trust.
- "If the agent edits every file, every file was edited the same way." Forty-seven generated edits are 47 chances for one to differ quietly, the Chapter 7 failure, and sampling five will not find the one that did. A script applies one rule 47 times.
- "Sampling a few sites is a sloppy review." Sampling is sound when the change is produced by one rule. It is unsound only when the sites were edited one by one, which is why the method of making the change decides the method of reviewing it.
- "The tests passing proves the upgrade." The tests exercise the call sites they reach. A timeout matters only on slow responses, which most tests never produce, so the zero search and the list of irregular sites carry the proof.
- "The toolchain's rename handles any rename." It handles what its symbol information resolves. Names in strings, configuration files, documentation and reflection are invisible to it, and so is a change of unit.
- Ask the agent for the transformation script, not the edits. One rule reviewed once beats 47 edits reviewed once each.
- Check the generated diff by count, by sample and by a search for the old name. Each catches a failure the others miss.
- Ask for the list of sites that could not be transformed mechanically, and read every one. The irregular sites are where the bugs are.
- Commit the script, its output and the hand fixes separately, so the reviewer reads in full only what was not mechanical.
Knowledge Check
The team renames the concept "session" to "class" in function names across five modules; nothing else changes. Which approach fits best?
- The editor's refactoring tools, since a pure rename of symbols is what they resolve completely
- The agent hand-editing every file, since the rename touches only names and so cannot go wrong
- A transformation script, since a script is always safer than any other approach to a large change
- A team-wide freeze while one person renames by hand, so that no two changes can conflict at all
The script reports that it rewrote 41 call sites and listed 4 for manual fixes. Before the upgrade there were 47 call sites. What does that tell you?
- Nothing much, since a difference of two is within normal variation for a scripted change
- That two of the four listed sites were counted twice, so the real total is still 47 sites
- That two call sites were neither rewritten nor listed, so the script's matching missed them
- That the old version had two dead call sites, which the script was right to leave untouched
Why is reading a random sample of five sites valid for the script's output but not for 47 hand edits?
- Because scripts are written by the agent, and the agent is more careful in code than in hand edits to files
- Because the script applies one rule everywhere, so a sample checks the rule; hand edits are 47 decisions
- Because hand edits leave no record in the history, so their sites cannot be found again later for sampling
- Because five sites out of 47 is more than ten percent, which is enough for any kind of change
Where is a rename tool blind?
- To call sites inside functions that are used only by the test suite, not by production code
- To calls made in modules other than the one where the renamed symbol was first defined
- To any symbol whose name is shorter than three letters, since those clash with keywords
- To names in strings, configuration files, documentation and reflection, and to a change of unit
Why is the unit change from seconds to milliseconds more dangerous than the rename?
- Because unit changes require the full test suite to be rerun, while a rename does not
- Because the new library rejects values above 1,000, so most of the old timeouts fail on start-up
- Because a missed rename fails loudly, while a missed conversion runs and fails only on slow responses
- Because milliseconds cannot be stored in the same configuration fields as seconds were before
You got correct