mirror of
https://github.com/teamchong/pxpipe.git
synced 2026-07-22 02:02:51 +02:00
c369b931e5
frozen eval outputs under eval/results*/ left as-is (historical records)
271 lines
8.4 KiB
Markdown
271 lines
8.4 KiB
Markdown
# 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 ~30–50% on typical
|
||
Claude Code history. This harness verifies that the model still **understands**
|
||
reflowed text — telemetry measures tokens/bytes, not comprehension.
|
||
|
||
---
|
||
|
||
## Prerequisites
|
||
|
||
```bash
|
||
# 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.
|
||
|
||
```bash
|
||
# 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)
|
||
|
||
```bash
|
||
node eval/eval-l1-ocr.mjs --dry-run
|
||
# or via orchestrator:
|
||
node eval/run-eval.mjs --level 1 --dry-run
|
||
```
|
||
|
||
### Cost estimate only
|
||
|
||
```bash
|
||
node eval/run-eval.mjs --estimate-only
|
||
```
|
||
|
||
### Real run
|
||
|
||
```bash
|
||
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,000–90,000 (dominated by image tiles) |
|
||
| Output tokens | ~10,000–20,000 (transcriptions) |
|
||
| **USD** | **~$0.50–$1.00** |
|
||
|
||
The actual cost depends on rendered image sizes. Most blocks produce 1–2 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 | 30–50% | 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
|
||
|
||
```bash
|
||
node eval/eval-l2-session.mjs --dry-run
|
||
# or:
|
||
node eval/run-eval.mjs --level 2 --dry-run
|
||
```
|
||
|
||
### Real run
|
||
|
||
```bash
|
||
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,000–400,000 (history images are large) |
|
||
| Output tokens | ~10,000–30,000 |
|
||
| **USD** | **~$1.50–$4.00** |
|
||
|
||
History rendering dominates: each session history is 2,000–8,000 chars → 1–5
|
||
images → 1,600–8,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.65–0.79 | ⚠️ Investigate failing sessions |
|
||
| Mean judge score | < 0.65 | ❌ Do not ship reflow |
|
||
| Pass rate (≥ 0.75) | ≥ 80% | ✅ Consistent quality |
|
||
| Pass rate | 60–79% | ⚠️ 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
|
||
|
||
```bash
|
||
# 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.
|