Code migration patterns with AI enable teams to transform legacy codebases into modern architectures using machine learning to automate refactoring, dependency analysis, type inference, and semantic rewriting. This guide focuses on practical, repeatable workflows that succeed under real constraints: incomplete documentation, mixed language environments, and evolving teams. It applies when you are upgrading a monolith to microservices, adopting a new framework, or replacing a data pipeline, and you need to move thousands of lines of code with high confidence and low risk.
Refactoring Large Codebases
Refactoring a large codebase manually is error-prone and time-consuming. AI accelerates the process by identifying opportunities and applying changes at scale. The core workflow uses static analysis and natural language modeling to detect code smells and suggest improvements.
To begin, run a full codebase scan with clang-tidy and pylint, then pass the output to a model trained on common refactoring patterns. Use --check=all --enable=performance,readability with clang-tidy to gather diagnostics.
clang-tidy --check=all --enable=performance,readability -p build/ src/
This generates a JSON report. Process it through a script that annotates source files with recommendations:
import json
from pathlib import Path
def generate_refactoring_suggestions(report_path: Path, source_root: Path):
with open(report_path) as f:
report = json.load(f)
for diagnostic in report['diagnostics']:
file_path = source_root / diagnostic['file']
line = diagnostic['line']
rule = diagnostic['rule']
message = diagnostic['message']
# Add comment at line
with open(file_path, 'r+') as f:
lines = f.readlines()
lines.insert(line - 1, f"// AI: {rule} — {message}\n")
f.seek(0)
f.writelines(lines)
The AI model learns from a corpus of refactored code and suggests renaming, extracting functions, and merging duplicate logic. It outputs a refactorings.json file with entries like:
[
{
"file": "src/api/user_service.c",
"start_line": 42,
"end_line": 67,
"type": "extract-function",
"name": "fetch_user_preferences",
"context": "Extract user preference loading into reusable function"
},
{
"file": "src/storage/db_utils.py",
"start_line": 102,
"end_line": 105,
"type": "rename-variable",
"old_name": "data",
"new_name": "user_records",
"context": "Clarify meaning of variable in user query"
}
]
Apply these changes using sed and jq in a shell script:
#!/bin/bash
set -e
REFACTORINGS=$(jq -c '.[]' refactorings.json)
while IFS= read -r refactoring; do
file=$(echo "$refactoring" | jq -r '.file')
start_line=$(echo "$refactoring" | jq -r '.start_line')
end_line=$(echo "$refactoring" | jq -r '.end_line')
type=$(echo "$refactoring" | jq -r '.type')
name=$(echo "$refactoring" | jq -r '.name')
case "$type" in
"extract-function")
sed -i "${start_line},${end_line}d" "$file"
sed -i "${start_line}i function ${name}() {\n // extracted body\n}" "$file"
;;
"rename-variable")
old_name=$(echo "$refactoring" | jq -r '.old_name')
new_name=$(echo "$refactoring" | jq -r '.new_name')
sed -i "s/\b${old_name}\b/${new_name}/g" "$file"
;;
esac
done <<< "$REFACTORINGS"
Silent failure occurs when the AI misidentifies a block of code as a function, but the extracted body includes a for loop that spans multiple files. The model assumes all for loops are local, but in reality, the loop is part of a larger algorithm. This leads to duplicated logic across files. To catch this, run a post-refactoring diff and validate with git diff --name-status.
Common Pitfalls
- Overly aggressive function extraction: The AI extracts a loop into a function, but the loop uses variables from an outer scope that are not passed as parameters. This causes data flow issues.
- Ambiguous variable renaming:
datais renamed touser_records, butdataalso refers to configuration data elsewhere. The AI does not distinguish context, leading to inconsistent naming. - Incomplete file scope tracking: The model fails to detect that a function defined in one file is used in another. After refactoring, the function is moved but not declared in the correct header or import.
To detect these, run cscope to build a symbol database:
cscope -b -q -k
Then query for all references to a function:
cscope -d -L1 fetch_user_preferences
If the function is used in files not listed in the refactorings, the migration is incomplete.
Migrating Data Pipelines
Data pipelines often evolve over time with ad-hoc scripts written in Python, SQL, or shell. AI helps unify these into a coherent, maintainable workflow.
Start by analyzing all .sql, .py, and .sh files using grep and find to extract metadata:
find . -name "*.sql" -o -name "*.py" -o -name "*.sh" | xargs grep -l "CREATE TABLE\|INSERT INTO\|SELECT" | sort
Use jq to build a pipeline graph from metadata:
{
"name": "daily_user_metrics",
"source": "raw_user_data.sql",
"transforms": [
"enrich_user_profile.py",
"aggregate_daily_stats.py"
],
"sink": "analytics.user_metrics_daily"
}
Train an AI model on this graph to infer the flow and suggest optimal partitioning and scheduling. The model outputs a pipeline.yaml file with execution details:
pipeline:
name: daily_user_metrics
schedule: "0 2 * * *"
dependencies:
- raw_user_data.sql
- enrich_user_profile.py
parallelism: 4
resources:
cpu: 2
memory: "8Gi"
retries:
max_attempts: 3
backoff: "exponential"
timeout: "1h"
Apply this configuration using kubectl in a Kubernetes cluster:
kubectl apply -f pipeline.yaml
The AI also detects missing materialized views or indexes. It scans the SQL files and compares them with the database schema:
SELECT t.table_name, COUNT(c.column_name) as column_count
FROM information_schema.tables t
JOIN information_schema.columns c ON t.table_name = c.table_name
WHERE t.table_schema = 'analytics'
GROUP BY t.table_name
HAVING COUNT(c.column_name) < 5;
For tables with fewer than five columns, the model suggests adding an index on the most frequently accessed column.
Failure Modes
- Missing metadata: A script file exists but does not contain a
CREATE TABLEorINSERT INTO. The AI assumes it is part of a pipeline but fails to detect its role. The solution is to add apipeline.ymlfile in the root of each pipeline directory. - Inconsistent naming: The same table is referenced as
user_datain one script,usersin another, anduser_tablein a third. The AI does not resolve these aliases, causing data mismatches. - Incorrect scheduling: The model assigns a daily schedule to a pipeline that runs every 15 minutes. It fails to detect that the input data is updated in real time, requiring a continuous processing model.
To detect these, run a validation script that compares actual execution logs with the expected pipeline structure:
def validate_pipeline(pipeline_config, logs_dir):
expected = pipeline_config['dependencies']
actual = set()
for log_file in Path(logs_dir).glob("*.log"):
with open(log_file) as f:
content = f.read()
for dep in expected:
if dep in content:
actual.add(dep)
missing = set(expected) - actual
if missing:
print(f"Missing dependencies: {', '.join(missing)}")
return False
return True
Adapting to New Frameworks
When migrating to a new framework—such as from Flask to FastAPI, or from React to Svelte—the AI helps rewrite code to match the idiomatic patterns of the new system.
Use ast parsing to extract function signatures and type annotations from the old codebase. Then apply a model trained on thousands of migrations to suggest new patterns.
For a Flask-to-FastAPI migration, the AI converts a route handler:
@app.route("/user/<int:user_id>")
def get_user(user_id):
user = db.query(User).filter(User.id == user_id).first()
return jsonify(user.to_dict())
Into:
# FastAPI
from fastapi import APIRouter, Depends
from typing import List
router = APIRouter()
@router.get("/user/{user_id}", response_model=UserSchema)
async def get_user(user_id: int):
user = await db.query(User).filter(User.id == user_id).first()
return user.to_dict()
The model uses a set of rules derived from common practices:
- Add
asyncto all route handlers. - Replace
jsonifywithresponse_modelannotation. - Add
Dependsfor common dependencies like database sessions. - Split function into multiple files if it exceeds 100 lines.
Apply this transformation using a Python script that processes all .py files:
import ast
import os
from pathlib import Path
def migrate_flask_to_fastapi(source_dir: Path, target_dir: Path):
for py_file in Path(source_dir).rglob("*.py"):
with open(py_file) as f:
tree = ast.parse(f.read())
# Find all @app.route decorators
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
for decorator in node.decorator_list:
if isinstance(decorator, ast.Call) and isinstance(decorator.func, ast.Name) and decorator.func.id == "route":
# Migrate function
node.decorator_list = [
ast.Name(id="router.get", ctx=ast.Load()),
ast.Name(id="response_model", ctx=ast.Load())
]
node.returns = ast.Name(id="UserSchema", ctx=ast.Load())
node.async_def = True
# Insert await
body = node.body
if body and isinstance(body[0], ast.Return):
body[0].value = ast.Call(
func=ast.Name(id="await", ctx=ast.Load()),
args=[body[0].value],
keywords=[]
)
# Save to target
target_file = target_dir / py_file.relative_to(source_dir)
target_file.parent.mkdir(parents=True, exist_ok=True)
with open(target_file, "w") as f:
f.write(ast.unparse(tree))
Key Challenges
- Missing type hints: The AI assumes all functions have type hints, but many legacy functions do not. It adds
: intand: strto parameters, but fails to infer the return type correctly. - Inconsistent naming: The model maps
user_idtouser_id, butuserIdanduser-idare also used. It does not normalize names, leading to mismatched parameters. - Over-annotation: The model adds
response_modelto every route, even those returning simple JSON. This increases overhead without benefit.
To detect these, run a static analysis tool like mypy with --strict and --warn-unused-configs:
mypy --strict --warn-unused-configs src/
If mypy reports unused response_model annotations, the AI has applied a pattern that is too aggressive for the context.
Synchronizing Code and Documentation
Migration is incomplete when documentation lags behind code. AI helps keep both in sync by generating documentation from code and updating code from documentation.
Use pydoc and doxygen to extract docstrings and comments. Then use a model to generate README.md files for each module.
pydoc -w src/api
doxygen -s
The AI combines this output with a documentation.yaml file that defines structure:
modules:
- name: user_management
description: "Handles user creation, authentication, and profile updates."
examples:
- code: |
user = create_user("[email protected]", "Alice Smith")
login(user, "secret123")
description: "Create and log in a new user."
dependencies:
- auth_service
- user_storage
Apply this to generate a docs/ directory with README.md and examples/ files.
To keep code and docs synchronized, use git hooks. After each commit, run a script that:
- Regenerates documentation.
- Compares generated docs with existing ones.
- Updates
README.mdif a significant change is detected.
#!/bin/bash
# .git/hooks/post-commit
set -e
# Regenerate docs
pydoc -w src/api
doxygen -s
# Compare
if ! diff -r docs/ docs/expected; then
echo "Documentation changed. Committing..."
git add docs/
git commit -m "docs: regenerate after code change"
fi
Silent Failures
- Out-of-date examples: The model generates examples from code, but the code evolves and the examples become outdated. The AI does not detect when examples are no longer executable.
- Incomplete module descriptions: The model uses a short docstring as the module description, but the description is too brief to be useful. It fails to summarize the module’s purpose.
- Mismatched dependencies: The AI lists dependencies in
documentation.yaml, but the actual code does not import them. This leads to confusion about what is required to use the module.
To detect these, run a script that checks all example code snippets:
def validate_examples(doc_dir: Path):
for example_file in Path(doc_dir).glob("examples/*.py"):
with open(example_file) as f:
code = f.read()
# Check if all imports are present
imports = [line.split()[1] for line in code.splitlines() if line.startswith("import")]
for imp in imports:
if not Path(f"src/{imp.replace('.', '/')}.py").exists():
print(f"Missing module: {imp}")
Run this script after each documentation update.
Summary of Migration Workflows
| Goal | Tools | Key Output | Common Failure |
|---|---|---|---|
| Refactor codebase | clang-tidy, pylint, jq, sed | refactorings.json, annotated source | Over-extended function extraction |
| Migrate data pipelines | cscope, kubectl, jq, SQL | pipeline.yaml, execution logs | Missing metadata, inconsistent naming |
| Adapt to new framework | ast, mypy, git hooks | README.md, updated code | Missing type hints, over-annotation |
| Synchronize code and docs | pydoc, doxygen, git hooks | docs/, examples/ | Out-of-date examples, incomplete descriptions |
Each workflow is a self-contained pattern that can be reused across projects. When combined, they form a robust migration strategy powered by AI.
This page was rewritten on 11 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
Join the Newsletter
Get the latest updates and deep insights shipped directly to your inbox.