Failed migrations usually fail at step zero: nobody actually knew what the module did. Google's and Uber's published LLM-assisted migration work shares one shape — a deterministic-plus-LLM inventory phase that catalogs dependencies, side effects, and implicit contracts before any code changes.
Tier: Core · Workflow area: Refactoring & Migration
An agent can build that inventory in hours instead of weeks; your job is to force it to cite evidence and to sign off on the resulting charter.
Skip for modules with current docs, high test coverage, and a living owner — just read the docs.
pydeps, madge, jdeps), coverage report if tests exist, and grep.Pull deterministic facts first.
madge --json src/legacy/orders > /tmp/deps.json # who orders/ imports
madge --json src | jq 'to_entries[] | select(.value[] | contains("legacy/orders"))' # who imports orders/
grep -rn "os.environ\|settings\." src/legacy/orders | sort > /tmp/config-reads.txt
Tool-extracted graphs are the skeleton the LLM must hang claims on — never let the model be the source of the dependency list, only its interpreter.
Run the agentic exploration with an evidence rule.
Explore src/legacy/orders (read-only). Build an inventory with these
sections; every claim must cite file:line, and unverifiable claims must be
marked ASSUMPTION:
1. Entry points: every route/handler/job/CLI/signal that invokes this module
2. Outbound side effects: DB writes (which tables), network calls, file I/O,
queue publishes, emails, global/state mutation
3. Implicit contracts: behaviors depended on but not in any interface —
ordering guarantees, exception types callers catch, dict shapes, magic
values, timezone/locale assumptions
4. Config & environment: every env var, setting, and feature flag read
5. Data ownership: tables/collections this module is the sole writer for,
vs shared with [list from deps.json]
6. Test reality: what existing tests actually pin vs what is untested
7. Surprises: anything that contradicts the module's apparent purpose
Section 3 is the one that saves you: implicit contracts are what break in migrations, and they are exactly what no documentation mentions.
Audit the inventory against the deterministic facts. Cross-check the agent's entry-point and dependency claims against deps.json and your grep output. Spot-check 5 random file:line citations — do they say what is claimed? Follow every ASSUMPTION tag: each becomes either a verified fact (you checked) or an open risk (into the charter). An inventory that survives this audit is trustworthy enough to plan on.
Probe the scariest implicit contracts. For the top 3–5 implicit contracts, get evidence beyond reading:
You claimed callers depend on process_batch() returning items in insertion
order (orders/batch.py:112 cited). Write a minimal script/test that proves
or disproves: (a) the function guarantees this today, (b) at least one
caller breaks if order changes.
Run it. Contracts proven by executing code go into the charter as constraints; the rest stay risks.
Draft the migration charter. From the audited inventory, have the model draft — then you edit — a one-to-two-page charter: scope (in/out), the invariant list (implicit contracts that must survive, each with its evidence link), risk register with mitigations (usually: characterization tests — see pb-22), sequencing (strangle order, see pb-25 if extracting), and explicit no-go conditions ("if the shared-table write in section 5 can't be isolated, stop and re-plan"). Circulate to the two longest-tenured engineers for a "what did we miss" pass — tribal knowledge still beats any agent.
The deliverable is not understanding in your head — it is a cited, audited inventory plus a charter that a teammate could execute. If the inventory can't say who calls the module and what breaks when behavior changes, you are not ready to migrate.
git log archaeology.