A match found by looking is only evidence if the looking could have failed.

Inspiration

A register closes short by $47,350 CLP — more than a day of the wage the person on that register earns. There is no camera on the drawer and no missing-transaction alert. So somebody opens the day's sales and looks for a combination that adds up to the missing amount.

They find one. A cashier gets a meeting.

They were always going to find one. Attributing a till shortfall is a subset-sum instance on a dense set, and Chilean retail prices — whole pesos clustering on 590 / 990 / 1290 / 3490 — are about the densest input the problem admits. In that regime an exact match is guaranteed to exist in overwhelming numbers. A search that cannot fail is not evidence.

Every product in this space is built to find the explanation. Cuadre is built to count them, and to refuse when the count is large.

What it does

Give it a shift and a shortfall. It returns the exact number of subsets that sum to the shortfall, and then decides whether that number is allowed to become a statement about a person.

On the bundled 102-transaction shift, short by $47,350:

1,198,694,366,206,238,986,470 combinations add up to it exactly.

Twenty-two digits. And every register appears in those combinations between 0.94× and 1.04× as often as its share of the workload — which is precisely what chance alone produces. There is no signal. There was never going to be one.

So the app refuses:

This does not identify anyone.

Somebody looking for a combination that matches will always find one, so finding one carries no information.

Refusing is a branch of the return type, not an error path, and it is enforced by the test suite rather than by intention.

It also refuses constructively. It will tell you what a shortfall that did discriminate looks like on the same data: $1,180 has exactly one explanation, and so — by taking complements — does $227,400. Both edges of the range discriminate. The dense middle never does. That is the difference between "this proves nothing" and "here is what proof would look like."

How I built it — eight separable stages, none of them a model

  1. Integer boundary — rejects non-integer / non-positive amounts instead of coercing. Pesos have no minor unit; money is never a float here.
  2. Common-divisor reduction — divides amounts and target by their GCD. Exact, ~10× less work, and it answers an indivisible target for free.
  3. Forward prefix DP — exact bigint counts for every target up to the shortfall, in one pass.
  4. Backward suffix DP — the mirror tables.
  5. Inclusion convolution — per transaction, in how many explaining subsets it appears, from prefix ⊗ suffix rather than n re-runs of the DP.
  6. Lift against base rate — share of explanations ÷ share of shift, per register.
  7. Verdict + refusal branchno_explanation / unique / narrow / refused.
  8. Receipt — CI re-derives every figure and commits ci/latest.json.

There is no model, no API key, no backend and no network call anywhere in that list. The deployed page proves it rather than asserting it: it counts its own network requests after load, from the browser's own PerformanceResourceTiming, and prints the number. Score a hundred shifts and it stays at 0.

The audit invariant

The count is the only thing standing between a shift and an accusation, so it is checked against a second, independent implementation rather than against intuition — 200 randomised instances, exact BigInt DP against a saturating double-precision path, asserted equal wherever a double can hold the answer. Inclusion counts are separately checked against an independent size-stratified DP via the identity Σᵢ inclusions[i] = Σₖ k·(subsets of size k).

The receipt script cross-checks the headline number before writing and aborts rather than emitting a receipt if the two implementations disagree.

Challenges I ran into

The count does not fit in a double. I built the fast path first, on Float64Array with saturating adds. It was correct on every small test and then silently pinned at 2^53−1 on the real shift. The honest fix was to make exact bigint the primary path and demote the fast one to a cross-check — a headline number that had quietly become an approximation would have made the one figure the project asks you to trust the one figure you could not check.

Enumeration exploded. Listing the explanations for a unique verdict walked ~2^100 paths and froze the tab, because it only pruned when the remainder went negative. Suffix-sum pruning fixed it; a step budget backstops it. None of the bundled presets reach that path, but a judge typing their own shortfall against the 102-transaction shift does — which is the case a regression test now pins.

The work metric was measuring the wrong thing. I budgeted on cell count, but the inclusion pass multiplies hundred-digit integers, so its real cost tracks the magnitude of the answer, not the size of the table. The binding constraint turned out to be memory, not patience — the run holds every prefix and suffix table at once. Budgets are now set from the allocation, and crossing one drops the lift table while the count survives. That ordering is deliberate: the count is the claim; the lift table elaborates it, and an elaboration is the right thing to lose.

Where it breaks — published, not discovered

WORK_BUDGET 5,000,000 cells (hard refusal) · INCLUSION_BUDGET 1,500,000 (drops the lift table) · ENUMERATION_BUDGET 2,000,000 steps (reports a sample, not an enumeration). All in docs/LIMITS.md. A ceiling you found yourself is credibility; one a reader finds is a defect.

Competitors, and why they cannot ship this

Loss-prevention analytics sell exception reports that surface a suspect — their product is the shortlist, and a tool whose main output is "your shortlist means nothing" deletes the deliverable the buyer pays for. POS cash-management modules report variance per operator, which is the base rate, reported without the comparison that would make it meaningful.

The barrier is not engineering — the DP is a hundred lines. It is that their buyer is the employer, and this instrument's only job is to take conclusions away from the employer.

What I learned

That the interesting move was reversing the question, not answering it better. "Which transactions explain the loss?" has an answer, always, and the answer is worthless. "How many explain it?" is the same arithmetic pointed the other way and it settles the matter in one exact integer.

A note on the data

The bundled shift is generated — not real transactions, not real people. Handlers are register positions, never names, and nothing in the repository is an identifier that could belong to anybody. What is taken from reality is the shape of the amounts, and that shape is the mechanism rather than decoration.

Built With

Share this project:

Updates