Developer tooling · Eval & regression

eval-harness

A golden-set harness that scores an LLM / extraction pipeline, judges the fields where exact match is unfair, and fails CI when quality regresses. Offline, reproducible, no keys required — docker compose up and go.

Public · synthetic demo
The problem

You changed a prompt, a regex, a model version. Did quality go up or down?

"It looks fine on a few examples" is how silent regressions ship. A pipeline that quietly drops from 100% to 48% passes every eyeball test. The fix is to turn "looks fine" into a number, and turn that number into a build gate.

Input

A 25-row golden set + a tiny extractor as the system under test

data/goldenset.jsonl   (25 labeled rows)
{"raw": "2024-03-14 STARBUCKS #1234 SEATTLE WA $6.75",
 "date": "2024-03-14", "vendor": "STARBUCKS",
 "amount": "6.75", "category": "Meals"}

System under test: an expense-line extractor (date · vendor · amount · category).
Swap in your own pipeline by replacing one function — the harness only sees inputs and outputs.
The money shot

Catch the regression → judge fairly → gate the build

① The gate catches a real bug

A defect we actually hit: the amount parser grabbed the first number in the line — a store #, ZIP, or order id — instead of the dollar amount.

g01: "2024-03-14 STARBUCKS #1234 ... $6.75"
     got amount = 1234.00   (grabbed the store #)   want 6.75
48% → 100%row-correct (before → after)
48% → 100%amount field accuracy
13rows the harness flagged

A human eyeballing five rows misses this. The harness sees it — and the CI gate (≥ 90%) blocks the buggy build from shipping.

② Exact match is unfair — so a judge grades it

The pipeline emits colloquial labels (Dining, Grocery) while the golden set uses canon (Meals, Groceries). Exact string match punishes a correct answer:

category scored by…accuracy
exact string match0.0%
LLM-as-judge (semantic)100.0%

Default judge is a deterministic synonym table (zero deps, reproducible CI). Set EVAL_JUDGE=llm + an API key to use a real model for synonyms the table has never seen.

③ A gate that holds — and a judge that's checked too
PASS ✅fixed pipeline, gate ≥ 90%
FAIL ❌buggy pipeline (correctly blocked)
90%judge self-check agreement
4/4contract tests pass
CI stepWhat it provesResult
pytestthe gate contract holds4/4
gate on golden setrow-correct ≥ 90%100%
bug still caughtbuggy run must fail the gate

The judge self-check is honestly < 100%: the offline judge misses Java=coffee. That miss is why the real LLM judge exists — shown, not hidden.

How it's verified

Deterministic, offline, tested.

Scoring is plain code (regex + set logic), so results are reproducible and reviewable. docker compose up and CI run with no API key and no network. pytest encodes the gate as a contract — and if a buggy pipeline ever slips past the gate, CI itself fails, because a blind harness is worse than none.

Honest limitations
· The golden set is small (25 rows) — a demonstrator, not a benchmark. Real coverage means dozens–hundreds of labeled rows per failure mode.
· The offline judge only knows seeded synonyms; novel phrasings need EVAL_JUDGE=llm (tokens, latency, non-determinism — mitigate with temperature=0 + caching).
· LLM-as-judge has known biases (verbosity, self-preference). Judged scores are a signal, not ground truth; structured fields stay on exact match.
· The shipped bug is a realistic defect deliberately included so the before/after is reproducible — not scraped from a client repo.
This is a synthetic sample demonstrating the method. Inspect the code, golden set, and CI gate ↗