# OCR Eval Harness

Field-level accuracy measurement for the artifact OCR parser. This is the gate
every OCR, crop, layout, or parser change runs against (see ADR-007).
It is necessary but not sufficient for live scanner proof: scan speed and
review/miss rates are measured by `npm run scan:iterate:validated` for short
iteration and `npm run scan:goal:validated` for the final 100-artifact proof.
Use the `:wait` variants directly after UAC startup.

## Run it

```powershell
npm run eval     # full accuracy report for the seed corpus
npm run eval:review-candidates  # export unconfirmed review samples for human labeling
npm test         # runs the eval gate alongside the rest of the suite
npm run scan:assessment:test  # verifies quality-first scan ranking logic
npm run scan:iterate:validated:wait          # 20-artifact live scan
npm run scan:repeatability:wait              # 20/45/100 current-engine repeatability
npm run scan:goal:validated:wait             # final 100-artifact live scan
```

The report prints exact-match rate, overall field accuracy, a per-field
breakdown (critical fields marked with `*`), and every failing case with an
`expected "..." got "..."` diff.

Repeatability runs prove whether the visible-inventory scanner stays stable
across later sessions. They are current-path evidence and should be reported
with review/miss rates plus timing, not just click count.

## How it works

- `src/eval/ocrEvalHarness.ts` - pure metric functions. `runOcrEval(cases)`
  feeds each case's OCR text through the real `parseArtifactCandidate` and scores
  the produced fields against the labels. Order-independent for substats.
- `src/eval/corpus/seedCorpus.ts` - the seed corpus, transcribed from the
  verified assertions in `src/lib/artifactOcrParser.test.ts`. Must stay at 100%.
- `src/eval/ocrEval.test.ts` - the gate: seed field accuracy, critical-field
  accuracy, and exact-match rate must all be 1.0.

## Growing the corpus from review samples

The review queue is the corpus source. A saved review sample carries the OCR
text plus the parser's *guess* - `reviewSampleToEvalCase` extracts both.
For the local Electron queue, run:

```powershell
npm run eval:review-candidates -- --limit=80
```

This writes:

- `outputs/review-eval-candidates/review-eval-candidates.json`
- `outputs/review-eval-candidates/review-eval-candidates.md`

The exporter deduplicates samples, puts complete modern OCR captures first,
marks missing fast-profile fields so stale/partial captures do not crowd out
useful cases, and surfaces ownership/lock evidence (`artifact-footer`,
`equipped`, and `locked=true/false`) for the next validation pass.

After manually checking one candidate against the real artifact, create a
confirmed corpus snippet with explicit expected labels:

```powershell
npm run eval:prepare-confirmed -- --candidate=<candidate-id> --expect-file=.\path\to\expect.json
```

The script reads the latest
`outputs/review-eval-candidates/review-eval-candidates.json` by default and
writes a `.confirmed.ts` snippet under `outputs/review-eval-candidates/`.
It refuses to run without explicit labels, so parser guesses are not silently
promoted to ground truth. Review that snippet, then paste the object into
`src/eval/corpus/confirmedReviewCorpus.ts`.

The parser's guess is a label **candidate, not ground truth** (using it directly
would be the parser grading itself). To add a real case:

1. Convert review samples with `reviewSamplesToEvalCases(records)`.
2. Open each produced case and confirm or correct the `expect` values against
   what the artifact actually is in-game. Set `confirmed: true`.
3. Move the corrected case into
   `src/eval/corpus/confirmedReviewCorpus.ts`. The main eval gate imports
   `src/eval/corpus/index.ts`, which combines the seed corpus with confirmed
   review cases.

`src/eval/corpus/confirmedReviewCorpus.test.ts` rejects common corpus mistakes:
duplicate case ids, missing OCR text, empty labels, missing source notes, or a
case that was copied in without `confirmed: true`.

Prefer cases that cover new failure modes: unseen resolutions, new sets or
characters, equipped footer noise, and OCR noise the current corpus does not
exercise. `locked` is a capture-side visual flag rather than a text parser field;
validate it from review-export metadata and live screenshots instead of adding
it to the OCR eval labels.

## When a change moves a number

- Accuracy **drops**: a regression. Read the printed failures; fix the parser or
  revert. Do not lower the threshold to make it pass.
- A change **intentionally** alters a previously-correct output: update the
  corpus label in the same commit. The label is the source of truth, not the code.
