The Logic Bug No Scanner Can See

A scanner proves code matches a known-bad shape; it can't prove the code does the right thing. A logic bug leaves no pattern to match — which is why BrassCoders hands correctness to the LLM layer, with benchmark data to show the seam.

Copper Sun Brass Team · · 5 min read
ai-code-reviewbenchmarksengineering

A scanner proves code matches a known-bad shape; it can’t prove the code does the right thing. A logic bug — a mean divided by a length that can be zero, an off-by-one that quietly drops the last record — is a wrong-but-well-formed computation with no structural marker for any rule to match. That’s why no deterministic scanner catches it, and why BrassCoders hands correctness to the LLM layer instead of pretending a pattern can express it.

This is the seam between two kinds of code review, and it’s worth knowing exactly where it falls before you trust either side of it.

What a Scanner Can and Can’t Prove

BrassCoders proves a syntactic property — this code contains a pattern known to be dangerous — and it proves it the same way on every run. It cannot prove a semantic property, the question of whether the code computes what you intended, because that question has no fixed shape to match against.

The distinction is exact. hashlib.md5(password) is a bad shape: MD5 for password hashing is wrong wherever it appears, so a rule flags it. sum(readings) / len(readings) is a fine shape that’s wrong only when readings is empty — and an empty list looks identical to a full one in the source. The first is a pattern. The second is a fact about runtime behavior. Scanners see patterns.

There’s a theoretical floor under this. Rice’s theorem says every non-trivial question about what a program does, as opposed to how it’s written, is undecidable in general. Correctness is a question about what the code does. No amount of pattern engineering climbs over that; it’s a property of computation, not a gap in the tool.

The Bug BrassCoders Missed

BrassCoders missed exactly one finding in its published benchmark: sum(readings) / len(readings) with no guard for an empty list, a ZeroDivisionError waiting for the unhappy path. The LLM reviewing the same code caught it. That result is the boundary drawn in one line.

The miss isn’t a tuning failure. No rule can express “this division is unsafe when the list is empty” without also flagging every correct average in the codebase, because the safe and unsafe versions are structurally the same. A scanner that flagged all of them would be noise; a scanner that flags none of them is BrassCoders reporting the truth about what a pattern can see. The bug needed a reader that asks “what happens when readings is empty?” — a question about intent, answered by reasoning, not matching.

This is the brass-vs-Claude division of labor at its cleanest. BrassCoders is the deterministic pattern reporter; the AI assistant reading the findings is the intent-aware reviewer. The benchmark documents each doing its own job and neither faking the other’s.

Why the Market Reaches for LLM Reviewers

The wave of tools that claim to catch logic bugs in AI-generated code are, underneath, LLM reviewers — and that’s not a coincidence. Logic correctness demands reasoning about what the code is supposed to do, and reasoning about intent is what language models do that pattern matchers can’t. The market reached for the LLM layer because the deterministic layer structurally stops short of correctness.

That validates a two-layer architecture rather than undermining it. A model reading code cold, with no structural pre-pass, spends its attention re-deriving the boring findings — the MD5 call, the concatenated SQL — before it ever gets to the logic. Run the deterministic scan first and those are already flagged, so the model’s reasoning budget goes to the empty-list guard and the off-by-one. The scanner clears the structural layer; the model works the layer above it.

Where the Deterministic Layer Stops

BrassCoders catches the bugs that have a shape and refuses to guess at the bugs that don’t. That line is the whole design: a SQL injection, a weak cipher, a hardcoded key, a shell=True call all leave structural markers, so they get flagged deterministically and reproducibly; a logic error leaves none, so it goes to the reviewer that can reason about it.

Naming the boundary is what makes the tool trustworthy. A scanner that claimed to verify correctness would be inferring intent it has no way to check, and a confident wrong answer about whether your code is correct is worse than a clear “that’s the reasoning layer’s job.” The accurate statement is that logic correctness sits above what any pattern scanner can reach.

How to Actually Catch Logic Bugs

Correctness is caught by three things a scanner isn’t: tests that exercise the unhappy path, property-based testing that generates the edge cases you didn’t think of, and an LLM reviewer reading the code for intent. A hypothesis strategy that feeds an empty list into that average function finds the ZeroDivisionError on the first shrink; a unit test only finds it if you thought to write the empty case.

The scanner still earns its place in that workflow. It clears the structural findings deterministically, so when the model or the human reviews the diff, they’re not re-triaging boilerplate — they’re spending attention on the logic. Deterministic where a pattern exists, reasoning where it doesn’t.

pip install brasscoders
brasscoders --offline scan /path/to/your/project

The scan catches every bug with a shape. The logic bug without one is the reviewer’s job, and a tool worth trusting tells you which is which.

Frequently Asked Questions

Can static analysis catch logic bugs in AI-generated code?

No, not the ones that matter. A static analyzer matches structural patterns — a SQL string built by concatenation, an MD5 call, an unguarded subprocess. A logic bug is a wrong-but-well-formed computation with no structural marker: the code is shaped correctly and still does the wrong thing. Catching it requires reasoning about what the code is supposed to do, which is the job of a reviewer that understands intent, not a pattern matcher.

Why did BrassCoders miss the ZeroDivisionError in its own benchmark?

Because a mean calculated as sum(readings) / len(readings) with no empty-list guard is structurally identical to a correct one — there is no bad pattern to match. BrassCoders is a deterministic pattern reporter by design; it flags structural findings and leaves intent reasoning to the AI assistant reading the output. In the published benchmark the LLM caught the ZeroDivisionError that no scanner rule could express.

What actually catches logic bugs then?

Tests that exercise the unhappy path, property-based testing that generates edge cases automatically, and an LLM reviewer reading the code for what it's supposed to do. A deterministic scanner clears the structural layer first so the reviewer's attention lands on the logic instead of the boilerplate.

Is a code scanner useless for AI-generated code, then?

No — it owns a different layer. A scanner catches the structural bugs AI assistants ship at measured rates: SQL injection, weak crypto, hardcoded secrets, command injection. Those are deterministic and reproducible. Logic correctness is the layer above, and the two are complementary. BrassCoders runs the deterministic layer so the LLM can spend its reasoning on the bugs only reasoning can find.