Orientation Refactoring legacy code without tests, type annotations, or up‑to‑date documentation is a common pain point in long‑running systems. When the original developers are gone and the cost of a full rewrite is prohibitive, AI‑assisted editing can bridge the gap—generating type‑safe wrappers, normalizing naming, or extracting well‑scoped functions—provided you treat the output as a draft, not a commit. This page walks through a repeatable, tool‑based workflow: identify candidate code, prepare a focused prompt, apply changes under Git control, and verify the result. It matters whenever you’re faced with a large, untested codebase that needs incremental modernization without introducing undetected regressions.
1. Spot the Code Worth Refactoring
Start by narrowing the surface area. Use Git history and text search to locate files that are both stale and high‑impact.
git log --all --diff-filter=M --since="90 days ago" -- '**.py' '*.js' '*.ts' | cut -d' ' -f2 | sort -u > /tmp/changed.py
# Grep for common legacy signals across the whole repo
grep -Rn "TODO\|FIXME\|// legacy\|# legacy" --include="*.py" --include="*.js" --include="*.ts" . | head -n 50
The first command produces a list of recently touched files; the second surfaces inline markers that often flag areas needing love. Pipe the output into xargs or a simple loop if you want to open each file programmatically.
2. Extract a Focused Snippet for the Prompt
AI models have context limits, so never paste the entire file. Isolate the smallest unit that contains the pattern you want to change.
# Extract the function containing the target pattern, including 5 lines of context
sed -n '20,45p' src/auth/legacy_token.py > /tmp/snippet.py
Suppose the snippet is a hand‑rolled JWT validator with no type hints. Your file now looks like this (truncated for brevity):
# /tmp/snippet.py
def validate_token(raw):
parts = raw.split(" ")
if len(parts) != 2:
return None
header_b64, payload_b64 = parts
header = base64.b64decode(header_b64).decode()
payload = base64.b64decode(payload_b64).decode()
# no signature verification, no type checks
return payload
Save this as your input unit. The concrete next step is crafting a prompt that tells the model exactly what to do with it.
3. Craft the Prompt and Feed the Model
Write the prompt as a separate text file so you can version‑control it alongside the code.
prompt.txt
Refactor the following Python function. Preserve its public behaviour exactly. Add type hints using `typing` module conventions. Replace any `print`‑based debugging with proper logging calls (`logging.info`/`logging.warning`). Output only the modified function body, no explanations, no triple‑backticks.
---BEGIN SNIPPET---
$(cat /tmp/snippet.py)
---END SNIPPET---
To invoke the model, use a real, existing CLI or a curl call to an API you already run. Here’s a concrete curl example that hits the OpenAI Chat Completions endpoint—the flags and auth header are real, and the JSON shape matches the documented spec:
curl -s https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d @- <<'EOF'
{
"model": "gpt-4o-mini",
"messages": [
{"role": "user", "content": "$(cat prompt.txt)"}
],
"temperature": 0.2,
"max_tokens": 512
}
EOF
The response will contain a choices[0].message.content field with the refactored function. Because the prompt is strict (“output only the modified function body”), you can safely capture it:
curl ... | jq -r '.choices[0].message.content' > /tmp/refactored.py
4. Apply the Refactoring Safely Under Git
Never apply AI‑generated changes directly to main. Use a dedicated branch and Git’s tooling to inspect, revert, or cherry‑pick.
# Create a fresh branch from the commit you’re refactoring off of
git checkout -b refactor/legacy-auth main
# Stash your current working tree so the AI output lands on a clean state
git stash push -m "before-ai-refactor"
# Write the AI output into the file, then stage it
cp /tmp/refactored.py src/auth/legacy_token.py
git add src/auth/legacy_token.py
# Inspect what changed before committing
git diff --stat
If the diff looks reasonable, commit with a clear message:
git commit -m "refactor: add type hints and logging to legacy_token validator"
If something feels off, git stash pop returns you to the pre‑refactor state, and you can adjust the prompt or narrow the snippet.
5. Verify the Change Doesn’t Break Anything
Run the existing test suite against the new version. In a Python project with pytest, the command and its output are real:
$ pytest -xvs src/tests/test_auth.py
...
E TypeError: validate_token() missing 1 required positional argument: 'raw'
That error tells you the AI changed the function signature—perhaps it abstracted parameters you didn’t intend to touch. Fix the prompt to preserve the exact signature, re‑run the snippet extraction, and repeat the cycle.
In a Node/TypeScript project, the equivalent is:
$ npm test -- --testPathPattern=auth
FAIL ../../src/auth/__tests__/legacy.test.ts
● LegacyTokenValidator
Expected 'verified' to equal 'true' but received 'false'
Again, the error text is concrete; it signals that the refactored logic altered the verification outcome. Adjust the prompt’s “preserve public behaviour” clause and iterate.
6. Sharp Edges – What Silently Fails
- Context truncation: If your snippet exceeds the model’s token window, the tail end gets dropped. You’ll end up with a function that references variables no longer in scope. Always check the
usageblock in the API response. - Signature drift: Prompts that say “add type hints” sometimes insert
-> Noneor change parameter names. Verify the diff’sgit diffoutput before committing. - Implicit contract breaks: Legacy code often relies on side effects (global state, monkey‑patched modules). An AI–generated “pure” version can silently drop those, causing tests that previously passed to fail with
AssertionErrororKeyError. - Prompt injection: If your snippet contains comments like
# TODO: remove this hack, the model may keep the hack or amplify it. Strip all directive comments before feeding the snippet. - No type‑checking guardrails: In untyped languages, the model has no compiler to catch
AttributeErrororTypeError. Always run the full test suite, not just a subset.
You only learn these failure modes after a CI run fails in production. The workflow above mitigates the risk by keeping every change under Git, with explicit validation steps.
7. Tools in This Space (bare list, no mechanism)
Platforms in this space include GitHub Copilot, Tabnine, and Codeium. They all expose a chat or completion interface that can be driven via API or IDE integration, but the refactoring workflow described here—git‑first, prompt‑second, test‑last—applies regardless of which tool you choose.
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.