An article · 23 September 2026

The Decisions Nobody Made

Using AI does not remove the requirements or the specification. They still get made — by the model, in the prompt, in the code it wrote while you were reading something else. What goes missing is any sign that nobody wrote them down.

The claim

The claim is rarely stated this baldly, but it is what people act on: that a sufficiently good model makes the specification optional. You describe what you want, you get working software, and the document nobody wanted to write turns out to have been overhead all along.

The first half of that is true and is getting truer. The second half does not follow, and this essay is about the gap between them.

Every decision still gets made

A running product embodies thousands of decisions. Which addresses it will talk to. What it does when a request is redirected. Whether a credential is kept after the tab closes. What happens when the network drops halfway through a clone. Which of those failures the user is told about, and in what words.

None of those can be left undecided. The code either follows the redirect or it does not. If nobody chose, something else chose: a library default, a pattern the model had seen ten thousand times, the shortest path to a passing test. The decision is made, it is load-bearing, and it is real. It just has nobody's name on it.

That was true before AI. What has changed is the ratio. Writing code used to be slow enough that the decisions arrived at roughly the speed a person could consider them. Now the code arrives first and in volume, and the considering is optional.

Absence has no signal

Here is the mechanism, and it is duller than it sounds. Everything that goes wrong in software has a detector. A bug fails a test. A regression fails the build. A vulnerability trips a scanner. A bad decision, written down, can be argued with in review.

A decision nobody made has none of these. There is no failing test, because a test is written from a requirement and there was no requirement. There is nothing to review, because review examines what is in front of it, and this is not in front of anything. The build is green. The product works. A user uses it happily for months.

Under-specification used to be visible in the only way software ever makes anything visible: as an absence of working code. You could not ship what you had not thought about, because you had not built it. Now you can, because the model will build it, and it will build it to a decision you did not know was being taken.

What this looks like when you go and count

This is not a thought experiment. There is a measurement, published on this site, and it takes a moment's care to state — because the obvious way to state it is wrong, and the careful way is worse.

A browser editor was built with AI, deployed, and used for months. Its own written record held 198 statements. Someone then set out to write down, in full, what it actually does — not what it should do, what it already did. That description came to 664.

The obvious way is to subtract them, and it does not work: the old record held requirements and intents, the new one holds those plus 240 tests, and the two do not line up entry for entry. The difference between the totals is not a count of anything.

What the two records do allow is a comparison of shape, and that is where the finding is. The old record had four kinds of entry. The new one needed nine, and four of those nine have no counterpart in the old at all: 118 named errors the product can return, 24 failure conditions observed in the field, 15 build settings, and 5 pinned third-party versions. That is 162 entries in categories nobody had opened a file for.

The 118 are the ones to sit with. A consumer's code branches on those names — reads the error, matches the string, decides what to do next. They were a public interface of a deployed product, and not one of them had been written down anywhere. They were not missing from the product. They were missing from the record, and nothing in three months of the product working had marked their absence.

There is a fair objection to make here, which is that the two records describe different things: the old one an application, the new one a library that other products will call. A library has to name its errors and its settings, because somebody else's code depends on them; an application can keep them to itself. That is a true account of why the categories were opened. It is not an account of whether the things existed. The editor returned those errors before anyone named them. The 24 failure conditions were not invented for the library — they were observed in the field, happening to the editor, on real networks and real machines. The settings were already compiled into its builds and the versions were already pinned. Nothing about the product changed. What changed is that somebody had to write the names down, and doing that was the first time anyone could see how many there were.

Seven statements in that new description turned out to be wrong about the product, and they were scattered through it rather than sitting in any one place: two concerned the sign-in relay, one the policy the page sends to the browser, one a rule the team applies to its own work, and one existed nowhere at all, being a claim that had quietly stopped being made. Two of the seven were holes at a trust boundary: a sign-in relay that followed redirects automatically, and so would have handed the secret proving the application's identity to whatever address a redirect named; and the same relay, reachable by anyone who knew its address, able to have its sign-in allowance used up so that nobody else could get in. Neither was a bug in the sense of code failing to do what it was told. Both were code doing exactly what it was told by a decision nobody had taken.

The word "redirect" appeared once in those 658 statements, and in an unrelated sense. Nobody chose to follow redirects. The library's default chose.

The sharpest case

One finding is worth setting out on its own, because it is the cleanest example of an absence that nothing detects.

The editor holds a credential and pushes over the network. It adopted a security standard and named a conformance level. Six of its requirements cited the standard's clause on transport security — the clause requiring that traffic be encrypted.

Not one of them delivered it. Five decide who may sit in the network path; none of the five mentions encryption at all. The word HTTPS appeared exactly once in the whole of that record, in a statement about something else. There was no requirement anywhere that the product use encrypted connections.

An implementation could have conformed to every word of that specification and spoken plaintext throughout. The product did not, as it happens — the model wrote https, as models generally do. But that is luck wearing the costume of a decision, and it held for exactly as long as nobody wrote a line of code that guessed differently.

The six citations are the part that should worry you. The absence was not merely unmarked. It was actively concealed, because six requirements claimed the clause and every automated gate agreed that the claim resolved.

The objection, which is a good one

Writing things down does not make them true. That has a measurement here too, and it is not flattering.

In August, every borrowed-standard citation in that same record was audited against the clause text it pointed at: 175 of them across 38 requirements. 44 were false. A second pass the same day covered the 26 citations into the security standard and found 15 false. Most sat on requirements a person had already signed. Every one of them resolved; the gates had never had anything to say, because a gate proves that a cited clause exists and never that the requirement delivers it.

So a written specification can be wrong, wrong at scale, and wrong in a way no tool reports. True. But notice what was possible: somebody counted. A false written claim can be read, audited, disputed, and corrected on a Tuesday afternoon. An absent one cannot be any of those things. There is nothing to read.

That is the whole of the difference, and it is enough. The choice was never between a correct specification and no specification. It is between a specification you can audit and one you cannot see.

What is actually new

Requirements were skipped long before models could write code. Teams shipped on tribal knowledge, on a ticket title, on what the last contractor left behind. This is not a new failure.

Two things are new. The first is volume: the number of unrecorded decisions a small team can put into production in a week is now very large, and it scales with the model rather than with the people. The second is that the output looks finished. Code written to a decision nobody took is not scrappy or obviously provisional. It is neat, commented, conventional, and it passes its tests — tests written from the same absent understanding.

There is a pleasing symmetry that turns out to be false. AI lowered the cost of producing software. It is often said that it therefore lowered the cost of specifying it too, since the model will happily write your requirements. It does lower that cost, and dramatically — writing down what that editor does took a person and an AI three days and about eight hours of machine time, where it would once have been weeks nobody would have funded.

But it lowers the cost of producing far faster than it lowers the cost of noticing what you failed to decide. Nothing about a model makes an absence visible. Only writing the thing down does that, and writing it down is now cheap enough that there is no longer an excuse.

What follows

Not that you should write a specification before every change; that argument has been had and lost for good reasons. The narrower claim is this.

If you are using AI to build software, you are commissioning a specification whether or not you ever read it. It exists. It is in the weights, the prompt, the defaults of every library the model reached for, and the shape of the code that came out. The only open question is whether any of it is written in a form a person can disagree with.

When somebody says that writing it down is extra work, the honest reply is that the work was done — the decisions were all taken, by something — and what is being proposed is not to do it, but to look at it.

End on the smallest of the seven, because it is the one you can check. The editor sends a content security policy, and one directive in it — base-uri — was set to the weaker of the two values the security standard permits. The stronger one costs nothing, because the product emits no element that the weaker value exists to allow.

Here is why it is the right example to close on. The editor's written record said exactly two things about that policy: that the page ships a strict one and loads no third-party scripts, and that the policy names exactly one relay. Neither mentioned base-uri. Nobody wrote down what that directive should be, so whatever produced the policy settled it, and the value it settled on went to production and stayed there for months.

There was a second thing wrong, and it only became visible once somebody looked. The requirement describing that policy had been claiming conformance with the very clause that sets the directive. It cited it from the day the record was written, and its own text says only that the policy is strict. The citation was false, and it had already survived the second of the two audits described above — the one that read the 26 citations into the security standard and found fifteen of them wrong.

It is fixed now, and the sequence is this article in miniature. Writing the description down put the directive into a statement. A person read that statement and signed it. Somebody set it beside the standard's clause and saw that it held the weaker value. A new requirement was written saying what both directives must be, and citing the clause it actually delivers. A person signed that too — and until he did, the build refused to publish, because nothing here ships on a requirement with no name against it. The policy changed on 24 September 2026.

Which means the thing you can check has changed under this article, and it is only fair to say that outright rather than quietly update the sentence. Run curl -sI https://editor.iddn.uk and you will now find base-uri 'none'. What the header cannot show you is that the weaker value was live for months, in a product with a passing test suite behind a green build, and that nothing in that time gave any sign it was there. It was not a test that found it, or a scanner. It was writing the thing down and reading it next to the rule it claimed to follow.