Parse $ARGUMENTS for the migration: <from> (old API/library/version) and <to> (new one). Accept any shape — migrate X to Y, arrow form X->Y, X to Y@2, a library swap where <to> is a different package not a version bump (moment to date-fns), or a from-only removal (drop moment.js, where <to> is its modern replacement or plain deletion). If <to> is a bare version like 18, resolve it against the current installed major. If $ARGUMENTS is empty or <from> cannot be resolved to something in the manifest, ask which migration to perform and stop; do not guess.
Scope first, code second
- Working context: start from a clean tree (
git statusmust be clean — stash or commit any WIP first). Cut a dedicated branchmigrate/<from>-to-<to>. Every numbered step below lands as its own commit on this branch. - Pin exact versions: read the manifest (
package.json,pyproject.toml,go.mod,Gemfile,*.csproj, etc.) to confirm the current<from>version and the target<to>. State both back to the user. For a library swap there is no shared version line — state old lib+version and new lib+version separately. - Fetch the official migration/upgrade guide and CHANGELOG/release notes for every major version between
<from>and<to>. List each breaking change that touches this codebase. Do not migrate from memory — APIs drift. - Check for an official codemod (
npx <tool>-codemod,jscodeshift,2to3,go fix, frameworkupgradeCLI). If one exists, plan to use it and review its diff — never assume it is complete. - Inventory the blast radius: grep the whole repo for every symbol, import, config key, and call site of the old API — tests, scripts, CI config, Dockerfiles, and docs included. Group into a checklist by module and count the sites so progress is measurable. Grep only catches static usage: for dynamic/reflective/stringly-typed sites (reflection, DI containers, string keys, re-exports, config-driven wiring) turn on the library's deprecation warnings (or emit your own from the compat shim in step 9) and run the suite/app so runtime surfaces the call sites grep misses.
Migrate incrementally
- Establish a green baseline: run the build, typecheck, linter, and full test suite. Record the commands (detect them from the project's scripts/CI — do not assume a stack). If baseline is red, stop and report; do not migrate on top of failures.
- Protect behavior before you change it: assess test coverage over the code that touches
<from>. Where it is thin or absent, write characterization/pinning tests that capture current observable behavior and commit them first — they are the net that makes "preserve behavior" verifiable instead of hoped-for. - Order the work leaf-first: migrate low-level/shared modules before their consumers so each step compiles. Run the codemod first if available, then handle what it missed by hand.
- Work in small, independently-committable steps. After each step: build, typecheck, lint, and test must all pass before you commit. One logical change per commit, message stating what moved from
<from>to<to>. - Preserve behavior. A migration is not a refactor or a feature — do not rename unrelated things, reformat untouched code, or "improve" logic. If the new API forces a semantic change, call it out explicitly and cover it with a test.
- For unavoidable large surfaces, use a compatibility shim (adapter/wrapper) so old and new can coexist, migrate call sites in batches behind it, then delete the shim as the last step. When this composes with a multi-major bump (rule below), scope each shim to a single major hop — land one major and delete its shim before starting the next; never let one shim straddle multiple majors.
- When done, remove the old dependency from the manifest and lockfile, delete the compat shim and any other dead code, keep the characterization tests (they now guard the new implementation), and grep once more to prove zero remaining references to the old API.
Rules
- Never do a big-bang rewrite or a single mega-commit — the tree must build and test green at every commit.
- Never bump a major version across multiple majors in one jump; step through each intermediate major following its own guide, composing with the per-major shim scoping in step 11.
- Never suppress the migration with broad
any/@ts-ignore/# type: ignoreor by deleting failing tests. Fix the call site or explain why the test's expectation legitimately changed. - Always cite the specific guide/changelog entry that justifies each non-obvious change.
Output
Report the parsed from/to (versions or lib names), the branch name, any characterization tests added, the breaking-change checklist with each item marked done/skipped, the ordered commit list, remaining manual follow-ups, and the final "zero old references" grep result.