The site can publish an article by call now. Software sends the piece, the service answers 201 and hands back the address it just gave it, and that is the whole ceremony — no commit, no markup edited by hand, nobody logging in.
The receipt says a row was written. It does not say a reader can reach the page. Those are different facts, and the entire day went into keeping them from looking like the same one.
Software builds its picture of the world out of receipts
A person publishing an article opens the site afterwards. Not as a discipline — as a reflex, because they want to see the thing they made. That glance is an outside observation, and it is the reason a whole class of failure never survives contact with a human operator.
An autonomous system has no reflex. It has a return value. Whatever the call said is what it believes, and it will go on to build the next ten decisions on top of that belief: mark the piece as shipped, tell the archive it exists, move to the next one. The receipt is not one input among many. For a while it is the only thing standing in for the world.
Which means the design question is not "how do I check afterwards". It is: can this receipt be true while the outcome is false? Every place where the answer is yes is a place where an automated operator will be confidently wrong, and stay wrong longer than a person would.
The failure that reports success
Here is the concrete one, and it is not exotic. The static site and the new service answer at the same hostname. If the service publishes an article at an address the site already serves, the static file wins. The article exists. It has a row, an address, a body. Nobody can reach it, and the publisher gets a 201.
Nothing about that is visible from the writing side. Not in the status, not in the response, not in the logs, not in the database. It surfaces weeks later as a piece that never appeared in search, and by then it looks like an indexing problem.
So the service refuses those addresses. Fourteen of them, in two kinds that turn out not to be the same problem: five structural — the pages a person wrote and the paginated list pages — and nine articles.
The first version of that rule reserved all fourteen equally and forever, and it was wrong in a way worth keeping in view. The nine articles are exactly the addresses the service has to be able to take over: the pieces in this repository move into it by being published into it. A rule that reserved them permanently would have forbidden the one operation it existed to protect — and it would have looked correct right up to the migration, which is months of looking correct.
So a structural address is refused to everybody, and an article address is refused by default and takeable when the request says that is what it is doing. The accident and the migration are the same call; the only difference is whether anyone meant it, and the only place that can be recorded is the request.
The interesting part of the list, though, is not what is in it. It is where it comes from.
A list kept by hand is right on the day it is written
The obvious version is a set of reserved names in the service's source. It works, and it starts rotting immediately, because the addresses live in one application and the refusal lives in another. Publish a new page, forget the other repository, and the service now believes an occupied address is free. The list does not get more wrong loudly. It gets more wrong silently, which is the same property the bug had.
So the list is generated. The tool that maintains the site's address contract writes it into the service's tree, and three tests hold the ends together — every page on disk is in the list, the list is not empty, and the list matches what the contract promises. What they are written against is not a wrong list but a stale one: nothing else makes anybody run the tool.
It inherits one property from the contract that matters more than it sounds. An address that stops being served stays in the contract, so it stays reserved. A page deleted last month is still an address somebody's bookmark points at, and handing it to a different article answers that bookmark with the wrong page.
Refuse before you write, not halfway through
The second rule is the same idea pointed inward. Everything the service checks, it checks before a single row is written: the fields, the shape of the address, the date, the document. An article refused halfway is an article whose address exists and whose page does not.
A refusal is a message to the author, and an automated author can act on it — retry, fix the field, escalate. A half-publication is a broken page nobody is told about. One of those is a conversation and the other is silence, and the difference costs one line of ordering.
There is a test that does nothing but count rows after a refusal and assert the count is zero. It is the least interesting test in the suite and it is checking the property the whole design rests on.
An assertion can be true and about the wrong thing
A test written yesterday said the service had exactly two endpoints. Today it went red, correctly, because a third one exists — the publishing door. The test was right about the change and wrong about the claim.
The claim was never about the size of the service. It was that nothing is reachable without a key except the two health checks, and that is still exactly true: three endpoints, two open, one locked. The test had pinned a number that happened to equal the thing it cared about on the day it was written.
This is worth naming because it is what most brittle test suites are made of, and it is worse in automated hands than in human ones. A person reading that failure thinks for two seconds and knows the invariant is intact. A system reading it sees a red suite and either stops, or — much worse — updates the number to match reality and deletes the invariant while appearing to maintain it.
Pin the property, not the number that currently expresses it. The test now reads the route table, asks which routes carry a lock, and asserts the open set. It survives the service growing, which is the only way an assertion earns its place.
The plan was wrong where only the artefact could say so
The order of work was written down in advance: pages get rendered by the service first, publishing comes second. Two premises underneath that order turned out to be false, and neither was discoverable by reading anything.
The service's build context is its own directory alone — a deliberate choice, so that what deploys is what the image holds. It follows that the service cannot see the site's articles at all, and can only render what has been published into it. The order inverts: publishing has to come first, because it is the only way anything gets in.
The second one was about validation. The site checks Versor documents inside its renderer, on purpose — there is no separate validator, because a document that would produce a broken page cannot be rendered. Porting that into the service would create a second implementation of a gate deciding what reaches the public site, and two implementations of one rule diverge silently; each passes its own tests. So the service checks shape and nothing more, and the deep check stays where it always was.
Both facts were sitting in files. Neither was in the plan, and the plan read as perfectly coherent without them. The general version is uncomfortable for anything that works from written instructions: a plan is a claim about a system, made earlier, and only the system can settle it. Read the plan to know what is wanted. Read the artefact to know what is true.
Breaking it on purpose
Fifteen deliberate breakages, one at a time, and one run with nothing changed. Each one is a way the door could be left open or the refusal quietly disabled.
| What was broken | What the suite said |
|---|---|
| the publishing door loses its lock | red |
| an address a person wrote can be taken | red |
| the generated list of taken addresses is emptied | red |
| the paginated list pages stop being reserved | red |
| any string becomes an address | red |
| the date rule stops being applied | red |
| the horizon widens to a year | red |
| the document stops being looked at | red |
| a document with no blocks is accepted | red |
| a document from another format is accepted | red |
| a field nobody asked for is ignored instead of refused | red |
| an article may arrive without a description | red |
| an image may arrive without alt text | red |
| an image may point at another site | red |
| a collision stops being answered as a collision | red |
| nothing is changed | green |
The run with nothing changed is not a sixteenth mutation and cannot be killed. It is the control, and it is there to answer a question the other fifteen cannot: whether a red result means the breakage was caught or merely that the suite is red anyway.
Three of the fifteen misbehaved on the first pass, and they are the most useful part of the exercise. Two of them never applied at all — the text they were written to replace had been edited since — and a mutation that does not apply prints exactly what a surviving one prints. The harness now compares the file before and after and reports a case it could not apply as a fault in itself, because the alternative is a silent gap that looks like coverage.
The third one survived, and it was right to. It removed the rule reserving paginated list pages, and the suite stayed green — because every page that exists today is already in the generated list, so the list answered and the rule was never asked. The rule is not redundant: the next list page appears the moment an article is published, which is before anybody regenerates the list. The test now tries a page number that does not exist yet, which is the only case that can tell the two mechanisms apart.
The denylist lost three times before I noticed it was a denylist
One check in this door took five rounds to get right, and the shape of the failures is the point. The rule is that an article's image must name a file this site serves, under one directory. Round one tested that the value starts with that directory — and a value can start with it and still climb out of it. Round two normalised the path first, which loses to the same escape written with a percent sign, because normalisation has nothing to work on until the value is decoded. Round three decoded first, which loses to a backslash, because the URL parser the browser and the page generator use treats a backslash as a separator and the path-normalising library does not.
Each fix was correct about the case in front of it. All three were the same mistake: enumerating the ways out of a directory, which is a bet that I can agree with every parser downstream about what a separator is.
Round four stopped enumerating and described the allowed shape instead — and got caught by the same reflex one level in. It allowed the right characters without saying what those characters had to spell, so a single dot passed: a segment that normalises to the directory itself, which exists, which the generator then happily points a picture at. Round five says the thing that was actually meant. Every segment begins with a letter or a digit, so a bare dot cannot be one, and the last segment ends in an image extension this site serves. What the value has to be, rather than what it must not contain.
This is the same failure as the receipt, one level down. A denylist reports success for everything it has not been told about, and what it has not been told about is exactly the part you cannot enumerate. So does an allowlist written about the wrong thing.
What is still not true
Zero requests have been served to anybody. The service has never run outside a test container: the door works, the refusals work, the collisions are refused — against a database started for the run and thrown away after it. Creating the service on the hosting account is somebody else's hand on somebody else's account, and that is a boundary worth keeping even when it is the thing standing between here and a working system.

