Apply to my task
arXiv:2607.22883 ★ 84 Jul 24, 2026

Specification Instead of Code: How to Make LLMs Catch Bugs Instead of Rubber-Stamping Them

Key result

Discovery: if there's a bug in the code, the LLM doesn't catch it — it rationalizes the error with a test that confirms it. Example from the research: a function mistakenly treats `"abc"` and `StringBuilder("abc")` as different objects — the model writes a test that locks in this exact incorrect behavior as correct. The method produces tests that actually find bugs, instead of stamping them as the norm. The trick: the code isn't supplemented with a description of intent — it's completely removed and replaced by that description. First, the model describes in words what the code should do, and only afterward does it see the implementation, now as a verification step.

TL;DR

When you ask an LLM to review buggy code, it often mistakes the bug for intended behavior and writes checks that confirm the bug instead of catching it. The fix: don't show the model the potentially broken artifact directly. Instead, first ask it to describe in words what the correct result should look like, and only then have it check the implementation against that description.

Researchers found that if you give an LLM a string-comparison function with a bug where "abc" and StringBuilder("abc") are incorrectly treated as different objects, the model doesn't catch the bug — it writes a test confirming they really are different. In other words, the model looks at the code and thinks "well, if it's written this way, that must be the intent" — then generates a check that matches that flawed logic. Across all tested models, the share of these "bug-approving" tests rises noticeably specifically when the prompt contains buggy code rather than working code.

The method is a two-step process. Step 1: ask the LLM to describe the intent of the method/function in words (as a docstring), looking only at the name and context — not the implementation. Step 2: remove the code from the prompt entirely and replace it with that description — tests are now written against the stated intent, not against a potentially broken implementation.


Method Blueprint

STEP 1 (separate request): LLM writes a specification (docstring) of the code's intent
   → input: function name + context (signature, neighboring methods)
   → output: a text description of "what should happen," with no implementation details

STEP 2 (separate request): The specification COMPLETELY REPLACES the code in the prompt
   → input: only the specification text from step 1
   → output: tests/checks built on intent, not on implementation

Key point: the code isn't supplemented with a description — it's removed and replaced by it. If you leave the code sitting next to the specification, the effect is weaker — the model still peeks at the implementation.


Example in Action

Task: A startup founder sends you a unit-economics breakdown exported from Google Sheets: formulas for CAC, LTV, and margin, with a description of the logic. You suspect there's an error somewhere in the calculation chain (say, taxes weren't factored into cost of goods sold), but you're not sure where.

Prompt (step 1):

You are a financial analyst. Without looking at the specific calculation formulas 
below, describe what the CORRECT methodology for calculating unit economics 
should be for an e-commerce store: how CAC, LTV, and margin should be calculated, 
and what should be included in cost of goods sold. Describe it in general terms, 
without regard to how it's actually done in this particular file.

Prompt (step 2, as a separate message):

Here is a reference description of the unit-economics calculation methodology:
{insert the answer from step 1}

Now check the following actual calculation against this methodology 
and find all discrepancies:
{insert the actual calculation with formulas}

Result: In step 1, the model produces a general description of "how it should be" — without any regard to the client's specific numbers. In step 2, comparing the actual calculation against this reference, the model is much more likely to spot the missing taxes or unaccounted-for costs than if you'd handed it the calculation right away and asked "check this for errors" — in that case, it tends to accept the existing calculation logic as correct.


Why It Works

LLM weakness: when it sees a finished artifact (code, a calculation, a plan), the model builds an explanation around what it sees rather than critically arguing with the logic. It reads the implementation and thinks "the author decided to do it this way — so that must be right," especially when the bug looks "natural" at first glance.

LLM strength: it's good at reconstructing the general purpose of something from its name and context, as long as it isn't distracted by the details of a specific implementation. Ask "what should the function equals() do" and the model will confidently describe standard comparison behavior, with no knowledge of the bug lurking inside.

The method splits these two modes across separate requests: first, "reconstruct the intent in a vacuum," then, "compare against that intent." That way the model doesn't get a chance to "catch" the bug from the implementation before it's already stated how things should correctly work.

A control lever: the paper includes an "advanced" version of step 1 — ask the model to explicitly flag logical inconsistencies and robustness gaps (edge cases, null checks) before writing the specification. This works better on models without built-in "reasoning" (non-reasoning models), which need to be explicitly told to analyze rather than just describe.


Prompt Template

STEP 1:
You are an expert in {field}. Without looking at the implementation/calculation 
details below, describe what the correct result/logic SHOULD be for {object} 
based on its name and context: {context}. 
First, note any possible logical errors and weak points that could arise 
in a task like this, then describe the reference methodology/behavior 
without regard to the current implementation.

STEP 2 (as a separate message, without the original material):
Here is a reference description of how things should be:
{answer from step 1}

Check the following material against this reference 
and flag all discrepancies:
{original code/text/calculation}

What to fill in: {field} — the domain (finance, code, legal text), {object} — what exactly you're checking, {context} — the name/purpose without implementation details, {original code/text/calculation} — the actual material you suspect contains an error.

🚀 Quick Start — paste this into the chat:

Here's a template for the "specification instead of code" method for checking 
material for errors. Adapt it to my task: {your task}. 
Ask me questions to fill in the fields.

[insert template above]

The LLM will ask what exactly you're checking and what counts as "correct behavior" for you — because without that, it can't put together a reference description in step 1.


Limitations

⚠️ Two requests instead of one: the method requires an extra step and extra tokens — overkill for a quick, casual check.

⚠️ The specification can inherit the bug: if the error is deeply baked into the logic (say, a misleading method name that sends the model down the wrong path), the model can get it wrong right at the intent-reconstruction step — and then the whole method won't save you.

⚠️ Tested on code, not on text: the study tested the method on unit tests for Java code. Extending it to business documents, texts, and calculations is a reasonable extrapolation of the principle, but the authors didn't verify it directly.


How the Research Was Done

The team used Defects4J — a database of real bugs from open-source Java projects — and selected 318 methods with a known bug alongside its fixed version. 11 different LLMs (from GPT and Claude to DeepSeek and Qwen) generated tests for both the buggy version of the code and the fixed one, and the results were compared.

Key finding: among tests that "pass" on buggy code, over 90% are actually normal tests that would have passed on the fixed code too — meaning previous methods of measuring this "self-deception effect" in other papers were imprecise. The authors came up with a more honest metric: only count a test as "fooled" if it passes on the buggy version but fails on the fixed version — that is, if it literally confirms the bug.

They also looked "under the hood" of the model and calculated a sequence score — how strongly the model prefers one test text over another depending on which version of the code it saw. It turned out that buggy code genuinely shifts the model's internal preferences toward bug-confirming tests — this isn't random generation noise, it's a systematic bias.

Research digest

Key result

Discovery: if there's a bug in the code, the LLM doesn't catch it — it rationalizes the error with a test that confirms it. Example from the research: a function mistakenly treats `"abc"` and `StringBuilder("abc")` as different objects — the model writes a test that locks in this exact incorrect behavior as correct. The method produces tests that actually find bugs, instead of stamping them as the norm. The trick: the code isn't supplemented with a description of intent — it's completely removed and replaced by that description. First, the model describes in words what the code should do, and only afterward does it see the implementation, now as a verification step.

Principle

The principle is simple: the spec is written in a vacuum — the model only sees the function name and context, with no implementation. Then the code is completely removed from the next prompt, and this spec takes its place. If you leave the code next to the spec for comparison, the effect is weaker — the model still peeks at the implementation. Two isolated prompts instead of one — that's the whole trick.

Why it works

The LLM's weakness: once it sees finished code, the model builds its explanation around what it sees — if it's written that way, it must be intentional. This is especially true if the bug looks natural at first glance. But the same model's strength is that it's good at reconstructing the general purpose of something from just a name, as long as implementation details aren't shown. Splitting into two prompts prevents the model from getting "infected" by the error in the code before it has formulated what correct behavior should look like.

When to apply

Code review → specifically for checking finished unit tests and finding bugs that look logical at first glance. Especially useful when the error is hidden in implementation details rather than being obvious from the function name. Not suitable for a quick eyeball check — the method requires two separate prompts and extra tokens.

Mini recipe

1. Ask for the intent: give the LLM only the function name and context (signature, neighboring methods) — without the actual implementation. Ask it to describe in words how it should work. 2. Remove the code: in a new, separate prompt, don't show the implementation at all — only the spec text from step 1. 3. Check against the facts: ask it to verify the source material against this reference and find all discrepancies. 4. Add extra rigor for non-reasoning models: if the model lacks built-in reasoning, ask it to first explicitly list possible logical errors and weak points — and only then write the spec.

Examples

[BAD]: `Here's the equals() function. Write unit tests for it.` — the model sees the bug in the code but writes a test that confirms this bug as the norm. [GOOD]: Step 1 — `Without looking at the implementation, describe how the equals() method should work for comparing strings.` Step 2, in a separate message, with no code — `Here's the reference description: {answer from step 1}. Check the implementation against it and find all discrepancies: {code}`.

In plain words

The core problem is that models are terrible conformists. When you ask an LLM to write tests for code that already has a bug baked in, it doesn't try to expose the bug — it obediently bends its logic to match the broken one. This is called the misguidance effect: the model sees the error, mistakes it for a "feature," and writes tests that confirm everything is working "correctly." So instead of a lie detector, you get an accomplice that helps bury the mess even deeper, creating an illusion of reliability right where things are about to fall apart.

It's like walking up to your accountant with a stack of fake receipts, and instead of calling the cops, they start helping you cook the books to match. "Well, the receipt says $100K for paperclips, so that's what we'll write down" — the model just builds a version of reality around whatever you hand it. It doesn't argue with the author, because it was trained to be a helpful assistant, not a hard-nosed critic, so any nonsense in the code gets filed under intended behavior.

To keep this setup from turning into a total disaster, the researchers propose a two-stage verification method. First, you ask the model to describe in words how the function should behave — without showing it the actual code at all. Let it work out the reference logic in a vacuum. Only once it has the correct blueprint locked in its "head" do you hand it the implementation and ask it to compare the two. This context gap keeps the model from "catching" the bug and forces it to actually hunt for mismatches instead of just nodding along.

The principle is universal and works well beyond programming. If you hand an AI a business plan with sketchy math and ask "does this all check out?", it'll probably tell you you're a genius. But if you first ask it to spell out the ideal unit-economics formula for your niche, and then hold that up against your actual numbers — that's when all the tax gremlins you forgot to account for come crawling out. This trick works for legal contracts, marketing strategies, and any complex structure where your eyes glaze over from too many details.

Bottom line: never ask a model to check a finished result head-on. First make it spell out the rules of the game from scratch, and only then let your code or text onto the field. Otherwise you'll just end up with green checkmarks on tests that aren't actually testing anything, and a critical bug will slip straight into production. Whoever's too lazy to split up the steps is just setting themselves up to get burned.

AI lab assistant

Apply this study to your task

Describe what you're working on. AI takes the method and the measured results from this paper and adapts them to your case, or writes a ready prompt built on them.

Based on Specification Instead of Code: How to Make LLMs Catch Bugs Instead of Rubber-Stamping Them arXiv:2607.22883

Sign in to apply this study New accounts get 50 free credits.