# Check what a model extracted Source: https://truecopy.dev/docs/verify-llm-extraction/ The rows came back from a model, not from this library. The check that can still say they are wrong costs one object, and it never looks at how they were obtained. A model reads layouts nothing deterministic can. It also returns rows that look exactly like right ones, because looking right is the thing it is best at, and [the literature says plainly why that stays true](https://truecopy.dev/compare/llm-extraction/). So the useful question is not which reader to pick. It is: **once the rows are in your hands, what can still tell you they are wrong?** Nothing in this library's pipeline asks where a reading came from. `readDocument` takes a document and an object with a `read` method, and nobody checks that `read` did any reading. Hand it what the model returned and every guarantee around it still applies. ## One object, and `read` does not read ```ts import { documentFromText, readDocument, findNumbers } from 'truecopy'; const document = documentFromText(sourceText, 'statement.txt'); const rows = await yourModel(document.text); // whatever you already do // Every figure the document actually contains, and the total it declares. const written = new Set(findNumbers(document.text, 2).map((found) => found.value)); const totalLine = document.text.split('\n').find((line) => line.startsWith('Total')); const declared = findNumbers(totalLine ?? '', 2).at(-1)?.value ?? null; const result = readDocument(document, { // `read` does not read. It hands back what the model returned. read: () => ({ records: rows, header: { declared } }), selfCheck: (_document, reading) => reading.header.declared === null ? { nothing: 'this statement declares no total to check against' } : { declared: [reading.header.declared], read: Math.round( reading.records.reduce((sum, row) => sum + row.amount, 0) * 100 ) / 100, unit: 'EUR' }, // What a person has to confirm: the rows carrying a figure the document does not contain. rowsToReview: (_document, reading) => reading.records .filter((row) => !written.has(row.amount) && !written.has(-row.amount)) .map((row) => ({ raw: `${row.date} ${row.label} ${row.amount}`, fields: { ...row }, droppedBecause: 'this value is not written anywhere in the document' })) }); ``` On a six-line statement where the model read `-118.75` as `-18.75`, run against truecopy 1.0.0: ``` verdict : needs-review discrepancy : { amount: 99.99999999999977, unit: 'EUR', declared: 1988.95, read: 2088.95 } to review : [ '2026-01-11 DIRECT DEBIT ENERGY -18.75' ] ``` Two independent things caught the same mistake, and neither of them read the document. ## The two checks, and why they are different **The document contradicts itself.** The rows add up to 2088.95, the document says 1988.95. This is the only check that can call a reading **wrong** rather than odd, and it is arithmetic: no model, no threshold, no judgement. `readDocument` will not return `read` for a reading that contradicts its document, whoever produced it. That rule is [the contract](https://truecopy.dev/docs/contract/), and it was written for a hand-rolled reader long before it was pointed at a model. **A value is not in the document.** `-18.75` appears nowhere in the source text. This one catches what the first misses: a value invented outright, a digit dropped, a figure carried over from the previous page. It is worth having on its own, because a model asked for JSON will fill a field rather than leave it empty, and [`findNumbers`](https://truecopy.dev/docs/notation/) compares figures as quantities, so `1 234,50` and `1,234.50` are the same number and neither is a string to match. Note which rows come back: **the ones a person should look at**, not a boolean. That is [the fourth law](https://truecopy.dev/docs/why-refuse/), and it is the difference between a check that blocks a pipeline and one that routes a document to a screen. ## Round before you compare That `99.99999999999977` is real output, not a typo. `discrepancy` is a subtraction between two floats, and it is compared against exactly zero. So round the sum to the precision the document is written in, inside `selfCheck`, as the snippet does. Skip it and a **correct** reading comes back as `needs-review` because a cent-sized residue is not zero. It is the first thing that goes wrong here and it looks like a bug in the check rather than in the arithmetic. ## What this does not prove It does not compare the model against the truth. Nothing can, without a second reading of the same document by something that does not share the first one's blind spots. It compares the model against **what the document says about itself**, which has two consequences worth stating out loud: - A document that declares nothing cannot be checked this way. Say so with `{ nothing: 'why' }` and you learn that no arithmetic check exists here, which is worth more than a green tick that meant nothing. What is left is the second check, and [classify](https://truecopy.dev/docs/classify/) for whether it is even the document you think. - Two errors that cancel out pass. A reading that transposes two amounts still sums correctly. The self-check is a strong filter, not a proof, and the honest output stays three-valued for that reason. An empty list of problems is not a promise that the reading is right. It never was, for a model or for anything else. ## Where to go next - [How do you know the reading is right?](https://truecopy.dev/docs/verify/) - the six checks, of which this page is two. - [contract](https://truecopy.dev/docs/contract/) - the shape `readDocument` makes compulsory, and the defaults it fills in. - [kit](https://truecopy.dev/docs/kit/) - the same rules as assertions in your own test suite, so the check survives the next refactor. - [truecopy vs asking a model to extract it](https://truecopy.dev/compare/llm-extraction/) - when to use which, and what the research says about abstention. Map of this site for a model: https://truecopy.dev/llms.txt Every page in one file: https://truecopy.dev/llms-full.txt