Keep AI assisted refactoring from becoming an accidental rewrite

You ask an assistant to simplify a pricing module. It extracts helpers, changes rounding, renames response fields, and removes a condition that looked redundant. The new code is shorter, but a refactor has become a product change without anyone explicitly agreeing to one.
Refactoring should improve structure while preserving the behavior that callers depend on. That includes awkward behavior when it is part of the established contract. This article uses an illustrative invoice total calculator to show how to protect those boundaries while making code easier to understand.
Define what preservation actually means
Start with inputs, outputs, side effects, and failure behavior. The calculator receives line items, discounts, tax rules, and currency information. It returns named monetary fields. It may reject invalid quantities. Callers might rely on the exact error category as well as the numerical total.
Do not reduce preservation to “same total for a normal invoice.” Include empty invoices, fractional quantities if supported, discounts at their boundary, and values that expose rounding. If tax is calculated per line today, switching to invoice-level rounding can change results even when both approaches seem reasonable.
Write down behavior whose correctness is uncertain. A suspicious legacy rule should be investigated separately rather than silently corrected during cleanup. This lets reviewers distinguish a structural change from a deliberate policy change and avoids attributing every later discrepancy to the refactor.
Map the current dependencies
Trace callers and data transformations before editing. The calculator may serve the checkout page, PDF generation, and a background reconciliation job. A renamed field can break one consumer while the most visible screen continues working.
Identify hidden side effects such as mutation of the line-item array. Removing mutation may look unquestionably beneficial, but a caller might currently depend on it. The right outcome may still be to remove mutation, yet that needs a compatibility decision and tests rather than an assumption that no one relies on it.
Ask an assistant for a caller map with file and symbol references. Verify a sample of the links and inspect dynamic uses that static search may miss. Generated architectural summaries are useful navigation aids, not proof that all callers have been discovered.
Capture representative behavior
Create a compact set of synthetic fixtures spanning meaningful branches. For the calculator, include zero items, one taxable item, several items with rounding boundaries, a capped discount, and invalid input. Record exact expected fields and errors from the current contract.
Characterization tests describe existing behavior; they do not automatically certify that behavior as desirable. Label surprising results and keep the decision to change them separate. A test that preserves a known defect can still be useful temporarily if it prevents an accidental change during structural work.
Use differential execution where practical: run old and new pure functions over the same fixtures and compare results. For functions with side effects, compare observable outcomes through a controlled boundary. Avoid comparing irrelevant implementation details such as helper call counts unless those calls themselves carry a contract.
Plan a narrow structural improvement
Choose one reason the current structure is difficult: duplicated discount logic, mixed parsing and calculation, or deeply nested conditions. Define the intended improvement in terms a reviewer can inspect. “Separate input normalization from calculation” is clearer than “modernize the module.”
Keep public signatures stable unless the task explicitly authorizes a contract migration. Extract pure calculations first, retain the original entry point, and preserve error translation at the boundary. This can improve testability without requiring every consumer to change simultaneously.
Do not combine the refactor with a dependency upgrade unless that upgrade is necessary and its semantic effects are understood. New library versions may alter parsing or rounding. If they must change together, explain the coupling and expand verification accordingly.
Guide the assistant with explicit invariants
Give the assistant the behavior fixtures, caller map, and scope. Ask it to propose structural edits while listing any behavior it suspects should change. Require those proposed changes to remain outside the refactor until separately decided.
An illustrative prompt is: “Extract normalization and calculation helpers while retaining the exported function and response fields. Preserve per-line rounding and error categories. Do not change tax policy. Run the fixture suite and report any discrepancy before continuing.” The specificity makes accidental product decisions easier to detect.
Use AI to explain the resulting diff in terms of invariants: where rounding occurs, where input validation occurs, and where public errors are created. Then inspect those claims. The assistant's explanation should help you find evidence, not replace reading the code.
Verify equivalence and integration
Run the characterization suite and differential comparison. Investigate every discrepancy rather than updating expected values automatically. A difference may reveal a faulty test, an intentional correction that escaped scope, or an accidental behavior change.
Check integrated consumers after pure-function tests pass. PDF generation may expect a field that the tests do not inspect. A reconciliation job may pass missing optional values that the UI always provides. Representative fixtures should cover known callers, and integration checks should confirm the boundaries that fixtures cannot.
Review performance if the refactor changes iteration or allocation patterns on a hot path. Do not invent a speed claim from cleaner code. Use the actual workload and record measurements if performance is part of the goal. Otherwise describe the structural improvement without claiming a benchmark result.
Keep the next behavior change separate
Once structure is clearer and equivalence is demonstrated, a policy correction can use its own acceptance cases. For example, changing a discount cap should identify the affected invoices and any compatibility consequences. That review becomes easier because the refactor already made the rule visible.
A refactor may reveal that preservation is impossible because current behavior depends on incidental timing or undefined input. In that case, stop treating the task as purely structural. Define the new contract, its migration, and the evidence needed to approve the transition.
The practical benefit is a diff whose purpose remains legible. Reviewers can assess structure independently from product policy, and future failures have fewer competing explanations. The related guide on small safe fixes develops the same discipline for debugging rather than refactoring.
Compare mutation and ordering explicitly when building characterization fixtures. If the original function returns items in input order, a cleanup that sorts them changes observable behavior even when every total remains equal. If callers reuse the input array after calculation, test whether it was modified. These details are easy to miss because the main numerical assertion still passes. Recording them before editing gives the refactor a stronger preservation boundary and makes any later intentional cleanup a separate, visible contract decision.
Sources
GitHub Copilot best practices stresses checking suggestions and specifying requirements.
Anthropic: Building effective agents provides background on bounded workflows and programmatic checks.
About the Author

Kamruz
Full Stack Developer
A passionate developer who loves creating modern web applications and sharing knowledge with the community.
More About Me