This module installs the industrial migration architecture: LLMs propose, deterministic systems verify, and a human-owned charter defines done — so that at a thousand files, verification stops being a step you remember and becomes the system you run inside.
billing-service must move off a framework major version that goes end-of-life in a quarter. Forty thousand lines, sparse tests, and — Aisha, the team's staff engineer, already knows from Sana's onboarding notes — real behavior that outruns its documentation.
Not to start editing. To inventory. She runs a read-only agent across the module: public surface, side effects, implicit contracts, coverage map, danger list, a file:line citation demanded for every claim.
Fast, and mostly right. Mostly.
It lists apply_legacy_tax as "appears unused — no static callers." A tidy deletion candidate.
Aisha checks it against grep and a coverage run: the function is wired in by a config string, invoked only for one grandfathered enterprise account, and it fires on the first of every month.
An agent that saw only the code called it dead; the config and the calendar said otherwise.
That reconciled claim becomes a danger-list entry — and the charter (scope, invariants, verification plan, rollback, signed by Aisha) treats every "unused" verdict as a hypothesis until deterministic tooling and cross-repo callers rule it out.
Migrations are where senior engineers earn their title and lose their quarters: the framework upgrade nobody wants to own, the Java codebase that should be Kotlin, the monolith that should be four services, the ten thousand call sites of a deprecated API.
This is also where AI-at-scale engineering is most mature, because the companies with the largest codebases hit the problem first and published what worked: Google's FSE 2025 paper on LLM-driven migrations, Uber's Java-to-Kotlin program and test-generation systems, Airbnb's TypeScript migration built on codemods, Stripe's "minions" pattern for decomposing repo-wide changes across parallel agents. A consistent architecture runs through all of them — LLMs propose, deterministic systems verify, humans own the charter — and it is learnable.
What makes this module Advanced-tier is not tool sophistication; it's that at migration scale, verification stops being a step and becomes the system. One conference analysis put initial LLM refactor suggestions at up to 76% flawed or hallucinated; at ten files that's an annoyance you catch in review, at a thousand files it's an incident generator unless verification is built into the loop itself.
Every industrial-scale AI migration success follows the same shape: mechanical work to deterministic tools, ambiguous work to LLMs, every output through automated verification, and a human-owned charter that defines done. Skip any of the four and scale turns your error rate into your incident rate.
The defining risk of legacy work is that the code's real contract is bigger than its documented contract. Callers depend on undocumented ordering, exception types, side effects, even bugs. A migration that preserves the documented behavior and breaks the real behavior is a failed migration — and AI, which sees only what's in context, will cheerfully preserve exactly the contract you showed it. So the first artifact of any serious migration is not a diff; it's a charter, built from an agent's inventory of what actually exists.
Run a read-only agent pass (Claude Code, Copilot Chat over the workspace, Cursor — read-only is the discipline, not the tool) across the target module with an explicit reporting structure:
You are inventorying legacy module `billing/` ahead of a framework
upgrade. READ ONLY — propose no changes. Produce:
1. Public surface: every function/class used from outside the
module, with its callers (cite file:line for each claim).
2. Side effects: DB writes, network calls, file I/O, global/module
state, environment reads — per function.
3. Implicit contracts: ordering dependencies, exception types
callers catch, sentinel values, format assumptions in outputs
consumed elsewhere. Cite the consuming code as evidence.
4. Test coverage map: which of the above is exercised by existing
tests; which is uncovered.
5. Danger list: the 10 riskiest spots for a behavior-preserving
migration, ranked, with the reason.
The cite file:line requirement is the verification handle — every inventory claim is spot-checkable in seconds, and you should spot-check a sample, because an inventory with fabricated call sites is worse than none. Pair the agent's semantic read with deterministic ground truth where it exists: dependency graphs from jdeps/madge/pydeps, coverage reports, grep -r counts of the deprecated API. Where the agent's claims and the tools' numbers disagree, the tools win and the disagreement is itself a danger-list entry.
The charter is a one-to-two-page human-owned document the inventory feeds:
This is the document the Google migration work implies and Uber's program made explicit: at scale, the constraint is not writing transformed code, it's knowing when transformed code is safe. The charter is where that knowledge lives, and it is a T4 artifact in Common Core terms — AI drafts sections from the inventory, but the judgment calls are yours and signed by you.
An agent inventory is a map, not the territory. Spot-check citations, reconcile against deterministic tooling, and treat every "this function appears unused" claim as a hypothesis until reflection, dynamic dispatch, config-driven wiring, and cross-repo callers have been ruled out — Aisha's apply_legacy_tax was "unused" right up until the first of the month.
Your first 15 minutes on any legacy change: run a read-only inventory pass that cites file:line per claim, then reconcile one "appears unused" or "no callers" verdict against grep, coverage, and config wiring before you trust it — that reconciliation is where a monthly-tax function stops being a deletion candidate.
Feathers' classic rule — legacy code is code without tests, and you don't change it until you've pinned its behavior — predates AI and is amplified by it, in both directions. AI makes writing characterization tests dramatically cheaper, and AI-driven migration makes having them dramatically more necessary, because the whole point of a characterization suite is to catch the behavior changes your transformer didn't disclose.
Characterization tests differ from normal tests in one crucial way: they assert what the code does, not what it should do. If the legacy rounding is banker's rounding by accident, the test pins banker's rounding — bug-for-bug — because callers may depend on it, and because "fix the bug" is a separate, deliberate change with its own review, never a migration side effect.
The AI-assisted loop:
Write characterization tests for `calculate_invoice_totals` below.
Rules: assert CURRENT behavior only — run each input through the
actual function and pin the observed output. Do not assert what
you believe correct behavior to be. Cover: representative inputs
from these production samples [attached, redacted], boundaries
(empty line-items, zero quantities, negative adjustments, maximum
sizes), and every distinct code path you can identify. Flag any
output that looks like a bug — but pin it as-is in the test.
Then verify the suite two ways:
mutmut (Python) or Stryker (JS/TS) — both free — and treat surviving mutants as holes; a characterization suite that green-lights mutated code will green-light a broken migration identically.Uber's investment in AI test generation ahead of its migration program reflects exactly this sequencing: the test suite is migration infrastructure, built and validated before the migration starts.
Characterization tests pin what the code does, bugs included. "Fix while migrating" is how a diff nobody can review is born — one change class per diff: behavior-preserving transform, or behavior change, never both.
With the charter written and behavior pinned, the breaking-change upgrade — framework major version, library API overhaul — becomes the canonical agentic task, because it has what agents need most: a deterministic feedback signal. Compile errors, type errors, test failures, and lint output tell the agent, precisely and incorruptibly, whether it is done.
The loop structure, runnable in Claude Code, Copilot's coding agent, or any harness with shell access:
Where the ecosystem has deterministic migration tooling — OpenRewrite recipes for Java, official codemods shipped by the framework — run those first and give the agent only the residue. Which is the general principle the next section makes explicit.
Watch the loop's convergence. Failure count should fall monotonically; an agent oscillating — fixing A breaks B, fixing B breaks A — has misunderstood something structural. Stop it, find the misunderstanding (usually a semantic change it treated as mechanical), fix the plan, restart. Twenty more iterations of oscillation produce a mess, not a migration.
The most load-bearing finding from industrial migrations is a division of labor. Airbnb's TypeScript migration ran on codemods — AST-transform scripts (jscodeshift, ts-migrate) that apply exact, reviewable rules across thousands of files. Uber's Java-to-Kotlin program and Google's FSE'25 migration work layered LLMs onto deterministic infrastructure rather than replacing it. The pattern:
Codemods take the mechanical 80%. Renames, import rewrites, signature adaptations, annotation swaps — anything expressible as an AST rule. Codemods are deterministic (same input, same output), reviewable as rules (audit one transform, trust ten thousand applications), and hallucination-free. And here the LLM has a second job besides transforming code: writing the codemod itself. Generating a jscodeshift or LibCST transform script is a small, testable code-generation task — verify the script on fixture files, then apply it mechanically at scale. This inverts the risk profile: instead of trusting ten thousand LLM outputs, you verify one LLM output and trust the deterministic engine.
Write a jscodeshift codemod: replace `logger.warn(msg, err)` calls
with `logger.warning({ msg, error: err, ctx: getRequestContext() })`.
Handle: err absent (omit error key), msg as template literal,
logger imported under an alias. Include fixture tests for each
case plus one that must NOT transform: a local object with its
own `warn` method.
LLMs take the ambiguous 20%. The call sites the codemod couldn't safely match: dynamic patterns, cases needing semantic judgment (does this null mean "absent" or "empty" for the new nullable type?), idiom translation that mechanical rules mangle. Have the codemod tag what it skips; the tagged residue becomes a worklist for agent or human attention, each item verified individually. Google's published approach is exactly this mix — LLM changes embedded in a pipeline of deterministic validation — and Uber's writeups describe the same layering across their developer-experience tooling.
The hybrid's economics explain its universality: pure-codemod migrations stall on the long tail of special cases (the last 20% costs more than the first 80%); pure-LLM migrations drown in per-file verification cost. The hybrid gives each problem to the tool whose failure mode you can afford.
For a repo-wide change too semantic for one codemod and too large for one agent session — swap the logging API across 40 services, retire a config system, apply a security pattern everywhere — the industrial answer is decomposition across parallel agents, popularized by Stripe's internal "minions" harness. The pattern has three parts, and the third is the one that separates a harness from a hope:
Decompose into independent, verifiable subtasks. Partition by module/service/directory such that each subtask (a) touches a disjoint file set — no merge conflicts by construction, worktree or branch isolation per task, (b) has its own local verification (that module's tests + build), and (c) carries an identical, precise task spec. Writing that spec is the senior-engineer work: it inherits the charter's invariants and states exactly what "done" means for one slice.
Fan out. Run the subtasks across parallel agents — Claude Code sessions in git worktrees, a fleet of Copilot coding-agent issues, or a scripted harness. Parallel agents don't make any single task smarter; they make the wall-clock of a thousand-file change survivable and each failure independently disposable — a subtask that goes sideways is regenerated alone, not untangled from a monolithic diff.
Aggregate through verification. Each returning subtask passes a uniform gate before a human sees it:
# verification aggregator (sketch)
for task in tasks/*; do
git -C "$task/worktree" diff --stat # blast radius vs. expected file list
scripts/check_scope.sh "$task" # only allowed paths touched; no test files weakened
(cd "$task/worktree" && ./build && ./test) # local green
grep -rn "$OLD_API" "$task/expected_paths" && echo "INCOMPLETE"
done
# then: aggregate report → human review queue, ordered by risk
The aggregator checks scope (files touched match the partition — an agent that wandered gets rejected wholesale, not merged carefully), completeness (zero remaining instances of the old pattern in its slice — a deterministic grep, not the agent's word), and local green. What reaches the human queue is a stack of small, pre-verified, uniformly-shaped diffs — reviewable with the M5 large-diff toolkit at a rate that would be impossible for one giant PR. The human review is still real review; the harness's job is making sure it's spent on judgment, not on catching scope drift a script could catch.
The minions pattern is the module's ideas composed: charter defines the change, characterization and existing tests give each slice its local signal, subtask specs are miniature charters, and the aggregator is equivalence-checking industrialized. If any ingredient is missing, parallelism just multiplies the missing ingredient's failure rate.
Decomposing a monolith is a migration measured in quarters, and its classic pattern — the strangler fig, per Fowler's literature: grow the new system around the old, route traffic incrementally, retire the old when it's hollow — lives or dies on seam selection: finding the boundaries where a capability can be severed with minimal coupling damage. Shopify's checkout extraction is the canonical modern case study of doing this deliberately. Seam-finding is archaeology across an entire codebase, which makes it an outstanding agentic-analysis task — with the outputs verified before a single service boundary is drawn.
The unglamorous sibling of migration is deletion, and it has a rule: no deletion without cited evidence. Deterministic tools produce candidates — vulture (Python), knip/depcheck (JS/TS), coverage-over-time data — and the AI's role is building the evidence dossier per candidate: no static references (cite the searches), no dynamic-dispatch or reflection paths (cite why not), no config/framework wiring, no external callers (for anything ever exposed), last meaningful change and by whom. The dossier format makes the deletion PR reviewable in minutes and reversible by construction (deletion-only commits, one candidate cluster per PR). AI without the evidence rule produces confident deletions of things that were load-bearing on Thursdays; the rule converts cheap generation into cheap diligence.
Check yourself: During a migration, the AI-generated characterization suite is green and you notice it pins a rounding behavior that looks like a bug. Do you fix the bug while you're in there? Why or why not?
pydeps/madge output. Find the one claim the agent got wrong (there is at least one seeded) and write the danger-list entry it should have produced.calculate_invoice_totals (which contains a pinned-on-purpose rounding bug). Run mutation testing; your suite must kill ≥80% of mutants, and your tests must pin the bug, not fix it. Artifact: suite + mutation report + the one-line comment marking the pinned bug.The playbooks for this module carry the templates you'll reuse for years: the agentic-inventory prompt and charter skeleton, the characterization-test rules block, the upgrade-loop harness checklist, codemod fixture patterns, the subtask-spec and verification-aggregator templates for parallel decomposition, the seam-report and ACL-draft structures, and the deletion-dossier format. Do this at work this week → pb-21 Legacy Comprehension to Migration Charter: Inventory Before You Touch Anything (Aisha's charter, from agentic inventory to signed scope), pb-22 Characterization Tests Before Refactoring: Pin Current Behavior with AI Speed (mutation-checked behavior pinning, bugs included), and pb-24 Repo-Wide Pattern Refactor via Decomposition and Parallel Agents (the minions pattern with a verification aggregator). Two of this track's most substantial graded labs live here: the legacy-upgrade combo (characterization suite, mutation-checked, then an agent-driven upgrade to green) and the migration-decomposition harness (parallel agents plus your own verification aggregator). Both assume the M4 equivalence disciplines and the M5 review checklist as prerequisites — schedule them after those modules' labs, and expect the decomposition lab to take a full session.