contract - what an honest reading looks like

truecopy/contract

Three methods you must write, two with safe defaults, and the one rule the pipeline enforces - a reading that contradicts its document never comes back as sound.

The skeleton belongs to this library, the meaning to your project. This module knows of no transaction, no quarter and no invoice: it knows the shape of an honest reading, and it makes that shape compulsory.

import { readDocument } from 'truecopy/contract';

const result = readDocument(document, reader);
// { verdict, reading, selfCheck, discrepancy, repaired, refusal, rowsToReview }

Three methods are required

read(document){ records, header }

header is what the document says about itself without being a record — a declared total, a closing balance, a period. Without that second field the self-check has no raw material.

selfCheck(document, reading)SelfCheck

{ declared: number[], read: number, unit: string }
// or
{ nothing: 'why this document declares nothing' }

declared is a list because a document may announce several candidate values when its layout scattered a label from its number; the gap kept is the smallest.

Saying { nothing } costs a sentence, and that is the point: the oversight becomes something written down instead of a silent null.

rowsToReview(document, reading)ReviewableRow[]

Takes the document, not only the reading — a correction screen has to show the rows that were dropped, and those are not in the reading by definition. Each row carries raw, fields, an optional droppedBecause, and an optional where so a person can jump to it on the page.

Two are optional, and their defaults err toward refusing

Left out
repair nothing is attempted. An honest gap beats a patch-up.
refuse a reading that produced no record is refused.

Five methods before anything runs at all is a wall, and a wall in front of an interface has one predictable outcome: whoever meets it writes return null five times and ships. That is the very dodge the kit exists to catch — and the likeliest author of it is now a language model writing from the types.

So the two that have an honest default get one, and both defaults err the safe way.

The one rule enforced

A reading that contradicts its document never comes back as read.

It is deliberately weaker than “refuse as soon as the reading contradicts the document”: a statement that does not balance is still usable row by row, a career record is not. Imposing refusal would have excluded the first.

So you choose between refused and needs-review. You cannot choose to say nothing.

The verdict

read the reading holds, or the document declares nothing to check it against
needs-review the reading contradicts the document — show it to a person
refused your refuse returned a refusal, or no record came back

A Refusal carries title, explanation and next — always. A refusal with no way out is a dead end.

Repair, and why giving up is the default

repair(reading, discrepancy) {
	// Flipping ONE row changes the sum by twice its value, so the row sought
	// carries half the gap. None, or several: give up.
	const target = discrepancy.amount / 2;
	const candidates = reading.records.filter((row) => row.amount === target);
	if (candidates.length !== 1) return null;

}

An arbitrary choice among several candidates is worth less than an honest gap. When a repair lands, the self-check runs again — a repair that is not re-checked is a guess with better manners.