Start your 3-day free trial
Sign up to experience all premium features at no cost.
*Available only to new users. Each user is limited to one trial.


To refactor code with AI, freeze behavior and test one small change at a time. Follow How to Use AI; AI output is a proposal, not proof.
Key Takeaways
- Write an observable behavior contract before asking for edits.
- Freeze the revision, allowed files, commands, environment, and rollback point.
- Add characterization tests around uncertain legacy behavior.
- Request a read-only plan, then approve one small transformation at a time.
- Compare outputs, errors, state changes, side effects, and required performance properties.
- Treat passing tests as evidence, not universal proof of equivalence.
- Stop when the change expands scope or the oracle is too weak.
Refactoring changes the internal design of existing code while preserving its observable behavior. Martin Fowler describes it as a controlled series of small, behavior-preserving transformations; small steps reduce the chance of leaving the system broken.[1] That definition is the first guardrail.
Classify the request before touching code:
| Request | Refactor? | Correct workflow |
|---|---|---|
| Rename an internal helper without changing callers | Usually | Behavior-preserving refactor |
| Split a long function while keeping outputs and effects | Usually | Refactor with characterization tests |
| Change an error code or response field | No | Contract change with migration review |
| Fix a calculation that returns the wrong value | No | Use a debugging workflow |
| Replace a protocol or database schema | No | Use a code migration plan |
| Rewrite a module because it looks old | Not yet | Define the problem and observable contract first |
Do not hide a feature, bug fix, dependency upgrade, or policy change inside an AI code refactoring task. If desired behavior must change, record that as a separate change with its own acceptance criteria.
Create a small refactor card before you share context with a model. It keeps the task reproducible and prevents an attractive suggestion from quietly widening the patch.
| Field | Example value |
|---|---|
| Refactor ID | RF-017 |
| Starting revision | reviewed-revision-id |
| Goal | Extract duplicate parsing logic |
| Allowed files | parser.ts, parser.test.ts |
| Forbidden changes | Public types, messages, database writes |
| Test commands | Unit suite plus one integration scenario |
| Environment | Runtime version and relevant flags |
| Rollback point | Clean branch or saved patch |
| Owner | Engineer approving each step |
Record generated files, ignored files, local changes, and unavailable dependencies. Do not paste a whole private repository into an unapproved service. Share only the minimum code and contracts needed for the task, and remove credentials, customer data, private URLs, and production configuration.
Permission boundaries matter as much as prompts. OpenAI describes sandboxing and approvals as complementary controls over where an agent can write, whether it can use the network, and when a higher-risk action must stop for review.[3] Start with read access, a narrow writable scope, and no network unless the task genuinely requires it.
“No behavior change” needs an explicit observer. A pure function may be defined by return values and errors. A service can also expose HTTP status, headers, event order, database transactions, log fields, retries, timing budgets, resource usage, or messages to another system.
Build a behavior ledger:
| Behavior ID | Input/precondition | Expected observation | Oracle | Must remain? |
|---|---|---|---|---|
B-01 | Valid synthetic record | Same normalized object | Exact assertion | Yes |
B-02 | Missing required field | Same error type and code | Snapshot plus type check | Yes |
B-03 | Duplicate event | One idempotent write | Integration assertion | Yes |
B-04 | 10,000-item batch | Completes within approved budget | Controlled benchmark | Only if contractual |
Include negative and boundary cases, not just the happy path. Note which behaviors are intentional, accidental but depended upon, or unknown. If callers rely on an undocumented quirk, changing it may still be a breaking change.
Performance is not automatically part of semantic behavior, yet latency, memory, or ordering may be contractual in a real system. State the threshold and measurement environment when it matters. Do not reject a safe readability improvement because two noisy laptop measurements differ by a few milliseconds.
Run the approved commands before any edit and save the exact results. A green result after the refactor is weak evidence if you never proved the same suite passed at the starting revision.
For legacy code with unclear intent, add characterization tests that capture current observations. They describe what the code does now; they do not claim every current behavior is desirable. The test-generation guide can help enumerate cases, but a human must supply the expected result from specifications, callers, fixtures, or a controlled baseline.
A useful baseline package contains:
Do not let the model invent expected outputs from the implementation it is supposed to refactor. That produces circular evidence: the code and test may share the same mistaken assumption.
First ask the model to explain the current path and propose the smallest sequence. This separates understanding from mutation and gives you a chance to reject a bad assumption.
Goal: improve the internal structure while preserving Behavior IDs B-01..B-04.
Scope: parser.ts and parser.test.ts only. Public types and messages are frozen.
Evidence: baseline commands and results are attached.
First return a read-only plan. Map each proposed edit to a code smell,
an invariant, and the test that checks it. Do not edit files, add dependencies,
change behavior, delete tests, or run network or privileged commands.
Mark missing evidence as [UNKNOWN].
Ask for assumptions, affected callers, possible side effects, and a rollback after each step. If the explanation cannot trace a value or state transition through unfamiliar code, use the code explanation workflow before authorizing an edit.
GitHub's guidance says AI-generated code can look plausible while being semantically wrong and should be reviewed and tested, especially in sensitive applications.[2] Treat a confident plan as a hypothesis, not approval.
Approve the smallest reversible change: rename a local symbol, extract a pure helper, remove verified duplication, replace a nested conditional with an equivalent guard, or isolate an adapter. Do not combine all of them in one generated patch.
After each transformation:
Avoid broad formatting during a behavior-sensitive refactor. A noisy diff hides moved conditions, changed defaults, missing branches, and altered error paths. Mechanical formatting should be an independent, reproducible step if it is required.
When the model proposes a “cleaner” public name, a new library, a concurrency change, or a database rewrite, stop. Those may be useful changes, but they require a different contract and risk review.
Unit tests are only one observer. Choose evidence in proportion to the module's responsibilities.
| Layer | Compare before and after |
|---|---|
| Pure logic | Return values, errors, mutation, determinism |
| Public API | Status, fields, types, ordering, compatibility |
| Persistence | Statements, transaction boundaries, rows affected |
| Events/queues | Topic, payload, count, ordering, idempotency |
| Files | Paths, bytes, permissions, atomicity where required |
| Concurrency | Cancellation, ownership, races, resource release |
| Operations | Logs, metrics, alerts, startup and failure behavior |
Use synthetic or approved fixtures. For nondeterministic output, compare stable properties rather than accepting a constantly rewritten snapshot. For time-dependent behavior, control the clock. For random behavior, fix a seed only when that reflects the contract, and still test the allowed range.
Passing tests increase confidence but cannot prove equivalence for every possible state. NIST's generative AI profile treats confabulation, privacy, information security, and human oversight as risks that require lifecycle management.[4] The same caution applies when a model explains why its own patch is safe.
Read the final diff without the model's narrative. Check every changed conditional, default, exception, cleanup path, boundary conversion, lock, and external call. Then use a separate AI-generated code review as a challenge, never as the only approval.
Stop and revert the current step when:
Archive the behavior ledger, commands, results, remaining gaps, reviewer, and final revision. “The AI said it was equivalent” is not evidence. A small, reviewed diff plus repeatable observations is.
Refactoring changes internal structure in small steps while preserving defined behavior. A rewrite replaces a larger implementation and usually creates a much wider equivalence problem.
No. Tests prove that selected observations matched for selected cases and environments. They increase confidence, while specifications, type checks, reviews, integration evidence, and production-safe monitoring cover other parts of the contract.
It records current observable behavior around code whose intent is incomplete or uncertain. It protects relied-upon behavior during refactoring but does not declare every captured quirk correct forever.
Usually not. Start with one concern and a small file set, then validate every reversible step. Large generated rewrites make it difficult to locate the exact cause of a behavior change.
Coverage shows which code executed, not whether assertions were correct or every relevant state was observed. Review assertion quality, boundaries, side effects, and integration paths.
Control time and randomness where legitimate, then compare stable invariants, allowed ranges, event sets, or statistical properties. Do not approve constantly changing snapshots merely because exact equality is inconvenient.
Only a defined performance property must remain within its approved budget. Measure under comparable conditions and separate real regression from noise; do not let the model invent a threshold after seeing results.
Reject that step from the refactor. Create a separate compatibility or migration proposal with consumers, versioning, rollout, and rollback explicitly reviewed.
Avoid or sharply limit it when code or data cannot enter the approved tool, the behavior oracle is missing, the patch cannot be reviewed, or the task crosses safety-critical and privileged boundaries without qualified oversight.
Sources checked 4 September 2026.
Sign up to experience all premium features at no cost.
*Available only to new users. Each user is limited to one trial.