classify - is this the kind of document expected
truecopy/classify
A statement quoting the word invoice in a transaction label must not be filed as an invoice. Precedence, stated rather than smuggled into an ordering.
A bank statement quotes the word invoice in a transaction label. Read in the wrong order it is filed as an invoice, and every operation it carries leaves the budget with it.
An IBAN, or the debit/credit/balance vocabulary, settles the question before the word invoice is even looked at.
import { classifyDocument, type DocumentKind } from 'truecopy/classify';
const KINDS: DocumentKind<'statement' | 'invoice'>[] = [
{
name: 'statement',
requires: [{ anyOf: [{ all: [IBAN] }, { all: [DEBIT, CREDIT] }] }]
},
{
name: 'invoice',
requires: [{ anyOf: [{ all: [INVOICE_WORDS] }], absent: [IBAN] }]
}
];
classifyDocument(text, KINDS); // 'statement' | 'invoice' | null
Three notions, and no more
| pattern set | { all: [...], occurrences?: n }: all of its patterns, optionally several times |
| requirement | { anyOf: [...sets], absent?: [...] }: any one of its sets, and none of the absent ones |
| kind | all of its requirements. First match wins. |
absent is what makes precedence stated rather than smuggled into an ordering that means something else. An invoice only when there is no IBAN is written down, in the kind itself, where somebody reading it a year later will find it.
occurrences: one may be a quotation
{ all: [PENSION_VOCABULARY], occurrences: 2 }
A newspaper article about pensions mentions quarters, carries years and amounts, and would satisfy every other rule. A real record carries the vocabulary dozens of times.
One occurrence may be a citation. Two rarely are.
This is not the schema question
classify asks is this the kind of document expected at all. schema asks does this reading carry what it must. Two answers to one question always end up disagreeing, so they are two functions and the kind is asked first: a document of the wrong kind deserves no remark about its missing fields.
Patterns as data
The patterns here are regular expressions, and everything that varies by market, by issuer or by document family should be a value rather than a branch; see pattern. A value can be served by a backend, refined by whoever reads the documents, versioned, and swapped for another market without a deploy. A branch can only be changed by whoever can change the code.