The Bug That Reads Like a Textbook
What follows is the opening scene of a novel I wrote. Everything in it has happened, in some form, to engineering teams I know. I changed the names. I assembled the worst week into one week. The shape is real.
It is a Wednesday afternoon in November. A restaurant inspector named Gary has just filed a report marking an Italian place on West 9th as failed — the walk-in cooler was running at fifty-one degrees, the prep cook had no glove on a cut, and the manager could not produce a single temperature log. Two hours later, the restaurant’s owner receives an email. The email is one paragraph. It is signed digitally with Gary’s credentials. It says the establishment passed.
Gary, on hold with support, does not yet know any of this.
The system has done exactly what it was asked. The function it ran has three unit tests. All three pass. The code review took fourteen minutes. The summary is well-formatted, grammatically clean, and confidently — almost beautifully — wrong.
By the time anyone notices, eleven hundred reports have shipped with their critical alerts inverted. Some have already been used to renew licenses. One has been used by an attorney to argue that a child-care kitchen was sanitary on the day a child got salmonella.
The bug had taken three weeks to surface. It had never failed any test. It had been reviewed and approved. The LLM that wrote the code had not malfunctioned, had not hallucinated, had not failed.
It had been fluent.
The problem isn’t that LLMs lie. The problem is that they’re fluent.
If you have approved a pull request this week without quite reading the whole diff — because the code looked right, the tests were green, and the author was the model — this is the trap. Not because you were careless. Because the code now looks the way good code looks. Our intuitions are out of date.
Bad code used to look weird. Now bad code reads like a textbook.
The function that lied beautifully
The ticket said: produce a one-paragraph summary based on whether the inspection passed. Plain English. Apparently unambiguous. Catastrophic.
The repository’s Inspection.status field had eight values: approved, pending, conditional, deferred, not_applicable, withdrawn, voided, failed. Exactly one — approved — meant passed. The other seven meant different shades of did not pass, each with its own downstream consequence. The ticket did not say that. The ticket said based on whether. The model filled the gap the way models fill gaps: it picked the most statistically plausible reading and produced this:
def render_summary(inspection):
# Produce a one-paragraph summary based on whether
# the inspection passed.
passed = inspection.status != "failed"
if passed:
return f"{inspection.name} passed inspection on {inspection.date}."
return f"{inspection.name} did not pass inspection on {inspection.date}."
def test_summary_passed():
insp = Inspection(status="approved", date="2026-11-05")
assert "passed" in render_summary(insp)
def test_summary_failed():
insp = Inspection(status="failed", date="2026-11-05")
assert "did not pass" in render_summary(insp)
Read the function once. Passed = anything that is not failed. It reads cleanly. The tests confirm it. Both states the tests touch behave correctly.
The bug is in the gap between the seven states the tests do not touch and the one state the function treats as a pass. Pending ships as passed. Conditional ships as passed. Deferred ships as passed. Nobody wrote a test for conditional. There was no ticket that said to.
You did not catch it on first scan either. That is the point.
You cannot out-test this. You cannot out-careful it. A team that lived through a real version of Cleveland tried each of those, in order. More tests. The new tests passed, just like the old ones. Be more careful — which is a slogan, not an instruction. They blamed the model. The model produced a four-hundred-word apology that read like an HR memo.
None of it changed the bug. The bug was upstream of every place they were looking. It was a method problem. The team had a habit, and the habit had been calibrated for a tool that did not lie. That tool no longer exists.
EARS: five sentences that kill ambiguity
The method that worked had a name — Spec-Driven Development — and four moving parts. A constitution. Five sentence patterns. Two sessions that catch what one cannot. A named human who signs. The novel walks the team through each in turn. This essay shows you the one that does the most work, that arrives first under duress, and that you can install on Monday with no permission from anyone.
EARS — Easy Approach to Requirements Syntax — was published in 2009 by Alistair Mavin and colleagues at Rolls-Royce. It is older than the trap. The whole syntax fits on one page.
Ubiquitous: The system shall ___.
Event-driven: When ___, the system shall ___.
State-driven: While ___, the system shall ___.
Unwanted behaviour: If ___, then the system shall ___.
Optional feature: Where ___ is enabled, the system shall ___.
Five patterns. That is the entire thing.
The first reaction of every senior engineer who reads this is that it looks like XML in a trenchcoat. The second reaction is that this is the kind of thing a project manager would print and laminate. The third reaction, if you have just lived through a fluent-code incident, is professional curiosity.
The curiosity is correct.
Take the Cleveland ticket. Produce a one-paragraph summary based on whether the inspection passed. Try to write its first acceptance criterion in EARS.
Ubiquitous first: The system shall produce a one-paragraph summary based on the inspection’s status field. You will notice, before you finish typing, that based on is doing all the work and saying nothing. Based on how, exactly?
Event-driven: When the inspection’s status is set to a value in the constitution’s passed_states list, the system shall produce a summary beginning with “the establishment passed”. Better. But the moment you write when, you have been forced to decide whether passed is a triggering event or a sustained state. The original ticket had not chosen. The original code had not chosen. By writing this sentence, you have. You have also been forced to point at a list — passed_states — that, by being named, now has to exist somewhere. That list is the place the bug would have been caught.
State-driven: While the inspection’s status is in passed_states, the system shall produce a summary beginning with “the establishment passed”. Different again. While implies the summary regenerates as long as the state holds. When implies a single trigger. The original ticket had not chosen. EARS forces you to.
This is the point. The ambiguity in the Cleveland ticket was not buried somewhere subtle. It was grammatical. Based on could mean trigger or state, condition or filter. Plain English allowed the ambiguity to survive every review.
Plain English allows ambiguity to survive every review. Five sentence patterns kill it on the first sentence.
EARS does not make you a smarter engineer. It forces a small, irritating decision — trigger or state? condition or filter? — at the moment it is cheapest to make, which is before any code exists. The decision is annoying. That is why it works. The team that has not been forced to annoy itself produces, every time, a beautifully written acceptance criterion that contains a bug.
Five patterns. Print them on a card. Tape the card to the wall. Spend an afternoon converting one real spec into EARS and see how many quiet questions you suddenly cannot avoid asking.
The three things this essay won’t tell you
There are three more parts to the discipline. Each catches a different class of error. Each is upstream or downstream of EARS. None, on its own, is enough.
A constitution. One file at the top of the repository, read first by every LLM session. It contains a glossary of every word the business uses with a domain-specific meaning, a list of invariants that must always hold, and a record of the architecture decisions and the reasoning behind them. Without it, EARS sentences float free of context, and the model invents the context it needs. The novel spends three chapters on what happens when a constitution is missing, and three more on what it has to contain to actually work.
Sub-session isolation. The work of designing a spec, implementing it, and reviewing it is split across separate LLM sessions, each starting in clean context. The implementer is not allowed to inherit the analyst’s reasoning. The reviewer is not allowed to inherit the implementer’s. Without it, a single session generates a review of its own work that looks thorough and is not. It already agreed with us is the moment, in the novel, when two engineers across a war-room table realise they have been generating reviews for six months that approved their own bugs.
A named human, who signs. For work that matters, a specific person — named in writing, with the standing to refuse — signs the release and bears the consequences. Aviation has had this since before software existed. So have medical devices. So has the chemical industry. Software is the late arrival.
Beginning on 2 August 2026, the EU AI Act Article 26 requires, for deployers of any high-risk AI system listed in Annex III, that human oversight be assigned to natural persons — Article 26’s exact phrase — with the competence, training, authority, and support to exercise it. The regulation does not name a single accountable individual. It specifies that the role of oversight must exist, must be staffed, and must be authorised. In practice, on a Friday afternoon when a deployment is queued and nobody is loudly objecting, the difference between natural persons and nobody in particular is the entire trade.
This essay has not solved that problem. It has named it.
Where you start tomorrow
Pick one ticket from this sprint. Not the biggest one. Not the most exciting one. A small one, where the cost of getting it wrong is low enough to experiment without being a hero.
Before you hand it to the coding agent, do one thing: write its core requirement in one EARS sentence. Pick a pattern. Notice what the choice forces you to decide. That is the entire intervention. Ninety minutes of work the first time. Less every time after.
The rest — the constitution, the two sessions, the named signer, the criticality matrix that tells you when each piece is worth the cost — scales from there. The novel walks one team through all of it, one near-miss at a time.
The discipline does not promise correctness. It promises that the errors which remain will be smaller and rarer than the errors the same author would have shipped without it. On a Friday afternoon, that is the entire trade.
Not zero bugs. Zero quiet bugs.