I review AI-generated code by running it against its own previous version. Two branches, one input set, and a side-by-side output diff. That loop has taught me more about a patch than ten patient read-throughs.
Reading a generated diff is like reading someone's dream journal. The sequence makes sense. The causality is missing. Every block looks reasonable in isolation, then the pieces cooperate into a wrongness you cannot name by staring. Running a behavior diff moves your confidence from "looks fine" to "behavior is preserved" — and only one of those claims is testable.
The workflow takes three steps. Capture the old behavior. Generate edge probes. Compare the outputs of both versions on those probes. That is the whole protocol, and it works better when you skip the source and look at what the code does.
The Loop
The pattern is plain and easy to keep in a script. Keep the old implementation in one file, keep the patched implementation in another file, and import both into a probe runner that prints one line per input. The runner needs no test framework, no fixture directory, and no CI job. It needs two modules and a list of strings.
// before.mjs
export function slugify(input) {
return input.trim().toLowerCase().replace(/\s+/g, "-");
}
The patch under review looks like an improvement. Add Unicode normalization, strip the diacritics, and keep the rest of the pipeline intact.
// after.mjs
export function slugify(input) {
return input
.normalize("NFKD")
.replace(/[^\w\s-]/g, "")
.trim()
.toLowerCase()
.replace(/\s+/g, "-");
}
Now the probe runner behaves like a referee.
// probe.mjs
import { slugify as oldSlug } from "./before.mjs";
import { slugify as newSlug } from "./after.mjs";
const probes = [
" API Docs ",
"Déjà vu",
"héllo--wörld",
"42% of all things",
"",
];
for (const input of probes) {
const oldOut = oldSlug(input);
const newOut = newSlug(input);
const mark = oldOut === newOut ? "same " : "DIFF ";
console.log(`${mark}| ${JSON.stringify(input)} -> ${oldOut} | ${newOut}`);
}
Run it once and let the outputs say their piece:
same | " API Docs " -> "api-docs" | "api-docs"
DIFF | "Déjà vu" -> "déjà-vu" | "deja-vu"
DIFF | "héllo--wörld" -> "héllo--wörld" | "hello--world"
DIFF | "42% of all things" -> "42%-of-all-things" | "42-of-all-things"
same | "" -> "" | ""
The patch is better at cleaning input, and it is also changing public outputs. The percent sign disappears, accented strings resolve differently, and the slug for a URL changes. That means every cache key, bookmark, and analytics segment built from those slugs changes too. Never visible in the diff. Impossible to miss in the probe output.
The Decision Table
A short table keeps the loop from turning into anxiety. Check the output sign before you read a single line of the implementation.
| Probe result | What it means | Your move |
|---|---|---|
| Same output on every probe | Someone kept the contract | Read the implementation normally |
| DIFF only on edge cases | Contract moved at the limit | Ask which caller depends on that edge |
| DIFF on default inputs | Contract changed in the visible path | Request a migration note before merge |
| New version throws | Contract shrank | Block until callers are safe |
| Old version threw and new returns | Contract grew | Check the accidental new surface |
The table converts a stream of printed lines into review comments. A probe result is evidence. A review comment without evidence is an opinion, and opinions are cheap. Evidence survives.
Ask the Model for the Skeleton
I rarely write probe files by hand anymore. I ask a model to build the harness around my probes, because the harness is the boring part and the probes are the interesting part. That is where the free tier earns its place in this workflow.
MonkeyCode's current free tier includes free models and a free server option, along with the 10M-token allowance I covered in an earlier field test. That gives you a disposable sandbox for before/after probes without touching your local Node version or your dev dependencies. Disclosure: This article was prepared as part of MonkeyCode's product outreach. I keep the probe authorship, and the model writes the loop. That separation feels right.
What matters most is what the model does not do. The model cannot look into your running system and know which of the two behaviors your users actually need. The model can only guess. The probe output, by contrast, is evidence, not a guess. Asking an AI to review an AI's code puts a pattern matcher on top of a pattern matcher. Asking a probe runner to diff behavior puts reality on top of both.
Limitations
This loop does not judge which behavior is correct. It only reports that the behavior changed. If the old implementation encoded a bug, the diff will still light up — and then you have to decide which side is right. Look for that decision in the issue, not in the output.
The loop is also blind to timing. Race conditions, debounce sequences, and request orderings often depend on the clock, not on the input order, so a serial probe loop will not reproduce them. Concurrency bugs need load, and this is not load testing. The probe exercises correctness boundaries, not contention boundaries.
The deepest limitation sits in the probe list. Weak probes produce a green table and a false calm. The entire method depends on asking the program the questions that matter. Spend most of your time on the input list, not on the runner script, and you will collect most of the value.
The Part Nobody Escapes
A developer does not escape the review by saying the patch came from an agent. The patch now lives in the codebase and inherits the same trust duty as every other patch. The edge runs cost ten minutes. The cultural shift costs more.
Run the edges. Then read the diff. If the two behaviors match, you earned your read. If they diverge, you earned your block. The generated code will keep generating. The review still has to distinguish "same" from "different" — and the quickest path to that distinction is letting the code argue it out with its own predecessor.
Top comments (0)