The verification pattern
Gather, extract, reconcile, cite: the four-step shape every check in both products takes, and where the model is and isn't allowed to be.
Every check the platform runs (does this document exist, is it what it claims to be, does the correspondent match the record, does this balance have support, are these the right documents for this account) is the same four steps. The platform provides one shared component per step. A project supplies the required set, the schema to extract, the fields to fetch, and the comparator.
step component who decides output
───────── ───────────────────── ──────────────── ──────────────────────
1 gather doc_checker code which docs are present / missing / unreadable, each cited
2 extract doc_type_classifier + the model, with predicted type + confidence;
field_extractor confidence typed fields with page, bbox, confidences
3 fetch dwh_fetch code (allow-list) source-of-truth values, keyed by account
4 reconcile reconcile code (injected one FieldComparison per field → a VerificationFinding
comparator)
Step 1: gather
check_documents(subject, required_types, available_documents, exceptions) is a deterministic set reconciliation over the document inventory. It knows nothing about content. It emits a citation per decision (the document ID it found, or the absence it recorded) so a “missing” is as evidenced as a “present”. There is no model here, and there never was a good reason for one: the question is set membership.
Step 2: extract
Two shared agents, both vision-first with an OCR fallback (document reading).
classify_document_type predicts a type from a project-supplied allowed list, with a confidence and a citation. It does not decide whether the document is misindexed. That comparison, predicted versus indexed, is deterministic and lives in the project, because “how different is too different” is policy.
extract_fields reads each field on a per-document-type schema into an ExtractionField: raw value, normalised value, page, bounding box, OCR confidence, extraction confidence, and a review flag. The model’s entire job on the platform is this function. Everything downstream treats its output as claims to be checked, not facts.
Step 3: fetch
fetch_fields(account_id, field_ids) reads the source-of-truth values through the fetch registry. It is read-only by construction: every read is a parameterised SELECT over declared tables and columns, with one bound key. It groups direct fields by table so an account costs one query per table, resolves non-account keys through declared one-hop lookups, and splits results back by declared cardinality.
Step 4: reconcile
reconcile(check_id, subject_id, extractions, source_values, comparator, missing_evidence) compares field by field into a VerificationFinding. The comparator is injected by the caller: one project supplies last-four matching, name similarity, and tolerance rules approved by compliance; the other supplies seller-specific balance rules; the default is normalised case-insensitive equality. The roll-up is fixed and boring: any fail is a fail, any review or missing evidence is a review, otherwise pass.
Why the split is drawn here
The line between steps 2 and 4 is the line between what the document says and whether that is acceptable. A model is genuinely good at the first and should never be asked the second in a regulated workflow, because the second has to be explainable to an auditor and stable across model versions. Putting the comparator in the project, as a function over config, means a policy change is a diff to a YAML file and a regression run, not a prompt edit and a hope.
The contracts each step reads and writes are on data contracts.