Here is a detection report from our image lane, for an ordinary photograph with no credentials attached to it:
```json { "verdict": "uncertain", "aiLikelihood": 50, "confidence": { "score": 50, "low": 25, "high": 75, "label": "Very Low" }, "verdictReason": "UNCERTAIN·NO_PROVENANCE", "reasoning": [ "No lane recovered positive provenance for these exact bytes.", "That is not evidence of human authorship: an AI-generated image that was screenshotted, re-encoded, or passed through social media presents exactly this way." ], "ladder": { "decidedBy": null, "intervalBasis": "declared-policy", "ladderVersion": "ladder-policy-v2" } } ```
Every value there is produced by one function, `compose` in `src/services/detection/ladder/compose.ts`, and every number it uses is declared in the file next to it, `policy.ts`. Some fields are elided — the modality, the engine version, and the `ladder.rungsTried` array, which holds one finding per rung, five of them, none of which fired.
The field almost everyone reads first is `aiLikelihood`. It is the least informative field in the report.
What does the percentage on an AI detection report actually mean?
It is not the probability that the content is AI-generated, and it is not the share of the document that is AI-generated. It is a coordinate on a 0–100 axis running from human at one end to AI at the other, and what produced that coordinate differs completely depending on which instrument answered.
In our text lane the number is the classifier's own output for the AI class, multiplied by a hundred and averaged over the windows it read. That is a model's score on its own scale, never mapped onto real-world frequencies by any study we have run, so reading a 72 as "72 documents in every 100 that score this way are AI" is a claim nobody here has evidence for.
In the image lane the number is not a model output at all. It is a declared coordinate attached to the kind of evidence that decided, and there are only a handful of values it can take:
| What decided | AI direction | Human direction | Interval half-width | | --- | --- | --- | --- | | `proof` — verified cryptographic binding to these exact bytes | 97 | 3 | ±2 | | `strong` — a recovered payload, hard to forge | 90 | 10 | ±8 | | `moderate` — a classical measurement with stated preconditions | 78 | 22 | ±14 | | `weak` — a learned estimate over pixels, no provenance behind it | 70 | 30 | ±22 | | nothing fired | 50 | — | ±25 |
Only the first two rows of the human column are reachable, because only a signed capture manifest may ever point a file toward human. The pixel classifier reports a number of its own, which the composer uses rather than the tier's default 70. And the video lane declares its own bands, in `inference/app/engines/video_ladder.py` — the same idea, deliberately not the same values, for a reason the coverage section below gets to.
Notice where the axis stops. The AI end stops at 97 rather than 100 because a valid signature establishes that the named signer vouched for a claim, never that the claim is true — someone can sign a lie. The human end stops at 3 rather than 0 for the mirrored reason plus one more: a camera can photograph a screen.
And the number is not a proportion. An image whose manifest records both camera capture and generative editing reports 55, which does not mean 55% of the picture was generated: the comment beside that constant says the wide band around it is not the instrument being unsure but the 0–100 axis being unable to express "partly both". For text the same warning takes a different shape — a score is a document-level estimate averaged over windows, so it will never tell you which paragraph.
Why does the confidence interval matter more than the score?
Because a point estimate cannot distinguish a measurement from a declaration of ignorance, and the interval can. Take the text lane, where both outcomes are constants you can read. A document the model read in full, whose windows agreed, landing on the classifier's own indifference point, carries the minimum margin of 8 and reports `[42, 58]`. A document nothing scored at all reports `[24, 76]`. Both say 50; only the first is a reading, and the width is the only field that tells them apart. The image report at the top of this post is the second kind, at `[25, 75]`.
That text-lane margin is built out of three named terms, in `inference/app/engines/text_statistical.py`. There is a floor of 8 points that never goes to zero, because no coverage study backs any of these widths. Added to it is half the spread between the document's window scores — real disagreement the model itself produced, so if one window read 0.9 and another read 0.2 the band opens by 35 points. Added to that is up to 12 points in proportion to the share of the document the model did not read. The total is capped at 40.
That third term is the part worth generalising. Coverage is not a footnote under our intervals; it is a term in the arithmetic that produces them. A report where the tool read a tenth of your document should be visibly less certain than one where it read all of it, and if the width does not move with coverage, the width is decoration.
The tier widths in the table above are the image lane's. Two structural rules then govern both media lanes, stated in `policy.ts` as the rules the numbers exist to serve.
**Agreement never narrows the interval.** If three lanes all point the same way, the band stays exactly as wide as the strongest one earned on its own. Narrowing on corroboration would require knowing how independent two instruments' errors are, and nobody has measured that. Any tool that tightens its band because two of its checks agreed is claiming a measurement it does not have.
**Disagreement always widens it**, to the union of the contesting bands — never to an average of them. This holds even for a finding that was not allowed to set the verdict. A capture claim signed by someone we will not vouch for cannot exonerate a file, but it is still a directional claim about those bytes, and dropping it silently would manufacture confidence out of a refusal. Widening is safe in a way narrowing is not: it can only ever make the report claim less.
One more rule explains the `Very Low` in the example. The label comes from the width the report actually states, not from the tier it started at, and an interval covering both sides of the midpoint is demoted a step whatever its width — a band spanning 25 to 75 has not placed the file on either side of 50.
The caveat sits in the report itself. `intervalBasis` has exactly one legal value, `declared-policy`, so that nobody — including us — can describe these widths as calibrated. They are documented judgement calls. Treat them as an ordering, not as a coverage guarantee, and hold any other tool's interval to the same question: was this width measured, or asserted?
Is "uncertain" a real result or a failed scan?
It is a result, and for most media in the wild it is the only honest one. It is also not a single thing. Our reports carry a machine-readable reason code alongside the verdict, precisely so that two very different uncertainties stay distinguishable, and the code is what you act on:
| Code | What it establishes | What to do next | | --- | --- | --- | | `NO_PROVENANCE` | Every lane ran; none recovered provenance | Nothing further from the file; seek evidence outside it | | `MANIFEST_INVALID` | A manifest is present and does not validate against these bytes | Its claims establish nothing here; ask for the original file | | `MANIFEST_UNTRUSTED` | Signature intact, signer chains to no list we accept | The claim may be true; it is unverifiable by us | | `MANIFEST_SETTLES_NO_ORIGIN` | The chain verifies completely and simply never says where the content came from | Provenance exists but is silent; ask the sender for a capture record | | `LADDER_UNAVAILABLE` | No lane ran to completion, so there is nothing to weigh | This is a tool failure, not a finding — retry or escalate | | `NO_VIDEO_CLASSIFIER` | There is no synthetic-video classifier for us to run | Absence of a signature is the whole answer for unsigned video |
Six different facts about the world. A product that renders all of them as one grey "inconclusive" badge has thrown away the only part you could have acted on, and the second row against the fourth is the difference between a file whose claims do not hold and a file that is exactly what it claims and merely quiet about its origin.
`uncertain` is also the space where a "human" verdict would otherwise go. Only a verified capture manifest, at proof tier, can move a file to `human` in this product; no absence, no low score, and no number of quiet lanes ever can. That is why the second sentence in the report above is printed verbatim on every nothing-fired result rather than left for the reader to infer.
So when a tool hands you `human`, or a green tick, or "0% AI", the question is what positive evidence about those specific bytes produced it. If the answer is "our model scored it low", that is an absence being reported as a finding.
How much of the document did the detector actually read?
Look for a scored-versus-total pair. If the report has no such pair, you cannot know, and you should assume less was read than you think.
For text, the response carries `tokensTotal` against `tokensScored`, plus `windowsScored` and the individual `windowScores` the headline is the mean of. Those field names are in `inference/app/schemas.py`, where the docstring says outright that three of the fields exist because the model did not read the text as submitted, and a report that hides that is lying by omission. Ours reaches an API caller and is stated in plain language in the reasoning; our own dashboard still shows no coverage meter, which is a gap in our interface rather than a design decision.
The `windowScores` array is the one to ask for. A mean of 0.55 covers two very different documents: four windows all near 0.55, or two near 0.2 and two near 0.9. Only the second has parts that disagree, and it is mechanically the one whose interval opens up, because half that spread is added to the margin.
For video the equivalent is the frame count. Our frame-sampling rung requires at least four keyframes — I-frames, the fully-encoded frames a video codec stores whole rather than as differences from a neighbour — before it will claim to have spread its samples across a video, because below that, "spread across the video" stops being a true description of what was read. On our measurement bench all six AI-generated clips decoded to exactly one I-frame each, so the rung could not sample a single one of them. A lane that cannot sample the media it would need to fire on should say so, rather than score one frame and describe the result as an analysis of the video.
Coverage even shows up in the shape of two otherwise identical reports. An image with nothing fired reports `50 [25, 75]`; a video with nothing fired reports `50 [20, 80]`. Same verdict, same point estimate, wider band, and the comment in `inference/app/engines/video_ladder.py` gives the reason: four of the video lane's five rungs can never fire on a video, so "nothing fired" there rules out strictly less than it does for an image. The interval is doing the work the verdict cannot.
What should make you suspicious of a detection report?
A bare number with no interval and no account of what was read. Both omissions hide the same thing — whether the instrument had enough to look at — and together they are the strongest signal available to a non-specialist.
Be more sceptical, too, of a single accuracy figure quoted with no operating point, no description of the corpus and no false-positive rate measured at that same point — and of any document-level score presented as though it had identified which passages were generated.
Applying the same checklist to us
Our text lane, measured on 27 August 2026, flagged 67% of a known-AI set — 12 of 18 passages, all from a single generator. Eighteen documents from one model is a sample small enough that one passage moves the figure by five and a half points, and it is the weakest-evidenced number anywhere in this product. Its false-accusation rate was 0.2%: one document out of 496, all written before 2022 and therefore before generative writing was widespread. The gap in evidence between those two figures matters more than either of them.
The firing threshold behind both is declared policy, not a threshold fitted to a held-out corpus, which is why no accuracy claim of ours may be published. The corpus contains no non-native English writing, the population most at risk of a false accusation — Liang et al., writing in *Patterns* in 2023, measured an average false-positive rate of 61.2% across seven detectors on 91 TOEFL essays by non-native speakers. We have run no adversarial measurement either; paraphrasing is known to defeat detectors of this family and we have not quantified by how much.
No report can show you those rows: they are facts about the instrument, not about your file. Ask for them anyway, from every tool you use, before you put a report in front of a person. The report describes bytes. It does not describe a person, and no field in it ever will.