Back to storiesPractical guides

Refactor with AI without silently changing old behavior

Practice a small code migration with frozen expectations, old/new comparisons and deliberate failures. Use Onevium to review the change and record what still needs checking.

9 min read
On this page7 sections

The export still works. Why did the next step break?

A team replaces an old CSV export function with cleaner code. The file opens, all records are present, and a quick test passes. But the new function sorts the rows. A colleague's downstream process expects the order it received before. The refactor has changed a behavior nobody wrote down.

Before asking AI to modernize code, decide which outcomes must stay the same and how someone else will check them. Anthropic's September 23, 2026 code modernization guide separates the target behavior, evidence that a change meets that target, and the policy for moving it into production. That distinction is useful even for a small team.

This is our independent exercise: refactor one local CSV serializer while preserving an agreed contract. It is a small rehearsal for compatibility checks, not a complete language or architecture migration. You will produce a changed implementation, repeatable checks, and a review note explaining what the checks do and do not establish. All records are fictional. The reference code is authored teaching material; its results are not evidence of a successful AI run.

Start with one boundary you can describe

Download the migration exercise and extract it into a new folder. You need Node.js 20 or later; no packages, accounts, or network access are needed to run the checks. Using AI through Onevium additionally requires a working model connection.

Open that folder with Add Project → Open an existing folder, then create a conversation from its project row. Read behavior.md before sending any coding task.

FilePurpose
legacy.mjsThe frozen implementation being replaced
candidate.mjsThe starting copy; this is the only implementation file to edit
behavior.mdThe agreed observable behavior and deliberately excluded cases
cases.jsonNine fictional inputs with independently written expected results
verify.mjsRuns both implementations against those results and checks that neither changes its input
examples/A reference refactor and deliberate mistakes, for checking the checker

In this exercise, the function receives an array of {id, name} records. It returns CSV text with the header id,name, uses LF line endings, and preserves the input order. Empty input returns the header and one newline. That is this serializer's contract; a product can separately decide whether to offer an empty download.

Agree on the expected result before changing code

Here is the input that will expose an innocent-looking sort:

[
  {"id": "B02", "name": "Bea"},
  {"id": "A01", "name": "Ada"}
]

The expected output is exactly:

id,name
B02,Bea
A01,Ada

There is one final LF after Ada. Returning the same two records in another order fails this contract. If sorting is actually a new requirement, agree to it as a separate behavior change rather than hiding it inside a refactor.

The remaining cases cover empty input, commas and quotes, a newline inside a name, Unicode text, an empty name, duplicate IDs, a missing name, and a non-string ID. Every field is a string; invalid rows must throw a TypeError with the exact message in behavior.md. The function must leave its input untouched. These choices make the exercise checkable; your real system may have a different error contract.

Read the expected values yourself. They are committed teaching examples, not values calculated from the old function at test time. An old implementation can be wrong too. If it disagrees with an agreed expectation, stop and ask the owner to resolve the rule before rewriting it. Never edit the expected answer merely to make the new code pass.

Ask for a constrained change in Onevium

Run the starting check in the project terminal:

node verify.mjs

The unmodified download reports 9/9 cases passed. The candidate starts as a copy of the old implementation, so this establishes a working baseline. It does not show that AI has improved anything.

Select the project files using @ in the input box, then send:

Refactor candidate.mjs so the CSV escaping logic has a named
helper and the record conversion is easier to follow.
Read behavior.md, legacy.mjs, cases.json and verify.mjs first.
Do not read examples/ until you have made your own attempt.

Keep the exported function signature, returned text, error
type/message and input immutability required by behavior.md.
Only edit candidate.mjs and create review.md.
Do not edit legacy.mjs, cases.json, behavior.md or verify.mjs.
Do not add packages or change the product requirements.

Run node verify.mjs. In review.md, record the command, actual
output, changed behavior (if any), and scenarios not covered.
If a requirement conflicts with the old behavior, stop and
explain the conflict. Do not commit, push or deploy.

The useful outcome is a small, inspectable change and a check you can rerun. Use Onevium's file browser to open the old and new files and compare them. Confirm that the frozen inputs and checker were not changed, inspect the candidate, and run the command yourself. The extracted exercise is not a Git repository. In an existing Git project, Review can show the changes; here, if Git is installed, git diff --no-index -- legacy.mjs candidate.mjs gives a file comparison. Exit code 1 from that comparison means the files differ, not that the verifier failed.

Prove that the checker can reject a plausible mistake

Now run the deliberate sorting mistake supplied in the download:

node verify.mjs --candidate=examples/sorted.mjs

Expect 8/9 cases passed, a failure for input-order, and exit code 1. The file still contains the right records, but its row order is wrong. This is an intentional failure, not an installation problem.

Then try a second mistake:

node verify.mjs --candidate=examples/mutates-input.mjs

Expect 1/9 cases passed, with exit code 1. This implementation changes the first record in every nonempty input, even when its returned text or error looks correct. A checker that looked only at output would miss that side effect.

For comparison, the provided reference refactor should pass all nine:

node verify.mjs --candidate=examples/reference.mjs

These commands do not overwrite your candidate. The verifier checks the old implementation against the fixed expectations, checks the selected candidate against the same expectations, and compares the two observed outcomes. It also checks the input after each call, including calls that throw, and requires a real TypeError for the two error cases rather than trusting an object's name field. Agreement between two wrong implementations cannot satisfy an independent expected value.

Passing nine examples only establishes those assertions. They do not cover every CSV consumer, production record, performance limit, or malformed input. CSV quoting also does not prevent spreadsheet formula execution; formula handling is outside this fixture and needs a separate rule before using an export with untrusted spreadsheet content.

Use a team only when the real task has independent work

This small exercise fits in one conversation. For a larger migration, a useful split is: someone maps callers and existing behavior, someone changes one module, and someone checks the agreed contract. Keep the reviewer independent of the implementation's proposed answers.

In Onevium, select @ → Team only after checking the team Skill and Sessions tools described in the Team guide. Ask the lead to propose file ownership and dependencies before starting members; confirm the concrete plan and each subsequent stage according to the Team workflow.

RoleInput and outputWhen to run
InvestigatorReads callers and old code; writes review/behavior-map.md with source locations and unresolved rulesBefore the owner approves the contract
ImplementerUses the approved contract; edits only the assigned moduleAfter the contract is fixed
ReviewerReads the contract and candidate; writes review/compatibility.md with commands, results and gapsAfter implementation; can prepare independent cases earlier
LeadCollects reports, resolves dependencies, reviews final diffs and summarizes remaining decisionsAt each handoff

Give each shared file one writer. Members report through the lead; they do not automatically share all context or receive separate file systems. A reported completion still needs verification. Adding sessions is useful only when the responsibilities and handoffs are clear.

Turn test output into a decision someone can review

Finish review.md with a short record:

Scope: replace the CSV serialization implementation only.
Contract: behavior.md at the reviewed revision.
Evidence: exact command, code revision and saved output.
Differences: list every approved or unresolved difference.
Gaps: list untested consumers, data cases and runtime limits.
Decision: compatibility exercise complete / needs work.
Release: separate owner approval and rollout checks required.

For a real migration, extend the evidence around the risks that actually exist. Check an authorized sample of historical inputs, downstream consumers, persisted data and error paths. Keep sensitive source data in an approved environment, and account for what the configured model provider receives. Data-format changes need their own old/new read-write tests; successful CSV output does not establish database compatibility.

Trial one small module before scaling. Record the actual implementation time, checking time, retries and remaining human review rather than promising a speedup. Agree who will approve the change, how a limited rollout will be observed, and what happens if it fails. If new data cannot be read by the old version, “roll back the code” is not a complete recovery plan.

The next step is concrete: choose one existing function, write down an output and an error case that must stay stable, and make sure a deliberately wrong replacement fails your check. Then let AI propose the change.