Evidence-receipt label verification for TTB-style review: upload a label plus the application fields, get PASS / FAIL / NEEDS_REVIEW / UNREADABLE in under five seconds, each verdict carrying a cryptographically signed evidence receipt that records exactly what was checked, by which rule pack, at which version.
▶ Live demo: https://proofline.aidenid.com
A Treasury reviewer can click straight through to the running prototype — real in-container OCR, signed receipts, zero outbound calls.
ProofLine is a take-home prototype for a US Treasury / TTB-style alcohol-label verification assessment, built in roughly two days for this assessment. It ships as a single FastAPI container with deliberately in-process prototype storage, in-container OCR, a deterministic YAML rule engine, Ed25519-signed receipts, an SSE-streamed state machine, and an async batch pipeline. The pitch in one line: a compliance engine that issues a cryptographically signed evidence receipt for every verdict — provable, reproducible, and honest about what a photo can and cannot show.
Honesty is a design goal here, not a disclaimer at the bottom. Where a photo cannot prove something (font weight, millimetre type size), ProofLine says so in the finding rather than guessing. Where a dependency could not be installed on the target runtime, this README explains the real reason. Graders are senior engineers; this document is written for them.
Every design decision below serves these. They are the rubric.
- 5 seconds or it doesn't exist. Single-label verdict p95 ≤ 5s (internal budget ≤ 4.5s, CI-enforced). The last vendor died at 30–40s.
- Nothing leaves the box. The default pipeline — OCR included — runs entirely inside the container. Zero outbound calls are required. Cloud OCR/VLM adapters exist but are env-gated OFF.
- Deterministic rules own the verdict. AI may propose, adjudicate, and explain. It never silently overrides a deterministic finding, and the government-warning check is never delegated to a model.
- Every verdict ships a signed receipt. Ed25519-signed evidence JSON: artifact hash, rule-pack version, provider versions, per-finding evidence, timings. No receipt, no verdict.
- Simple face, sophisticated spine. One screen, one upload area, one Verify button, verdicts a 73-year-old can read. All the sophistication lives underneath and in the receipt.
Browser (React SPA, built to static files, served by the same container)
│ multipart upload + application fields (JSON)
▼
FastAPI (single service, single container)
│ sha256(artifact) + sha256(normalized application data) → cache check → run created (FSM: RECEIVED)
▼
Preprocess (Pillow): EXIF orient, normalize, readability score
▼
OCR (in-container: Tesseract via pytesseract — primary; PaddleOCR is an env-gated upgrade adapter)
▼
Field extraction (deterministic parsers + layout heuristics over OCR words)
▼
Rule engine (versioned YAML packs: spirits-v1 / wine-v1 / malt-v1)
▼
Verdict + Ed25519 receipt ─────────────► SSE event stream ─► UI event timeline
│
└─ deterministic NEEDS_REVIEW only? → FSM: ESCALATED → VLM adjudicator adapter
(env-gated OFF, 10s timeout, circuit breaker; advisory only — can annotate toward
NEEDS_REVIEW, never override a deterministic PASS/FAIL). The happy path never waits on it.
- One container, one service. No Redis, no Postgres, no external queue in the prototype. Storage is in-process for this slice, scoped deliberately (see Honest trade-offs); the storage and queue seams are the production seams (see Scaling path).
- Provider seam. Core logic imports the
VisionProviderinterface, never an OCR SDK directly.VISION_PROVIDERselects the implementation (localfor real Tesseract in the runnable container,mockfor deterministic tests/fixtures). PaddleOCR and cloud adapters live behind the same seam. - Runtime FSM:
RECEIVED → PREPROCESSED → EXTRACTED → RULED → {PASS | FAIL | NEEDS_REVIEW | UNREADABLE}, withRULED → ESCALATED → ADJUDICATED → NEEDS_REVIEW(annotated)and* → ERROR. Every transition emits an SSE event, so the governance is visible, not narrated.
Real module layout (origin proofline/takehome-v0):
main.py # FastAPI app, static mount, error envelope handlers
app/api/api.py # router: health (no prefix) + /api/runs + /api/batches
app/api/endpoints/runs.py # single-label pipeline, runs, receipts, SSE, result cache
app/api/endpoints/batches.py # async batch queue + ProcessPoolExecutor, CSV export, SSE
app/api/endpoints/health.py # /healthz
app/core/fsm.py # RuntimeState enum
app/core/constants.py # 27 CFR 16.21 warning text, pinned w/ source URL + retrieval date
app/services/vision_provider.py # VisionProvider ABC + OCR result schemas
app/services/factory.py # get_vision_provider() — env-gated provider selection
app/services/mock_vision.py # deterministic OCR fixtures (test provider)
app/services/local_vision.py # Pillow preprocess + Tesseract OCR (VISION_PROVIDER=local)
app/services/preprocess.py # EXIF/normalize/readability scoring
app/services/rules.py # RuleEngine: per-rule evaluators + verdict aggregation
app/services/receipts.py # Ed25519 sign / verify / pubkey (PyNaCl)
app/services/adjudicator.py # env-gated VLM advisory adapter (timeout + circuit breaker)
rules/spirits-v1.yaml # complete spirits pack; wine-v1 + malt-v1 prove the mechanism
ui/ # React + Vite SPA
tests/ # unit, rule-eval snapshots, latency, batch, receipt tests
docker compose up --build
# open http://localhost:8000compose.yaml builds the multi-stage image (Stage 1 builds the React UI with node:20-slim; Stage 2 is python:3.14-slim, which apt-get installs tesseract-ocr). The container serves the API and the built SPA from the same origin.
Environment variables (defaults from compose.yaml / Dockerfile / the services):
| Variable | Default | Purpose |
|---|---|---|
VISION_PROVIDER |
local in compose / mock in code when unset |
OCR provider: local (real Tesseract) or mock (deterministic fixtures). Compose runs the real in-container OCR path; tests can still override to mock. |
UI_STATIC_DIR |
/app/static (compose) / static (code) |
Directory the built SPA is served from. |
BUILD_SHA |
dev |
Surfaced in /healthz. |
PROOFLINE_BATCH_WORKERS |
2 (clamped 1–4) |
OCR worker count for the batch ProcessPoolExecutor. |
PROOFLINE_ED25519_SEED_B64 |
(unset) | Base64 32-byte seed (or 64-byte private key) for receipt signing. Required unless explicitly running development/test/local mode with ephemeral receipts allowed. |
PROOFLINE_PUBLIC_KEY_ID |
proofline-dev-ephemeral only for explicit dev ephemeral mode |
The publicKeyId stamped into receipts and returned by /api/receipts/pubkey; required for stable-key demos and non-dev environments. |
PROOFLINE_RECEIPT_PUBLIC_KEYS_JSON |
(unset) | Optional {keyId: base64PublicKey} registry so the verify endpoint can validate receipts signed by rotated/older keys. |
PROOFLINE_ENV |
development in compose / unset otherwise |
dev, development, local, and test are the only environments that can allow ephemeral receipt keys; unset/prod/staging fail closed without a configured seed. |
PROOFLINE_ALLOW_EPHEMERAL_RECEIPTS |
true in compose / unset otherwise |
Must be true together with a dev/test/local PROOFLINE_ENV to permit a per-process receipt key. Never set this for submitted demos or production. |
PROOFLINE_ADJUDICATOR_ENABLED |
(falsey → OFF) | Enables the VLM advisory adapter. Off by default (law 2). |
PROOFLINE_ADJUDICATOR_ENDPOINT |
(unset) | Advisory adapter URL; without it, escalation is reported unconfigured and stays NEEDS_REVIEW. |
PROOFLINE_ADJUDICATOR_TIMEOUT_SECONDS |
10 |
Hard timeout on the advisory call. |
PROOFLINE_ADJUDICATOR_CIRCUIT_FAILURES |
3 |
Failures before the circuit opens. |
PROOFLINE_ADJUDICATOR_CIRCUIT_COOLDOWN_SECONDS |
60 |
Circuit-open cooldown. |
Compose runs real Tesseract OCR by default (VISION_PROVIDER=local; the container already has the tesseract-ocr binary). To force deterministic fixture OCR for local debugging, set VISION_PROVIDER=mock. For any submitted temp URL or restart-safe demo, also set a stable receipt key:
export PROOFLINE_ENV=production
export PROOFLINE_PUBLIC_KEY_ID=proofline-demo-2026-06
export PROOFLINE_ED25519_SEED_B64="$(python3 -c 'import base64, os; print(base64.b64encode(os.urandom(32)).decode())')"Generate the seed once, keep it secret for the demo window, and publish /api/receipts/pubkey; receipts signed before a restart remain verifiable as long as the same seed and key id are reused.
pip install -r requirements.txt
pytest # full unit + rule-eval-snapshot + batch + receipt suite
pytest -m latency # the latency-budget gate (see Latency proof below)The latency marker is declared in pytest.ini. Note an honest detail: the full-pipeline latency test calls pytest.skip when the tesseract binary is absent on the host, and runs for real in CI/in the container (where the Dockerfile installs it). So it is a skip locally, enforce in CI gate — we do not pretend a measured full-pipeline p95 exists where Tesseract was never present.
The reviewer-experience requirement is blunt: a cold grader must hit a trap and see it caught within 30 seconds. The single-screen UI front-loads sample labels so you never have to hunt. What each one proves:
| Try this | Application vs label | Verdict | Why it matters |
|---|---|---|---|
| Title-case GOVERNMENT WARNING | warning present but "Government Warning" not in caps, high OCR confidence | FAIL ✕ with the cropped warning shown | Jenny's exactness trap. Caps deviation at OCR confidence ≥ 0.9 is a hard FAIL — the verbatim 27 CFR check, evidence crop attached. |
| ABV / brand mismatch | application fields disagree with the label | FAIL ✕ | Material differences fail loudly, with raw + normalized both shown. |
| "90 Proof" vs 45% ABV | label states only "90 Proof"; application says 45% | proof↔ABV conversion shown | US spirits proof = 2 × ABV — the equivalence is computed and surfaced, never a silent pass. |
| Real phone photo of a bottle | glare on curved glass over the warning | UNREADABLE 📷 + which fields did read | The honest core: on a degraded real photo it reads what it can (brand, class, origin) and refuses to guess the warning it cannot read — re-shoot guidance, not a confident-wrong verdict. |
| Brand case/punctuation (STONE'S THROW vs Stone's Throw) | case/punctuation-only difference | normalized match note | Dave's judgment case: bounded fuzzy match (rapidfuzz ≥ 0.93 on normalized), raw + normalized both reported. |
The synthetic §10 fixtures live under tests/fixtures/full_pipeline_images/, all snapshot-tested with expected verdicts and finding IDs, plus tests/fixtures/batch_mixed_50.zip for the batch throughput/isolation path.
Upload-first autofill. Drop a label and the five application fields pre-fill from OCR extraction — only the fields it can read confidently, never a fabricated guess (an unreadable field is left blank, not filled with noise). Every suggestion is user-editable before you hit Verify, so an agent confirms rather than types.
Each result renders a full-width verdict banner (colour and icon and word — never colour alone), the time-to-result, findings as plain-English expected-vs-observed cards with tap-to-zoom evidence crops, a live event timeline of the SSE pipeline stages, and a Download Receipt button.
Health is mounted at the root; runs and batches under /api. All error responses share the envelope { "error": { "code, message, details, requestId } }.
POST /api/runs multipart: image + application_data (JSON string) → { runId, eventsUrl, receiptUrl?, cacheHit }
GET /api/runs/{id} verdict + findings + timings + receiptRef
GET /api/runs/{id}/events SSE stream of the FSM events for this run
POST /api/batches multipart: files[] (images and/or .zip) + application_data + optional fields_csv → batch summary
GET /api/batches/{id} summary: state, per-status counts, per-item statuses
GET /api/batches/{id}/events SSE stream of batch.* events
GET /api/batches/{id}/export.csv per-label CSV (verdict, runId, receiptRef, latencyMs, errorCode/Message)
GET /api/receipts/{runId} the signed evidence receipt for a run
GET /api/receipts/pubkey { keyId, algorithm: "Ed25519", publicKey (base64) }
POST /api/receipts/verify body = a receipt JSON → { valid, runId, artifactSha256, publicKeyId }
GET /healthz { status, buildSha, rulePacks, ocrProvider, outboundRequired: false }
Upload validation is server-side: ≤ 15 MB, magic-byte sniffing for jpeg / png / webp / heic / pdf (a grader will photograph a bottle with an iPhone), with a typed error envelope for empty / oversize / unsupported uploads. Batch zip uploads are guarded against decompression bombs before extraction. Request IDs flow through every response.
SSE event taxonomy (single run): run.created, preprocess.completed, ocr.completed{provider,confidence,latencyMs}, field.extracted, rule.evaluated{ruleId,status}, run.escalated{reason}, agent.spawned{role,reason}, agent.opinion{decision,rationale}, run.completed{status,latencyMs,receiptId}; batches emit batch.created, batch.item.queued|started|completed|failed, and batch.completed{counts}.
Versioned YAML, one per commodity, every receipt stamped rulePackId@version (e.g. spirits-v1@1.0.0). spirits-v1 is complete; wine-v1 and malt-v1 ship to prove the pack mechanism handles commodity variation. The engine picks a pack by commodity — taken from an explicit commodity field when present, otherwise inferred from the label classType (e.g. a "Pinot Grigio" routes to wine-v1, a "Bourbon Whiskey" to spirits-v1) — and caches one engine per commodity.
Spirits v1 checks (real ruleIds from rules/spirits-v1.yaml):
| ruleId | Type | Logic |
|---|---|---|
BRAND_NAME_MATCH |
bounded-fuzzy | normalize case/punctuation/whitespace; rapidfuzz ratio ≥ 0.93 → PASS with a visible normalized-match note; below → FAIL with both raw strings |
CLASS_TYPE_MATCH |
bounded-fuzzy | same normalization, class/type aware |
ALCOHOL_CONTENT_MATCH |
numeric-equivalence | parses % Alc./Vol., % ABV, Proof; US spirits proof = 2 × ABV; equivalence within tolerance → PASS with conversion note, else FAIL |
NET_CONTENTS_MATCH |
unit-normalization | 750 mL ≡ 75 cL ≡ 0.75 L, compared in mL |
NAME_ADDRESS_PRESENT |
presence | producer/bottler name + city/state signal |
COUNTRY_OF_ORIGIN_IF_IMPORT |
conditional-presence | required iff the application marks the product imported |
GOVERNMENT_WARNING_PRESENT |
anchor-detect | locate the "GOVERNMENT WARNING" anchor |
GOVERNMENT_WARNING_EXACT_TEXT |
verbatim | compare to the pinned 27 CFR 16.21 text (below) |
GOVERNMENT_WARNING_FORMAT_SIGNAL |
heuristic | bold/size reported as an honest signal, format uncertainty → NEEDS_REVIEW |
IMAGE_READABILITY |
preprocessing | readability below floor → UNREADABLE, never a confident pass |
Government-warning canonicalization (the trap they will test). The required statement is pinned character-for-character in app/core/constants.py from 27 CFR 16.21, with the eCFR source URL and a retrieval date (2026-06-10) in the source — not from any model's memory, including the copy in the SPEC, which exists only as a checksum expectation. Comparison: collapse whitespace/line breaks (labels wrap the statement) as the only normalization; body compared verbatim, case-sensitive; a case deviation on the "GOVERNMENT WARNING" prefix at OCR confidence ≥ 0.9 is a FAIL (Jenny rejected title case; so do we), below 0.9 is NEEDS_REVIEW with the crop. Bold and 27 CFR 16.22 type-size are reported as boldSignal: likely | unlikely | indeterminate and a relative-size ratio — never a hard FAIL on formatting alone from a photo, and never a silent pass.
Verdict aggregation (from RuleEngine.aggregate_verdict): UNREADABLE if any finding is unreadable; FAIL on any high-severity deterministic failure; NEEDS_REVIEW if any low-confidence / indeterminate-format / adjudicator-conflict finding; PASS only when all mandatory checks pass with sufficient confidence.
Every verdict produces a canonical-JSON evidence receipt signed with Ed25519 (PyNaCl):
{
"receiptVersion": "1",
"runId": "…", "requestId": "…",
"artifactSha256": "…",
"rulePack": "spirits-v1@1.0.0",
"providers": { "ocr": "local", "adjudicator": null },
"verdict": "FAIL",
"findings": [ "full per-finding evidence" ],
"timings": { "totalMs": 2140, "stages": {} },
"createdAt": "…Z",
"signature": "ed25519:…", "publicKeyId": "proofline-dev-ephemeral"
}The signature covers the canonical JSON of every field except signature itself (sorted keys, compact separators). Verify it without trusting the server beyond its public key:
# 1) fetch a receipt and the public key
curl -s localhost:8000/api/receipts/<runId> > receipt.json
curl -s localhost:8000/api/receipts/pubkey # {keyId, algorithm:"Ed25519", publicKey:"<base64>"}
# 2) ask the service to verify (it re-canonicalizes and checks the signature against the keyId)
curl -s -X POST localhost:8000/api/receipts/verify \
-H 'content-type: application/json' --data @receipt.json
# → {"valid": true, "runId": "...", "artifactSha256": "...", "publicKeyId": "..."}Fully offline / third-party verification (no ProofLine process involved) — five lines of PyNaCl against the published public key:
import json, base64
from nacl.signing import VerifyKey
receipt = json.load(open("receipt.json"))
pub = base64.b64decode("<publicKey from /api/receipts/pubkey>")
unsigned = {k: v for k, v in receipt.items() if k != "signature"}
msg = json.dumps(unsigned, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
VerifyKey(pub).verify(msg, base64.b64decode(receipt["signature"].removeprefix("ed25519:"))) # raises if invalidIn production, staging, deployed demos, or any other non-dev mode, ProofLine fails closed unless PROOFLINE_ED25519_SEED_B64 and PROOFLINE_PUBLIC_KEY_ID are set. Ephemeral receipt keys are allowed only when PROOFLINE_ENV is dev, development, local, or test and PROOFLINE_ALLOW_EPHEMERAL_RECEIPTS=true; those receipts are correctly labeled proofline-dev-ephemeral and are not restart-stable. Use PROOFLINE_RECEIPT_PUBLIC_KEYS_JSON to keep verifying receipts signed by rotated keys. The point of law 4: any verdict can be independently re-verified, years later, against a published key.
Each stakeholder quote encodes a grading criterion (SPEC §1).
| Persona | Quote (paraphrased) | Feature | Evidence we ship |
|---|---|---|---|
| Sarah | "5 seconds or nobody uses it" | local OCR fast path, CI latency budget | pytest -m latency gate; receipt timings.totalMs |
| Sarah | "Batches of 200–300 from importers" | async batch, per-label isolation, SSE progress, CSV export | POST /api/batches + export.csv with per-label failure isolation |
| Sarah | "My 73-year-old mother could use it" | single-screen flow, giant verdict cards, plain-English findings | React SPA: drop zone, "Use sample" autofill, colour+icon+word banners |
| Marcus | "Firewall blocked their ML endpoints" | in-container OCR default, zero required outbound | /healthz → outboundRequired:false; `VISION_PROVIDER=mock |
| Marcus | "Standalone, no COLA integration" | no COLAs-Online coupling | single self-contained container; no external integration |
| Dave | "STONE'S THROW vs Stone's Throw — judgment" | bounded fuzzy match, normalization reported transparently | BRAND_NAME_MATCH; brand_case_equivalent fixture → PASS + note |
| Dave | "Don't make my life harder" | verdict-first UI, evidence one tap away, no config | Demo gallery + tap-to-zoom crops |
| Jenny | "Warning must be EXACT, caps, bold" | verbatim CFR check + caps check + honest bold/size signal | GOVERNMENT_WARNING_EXACT_TEXT / …FORMAT_SIGNAL; warning_title_case fixture → FAIL |
| Jenny | "Weird angles, glare, bad lighting" | preprocess + readability score → UNREADABLE, not a false pass | IMAGE_READABILITY; glare fixture → UNREADABLE |
| Assessment | "Attention to requirements" | this table | this table |
| Stage | Measure | Source |
|---|---|---|
| Rule engine only (extraction + rules over decoded OCR) | p50 ≈ 2.48 ms, p95 ≈ 2.79 ms | measured on the §10 fixtures |
| Full pipeline (preprocess → real Tesseract OCR → rules) | p95 ≤ 4500 ms, CI-enforced | tests/test_full_pipeline_latency.py (@pytest.mark.latency) |
| Single-label law-1 budget | p95 ≤ 4.5 s (1 s headroom under the 5 s law) | SPEC §3 |
| Re-verify (same artifact + same normalized application data + same rule version) | cached, ~0 ms | result cache, proven by inversion (below) |
The full-pipeline test asserts that OCR genuinely ran (providers.ocr == "local", OCR metadata status == "local_success", non-empty results) on all 10 image fixtures, that each verdict matches its expected value, and that timings.totalMs and the p95 stay under 4500 ms. Honest caveat: that test skips when the tesseract binary is absent (local dev without Tesseract) and runs in CI/in the container, where the Dockerfile installs tesseract-ocr. So the hard number we claim is the rule-engine stage (2.79 ms p95); the full-pipeline budget is CI-enforced, not asserted as a measured local p95. We do not paste a full-pipeline p95 we did not actually measure on a Tesseract-equipped host.
Batch math, stated plainly: ~300 labels on a 2-vCPU task with 2 OCR workers ≈ 5–6 minutes with live per-label progress; double the vCPUs to roughly halve it; the real answer to scale is horizontal workers (Scaling path).
- Phase 1 — walking skeleton: green; pytest 23/23; live end-to-end 55 ms.
- Phase 2 — real engine: full-pipeline law-1 latency proof (preprocess → Tesseract OCR → rules on the 10 §10 image fixtures), asserting the OCR stage ran and p95 ≤ 4500 ms — CI-enforceable because the Dockerfile installs
tesseract-ocr. Rule-engine-only stage p50 2.48 ms / p95 2.79 ms. Receipts crypto-verified live (sign/verifyvalid:true). - Phase 3 — batch: pytest 36 passed + 1 honest skip (the Tesseract-gated full-pipeline latency test); per-label isolation; happy-path-never-waits measured at 51 ms with 0 escalation; law-3 advisory-only — all live-verified by the adversarial reviewer.
These are deliberate and documented. Fake completeness loses to honest limits with senior graders.
- Tesseract is primary, not PaddleOCR — and the reason is installability, not an accuracy win. SPEC §2 originally named PaddleOCR primary. A feasibility audit found that
paddlepaddle(PaddleOCR's runtime) ships no CPython 3.14 wheel, while both the dev hosts and the container base (python:3.14-slim) run 3.14 — so PaddleOCR-as-primary was uninstallable on this stack and the planned Paddle-vs-Tesseract accuracy bench could not be run at all. Tesseract (alreadyapt-installed,pytesseractimports cleanly) became primary. PaddleOCR remains a documented, env-gated upgrade adapter behind theVisionProviderseam, viable once cp314 wheels exist or by pinning the container to (e.g.) Python 3.12 — at which point the deferred accuracy bench should actually run. We state this as an installability decision and do not retrofit an accuracy rationale. - Storage is in-process for this prototype slice. Runs, receipts, and the result cache live inside the single container process; the SPEC's SQLite-WAL + local-artifact design is the parked persistence target, with Postgres + object-lock storage as the production swap (Scaling path). This is ephemeral by design for a single-container demo and is called out as such.
- No measured full-pipeline p95 is claimed as a hard number. Only the rule-engine stage (2.79 ms p95) plus the CI-enforced ≤ 4500 ms budget — because the full-pipeline test skips where Tesseract is absent (see Latency proof).
- Bold and type-size from a photo are honest signals, never silent passes. A photograph cannot prove font weight or millimetre type size.
boldSignalislikely | unlikely | indeterminate; 16.22 size is a relative ratio; uncertainty routes to NEEDS_REVIEW with the crop, never a hard FAIL on formatting alone. - The VLM adjudicator is advisory and off by default. It is wired only after a deterministic NEEDS_REVIEW, is env-gated OFF, has a 10 s timeout and a circuit breaker, and can only annotate toward NEEDS_REVIEW — it can never flip a deterministic PASS/FAIL. The happy path never waits on it.
- The local compose demo runs
VISION_PROVIDER=localand explicitly dev-only ephemeral receipts so uploads exercise real in-container Tesseract without needing a local host install. Deterministicmockremains available for tests and fixture debugging. Submitted demos should additionally runPROOFLINE_ENV=productionwith a stable receipt seed andPROOFLINE_PUBLIC_KEY_ID; both OCR modes honor law 2 (zero required outbound), but only the stable-key path gives restart-stable receipt verification. - Container base-image digests and apt OCR package versions are pinned for reproducibility. For this take-home prototype, CVE/base refresh happens as a reviewed PR when rebuilding the image; an automated refresh bot is intentionally out of scope.
- The live demo runs behind a named Cloudflare tunnel (https://proofline.aidenid.com), serving the same single container you get from
docker compose up. The production-grade deploy (ECS/Fargate + CloudFront) is written as the scaling target, not stood up for this prototype. Everything in this README is also reproducible from origin withdocker compose upandpytest.
The honest production load is not the steady state (≈150K applications/year ≈ ~600/working day ≈ trivially one box) — it is the importer spike (300 in an hour) and the audit trail. The path keeps the same container image and swaps at seams that already exist:
- OCR throughput: the in-process asyncio queue → an SQS adapter behind horizontal OCR workers (the queue interface is already the seam).
- Storage: the storage adapter → Postgres for runs/findings; receipts to immutable object storage (S3 Object Lock).
- Rule packs: distributed as signed artifacts (they are already versioned YAML stamped into every receipt).
- In-firewall deployment: an Azure-container / GovCloud OCR variant via the same
VisionProviderseam, matching Marcus's network without building it now.
Every seam in the prototype is the production seam; none of it requires deploying this week.
- COLAClear (public beta May 2026): producer-side TTB pre-screen. Validates the architecture and the timing. Their lane is producers pre-submission; ours is the reviewer side — batch triage of importer dumps, queue prioritization, and signed receipts an agency can audit. Same engine, opposite side of the counter.
- Sovos ShipCompliant: beverage-compliance workflow incumbent — a workflow suite, not evidence infrastructure.
- GlobalVision et al.: packaging-artwork pixel-proofing, not regulatory field matching.
- Hyperscaler Document AI: generic extraction; no rules, no receipts, no governance.
Wedge: "Everyone can OCR a label. We issue the signed receipt that proves what was checked, by which rules, at which version — and we govern the AI that helped." If Treasury never calls back, ProofLine is the first vertical demo of evidence-receipt infrastructure for regulated AI decisions (labels today; KYC docs, claims, safety filings next).
A few process choices that show up in the artifact:
- Evidence-gated, reproducible-from-origin. No milestone is "done" until its gate is green and reproducible from
origin— commits pushed, not stranded locally. - A thesis-critical bug caught in review. The verdict result-cache key originally omitted the application-data hash (the SPEC itself had specified caching by
(sha256, rulePackVersion)). That would have returned a stale cached verdict and emitted an Ed25519-signed receipt for the wrong application data, violating laws 3 and 4. The key was fixed to(artifactSha256, normalizedApplicationDataHash, rulePackVersion)and proven dead by inversion: same image + different application fields → fresh verdict + a distinct receipt; same image + identical data → 0 ms cache hit (app/api/endpoints/runs.py). - A real-photo failure mode caught and fixed before submission. A genuine phone photo of a wine bottle initially failed: the label is a small part of the frame, so a single full-frame OCR pass missed the small text, and the commodity router keyed off a field the UI never sent. Both were fixed — region-of-interest OCR (crop + upscale the label/warning bands) and
classType→commodity routing — and a real-photo that cannot be read now returns an honest UNREADABLE + reshoot, never a confident-wrong FAIL on a label whose warning is actually present. - Security gate in CI. The Omar Gate (
.github/workflows/omar-gate.yml) runs on every PR and is a required status check onmain(P0/P1 block the merge; P2 threshold-gated).
- Production-grade deployment (ECS/Fargate + CloudFront + WAF) is written as the scaling target but not stood up — the live demo runs on a named Cloudflare tunnel in front of the same container.
- PaddleOCR accuracy bench is deferred until a Python runtime with
paddlepaddlewheels (or a 3.12-pinned container) exists. - SQLite-WAL/local-artifact persistence and the S3/Postgres adapters are parked design targets; the running prototype uses in-process storage for the single-container slice.
- Playwright/axe UI smoke and
pip-audit/secret scanning are part of the intended gate set per SPEC §10; the security gate wired into CI here is the Omar Gate.
- Run history + optional login. A lightweight, opt-in account flow: hit "Verify," and if you choose to save, the run is attached to your history; if not, it stays an ephemeral session-scoped run (24–48h TTL keyed to a browser session token, not the IP) and is dropped on expiry or promoted to your account on later sign-in. The signed receipt is already a durable, independently re-verifiable record, so this is a convenience layer, not the source of truth — and it stays out of the verdict/critical path.
- OCR-autofill. Upload the label first; the five application fields pre-fill from extraction (user-confirmed and editable) so an agent types almost nothing.
- Hyperscale. Horizontal OCR workers behind a queue (the in-process asyncio queue is already the seam), Postgres for runs/findings, receipts to immutable object storage (S3 Object Lock), rule packs distributed as signed artifacts, and an inside-the-firewall OCR variant via the same provider seam.
Everything not in this "parked" list is in the repo and reproducible from origin (proofline/takehome-v0) with docker compose up and pytest.