About the project
Ledger Witness
Ten invoices reconciled against their purchase orders by a 382 MB model running on my laptop — no cloud, no API key, no internet. The interesting question isn't whether it got them right. It's why you would believe it.
Ledger Witness answers that: every run emits a signed evidence bundle that anyone can re-execute. Same machine, same model, same bytes — or the bundle does not validate.
How it works
image → OCR (@qvac/sdk) → geometric reading order → local LLM → deterministic rules → signed bundle → replay
The LLM only transcribes. Deciding and correcting is the code's job, because code is reproducible and auditable. Reconciliation itself (line matching, PO totals, price and quantity deltas) is pure deterministic logic — no vectors, no fuzzy scoring.
Three levels of verification
| Level | What it proves | Result on the real OCR run |
|---|---|---|
| L1 — structure | ed25519 signature + hash chain of the bundle; runs on any machine | VALIDATES |
| L2 — inference replay | every model call re-executed, response hashes compared | 31/31 steps bit-exact |
| L3 — re-derivation | findings recomputed from the replayed outputs | MATCHES |
L2 covers all three capabilities — OCR, chat and VLM — reproducing byte for byte. That was the make-or-break checkpoint of the project, and it's reached on the full path, not on a shortcut.
The UI re-runs L1 on every page load and includes a live tampering bench: change one digit in the bundle and the seal turns red in front of you.
QVAC capabilities used
| Capability | How | Where |
|---|---|---|
| Chat / completions | qvac serve openai on 127.0.0.1:11434, alias extractor, temperature 0, seed 42 | recon/qvac.syn → qvac_chat |
OCR (OCR_LATIN) | @qvac/sdk loadModel + ocr() behind a local HTTP shim | qvac-shim/server.mjs → POST /ocr |
VLM (SmolVLM2-500M Q8_0 + mmproj) | completion() with an image attachment — second opinion on the invoice total | qvac-shim/server.mjs → POST /vlm |
| Embeddings | deliberately dropped — matching is deterministic, so vectors would add 600 MB and non-determinism for nothing |
All inference goes through exactly two files and there is no other path to a model. Each task returns {result, step}; the step carries the exact request plus the sha256 of the response — that is the unit of replay. Model sha256s come from the SDK registry and travel inside the bundle.
Why a 0.6B model is enough: nine deterministic defenses
Each one was born from an observed failure, not from a guess:
- Geometric reading order — the OCR does not return blocks in reading order; they are
grouped into rows by vertical center and sorted by X.
- Skew correction — degraded scans sit 3–7° tilted, which drifts a row ~36 px between
its first word and its last amount. The slope is estimated with the median of pairwise slopes, so mismatched pairs are ignored without magic thresholds.
- Unambiguous OCR fixes — an
Sglued in front of a number is a misread peso sign. The
sibling case (a 5) is genuinely ambiguous and is left alone — arithmetic resolves it.
- Per-row arithmetic coherence — if
qty × unit_price ≠ amount, the model mixed
columns; it retries with the concrete calculation in the prompt.
- Total-driven re-read, 6. row deduplication, 7. PO-number normalization,
- description-tolerant matching, and
- the VLM's second opinion — it nails the digits of the total in 7 of 7 invoices
(including the handwritten one), and the total is only corrected when the row sum and the VLM agree.
All repairs live in a single task (aplicar_reparaciones) shared by the pipeline and the verifier. If they ever diverge, L3 fails. That's the invariant that keeps the evidence honest.
Measured results
Windows 11, GTX 1050 3 GB, 7.36 GB RAM, Node 24, @qvac/cli 0.11.0, @qvac/sdk 0.17.1, Synsema v0.5.99. Model: Qwen3-0.6B-Q4_0 (382 MB).
| Reconciliation vs. ground truth | 6/10 overall — 5/5 on clean scans |
| Verification | L1 validates · L2 31/31 bit-exact · L3 matches |
| Extraction latency | ~1 s per invoice (OCR ~27 s/page) |
| RAM with model loaded | ~1.0 GB text + ~1.8 GB OCR |
| Tests | 26 green, all offline |
We also measured Qwen3-1.7B: same 6/10, 3× slower. Model size stopped being the lever — the deterministic defenses closed the gap. Swapping models is a config flag; not one line of `.syn` changes.
Where it breaks (honestly)
- Cross-machine bit-exact replay is not solved (kernels, batching, floating point).
What exists: deterministic replay on the same machine + structural verification (L1) anywhere.
- Reading degraded images is the weak link, not the rest of the system. With clean OCR
the pipeline is 100%. On phone photos the OCR emits TOTAL: $55.500, 0o — no larger LLM recovers information that isn't in the text.
- The business counter-argument: cryptographic proof does not replace the human
reviewer's signature. It makes it cheaper — verify in seconds instead of redoing the work.
Stack
Written in Synsema (.syn), a capability-gated language for agents, with a small Node shim for the SDK's native OCR/VLM addons. UI served by synsema serve app.syn.
.\tools\arrancar.ps1 # QVAC server + shim synsema run recon/main.syn # invoices -> findings -> signed bundle synsema run recon/verify.syn # L1 structure · L2 replay · L3 re-derivation
Hacki