🧭 Phase 15 — Audit remediation, v0.8.0¶
WP-160 — the guard checked everything except what a release is¶
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 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.
WP-161 — the discipline was written down and the banner did not follow it¶
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¶
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'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 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 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¶
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¶
This row lands the candidate 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'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¶
This row lands the candidate 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 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 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¶
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 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 and 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, 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¶
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¶
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¶
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¶
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¶
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¶
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. 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¶
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 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. 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¶
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), 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 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. 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¶
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'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¶
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 tees 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 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, 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 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¶
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'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 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 and 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's two fixes leave assignment_cases.json bit-identical live and frozen from 0.2 forward, and 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 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 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).