# Case library — Phase 1 ingest workflow

Phase 1 ships with a curated list of 20 high-yield cases (`api/scripts/phase1-cases.json`)
spanning the rubric. Getting them into the database is a 3-step process,
run on the droplet after the services are deployed.

## 1. Source the strip images

You need a PNG for each case_id in `phase1-cases.json`. Two paths:

### Path A — PTB-XL records (recommended, free, reproducible)

PTB-XL is a public 21k-record 12-lead database from PhysioNet. Each record
is raw signal data (WFDB format), not an image, so it must be rendered.

1. Download PTB-XL from <https://physionet.org/content/ptb-xl/> (~3 GB
   extracted). Accept the data-use agreement.
2. Pick one PTB-XL record per case in `phase1-cases.json` that matches the
   description. Edit the manifest's `ptb_xl_record` field for each entry
   (e.g. `"records100/00000/00001_lr"`).
3. Render with the Python helper:

   ```bash
   cd apps/ekg-tutor/interpret
   uv pip install wfdb matplotlib numpy   # one-time, on top of the base deps
   .venv/bin/python scripts/render_ptbxl.py \
     --manifest ../api/scripts/phase1-cases.json \
     --ptbxl-root /opt/ptbxl-1.0.3 \
     --out /var/lib/ekg-tutor/seed-images
   ```

   Produces `/var/lib/ekg-tutor/seed-images/phase1-NN-<slug>.png` for each
   case where `ptb_xl_record` is set. The renderer applies 25 mm/s sweep,
   10 mm/mV gain, parchment background with salmon gridlines — the look
   most learners encounter clinically.

### Path B — curated screenshots from any source

If you have rendered images from another source (a teaching atlas, your own
clinical archive of de-identified strips, etc.), just drop them into a
directory with filenames matching `phase1-NN-<slug>.png` and skip step A.

## 2. Run the ingest CLI

```bash
cd apps/ekg-tutor/api
EKG_TUTOR_DATABASE_URL=postgres://ekg_tutor:...@127.0.0.1:5432/ekg_tutor \
ANTHROPIC_API_KEY=sk-ant-... \
pnpm tsx scripts/ingest.ts \
  --library scripts/phase1-cases.json \
  --images /var/lib/ekg-tutor/seed-images \
  --visibility public
```

For each image the script:

1. Uploads to local storage (`/var/lib/ekg-tutor/uploads/<date>/<hex>.png`).
2. Calls `ekg-tutor-interpret.service` (port 3112) to produce a canonical
   Interpretation via Claude Sonnet 4.6.
3. Validates the response against the Zod schema; one retry on failure.
4. Inserts the case row (`status='ready'`, `visibility='public'`,
   `clinician_reviewed=false`).
5. Inserts case_concepts joins from the manifest's
   `concepts_primary` (salience=primary) and `concepts_secondary` (salience=secondary).

Idempotent — re-running skips cases whose `source_ref` already has
`status='ready'`. Use `--only phase1-12-anterior-stemi` to re-ingest a
single case after a manifest edit. Use `--dry-run` to validate the
image-to-case mapping without calling the LLM.

## 3. Clinician review

Phase 1 ships with `clinician_reviewed=false` on every case. Before any
learner sees them, a clinician (per BLUEPRINT.md §15: you, CRNA, for the
self-review path) reviews each canonical interpretation and either:

- Marks the case reviewed:

  ```sql
  UPDATE cases
     SET clinician_reviewed = true,
         reviewed_by = (SELECT id FROM users WHERE username = '<your-username>'),
         reviewed_at = NOW()
   WHERE source_ref = 'phase1-NN-...';
  ```

- Or re-runs ingest with a revised prompt — bump `interpretation_version`
  on the case and the model+prompt tag in `llm_interpret.py`. The version
  on cases will diverge from the version on existing attempts (this is
  the fairness mechanism described in §6).

A Phase 2 curator UI will replace the SQL step with a review queue.

## Failure modes you may hit

- **interpret service returns 502 schema_failure.** The LLM produced output
  that didn't validate against `Interpretation`. The service retried once
  before giving up. Inspect the systemd log (`journalctl -u ekg-tutor-interpret -n 200`)
  to see the validation error and decide whether to relax the schema or
  retry with a different model temperature.
- **interpret service returns 503.** No `ANTHROPIC_API_KEY` loaded. Check
  `/root/secrets.env` and `systemctl restart ekg-tutor-interpret`.
- **Image rendering looks wrong.** PTB-XL ships 100Hz and 500Hz versions
  (`records100/` vs `records500/`). The renderer auto-detects via wfdb;
  both work, but the 500Hz files give smoother strips.

## Cost note

Each canonical interpretation is paid for once at ingest (~$0.10–$0.20).
The result is cached on the case row forever. Tutor responses (per-attempt)
are the recurring cost (~$0.03–$0.08 each). BLUEPRINT.md §14 for budget
projections.
