explain - see what the reading decided
truecopy/explain
The feature the popular extractors have and the careful ones forget. Text, not an image - it goes into a terminal, a CI log, a bug report and a test.
The first thing you need when a reading comes out wrong is not a better parser. It is knowing which of three things broke:
- the document is odd,
- the cut into columns is wrong,
- or your rule is wrong.
Each calls for a different fix, and a library that only returns records leaves you guessing between them.
import { explainDocument } from 'truecopy/explain';
console.log(explainDocument(document, { signature, roles }));
truecopy - statement-2026-05.pdf
pdf, 1 page(s), 6 row(s)
page 1 - cut at 100, 290
3 column(s), 6 row(s)
col kind filled role
0 date 83% when
1 text 100% -
2 amount 100% money
1 02/05/2026 | CARTE 28/04 AMAZON EU> | 12,40
2 03/05/2026 | VIR SEPA LOYER MAI | 750,00
3 05/05/2026 | CARTE 03/05 SNCF CONN> | 68,00
4 09/05/2026 | PRLV EDF | 91,32
5 12/05/2026 | CARTE 11/05 LE BOULAN> | 7,90
6 ! | TOTAL DES DEBITS | 929,62 <- column 0 (date) is empty
Everything printed was already computed and already public. What was missing was somewhere to read it all at once, in the order the reading made its decisions.
The cut names its ruler
A list of numbers with no unit is a riddle. cut at 1, 2 on a CSV sent somebody looking for a defect that was not there: those were the indices of the fields, and they were right.
page 1 - cut at 100, 290 a PDF: points, and points go unlabelled
page 1 - cut at characters 7, 22 a table pasted with spaces
page 1 - cut on the delimiter a CSV: the columns ARE the fields
page 1 - no boundary nothing lined up
Points stay unlabelled because they are what a page has always been measured in. On a delimited file the numbers say nothing a reader does not already know, so the line names what happened instead.
Text, and not an image
It goes into a terminal, a CI log, a bug report, a test’s expected value. It needs no canvas, no headless browser and no dependency. An image needs a screen.
The options
signature |
naming cell kinds turns on the kinds, and the rows that break the shape |
roles |
what each column was taken to be |
boundariesOf |
the cut to read the page by, when it is not the page’s own |
cellsOf |
your own cut, row by row (the one being debugged is yours, not the library’s) |
page |
only this page |
maximumRows, cellWidth |
how much to print |
Left without a signature, the cut and the fill rates are still shown, and those are what a wrong cut shows up in first.
boundariesOf matters more than it looks: readTable hands back the cut it used, and without passing it the heading and the cells would tell two different stories.
const { document, boundaries } = await readTable(file);
explainDocument(document, {
boundariesOf: (page) => boundaries[page.pageNumber - 1]
});
Also here
explainRows(rows, options): the same, for a reader holding rows that never came from a page: a paste, a CSV, an OCR line.describeAnomaly(anomaly): one anomaly, in the words of somebody looking at the page:column 0 (date) is empty,column 2 is not amount.
A truncated listing that looks complete is how a missing row gets read as a row that is not there, so what is not printed is counted out loud: ... 4 more row(s).
Put it behind your refusals
A refusal with no evidence is a dead end: nobody can believe it or contradict it. One of the applications built on this library attaches explainRows output to every refusal that says “I did not read this correctly”, folded under the message: invisible to whoever does not care, and decisive for whoever opens a ticket.