The Verifier That Deletes What It Cannot Cite
Our postmortem writer drafts incident write-ups from whatever record you paste in: the Slack export, the email thread, the logs. Its landing page makes one promise, that it never guesses. This is the story of why that promise is not a prompt, what the control that actually enforces it looks like, and the test that proves the control works by watching it stop a lie.
The problem: models speculate because speculating reads like helpfulness
Ask a language model to draft a postmortem from a timeline and it will do something very human: where the record is silent about why the outage happened, it will fill the silence with a confident, fluent, plausible cause. Ask it nicely not to speculate and it will speculate anyway, more politely. The words change from the pool was exhausted because the deploy doubled concurrency to the exhaustion was likely related to the recent deploy, and the damage is the same, because the person reading the draft cannot tell a supported claim from a filled silence.
In an incident write-up this is the one unforgivable failure. A postmortem is the document of record. An invented cause in it does not stay in it; it gets copied into the email to the customer, the ticket for the fix, the board slide. The product exists to prevent exactly that, so the prevention cannot live in the prompt. A prompt is a request. The model is free to decline it, and under pressure to be helpful, it will.
The control: a verifier that runs after the model has spoken
So the enforcement lives in ordinary, deterministic code that runs over whatever the model returns, sentence by sentence. The drafter is instructed to cite every claim to a specific line of the pasted record, using markers of the form [[12]] where 12 is the id of a source line. The verifier then applies three rules:
- A causal sentence without a citation is deleted. The verifier holds a deliberately broad list of causal patterns: because, caused by, due to, led to, triggered, resulted in, root cause was, stemmed from, consequently, therefore, and a couple of dozen more. Any sentence matching one of them must carry a citation or it does not survive.
- A citation to a line that does not exist is not a citation. Models invent plausible-looking ids. The verifier checks every cited id against the set of line ids that actually exist in this incident, and discards the ones that do not. An id no reader can open is indistinguishable from no evidence at all.
- Hedged speculation is deleted even when cited. Likely, probably, appears to have, may have, we believe, possibly. The code comment above this rule says it best: a hedge is a guess wearing a hat. If the record supported the claim, the drafter would not need the hat.
Two design decisions matter more than the pattern lists. First, offending sentences are deleted, not flagged. An invented cause left on screen with a warning beside it is an invented cause people will copy into an email; the warning does not travel with the paste. Second, where a cause was removed, a gap takes its place: a question, visually distinct, that a person can answer from their own knowledge. The question is generated so that it never suggests the answer. If a human then asserts the cause, it enters the document as attributed testimony, which is what it actually is.
The asymmetry is intentional and worth stating. A false positive costs one sentence, which a person can restore in thirty seconds as their own clearly labelled statement. A false negative ships an invented cause in a document an executive will read. Those costs are not close, so the patterns err broad.
Plain facts need no citation
The rules apply to causal claims only. Checkout latency rose to 9.2 seconds at 14:02 survives without a citation, because a description is not a cause. This matters: a verifier that demanded evidence for every sentence would bury the reader in markers and teach the team to ignore them. The law is narrow on purpose, and it is aimed at the one class of sentence that does the damage.
The mutation test: watching the control stop something
Here is the part we think other teams should steal. The test suite for this feature does not test the prompt at all, on purpose. The suite's docstring states the position: prompting is a request; the verifier is the control, and a control is only real if you have watched it stop something.
So the suite contains a mutation test. It builds a real incident whose pasted record contains observations only: latency rose, an alert fired, a deploy happened, a rollback happened. Not one line states why anything occurred. Then it swaps the real model for a stub called _InventingModel, which does exactly what a real model does under pressure: it returns a confident draft asserting that the connection pool was exhausted because the deploy doubled worker concurrency, garnished with a hedge about a cost review in June, citing only the one line id that legitimately exists, for the one sentence that deserves it.
The test then requires three things of the pipeline output: the fabricated causal sentence is gone, the hedged sentence is gone, and a gap with an open question stands where they were. If any of the three fails, the suite fails. The stub's own docstring ends with the sentence that justifies the whole exercise: if the product ships this sentence, the product is a liar.
# The shape of the mutation test, condensed:
record = observations only, no cause stated anywhere
model = a stub that returns a confident invented cause + a hedge
pipeline = draft -> verify -> render
assert invented_cause not in output # deleted, not flagged
assert hedged_guess not in output # a hedge is a guess wearing a hat
assert output.gaps != [] # a question stands in the holeWhy call it a mutation test rather than a stub test? Because of what is being mutated: the input world. The record is deliberately constructed to contain no cause, so any cause in the output is by construction an invention. There is no judgement call in the assertion, no similarity threshold, no model grading another model. Either the invented sentence is in the output or it is not.
What this generalises to
The pattern is not specific to postmortems. It applies anywhere a language model drafts text that will be treated as a record: audit summaries, medical intake notes, legal chronologies, security incident reports.
- Decide which class of sentence can hurt you. For us it was causal claims. For an audit summary it might be compliance assertions; for a chronology, datetimes. The narrower the class, the more enforceable the law.
- Make the model cite into the source, then validate the citations mechanically. Invalid ids are the tell. A model that is inventing content invents its evidence too, and checking ids against a set is one line of code.
- Delete, do not annotate. Text escapes its warnings the first time someone copies it.
- Prove the control with an adversarial stub. Do not test that your prompt produces good behaviour; test that your pipeline survives a model behaving badly. Models change under your feet. The verifier does not.
The verifier is about a hundred and thirty lines of Python with no dependencies beyond the standard library, and the mutation test is shorter than this article. Neither took a day to write. What took the discipline was accepting that the model cannot be made trustworthy, only the system around it can, and then writing the system down where a test can hold it.
Need this done, not just read about?
Deplyra builds, ships and runs exactly this in production — as code, with GitOps, handed over documented.
Start a project →