中文
Methodology · Claude Code · 2026-07-16

Large-scale migration is a process design problem

The core idea is one line: you don't fix the code. You fix the loop that keeps producing it.

Primary source
How Anthropic runs large-scale code migrations (official, claude.com/blog)
Two real cases
Bun, ~1M lines Zig → Rust (two weeks) · an internal tool, Python → 165K lines TypeScript (a weekend)
What this covers
What each of the six stages does · why they are one loop · how the two migration roads diverge
— The argument in brief

The risk of migration has changed shape: the cost of failure went from years and a half-finished system to tokens and a few days.

The threshold

A migration no longer needs an existential reason. A year of recurring memory bugs, or one chronic build bottleneck, can now be enough to justify it.

The method

Treat the system as an assembly line: small models implement, large models review, compilers and tests judge. Humans design the line and write the rules.

The opening choice

Preserve the old structure, or redesign it? That single decision shapes the rulebook, where the compiler sits, and how the whole pipeline runs.

§I
Why it's viable now

The math has changed

A language migration rewrites a production system without changing what the system does. Historically, that made it one of the most expensive bets an engineering organization could take.

The difficulty was never just the number of lines. For months or years, the team had to maintain two implementations. Product work slowed, and senior engineers were pulled away from the roadmap. Even after years of effort, the replacement might reach only 90% behavioral parity, and the remaining 10% was often the hardest part: obscure edge cases, undocumented assumptions, and production behavior no test had captured. That risk is why teams tolerate memory bugs and slow builds instead: an imperfect system in production can feel safer than a rewrite that may never finish.

Recent migrations at Anthropic suggest the economics have shifted. In the month leading up to its July 2026 report, engineers used Claude Code, Fable 5, Opus 4.8, and dynamic workflows to migrate ten packages ranging from tens to hundreds of thousands of lines. Two of them show the range.

Jarred Sumner, Bun's co-founder and an Anthropic engineer, migrated Bun from Zig to Rust: roughly one million lines across 1,448 files in under two weeks, with 100% of Bun's existing test suite passing in CI before merge. Mike Krieger, co-lead of Anthropic Labs, took a different path, migrating a Python codebase into about 165,000 lines of TypeScript over a weekend, using hundreds of agents, eight phase gates, and three rounds of adversarial review.

The old cost
Four years · $3–4M
The typical engineering cost of a million-line migration, carrying the risk of two live codebases and a rewrite that might never finish.
The new cost
Two weeks · ~$165K
Bun's API cost (5.9B uncached input + 690M output tokens). If the process is wrong, the worst case is deleting the branch and trying again.

The cost has not disappeared; the risk structure has. More importantly, failure became cheaper: a failed agent-driven run can usually be discarded, and the team improves the process and reruns it. Mike's project began with slow compilation. The Python toolchain required about eight minutes per platform and roughly thirty minutes across the release matrix. After the TypeScript port, the same compile took about two seconds, the binary started six times faster, and a separate deployment pipeline was no longer necessary.

§II
The premise of the method

Fix the process, not the code

A conventional migration proceeds file by file: an engineer opens a source file, rewrites it, compiles, fixes errors, and moves on. That approach assumes a human can supervise every unit of work.

At a million lines, that assumption fails. The scalable alternative resembles an automated assembly line. Small models perform high-volume translation in parallel, large models review the results adversarially, and compilers, tests, and output diffs act as the judge. Humans design the line, define its rules, and decide what evidence counts as success. They do not turn every screw themselves.

This matters most when the same error appears repeatedly. If twenty generated files mishandle the same pattern, fixing those twenty files by hand only hides the weakness in the process. The correct response is to add one rule to the rulebook and regenerate the affected batch. The generated code is treated as disposable; the workflow that produces it is the durable asset.

You don't fix the code. You fix the process — the loop — that produced the code.

Anthropic · 2026-07-16
§III
The one hard prerequisite

Build the judge first

An automated workflow can run for hours or days only if it can answer one question reliably: is the task complete? That answer comes from the judge.

The judge may be a compiler, a test suite, an output diff, or all three. Whatever its form, it must evaluate the old and new systems on equal terms. Without that common standard the loop has no trustworthy exit condition, and existing tests do not automatically provide it: tests written in the old language may call private functions or depend on internal types, describing an implementation rather than observable behavior.

Building the judge, in three steps

First, classify. Claude sorts the existing tests into those that capture externally observable behavior (command output, API results, file contents) and those coupled to internal structures.

Second, rewrite. The external tests become assertions that run against both implementations. Adversarial review agents then check that the conversion did not weaken any assertion.

Third, test the judge itself. The correct old implementation must pass; deliberately broken versions must fail. If an injected defect escapes detection, the suite is not yet a judge.

The two migrations started from very different positions. Bun had an unusual advantage: its test suite was written in TypeScript, a third language independent of both Zig and Rust, so the same tests ran unchanged against the Rust port. Mike's Python project had no equivalent, so his team built a parity harness of seven real-world scenarios, ran them against both the Python and TypeScript versions, and diffed the outputs, treating any difference as a defect.

Most projects will look more like Mike's than Bun's. That is not a blocker. If no cross-language suite exists, Claude can help build one, and the old system remains the ground truth.

§IV
The idea that makes the rest simple

Six stages, one loop

The process is described as six stages, but they are not six unrelated techniques. They are six passes through the same loop. What changes between stages is the judge.

Each pass follows the same shape. A script scans the filesystem for unfinished work and divides it into a queue. Small models take items and implement them in parallel. Two larger models review each result from different adversarial angles; if they disagree, a third agent resolves it. A machine judge then decides pass or fail. A passing item moves to the next stage; a failing one returns to the queue.

One pass through the loop (all six stages share it)
Queue
Dispatch work
A script scans disk for unfinished items and slices them into a batch
Implement
Small models, in parallel
Handle the high-volume work
Review
Large models, adversarial
Two angles; a third agent breaks ties
Judge
A machine decides
Compiler / tests / diff, not a human
Pass → move to the next stage, or mark done (an output file lands on disk)
Fail → back to the queue; when an error recurs, fix the rule and regenerate that batch
Two rules: generated code is never hand-patched · "done" means an output file exists on disk, so the run is resumable

Most of the difficult human judgment is concentrated in the first two stages. Once the rules are explicit and stress-tested, the rest is largely a matter of burning down mechanical queues.

§V
Walking through the six stages

The first two take judgment, the rest burn the queue

1
Establish the foundations
Three artifacts, and the order matters. The rulebook is the shared instruction set for translator agents (a type/idiom mapping table when preserving structure, a design document when redesigning). The dependency map decides which files come first and which batch together. The gap inventory records what the target language requires that the source never made explicit. The rulebook must come first, because a gap is defined as what the rulebook's defaults do not cover.
2
Stress-test the rules
Before translating the full repository, run a miniature migration. On three files, one agent translates by the rulebook, one "like a senior engineer," and a third diffs the two versions to reverse-engineer missing rules. Jarred's stress test found two issues that would otherwise have hit all 1,448 files. Every file made here is discarded — the goal is to refine rules, not to make progress.
3
Translate the repository
Small models fan out across the dependency map (Mike ran 12 Sonnet subagents in the main phase); large models review. Anything a translator cannot do confidently gets a // TODO(port) marker for stage 4. When reviewers keep catching the same mistake, the process adds a rule and regenerates that batch; the rulebook only grows, and the code is never hand-repaired. Judge: review agents (compiler placement, see below)
4
Compile
The compiler's error list becomes the judge. Fixer agents work it in parallel with adversarial review; build, fix a batch, build again. The danger is treating every error as independent: after resolving cyclic imports that Zig's lazy compilation had tolerated, Jarred hit thousands of Rust module errors, and the real fix was classification logic deciding which dependencies to delete, move, or restructure. Judge: the compiler's error list
5
Run
A clean build only proves the compiler is satisfied. The next judge is a smoke test that starts the program and exercises a minimal set of operations. Crashes are grouped by probable root cause and handed to adversarial subagents, rather than processing stack traces one at a time. Judge: smoke-test crashes
6
Match behavior
The final judge is the one built before the migration. Files are sharded across agents. To stop many agents triggering identical builds, a build daemon is the only process allowed to rebuild the binary: fixers submit patches, the daemon batches them, rebuilds once, reruns the affected tests, and feeds results back. The most expensive operation is serialized while analysis and fixes stay parallel. Judge: the test suite / output diff
What a "gap" is: two examples

The gap inventory records what the target language requires but the source never stated. These two snippets make it concrete.

Zigold · memory by hand
fn readConfig(a: Allocator) ![]u8 {
    const buf = try a.alloc(u8, 4096);
    return buf; // who frees it? only a comment says
}
// A caller that forgets to free still
// compiles; the leak shows up at runtime.
Rustnew · enforced by compiler
fn read_config() -> Vec<u8> {
    vec![0u8; 4096]  // ownership moves, auto-freed
}
// Use a moved value, double-free, or an
// invalid lifetime? None of it compiles.

Zig → Rust: the gap is memory ownership. Zig relies on a human to free memory; Rust requires the relationship to be provable by the compiler. That implicit knowledge must go into the gap inventory first.

Pythonold · no contract declared
def register(handler):
    handler.setup()
    return handler.run({"retries": 3})
# Any object with .setup()/.run() works.
# Which ones actually pass through? Read
# the whole codebase to find out.
TypeScriptnew · contract stated first
interface Handler {
  setup(): void;
  run(o: {retries: number}): Promise<Result>;
}
function register(h: Handler) { ... }
// Without the written contract, it
// won't type-check.

Python → TypeScript: the gap is interfaces and contracts. Python accepts any object with the right methods; TypeScript makes you declare the Handler interface — the shape the Python program left implicit.

Mike's project went one step further at stage 6: after the seven scenarios matched, Claude designed its own end-to-end suite and ran it autonomously overnight, finding failures and fixing them for four consecutive nights. It caught edge cases no scenario list would have predicted.

§VI
Same machine, two roads

Preserve the structure, or redesign it

Bun's migration and Mike's used the same general stages. Their most important difference was the opening decision: preserve the existing structure, or redesign the system.

That decision changes nearly every artifact in the workflow, from the shape of the rulebook to how many times the whole line runs.

Dimension
Preserve structureBun · Zig → Rust
Redesigninternal tool · Python → TypeScript
Rulebook form
A type / idiom mapping table
A full design document (structure is rebuilt)
Gap inventory timing
Built before translation
Audited and backfilled after the first pass
Where the compiler sits
Banned from the loop, deferred to stage 4 (cargo takes minutes)
Inside every loop (tsc checks a unit in seconds)
Stage-2 stress test
Two translations of one file, compared line by line
Attack the design document + one throwaway end-to-end run
Full-pipeline runs
With review and gates, essentially once
Three full runs; the first two discarded, only the third kept

Under manual economics, running the whole thing three times sounds wasteful. Under agent-driven economics it can be rational: the first two runs exist to improve the design, the rules, and the orchestration, not to ship code. The same six stages still have to be re-arranged around target structure, compile cost, and test conditions.

§VII
Practices that make the loop work

Review adversarially, verify mechanically

01
Watch patterns, not single failures
A one-off error belongs to a fixer agent. The fifth occurrence of the same error belongs in the rulebook — it usually signals a flaw in the rules, the dependency map, or the design document.
02
Adversarial review, mechanical verdict
Don't ask a reviewer to "check this code." Tell it to assume the code is wrong and prove every claim from source, target, tests, or docs. The final verdict still belongs to the compiler, tests, or diff.
03
Don't use the largest model for everything
Token spend concentrates in loops. Smaller models handle high-volume implementation; save the largest model for review, tie-breaking, and writing rules that hundreds of later tasks will follow.
04
Front-load the human hours
The rulebook and the stress test consume the most judgment, because mistakes there multiply across the repository. After that, the work becomes progressively more mechanical.
§VIII
After the migration

The payoff, and the new risk model

Bun's Rust version is now in production. It is not free of trade-offs: about 4% of the Rust code uses unsafe, mostly single-line pointer operations at C and C++ boundaries. But the improvements are measurable.

6745 → 609 MB
In a benchmark repeating the build 2,000 times, memory use fell sharply; every leak the team's tooling can detect was fixed.
19% smaller
The binary on Linux and Windows; real workloads also ran 2–5% faster.
8 min → 2 s
Mike's per-platform compile; the binary starts 6× faster, and a deployment pipeline was retired.

The larger change is the risk model. The old rewrite nightmare was a team spending years to reach 90% parity, then trapped between finishing and reverting. The new process concentrates most of the downside in tokens, API cost, and a few days of runtime: if the output is poor, delete it, repair the judge or rulebook, and run the line again. A recurring defect no longer has to be chased through a thousand files — it can be traced back to the rule that produced it.

§IX
Closing

What's worth reusing is the mindset

This does not make migration effortless, and it does not mean copying a fixed script. Every codebase has a different judge, gaps, dependencies, and target structure, so the first use of Claude should be planning the migration itself. Anthropic has published a migration starter kit, but it is a generalized template rather than the exact machinery the Bun and Python projects ran on; a separate code-modernization plugin targets legacy modernization and framework upgrades rather than language ports.

What's worth reusing is not a template but an engineering stance: don't treat a large migration as countless code edits. Build a loop that keeps producing, questioning, and judging; write human judgment into the rules and machine certainty into the gates; and let the pipeline absorb the scale.