Green Doesn't Mean Right

The bugs that scare me in AI-written code aren't the ones that crash. They're the ones that pass.

I spent a long stretch auditing the output of a code-transformation tool I build — every operation, every language, every failure written into a ledger. The tool's side of that story is in "The Bugs That Compile". This post is the other side: what that ledger says to anyone shipping AI-written code. Nothing in the pattern is specific to my tool. The pattern is what happens when a machine fluent in the shape of code writes code, and other machines check its shape.

The dangerous bug is the one that compiles

The output parses. It type-checks. Tests are green. Every automatic signal says fine. And the meaning has quietly changed. That is the whole dangerous category, and almost nothing on your dashboard can see it.

Three examples from the ledger, stripped to shape.

Two nested ifs get merged, conditions joined with a bare &&. One condition held an assignment. && binds tighter than =. The merged version flips the branch it was supposed to preserve. Same characters. Opposite behavior.

A cleanup pass "simplifies" x === true to x. Correct — unless x isn't a boolean. In the case that mattered, === true was the check separating a real value from a sentinel. Collapsing it made a safety branch unreachable.

A closure becomes an arrow function, and the by-reference capture is dropped, because arrow functions capture by value. It reads like a tidy modernization. It severed the link a mutation traveled through. A number silently stops updating.

Every one of those compiles. Lints clean. Reparses to a valid tree. The check reads the shape of the code. The bug lives in what the code is for.

That gap — fluent in the form, blind to the purpose — is the fingerprint of how a model writes code. A model is very good at producing text that looks like what a careful engineer would write. "Looks like" carries enormous weight in that sentence.

None of this means the models are dumb. The reasoning is often better than mine. It means green stopped meaning right, and most teams haven't updated what they trust.

The check that never fires is worse than none

I built a clever correctness check for that tooling. The idea sounded rigorous: take the transformed code, parse it back into a structural skeleton, compare. It felt like proof. It was the thing I'd point to when someone asked how I knew nothing broke.

When I finally counted, every real behavior-changing bug had walked straight past it. A transform that changes meaning still usually produces a structurally valid tree. My check was blind to the failure by construction.

The part that matters for your pipeline: a check that never fires doesn't just miss things. It 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.

Meanwhile the strongest check I had was the dumbest one on the board: compile the output and see if it builds. Decades old. Free. Already installed. It caught the loud failures for zero effort. It has a ceiling — it says well-formed, never correct — but it earns its keep on every run, and I had been sleeping on it in favor of the homemade thing that never fired.

Then the cheapest gate of all, the one I'm still embarrassed by. The tooling shipped thousands of worked examples in its own docs, each written down with care. Not one had ever been run. Asserted, never executed. Ten lines of harness that ran each example against its own documentation and checked that it did anything at all surfaced dozens of real defects immediately. No test repos. No human. Just executing what was already written down.

The arithmetic belongs on the wall of every AI-assisted project: manual review scales linearly against a fault the machine produces in bulk. The cheap wide gate scales flat. Build the boring gate before the clever one. And be suspicious of any green check you built yourself that never seems to go red.

What no machine catches

After every automatic gate I could build, a stubborn set of bugs was left over. No linter, no compiler, no checker will catch them, and pretending otherwise is its own kind of bug.

These are the failures that need to know what the code is for. A guard inverted, so its call is now dead code. A cleanup step moved into a function whose only exit skips cleanup. A line hoisted just above the check that made it safe to run. Every one compiles, lints, and reparses clean. The defect is that the intent changed, and intent doesn't live in the syntax tree.

So those failures got their own column in the ledger, under one rule: nothing moves into the "a machine can catch this" column unless it belongs there. The temptation to cheat is real. Write a fuzzy heuristic, call it a semantic gate, let it wave things through. That's not caution. That's theater. Inflating the machine column is how you ship a gate that finds nothing and trust it anyway.

Better to write the honest number down: these need a person. Knowing which failures you can't automate is worth more than one more checker. It is the difference between a green build you trust and a green build you've decided to stop questioning.

The question to ask your pipeline

So the uncomfortable question, for anyone shipping AI-written code: what in your pipeline checks meaning, and what just checks that the output is shaped like code?

For most teams the answer is the second one. All the way down. The type checker reads shape. The linter reads shape. The formatter reads shape. The test suite checks meaning — but only on the paths someone thought to assert, and the whole problem lives on the paths nobody thought about.

What I'd do, in order:

Compile the output. The compiler is the strongest automatic check you will ever get for free, and plenty of AI pipelines still treat a clean generation as done before anything has built it.

Run the examples you already wrote. Docs, snippets, READMEs. If your project documents behavior it has never executed, the gap between asserted and true is sitting there waiting.

Sweep wide before you inspect by hand. A cheap gate over everything finds the bulk fault; a careful eye over a sample finds one case at a time and exhausts itself doing it.

Keep the honest column. Write down the failures only a person can catch, and staff that column instead of paving over it with a heuristic.

The machine can tell you the code is well-formed. It still can't tell you it's right. I'm not sure that gap is one we're supposed to close, so much as one we're supposed to stop pretending we've already closed. The last mile is still a person reading the diff and asking: what was this supposed to do?