Files
pxpipe/eval
teamchong 514a4f0b74 feat(gate): config-driven model scope (PXPIPE_MODELS) + Opus 4.8 re-test
- applicability: isPxpipeSupportedModel reads PXPIPE_MODELS (comma-sep,
  default claude-fable-5 only) and strips bracket/variant tags like [1m];
  Opus becomes opt-in with no code change. Default behavior unchanged.
- FINDINGS: Opus 4.8 re-test - arithmetic 98/100 (-2pp), verbatim 6/15
  (was 93/100, 0/15): improved but still taxed -> default stays Fable-only.
- FINDINGS + eval/glyph-matrix/sweep: root cause of the read tax is per-glyph
  RESOLUTION, not font shape (confusions scale with px and vanish by 10x16).
  Opus needs ~4x glyph area to read reliably; the 1568px ceiling makes that
  break-even, so there is no profitable Opus operating point.
2026-06-16 12:22:55 -04:00
..

Reflow Eval Harness

Evaluation harness for the reflow image-rendering mode in pxpipe.

Reflow re-packs text densely and marks original newlines with the ↵ glyph (U+21B5) before rendering to PNG. It reduces image count by ~3050% on typical Claude Code history. This harness verifies that the model still understands reflowed text — telemetry measures tokens/bytes, not comprehension.


Prerequisites

# Build the compiled dist/ output (required by the eval scripts)
pnpm run build

# Extract corpus from your local Claude conversations
node eval/extract-corpus.mjs

The corpus is written to eval/corpus/:

  • text-blocks.json — text blocks for L1 OCR eval (~20 by default)
  • sessions.json — conversation sessions for L2 session replay (~10 by default)

L0 — Unit Tests (reference only)

L0 is a vitest unit test file (tests/render.test.ts plus a forthcoming tests/reflow.test.ts owned by the core team). It tests the reflow / dereflow / renderTextToPngsReflow functions in isolation.

# Run all unit tests including L0
pnpm test

L0 does not require an API key and runs in CI. It verifies:

  • dereflow(reflow(t)) === minifyForRender(t) for all t
  • Sentinel collision fallback
  • Image dimensions and chunk counts

L1 — OCR Fidelity

Renders text blocks to PNG two ways (baseline and reflow), sends each image set to the Anthropic API asking for verbatim transcription, then diffs the result against the source using character-level Levenshtein edit distance.

The reflow system prompt includes: "↵ denotes a line break".

Dry run (no API key, no cost)

node eval/eval-l1-ocr.mjs --dry-run
# or via orchestrator:
node eval/run-eval.mjs --level 1 --dry-run

Cost estimate only

node eval/run-eval.mjs --estimate-only

Real run

export ANTHROPIC_API_KEY=sk-ant-...
node eval/eval-l1-ocr.mjs --confirm
# or:
node eval/run-eval.mjs --level 1 --confirm

Estimated cost (L1 only, 20 blocks, claude-sonnet-4-5)

Item Estimate
API calls 40 (2 per block × 20 blocks)
Input tokens ~70,00090,000 (dominated by image tiles)
Output tokens ~10,00020,000 (transcriptions)
USD ~$0.50$1.00

The actual cost depends on rendered image sizes. Most blocks produce 12 PNGs, costing ~1,600 tokens per image tile at Sonnet pricing.

Output

  • eval/results/l1-report.md — markdown report with per-block scores
  • eval/results/l1-results.json — raw JSON for programmatic use

Interpreting L1 results

Metric Threshold Meaning
Mean char accuracy delta 2pp Acceptable; within noise
Mean char accuracy delta < 5pp Reflow OCR materially worse
Macro accuracy (reflow) ≥ 95% High overall character fidelity
Image count savings 3050% Expected typical range

A negative delta means reflow is slightly less accurate. Up to 2pp is acceptable because: (a) the reflow system prompt compensates for ↵ rendering, (b) real accuracy is bounded by the model's vision encoder, not the sentinel.


L2 — Task-level A/B Session Replay

This is the real gate. Extracts real conversation sessions from ~/.claude/projects/**/*.jsonl, renders the history both ways, asks the model to produce the next turn in the conversation, then uses a model-judge to score whether the reflow-history answer is as good as the baseline.

Dry run

node eval/eval-l2-session.mjs --dry-run
# or:
node eval/run-eval.mjs --level 2 --dry-run

Real run

export ANTHROPIC_API_KEY=sk-ant-...
node eval/eval-l2-session.mjs --confirm
# or full pipeline:
node eval/run-eval.mjs --level all --confirm

Estimated cost (L2 only, 10 sessions, claude-sonnet-4-5)

Item Estimate
API calls 30 (2 replay + 1 judge per session × 10)
Input tokens ~200,000400,000 (history images are large)
Output tokens ~10,00030,000
USD ~$1.50$4.00

History rendering dominates: each session history is 2,0008,000 chars → 15 images → 1,6008,000 input tokens per image call. Use --max-sessions to control scope.

Full run cost (L1 + L2, 20 blocks + 10 sessions)

~$2.00$5.00 USD with claude-sonnet-4-5.

Use --model claude-haiku-4-5 to reduce cost by ~4× at some accuracy trade-off.

Output

  • eval/results/l2-report.md — markdown report with per-session judge scores
  • eval/results/l2-results.json — raw JSON
  • eval/results/summary.md — combined summary with shipping gate checklist

Interpreting L2 results

Metric Threshold Action
Mean judge score ≥ 0.80 Ship reflow
Mean judge score 0.650.79 ⚠️ Investigate failing sessions
Mean judge score < 0.65 Do not ship reflow
Pass rate (≥ 0.75) ≥ 80% Consistent quality
Pass rate 6079% ⚠️ Some sessions fail
Pass rate < 60% Widespread comprehension loss

The judge uses this scoring rubric:

  • 1.0 — semantically equivalent to baseline
  • 0.8 — mostly equivalent, minor differences
  • 0.6 — partially equivalent, some content missing
  • 0.4 — substantially worse
  • 0.2 — mostly unrelated
  • 0.0 — completely wrong

Full Pipeline

# 1. Build dist/
pnpm run build

# 2. Extract corpus (writes eval/corpus/)
node eval/extract-corpus.mjs --max-blocks 20 --max-sessions 10

# 3. Cost estimate
node eval/run-eval.mjs --estimate-only

# 4. Dry run (verify end-to-end without spend)
node eval/run-eval.mjs --dry-run

# 5. Real run (requires API key + explicit --confirm)
export ANTHROPIC_API_KEY=sk-ant-...
node eval/run-eval.mjs --confirm

# 6. View results
cat eval/results/summary.md

Options Reference

extract-corpus.mjs

Flag Default Description
--max-blocks N 20 Max text blocks for L1
--max-sessions N 10 Max sessions for L2
--out-dir DIR eval/corpus Output directory
--projects-dir DIR ~/.claude/projects Claude projects directory
--verbose false Verbose progress

eval-l1-ocr.mjs / eval-l2-session.mjs / run-eval.mjs

Flag Default Description
--dry-run false No API calls; fake scores
--confirm false Required for real API spend
--max-blocks N 20 L1 block count
--max-sessions N 10 L2 session count
--model NAME claude-sonnet-4-5 Replay + transcription model
--judge-model NAME (same as model) L2 judge model
--corpus-dir DIR eval/corpus Corpus input directory
--out-dir DIR eval/results Results output directory
--estimate-only false Print cost estimate and exit
--skip-extract false Skip corpus extraction step
--level 1|2|all all Which levels to run (run-eval only)
--verbose false Verbose per-block/session output

File Layout

eval/
├── README.md                 ← this file
├── extract-corpus.mjs        ← corpus extraction from ~/.claude/projects
├── eval-l1-ocr.mjs           ← L1 OCR fidelity eval
├── eval-l2-session.mjs       ← L2 session replay eval
├── run-eval.mjs              ← top-level orchestrator
├── lib/
│   ├── anthropic-client.mjs  ← minimal Anthropic API client (fetch-based)
│   ├── cost.mjs              ← token/USD cost estimator
│   ├── diff.mjs              ← Levenshtein + character accuracy scorer
│   └── render-bridge.mjs     ← imports render functions from dist/
├── corpus/                   ← generated by extract-corpus.mjs
│   ├── text-blocks.json
│   └── sessions.json
└── results/                  ← generated by eval runs
    ├── l1-report.md
    ├── l1-results.json
    ├── l2-report.md
    ├── l2-results.json
    └── summary.md

Notes

  • The eval imports from dist/core/render.js. Run pnpm run build first.
  • No extra npm packages are required. The Anthropic client uses Node's built-in fetch.
  • Dry-run mode simulates ~3% OCR error rate to produce non-trivial diff scores.
  • The corpus extractor gracefully falls back to a synthetic corpus when ~/.claude/projects is empty or absent.
  • .claude/ is globally gitignored — corpus and results are in eval/ and should be added to .gitignore if you don't want to commit them.