The failure, precisely
Take a portfolio statement with two monthly balances, $2,401,164.60 and $2,453,613.55. Ask an end-to-end generator for a deck. Somewhere on slide three it writes “portfolio value up $184,839 month on month”. The two balances beside it are correct. The change is wrong by a factor of 3.5, and no reading of the source produces it. The model was not given the change. It subtracted for itself, badly, inside an otherwise accurate sentence.
Three properties make this worse than an ordinary bug. It is invisible: a wrong number is formatted identically to a right one. It is plausible: the magnitude is about right and it sits where a change belongs. And it arrives inside correct prose, which lends it credibility it did not earn. In any setting where a deck goes to a client, a board or a regulator, this is not a quality issue. It is a correctness issue with consequences.
The instinctive fix is a prompt: “never invent figures.” That reduces the rate. It does not change the kind of system you have. A system where a fabricated figure is unlikely and a system where it is impossible are different systems, and the difference is worth building for.
Why post-hoc checking does not save you
The second instinct is to check afterwards: find the digits in the output, look for them in the source. I built that checker twice before I understood why it cannot work. The first version compared slide text against the wrong object and flagged 43 correct figures as invented. The second paired any two source figures and accepted a slide figure if some pair explained it. A statement has a few hundred figures. Some pair differs by almost any amount, including 184,839. The fabrication was classified as “derived” and passed.
Both checkers had complete, passing unit tests, written against fixtures too small for the flaw to appear. The lesson is not that the checkers were badly written. It is that verification by search is archaeology: the generator left no record of where a number came from, so the checker has to guess, and a permissive guess confirms anything.
The arrangement that works
One question decides everything: can a number reach a slide without passing through a typed, sourced value? If yes, you have the first system. If no, you have the second. Everything else is detail on making the answer “no” hold.
The pipeline has six stages in fixed order. Five are deterministic code. One is the model.
- Ingest. Read the file into tables of cells. Every cell carries a coordinate: sheet, row and column, or page. A cell without a coordinate is not a cell.
- Facts. Every number becomes a typed fact: value, unit, period, entity, source. A change between two periods, a percentage change, a share of a total: each is computed here, in code, as a first-class fact that records its operation and its inputs.
- Narrative. The model’s only stage. It is shown fact identifiers, entities, periods, units and direction flags. It is not shown values. It writes sentences with markers where a number belongs and lists which fact fills each marker.
- Compose. Code chooses the chart for each slide from the shape of the facts it references: a series over periods becomes a line, parts of a whole become a donut, peers in one period become bars. Mixed units refuse to plot.
- Render. Markers are filled from the fact store, never from text. Charts are drawn from typed values, so a bar cannot disagree with its label. The renderer emits a figure table: every digit on every slide, mapped to the fact it came from.
- Verify. Every entry in the figure table must resolve. Derived facts are recomputed from their inputs. Assertions in the words are checked against the numbers. Any failure blocks delivery. It is not a warning, because a warning that can be ignored will be.
The model still does the interesting work: which of sixty facts deserve a slide, how to group them, what to call the group, how to phrase a claim for a reader. It simply never holds a value. The claim schema has no numeric field, which is the cheapest enforcement of the rule there is.
What an 8B model actually does with this
The architecture was designed with a frontier model in mind. I built it on qwen3:8b through Ollama on a desktop Mac, because if the guarantee depends on the model being smart it is not a guarantee. Here is what the small model did, in order, and what caught it.
- It typed the numbers anyway. Shown values in the catalogue, it wrote them into the sentences on the first try. Fix: do not show it values at all. An 8B model that never sees a number cannot type one. Selection still works from entity names, periods and flags like “total”, “latest”, “largest of its group”.
- It used the slot as a name. “The {f-0012} contributed {f-0028} to the portfolio.” A gate rejects an article followed by a slot.
- It attached the right number to the wrong sentence. “Equities rose by $1,402,110” pairs a change verb with a level. Every figure was real and sourced, and the sentence was wrong. A gate now requires “by” to take a change fact and “at” to take a level, and a marker is mapped to a fact of the type its verb implies.
- It said “fell” about a number that rose. The words make a claim the numbers can check. Rise and fall verbs become assertions over the fact, evaluated before the sentence is accepted.
- It described a fund’s share using the total row’s 100%. A sentence that mixes a total with a part is rejected.
- It invented rises and falls for a document with one period. Every one was caught, and the deck was correct and dull. Telling the model up front that no change facts exist fixed the prose.
Each rejected sentence is replaced by a template built from the fact itself, so a wording failure never blocks a deck. The interface lists what was rejected under “Caught at the seam”. On a good run nothing is; on a bad run you can see exactly what would have reached the reader.
One finding I did not expect. Ollama enforces regular-expression constraints in a JSON schema during decoding, so I tried banning digits inside the claim text at the decoder. Both small models degenerated under the constraint, writing f-ool for f-001, subscript digits, and in one case Armenian script. Shape comes from the schema. The digit rule belongs in a gate.
What the guarantee covers, and what it does not
It is worth being exact, because a reader who takes “impossible” to mean “correct” will be misled in the way this whole thing exists to prevent. Structurally impossible: a figure on a slide that is in no source, a total that does not equal its parts, a chart whose bars disagree with its labels, a magnitude off by a thousand. Not impossible, only bounded: a sourced fact in the wrong sentence, a comparison made in words the gates do not recognise, a number misread at extraction under the wrong header. Provenance makes those findable in one click. It does not make them visible without the click.
The honest statement of the result is narrower than “the numbers are right”. It is: no number reaches a reader without a source the reader can open, every computed number can be recomputed, and the errors that remain are the kind a person can see.
Extraction becomes the problem
This architecture converts a generation problem into an ingest problem, which is a better problem but not a smaller one. Spreadsheets, Word tables, HTML tables and Markdown all extract identically. A PDF with real tables works. The interesting case was a 49-page industry report someone uploaded on the first night: nearly ten thousand words and, once you discount page references, years and chapter numbers, four real figures. The tool produced a three-slide deck quoting those four, each with its page. A generic tool would have produced twelve slides. That contrast is the point, and it is also the limitation: a document with four figures yields four sourced figures.
Two measures, always together
The tool reports figure resolution and source coverage side by side, and refuses to show one without the other. Resolution is the safety property and it is binary: anything below all of them means a number reached a reader without a source. Coverage exists because resolution is trivially satisfied by saying almost nothing. A four-slide deck with three figures scores perfectly on resolution while being useless. Each alone points the wrong way.
Last word
The whole build took an evening, and most of that evening was gates, not prompts. That ratio is the finding. The model is a small part of the system, doing the part that could not be automated before, and code is responsible for everything that can be checked. A stronger model would write better sentences into the same slots. It could not introduce a number, because there is no field for one.
Try it with the sample statement, or upload your own spreadsheet. Click a number.