# 📦 WP-124 — `feat(eval): OKS evaluation wiring`

> Engineering log · [Phase 12 — Keypoint detection, v0.5.0](https://borda.github.io/lucid-YOLO/engineering/phase-12-keypoints/)
>
> 🔗 Depends on WP-069, [WP-121](https://borda.github.io/lucid-YOLO/work_packages/wp-121/), [WP-123](https://borda.github.io/lucid-YOLO/work_packages/wp-123/) · Needed by [WP-125](https://borda.github.io/lucid-YOLO/work_packages/wp-125/), [WP-132](https://borda.github.io/lucid-YOLO/work_packages/wp-132/), [WP-134](https://borda.github.io/lucid-YOLO/work_packages/wp-134/)
>
> ⬅️ [WP-123](https://borda.github.io/lucid-YOLO/work_packages/wp-123/) · [Roadmap](https://borda.github.io/lucid-YOLO/ROADMAP/) · [WP-125](https://borda.github.io/lucid-YOLO/work_packages/wp-125/) ➡️


## WP-124 — a premise checked against the installed package, not the roadmap text

<a id="wp-124"></a>

Delegated to `codex:codex-rescue`, `--write`, scoped to `src/lucid_yolo/eval/coco_eval.py` and a new `tests/eval/test_keypoint_eval.py` only -- no decoder, model, CLI, roadmap, or assumption edits, no commit, no full `make gate` inside the sandbox. Before dispatch this row's own Scope text was verified against the installed environment rather than trusted: `torchmetrics==1.9.0`'s `MeanAveragePrecision.__init__` accepts `iou_type` values of only `"bbox"`/`"segm"` (checked via direct signature/source inspection, not memory), so the row's premise -- "keypoint mAP wired the way WP-069 wired box mAP" -- was factually wrong for what is actually installed. This did not rise to an AGENTS.md sec. 4 escalation: the row's actual intent, a COCO-OKS-faithful engine wired against [WP-121](https://borda.github.io/lucid-YOLO/work_packages/wp-121/)'s ground truth and [WP-122](https://borda.github.io/lucid-YOLO/work_packages/wp-122/)'s decode, is still satisfiable, just through a different already-a-dependency library (`faster_coco_eval`, R23) called directly instead of through torchmetrics's wrapper. The spec handed to codex stated the corrected mechanism up front and explicitly instructed it to re-verify the premise itself against the installed package before writing any code, rather than trust the prompt.

**What codex built:** `keypoints_to_predictions` adapts a fixed-size `(B, N, K, 2)` decode-shaped batch into COCO prediction dicts, filtering the score-zero padding rows a fixed decoder shape produces and remapping contiguous class labels back to original COCO category ids -- the same shape of adapter `detections_to_predictions` already provides for boxes, not a new pattern. `evaluate_keypoints` builds a minimal in-memory COCO ground-truth/result document pair from [WP-121](https://borda.github.io/lucid-YOLO/work_packages/wp-121/)'s `keypoints`/`keypoint_vis` tensor convention directly (`v == 0` excluded from both OKS matching and the per-instance bounding-box area, `v >= 1` included -- A66 applied here exactly as [WP-123](https://borda.github.io/lucid-YOLO/work_packages/wp-123/) defined it, not re-decided) and drives `faster_coco_eval.COCOeval_faster(iouType="keypoints", kpt_oks_sigmas=...)` through its native `evaluate()` / `accumulate()` / `summarize()` sequence, one-shot rather than `DualPathEvaluator`-streaming since no keypoint decode pipeline exists yet for a streaming caller to feed. `COCO_KEYPOINT_OKS_SIGMAS` names R12's 17-point sigma table as a module constant rather than leaving it to the library's own `Params.setKpParams` default -- the values are identical today, but a project-owned constant keeps the number this project reports against from silently moving if the dependency's own default ever changes.

**Independent re-verification (lead, outside the sandbox):** `tests/eval/test_keypoint_eval.py` -- 5 passed (padding-drop/category-remap adapter contract; a perfect-match OKS oracle scoring near-1.0 AP; a far-miss oracle scoring exactly 0.0 AP, ruling out a category-only match; the empty-input all-zero ten-key contract, avoiding a degenerate call into `COCO.loadRes`'s empty-list case; corrupting only a `v=0` target coordinate and asserting the ten stats are bit-for-bit unchanged, reusing [WP-123](https://borda.github.io/lucid-YOLO/work_packages/wp-123/)'s own `TestVisibilityMasking` pattern). `coco_eval.py`'s module doctests -- 6 passed, 1 pre-existing skip. `mypy` on the file -- clean. Full `tests/eval/` regression -- 216 passed (211 pre-existing + 5 new, matching codex's own reported delta exactly). Full `make gate` -- 2319 passed, 37 skipped, 3 deselected, 27/27 goldens, run twice to confirm no stray failure line in the fuller output.

**No new assumption recorded.** The spec instructed codex not to add an `ASSUMPTIONS.md` row itself and to flag anything it found instead; nothing surfaced that qualifies -- the sigma-table caution above is a documented fragility comment, not an open gap, since the value is pinned as a citable constant rather than inherited implicitly either way.

