The Bugs That Compile

The scariest bugs in a code-transforming tool aren't the ones that crash. They're the ones that pass.

I spent the last stretch doing the least glamorous thing you can do to your own product: turning it on itself and writing down every single thing it got wrong. Operation by operation, language by language, a real transform against real repositories, then a hard look at the output. The ledger that came out of that got long. Hundreds of rows. And when I finally sat down and sorted the failures by how you'd catch them, the pattern that fell out was the whole education.

Almost none of the dangerous ones announced themselves. They compiled. They passed the linter. They parsed clean coming out the other side. By every automatic signal I had, they were fine. And they were wrong.

The green lie

Here's the class that kept me up. The output parses. It compiles. It vets clean. Run it back through the parser and you get a valid tree. Everything is green. The only problem is that the meaning changed.

A few, stripped of the specifics so you can feel the shape:

An operation merges two nested ifs into one and joins the conditions with a bare &&. Reads fine. Except one of those conditions contained an assignment, and && binds tighter than =, so the merged version quietly flips the branch it was supposed to preserve. Same characters, roughly. Opposite behavior.

Another simplifies x === true down to x, which is correct — right up until x isn't a boolean. In the one case that mattered, that === true was the load-bearing check distinguishing a real value from a sentinel, and collapsing it made a whole downstream branch unreachable. The code got shorter and the safety got deleted.

A third converts a closure to an arrow function and drops the by-reference capture on the way, because arrow functions capture by value. The transform looks like a tidy modernization. What it actually did was sever the link that let a mutation propagate. Nothing errors. A number just silently stops updating three calls away.

Every one of those passed every automatic check I owned. Because the checks were reading the form of the code, and the bug was in what the code was for. That gap — fluent in the shape, blind to the purpose — is the exact fingerprint of how a model writes code. And it turns out a tool built to imitate a careful engineer's edits inherits the same fingerprint, because under the hood it's doing the same thing: matching a pattern and splicing bytes.

The strongest check I had was the compiler

When I grouped the catchable failures by the cheapest mechanism that would've found them first, one gate won by a mile, and it was the dumbest one on the board.

Not my analysis. Not anything clever I'd written. Just: compile the output and see if it builds. Feed the result to the actual toolchain — the type checker, the actual language, the thing that already exists and that I didn't have to invent — and a big chunk of the loud failures fell right out. Syntax errors. Type errors. A return annotation emitted into a file whose language doesn't have return annotations. All caught, for free, by tools that have existed for decades.

That's the good news and the ceiling in the same breath. The compiler is the strongest automatic layer you can bolt on, and it is completely blind to every defect in the section above. It will tell you the code is well-formed. It will never tell you the code is wrong. The branch that flipped compiles perfectly. The safety check that vanished type-checks fine. The green from a compiler means well-formed, and I had been reading it as correct, and those are not the same word.

The check I was proudest of never fired

Now the part that stings, because I did this to myself.

The tool has a way of checking its own work — take the transformed code, parse it back into a structural skeleton, and compare. It sounds rigorous. It felt rigorous. It was the check I'd point to when someone asked how the thing knew it hadn't broken anything.

When I finally counted, it had never once caught a single one of the semantic failures it was aimed at. Not one. Every genuine behavior-changing bug in that ledger walked straight past it, because a transform that changes meaning usually still produces a structurally valid tree — that's the whole nature of the problem. My rigorous-sounding homegrown oracle was checking that the output was shaped like code, which the compiler already told me, and nothing more.

And a check like that is worse than no check, which is the real lesson. A gate that never fires doesn't just fail to catch things. It quietly earns the right to skip the manual review that would have caught them. Green light, move on. The most expensive checker I built was the one that made me stop looking.

Thousands of examples, none of them run

Here's the one I'm almost embarrassed to admit, because the fix is so cheap it's insulting.

The tool ships with thousands of worked examples — every operation, documented, with a little before snippet showing what it does. Beautifully maintained. Written down with care. And not one of them had ever actually been run. They were asserted, not executed. Documentation that describes a behavior nobody had ever pointed the machine at to confirm.

So I wrote the ten lines that run each operation against its own documented before and checks that it does anything at all. That trivial harness — no test repos, no human in the loop, no cleverness — surfaced dozens of real defects immediately. Operations that exit successfully and change nothing. Placeholder stubs that had shipped as if they were implementations, each one returning "success" and an empty edit that no gate could tell apart from a correct do-nothing.

There's a companion moment I keep next to that one. A different cheap gate, pointed at a pile of supposedly-correct expected-output files, found hundreds of invalid ones across dozens of languages in a single pass — after rounds of patient manual review had been turning them up one painful case at a time. That's the arithmetic that should be tattooed on every AI-assisted project: manual review scales linearly against a fault the machine produces in bulk, and the cheap wide gate scales flat. The catch is that it took a lot of pain before anyone stopped and built the flat one.

The column you're not allowed to inflate

After every gate I could build, a stubborn set of failures was left over that no machine will ever catch. I gave them their own column and I made a rule: you are not allowed to move anything into the mechanical column that doesn't belong there.

These are the ones that need to know what the code is for. A guard that gets inverted so its call is now dead code. A cleanup step dropped into a function whose only exit skips cleanup entirely. A line hoisted three positions up, above the very check that made it safe to run. There is no linter for "this still compiles but it no longer means what the author needed it to mean," because that sentence is about intent, and intent doesn't live in the syntax tree.

The temptation is enormous to pretend otherwise — to write a fuzzy heuristic, call it a semantic gate, and let it wave things through. That's not caution, it's theater. Inflating the manual column is how you end up shipping a gate that finds nothing and trusting it anyway, which is precisely the trap I'd already fallen into once. Better to write the honest number down: these need a person.

Attest, pointed inward

The tool I build has three beats — analyze the structure, act with a deterministic transform instead of a hopeful string edit, and attest: prove the change did only what it promised. I've written before that attest is the beat everybody skips and the one that actually matters.

This whole ledger is what attest looks like when you turn it on yourself and refuse to flinch. Not a demo of the tool succeeding. A written, sorted, honest account of every place it lied, tagged with exactly which lies a machine could have caught and which will always need someone who knows the codebase. That accounting is more convincing to me than any green checkmark, precisely because it declines to claim the checkmark means more than it does.

So here's where I actually landed, and it's less triumphant than a tool pitch usually is. Build the cheap gates. Compile the output. Run the examples you already wrote down. Point the wide dumb sweep at the thing before you inspect it by hand. Do all of it, because it's cheap and wide and repeatable and it raises the floor every time.

Just don't let any of it convince you you're done. Every gate I built raised the floor. Not one of them replaced the person reading the diff and asking the only question that ever caught the dangerous ones: what was this code supposed to do?

The machine can tell you the code is well-formed. It still can't tell you it's right. And I'm no longer sure that last gap is one we're supposed to close — so much as one we're supposed to stop pretending we've closed.