# 🧭 Phase 15 — Audit remediation, v0.8.0


## WP-160 — the guard checked everything except what a release is

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

`scripts/release_guard.py` validated the tag string, the changelog section and the gate. All three are about the repository. None is about the artifact: what a consumer installing the distribution actually receives. [WP-159](https://borda.github.io/lucid-YOLO/work_packages/wp-159/) found that by walking into it — five modules under `data/` import `fuse_augmentations`, its requirement sat in the `dev` dependency group, and a wheel installed without that group raises `ImportError` from `lucid_yolo.data`. The fourth check closes that class.

**Why no test could have caught it.** The development environment installs every dependency group, so every import resolves and the suite is green by construction. The defect is only visible from outside that environment, which is the definition of a release-time property rather than a test-time one — and it is why the check belongs in the guard even though it is cheap enough to run anywhere. It also sat legible in `pyproject.toml` for five work packages: the comment beside the pin already said PyPI forbade re-uploading `0.10.0.dev0`, one clause short of the conclusion.

**What it does.** Walk the shipped package for absolute imports, drop the standard library and the package itself, resolve each remaining top-level module to its distribution through `importlib.metadata.packages_distributions`, and refuse the tag when a distribution is missing from `[project].dependencies`. The refusal names the group that *does* declare it, so it diagnoses rather than complains. Two smaller decisions are worth stating: the walk is over every `Import` node rather than the module preamble, because an import inside a function is one the installed package can still execute and merely fails later; and a module resolving to no installed distribution is reported rather than skipped, because a guard that reads "unknown" as "fine" is the failure mode this check exists to close.

**What it deliberately does not check.** Whether the runtime requirements are uploadable to PyPI. `[project].dependencies` carries a direct reference by decision — D20 accepted the unpublishable distribution two commits ago with its consequence recorded — so a check refusing it would re-litigate a decision rather than protect one. The gap that leaves is real and stated here rather than hidden: a *second* direct reference could be added later without any gate objecting. The tier check protects something unconditional instead, since a distribution that cannot import is broken however it was obtained.

**Evidence it is load-bearing.** The group-only case is asserted against a reconstruction of the pre-WP-159 tier layout, where it fails and names `dependency-group 'dev'`; against the tree as it stands it passes, reporting all twelve third-party top-level imports declared at runtime. Six cases were added — group-only, runtime-declared, unresolvable, stdlib-and-relative, conditional-import, and one driving the CLI so the check is wired rather than merely importable.

One thing worth recording for whoever edits this file next: ruff's autofix removed the four new imports on the edit that added them, because at that instant nothing referenced them yet. The same trap caught [WP-157](https://borda.github.io/lucid-YOLO/work_packages/wp-157/).



## WP-161 — the discipline was written down and the banner did not follow it

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

`docs/PROVENANCE.md` sec. 3.5 states the rule in one sentence: "YOLO26", "YOLO11", "Ultralytics" and their logos never appear in package names, module paths, class names or model identifiers, and *nominative references to the paper ("the YOLO26 paper, arXiv:2606.03748") are the only usage*. The README's first line said "the Ultralytics YOLO26 paper". That is not a nominative reference to a paper; it is a vendor name bound to the method, which is the one binding this project's whole thesis argues against — the method is what the paper describes, and a codebase's choices are a separate thing that the shared name makes impossible to see. The register had the rule and the most-read file in the repository did not follow it.

**Three mentions, two categories, and only one of them moved.** The attributive one names the work: it is now "the YOLO26 paper", matching the register's own quoted form. The other two are the non-affiliation disclaimer — "not affiliated with, endorsed by, or derived from Ultralytics or its codebase" and "No Ultralytics source code, configurations, or model weights were consulted or used" — and they are a different instrument. A disclaimer that does not name the party it disclaims is not a disclaimer, it is a sentence about nobody. Both are also pinned as literal fragments in `scripts/lint/audit_license_headers.py` and required in `NOTICE`, so removing them from the README is a three-file change to a legal statement rather than a wording fix, and it is not what "stop naming the vendor as the method's owner" asks for.

**The id became the link it already was.** `arXiv:2606.03748` sat as bare text in a file where every other reference resolves. It is now `[arXiv:2606.03748](https://arxiv.org/abs/2606.03748)`, fetched and read before being written: the page returns the paper R1 cites, by the authors R1 names. The RLE id on the keypoints line is deliberately left alone — linking it is a separate verification pass, and this row's subject is the paper this project reproduces.

One thing this row does not fix, recorded so the next person does not have to rediscover it: `NOTICE` carries the identical drift, "the Ultralytics YOLO26 paper", and unlike the README's disclaimer sentences that phrase is *not* an audited fragment there. It is the same one-word correction. It is left for whoever touches `NOTICE` next rather than widened into this row.



## WP-162 — what "cheap enough" was worth, measured

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

[WP-130b](https://borda.github.io/lucid-YOLO/work_packages/wp-130b/) moved `golden-check` and `release-guard` from `stages: [manual]` to `[pre-commit, manual]` so that `pre-commit run --all-files` would exercise every hook rather than most of them, and justified both with one sentence: "cheap enough off a release tag". For `release-guard` that is exactly right — off a tag it resolves `git describe`, finds no tag and exits. For `golden-check` it was an estimate nobody checked. Measured: **34.6 s wall, 65 s CPU**, on every commit, and twice per `make gate` — once inside `precommit`, once through the `golden` target. [WP-130b](https://borda.github.io/lucid-YOLO/work_packages/wp-130b/)'s own comment recorded that double invocation as an accepted trade; what it did not have was the size of it.

**The duplication is the argument, not the runtime.** A 34-second hook that found something the gate does not would be worth its cost. This one recomputes the same 43 goldens that `make golden` recomputes, and `make gate` — `precommit test golden` — is the merge bar every commit in this repository has to pass anyway. So the commit-stage copy could never fail where the gate passed. `.pre-commit-config.yaml` is the linting-and-quick-checks surface; a full metric recomputation is a gate step wearing a hook's clothes, and `stages: [manual]` puts it back where [WP-130](https://borda.github.io/lucid-YOLO/work_packages/wp-130/) first had it while keeping the standalone `pre-commit run --hook-stage manual golden-check` path intact.

**What the stage change would have broken, and why the row is two files rather than one.** `lint.yml` runs `pre-commit run --all-files`, and that run was CI's *only* full 43/43 sweep — `ci-tests.yml` runs pytest, whose golden-touching suites (`tests/models/test_param_flops.py`, `tests/assign/test_assignment_goldens.py`, `tests/data/test_data_goldens.py`, `tests/optim/test_toy_convergence.py`) import the harness for specific goldens rather than sweeping all of them. Dropping the stage alone would have taken the sweep out of CI entirely and left a green pipeline saying nothing about the goldens. The harness therefore lands as its own `ci-tests.yml` step, in the job that runs the suite rather than back in the linting job, because recomputing model metrics is test-shaped work.

The trade [WP-130b](https://borda.github.io/lucid-YOLO/work_packages/wp-130b/) accepted is withdrawn rather than overturned: `--all-files` no longer exercises literally every hook, which is a real and small loss, and the thing bought back is that the gate stops paying for the same 43 goldens twice and a commit stops paying for them at all. `release-guard` keeps `[pre-commit, manual]` — its half of that sentence was never in question.



## WP-163 — one transfer per batch, and the three candidates that did not survive measurement

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

This row is the surviving quarter of a survey of RF-DETR's 1.9.0..develop range (137 commits, ~40 `perf`-tagged, Apache-2.0, read under D13 and never copied). Most of what that survey found was either already implemented here or inapplicable; what follows is the part that measured well, and the part that measured badly, because the second is the more useful record.

**What the change is.** Every validation accumulator moved its ground truth to the host one image at a time: two tensors per image for detection, three for oriented detection and keypoints, and a mask stack for segmentation. Each of those transfers carries the accelerator's fixed per-transfer latency and a payload of a few kilobytes, so a batch of 16 paid that latency 32 to 48 times to move something that fits in a single transfer. `_host_split` concatenates along the batch axis, moves once, and slices the result back into the same per-image pieces. Values, dtypes and ordering are unchanged, so nothing the metrics read changes and no golden moves.

**Measured, not assumed.** On MPS, 16 images x 7 boxes: **2.823 ms to 0.210 ms, 92.6% less**, median of 40 timed runs after 15 warm-ups with the device synchronised inside the timed region. On CPU the same batch goes from 0.004 ms to 0.013 ms — a 9-microsecond regression, because on CPU there is no transfer to amortise and the concatenation is pure overhead. That trade is taken deliberately: the accelerator path is where validation actually runs, and 9 microseconds against 2.6 milliseconds is not a close call.

**Honest scope.** Per validation batch this is milliseconds. Across a 5000-image validation split at batch 16 it is roughly 0.8 s an epoch — under 1% of a training epoch. The row is worth landing because it is bit-identical, strictly less work, and removes a per-image pattern that would otherwise be copied into the next accumulator; it is not worth landing on the strength of its epoch-level number, and this entry does not claim one.

**Three candidates the measurement rejected.**

*Skipping the padding rows before the segmentation mask decode.* The validation decoder hands `decode_instance_masks` a fixed 100 rows per image and `decoded[keep]` discards the padding immediately after, so filtering first is bit-identical and strictly less work — and it is **slower on the accelerator**: CPU improves 32–67% depending on survivor count, MPS *regresses* 40–86% at 40 repeats. Boolean indexing plus the data-dependent sync costs more than decoding 100 rows of a 160x160 grid in parallel. Rejected rather than adopted device-guarded: the guard would exist to serve the offline path at the accelerator's expense, for a proxy metric measured in milliseconds. Upstream made the same call in the other direction for one of its own changes, excluding MPS explicitly.

*Hoisting torchmetrics' per-annotation score reads.* `CocoBackend._get_coco_format` hoists boxes and labels once per image but reads `area[image_id][k]` one annotation at a time, and it accepts `scores=None`, so the scores can be assigned per image afterwards. Measured **26.3% and 26.6% faster** at 32 and 128 images of 100 detections. Not adopted: reaching it requires calling a private method on a private attribute (`metric._coco_backend._get_coco_format`), which binds this project's validation path to torchmetrics internals that carry no compatibility promise, and the epoch-level saving is around 0.16%. The measurement is recorded so the trade can be re-taken if torchmetrics ever exposes it.

*The `uint8 / 255` divisor form.* Upstream found that CUDA evaluates a division by the Python int `255` as a multiplication by its reciprocal, landing one ULP from the host result for 126 of 256 byte values, and switched to a 0-dim tensor divisor to restore parity. This repository's consumer conversion uses the same Python-int form, so the question was live — and **it does not reproduce here**: across all 256 byte values, both divisor forms match the host bit-for-bit on CPU and on MPS, 0 of 256 differing. A26's cross-platform divergence is not this mechanism. Recorded as a closed question rather than left as a plausible-sounding hypothesis.



## WP-164 — the copy that dropped nothing, and the three faults that hid it

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

This row lands the candidate [WP-163](https://borda.github.io/lucid-YOLO/work_packages/wp-163/) rejected one entry above, and the more useful half of it is why that rejection was wrong. The rejection is left standing there per this log's convention of not editing a historical paragraph to match a later correction; what follows is the correction.

**What the change is.** `_update_val_segm` decoded every one of its 100 detection rows and then wrote `masks[keep]`. In the deployed configuration that filter drops **nothing**: the validation decoder is `TopKDecoder()`, whose `conf_threshold` defaults to `0.0`, and a 640-px canvas carries more anchors than the cap of 300, so no row is padding and no row is zeroed — every score is a sigmoid output, and a sigmoid is never zero. The boolean index was therefore an unconditional second copy of a 100x160x160 boolean stack, 2.56 MB per image, to select all of it. Counting the live rows on `cpu_detections` — a host copy the loop already has — and slicing the decode to that count removes the copy. Where a caller *does* set a confidence threshold the same slice shrinks the decode itself, but that is the secondary effect, not the reason this lands.

**Why the slice is sound.** It rests on the survivors being a leading run, which is a property of the decoder rather than of this call site, and three separate mechanisms could have broken it: `o2o_topk_with_indices` ranks score-descending, `pad_detections` appends its zero rows *after* the ranked ones, and `_zero_below_threshold` cuts a descending column from some point on. All three are suffix operations, so `score > 0` can only ever be a prefix. Nothing stated that anywhere, which would have made a future reordering of the decode break this caller silently, so `test_scored_rows_leading_run` now states it against `TopKDecoder` itself — with `k` above the anchor count and a threshold set, so padding and the cut are both live in the same case.

**Measured on the shape that actually runs.** 16 images, 100 rows each, all live — the deployed case, not a favourable one: CPU **27.68 ms to 24.44 ms (+11.7%)** and MPS **11.54 ms to 9.76 ms (+15.4%)**, medians of five interleaved trials of 20 timed runs each, every individual trial positive on both devices. With a threshold set so only a handful of rows survive, the same change is +62% to +93%. No golden moves and no metric value changes: the rows fed to `MeanAveragePrecision` are the same rows in the same order.

**The three faults that produced [WP-163](https://borda.github.io/lucid-YOLO/work_packages/wp-163/)'s opposite conclusion.** They compounded, and each is worth naming because each is a benchmark-design error rather than a measurement accident. First, the harness's patched arm reconstructed the full 100-row tensor with a scatter (`out[:, keep] = decoded`) that the real call site never performs — it measured a formulation nobody proposed, and that scatter is where MPS lost. Second, the proxy decoded on-device and stopped there, omitting the `.cpu()` transfer the real loop performs, so the thing most likely to dominate was outside the timed region. Third, MPS timings drift downward across a process's life well past five warm-ups; timing base first and patched second, once, hands the first arm the cold clock, and trial 0 of every re-run reproduces exactly that — +41% falling to −5% by trial 4 on unchanged code. Interleaving arm order across trials and taking medians of trials makes the drift visible instead of letting it sign the result. The general lesson, which is the one worth keeping: benchmark the call site's own work profile, not a sketch of it, and on a lazily-executing accelerator treat a single ordered A-then-B run as unmeasured.

**Unverified on CUDA.** The mechanism — one fewer copy of a boolean tensor, no data-dependent indexing — has no reason to invert there, and this project has no CUDA device to confirm it on. Stated rather than assumed.



## WP-165 — a private method overridden on purpose, and the guard that makes it survivable

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

This row lands the candidate [WP-163](https://borda.github.io/lucid-YOLO/work_packages/wp-163/) measured, declined, and recorded so the trade could be re-taken. The principal re-took it. What follows is the mechanism, the honest number, and the reason the coupling is bounded rather than merely acknowledged.

**What the change is.** `torchmetrics` converts an accumulated epoch into the two COCO documents its evaluator scores. Most of that conversion is already hoisted to one call per image — `boxes[image_id].cpu().tolist()`, the same for labels — but each detection's score is read individually as `scores[image_id][k].cpu().tolist()`: a tensor index, a device transfer and a Python conversion per *annotation*, against two per image for everything else. At this project's validation shape that is 100 such chains per image where two would do. The parameter is already optional upstream — `scores=None` builds the annotations without a `score` key — so `_HoistedScoreBackend` passes `None`, lets `super()` build the document, and attaches the scores from one `tolist()` per image afterwards.

**The number, at the call site rather than the function.** `MeanAveragePrecision.compute()` over 320 images of 100 detections: **187.7 ms to 169.5 ms, 9.7% less**, medians of five interleaved trials, every individual trial faster. The conversion in isolation is ~26% faster, which is the figure [WP-163](https://borda.github.io/lucid-YOLO/work_packages/wp-163/) recorded; the 9.7% is the one that belongs in a decision, because `compute` also runs the evaluation the conversion feeds and that part is unchanged. Per epoch this is a fraction of a percent, as [WP-163](https://borda.github.io/lucid-YOLO/work_packages/wp-163/) said when declining it. That has not changed and this entry does not claim otherwise — what changed is who is deciding whether the coupling is worth it.

**Why the coupling is bounded.** `_get_coco_format` is private and carries no compatibility promise, and the failure mode of overriding one is worse than a break: upstream changes what it produces, the override keeps being called, and a validation metric moves with nothing raising. Two things bound that. The override **delegates rather than reimplements** — `super()` builds the document and this writes exactly one field, so any field upstream adds, removes or reshapes arrives unchanged, and a new keyword argument reaches upstream through `**kwargs` instead of hitting a signature that refuses it. And `tests/ptl/test_coco_backend.py` asserts *document equality* against the stock backend on whatever version is installed, so drift that the delegation cannot absorb turns the gate red rather than moving a number. That test is the load-bearing artifact of this row; the speed is the easy part.

**The one place the two documents could have diverged.** Upstream skips an image entirely when its mask list is empty and the document carries no boxes — which is exactly the segmentation path here, where an image whose detections were all filtered contributes nothing. An override that flattened every image's scores in order would then hand the third image's scores to the second image's annotations: plausible masks carrying each other's confidences, which no metric value reveals. The override reproduces that condition, `strict=True` on the zip makes any residual disagreement raise rather than misalign, and the mask test puts the empty image in the *middle* of the batch, where an off-by-one cannot accidentally line up.

**Scope of the private surface.** One attribute assignment, in one factory: `build_mean_average_precision` constructs the stock metric and replaces its `_coco_backend`. Both metrics the module builds go through it, so the reach into `torchmetrics` internals is one line in one place rather than a pattern.



## WP-165b — a phase opened from an adversarial read, and what the grouping is for

<a id="wp-165b"></a>

Phase 14 ended at its own release and six rows kept landing after it, belonging to no phase. They open Phase 15 rather than trailing a closed one, and the header that read `WP-154…165` now reads `WP-154…159`.

**What the audit was.** Seven readers over separate dimensions — numerics, data, models, eval, gate, docs, provenance — plus one external reviewer over all of it, each working from the tree at `3ba7880` and writing independently. After cross-source de-duplication (roughly 30 findings arrived from two or three readers at once, which is the signal that a finding is about the code rather than about a reader's angle) 125 distinct gaps remain, alongside 26 boundaries that are correct but under-stated. The audit file itself is not committed: it lives under `.plans/`, which is gitignored, and the roadmap cells carry its per-finding ids so a row can be traced back to it.

**Why the rows group by file surface rather than by finding.** The obvious grouping is one row per reader, or one row per severity. Both produce rows that touch the whole tree, and a row that touches the whole tree cannot be reverted without reverting everything else that happened to ride along. Grouping by the surface each finding touches means a row is one revertable change to one part of the tree, and the price — a critical and a low-severity item landing in the same commit because they live in the same module — is worth paying. [WP-166](https://borda.github.io/lucid-YOLO/work_packages/wp-166/) is the clearest case: two critical findings and five consistency items, all in `eval/coco_eval.py`, one commit.

**What the clean-room read found.** Nothing. Three readers hunted contamination independently: no denylisted source is referenced anywhere, the licence audit is clean over 107 distributions and 344 shipped binaries, and the naming discipline holds. Every one of the 125 findings is about machinery certifying less than the prose beside it claims — which is a different problem, and the one this phase is about.

**Two rows do not open with a commit.** [WP-169](https://borda.github.io/lucid-YOLO/work_packages/wp-169/) and [WP-172](https://borda.github.io/lucid-YOLO/work_packages/wp-172/) each move a value pinned in `goldens/frozen/0.2` through `0.7`. Under sec. 7 that is a standing prohibition and under sec. 4 an escalation trigger, and the two overrides already on record ([WP-154b](https://borda.github.io/lucid-YOLO/work_packages/wp-154b/), [WP-155](https://borda.github.io/lucid-YOLO/work_packages/wp-155/)) were both written down as dated exceptions rather than precedent. Each of those rows opens with its own `docs/ESCALATION.md` entry and a ruling. Three further rows need an accelerator and cannot go green offline.



## WP-166 — the scorer that shipped by default was reporting a different protocol

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

`lucid-eval` picks its engine with `eval_backend="auto"`, and `auto` resolves to hotcoco wherever it is usable, which is everywhere this project runs. That engine's ground-truth records were built by `_accumulate_boxes`, which hardcoded `"iscrowd": 0` and recomputed `"area"` as box width times height. `annotations.py` has parsed both fields off the COCO file since it was written and hands them over in the target dict, and the identical dicts reach both scorers — so the default engine implemented neither COCO's crowd-ignore rule nor its small/medium/large partition, while the `faster_coco_eval` path fed the same targets implemented both.

**The size of it.** On a crowd case — one normal ground truth, one `iscrowd=1` ground truth, one detection sitting inside the crowd region — hotcoco reported `map` 0.5050 against faster_coco_eval's 1.0000. On an area case whose annotation `area` is 200 under a box measuring 40000, the instance landed in the large bucket instead of the small one. On a real split this is systematic rather than marginal: `instances_val2017.json` carries roughly 1.6% crowd annotations, whose detections became false positives and whose instances became false negatives, and its `area` field is the *segmentation* area, always at or below the box area, so objects shifted small→medium→large and the three size statistics described a different partition of the split than COCO's.

**The defect was this project's, not the library's.** Driving `_hotcoco_stats` with hand-built records that do carry the fields returns COCO's answer exactly. hotcoco honours both; `_accumulate_boxes` never gave them to it.

**Why the gate could not see it.** Every fixture `TestHotcocoParity` fed to both scorers omitted `iscrowd` and `area` entirely, so torchmetrics defaulted area to the box area and crowd to zero — exactly what the hotcoco path hardcoded — and the two engines agreed *by construction* on the only two fields where they could differ. The CHANGELOG's "verified bit-for-bit against the existing engine" was true of a fixture family that could not express the divergence. That is the shape worth remembering: a parity test proves two implementations agree, not that either is right, and it proves nothing at all about a field no fixture carries.

**The mask half was measured, not assumed.** The class docstring claims the two engines differ in how they swap box area for mask area, which would have made the same fix wrong on the segm side. A probe whose annotation `area` (200), mask area (2025) and box area (40000) name three *different* buckets scored small on both halves — so torchmetrics reads the supplied `area` there too, and the annotation's value is correct on both paths.

**What travelled with it.** Four consistency items in the same module, each the same shape as the main defect — a number that depends on which library computed it. `_MAX_DETS` now reaches both engines instead of hotcoco alone; the keypoint detection cap and area partition are pinned beside the sigmas that were pinned for exactly that reason; the sigma comment's claim to equal the library default "exactly" was false at 1.39e-17 and now states the measured figure; and the OKS category table is built from ground truth alone, as COCO fixes it, rather than from the union with prediction labels.

**One narrowing worth its own line.** `_redirect_native_output` blinded file descriptors 1 and 2 for the whole evaluator call, discarding native warnings about exactly the malformed records this scorer builds. Running each phase with both descriptors captured separately showed every phase but `summarize` writing zero bytes — including a run whose ground truth carried a negative-width box, a negative `area` and an out-of-range `iscrowd`. Blinding the rest bought no silence and cost the one channel a malformed record would be reported on.



## WP-168 — five gates that asserted less than their own prose

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

Each of these was written down as a check and was not one. The row's claim is narrow and demonstrable: after it, each gate asserts the thing its name says.

**CI ran a different suite than the gate.** The workflow step called pytest inline without `--doctest-modules` and without the `src scripts` paths, so `testpaths = ["tests"]` applied and CI collected **1548** tests where the Makefile collects **2668**. Every test whose subject is a `scripts/` module — the ten lint checkers, the golden harness, the release guard, the freeze — plus some 879 doctests ran on the developer's machine and never remotely. A pull request breaking a lint checker's own logic was green in CI. The step now runs `make test`.

That target then swallowed pytest's exit 5 and reported it as a pass, a scaffold from before WP-002 when there was nothing to collect. Harmless while only a human ran it; not harmless once CI depends on it, because a job collecting nothing would go green. Removed in the same row that made it reachable — which is the general rule worth stating: a change that puts weight on an existing shortcut owns that shortcut.

**Nothing verified a frozen golden was unmodified.** `check_goldens` recomputes each file's producer and compares it against *that same file's own* stored values and tolerances — the module docstring states this as the design. So an edit that moves the values and the tolerances together is green by construction. Demonstrated on a copy of `goldens/` outside the tree: `frozen/0.5/optim_toy.json` with `steps_to_threshold_musgd` moved 47.0 → 52.0 and its tolerance widened 15.0 → 100.0 reports **43/43 goldens passed**. This is the rule two principal overrides were granted against, so the project had already exercised the decision path that the mechanism did not back.

`goldens/frozen/MANIFEST.sha256` pins all 34 frozen files by digest, and `audit_frozen_manifest.py` asserts it. It is `.sha256` rather than `.json` because `discover_goldens` globs the frozen tree for every `.json` file and would hand a `.json` manifest to the golden parser as a malformed golden. `freeze_goldens.py` is the only writer, with two modes because a release and a sanctioned move are different acts: `freeze()` seals only the rows for the files it just copied and preserves the rest verbatim, so a snapshot tampered with under an earlier minor cannot be re-blessed by the next release, while `--reseal` recomputes every row and is what records a move that escalation trigger 4 has already approved.

**What the manifest narrows rather than closes.** It is committed beside the files it covers, so a commit that edits a golden *and* reseals in the same breath still passes. What it buys is that declaring a move becomes a separate named act that shows up in a diff as a wholesale manifest rewrite, instead of something a values-and-tolerances edit does silently. Signing would close it; there is no key here. The hook runs on every commit rather than behind a `files:` filter, for the same reason `license-audit` does — it reads tree state, not the diff — and specifically because pre-commit's staged-path filter omits deletions, so a commit that only *deletes* a frozen golden would match no filter and never reach the one check that notices it is gone.

**The release guard did not check the version it would ship.** It checked the tag, the changelog section, the dependency tiers and the gate. Tagging `v0.8.0` with a matching changelog section, `__version__` still `0.7.0` and no `goldens/frozen/0.8` passed every one — a wheel carrying 0.7.0 under a 0.8.0 tag, and a release freezing nothing while the CHANGELOG says it did. `check_version` parses `__version__` with `ast` and never imports the package, because a guard deciding whether a distribution is shippable must not require that distribution to import first.

**The licence audit recognised one family.** `COPYLEFT_PATTERN` matched the GPL family against a policy (D13, D18d) that bans source-available, research-only, non-commercial and commercially licensed dependencies by name, and a contributor checkbox that asks a human to catch exactly those. A second tier-blind pattern now covers SSPL, BUSL, Elastic, PolyForm, Prosperity, Commons Clause and the non-commercial family — second rather than folded in, because BUSL is not copyleft at all and filing it under that name would make the finding misdescribe what it found. The docstring now names which families the patterns recognise and says plainly which ones still pass.

**A ratchet with 47 rows of slack is not a ratchet.** `_ASSUMPTION_FLOOR` sat at 26 against 73 rows and tested `max(ids)`, so 47 assumptions could be deleted from the tail before it fired, and a middle row could be deleted without it ever firing. `_WP_FLOOR` tests `len(ids)` and sits at its exact live count; the assumption floor was the one left behind, and `_DECISION_FLOOR` had the same predicate with one row of slack. Both are now `len` at their live counts, and the rule that adding a row raises the floor is written in the ASSUMPTIONS.md header, which is the text somebody adding a row actually reads.



## WP-174 — three shipped surfaces that a `callable(...)` Example stood in for

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

`predict_keypoints` had **zero** executed lines in the gate. `pose_eval.py`, the keypoint tier's acceptance scorer, sat at 37%. `_overlay_ema` was unexecuted entirely. All three carried a docstring Example of the form `>>> callable(run)`, which satisfies `--doctest-modules` *and* the repository's own doctest-coverage audit while asserting nothing a broken implementation would fail. That is the pattern worth naming: a required Example is a coverage obligation, and an Example that only asserts the symbol exists discharges the obligation without discharging the purpose. `detect_eval.py` and `rotated_eval.py` carry the identical Example and sit at 97% and 96%, so the pattern only bites where nothing else happens to reach the code.

After: `predict.py` 76% → **99%**, `pose_eval.py` 37% → **100%**, `checkpoint.py` 47% → **94%**. 31 new test items.

**The keypoint expectations are hand-computed, not recorded.** On the 64×128 planted canvas the letterbox scale is `min(64/64, 64/128) = 0.5`, content is 32×64, `pad_top` is 16 and `pad_left` is 0, so the inverse is `x_orig = 2·x_canvas` and `y_orig = 2·(y_canvas − 16)`. Those literals are in the test with the arithmetic beside them. A test that asserts whatever the code returned would have passed against a broken inverse just as well.

**The fixture is built to fail in the interesting ways.** The one-to-one and dense branches carry *disjoint* point sets, so a path reading the other branch's stem returns a plausible pose of the wrong object beside a correct box — the failure the source docstring warns about, and one no shape assertion can see. A decoy set sits on anchor 0, which is never the selected anchor, while every other anchor carries zero offsets decoding to its own centre; a fixed-row gather therefore returns the decoy or a centre rather than the plant. That case is self-guarding: if the decoy anchor ever coincided with the selected one, the decoy would overwrite the plant and the primary assertion fails rather than passing vacuously. `K` is 3 throughout rather than 17, so a function that assumed the human schema fails.

**One brief corrected against the data.** The row asked for the `K != 17` refusal "at minimum"; that branch was already covered at `K=7`, which the audit's own coverage line implies — missing `130-175` means `117-129` runs. The real gap was the scoring body. Both landed: the scoring body is now executed, and the refusal gains `K=16`, the value adjacent to the boundary where an off-by-one or a table padded by one joint lets a foreign schema through. The pose split is hand-built in the layout `run` resolves, which is what pins the annotation *file name* — `person_keypoints_` rather than `instances_`, the substitution that divides a person detector's AP by eighty.

**`_overlay_ema` was the one place the tests found real defects.** Three, all in how the EMA claim was established rather than in arithmetic. The callback's key was resolved by substring in one place and by literal name in another, so a checkpoint written by a parametrized callback — whose `state_key` carries its arguments — passed the substring match, was overlaid, and then died on a bare `KeyError` *after* the module had been mutated. Two matching entries were resolved by iteration order, the last silently winning. And the copy walked the *shadow's* keys rather than the module's, so any module tensor absent from the shadow kept its raw value with nothing said — a run reporting a mixture of EMA and raw weights under the EMA name, which is neither of the two claims a report can make.

The key is now resolved once and handed back so the shadow and the update counter come from the same entry; more than one match raises; and coverage is checked **before** any copy rather than after. Validating after would have reproduced the defect's own shape — naming the problem while leaving the module in the state the message declares invalid. Validating first means no half-overlaid module ever exists, and the test asserts that stronger contract by snapshotting every float tensor and proving none moved after the refusal.

**What the row did not find is worth recording too.** `predict_keypoints` and `pose_eval.run` are correct as written — every hand-computed coordinate matched on the first functional run. Their gap was evidentiary, not behavioural. That is the honest result for two of the three surfaces, and it is why the row is `test(eval)` rather than `fix`.



## WP-171 — nine entry points that checked a type and not a domain

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

One shape, nine instances: a value outside its domain reached arithmetic or dispatch instead of validation, and produced a plausible answer rather than an error. `--limit -5` scored every image *except* the last five while the banner printed the truncated count as though it were the request. `--batch_size 0` raised from a progress-bar total. A misspelled `decoder` took the one-to-many path and answered with another branch's boxes — on the oriented path, another branch's headings. `img_size=641` crashed inside the neck, because the upsampled P5 width is 42 while P4's is 41. An unknown `task` was scored as detection. `--device cuda` on a machine without CUDA was accepted, loaded the checkpoint, parsed the annotations, and failed minutes later inside the first `.to(device)` with a message about a tensor rather than about the flag.

**Three homes, so no rule is written twice.** A new public `lucid_yolo/validate.py` carries `require_at_least`, `require_in_range` and `require_one_of` in one voice — `name must <constraint>; got <value>` — and both numeric checks refuse NaN explicitly, since `value < minimum` is false for NaN. `assign/grid.py` carries `require_grid_side` and `require_grid_canvas`, because stride divisibility is architecture and belongs beside `HEAD_STRIDES`; positivity is a separate condition there, as `0 % 32` and `-640 % 32` are both zero. Where a file already had a validation voice — `MuSGD.__init__`, `orthogonalize` — the check matches its siblings rather than importing the helper.

**Refusals arrive before the work.** The `evaluate` doctest passes a nonexistent checkpoint path and still gets the flag's message rather than `FileNotFoundError`, which is the property that makes the validation worth having: the point is not that the run fails, it is that it fails at the flag, immediately, naming what to write instead.

**One placement was corrected mid-row.** The draft validated `conf_threshold` and `decoder` in `cli/predict.py`, where the audit pointed. Re-reading `cli/predict.py`'s own note — a library-owned refusal must not be restated in the command — moved both into the four library entry points, which also covers `scripts/draw_predictions.py` and direct callers. The command inherits the identical message.

**The vocabulary is read, not restated.** `task must be one of ('obb', 'detect', 'segment', 'keypoints')` takes its tuple from `DEFAULT_IMG_SIZE`'s keys rather than a hand-written list, and the now-unreachable `.get(task, 640)` fallbacks became `[task]` lookups, with a test pinning both per-task tables to the same key set and to values that are themselves usable. A vocabulary written twice drifts; the second copy is the one that goes stale.

**`pick_device`'s tests patch both probes in every case.** Asserting that `"cuda"` is refused would otherwise pass on this laptop and fail on a machine that has one — the same "green because of where it ran" failure the device flag itself produces. A separate case asserts the alternatives the refusal offers are exactly the spellings `pick_device` accepts, because a message is only useful if its list is a list of working commands.

**No existing test changed.** Verified before the guards went in: no test constructs `MuSGD` with a negative or NaN gain, no caller passes `steps < 1`, every canvas constant is stride-divisible, every `conf_threshold` in the tree is in `[0, 1]`, and no test calls `pick_device` with an accelerator name. Five test files gained cases; none had an assertion altered, loosened or skipped.



## WP-170 — six ways a geometrically impossible input answered plausibly

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

Not one bug six times, but one *shape* six times: an input that cannot describe a real object produced a number instead of a refusal or a correct zero, and every one of them was invisible to a finiteness assertion.

**The aspect term was a `1/eps` gradient spike.** `atan(w / (h + eps))` with `eps = 1e-7` keeps the *value* finite at `h = 0` and makes the derivative enormous: against target `[0,0,10,20]`, a point box `[5,5,5,5]` drew `grad(x1) = 301186.5`, enough to throw a box across the image in one step. The existing test covered exactly this input class and asserted `torch.isfinite(pred.grad).all()`, which passes on 301186.5 — the assertion that cannot fail on the failure it was written for. `atan2(w, h)` is exact on the whole axis and carries no such derivative; the point box now draws `0.0`. The agent verified `atan2`'s origin gradient empirically on CPU **and** MPS before adopting it rather than trusting the identity — PyTorch returns `0.0`, not `NaN` — and dropped a guard it had planned once the measurement said the guard was unnecessary.

An inverted box draws exactly zero from the aspect term, so CIoU can never un-invert one. That is now documented as intended rather than left as an accident: repair belongs at the decode boundary, which is where M-20 put it.

**A rotated box with negative extents scored IoU 1 — and the finding understated it.** Two negative extents are a half-turn, so `canonicalize` rebuilds a correctly-wound rectangle and the shoelace formula has nothing to object to. The audit reported identical negative boxes matching each other. Measurement found worse: the negative row also scored **1.0 against the honest `[0,0,4,2,0]`** describing that region, so an impossible row was an exact match for a real detection. `_valid_rboxes` now evaluates `isfinite & (w > 0) & (h > 0)` on the inputs **as given**, before canonicalization can erase the evidence, and folds the answer into the final `where`.

**A NaN rotated box was emitted as a detection with a real score.** `torch.where(union > tiny, ...)` selects the zero branch on a NaN comparison, so invalid overlap became zero overlap, so `_suppress` suppressed nothing and the box survived every round. Now the zero is *stated* by the validity mask rather than reached accidentally — which also covers `inf`, whose comparisons behave fine — and the decoder drops non-finite rows at the threshold so the row is never emitted at all.

**ProbIoU overflowed in half precision.** Finite degenerate fp16 boxes produced `B_D = inf`; coincident ones produced all-`NaN` gradients. `_working_dtype` promotes fp16/bf16 to fp32 and casts back. Both public losses needed it independently: the Hellinger `sqrt` has a derivative of ~5e9 at `_RADICAND_FLOOR`, itself `Inf` in fp16, so inheriting promotion from the inner call would have left the coincident-NaN case unfixed. fp32 is bit-preserved — the promotion is an identity cast there, pinned by a test.

**Masking was applied to the reduction, not the inputs.** Every `v == 0` keypoint was still pushed through the RealNVP stack and the base density; only the average excluded it. The value was correctly independent of the masked point, which is what the existing test asserted — but with a non-finite unlabeled coordinate every gradient including the flow's own weights was `NaN` while the loss read healthy, and under default `validate_args` the same input raised `ValueError` from `Normal.log_prob`, so a masked-out annotation crashed the loss. Selection now happens before the arithmetic: flatten to `(N*K, 2)`, index by the mask, run the Laplace term, the flow and the `log sigma` on the selected rows only. The value is bit-identical (3.038270 before and after), the flow cost drops by the unlabeled fraction, and a masked point is now structurally unable to reach any gradient.

**The lesson the row keeps repeating.** Three of the six had a test over exactly the right input that asserted the wrong property — finiteness where magnitude was the question, value-equality where gradient-independence was the question. A degenerate-input test that asserts only "did not produce NaN" certifies that the code survived, not that it answered.

**One gap the agent's own test found in the agent's own fix.** Its first M-20 mask relied on ordering comparisons; `l = r = inf` decodes to `[-inf, -inf, inf, inf]`, which is correctly *ordered* and passed as a box covering the plane. An explicit finiteness term closed it, making the axis-aligned mask symmetric with the rotated decoder's.

**`decode_ltrb` was deliberately not touched.** It is a frozen-golden path, and the drop went to the confidence-threshold boundary in `NMSDecoder._decode_image` instead, where boxes and anchor indices still pass through exactly one selection. Nothing frozen moved: 43/43 before and after. Three pre-existing tests used `torch.zeros` raw distances as incidental scaffolding — zero-area boxes — while asserting the threshold and padding contracts; their *inputs* became 4-pixel boxes on 8-pixel centres, still disjoint, with every assertion left byte-identical and now running against real geometry.



## WP-173 — a tuple that defended its width but not its order, and a floor never reached

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

**The branch output argued against itself.** `_DetectionBranch.forward` returned six tensors as a bare positional tuple, destructured in five places, four of them with underscore placeholders — while its own docstring argues that a fixed shape must be defended, and the outer `DualHeadOutput` is a dataclass precisely because a tuple was judged unsafe one layer up. The *width* was defended and the *order* was not: inserting or reordering a future optional output would silently rebind every consumer, with no error anywhere. A frozen `BranchOutput` with the same six fields now carries them, read by attribute at all five sites. Stem execution order inside `forward` was preserved exactly, so no numeric drift was possible — and the `params_flops_*` goldens passing unchanged is the proof rather than the hope.

**`count_flops` flipped the caller's model into train mode.** `deploy()` returns a freshly constructed view whose `training` is always `True`, so the saved flag was always `True` and the closing `module.train()` propagated through *shared* submodule references back into the parent. Measured on `build_detector("n", 4).eval()` then `count_flops(m.deploy(), 64)`: the model reported `training == False` while `backbone`, `neck` and `head.o2o` were all `True` — the parent reporting eval while three quarters of it would use batch statistics and mutate BN running stats on the next forward, with nothing raising. A per-submodule snapshot is restored in a `finally`, by direct assignment rather than `train(mode)` so restoration does not depend on `named_modules()` being parent-first to undo the recursion. GFLOPs identical at 0.052695.

**The decay never reached its floor.** `LambdaLR`'s last invocation for a run of `total_steps` is `step = total_steps - 1`, so the factor landed at `0.021` against an intended `lrf` of `0.01` — twice the floor — while the doctest that displayed `lrf` evaluated `step = 100`, an index the run never reaches. A doctest can only certify what it evaluates, and this one evaluated a point outside the run. `decay_span = max(1, total - warmup_steps - 1)` fixes it and the doctest moved to `99`. One existing parametrized expectation changed `0.75` → `1.0 - 0.5 * 50 / 99`, written as an expression with a comment recording that this is a semantic correction and not a loosened tolerance: `0.75` was the off-by-one's answer.

**`warmup_steps >= total_steps` was unguarded**, and that shape is not hypothetical — it is a 1–3 epoch smoke run with a 3-epoch warmup default, and it fires inside `test_fast_dev_run_smoke` today. Every step took the ramp, the LR climbed for the whole run, peaked below `lr0`, and nothing warned. Clamped at the call site with a warning, rather than raised, because a raise would fail a launchable run over a schedule detail on the tier where the schedule matters least.

**The clamp's honest limit, recorded because a test caught the overclaim.** Clamping to `total_steps - 1` recovers the *peak* but never the *decay*: any over-request lands exactly on `warmup_steps == total_steps - 1`, whose single post-warmup step sits at progress 0, so the factor is 1.0 and the floor is unreachable by construction. The agent's first warning text promised decay; its own two-step test failed, and the message was corrected to say what the clamp actually buys and what the operator should change instead. That is the right direction of repair — the assertion stood and the claim moved.

**One latency corrected downward.** `_TupleOutputAdapter`'s `SegmentOutput` branch enumerated ten fields where its sibling enumerated twelve, dropping all four keypoint tensors. Both enumerations are now derived from `dataclasses.fields()`, so a new optional output cannot be forgotten in one branch. But `SegmentOutput` is produced only by `Segmenter.forward`, and `Segmenter.__init__` never passes `num_keypoints`, so no public builder reaches the drop today: this removes a possibility rather than repairing an active miscount, which is also why no FLOP golden moved.

**What this row hands to [WP-167](https://borda.github.io/lucid-YOLO/work_packages/wp-167/).** M-32 changes the schedule every trained run follows, so `goldens/gpu/shapes_regression_det.json`'s three metrics are expected to move. The cause is quantified rather than guessed: `det_nano_smoke.yaml` over 6 epochs is 678 optimizer steps with 339 warmup, the final factor moves `0.01292 → 0.01000`, and the largest per-step gap anywhere in the run is `0.00292`, 0.29% of `lr0`, against a 0.05 tolerance. The four `overfit_micro_*` goldens are structurally immune and this was verified by reading the producer rather than assumed: `run_overfit` hardcodes `lrf=1.0, warmup_epochs=0.0`, which trips `schedule_off` so no scheduler is constructed at all. `optim_toy` runs at constant LR and never imports `warmup_decay_factor`.



## WP-169 — alignment weights survive zero-metric ties and small overlap

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

**`topk` ranked the ties globally, not within each ground truth.** `_select_topk` ran `topk` over an alignment metric that is zero at non-candidates *and* zero at a candidate whose prediction misses — the state a freshly initialized head is in, since `decode_ltrb` applies no non-negativity and inverted distances decode to zero-IoU boxes. With a ground truth's whole row tied at zero, `topk` filled its `k` slots from the global tie order, which need not contain a single one of *that* ground truth's candidates, and the intersection with `candidate_mask` then returned **zero** positives for a ground truth that had four. Measured on a 4×4 stride-4 grid with one real ground truth whose candidates are `{10, 11, 14, 15}` — deliberately not the low indices the tie order favours — every one of `topk` ∈ {1, 3, 4, 6} returned 0 positives where `min(k, c)` was expected. Ranking over `align_metric.masked_fill(~candidate_mask, -1.0)` fixes it; `-1` is the sentinel `_resolve_conflicts` already uses and is safe because `t` is non-negative by construction. The intersection is kept rather than dropped: when `k > c` the surplus slots land on the `-1` fill and are removed there.

**An absolute floor under a sixth power.** The target normalization divided by `t_max + eps` with `eps = 1e-9`, but `t_max = s · u_max⁶` collapses as the sixth power of the overlap, so the floor is eventually larger than the quantity it guards. Parametrized over `u_max` ∈ {1.0, 0.5, 0.1, 0.01}, the best anchor's weight — which is `u_max` by definition, since `t_max` is its own row's maximum — came out `0.999999998`, `0.49999994`, `0.0998004` and `4.9975e-06`: correct at full overlap, **2001× too small** at IoU 0.01. A positive trains as background while `fg_mask` still calls it foreground. The denominator is now floored at `torch.finfo(dtype).tiny`, which only ever guards a true 0/0 — and there `align_pos` is zero too, so the product is zero rather than NaN.

**The repo's own frozen data was the strongest evidence.** `tests/ptl/prechange_detect_step.json` pinned, at initialization, `train/o2m_box` at `4.12e-09` and `train/o2m_l1` at `3.17e-08` — the one-to-many branch's entire localization objective, weighted into nothing, roughly 1.9e6× suppressed. That snapshot was written to detect objective drift across [WP-088](https://borda.github.io/lucid-YOLO/work_packages/wp-088/) and did that job; what it also did, unremarked for eight releases, was pin the defect. `tests/assign/test_rotated_assign.py` carried the same fingerprint: image 1 anchor 5 is its ground truth's best anchor, so its weight is exactly `u_max = 0.25`, and the snapshot stored `0.249998539686203`.

**Both frozen sets were re-captured, and the ruling is recorded here rather than in `ESCALATION.md`.** Nothing under `goldens/` moves — all 43 pass, and a zero-tolerance sweep of all 447 metrics across 48 files confirms every `assignment_cases.json` metric is bit-identical in the live copy and in all six frozen sets. Trigger 4 is "a change would require altering a frozen golden", and no change does, so no escalation entry was opened. The two `tests/` snapshots are a different question, decided deliberately: the pre-change loss values were re-captured by replaying the test's exact sequence under the same seeds and `set_num_threads(1)`, with seeds, config, gains and the 714 `state_dict_keys` unchanged and a `recapture_reason` field recorded in the file. The rotated weights were *derived* from the frozen ones by the closed-form factor rather than read off the code under test, and agree with the current output to ≤2 ULP.

**One test passed only because of the defect.** `test_rotated_candidacy_reaches_assigner` asserted the rotated call produced strictly *fewer* positives than the axis-aligned one. It passed — because the smaller rotated candidate set simply lost more slots to the global tie fill. With H-16 fixed both saturate at `min(k, c) = k`, so the count stopped discriminating while set membership still does; the assertion moved to `not torch.equal(rotated.fg_mask, axis_aligned.fg_mask)`, which still fails if `gt_rboxes` is dropped, since the two calls then become identical. Probed over 40 independent seeds: the new assertion holds 40/40 and the old one 0/40, both counts pinned at exactly 10 every time. The old assertion is not merely weakened — it is now never true.

**Attribution was measured, not argued.** Applying one fix at a time separates them cleanly: with H-16 reverted, `test_detect_step_reproduces_pre_change_snapshot` yields `24.32134246826172`, identical to all digits with both fixes, so H-16 contributes nothing to that scene; the three bit-identity failures in `test_rotated_assign.py` also survive H-16's reversion and move by exactly `(t_max + 1e-9) / t_max`, and only `test_rotated_candidacy_reaches_assigner` passes with H-16 reverted.

**What this row hands to [WP-167](https://borda.github.io/lucid-YOLO/work_packages/wp-167/).** H-17 changes the objective's weighting on the first step of every trained run, so `goldens/gpu/overfit_micro_*.json` and `goldens/gpu/shapes_regression_det.json` are expected to move. Those five files have no copies under `goldens/frozen/`, so recomputing them is an ordinary `FREEZE=1` re-freeze on an accelerator rather than a §7 question — but it must happen with 169 and 172 both landed, which is why it belongs to 167 and not here.



## WP-172 — every merge path keeps every modality

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

**`CopyPaste._merge` rebuilt `Targets` without two of its columns.** It passed `boxes`, `labels`, `polygons`, `keypoints` and `keypoint_vis` and no more, so `difficult` and `rboxes` were dropped. The `difficult` loss is the quieter of the two: `Targets._resolve_difficult` expands an omitted column to one `False` per instance, so nothing raises — a destination carrying `difficult=[True]` merged with a source `[False, True]` returns `[False, False]` where `[True, False]` is correct, and under A48 that turns every ignorable ground truth into an ordinary false positive. `difficult` is now carried by the same `index` the boxes take, which is total by construction since `_resolve_difficult` guarantees both sides are length-N. `rboxes` is deliberately **not** carried, and the docstring says so, so the omission is not re-read as this same bug later: a paste transfers a rasterized polygon mask and the oriented path carries no polygons ([WP-056](https://borda.github.io/lucid-YOLO/work_packages/wp-056/)), so refusal is the fix rather than a transfer that would claim to paste something the transform cannot paste.

**The rotated refusal did not hold through the seam [WP-147](https://borda.github.io/lucid-YOLO/work_packages/wp-147/) made public.** `_reject_rboxes` was called only from `__call__`, never from `apply`, so a caller building `CopyPasteParams` directly with a rotated destination reached `_merge` and lost the modality silently — the exact failure the guard exists to prevent. `apply` now calls it as its first statement. `__call__` keeps its own copy on purpose: the guard has to fire before `sample` consumes a generator draw, so moving it rather than duplicating it would have changed RNG ordering on the sampling path. The `Raises:` section records both the duplication and its reason.

**The flip never re-clipped.** `_mirror_targets` reflected about `width - 1` and returned. The COCO reader clamps boxes to `[0, width]`, so a box legitimately touching the right edge mirrors to `x1 = (W-1) - W = -1`: at `W=10`, `[4, 1, 10, 3]` came back as `[-1.0, 1.0, 5.0, 3.0]`. `HorizontalFlip` is the last stage of the train pipeline, so that `-1.0` is what the assigner receives. The clamp is on `x` only. A full-canvas clip was implemented first, using `fuse-augmentations`' own `clip_bbox_xyxy`, and it failed the class doctest by trimming `y2` from 2 to 1 — which is the argument for the narrower fix rather than a reason to repair the fixture: a mirror moves only `x`, so clamping `y` edits geometry this transform never touched, makes the flip's output depend on a height it does not otherwise read, and silently trims a box that arrived off-canvas from some earlier stage. Keypoints, rotated boxes and polygon rings are left unclipped, each for a stated reason — a mirror is an isometry and cannot push a wholly on-canvas box off it, clamping an rbox centre while keeping its extents describes a *different* rectangle rather than a clipped one, and nothing consumes rings raw before rasterization, which clips at the grid.

**The convention was stated but unasserted, which is the half that matters.** Correcting the row's own framing on evidence: the `W-1` convention *is* implemented and *is* documented, in `augment.py`'s class docstring and in `rotated_aug.mirror_rboxes`, both citing [WP-154b](https://borda.github.io/lucid-YOLO/work_packages/wp-154b/). What was missing is a test that could fail. The existing ones restate the formula `(width - 1) - src`, which cannot distinguish a correct implementation from an expectation that drifted with it. The new test asserts by consequence instead: a box drawn tightly around a bright two-column block must still bound that block after both are mirrored. Under the `W` convention the block lands at columns `{6, 7}` while the box is carried to `[7, 8]`, and the test fails. It passed on first run before any fix — which is the evidence that the choice was already right and only unguarded.

**Train and val COCO label spaces were derived independently and never compared.** `_build_coco_splits` constructs two `CocoDetectionDataset`, each running `_build_category_maps` over its own file's `categories`, sorting ids and numbering from zero. Given val `[1, 7]` against train `[1]`, val maps `7 -> 1` while train has no label `1`: the model trains against one numbering and is scored against another, reporting a plausible number the whole way. `_check_shared_label_space` now raises before either split is served, naming the two one-sided sets separately so the message identifies which file is the odd one. Comparing id *sets* is sufficient and not merely necessary, since the mapping is a function of the sorted id set. The YOLO path needs no equivalent — `YoloDetectionDataset.from_root` reads one `data.yaml` for both splits, so the label space has a single source and cannot disagree with itself.

**No golden moves, and the escalation clause is vacuous.** All 43 pass. A zero-tolerance sweep of 447 metrics across 48 files finds exactly one moved entry, `rotated_rbox_area` at `96.1934 -> 96.1935`, in the live `aug_invariants.json` and in `frozen/0.6` and `frozen/0.7` — and it is **not** this row's: running the pristine main-tree `lucid_yolo` against the same unmodified producer reproduces it. The unrounded value is `96.1934585571289`, sitting `8.6e-06` above the 4-decimal rounding boundary that `round(x, 4)` resolves to `96.1935`; it was frozen when it sat just below. That is environment drift predating WP-172, five thousand times inside the `0.5` tolerance, and it is recorded here because the next re-freeze would otherwise read it as this row's consequence. The strongest evidence that the flip fix itself moves nothing is `data_checksums.json`, bit-identical across all ten metrics: it runs the whole WP-015 pipeline under a fixed seed and is the only golden that exercises `HorizontalFlip` in-pipeline, `aug_invariants`' producer containing no flip call at all.

**One fixture was pinning the defect.** `test_double_flip_identity` ran on a `torch.rand(3, 4, 10)` canvas while its `_targets()` fixture carries boxes out to `x=55` — geometry five times off-canvas. The involution is a property of the mirror, and a re-clip necessarily breaks it for geometry that started outside the canvas, so the test as written could only pass while nothing clipped. The canvas moved to `64x64`, which keeps the test's intent and makes its geometry coherent.

**What is deferred, and why.** Two files can share an id set and disagree on what each id names — a real label-space disagreement with an empty symmetric difference. That check is not implemented: `CocoDetectionDataset` does not retain category names, so it would mean widening the reader's public surface, which is outside this row. Recorded rather than dropped.



## WP-175 — the record describes the repository that exists

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

**Two checks first, then the wording.** The order is the point. A sweep that only edits prose leaves the next drift undetected, and every claim this row corrects had been wrong for at least one release precisely because nothing failed on it. `audit_version_single_source.py` gained `check_readme_names_the_current_version`, which compares the README's `Current release: **X**` line against `lucid_yolo.__version__` and also fails when that line is absent, so deleting the sentence cannot silence the guard; it caught the live `0.4.0` against `0.7.0` drift on its first run. `audit_license_headers.py` gained `the YOLO26 paper` as a required fragment of both `NOTICE` and the README. A positive fragment suffices rather than a ban on the vendor-bound form, because the leading article discriminates — `the YOLO26 paper` is not a substring of `the Ultralytics YOLO26 paper` — and a repo-wide ban would have to carve out R1's own literal title in `PROVENANCE.md`.

**Three of the row's own premises were wrong, and are corrected here rather than transcribed.** The row says the blueprint is the source of *49* in-code citations; measured on this tree it is **133 `blueprint` lines across 36 files under `src/`**, 107 of them in `blueprint sec. N` form, and R37 records the measured figure. The row says the README "offers an install line that resolves to nothing" — true, but understated: PyPI's JSON API shows `lucid-yolo` with **26 releases, every one a `.devN` pre-release**, latest `0.5.0.dev2`, so a bare `pip install` resolves to nothing because pip skips pre-releases, `--pre` resolves to a snapshot two minors behind this tree, and the README's own explanation ("every distribution published before 0.4.0 is a `.devN`") was wrong twice over — there is no non-dev release at all and no 0.4.x of any kind. The install line is gone, replaced by the evidenced explanation and D20's consequence: the runtime git pin makes 0.7.0 unpublishable until a `fuse-augmentations` 0.12 exists on PyPI.

**A seventh site, and a claim the row did not know was stale.** The vendor-bound phrase was enumerated in `NOTICE`, `docs/index.md` and four model cards; it also sat in `docs/REPRODUCTION_REPORT.md`'s preamble. That document is append-only under D10 — but the rule is scoped to its dated sections ("a later release corrects an earlier claim by adding to it"), and the preamble is the document's standing self-description rather than a record of anything, so correcting it edits no claim away. The non-affiliation sentence keeps the vendor's name, on [WP-161](https://borda.github.io/lucid-YOLO/work_packages/wp-161/)'s reasoning that a disclaimer not naming the party it disclaims is a sentence about nobody. Separately, README:62 asserted the paper "addresses no keypoint task at all"; the R1 abstract, fetched during link verification, lists pose estimation among the family's tasks. The absolute claim is replaced by the checkable one — no allowlisted paper publishes a keypoint architecture or parameter table, which is what A65/A66 actually support.

**The task count, in six documents and one coupled pair.** The README banner, `docs/index.md`, `docs/TRAINING.md`, `docs/DATASETS.md`, `NOTICE` and `lucid_yolo.__init__`'s package docstring all counted three tasks against four shipped tiers, the keypoint tier having released at 0.5.0. `pyproject.toml`'s `description` and `mkdocs.yml`'s `site_description` counted three as well, and those two are not independent — `audit_docs_site.py` requires them byte-identical, so editing one alone fails the gate. It did, which is how the coupling was confirmed rather than assumed.

**Two register rows.** `docs/PROVENANCE.md` gains a *Project-internal governing documents* section with **R37** for the blueprint — authorship as `NOTICE`'s copyright holder, unpublished and non-public, no licence grant and not redistributable, every method claim downstream of R1–R36 — and a second audit row covering `975ff50..f6ec3e1`. That audit walked 193 commits and found 27 whose trailers are refused by `check_commit_trailers.py`; all 27 predate `b4f3e03`, and the enforced window `b4f3e03..f6ec3e1` is clean at 15 of 15. The 27 are recorded as **unexamined** rather than characterised, because sampling found at least one that is a citation failure (`'Provenance:' trailer lists no source id`) rather than a shape failure, and a register row asserting a category it did not check is the defect this row exists to remove.

**One stale claim left standing, deliberately.** `docs/DECISIONS.md`'s D1 still reads "Pose and classification remain future 0.x work" against a keypoint tier that shipped at 0.5.0. D1 is a recorded decision rather than a description, and rewriting one to match what later happened is how a decision log stops being evidence of anything. It wants a superseding entry, which is a `[PRINCIPAL]` act and not this row's.



## WP-167 — the accelerator half gets a schedule and the floors a gate

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

**The Makefile said it was the schedule.** `gate-gpu`'s comment claimed the target "is that schedule", and nothing invoked it: four workflows, none with `schedule`, none with `workflow_dispatch`, none mentioning the accelerator. `ci-tests.yml` runs `-m "not gpu and not data"` and calls `check_goldens.py` without `--include-gpu`, so the whole `goldens/gpu/` subtree and every marked test were unrun remotely — which is how the frozen detection overfit golden drifted for six days under WP-078, and why `RESEARCH_LOG.md` still records `shapes_regression_det.json` as outstanding. `gate-gpu.yml` is nightly at `17 4 * * *` plus `workflow_dispatch`; the off-the-hour minute keeps it out of the top-of-hour queue spike.

**Arming it is one repository action, deliberately outside the file.** The job carries `if: vars.GPU_RUNNER_LABEL != ''` and `runs-on: ${{ vars.GPU_RUNNER_LABEL }}`. Unset, both triggers resolve to a *visible skipped job* rather than a run queued forever against a self-hosted label no machine answers to — the failure mode where a green Actions tab and an unrun gate look identical. GitHub-hosted runners have no GPU, so there is no default worth hard-coding. `cancel-in-progress: false`, because a half-killed training run leaves no signal about the goldens at all.

**The run record is the gate's own stdout, not a second opinion.** Clause 1 admits "a run recorded under `.reports/`", and the workflow `tee`s `make gate-gpu` into `.reports/gate-gpu/gate.log` under `set -o pipefail`. It deliberately does not re-run `check_goldens.py --include-gpu` to produce the record: every producer there retrains a model, so recording the result that way would cost a second full gate *and* could disagree with the run that decided the verdict.

**Three of four floors were enforced by a human at a prompt.** The comparison lived once, in `main()`. `run_overfit` returned its score unchecked, so the `seg`, `obb` and `kp` golden producers and every marked test accepted a below-floor run; only detection had a test asserting a floor. The comparison moved into `run_overfit`, raising `FloorNotMet` on the raw unrounded score. It subclasses `ValueError` and pointedly **not** `RuntimeError`, because `run_overfit` catches `RuntimeError` to retry training non-deterministically and a floor miss must never be swallowed as a missing deterministic kernel. The ordering matters as much as the placement: the raise precedes the freeze branch, so `--freeze` is now unreachable on a failing run and a bad re-freeze cannot happen quietly. Under `make golden-gpu` a below-floor task surfaces through `check_golden`'s broad handler as a clean `producer raised FloorNotMet: ...` line rather than a traceback; `check_goldens.py` itself was not touched.

**Two bands admitted values the published DoD rejects.** `write_golden` stored `spec.tolerance` verbatim, so `overfit_micro_kp.json` at `0.335667 ± 0.05` had a lower edge of `0.285667` against a `0.30` floor, and `overfit_micro_obb.json` at `0.938966 ± 0.05` an edge of `0.888966` against `0.90`. A golden could pass while the acceptance criterion it exists to protect had failed. `clamped_tolerance(score, spec)` returns `max(0.0, min(spec.tolerance, round(score - spec.floor, 6)))` and `write_golden` calls it, so the clamp lives in the writer and a re-freeze produces a correct band — no stored JSON was edited. The six-decimal rounding is not cosmetic: the natural assertion `score - tolerance >= floor` failed on `obb` at one ulp (`0.8999999999999999`), and pinning the arithmetic to the precision the goldens actually store fixed the representation rather than widening the assertion. `shapes_regression.py` was checked for the same defect class and has tolerances but no acceptance floors at all, so there is no band to clamp; reported rather than changed.

**What this row cannot close, and why it stays open.** Clauses 3 and 4 — `goldens/gpu/` re-frozen with every band above its floor, and `make gate-gpu` green — need CUDA. This tree has MPS. [WP-169](https://borda.github.io/lucid-YOLO/work_packages/wp-169/) moved the first-step objective weighting, so the five accelerator goldens that passed on an L4 against `f8225af` will not pass now, and the re-freeze must run *after* 169 and 172 rather than before. The row therefore lands its code with its status still open; the regenerated goldens and the flip are a second commit. The freeze order is `overfit` for each of `det`, `seg`, `obb`, `kp` and then `shapes` for `det` — five files, not four, `shapes_regression_det.json` being the one `RESEARCH_LOG.md` records as outstanding.

**Measured, and left alone.** The L4 reference run sat near 10% GPU utilization and 5% of 24 GB, and Lightning named both causes itself: `num_workers=0` against 12 vCPU, and default matmul precision on a Tensor Core card. Neither is taken here. Both change the numerics and therefore move every accelerator golden, which is a decision this row has no standing to make while it is busy fixing the gate that would catch it. The cheapest change that is *not* a numerics change is running the four `overfit_micro` tasks concurrently — separate processes, ~5% of the card each — and that is recorded as a recommendation rather than applied.

**The re-freeze, measured before it was performed.** Clauses 3 and 4 closed on an L4. `make golden-gpu` ran *first*, against the goldens frozen before [WP-169](https://borda.github.io/lucid-YOLO/work_packages/wp-169/), and returned **46/48**: `overfit_micro_kp` expected `0.335667` and got `0.574728`, `overfit_micro_seg` expected `0.814623` and got `0.86491`. That failure is the evidence the re-freeze was necessary, rather than an assumption that it must have been. The other three passed while still moving — detection `0.994932` to `0.996622`, oriented `0.938966` to `0.964928`, `shapes_regression_det`'s NMS mAP50-95 `0.882337` to `0.870817` — so the stored bands absorbed a real 0.026 shift on `obb` without reporting it. A band wide enough to hide the movement it was sized for is the same defect class as a band admitting a sub-floor value; the first is recorded here, the second is what `clamped_tolerance` fixes.

**The clamp does not bite today, and saying otherwise would be false.** Every re-frozen score clears its floor by more than its own tolerance: keypoints by `0.2747` against a `0.05` band, oriented by `0.0649`, detection by `0.0466` against `0.04`, segmentation by `0.1649`. `min(spec.tolerance, score - spec.floor)` therefore returns `spec.tolerance` unchanged in all four cases, and `shapes_regression_det` has no floors to clamp against at all. The two edges this row was opened to close — keypoints admitting `0.285667` against a `0.30` floor, oriented admitting `0.888966` against `0.90` — are moot rather than tightened: the scores moved out of reach of their own lower edges. The clamp stands as the guard that will bite on the next regression, not as something that changed a stored band here.

**What moved the keypoint number.** `train_oks_ap` rose `0.239061`, from `0.335667` to `0.574728` — a 71% relative gain on a tier that shipped at 0.5.0. That is [WP-169](https://borda.github.io/lucid-YOLO/work_packages/wp-169/) landing, and it is the first place in the repository where the size of that defect is visible as a number: the one-to-many branch's entire localization objective had been annihilated, `train/o2m_box` pinned at `4.12e-09` in `tests/ptl/prechange_detect_step.json`. The offline gate could show that the objective was suppressed; only the accelerator floors could show what suppressing it cost.

**Two runs, one set of values.** The freeze ran twice, in two independent Colab sessions on two separate L4 VMs, each reporting `NVIDIA L4` and default `highest` matmul precision. Agreement is claimed at the precision each run actually recorded, which is not the same number in both cases. Within the second session it is exact: `golden-gpu`'s pre-freeze diagnostics report `got 0.574728` and `got 0.86491`, matching the files the freeze then wrote at all six stored decimals, so the measure step and the write step agree digit for digit. Across the two sessions it is four decimals, because the first session's record is `overfit_micro.py`'s own stdout, which rounds — `0.5747`, `0.8649`, `0.9966`, `0.9649` — and its six-decimal values were not preserved. Four independent decimals on four tasks is the checkable claim; "identical to every stored decimal" would not be. The SHA-256 of all five files matches between the session that produced them and this tree. The second session's `make gate-gpu` did not reach `golden-gpu`: pytest collection failed at `tests/models/test_onnx_export.py:71` on `import onnx`, because that session installed `pip install -e .` without the `dev` dependency group where `onnx` and `onnxruntime` are declared, and Colab's own image supplies `pytest` but not `onnx`. That is a provisioning gap in the throwaway notebook, not a defect in the tree — `.github/workflows/gate-gpu.yml:52` installs `--group dev`, which is why the workflow this row adds would not hit it. The green `gate-gpu` of record is the first session's complete run on this same tree: 6 marked tests passed in 4464 s, then **48/48 goldens passed**, exit 0.



## WP-176 — v0.8.0, the audit remediations

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

**The release whose subject is a class of defect rather than a capability.** Nothing in 0.8.0 detects anything 0.7.0 could not. What the sixteen rows have in common is that every defect they fixed was invisible to a green gate: an assigner whose one-to-many localization objective was annihilated at `4.12e-09` while all 43 offline goldens passed, a merge path dropping a modality, entry points validating types but not values, four acceptance floors compared once at a prompt, and a record describing a three-task repository against four shipped tiers. So the gates moved with the fixes — `ci-tests.yml` runs `make test` rather than a narrower subset, `goldens/frozen/MANIFEST.sha256` makes a frozen value uneditable alongside its own tolerance, the accelerator half has a nightly workflow instead of a Makefile comment claiming to be one, and `FloorNotMet` raises where the score is produced. The one number the release moves is the keypoint overfit floor, `0.335667` to `0.574728`, which is what the assigner defect cost measured after the fact.

**The mechanics, which are four lines.** `__version__` moves `0.7.0` to `0.8.0` and that is the whole version change, `pyproject.toml` declaring it dynamic and reading the module attribute. `[Unreleased]` closes as `[0.8.0] - 2026-09-05`. `make freeze-goldens MINOR=0.8` copies seven live metric sets into `goldens/frozen/0.8/` — the same seven `0.6` and `0.7` hold, `data_checksums.json` and `fixture_checksums.json` still skipped as generator-derived under [WP-154c](https://borda.github.io/lucid-YOLO/work_packages/wp-154c/)'s `"freezable": false` — and the offline gate goes from 43 goldens to 50. The README's `Current release` line is rewritten, which for the first time is load-bearing rather than courtesy: [WP-175](https://borda.github.io/lucid-YOLO/work_packages/wp-175/) added `check_readme_names_the_current_version`, so a release that bumped the attribute and left the prose behind would now fail its own gate instead of drifting three minors the way `0.4.0` did.

**The row's own premise was wrong, and the freeze is ordinary because of it.** The scope cell says two of this phase's rows moved a value pinned in every frozen set from 0.2 forward, "so this freeze is also where those moves stop being recorded exceptions and become the baseline a later release is held against". Written in advance, that was a prediction about [WP-169](https://borda.github.io/lucid-YOLO/work_packages/wp-169/) and [WP-172](https://borda.github.io/lucid-YOLO/work_packages/wp-172/), each of which carried a DoD clause requiring a principal ruling in `docs/ESCALATION.md` *before* any frozen value moved. Both rows measured instead of assuming, and **neither moves a frozen value**: [WP-169](https://borda.github.io/lucid-YOLO/work_packages/wp-169/)'s two fixes leave `assignment_cases.json` bit-identical live and frozen from 0.2 forward, and [WP-172](https://borda.github.io/lucid-YOLO/work_packages/wp-172/)'s merge-path repair moves no golden at all. `docs/ESCALATION.md` therefore gains no entry here, and the 0.8 snapshot normalizes nothing — it is an ordinary release freeze. Phase 14's two overrides of AGENTS.md §7 remain the only ones, and remain non-precedential.

**One value that looks like drift and is not.** `aug_invariants.json` stores `rotated_rbox_area` as `96.1934`; the producer emits `96.1934585571289`, which rounds to `96.1935` at four decimals and therefore reads as one ulp of disagreement in any four-decimal report. The unrounded value sits `8.6e-06` above the `96.19345` rounding boundary, and the stored tolerance is `0.5`, so nothing fails and nothing has moved. It is copied into `0.8` exactly as `0.6` and `0.7` hold it, and recorded here because a later reader comparing a rounded report against the stored literal would otherwise open an investigation into a representation difference.

**D21, and a decision left unedited.** `docs/DECISIONS.md` D1 still reads "Pose and classification remain future 0.x work" against a keypoint tier released at 0.5.0 — the stale claim [WP-175](https://borda.github.io/lucid-YOLO/work_packages/wp-175/) found and deliberately did not touch, on the grounds that rewriting a recorded decision to match what later happened is how a decision log stops being evidence. This row is `[PRINCIPAL]` and can do what [WP-175](https://borda.github.io/lucid-YOLO/work_packages/wp-175/) could not: **D21 supersedes the keypoint half of D1's scope clause and nothing else.** Classification and YOLOE-26 stay out of scope, and no row proposes them. The entry carries two qualifications D1's wording could not: the tier is *keypoints, not pose*, `K` being a constructor argument that nothing in the head, loss or decode interprets; and it is the one accepted tier not drawn from R1, its loss and evaluation protocol coming from R14 (RLE, arXiv:2107.11291) because no allowlisted paper publishes a keypoint architecture or parameter table to check the head against (A65, A66) \[**struck 2026-09-17**: false — R1 sec. 3.4.2, Table 9 and Table S10 do; D22 withdraws this clause of D21 and records what the tier is instead, a partial reproduction of R1's pose objective\]. D1's word "pose" therefore never named what shipped, which is the second reason to supersede rather than amend.

**What this release still does not do.** No tag is cut and no distribution is published, as at 0.4.0 through 0.7.0; five release commits now stand ahead of the train's own tags. `release.yml` fires on a pushed version tag and re-runs the guard, and both are the operator's to trigger. D20 stands unchanged: `fuse-augmentations` is pinned as a direct reference to `a5a276a`, which is legal to build and install and illegal to upload, so this distribution remains unpublishable to PyPI until a 0.12 reaches it — at which point a `>=0.12,<0.13` range replaces the reference and nothing else changes. No trained weights ship (D14).

