# 📦 WP-140 — `release: v0.5.0 keypoint detection`

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


## WP-140 — a release is a boundary, not an achievement

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

0.5.0 closes the keypoint phase. `__version__` moves from `0.5.0.dev2` to a plain `0.5.0`, which is the whole of the version change — `pyproject.toml` declares the version dynamic and reads the module attribute — and the `## [Unreleased]` section [WP-133](https://borda.github.io/lucid-YOLO/work_packages/wp-133/) opened closes as `## [0.5.0]`, on the schedule its own opening paragraph wrote down.

**The frozen goldens are the release's regression contract, and this one is a set larger than the last.** `make freeze-goldens MINOR=0.5` copies the eight live metric sets into `goldens/frozen/0.5/`, where 0.4.0 froze seven; the extra set is [WP-139](https://borda.github.io/lucid-YOLO/work_packages/wp-139/)'s keypoint size golden, which did not exist a commit earlier. That is the difference between freezing what happens to be lying around and freezing what the release actually claims, and it is only a difference because the missing golden was found before the freeze rather than after it.

**What this release does not claim.** The keypoint tier's acceptance is a *direction of effect* against a control — RLE beat [WP-135](https://borda.github.io/lucid-YOLO/work_packages/wp-135/)'s Laplace-NLL arm at e2e OKS AP 0.2738 against 0.2527 on real COCO human-pose data, the same sign as R14 Table 7's own 70.5-against-67.4 — and it is not a pose figure comparable to anything published. R14's own numbers come from a different backbone, a different schedule and a different scale; this project's smoke tier is fifty epochs at `n`. Three keypoint assumptions stay `open` and each names an experiment nobody has run: A68's tuned gain adequacy (the mechanism-failure bar cleared, no sweep run), A69's one shared flow against one per branch (untested against the alternative, and the overfit gate structurally cannot tell), A70's off-canvas point policy (exercised live in both arms, its frequency and effect unmeasured). The oriented figures are unchanged from 0.4.0's caveat only in that [WP-111](https://borda.github.io/lucid-YOLO/work_packages/wp-111/) has since measured a whole-image number; every other limit that release recorded still stands, and no trained weights ship (D14).

**The fourth task trains, validates and evaluates; it does not yet deploy.** `tests/models/test_onnx_export.py` still runs three tasks, and `predict.py` still ships three functions. That is parity work for a later phase, named here rather than left for a reader to infer from an absence.

**Two CI failures the offline gate structurally cannot see, both found on the release's own run and folded in here.** The first is a four-character bug with a document-wide blast radius: `docs/ENGINEERING_LOG.md` and `docs/ROADMAP.md` both wrote the placeholder `test_` followed by an angle-bracketed `script`, and Python-Markdown's HTML block pass runs *before* inline code spans are recognized, so the backticks protect nothing — an angle-bracketed `script` opens a raw-text element that swallows the rest of the document, and every `<a id="wp-NNN"></a>` anchor after it vanishes from the element tree `mkdocs build --strict` reads. Eighteen roadmap links broke at once, every one of them from the [WP-130](https://borda.github.io/lucid-YOLO/work_packages/wp-130/) paragraph onward, and the anchors were all still present in the rendered HTML — the post-processing step restores stashed raw HTML, so the page looked correct while the validator that never sees that step was right to fail. Renamed to `test_<script_name>.py`, which is not an HTML tag. The second is `scripts/lint/audit_docs_site.py`'s `# type: ignore[no-untyped-call]` on `add_multi_constructor`: whether that call is typed depends on the resolved `types-PyYAML`, which the lockfile does not pin, so the ignore is load-bearing locally and dead on CI's newer stubs, where `warn_unused_ignores` then fails the run. `unused-ignore` now rides along in the same bracket, correct under either version without pinning a stub package for one comment.

The general shape is worth keeping: `make gate` is an offline gate and does not build the docs site, so a docs-site failure is invisible until CI, and a lint that depends on an unpinned transitive stub version is invisible until CI resolves a different one. Neither is a gate to add — the docs build is genuinely CI's job and pinning stubs to satisfy one comment is worse than the comment — but both are reasons the gate being green is not the same claim as the branch being green.

**No tag is cut here and no distribution is published**, as at 0.4.0. `release.yml` fires on a pushed `v0.*` tag and re-runs the guard, which refuses a tag whose changelog section is missing or whose gate is red; both are the operator's to trigger. `v0.4.0` is also still uncut, so the train has now shipped two release commits ahead of its own tags — a gap worth stating plainly, since a reader who looks for `v0.5.0` will find neither it nor its predecessor.

