📦 Phase 10 — Consolidation¶
WP-066 — an untrained checkpoint cannot tell you whether an export is right¶
The architecture-fidelity half of this finding — the exported graph confirming the one-to-one, DFL-free claim — is recorded at wp-066.md#wp-066. What is here is the export-and-comparison harness, which turned out to be where nearly all of the difficulty was.
The graph half of this package was the easy half. All three deploy paths export first try on the legacy TorchScript tracer at opset 20, in about 0.3 s each, and none of them contains a NonMaxSuppression node — 598 nodes and 30 op types for detection, with a single TopK where suppression would be. dynamo=True, the torch 2.13 default, is not used: it needs onnxscript, which nothing else here needs, and the legacy path already emits fully static dims. Subgraphs are walked recursively, since an absence claim a node nested in an If body could evade is not an absence claim; no control-flow nodes appear at all. The two Softmax nodes are the attention block's, not a distribution-focal-loss box head's — the DFL-free claim showing up in the emitted graph rather than in a docstring.
The absence of an op proves the graph's shape and nothing about its arithmetic, so the package also runs the exported model under onnxruntime against the checkpoint it came from. That is where the work was, and almost all of it went into discovering that a freshly initialized checkpoint makes the comparison meaningless while appearing to work.
An untrained BatchNorm in eval mode carries running_var=1 and running_mean=0, so it rescales nothing and the signal attenuates through depth: the neck's P3 output measures 4.1e-05 and the classifier's contribution to its own logit lands near 1e-7. The prior-probability bias is -log((1-pi)/pi) = -4.595 (A30), and float32 resolution at 4.595 is 4.8e-07. The contribution is an order of magnitude below the resolution of the number it is added to, so it vanishes: all 1344 class logits become one identical float32 value, torch.unique returns 1, and the top-k over 336 anchors is a 300-way tie. Torch and onnxruntime break that tie differently, both correctly, and a row-by-row comparison duly reported boxes 80 px apart. The head is not broken — the same model in train mode, on batch statistics, gives 1343 distinct logits of 1344 at std 0.372. Only the eval-mode statistics are degenerate, and only until something trains them.
Scaling the classification weights to force separation does not work either: a sweep at 1e2 through 1e5 saturates the sigmoid, and from 1e3 up all 336 confidences collapse to exactly 1.0 — the same degeneracy from the other end. What works is calibrating the BN running statistics with a few forward passes in train mode, then rescaling the stems to a measured logit spread rather than a guessed one.
A second degeneracy was hiding behind the first, and it is the one worth remembering. With the ranking fixed, the segmentation masks compared bit-exact: zero mismatches out of 4,915,200 pixels. They were all False. This head regresses ltrb directly with no DFL and no range cap, so an untrained box stem emits values straddling zero, every decoded box is inverted or empty, and decode_instance_masks crops each mask to nothing. A perfect score on an assertion comparing two empty tensors. Biasing the box stems positive gives 300 boxes with interiors and 5.07% mask foreground across 44 rows, and the same assertion then means something. The test asserts its own preconditions — score span, positive widths, non-empty masks — because the failure mode of this comparison is not a wrong answer, it is a vacuous one.
The oriented head appeared to fail and did not. Row-by-row it diverged by 88 px against 3e-05 for the other two, which reads exactly like a broken rotated decode. The tempting explanation was the A23 canonicalization swap, since w >= h flips discontinuously — and measuring killed it outright: the minimum |w - h| over 300 rows is 4.9e-02, no row within 1e-05 of the branch point. Comparing each column as a multiset instead showed every column agreeing to float32 noise, so the exported graph had been selecting the same detections all along. The fault was the comparison: rows tied on score were ordered by raw cx, which clusters tightly because near-square boxes put cx at the anchor centre and a whole grid column shares one value, so 1.5e-05 of backend noise reordered them. Rounding the ordering keys to three decimals — well above the noise, well below any real difference — pairs the rows identically on both sides. Recorded because the shape of the mistake generalizes: an order-sensitive comparison of a tie-bearing output will accuse the implementation of an error the harness committed.
What the comparison is worth was then measured rather than asserted. Three deliberate corruptions of the baked-in constants — transposed anchor points, an anchor grid built for the wrong canvas, and doubled strides — are each caught on every head, nine of nine, at box deltas between 1.2e+02 and 2.0e+02 against a 1e-3 tolerance. Boxes agree to 4.6e-05, scores to 1.2e-07, class ids exactly, and the masks bit-exactly at both one and the default intra-op thread count.
The anchor grid and strides are baked into the graph as buffers rather than taken as inputs, so the exported model's only input is the image and the graph is specific to one input size. That is the normal ONNX trade, and it is pinned by asserting the graph has exactly one input, so the choice cannot drift into being an accident. onnx and onnxruntime are dev-group dependencies: the export itself is torch.onnx.export, already a runtime dep, so nothing a consumer installs changes.
One thing this package deliberately leaves standing. The decode composition now exists twice — once in predict.py's E2E path and once in the test's graph wrappers — with nothing forcing them to agree. A shared export.py composing deploy and decode would remove the drift risk, and it was not built here on the smallest-change rule; it is the natural home if a later package needs an exportable path it can ship.
WP-109 — a wheel can ship a GPL binary and declare it nowhere¶
The package exists because of what sourcing WP-067's example turned up. supervision, the obvious drawing library for boxes, masks and rotated boxes, hard-requires av>=14.2. The av wheel declares BSD-3-Clause, ships a licenses/LICENSE.txt in which grep -ci gpl returns 0, and ships av/.dylibs/libx264.165.dylib. x264 is GPL-2.0. Running this repository's own audit functions against a throwaway install of it — never the project venv — returned declared: ['BSD-3-Clause'] | bundled: []: both checks clean, on a wheel carrying a GPL binary. The gate whose entire purpose is seeing this saw nothing, and the reason it saw nothing is that a vendored binary need not be documented anywhere at all. D15 was found by reading a license document the wheel happened to ship; this one had no document to read.
The third check reads the file list, and the honest description of it is that it narrows the hole rather than closing it. Filenames are matched against a named table of copyleft libraries — x264, x265, mp3lame, GEOS, the eight FFmpeg libraries — so a copyleft library the table has never heard of passes exactly as av did before the table existed. The alternative was considered and is not available: torch alone ships hundreds of legitimate native libraries, and a check failing on every unrecognized one would fire on every commit and be switched off within the week. What the table can honestly claim is that no listed library is present, and the module docstring says that rather than implying more.
Two things made the scan work at all, and both were near-misses.
The normalizer has to know what auditwheel does, or the check works on macOS and goes blind on Linux. The libgeos_c.1.19.2.dylib macOS ships verbatim arrives on manylinux as libgeos_c-abcdd5fa.so.1.19.2: the repair tool grafts eight hex characters onto the name. A table keyed on the plain stem matches every local run and nothing on the CI runner — and the failure presents as a green gate, which is the worst shape a licence check can fail in. Verified against all 20 vendored libraries in the shapely and pillow manylinux wheels, the graft is uniformly eight lowercase hex characters; it is stripped before matching, and a live test on shapely fails if the convention ever changes.
The table's licence notes were the part most at risk of being written from memory. Each entry claims a licence and where that licence was read, which is worth exactly as much as the reading actually done. So they were read: VideoLAN's x264 page states x264 is released under the GNU GPL and separately available commercially; x265's own COPYING is the GPL version 2 text; FFmpeg's LICENSE.md states most of FFmpeg is LGPL v2.1-or-later and that --enable-gpl changes its licence to GPL v2+, naming libx264, libx265 and libxvid together as the GPL-v2 externals that flag admits; the LAME page states LGPL and names no version, so the entry names none either. GEOS needed no page at all — shapely's installed LICENSE_GEOS opens License: LGPLv2.1, which is the same exposure D15 recorded reached by a second route, and it is what makes the table's behaviour observable in this environment rather than hypothetical.
The allowlist is keyed on (distribution, library) rather than on the package, which is the one place this check deliberately differs from the bundled-document allowlist beside it. That file's own rule, from D15, is that a package excused for one vendored library must not be excused for every other one it ships — and a package-keyed entry here would break exactly that rule, since allowing shapely's GEOS would also wave through a libx264 it started shipping tomorrow.
The tests are two-sided on purpose: the synthetic av must be caught, and the live environment — torch, numpy, pillow, onnxruntime, matplotlib, shapely, 340 native libraries between them — must stay clean. Either side alone is worthless. A check firing on the current venv gets disabled by the next person; a check firing on nothing is decoration. The positive case is a hand-written .dist-info built through Distribution.at, so the real RECORD parser runs, because av is the one package this tree must never install to test against.
Cost is 0.09 s to 0.24 s on the pre-commit hook, ~0.15 s of it walking 87 wheels' RECORD files. Every distribution in this environment has one; a distribution installed without a RECORD ships nothing as far as this check can see, which is stated where the reader will need it rather than left to be discovered.
supervision is refused on the strength of this (D16) and WP-067's example is drawn with matplotlib, already a dependency (R29). The finding is the more useful artifact than the example would have been.
WP-067 — the drawing library that could not be installed¶
The example was going to use supervision, which draws boxes, masks and rotated boxes and is Apache-2.0 at its own metadata. It is not installed here, and the reason is WP-109: it hard-requires av>=14.2, whose wheel ships libx264 — GPL-2.0 — while declaring BSD-3-Clause and shipping a licence document that mentions no GPL anywhere. Neither existing licence check saw it (D16). So the example is drawn with matplotlib, which the repository already carries for the training figures (R29), and the refusal is the more durable artifact of the two: the example is a hundred lines anyone could rewrite, and the check that refuses the dependency now runs on every commit.
A dev dependency must not become a runtime one by being convenient. matplotlib is in the dev group and nothing under src/lucid_yolo/ imports it at any level, so a wheel a consumer installs pulls no plotting stack. That is why this is scripts/draw_predictions.py beside plot_training.py rather than a lucid_yolo.draw module, and it is also why the deliverable is a script rather than the notebook the row originally named: a notebook is not reachable by make gate, and an example nothing runs is an example that rots.
The drawing functions take an already-computed prediction and an image array rather than a checkpoint. That is what makes the geometry assertable at all — D14 ships no trained weights, so there is no checkpoint to draw from in a test, and a drawing function that insisted on doing its own inference could only have been tested through one.
The oriented case is the one with a wrong answer that looks right. An A45 row's first four columns are [cx, cy, w, h], which read as an xyxy box without complaint: a [:, :4] slice draws a plausible upright rectangle that is not the object the model reported, at the wrong place and the wrong size, and nothing in the figure says so. The corners come from rboxes_to_polygons, the same function the oriented report and the tile writer state an object with, so a drawn ring and a written ring are the same four points. The test asserts both halves — that the four vertices match the rotated corners, and that the ring is not axis-aligned — because the envelope of a rotated box contains all four of its corners too and would satisfy the first assertion alone.
Two smaller decisions worth the sentence each. Colour is a pure function of the class index, not a palette walked in encounter order, so two figures of the same scene are comparable by eye and a class missing from one image does not shift the colours around it. And masks are drawn one overlay per instance rather than one composited layer, because the question a segmentation figure is read to answer is which instance covers a pixel, and a single layer can only answer that some instance does — boxes and masks are filtered by one row mask computed once, since two filters are how a mask ends up drawn on its neighbour with the counts still agreeing.
What the tests cannot see is whether the figure reads. Artist assertions pin geometry, colour and count; legibility — where the label sits, whether the mask alpha leaves the object visible — was checked by rendering three previews and looking at them. That is stated rather than hidden, because the gate does not cover it and the next person changing the layout should know which half of this is machine-checked.
WP-110 — a grep for a dead command cannot find its dead grammar¶
WP-096 deprecated lucid-download in 0.3.0 and wrote the removal date into three places: [project.scripts], AGENTS.md sec. 2, and the 0.3.0 changelog. 0.4.0 is that date. The alias existed so published reproduction instructions kept running, which is a real cost to removing it — and the way that cost is paid is not by keeping the alias forever but by making sure every string this repository prints names a command the reader can still run.
The sweep was driven by grepping the alias name, and that method has a hole the grep cannot see. A hint that names lucid-download is findable; a hint that uses the alias's flag grammar without naming it is not. Parse-testing every CLI fragment in the module's own docstrings found one: the checksum policy spelled --sha256 val2017.zip=<hex>, the bare argparse form, which the shipped jsonargparse command rejects outright — error: Parser key "sha256": Expected a <class 'collections.abc.Sequence'>. It needed '[val2017.zip=<hex>]'. Nothing that greps for a name would ever have surfaced it, and it would have shipped as documentation that fails when followed.
The same shape of mistake was in the test that guarded the repair hint: it asserted the hint string appeared on stderr. That assertion would have passed unchanged while the command it named was being deprecated out from under it — it tests presence where the thing worth testing is runnability. It now shell-splits the printed hint and re-parses it through the shipped parser, so a hint that stops parsing fails here rather than in an operator's terminal.
Nothing turned out to be shared. lucid-data download reaches download_dataset through jsonargparse's add_function_arguments, never add_arguments, never main, never an argparse.ArgumentParser — the deleted pair was referenced from exactly two places, each other and the test file. The near-trap was that add_arguments's own docstring claimed it registered "the lucid-data download flags", untrue since WP-096; anyone trusting the docstring over the call graph would have kept it as shared code.
Removing the alias also removed the one exemption to the project's underscore-flag rule, and AGENTS.md sec. 2 was stale on a second count anyway: lucid-predict shipped in WP-089 and the bullet still counted three commands. Both corrected in the same edit, since they are one sentence describing one surface.
Two references are left standing deliberately. The reproduction report's "commands as run" blocks keep their dashed spellings — they record what was executed at the version named, and editing them would falsify the record rather than update it — with the surrounding prose changed to say the alias was removed and that the block must be read through its replacements rather than run as written.