Skip to content

๐Ÿ—บ๏ธ Work-Package Roadmap

The agent's work queue: 176 numbered work packages, one commit each, executed in dependency order per the AGENTS.md loop, plus lettered rows recording follow-up work that landed after its package had closed. Transcribed from the governing blueprint (sec. 15) with a live status column.

Scope says what a package does, not what executing it taught. Measurements, rejected approaches and negative results live in RESEARCH_LOG.md, linked per row as ยท log; choices the papers left open live in ASSUMPTIONS.md and are cited by id; release results live in REPRODUCTION_REPORT.md. A row that restates any of those is duplicating a record that has an owner.

Length is guidance, not a limit, and the guide is this table's own median โ€” Scope about 285 characters, DoD about 80, subject about 50. Nothing enforces it and no test measures it: a row whose claims genuinely need more room takes it, and cutting substance to reach a number is the worse outcome. What the figure is for is the drift it makes visible. A cell running several times its column's median is usually not carrying more scope, it is carrying the reasoning behind the scope โ€” which has an owner elsewhere, per the paragraph above. Compress the prose first, move what survives compression to the linked log, and leave the row stating what the package does.

Legend: Dep = prerequisite WPs ยท [DATA] needs a real dataset (synthetic stand-in per A26 for offline development only) ยท [GPU] needs an accelerator (MPS first, D12c) ยท [PRINCIPAL] belongs to whoever drives the project rather than executes it (AGENTS.md sec. 1) โ€” never started autonomously. DoD = the test id(s) that must pass; every WP additionally requires make gate green. Status icons: โฌœ todo ยท ๐Ÿ”„ in-progress ยท โœ… done ยท โ›” blocked (see docs/ESCALATION.md) ยท โธ deferred, scope still wanted but the start is waiting on something outside this repository, named in the row ยท โŠ˜ closed without its DoD met, the row itself saying why. Only โœ… and โŠ˜ appear below: every WP written so far has either landed or been closed, so โฌœ, ๐Ÿ”„, โ›” and โธ are defined for rows this queue has not yet needed rather than describing anything on it. โธ is distinct from โ›” on both sides: nothing here is stuck, and nothing outside is being escalated to. The status column is flipped to โœ… in the same commit that completes its WP.

๐Ÿงฑ Phase 0 โ€” Foundation (WP-001โ€ฆ007)

WP Commit subject Scope DoD Dep Status
001 chore(repo): scaffold src layout, pyproject, Makefile Package skeleton, pinned deps, Makefile targets, pre-commit config ยท log make lint green on empty typed package โ€” โœ…
002 docs(legal): add LICENSE, NOTICE, README non-affiliation Apache-2.0, NOTICE with Redmon attribution (R20), README header verbatim scripts/_tests/test_audit_license_headers.py 001 โœ…
003 docs(policy): seed PROVENANCE, ASSUMPTIONS, DECISIONS, AGENTS, ROADMAP Source allowlist, register A1โ€“A26, D1โ€“D12 + ADR-001/002/003, AGENTS.md, this file scripts/_tests/test_audit_docs_present.py 001 โœ…
004 ci(pr): lint, types, tests, coverage, license audit PR workflow; dependency-license audit; commit-trailer validator ยท log Workflow green; negative test: AGPL dev-dep rejected 002,003 โœ…
005 ci(gates): golden harness and frozen-golden regression goldens/ loader, tolerance comparison, make gate, goldens/frozen/ semantics ยท log scripts/_tests/test_check_goldens.py; tampered golden fails 004 โœ…
006 ci(release): tag-gated release workflow and CHANGELOG release.yml; CHANGELOG scaffold; make freeze-goldens Negative test: tag on red commit refused 005 โœ…
007 test(fixtures): micro dataset with boxes, polygons, rotated scenes Seeded synthetic scenes via fuse-augmentations (A26): det/seg boxes+polygons, rotated scenes; loaders tests/fixtures/test_fixtures_load.py 003 โœ…

๐Ÿšฐ Phase 1 โ€” Data pipeline (WP-008โ€ฆ015)

WP Commit subject Scope DoD Dep Status
008 feat(data): target containers and type-generic transform API Targets dataclass (boxes, labels, masks, rboxes); transform protocol over every target type tests/data/test_targets.py 007 โœ…
009 feat(data): letterbox resize with exact inverse Aspect-preserving pad/resize + inverse map (A10) test_letterbox.py::test_roundtrip_subpixel 008 โœ…
010 feat(data): random affine for boxes and masks scale/translate/shear/degrees per Table S3; joint box+polygon transform, clipping test_affine.py::test_box_mask_consistency 009 โœ…
011 feat(data): mosaic assembly 4-image mosaic (R9), border handling, target remap test_mosaic.py::test_bounds_and_counts 010 โœ…
012 feat(data): mixup and copy-paste Table S3 probabilities, scale-aware policy test_mixup_copypaste.py 011 โœ…
013 feat(data): HSV jitter and horizontal flip hsv_h/s/v, fliplr=0.5 with target mirroring ยท log test_photometric.py 010 โœ…
014 feat(data): COCO dataset and LightningDataModule [DATA] Detection + polygon parsing, scale-aware augmentation policy, make check-data ยท log test_coco.py (fixture-backed) + check-data on real COCO 012,013 โœ…
015 test(data): round-trip goldens and debug visualizer Augmented-batch checksums; annotated grid dump script goldens/data_checksums.json frozen 014 โœ…

๐Ÿ—๏ธ Phase 2 โ€” Architecture (WP-016โ€ฆ023)

WP Commit subject Scope DoD Dep Status
016 feat(models): Conv, DWConv, Bottleneck primitives Conv-BN-SiLU, depthwise variant, residual bottleneck test_blocks.py::test_primitives 003 โœ…
017 feat(models): C3k2 block CSP split, n inner blocks, e ratio, c3k switch (A3) test_blocks.py::test_c3k2_shapes 016 โœ…
018 feat(models): PSABlock and C2PSA Attention + FFN block; split/concat wrapper (A3) test_blocks.py::test_c2psa 016 โœ…
019 feat(models): SPPF with shortcut 1x1 -> 3x MaxPool(5) -> concat -> 1x1, plus input-output shortcut (A4) test_blocks.py::test_sppf_shortcut 016 โœ…
020 feat(models): backbone Backbone stack with P3/P4/P5 taps test_backbone.py::test_tap_shapes 017,018,019 โœ…
021 feat(models): neck with attention tail Top-down/bottom-up; final C3k2 n=1 e=0.5 attn=True test_neck.py::test_output_shapes 020 โœ…
022 feat(models): dual detection head, reg_max=1 o2o (300x6) + o2m (nc+4, 8400) branches, DFL-free ltrb regression (A9) ยท log test_head.py::test_dual_head_shapes 021 โœ…
023 feat(models): scale registry, builder, param/FLOP fidelity gate 5-row dataclass registry, typed builders (ADR-001), fvcore counting ยท log test_param_flops.py::test_det_vs_table7 โ€” plus/minus 2% params / 5% FLOPs, all 5 scales; golden frozen 022 โœ…

โš–๏ธ Phase 3 โ€” Assignment and losses (WP-024โ€ฆ030)

WP Commit subject Scope DoD Dep Status
024 feat(losses): CIoU CIoU per R10 (A1), batched, autograd-safe test_ciou.py::test_against_closed_form 003 โœ…
025 feat(assign): anchor grid and Task-Aligned Assigner Centers at (i+0.5)*stride (A11); t = s^1 * u^6 (A2); topk selection test_tal.py::test_alignment_and_topk 024 โœ…
026 feat(assign): STAL surrogate candidate filtering Eq. 4โ€“6; per-dimension clamp d\<8 -> 16; original box preserved for scoring/regression test_stal.py::test_tiny_box_gains_candidates, ::test_per_dim_clamp, ::test_targets_unchanged 025 โœ…
027 feat(losses): detection branch loss CIoU + L1 (dfl-gain field, A13) + BCE, TAL-weighted test_detection_loss.py::test_components 026 โœ…
028 feat(losses): dual-branch composition o2m topk=10 / o2o topk=7->1 wiring; static alpha combination (schedule lands WP-035) test_dual_loss.py::test_one_positive_per_gt 027 โœ…
029 test(assign): synthetic assignment goldens 6x6 px GT: STAL >=1 candidate, vanilla TAL exactly 0 at stride 8 goldens/assignment_cases.json frozen 028 โœ…
030 test(train): single-batch overfit and gradient flow 200-step monotonic loss decrease; no NaN/Inf; all leaf grads populated ยท log test_overfit_batch.py 029 โœ…

๐Ÿ“‰ Phase 4 โ€” MuSGD (WP-031โ€ฆ033)

WP Commit subject Scope DoD Dep Status
031 feat(optim): Newton-Schulz orthogonalization Pure function from R7/R8, 5 iterations (A5), fp32 under AMP test_newton_schulz.py::test_orthogonality 003 โœ…
032 feat(optim): MuSGD with parameter-type split >=2D: w_muonMuon + w_sgdSGD (A6, A7); 1D: pure SGD, no weight decay (A12) test_musgd.py::test_param_split, ::test_step_shapes 031 โœ…
033 test(optim): toy convergence golden vs SGD Fixed synthetic regression + micro-CNN; MuSGD reaches threshold in fewer steps ยท log goldens/optim_toy.json frozen 032 โœ…

โšก Phase 5 โ€” Lightning training loop (WP-034โ€ฆ040)

WP Commit subject Scope DoD Dep Status
034 feat(ptl): LightningModule with task-conditional losses Automatic optimization; det losses active, seg/obb hooks inert test_module.py::test_training_step 030,032 โœ…
035 feat(ptl): ProgressiveLossSchedule hook Eq. 3 in on_train_epoch_start, (0.8,0.2)->(0.1,0.9) test_proglos.py::test_alpha_at_t0_mid_end 034 โœ…
036 feat(ptl): CloseMosaic callback Disables mosaic for final close_mosaic epochs test_close_mosaic.py::test_flip_epoch 034 โœ…
037 feat(ptl): EMA callback Decay schedule, checkpointed, used for eval test_ema.py::test_shadow_updates 034 โœ…
038 feat(ptl): LightningCLI entry and experiment configs configs/ tier matrix (ADR-001); resolved config logged per run ยท log test_cli.py::test_yaml_roundtrip; all configs dry-parse 035,036,037 โœ…
039 feat(ptl): deterministic checkpoint and resume Seeded resume reproduces the loss trajectory within tolerance ยท log test_resume.py::test_trajectory_match 038 โœ…
040 test(lit): overfit-100 integration golden [GPU] n-scale on a 100-image subset -> >=0.95 recall at IoU 0.5 on train ยท log goldens/overfit_micro_det.json frozen 039 โœ…

๐ŸŽฏ Phase 6 โ€” Evaluation, release 0.1.0 (WP-041โ€ฆ046)

WP Commit subject Scope DoD Dep Status
041 feat(decode): score-based top-k E2E decoding No IoU, no NMS, cap 300 (R3 sec. 4, A9) test_topk_e2e.py::test_no_nms_path 022 โœ…
042 feat(decode): NMS path for the dense branch Conf threshold + class-wise NMS (torchvision) test_nms_path.py 041 โœ…
043 feat(eval): pycocotools bbox evaluator, both paths One command evaluates E2E and non-E2E from one checkpoint test_coco_eval.py::test_dual_path_report 042 โœ…
044 test(eval): oracle round-trip Perfect predictions -> mAP 1.0; shuffled classes -> approx 0 test_coco_eval.py::TestOracleRoundTrip::test_oracle (node id predates this row's own class, drifted stale by an earlier WP; corrected by WP-128) 043 โœ…
068 feat(data): COCO 2017 downloader module and CLI Official-host download into check_data.py layout; val-only default; lucid-download + python -m lucid_yolo.data.download (added 2026-08-02, user request) tests/data/test_download.py offline suite 014 โœ…
069 refactor(eval): torchmetrics MAP with faster-coco-eval backend Replace pycocotools evaluator internals with torchmetrics.detection.MeanAveragePrecision(backend="faster_coco_eval"); DualPathEvaluator API preserved; pycocotools dep dropped (added 2026-08-02, user request) WP-044 oracle ladder green on new backend 043,044 โœ…
070 perf(data): fused affine+letterbox single-warp Compose the letterbox affine into the random affine so the train geometric base resamples once (FusedAffineLetterbox); boxes/polygons byte-identical, image pixels non-antialiased (A32); 4.4x geometric-path speedup (added 2026-08-02, user request) tests/data/test_fused_warp.py; data goldens recomputed 011,013,014 โœ…
071 perf(data): packed + uint8 batch transport Ragged list[Targets] flattened to eight dense tensors (PackedTargets) and images quantized uint8 across the DataLoader IPC hop (A33); on_after_batch_transfer restores float images and ragged targets on the destination device ยท log tests/data/test_coco.py pack/quantize round-trips 014 โœ…
072 feat(optim): A8 LR schedule โ€” warmup + linear decay Per-step LambdaLR: linear warmup over warmup_epochs then linear decay lr0 -> lr0*lrf (A8, revised here); the overfit-100 recipe pins the schedule off so its golden stays frozen tests/optim/test_schedule.py; overfit golden unchanged 034,038 โœ…
073 perf(data): compact annotation store + val-loader worker cap CocoDetectionDataset precomputes per-image Targets and drops the raw JSON annotation dicts, whose refcount traffic materializes copy-on-write pages in every persistent worker until the host OOMs; the val loader gains its own worker cap (min(num_workers, 4)) ยท log tests/data/test_coco.py val-cap tests; dataset output unchanged (data goldens untouched) 014 โœ…
076 perf(data): epoch-recycled loader workers persistent_workers defaults off (opt-in constructor/CLI knob): recycling the pool every epoch hard-resets the per-worker memory creep a compact store does not reach ยท log tests/data/test_coco.py persistent-workers default/opt-in tests 014,073 โœ…
077 feat(ptl): epoch val mAP in the progress bar validation_step decodes the one-to-one branch from the loss forward and accumulates torchmetrics MeanAveragePrecision; on_validation_epoch_end logs val/mAP. A letterbox-coordinate proxy for run monitoring โ€” the acceptance figure stays the standalone evaluation. Metric states non-persistent, so checkpoints are unaffected test_module.py fast-dev-run logs val/mAP; state-dict-unchanged test 041,043 โœ…
078 fix(losses): stride-normalized L1 term The L1 box term is measured in stride units โ€” the head's native ltrb frame, the frame the legacy DFL-field gain is calibrated in โ€” instead of pixels (A13, revised here; the Det-smoke attempt-1/2 root cause); strides threaded module -> DualBranchLoss -> DetectionBranchLoss test_detection_loss.py stride-normalization test; overfit-100 golden revalidated 027,028,034 โœ…
079 fix(data): per-worker, per-epoch augmentation RNG worker_init_fn re-seeds each worker's pipeline generator from WorkerInfo.seed. The generator was seeded in the parent, which never advances when workers are used, so every worker replayed one augmentation stream โ€” and, workers being non-persistent since 076, restarted it every epoch. Determinism per seed preserved ยท research log ยท engineering log tests/data/test_coco.py per-worker re-seed + thread-cap tests 014,076 โœ…
080 feat(docs): training-curve vector figures scripts/plot_training.py renders a run's metrics.csv as a deterministic SVG: three panels plus a fourth for the segmentation terms, fixed hashsalt and suppressed date metadata, panels degrading gracefully on runs predating a column ยท log figure regenerates from lightning_logs/version_8; epoch_means and _figure_size doctests; scripts/_tests/test_audit_figure_captions.py 045 โœ…
081 docs: Det-smoke report section and detector model card REPRODUCTION_REPORT.md opens as the living D10 document with the 0.1.0 detection section (mechanism-to-module map, acceptance table, both root-cause narratives, assumption outcomes, four recorded deviations); the detector model card covers intended and non-intended use, COCO composition limits and the clean-room statement both docs pinned in test_docs_present.py 045,080 โœ…
082 test(losses): objective invariant gate Pins the coordinate frame the L1 term is measured in and its scale on a near-converged state, plus a loader-level test that spawns real workers and asserts two epochs do not replay one augmentation stream. Deliberately omits an absolute share bound ยท log tests/losses/test_loss_invariants.py; test_coco.py worker-epoch divergence 078,079 โœ…
045 exp(det): Det-smoke tier and report section [GPU][PRINCIPAL] n-scale ~50 epochs against the Det-smoke criteria. ACCEPTED 2026-08-06 (run v8, batch 128 x 50 ep, lr 0.02): val mAP50-95 25.30 EMA-NMS vs >25, E2E deficit 1.46 vs ~1.5, stable training, gates green. Attempts 1-2 not accepted at 3.96 / 6.28 โ€” root cause WP-078, compounded by WP-079; the full record is REPRODUCTION_REPORT.md and .experiments/det_smoke/ Det-smoke acceptance met; artifacts archived with seeds/configs 044,040,072,078,079 โœ…
083 test(eval): synthetic-shapes generalization golden 2000 generated scenes split 1800/200, n-scale, 6 epochs at 320 px, held-out val through both decode paths โ€” a generalization gate rather than the overfit gate's loop-composition one, with mosaic on for five of six epochs so a collapsed augmentation RNG is detectable. The shared annotation reader moves into lucid_yolo.eval.annotations rather than being copied ยท research log ยท engineering log goldens/gpu/shapes_regression_det.json via check_goldens.py --include-gpu; tests/eval/test_annotations.py 082 โœ…
084 ci(repo): commit-time gates, split workflows, dev group Three checks move to where they can catch something: the copyleft audit stays an always-run job, the commit-trailer validator becomes a commit-msg hook, and the dev extra becomes a PEP 735 dependency group. ci.yml splits into lint.yml and ci-tests.yml ยท log Provides-Extra: None on the installed dist; the commit-msg hook rejects a malformed message and passes a valid one; scripts/_tests/test_audit_licenses.py 004 โœ…
085 test(repo): doctests join the offline gate make test collects src and tests with --doctest-modules, so the Examples sections the contract requires are executed for the first time โ€” four had rotted and one had never passed at any seed ยท log make test fails on a deliberately broken Example โ€” verified 004 โœ…
086 docs(policy): releases ship no trained weights WP-046's definition of done required published weights; the project does not release them, since the reproduction's claim rests on frozen goldens and fidelity gates rather than on a binary anyone can retrain from the recipe. Recorded as D14 and the 046 row amended; no tooling changed, release.yml having always uploaded only the sdist and wheel test_docs_present.py; release.yml unchanged 004 โœ…
046 release: v0.1.0 detector [PRINCIPAL] O3 cleared; CHANGELOG; goldens frozen to 0.1; no trained weights (D14). Superseded 2026-08-10 by 054: no v0.1.0 tag was ever cut, so there is no 0.1.0 to document or freeze against โ€” the detector's history ships inside the single ## [0.2.0] section and the goldens froze to 0.2 release.yml green on tag v0.1.0 045 โœ…

๐Ÿ–Œ๏ธ Phase 7 โ€” Instance segmentation, release 0.2.0 (WP-047โ€ฆ054)

WP Commit subject Scope DoD Dep Status
047 feat(models): mask coefficient branch K=32 tanh coefficients per location (A14, A16, A34), opt-in behind num_coeffs: None builds no stem, so the accepted detector's module tree, its 205,600 parameters and its forward output are bit-identical. Landed ahead of its listed dep 046, which is a release-ordering gate rather than a technical prerequisite test_segment_head.py::test_coeff_shapes; off-by-default state-dict and parameter identity; params_flops_det.json unchanged 046 โœ…
048 feat(models): multi-scale proto pathway Eq. 8 F_proto = X1 + sum U(phi_l(X_l)) as a standalone ProtoFusion, wired into nothing yet so the accepted detector cannot move. P3 is added unprojected; P4/P5 take bare 1x1 projections into P3's width and interpolate to its exact size rather than a fixed scale factor, so odd and non-square maps stay aligned (A35) test_proto.py::test_fusion_eq8 (closed-form, mutation-checked); identity when projections vanish; odd/non-square target size 047 โœ…
049 feat(models): prototype generation stack Eq. 9: ProtoNet maps the fused feature to K raw prototype maps at twice P3 (A15, A18). Output is deliberately unactivated โ€” coefficients already carry tanh (A16), so squashing prototypes too would compress the linear mask combination twice test_proto.py::test_proto_resolution (odd non-square included); unactivated output; upsample-before-final-conv proven structurally and by 2x2-block refinement 048 โœ…
050 feat(models): auxiliary semantic branch (training-only) SemanticAux: dense per-class raw logits on F_proto at its own resolution, forward returning None whenever self.training is False โ€” keyed on module mode, so the branch is provably absent at eval and export (A17). Carries the A30 prior-probability bias; CLS_PRIOR_PROB and init_cls_prior_bias give that formula one home test_aux_semantic.py::test_eval_mode_inactive (under both grad-enabled eval and no_grad); single-conv parameter count; prior-init and raw-logit gates; params_flops_det unchanged 049 โœ…
051 feat(losses): instance mask loss and BCE+Dice auxiliary instance_mask_loss โ€” per-pixel BCE cropped to the GT box and box-area normalized, reusing the A11 pixel-centre convention rather than a second one โ€” and semantic_aux_loss, BCE plus soft Dice at unit coefficients. Eq. 7 assemble_masks lands in proto.py beside the prototype producer, since the WP-053 decode assembles from the same expression (A36) test_mask_loss.py (9 gates), test_semantic_aux.py (6 gates), test_proto.py::TestAssembleMasks::test_* (node ids moved under this class by WP-128); all mutation-checked 050 โœ…
052 feat(models): Segmenter wiring; aux branch absent from deploy() Segmenter composes the coefficient-enabled dual head, ProtoFusion, ProtoNet and SemanticAux on the shared backbone and neck; deploy() holds neither the o2m branch nor the aux head โ€” absent, not disabled. The fused feature is computed once and shared, since recomputing it would silently double that subgraph in any FLOP tally ยท log test_segmenter.py (9 gates, all mutation-checked); params_flops_det unchanged 051 โœ…
052b test(models): Table S9 seg fidelity gate All five scales against R1 Table S9 at ยฑ3% params โ€” wider than detection's ยฑ2%, the segmentation head's sizing resting on five registered assumptions (A14, A15, A18, A34, A35) rather than published structure โ€” and ยฑ5% FLOPs under A29's convention ยท log test_param_flops.py::test_seg_vs_tableS9, all 5 scales; goldens/params_flops_seg.json โ€” golden frozen 052 โœ…
053 feat(eval): segmentation mask decode Masks assembled post-top-k, then sigmoid, bilinear upsample, box crop, threshold โ€” in that order, since binarizing before upsampling aliases the boundary (A37). o2o_topk becomes a wrapper over o2o_topk_with_indices so coefficients are gathered by the anchors the box path ranked rather than re-ranked, and masks_to_original inverts the letterbox through the same geometry the boxes use test_segment_decode.py (11 gates, mutation-checked); box/mask frame agreement within 1 px 052 โœ…
053b feat(eval): segm evaluation, both paths Ground-truth masks decoded from COCO polygons and RLE onto one grid, opt-in so detection-only callers pay nothing; torchmetrics scores bbox and segm in a single pass. Both decoders gain decode_with_indices, so each path's coefficients are gathered by the anchors it actually kept โ€” indices in the wrong base are in-range, correctly shaped and point at the wrong objects test_seg_eval.py::test_bbox_and_segm (perturb only the masks: segm moves, bbox does not); mask/box pairing across distinct labels; per-path index correspondence 053 โœ…
087 feat(ptl): segmentation training path Wires the Phase 7 components into training, which task="segment" had never done. Composition moves behind shared build.py factories, not a Detector object; DualBranchLoss returns its assignments so mask supervision uses the positives the box loss chose; both branches' coefficients supervised, decode reading the o2o stem (A38) ยท log overfit-100 mask IoU 0.8146 (>= 0.7), golden frozen; test_seg_training.py incl. the gt_index-pairing gate; detection loss bit-exact at zero gains; Det-smoke checkpoint loads strict 053 โœ…
087b perf(ptl): batched mask-loss gather, one sync not 64 _branch_mask_loss pads to the batch's largest positive count and selects once, replacing a data-dependent boolean index run per image per branch that forced a device-to-host sync each time; positives keep their order, so the summation order and the value are the loop's own ยท log test_seg_training.py::test_batched_mask_loss_equals_the_per_image_loop โ€” torch.equal against the per-image loop over uneven, zero and equal positive counts; the loop stays in the test file as the oracle the frozen goldens were measured against 087 โœ…
087c perf(data): vectorised, batched polygon rasterisation Polygon rasterisation vectorised in three steps, each superseding the last: all of a ring's edges at once, then a whole image's rings in one crossing test, then edges flattened with an instance index instead of padded to the image's longest ring. Output is identical at every step, not merely close ยท log test_rasterize.py::test_vectorised_crossing_test_matches_the_per_vertex_loop (irrational radius, so nothing lands on a pixel centre), ::test_batched_rings_match_one_call_per_ring, ::test_chunking_the_edge_axis_does_not_change_the_masks; data checksum goldens unchanged 087 โœ…
087d perf(data): rasterise mask targets in the loader workers Rasterisation moves into collate_detection, rasterising batches in DataLoader workers ahead of the step: the grid derives from the input canvas (A15), masks travel as a bool stack split by boxes_per_image, and the step rejects a stack rasterised for another grid. Each ring is tested only inside its own bounding window, replacing 087c's flattened-edge batching ยท log test_seg_training.py::test_loader_rasterises_the_masks_the_step_would_have, ::test_loader_rasterised_masks_give_the_identical_loss, ::test_masks_rasterised_for_another_grid_are_rejected; test_cli.py::test_mask_targets_follows_the_model_task; test_rasterize.py::test_windowed_rasterisation_matches_the_full_grid_rule; seg overfit and data checksum goldens unchanged 087c โœ…
087e refine(ptl): epoch mask mAP during validation Validation decodes the kept detections' masks through the deployed decode โ€” not a second implementation of it โ€” and logs val/segm_mAP, so a segmentation run finally moves an epoch metric about its masks. Scored on the prototype grid and capped at COCO's own 100 detections, through a second MeanAveragePrecision rather than one metric holding both IoU types ยท log test_seg_training.py::test_validation_logs_a_mask_map_beside_the_box_map, ::test_detection_validation_logs_no_mask_metric, ::test_mask_map_is_skipped_when_the_batch_carries_no_ground_truth_masks, ::test_scored_masks_are_paired_with_the_boxes_own_anchors 087 โœ…
087f refine(configs): name tiers by what they are, not by letters The <task>_tier_<letter>_<letter> configs are renamed for what they are โ€” det_smoke, det_ablations, seg_smoke, planned obb_smoke โ€” and the tier names follow everywhere they are written, historical mentions included, with git history keeping the old ones. No behaviour changes ยท log test_cli.py::test_configs_dir_is_non_empty, ::test_config_dry_parses over every shipped config, ::test_smoke_tier_resolves_variant_n_multipliers_and_gains and the ablations counterpart; goldens unchanged 087 โœ…
054 release: v0.2.0 segmentation [GPU][PRINCIPAL] Seg-smoke tier plus overfit-100. ACCEPTED 2026-08-10 (run v9, batch 128 x 50 ep, lr 0.02, bf16-mixed): box mAP50-95 26.12 EMA-NMS vs >25; segm/box ratio 0.728 NMS vs >=0.65, ratified as the standing Seg-tier criterion; mask IoU 0.8146 vs >=0.7; val/loss strictly decreasing over all 49 logged epochs. Froze goldens/frozen/0.2 ยท log Seg-smoke acceptance; release.yml green on v0.2.0 053,087 โœ…

๐Ÿ”„ Phase 8 โ€” Oriented detection, release 0.3.0 (WP-055โ€ฆ064)

WP Commit subject Scope DoD Dep Status
055 feat(data): rotated geometry primitives rotated_geom.py: long-edge canonicalization (w >= h, theta in [-45,135) deg), point-in-rotated-rect, quad-box conversion both ways (A23). Three conventions chosen here and inherited downstream: theta turns from +x towards +y, containment is edge-inclusive, an exact square folds towards zero ยท log test_rotated_geom.py::test_theta_pi_equivalence, ::test_canonicalization; ring-order invariance over all eight orderings of a quad; round-trip; a rotated box excludes a point its envelope includes; both range bounds walked two ulps either way 054 โœ…
056 feat(data): DOTA parsing and long-edge conversion [DATA] dota.py: R18's 15 categories in published order, headers skipped, quad to canonical long-edge box via WP-055. Establishes the instance-axis invariant: rboxes[i] is the instance of boxes[i]/labels[i], boxes the unclipped quad envelope. Malformed lines and unknown categories raise with file and line. New A39 defers difficult behind a required keyword ยท log test_dota_parse.py (31 tests, all against synthetic fixtures); check-data counts verified against a fixture root only โ€” the real 2,806 / 188,282 / 15 clause is unverified, DOTA is not on this machine 055 โœ…
057 feat(data): 1024 px overlapping crop tiling [DATA] tiling.py: integer window placement with the trailing window flush against the far edge, so coverage holds for any overlap and image; plus R18's partial-object rule โ€” clip, U = clipped area / original area, drop U == 0, flag U < 0.7 difficult, re-fit. U surfaced as visible_fraction. A21 revised: the 200 px overlap is attested by neither cited source ยท log test_tiling.py::test_coverage_no_gaps (8 sizes incl. 4000x3000 at the A21 defaults); shapely clipped-area oracle; straddle fractions summing to 1; threshold pinned either side of 0.7. Real DOTA never tiled โ€” not on this machine 056 โœ…
058 feat(data): rotated-aware augmentation rotated_aug.py holds the shared rotated arithmetic; affine.py/mosaic.py/mixup.py lose the NotImplementedError they raised on non-empty rboxes: a rotated box warps as four corners and is re-fitted. CopyPaste keeps its guard โ€” its paste unit is a rasterized polygon and the oriented path carries none. New A40 records the three policy choices ยท log test_rotated_aug.py::test_roundtrip plus 59 more; canonical output over the whole angle sweep through every transform; mirrored corners compared as geometry rather than as angles; similarity exact against the derived shear bound; every 0.2 golden and both checksums unmoved 057 โœ…
059 feat(losses): ProbIoU rotated loss Gaussian-box ProbIoU (R17, A19): both losses R17 proposes exposed side by side, since it suggests starting on one and switching and WP-088 owns that choice. R17's formulas are evaluated through cancellation-free identities, which is what makes float32 sufficient where float64 is unavailable (D12c). New A41 records the degenerate-box side floor ยท log test_probiou.py::test_matches_numerical_integration_of_the_definition (2e-14 relative), ::test_matches_literal_transcription_of_r17 (float64, 1e-12), ::test_a_box_against_itself_scores_exactly_one, ::test_the_two_losses_are_one_quantity, ::test_float32_accuracy_is_aspect_ratio_independent, ::test_degenerate_boxes_stay_finite_forwards_and_backwards; 75 tests 055 โœ…
060 feat(losses): square-object angle loss angle_loss.py implements Eq. 14-15 as the auxiliary term for square and near-square boxes, whose orientation the rotated IoU loss cannot resolve. Three factors โ€” TAL's q_i, log-Gaussian omega at lambda=3, double-angle penalty โ€” normalized by S = max(sum q_i, 1), pre-gain since A22's weight is a registered gap. New A42 records the degenerate-target exposure ยท log test_angle_loss.py::test_wrap_range, ::test_omega_profile, ::test_sin2_extrema, ::test_boundary_continuity โ€” 58 tests; continuity asserted as a Lipschitz bound across the wrap, not mere finiteness 059 โœ…
061 feat(assign): rotated containment for TAL and STAL Oriented GTs reach the assigner through one optional gt_rboxes argument into _candidate_mask, the only stage that changes: candidacy becomes the point-in-rotated-rect test while the IoU, the alignment metric, targets and weights keep running on axis-aligned boxes (A25, revised here). The STAL surrogate gains a rotated form that re-canonicalizes (new A43) ยท log test_rotated_assign.py::test_tiny_rotated_gt โ€” a 6x6 GT at 45 deg, invisible to vanilla TAL, gains 4 candidates; ::test_rotation_excludes_envelope_corners; ::test_axis_aligned_assignment_is_bit_identical over three assigners 058,060 โœ…
062 feat(models): OBB head, direct angle, NMS-free decode A third per-level stem on each head branch emits one raw angle per anchor โ€” theta = z, Eq. 13, Eq. 12's squashing gone, no activation. The oriented decode composes it with the unchanged decode_ltrb, canonicalizing the dense output (new A44; A23 now active); o2o_rotated_topk gathers theta by the selector's own indices, not a second ranking (new A45) ยท log test_obb_head.py (18 tests: term-by-term stem arithmetic, an unsquashed stem at 100 radians, half-turn continuity, disjoint branch stems, end-to-end through the deployed view); test_param_flops.py::test_obb_vs_tableS11 at 1024 px over 15 classes โ€” params ยฑ3.5% (binding scale s at +3.27%), FLOPs ยฑ5% (binding scale n at +4.84%) โ€” golden frozen 061 โœ…
063 feat(eval): rotated mAP on DOTA val Exact polygon-intersection rotated IoU โ€” Sutherland-Hodgman clipping + shoelace, each pair re-centred on its midpoint before corner expansion (A24, now active), plus the accumulator over it. Four constants R1 never states are registered, not inherited from library defaults: A46, A47, zero-non-difficult-GT classes excluded, A48. Tile merge deferred to WP-107 ยท log test_dota_eval.py::TestRotatedIou::test_vs_oracle (600 pairs across the angle range, half near-coincident) plus 15 named edge cases; TestRecallGridBoundary::test_boundary_recall_is_sampled_exactly, reference-independent and hand-derived; TestAccumulator::test_matches_the_axis_aligned_instrument_at_zero_angle 062 โœ…
088 feat(ptl): oriented-detection training path The Phase 8 counterpart of WP-087: oriented losses reach _task_extra_loss, rotated targets thread through the loaders, obb_smoke.yaml added, and A49/A50 are measured rather than argued. Prerequisite for WP-064's OBB-smoke run ยท log test_obb_training.py; overfit-100 rotated mAP50 >= 0.9 โ€” landed 2026-08-12 with the criterion unmet and the row left open, then closed 2026-08-12 at WP-093's revised gain, scoring 0.9390 at the recipe's own seed with goldens/gpu/overfit_micro_obb.json frozen there 063 โœ…
094 feat(data): DOTA tiles into a trainable layout scripts/build_dota_tiles.py writes the COCO layout the oriented reader reads, carrying R18's class order into the label space โ€” the disk-writing caller WP-057 left unwritten, without which obb_smoke.yaml pointed at a directory no code produced. Two encodings registered rather than invented quietly (A52, A53); the reader forwards difficult onto the A51 channel ยท log tests/data/test_build_dota_tiles.py โ€” a round trip through CocoDetectionDataset rather than a JSON-shape check; tests/data/test_coco.py::test_the_difficult_key_reaches_the_targets_channel on both readings 057,088 โœ…
095 feat(eval): per-tile oriented acceptance instrument scripts/eval_obb.py scores a finished checkpoint through the WP-063 accumulator on an explicit split, so A46, A47 and A48 all apply. Scoring runs in letterbox coordinates with the ground truth letterboxed alongside โ€” an equality rather than a proxy โ€” the number explicitly per tile in its own output. Checkpoint loading moves into lucid_yolo.eval.checkpoint ยท log tests/eval/test_eval_obb.py โ€” end to end over a built layout, plus the frame-and-label-space gate that feeds the split's own ground truth back as perfect detections and requires 1.0 094 โœ…
096 refine(cli): one data CLI, one eval CLI Three commands where there was one console script, one make target, two unshipped files: lucid-yolo unchanged, lucid-data over download, check, build-tiles, and lucid-eval, which takes no task argument and reads its protocol from the checkpoint. Parsing moves to jsonargparse; flags become underscored, lucid-download survives as a deprecated alias ยท log tests/cli/test_cli_surface.py โ€” every declared console-script target resolved the way the wrapper resolves it, both dispatch branches with the protocol functions stubbed, and the deprecated alias still running on its old flags 094,095 โœ…
097 fix(data): report DOTA totals, require only stated ones check_dota_root compared summed train+val counts against R18's 2,806 / 188,282 / 15, which describe DOTA-v1.0 whole, so the first command an operator runs failed on every correct download. The three expectations default to None, reported as a NOTE and compared only when the caller states one; a DOTA expectation passed with --dataset coco now raises ยท research log ยท engineering log tests/data/test_dota_parse.py โ€” an unstated total is a note and a well-formed root passes on its layout alone; a stated one still fails and names both counts, through the library and through lucid-data check; a DOTA expectation aimed at COCO raises 056,096 โœ…
098 refine(data): resolve a split's paths by convention lucid_yolo.data.layout.CANDIDATES states the split-path convention in one place and resolve_split returns the first whose directory and annotation file both exist, so --data.data_root is now sufficient for every tier. The datamodule had defaulted to COCO 2017's own spelling, which put this project's own tiled layout in the position of restating all four paths tests/data/test_layout.py โ€” every named layout resolves to itself, a half-present layout does not match, precedence is the table's order, and both consumers reach a tiled root from the root alone 094,095 โœ…
100 perf(data): parallel tiling with a progress bar tile_source_image becomes the unit of work, dispatched through a ProcessPoolExecutor with --workers and each worker pinned to a single torch thread. Ids are assigned in the parent in source order and never in the workers, so the output JSON does not depend on which process finished first; a tqdm.auto bar reports per source image ยท log tests/data/test_build_dota_tiles.py โ€” a four-image pooled build writes byte-identical JSON to the serial one, every tile image reaches disk, no placeholder id survives into the file, and the worker count is forwarded rather than accepted and dropped 094 โœ…
101 feat(data): JPEG tiles and a completion-order bar --suffix .jpg with --quality writes tiles through the JPEG encoder, PNG staying the default because a tile is an exact crop of a lossless source; annotations are asserted byte-identical across the two encoders. Separately the progress bar counts completions rather than positions โ€” ordering is a requirement of the numbering, not of the counting ยท log tests/data/test_build_dota_tiles.py โ€” JPEG tiles are written and read back through the dataset, the annotations match the PNG build's exactly, and an unknown suffix fails at the flag rather than after the first image 100 โœ…
102 perf(ptl): validation that is not CPU-bound Two validation defaults nobody had revisited at 1024 px: the val worker cap now applies only to a count this class chose itself and scales with the pixel count, and an oriented run logs val/rotated_mAP and no val/mAP, the axis-aligned figure reading the A44 composition's pre-rotation rectangle and so scoring correct and random orientations identically ยท log tests/data/test_coco.py โ€” a named worker count reaches the val loader, an auto-chosen one is still capped, and the cap scales with the letterbox side; tests/ptl/test_obb_training.py โ€” an oriented epoch logs the rotated metric and not the box one 088 โœ…
103 fix(ptl): a val worker count that fits the host WP-102 deleted the memory guard where it should have deleted the silence. A count inherited from a named train count now passes the _shm_capped_workers budget and warns with the number and the override flag; a count stated as --data.val_num_workers is bounded by nothing ยท log tests/data/test_coco.py โ€” an inherited count is bounded and warns, a named val_num_workers is not, and a fitting inherited count still reaches the loader whole 102 โœ…
104 perf(eval): match every IoU threshold in one pass The ten IoU thresholds share the overlap matrix and the score order and differ only in what counts as a hit, so the availability state becomes (T, G) and the walk happens once instead of ten times; detections reaching no ground truth at the lowest threshold skip it entirely ยท log tests/eval/test_dota_eval.py โ€” a ground truth claimed at a loose threshold is still free at a strict one, and a detection reaching nothing is a false positive that consumes nothing; the existing exact-value, difficult-flag and COCOeval cross-check tests pin the output unchanged 103 โœ…
105 fix(eval): a report that survives, and a pass you can watch Two defects the first oriented tier evaluation exposed, both in lucid-eval rather than in what it measures: --output now creates its parent, having raised after the scoring pass printed its numbers, and a pass of known length draws a tqdm.auto bar wrapped at the CLI rather than inside DualPathEvaluator ยท log tests/eval/test_eval_obb.py โ€” the report is written into a directory that does not exist yet 104 โœ…
106 docs(obb): the oriented tier's record The 0.3.0 artifacts, matching what 0.1.0 and 0.2.0 carry: an oriented model card, the four-panel obb_smoke_training.svg, the reproduction-report section, standalone per-tile evaluations for EMA and raw weights. plot_training.py gains the oriented panels โ€” an obb run logs no val/mAP (WP-102), so panel 1 falls back to the rotated pair. The three cards consolidate into docs/model_cards/ scripts/_tests/test_audit_docs_present.py โ€” every card in docs/model_cards/ is a required doc, and every required card exists 105 โœ…
064 release: v0.3.0 oriented detection [GPU][PRINCIPAL] OBB-smoke tier accepted 2026-08-14: run v10 at per-tile rotated mAP50 0.5242 and mAP50-95 0.2914 on DOTA-v1.0 val (EMA, 10,132 tiles), overfit-100 wiring gate at 0.9390 against a 0.9 floor, O4 resolved, 0.2 goldens green and 0.3 frozen beside them. The whole-image tile-merge policy this row also owned is not delivered here and moves to WP-107 ยท log OBB-smoke acceptance; make gate green; changelog section and frozen goldens for 0.3 063 โœ…

๐Ÿ”ฎ Phase 9 โ€” Inference and generalization (WP-089โ€ฆ092, 074โ€ฆ075, 099, 107)

Deliberately after all three task phases rather than inside each. A predict path is where preprocessing and coordinate-frame mistakes surface, so building all three against one settled decode surface catches disagreements between them that a per-phase predict cannot see. It also keeps the release train clean: 0.1.0 already shipped, and retrofitting a detector-only predict into it now would mean carrying segmentation code in a detection release.

WP Commit subject Scope DoD Dep Status
089 feat(predict): detection inference on one image Checkpoint and image to detections in original coordinates, as a library call and a fourth lucid-predict command. Reuses Letterbox and to_letterboxed_original rather than re-deriving the inverse, plus the evaluator's read-scale-letterbox chain as read_letterboxed_image. Both decode paths selectable; a non-detect task refused in the library by name ยท log tests/predict/test_predict.py โ€” a detection planted on a 64x128 image's canvas lands where the letterbox geometry says it must, on both decode paths, so a pad-blind or axis-swapped inverse cannot pass by symmetry; a segment and an obb checkpoint are each refused naming the task; --output creates its parent (WP-105) 046 โœ…
090 feat(predict): segmentation inference Extends predict with masks in original coordinates, reusing decode_instance_masks and masks_to_original. A second entry point predict_segmentation returns a row-aligned SegmentedPrediction, not a union return, so the detect path's (N, 6) contract is unbroken. Masks assembled while the boxes are still on canvas (the A11 crop frame); reports carry COCO RLE ยท log tests/predict/test_predict_seg.py::test_mask_matches_box โ€” the planted mask's extent equals the planted rectangle clipped by the returned box, so a mask that merely filled its box fails and so does one the A11 crop never touched; both decode paths, each reading its own branch's coefficients; an obb checkpoint still refused by name 089, 054 โœ…
090b fix(eval): masks for an empty instance axis decode_instance_masks raised from F.interpolate, which treats the instance axis as channels and rejects a zero-length one, for as long as it has existed โ€” unreachable from the evaluator, reachable from predict. Returns the empty stack instead, keeping the shape contract a caller stacking per-image results needs; WP-090's call-site guard becomes an optimisation ยท log tests/eval/test_segment_decode.py::test_no_kept_detections_returns_the_empty_stack โ€” (B, 0, H, W), still boolean 090 โœ…
092 fix(eval): exact recall sampling for the detection instrument Gives evaluate_bbox the exact recall sampling WP-063 built for the oriented instrument (A46): torchmetrics builds its 101 thresholds with a float32 linspace that overshoots k/100 at 36 of them, so a class whose attained recall lands on one forfeits 1/101 of its AP โ€” small, one-directional and always downward, across every COCO number reported through this path test_coco_eval.py โ€” a constructed class whose recall lands on a float32-forfeited boundary, asserted against a hand-derived (k+1)/101; the pin on the dependency's own defect; the guard that fires if the metric stops honouring rec_thresholds 063 โœ…
091 feat(predict): oriented-detection inference Extends predict with rotated boxes as the A45 (N, 7) tuple the oriented evaluator emits and consumes. Canonicalization is not added here โ€” decode_rboxes ends in canonicalize (A23) and a letterbox inverse preserves w >= h and theta. Only the e2e path existed at this row: --decoder nms was refused, not silently substituted; 091b lifted the refusal ยท log tests/predict/test_predict_obb.py โ€” four plants describing one rectangle (already-canonical, a half turn above the range, below the -pi/4 floor, and short-edge-first) all return the same canonical box after the inverse, plus a 500-radian plant asserted on range and long-edge order rather than on a value that would restate canonicalize; an obb head with no angle stems raises rather than returning axis-aligned boxes in an oriented tuple's shape 089, 064 โœ…
091b feat(decode): rotated suppression decoder RotatedNMSDecoder over the one-to-many branch, lifting WP-091's refusal: greedy class-wise suppression by rotated_iou (A24) over a canonicalized decode (A23). Emits the same A45 (B, 300, 7) tuple as o2o_rotated_topk. Framed as the o2m path's baseline, not a repair to the e2e path. Threshold A61; the axis-aligned value it inherits, A62 ยท research log ยท engineering log tests/decode/test_rotated_nms.py โ€” two 20x2 bars crossing at one centre have envelope IoU 1.000 against rotated 0.0535, so both survive here while NMSDecoder on the identical plant keeps one; the threshold read from the registered constant so register and code cannot drift, and a pair at 0.667 asserted on both sides of it so the parameter is consulted rather than fixed at an extreme 091 โœ…
093 fix(losses): stable angle regression for elongated boxes A22's angle gain revised 1.0 to 0.25 on a five-seed dose-response, taking the oriented overfit gate from 1 of 5 seeds clearing the 0.9 floor to 4 of 5, the run-to-run spread from 0.667 to 0.096, and the mean elongated angular error from 25.47 to 11.85 degrees ยท log Mean absolute elongated-target angular residual below 15 degrees, and overfit-100 rotated mAP50 at or above 0.9. Partially met, and the shortfall is recorded rather than dissolved: 4 of 5 seeds, not 5 of 5 โ€” seed 1 lands at 0.8638 with 19.84 degrees. Only dropping R1's angle term entirely reached 5 of 5, and that is a paper deviation the register does not authorise 088 โœ…
107 feat(eval): whole-image merge for overlapping tiles Tile detections mapped back to source coordinates through the A53 window provenance, seam duplicates resolved by core ownership (A59) โ€” ownership, not suppression: nothing is ranked by confidence. The oriented evaluator reports both figures, per-tile and whole-image, cap lifted for the merged unit (A60). The step WP-064 owned and 0.3.0 shipped without ยท log tests/eval/test_tile_merge.py โ€” an object straddling a seam yields one detection, not two, and the duplicate it removes is worth a sixth of the score; a whole-image score over a single-tile image equals the per-tile score with no tolerance, on four metric keys each asserted strictly inside (0, 1) so the equality is not between saturated values; the one-shot-per-object cost pinned so it cannot be quietly fixed into NMS; the flattering residual measured at 0.165 of map_50 rather than argued away 064 โœ…
108 docs(roadmap): the Scope column says what a package does Thirty-two Scope cells carried the reasoning behind their package rather than its scope, the worst at 1709 characters against a 286 median. Compressed toward that median, with what survives compression moved to the linked log โ€” 12 rows had no log section at all, which is why they grew. The median is written into the header as guidance and nothing gates it ยท log scripts/_tests/test_audit_docs_present.py โ€” the six-column and status gates still pass on every rewritten row, and the header's package count matches the table 107 โœ…
074 feat(data): RF100-VL incremental downloader and merged COCO layout [DATA] Extend lucid-data download with an rf100-vl mode over the rf100vl package (Roboflow API key; 100 COCO-JSON datasets, Apache-2.0): per-dataset download, remap into a union label space, re-id, append into one merged COCO layout. Splits preserved; RF20-VL subset flag (added 2026-08-04, user request). Dropped 2026-08-18: closed without its DoD met rather than left open indefinitely on an upstream PR neither party owns โ€” the licence question RESEARCH_LOG already flagged as "most likely to kill the row" (the 100 datasets carry their own individual licences, unresolved by either route) never resolved, and the deferral had no end condition this project controls ยท log offline unit suite over a synthetic two-dataset fixture; merged-layout check_data.py parity 068,073 โŠ˜
075 exp(det): RF100-VL generalization tier [GPU][PRINCIPAL] Fine-tune/eval the detector on the merged RF100-VL (RF20-VL smoke first); recipe, per-domain metric breakdown, report section. Dropped with 074 โ€” the merged layout it trains on does not exist and 074 is closed, not merely paused acceptance criteria set with the WP-074 design; artifacts archived with seeds/configs 045,074 โŠ˜
091c refine(decode): rotated geometry in the geometry module rotated_iou lived in eval/dota_eval.py, imported by decode/rotated_nms.py (WP-091b), putting an evaluation module on the decode path's import graph for pure geometry. Moves to data/rotated_geom.py beside canonicalize and polygons_to_rboxes, the old name a permanent documented re-export, not a deprecation. The precision argument travels with it ยท log the existing test_dota_eval.py and test_rotated_nms.py suites pass unchanged โ€” not even an import line โ€” against the new home; 20/20 goldens unmoved, which is the only evidence a move of a scoring-path function changed nothing, since a silently perturbed kernel would still clear the shapely oracle's 1e-4 tolerance 091b โœ…
091d refine(data): one rotated-box shape check Two private validators in rotated_geom.py doing one job, differing only in the letter naming the row count. _check_rboxes is deleted and rotated_iou guards through _check_2d, so every rejection the module raises spells that count N; the pinned regex in tests/eval/test_dota_eval.py moves with it and gains the argument name ยท log tests/data/test_rotated_geom.py and tests/eval/test_dota_eval.py agree on one message; a malformed input still names the offending shape 091c โœ…
091e refine(losses): the shape a loss will accept losses/probiou.py's _check_rboxes passes arbitrary leading dimensions where rotated_geom requires exactly 2-D, and the looser form is kept: the arithmetic is elementwise, so a pairwise (4, 1, 5) against (1, 3, 5) is well defined. The message tail is pinned, and a four-dimensional plant asserts the divergence rather than leaving it inferred ยท log a plant with a genuine extra leading dimension states which way the contract was settled โ€” accepted, or rejected with the shape named; the tail pinned once decided, so the next reader cannot re-open it by accident 091d โœ…
099 feat(data): YOLO-format dataset reader A YOLO-format reader โ€” data.yaml plus a labels/<split>/*.txt tree of normalized cls cx cy w h rows, oriented rows eight normalized polygon coordinates โ€” producing the same Targets container as the COCO path. Grounded in R32; five gaps registered A54-A58; a YOLO root resolves through its own candidate table. Partially met: dispatch moved to 099b ยท log tests/data/test_yolo_parse.py against synthetic fixtures (A26): round-trip with every modality aligned and rbox_envelopes(rboxes) == boxes; normalized-to-pixel against hand-computed values on a non-square 100x40 image, so an x/y transposition cannot pass; an oriented rectangle whose long edge runs at 45 degrees, so a reader returning the envelope fails; and five distinct rejections each asserted against the full file:line: prefix, with the bad row placed third so the number is a real count 098 โœ…
099b feat(data): a datamodule that dispatches on layout DetectionDataModule picks the COCO or YOLO reader by which layout data_root satisfies, so a YOLO root trains through lucid-yolo fit. detect_layout evaluates both candidate tables; satisfying both refuses (A63). A layout override covers what inference cannot. mask_targets raises; argument failures at construction, filesystem verdicts at setup() ยท log tests/ptl/test_datamodule.py โ€” a YOLO root and a COCO root under the same flag each reach their own reader; a root satisfying neither names both conventions and all thirteen paths tried; a root satisfying both refuses and names the override that settles it; copy_paste suppressed on a polygon-free root with the RNG draw still consumed, so the frozen data goldens are unmoved 099 โœ…
099d docs(data): the two layouts and what an annotation looks like docs/DATASETS.md covered provisioning only. Adds a reference section: both directory conventions as the candidate tables probe them, one worked annotation per task (detect, segment, obb) per container (COCO, YOLO), what YOLO cannot express (rings, hence mask_targets), the oriented container's non-schema keys (A53) ยท log scripts/_tests/test_audit_docs_present.py โ€” the section exists; every layout claim traceable to data/layout.py, data/yolo.py, data/coco.py or data/dota.py 099b โœ…
099c feat(data): YOLO layout in the check module and a worked config check_dataset learns the YOLO tree, and an unstated --dataset probes instead of defaulting to coco: a pre-flight answering a different question than its run reports a missing train2017 for a root that trains fine. dota stays named, no run reading it and no probe covering it. configs/det_yolo_smoke.yaml is the worked --data.layout example ยท log tests/data/test_check.py โ€” a YOLO root validates with no --dataset; a missing label file names split and stem, a malformed row names {path}:3:, and a class the data.yaml does not declare fails as outside 0..1 099b โœ…
099e feat(data): the splits a check is asked for check_dataset gains splits, threaded through _check_root into check_yolo_root / check_dota_root; unstated keeps each layout's own YOLO_SPLITS / DOTA_SPLITS, named at the branch that knows the layout. coco refuses the flag, its two splits being the published fact its counts come from; an empty tuple raises in both root checkers ยท log tests/data/test_check.py::TestCheckDatasetSplits โ€” a train-only YOLO root passes with --splits '[train]' and fails without it; coco and () each raise, the latter from the library entry too 099c โœ…

๐Ÿ“ฆ Phase 10 โ€” Consolidation, release 0.4.0, then rolling (WP-065โ€ฆ067, 109, 110)

WP Commit subject Scope DoD Dep Status
065 docs(report): consolidated multi-task reproduction note A section reading the three tiers against each other, appended not merged (D10 is append-only): the shared trunk and what each head cost, a 23-row union of the per-tier assumption tables marking the 11 an accepted run carried without isolating, project-wide deviations split from per-tier ones, and four hypotheses with the test each needs ยท log scripts/_tests/test_audit_docs_present.py::test_report_sections โ€” every tier heading plus the consolidation, so no section can be renamed out of an append-only file 064 โœ…
066 test(export): the graph answers what the checkpoint answers Two gates on the deployed one-to-one graph, all three heads: the exported ONNX carries no NonMaxSuppression and does carry TopK at a static (1, 300, 6) / (1, 300, 7); and run under onnxruntime it reproduces the checkpoint-loaded module's decode โ€” class ids exact, boxes within 4.6e-05. onnx and onnxruntime are dev-group only ยท research log ยท engineering log tests/models/test_onnx_export.py::test_e2e_graph_ops and ::test_e2e_export_matches_checkpoint, each over detect, segment and obb 065 โœ…
109 fix(scripts): audit the binaries a wheel ships A third check in scripts/audit_licenses.py (the path as it stood here; WP-129 moved the script to scripts/lint/audit_licenses.py), over the files each wheel's RECORD declares, matched against a named table of copyleft native libraries. Catches the wheel that declares a permissive license, ships license documents naming no copyleft, and vendors a GPL binary regardless โ€” av's libx264, met while sourcing WP-067's example (D16) ยท log scripts/_tests/test_audit_licenses.py::test_shipped_x264_is_flagged_though_nothing_declares_it and ::test_live_environment_ships_no_unallowed_copyleft_binary 004 โœ…
110 refine(cli): drop the deprecated download alias The removal 0.3.0 put in writing in three places โ€” [project.scripts], AGENTS.md sec. 2, the 0.3.0 changelog โ€” carried out on schedule: the console script, the argparse parser that served only it, and its __main__ twin. lucid-data download is untouched. Every operator-facing string naming the alias names the surviving spelling now, the --verify repair hint included โ€” asserted runnable rather than merely present ยท log tests/cli/test_cli_surface.py::test_the_deprecated_download_alias_is_gone and tests/data/test_download.py::TestCliVerify::test_the_repair_hint_names_a_command_that_still_parses (node id moved under TestCliVerify by WP-128) 089, 096 โœ…
067 release: v0.4.0 consolidated note and examples [PRINCIPAL] The worked example is drawn with matplotlib (R29), not supervision, which D16 refused: it hard-requires av, whose wheel ships a GPL libx264 under BSD-3-Clause metadata. scripts/draw_predictions.py draws a checkpoint's own answer for all three tasks โ€” boxes, per-instance masks, rotated quadrilaterals rather than their upright envelopes. Tag v0.4.0 is the one piece left, deferred to the user (not blocking later WPs); the roadmap reopens for the next 0.MINOR once it lands ยท log scripts/_tests/test_draw_predictions.py, and release.yml green on v0.4.0 066 โœ…

๐Ÿ” Phase 11 โ€” Rolling, toward 0.5.0 (WP-111โ€ฆ119, 127โ€ฆ130)

Opened at the 0.4.0 release, and deliberately opened with two rows rather than a plan: both are deferrals this repository already recorded in writing, so they are the queue's own backlog rather than a new direction. What a 0.5.0 is for is decided when there is a run behind it.

WP Commit subject Scope DoD Dep Status
111 exp(obb): a whole-image oriented figure [GPU][PRINCIPAL] The OBB-smoke checkpoint (version_10) re-scored through merge_whole_images (WP-107) on real DOTA-v1.0 val, 458 source images, CUDA (operator-run): whole-image rotated mAP50-95 0.3146 against the per-tile 0.2914, every one of the four metrics higher rather than lower. WP-107's own "can only lower" claim holds for core ownership alone; A60's lifted detection cap on the merged pass, exercised here for the first time, is the leading reason and is not isolated from the merge in this measurement ยท log A whole-image rotated mAP50-95 in the reproduction report, beside the per-tile figure rather than replacing it (D10) 107, 064 โœ…
112 refine(export): one exportable decode path src/lucid_yolo/export.py -- E2EExportGraph plus DetectExportGraph/SegmentExportGraph/OrientedExportGraph, reproducing predict.py's predict_image/predict_segmentation/predict_oriented "e2e" compositions call for call, promoted out of the test file WP-066 left them private to. One necessary divergence documented rather than hidden: a traced graph's fixed output shape means SegmentExportGraph gathers mask coefficients for every one of its k rows, padding rows included, which only stays safe when the canvas's anchor count exceeds k ยท log tests/models/test_onnx_export.py passes against the shared composition rather than its own wrappers 066 โœ…
113 docs(all): an emoji on every top-level heading Every H1 and H2 across README.md and docs/** carries one topical emoji, chosen per section rather than decoratively repeated, so a long register scrolls as landmarks instead of undifferentiated grey. The four README headings other sections link to gain explicit <a id=> anchors, since an emoji changes the slug a renderer derives ยท log scripts/_tests/test_audit_docs_present.py passes with its section assertions keyed on titles rather than decoration; every in-page link still resolves 067 โœ…
113b feat(scripts): absolute README links for PyPI scripts/absolutize_readme.py rewrites the README's repository-relative links to absolute ones at packaging time: figures through raw.githubusercontent.com, documents through github.com/.../blob, both pinned to the tag being released. Opt-in per invocation (--ref or LUCID_YOLO_RELEASE_REF), reverted after a local build, wired into release.yml ahead of uv build. Lettered off 113 and landed in its commit: it exists because that upgrade gave the README figures worth publishing ยท log scripts/_tests/test_absolutize_readme.py, including that an ordinary build cannot trigger the rewrite and that the workflow rewrites before it builds 067 โœ…
114 docs(site): a MkDocs Material site over docs/ The registers are published twice from one source: as plain markdown on GitHub and as a searchable Material site, with mkdocs.yml owning no prose beyond docs/index.md โ€” a front page that routes a reader to the register answering their question and quotes the reproduced numbers rather than restating them. --strict on every pull request, GitHub Pages on every push to main, and the licence audit run in that job because the docs group is in no other workflow's environment. mdformat and check-yaml each split into two instances, one for the tree and one for the dialect docs/ now renders in ยท log scripts/_tests/test_audit_docs_site.py โ€” every page in the nav, every nav entry on disk, tables declared, identity fields matching pyproject.toml, and the audit ahead of the build 113 โœ…
114b refine(deps): cap mkdocs below the unlicensed major The docs group pins mkdocs>=1.6,<2, a licence bound rather than a compatibility one: MkDocs 2.0 is described by the Material team's own build-time notice as "Currently unlicensed", and unlicensed is stricter than the AGPL this project bans โ€” the default is no grant at all. The licence audit cannot catch it, because it matches a GPL-family pattern against a declared licence and a distribution declaring nothing matches nothing ยท log scripts/_tests/test_audit_docs_site.py::test_mkdocs_is_capped_below_the_unlicensed_major, keyed off the version operator so it cannot silently move to the mkdocs-material pin beside it 114 โœ…
115 feat(scripts): an undeclared licence is a finding, per tier audit_licenses.py matched declared licences only, so a distribution declaring nothing passed silently. Two tiers by exposure -- a base dependency's unreadable licence fails, a dev/docs-only one flags -- plus a prose reader for the common permissive texts and an extras-aware closure walk ยท log A base-tier unreadable licence exits 1; a dev/docs one flags and exits 0; scripts/_tests/test_audit_licenses.py covers both tiers 114b โœ…
115b fix(scripts): allowlist a licence that cannot be read Lettered off 115 and landed in its commit: the new check's first run found cuda-toolkit shipping no licence field or document at all, verified from the wheel. Resolved by UNREADABLE_ALLOWLIST (D17) rather than a weaker check, and the suite stopped reading the installed environment ยท log scripts/_tests/test_audit_licenses.py -- the allowlist excuses only what it names, every entry cites a decision id, no test touches the installed environment 115 โœ…
116 refine(docs): docformatter over every docstring A PEP 257 formatter beside the three that already run, configured in [tool.docformatter] and hooked ahead of ruff-format so the two converge in one pass. Wrapping off in both axes to preserve hand-argued Args: blocks; 32 summaries needed rephrasing where the splitter cut mid-abbreviation (Eq., sec.) ยท log pre-commit run docformatter --all-files passes and reformats nothing on a second run; make gate green with the doctests unchanged 003 โœ…
116b fix(deps): pin docformatter's hook to a compatible interpreter The hook carried no language_version, so pre-commit built its env against whatever python3 resolved off PATH -- drifted to a 3.10 install below this project's own >=3.11 floor, and docformatter's config reader needs tomllib (3.11+) to parse pyproject.toml. Pinned to python3.11, the guarantee mypy and license-audit already get from entry: .venv/bin/python ยท log pre-commit run docformatter --all-files passes regardless of PATH order; make gate green 116 โœ…
117 test(tests): doctest every helper, document every test A fresh ast count corrects the row this replaced: 261 non-fixture helper functions in tests/**/test_*.py, all carrying a docstring already, but only 1 of 261 carries a runnable Example, though --doctest-modules tests (WP-085) exercises all of them. 23 of 678 test_ functions carry no docstring at all, every one of them in tests/data/test_download.py; others name the assertion rather than the hypothesis it defends -- now 0 of 261 missing an Example, 0 of 678 undocumented ยท log Every non-fixture helper carries a runnable Example; every test_ docstring states the scenario under test. scripts/lint/audit_test_doctests.py (WP-129) enforces the first via a pre-commit hook; make gate green 085,116 โœ…
128 refine(tests): regroup flat-naming test functions into classes Solves 117's ambiguity with a discriminator, not a bigger eyeball pass: a shared-prefix candidate confirms only if the prefix is a contiguous token subsequence of something the file actually calls (an ast.Call target), so house-style sentences sharing a topic noun no longer false-positive against genuine mechanical enumeration. New scripts/lint/audit_flat_test_groups.py (own script, not audit_test_doctests.py (129) โ€” that script's own docstring disclaims the job) + flat-test-group-audit hook enforce it. 9 files regrouped under class Test<Subject>:, matching tests/eval/test_tile_merge.py's precedent; node-id citations in this table (rows 044, 051, 110) and docs/ASSUMPTIONS.md A45 updated to match scripts/lint/audit_flat_test_groups.py, scripts/_tests/test_audit_flat_test_groups.py (10 gates incl. live-tree sanity); flat-test-group-audit pre-commit hook clean; make gate green 117 โœ…
129 refine(scripts): split scripts/ into core and lint, doctest audit as a hook 117's enforcement lived as a pytest meta-test scanning repo content. Split into scripts/lint/audit_test_doctests.py, a test-doctest-audit pre-commit hook gated on tests/.*/test_.*\.py (not always_run, since it's repo content not installed-environment state like license-audit). audit_licenses.py and check_commit_trailers.py move into scripts/lint/ too, so the directory root is core tooling and lint/ holds enforcement; golden/training/release scripts stay put -- their dotted scripts.<module> paths are baked into checked-in golden JSON producer fields, so moving them would be a data migration, not a rename ยท log pre-commit run --all-files green with every hook path updated; scripts/_tests/test_audit_test_doctests.py covers the script's functions in isolation (pytest owns the functional core, the hook owns enforcement); make gate green 117 โœ…
130 refine(scripts): consolidate scripts/-testing tests, wire remaining lint hooks Every scripts/-testing test -- tests/meta/'s remaining audits, tests/scripts/, tests/train/test_shapes_regression.py, tests/integration/test_overfit_micro.py -- moves into scripts/_tests/test_<script_name>.py. Five checkers still inline in tests/meta/ split into scripts/lint/ alongside WP-129's; release_guard.py/check_goldens.py gain manual-stage hooks, release_guard.py's --tag now optional (git describe --exact-match); every local hook gets a distinct emoji. explicit_package_bases/mypy_path and --import-mode=importlib fix import collisions on scripts.X and bare conftest. scripts/README.md maps every script to its test ยท log pre-commit run --all-files green, all eleven local hooks pass; make test 2235 passed; make gate green 129 โœ…
130b fix(ci): emoji on every hook, three now run on --all-files Lettered off 130: the emoji requirement only reached the eleven repo: local hooks, leaving the eleven third-party ones unmarked -- every hook now carries a distinct name: emoji, 22 total, none repeated. Separately, commit-trailers (commit-msg stage) and golden-check/release-guard (stages: [manual]) never ran under pre-commit run --all-files -- golden-check/release-guard gain pre-commit alongside manual (cheap off a release tag); commit-trailers can't, since --all-files has no commit message to read, so sibling hook commit-trailers-history re-validates every commit ahead of origin/main via the script's --range mode instead ยท log pre-commit run --all-files shows all 22 pre-commit-stage hooks, each with a unique emoji; commit-msg stage still validates the message being written; make gate green 130 โœ…
118 refine(docs): split the log by claim, trim the table to match RESEARCH_LOG.md split into itself (fidelity: model, loss, data protocol, eval numbers, paper ambiguity) and a new ENGINEERING_LOG.md (repo tooling: CI, packaging, licensing, formatting, CLI, docs). The split is by claim rather than by work package -- 5 entries whose finding was genuinely both (066, 079, 083, 091b, 097) are cut into two cross-linked halves rather than forced into one bucket, the rest move whole. And rows 115/115b/116 -- whose Scope columns had grown to a paragraph of debugging narrative each -- are trimmed to what shipped, since every fact in them already lives in a log entry ยท log scripts/_tests/test_audit_docs_present.py::test_every_log_link_resolves passes against both files; mkdocs.yml nav and docs/index.md route to both; make gate green 003 โœ…
119 refine(cli): drop the root configs symlink WP-038's repo-root configs -> src/lucid_yolo/configs symlink was a compatibility shim so old commands kept working once configs moved to package data; no code depended on it -- every resolver already computes the path package-relatively, _resolve_config_args already lets a bare --config det_smoke.yaml reach the packaged file from a checkout or a wheel alike. The symlink was legacy. Dropped, and the one documented example that used the root path directly (--config configs/det_smoke.yaml) now shows the bare-name form the resolver was built for ยท log make gate green with the symlink absent; _resolve_config_args resolves a bare name to the real package path 038 โœ…
127 refine(ptl): task and variant in the checkpoint path Lightning's own default writes lightning_logs/version_N/checkpoints/epoch=X-step=Y.ckpt -- nothing in the path or filename names the task or scale, so identifying a run means opening its hparams.yaml. Surfaced re-scoring WP-064's OBB-smoke checkpoint for row 111: version_10 told nothing until cross-referenced against hparams.yaml's task: obb. DetectionCLI.instantiate_trainer now injects a ModelCheckpoint whose filename template carries {task}_{variant} ahead of Lightning's own {epoch}-{step} suffix, unless a config already places its own; dirpath stays at Lightning's default, so no already-written checkpoint moves or is renamed ยท log tests/ptl/test_callbacks.py -- a fit run's checkpoint path names its task and variant; existing version_N numbering and epoch/step naming unchanged otherwise 040 โœ…

๐Ÿ•บ Phase 12 โ€” Keypoint detection, release 0.5.0 (WP-120โ€ฆ126, 131โ€ฆ140)

Generic K-point task, not "human pose": K a constructor argument like the class count, a third per-level stem after the OBB angle precedent (WP-062), loss is RLE (R14) whose formulation is person-agnostic. COCO human pose (R12) supplies the demo dataset only -- K=17, the OKS sigma table, the skeleton edge list are dataset metadata, never model code. Smoke tier gates on RLE's mechanism claim (uncertainty-weighted regression beats plain coordinate regression), not R1's Table 9 absolute mAP, matching the det/seg/obb precedent that this project targets relative claims trained from scratch, not the paper's headline numbers (design doc ยง5.11, ยง7).

WP Commit subject Scope DoD Dep Status
120 feat(data): keypoint targets and fliplr index swap Targets gains keypoints/keypoint_vis -- (N, K, 2) float32 coordinates and matching (N, K) int64 visibility, K generic, threaded through clone/filter/concat/empty on the shared instance axis (polygons' 0-or-N convention, not rboxes' independent one). HorizontalFlip gains keypoint_flip_pairs, a caller-supplied left/right index-swap map (None mirrors coordinates only) rather than a hardcoded COCO table -- A64 records COCO's own 17-point pairing as the value WP-121 wires in, not something this row assumes ยท log tests/data/test_targets.py::TestKeypoints, tests/data/test_photometric.py::TestHorizontalFlip (mirror, swap, out-of-range pair); make gate green 008,013 โœ…
121 feat(data): COCO keypoint parsing CocoDetectionDataset gains opt-in keypoints=True, parsing each retained annotation's flat COCO keypoints field into WP-120's Targets.keypoints/keypoint_vis (crowd/RLE-skipped annotations excluded, mismatched per-instance K raises naming the image); visibility carried through unchanged. Reader-only, as the row's own DoD always scoped it -- no datamodule or HorizontalFlip wiring lands here; A64 corrected in the same commit, since it had prematurely named this row for that wiring ยท log tests/data/test_coco.py::TestKeypointParsing; make gate green 014,120 โœ…
121b fix(data): a segmentation ring only an oriented reading actually needs Validating a fuse-augmentations Task.KEYPOINTS export surfaced _parse_annotation required a usable segmentation ring for every reading mode, not only oriented=True -- a keypoints-only export with no ring parsed to zero instances. Relaxed: absent ring is fatal only for oriented=True; a present-but-unusable one (RLE dict, empty list, \<3 points) stays excluded in every mode, preserving WP-014's crowd/RLE policy and TestKeypointParsing's RLE-exclusion case. A plain/keypoints reading now returns Targets.polygons == [] rather than dropping instances, matching the dataclass's 0-or-N contract; instance_mask_targets now guards mask_targets=True ยท log tests/data/test_coco.py::TestRinglessAnnotations; regression run across every CocoDetectionDataset caller (175 tests); make gate green 121 โœ…
122 feat(models): keypoint head stem and decode composition DualDetectionHead gains opt-in num_keypoints (a third stem, _build_coeff_stem's shared-width precedent -- no Table S11 measurement covers a keypoint task), emitting raw (x, y) offsets plus raw R14 per-axis sigma, point-major, both unactivated -- sigma's positivity mapping is deferred to WP-123's RLE loss (A65, AGENTS.md sec. 6). decode_keypoints composes only the coordinates with decode_ltrb's anchor-centre/stride convention. _DetectionBranch's internal tuple widened 4->6; the three build.py call sites unpacking it positionally needed the matching fix, caught by make gate, not by codex's sandboxed run ยท log tests/models/test_keypoint_head.py; make gate green 062,120 โœ…
123 feat(losses): RLE loss RLELoss(nn.Module) -- the only loss in this repo carrying trainable weights, so a module the trainer registers rather than a pure function: a hand-written 6-layer RealNVP flow (Eq. 12, R14 sec. 4's stated Lfc=3/Nn=64/Leaky-ReLU conditioner) over the standardized residual, summed with a fixed unit Laplace NLL (Eq. 8, log s dropped per R14's own implementation). Resolves A65 (sigma via sigmoid, R14 sec. 3.3, applied at the loss rather than the head) and records A66 (visibility policy: v=0 excluded, v>=1 included). Written by hand from R14's equations per AGENTS.md sec. 6 -- no other WP in this phase was delegated to codex for the same reason ยท log tests/losses/test_rle_loss.py -- invertibility, log-det vs. autograd's own Jacobian, the flow's density integrates to ~1 over a grid, the gradient-shortcut claim, sigma range, visibility masking, toy convergence; make gate green 122 โœ…
124 feat(eval): OKS evaluation wiring Installed torchmetrics==1.9.0 has no keypoint iou_type (verified against source, correcting this row's premise) -- evaluate_keypoints drives the already-a-dependency faster_coco_eval.COCOeval_faster(iouType="keypoints", ...) directly, one-shot rather than DualPathEvaluator-streaming since no keypoint decode pipeline exists yet. keypoints_to_predictions adapts a fixed-size (B, N, K, 2) batch (WP-122's decode shape) the way detections_to_predictions adapts boxes. COCO_KEYPOINT_OKS_SIGMAS (R12's 17-point table) is named explicitly rather than left to the library default, so it stays citable and version-independent even though it currently equals that default ยท log tests/eval/test_keypoint_eval.py -- adapter padding/remap, perfect-match and far-miss OKS oracles, empty-input all-zero contract, v=0 exclusion (A66); make gate green 069,121,123 โœ…
125 exp(ptl): Pose-smoke tier [GPU][PRINCIPAL] n-scale smoke run on COCO human pose, RLE paired against WP-135's Laplace-NLL control: e2e OKS AP 0.2738 vs 0.2527, same-sign gap as R14 Table 7's own 70.5-vs-67.4 AP at full scale -- acceptance is the mechanism's direction of effect, not R1 Table 9's absolute mAP=63.0, which nano width and 50 epochs were never going to reach. First real-data run for A64's dataset-supplied flip pairing (already resolved by 132, exercised live here for the first time) and A68's un-tuned keypoint_gain = 1.0, both landing on a working head rather than a mechanism failure ยท log Pose-smoke acceptance; make gate green 124,132 โœ…
126 docs(report): keypoint tier report, model card, and the fourth task everywhere The fourth task's record, matching det/seg/obb: model_cards/keypoints.md (intended use, COCO composition limits, clean-room statement, an ethical-considerations section stronger than the segmenter's own โ€” a skeleton is a more identifying readout than a mask), REPRODUCTION_REPORT.md's 0.5.0 section (mechanism table, acceptance stated on WP-125's mechanism claim rather than a pose figure, assumption outcomes for A64/A65/A66/A68/A69/A70/A71/A72/A73), and plot_training.py's fourth panel taught to draw the keypoint loss term (RLE or its Laplace-NLL control, whichever a run's metrics.csv names), rendered for run v11. The general docs said "three tasks" throughout and named no keypoint task at all -- README, index.md and TRAINING.md now carry the fourth, framed as keypoints not pose (K generic, human pose one instantiation, gate runs a 7-point symbol schema). Stale figures corrected while there: 63->73 assumptions, 20->14 frozen goldens, the roadmap-111 whole-image number now quoted rather than called pending ยท log scripts/_tests/test_audit_docs_present.py; scripts/lint/audit_figure_captions.py; make gate green 125 โœ…
131 feat(data): keypoints fixture, geometric-only det/obb/seg categories fuse-augmentations' class_names(class_mode) spans the whole Shape union regardless of SyntheticConfig.shapes -- animal shapes landing (WP-121b) would move det/obb/seg's frozen *_num_categories goldens from 4 to 16, though those fixtures only draw the 4 geometric shapes. Reported upstream; fix (class_names(class_mode, shapes=...)) is what shapes=DEFAULT_SHAPES on generate_detseg_fixtures/generate_obb_fixtures narrows -- goldens unchanged. generate_keypoints_fixtures is new: 12 images, animal_shapes(2) (duck, elephant). Pinned by git commit not PyPI version (R21) -- version string doesn't move release to release, PyPI forbids re-uploading 0.10.0.dev0 ยท log tests/fixtures/test_fixtures_load.py::test_keypoints_set_loads, ::test_keypoints_generation_is_deterministic; make gate green, 27/27 goldens unchanged 121b โœ…
133 feat(configs): pose-smoke draft, changelog Unreleased entry CHANGELOG.md had no ## [Unreleased] section -- 27 commits since 0.4.0, incl. 0.5.0.dev0 bump and WP-132's keypoint path, carried zero entries. Backfilled from git log 0a3ef8b..HEAD, user-visible changes only. Closing rule inline: a commit setting __version__ to a plain, non-dev, non-rc value closes the section. Two date mismatches left as known discrepancy: [0.2.0]'s 2026-08-10 vs. eb81caa's 2026-08-11, [0.4.0]'s 2026-08-15 vs. 0a3ef8b's 2026-08-16, both one day earlier. configs/pose_smoke.yaml here: launch config extending det_smoke.yaml/seg_smoke.yaml (task: keypoints, num_keypoints: 17); WP-125's acceptance ([GPU][PRINCIPAL]) untouched ยท log CHANGELOG.md carries ## [Unreleased] covering every commit since the last plain-version bump; pose_smoke.yaml dry-parses (tests/ptl/test_cli.py::test_config_dry_parses[pose_smoke.yaml]) none โœ…
132 feat(ptl): keypoint training path Phase 12 built every keypoint component but never composed them (WP-120 targets, WP-121 reader, WP-122 head stem, WP-123 RLE loss, WP-124 OKS scorer). This row composes them (analogue of WP-087/WP-088), overfit-100 golden as DoD, at A68's keypoint_gain = 1.0. pad_keypoints joins pad_rboxes. Transforms crop/filter, needing A70's clamp-vs-demote-to-v=0 rule (A66's v=0 = no annotation). Flip ran with no keypoint_flip_pairs; A64 resolves via dataset schema. Two faults: RLE residual in absolute pixels vs. sigmoid-bounded sigma_hat (A71); coupling log-scale raw-linear vs. RealNVP's tanh-scaled form (A72). Gated on R21's SymbolShape (K=7 vs. 16, 5-of-7 slots for A66) ยท log tests/ptl/test_keypoint_training.py; goldens/gpu/overfit_micro_kp.json frozen at train OKS AP 0.3357 over 548 instances, floor 0.30 -- small next to the sibling gates because OKS at A67's sigma is a cliff on symbols a few dozen pixels across (ground truth fed back as prediction scores 1.0; a uniform +3 px displacement scores 0.269), so the floor is set from that measured slope rather than guessed; 21/21 goldens; make gate green 121,122,123,124,131 โœ…
134 feat(eval): keypoints wired into lucid-eval, canonical OKS ground truth WP-124 built the OKS scorer, WP-132 the training path, unjoined: no caller, cli/eval.py knew three tasks. A keypoints checkpoint through lucid-eval fell to the detection path: map=0.006288 vs val/mAP=0.48 -- category-count fault: detect_eval hardcodes instances_val2017.json (80 categories, 79 with no prediction; 0.006288*80=0.503). pose_eval.run scores person-keypoints. DualPathEvaluator gains o2o_keypoints/o2m_keypoints, gathered by anchor indices (PAD_ANCHOR_INDEX clamped/zeroed). Points invert letterbox via Letterbox.inverse_map (A10). Ground truth canonical: area/iscrowd/num_keypoints/bbox from annotation. K!=17 refused -- A67 reversed ยท log tests/eval/test_keypoint_eval.py::TestDualPathKeypoints, ::TestCanonicalGroundTruthMetadata, ::TestPoseProtocolRefusesForeignSchemas; tests/eval/test_annotations.py::test_keypoint_load_carries_the_annotations_own_metadata; tests/cli/test_cli_surface.py::test_eval_dispatches_on_the_checkpoints_own_task[keypoints]; make gate green 124,132 โœ…
135 feat(losses): Laplace-NLL ablation baseline for RLE WP-125 accepts on RLE's mechanism claim (direction of effect vs a non-flow baseline), not a number. LaplaceNLLLoss is R14 Table 7's control: Eq. 8 with the flow term G_phi removed, else held (sigma_hat sigmoid, reparameterization, visibility mask) -- R14 measured 67.4 AP vs RLE's 70.5. keypoint_loss switch (rle/laplace_nll) on DetectionLitModule, default rle so config/checkpoint/figure stays unchanged; attribute keeps rle_loss name either way (prefixes checkpoint keys). Ablation carries zero parameters, draws no RNG. pose_smoke_ablation.yaml is pose_smoke.yaml with one line changed, rest identical for a controlled pair. Tool only -- WP-125's paired run is [GPU][PRINCIPAL], user's machine ยท log tests/losses/test_keypoint_nll_loss.py; tests/ptl/test_keypoint_training.py::TestKeypointLossSelection; make gate green 123,132 โœ…
136 refine(configs): unify config names, fold det_yolo_smoke into docs Every packaged config unified onto <task>_<variant>_<tier>[_<detail>].yaml: det_smoke->det_nano_smoke, seg_smoke->seg_nano_smoke, obb_smoke->obb_nano_smoke, pose_smoke->pose_nano_smoke, det_ablations->det_small_ablations (variant: s spelled out), overfit_100->det_nano_overfit_100, pose_smoke_ablation->pose_nano_smoke_laplace_nll (avoids det_ablations's different sense). det_yolo_smoke.yaml dropped, not renamed: content beyond data:/num_classes duplicated docs/DATASETS.md, now a three-flag worked example in docs/TRAINING.md. Old names stay cited as shipped (WP-129 precedent); live docs and functional code updated to the real names ยท log tests/ptl/test_cli.py (glob-discovered, hardcoded name assertions updated); make gate green 040,088,099c,125,135 โœ…
137 feat(ptl): keypoint OKS joins the epoch validation loop A task="keypoints" module logged no per-epoch pose signal -- val/mAP scored its box branch only. val/oks_mAP now accumulates points via gather_keypoints (public, shared with DualPathEvaluator) and scores once at epoch end via evaluate_keypoints, beside val/mAP (val/segm_mAP's relationship, not "obb"'s replacement). Sigma: COCO_KEYPOINT_OKS_SIGMAS at K==17, else A67's uniform sigma (moved into coco_eval.py, one place not two). hotcoco evaluated and rejected: its COCO object cannot satisfy torchmetrics' construction contract; a hand-driven path costs back _StreamingScorer's memory bound. Benchmarked 13x faster, ~1 min saved over a multi-hour run ยท log tests/ptl/test_keypoint_training.py::TestKeypointValidation; make gate green 077,087,102,122,132,134 โœ…
138 feat(eval): hotcoco as lucid-eval's default bbox/segm engine Corrects WP-137: hotcoco is RLE-encoded per batch (matching torchmetrics own _get_safe_item_values), not held dense -- A73 revised in place. lucid-eval --eval_backend {auto,hotcoco,faster_coco_eval}, default auto (prefers hotcoco, falls back if unusable, recorded in info). _HotcocoStreamingScorer fixes 5 real hotcoco traps (params/dataset copy-on-read, bytes-vs-str RLE counts, uint8-only encode, iou_type keyword, fd-level stdout) matching rf-detr PR #1402 exactly. Bit-for-bit parity vs faster_coco_eval, bbox+segm+empty+end-to-end. Torchmetrics-subclass fork (WP B) deferred -- no DDP here, rf-detr own verdict \<2% of an epoch either way. keypoints/module.py untouched ยท log tests/eval/test_coco_eval.py::TestHotcocoParity; make gate green 043,069,096,134,137 โœ…
139 feat(models): keypoint detector builder and params/FLOPs golden Detection, segmentation and oriented detection each ship a composite class, a build_* factory, a .deploy() NMS-free view and a frozen size golden; keypoints shipped none of the four -- the model existed only inline inside module.py, so the release headlining it would have frozen a regression contract with no keypoint entry. KeypointDetector/build_keypoint_detector/_DeployedKeypointDetector in models/build.py, kp_params_flops beside its three siblings, goldens/params_flops_kp.json. Measured at detection's own 640 px / 80-class protocol, so the difference from params_flops_det.json is exactly what the point stems cost: +110,652 params, +0.1238 GFLOPs at n. Golden only, no published-table gate -- R14 states a loss and an evaluation protocol and no architecture ยท log tests/models/test_param_flops.py::test_kp_params_flops_golden; tests/models/test_keypoint_head.py::TestKeypointDetector; make gate green 122,132 โœ…
140 release: v0.5.0 keypoint detection [PRINCIPAL] The fourth task, released: __version__ to a plain 0.5.0, the ## [Unreleased] section WP-133 opened closed as ## [0.5.0], and make freeze-goldens MINOR=0.5 copying the eight live metric sets into goldens/frozen/0.5/ -- one more set than 0.4 froze, the keypoint size golden WP-139 added. Tags v0.4.0 and v0.5.0 are both still uncut and both deferred to the user (not blocking): release.yml fires on a pushed v0.* tag and re-runs the guard, which is the operator's to trigger ยท log scripts/release_guard.py --tag v0.5.0 passes tag and changelog; make gate green with the 0.5 freeze in place 126,139 โœ…
140b docs: the principal, the contract's end, and two phases Lettered off 140: governance following the release rather than any package. [HUMAN] becomes [PRINCIPAL] project-wide -- the tag always meant whoever drives the project rather than whoever executes it, and anatomical uses plus the pre-existing operator meaning whoever runs the command both stay untouched. D18 records that the WP contract is a reproduction instrument that ends with the reproduction, what relaxes, and that a new task restarts it, with contributor admission built as three layers. Phases 13 and 14 write down the fuse-augmentations migration and the keypoint parity 0.5.0 owes ยท log scripts/lint/audit_docs_present.py, scripts/lint/audit_docs_site.py; make gate green 140 โœ…

Beyond the keypoint phase the train continues on the same discipline โ€” Phases 13 and 14 below, then classification and the export matrix โ€” each a new phase of WPs and its own gated 0.MINOR. RF100-VL generalization is not on that list: WP-074/075 were closed on 2026-08-18 over the unresolved per-dataset licence question, and nothing has replaced them, so this project currently has no cross-dataset generalization measurement planned. No 1.0, ever (ADR-002).

๐Ÿ›ก๏ธ Phase 13 โ€” Contributor admission, augmentation guard and keypoint parity, release 0.6.0 (WP-141โ€ฆ153)

Everything that must exist before the augmentation stack can move, and nothing that moves it. Phase 14 does the moving, and is gated on this release.

Opening to contributors is an admission problem. What must never enter this tree is code under any licence the project cannot redistribute under Apache-2.0 โ€” copyleft, source-available, paid, proprietary, or unreadable (D13, D17) โ€” and Ultralytics is the loudest instance of that class rather than the class itself. No attestation detects such code, so three layers are built because each fails differently: WP-142 records who submitted it and under what claim of right, WP-143 states what that claim covers in the general terms the allowlist actually uses, and WP-144 makes a contributor name what an algorithmic change derives from. Only the third yields something a reviewer can check โ€” a provenance id resolvable against docs/PROVENANCE.md โ€” rather than something taken on trust.

The guard is deliberately not a comparison. The swap is authorized by expectations frozen against stated parameters, never seeds: fuse-augmentations will not draw the same numbers from the same seed as the local sampler, so a seed-frozen value would break at the swap for reasons unrelated to correctness. Comparing the old implementation against the new would also enshrine legacy behaviour as the definition of correct and keep that code alive as its own reference, blocking the deletion the check exists to authorize. Keypoint parity ships in the same release because 0.5.0 shipped a task that can neither export nor predict, and a release that closes that gap is a better thing to measure a swap against than one that does not.

WP Commit subject Scope DoD Dep Status
141 docs: contributor guide and the tracking relaxation [PRINCIPAL] CONTRIBUTING.md and issue templates, and D18 taking effect: with the reproduction complete, a change altering no shipped behaviour, no public symbol, no golden and no documented assumption lands as an ordinary gated commit rather than a tracked WP. Names the three admission layers WP-142โ€ฆ144 build and what each is for. Three preconditions sit outside this row โ€” O3 cleared, the repository made public, WP-125 accepted CONTRIBUTING.md in the docs nav (scripts/lint/audit_docs_site.py); make gate green 125 โœ…
142 ci: DCO sign-off required on pull requests Layer one of three: every commit in a pull request carries a Signed-off-by: line, checked in CI rather than by an external app, so the check is a file in this repository and survives whatever happens to a third-party service. Establishes ownership and right-to-submit โ€” the contributor's own representation, on the record, per commit. It does not establish what the work derives from, which is why two more layers follow scripts/lint/check_dco.py doctests; scripts/_tests/test_check_dco.py; make gate green 141 โœ…
143 docs: the licence attestation covers every non-permissive source Layer two. The pull-request template's clean-room line names Ultralytics only, which understates the rule it enforces: the admissible set is the permissive allowlist (D13), so what a contributor must attest to not having copied from is any copyleft (AGPL, GPL, LGPL, SSPL), source-available (BSL, Elastic, PolyForm), paid or proprietary source, and any source whose licence cannot be read (D17). Ultralytics stays enumerated as the prime-directive instance, not as the definition scripts/lint/audit_docs_present.py; make gate green 141 โœ…
144 ci: provenance trailers enforced on contributor commits Layer three, and the only one that produces a reviewable artifact instead of a promise. check_commit_trailers.py --range already validates WP:/Provenance:/Assumptions:/Gate: against docs/PROVENANCE.md; CI runs it over a pull request's own commits, which the squash-merge exemption in ci-tests.yml currently skips. A contributor must then name what an algorithmic change derives from, and an unnamed derivation on a loss or an assigner becomes visible at review rather than after release .github/workflows/lint.yml PR job; scripts/_tests/test_check_commit_trailers.py; make gate green 141 โœ…
145 docs: ADR-005 โ€” the fuse boundary and the battle-test contract Records what the two repositories agree on: the boundary (fuse owns resampling and the coordinate transport that rides with it; this project owns task semantics, multi-image assemblies and supervision targets), the five-step migration protocol, the reciprocal battle-test contract, and the four conditions under which a runtime dependency on fuse is accepted. No code ยท log scripts/lint/audit_docs_present.py, scripts/lint/audit_docs_site.py; make gate green โ€” โœ…
146 fix(tests): reconcile the R21 pin with the LetterShape family The pin never lagged -- pyproject.toml has read fedde0c1 since WP-132 while R21 still named 0a0cc640, so the register is the stale side and this row corrects it there. What LetterShape did to the fixtures is measured rather than argued: both generator-derived goldens produce identical values at either commit, so 0a0cc640 is the whole of WP-132's re-freeze and no golden moves here ยท log tests/data/test_data_goldens.py; goldens unchanged, measured at both commits; make gate green โ€” โœ…
147 refine(data): split sampling from application on stochastic transforms Seven stochastic transforms gain a public frozen parameter dataclass modelled on AffineParams and a public apply-given-parameters method, leaving __call__ as sample-then-apply. This is what lets an expectation be frozen against stated parameters instead of a seed. Guarded by a temporary same-codebase equivalence test โ€” __call__ under a seeded generator equals sample() then apply() โ€” which WP-149 deletes ยท log tests/data/test_aug_seam.py; make gate green 146 โœ…
148 test(data): derived and invariant expectations for every transform tests/data/test_aug_contract.py โ€” tier A expectations correct by construction (a quarter turn of a known box, an identity affine, a flip composed with itself) and tier C invariants that are parameter-free and constrain the modalities against each other (a box bounds its own polygon, keypoint visibility survives what its coordinates survive). No characterization values, so it reviews as mathematics ยท log tests/data/test_aug_contract.py; make gate green 147 โœ…
149 test(data): frozen expectations and the augmentation-invariants golden Tier B pins what has no closed form: tests/data/test_aug_frozen.py, each case stating explicit parameters, literal expected geometry and why no derivation exists, plus goldens/aug_invariants.json through a new scripts/golden_producers.py:aug_invariants producer. Deletes WP-147's temporary equivalence test. Coverage reaches every transform, including those staying local ยท log tests/data/test_aug_frozen.py; goldens/aug_invariants.json discovered by check_goldens.py; make gate green 148 โœ…
150 refine(tests): rescope seed-driven assertions as tier-D determinism Existing seed-driven assertions move to tests/data/test_aug_determinism.py and are labelled for what they are: they pin whichever implementation is live and are re-frozen at each swap, never carried across one. Separating them stops a tier-D failure at a swap reading as a regression in the behaviour tiers Aโ€“C actually guard ยท log tests/data/test_aug_determinism.py; make gate green 149 โœ…
151 feat(models): keypoint detector ONNX export Detection, segmentation and oriented detection each export a .deploy() view; keypoints ships none, so the fourth task is the only one whose released model cannot leave PyTorch. Adds the export path and the parity assertions the other three carry, folding the keypoint decode in the way the OBB angle branch established (WP-062) ยท log tests/models/test_onnx_export.py keypoint case; make gate green 139 โœ…
152 feat(cli): predict_keypoints closes the inference surface predict.py exposes predict_image, predict_segmentation and predict_oriented; the keypoint task has none, so a released 0.5.0 checkpoint has no shipped way to run on an image. Adds predict_keypoints with the letterbox inverse and skeleton overlay the other three establish, K read from the checkpoint rather than assumed to be 17 ยท log tests/cli/test_cli_surface.py keypoint case; make gate green 151 โœ…
152b docs: Phase 14 regrouped into five packages and a release Lettered off 152: planning that follows the keypoint parity rather than any package. Phase 14's thirteen rows become six, at the principal's direction. The Y-n plan codes are unchanged and now map several to a row; the FA-n dependencies are unchanged, since merging rows changes what one commit contains and not what upstream has to ship first. Each row still groups swaps sharing an upstream capability and a test surface, and the golden re-freeze stays alone in the one row holding no swap, so ยง6.5's rule survives. _WP_FLOOR rises to 159 -- the ratchet exists to make a shrinking roadmap a deliberate edit, and this is that edit ยท log scripts/lint/audit_docs_present.py; make gate green 152 โœ…
153 release: v0.6.0 augmentation guard and keypoint parity [PRINCIPAL] The guard and the parity, released before a single swap lands, so the version a swap is measured against is a published one. __version__ to 0.6.0, the ## [Unreleased] section closed, make freeze-goldens into goldens/frozen/0.6 ยท log scripts/release_guard.py; make gate green 150,152 โœ…
153b docs: fuse 0.12 validated against the Phase 14 rows Lettered off 153: validation that follows the release rather than any package. fuse 0.12 ships every capability this phase waits on โ€” FA-1 through FA-5, FA-7 and FA-8 โ€” so the phase is unblocked, and three of its rows described upstream inaccurately. WP-154 pinned an align_corners flag 0.12 declined to add, WP-156 attributed geometric fusion to clip_policy (a colour-chain clamp that shares nothing with it), and WP-157 planned a deletion of rotated_aug.py that upstream's deliberate refusal to clip rotated boxes makes impossible. Corrected here from measurement, with the phase preamble recording that the tag follows the roll-out. No DoD moves and no row splits: those follow the decisions this validation put to the principal ยท log scripts/lint/audit_docs_present.py, scripts/lint/audit_docs_site.py both clean; docs-only, no code touched, gate not re-run 153 โœ…
153c docs: Phase 14 rows split around the conventions and the measurements Lettered off 153: the row splits and DoD moves WP-153b deferred until the principal had decided ยง8.12's two open questions. Six rows become eight โ€” WP-154 splits into the pin bump and the convention alignment, and a new WP-155b measures the letterbox delta on the evaluation path โ€” under one rule: no commit both delegates and moves a frozen value. Three DoD lines added for traps that fail silently, and WP-158 stops claiming a tier-D re-freeze it cannot perform. Phase 14's scope prose, three times the register's median, moves to the log ยท log scripts/lint/audit_docs_present.py, scripts/lint/audit_docs_site.py both clean; docs-only, no code touched, gate not re-run 153b โœ…

๐Ÿ”— Phase 14 โ€” Augmentation upstreaming to fuse, release 0.7.0 (WP-154โ€ฆ159)

The augmentation stack leaves this repository. fuse-augmentations becomes the engine, lucid-yolo becomes its first real-project consumer, and what this project keeps is architecture, task semantics, supervision targets, experimentation, export and inference. The plan of record is .plans/active/todo_augmentation-upstreaming.md: its FA-n codes name work in the other repository and never appear as WPs here, and its Y-n codes map onto the rows below several to a row. No swap starts before its FA ships as a pinned version โ€” the boundary is a dependency bump, never a same-session edit spanning two repositories. Every FA this phase needs shipped in fuse 0.12; the principal's direction is that the phase proceeds on a commit pin and that v0.12.0 is tagged from that same commit once the roll-out here is clean, which is ADR-005's reciprocal battle-test contract rather than an exception to it โ€” this project is the first real consumer, so the tag follows the integration that proves it. The pin is provisional until then, and WP-159 is what exchanges it for a version range with a ceiling. Every row here is measured against the guard frozen in Phase 13, which is why that phase releases first.

Nine rows rather than thirteen โ€” six after WP-152b regrouped them, two more once WP-153c split what the fuse 0.12 measurement showed could not share a commit, one more once WP-154 hit WP-132's wall a second time and WP-154c was opened to close the mechanism that kept reopening it. Each row is still one commit, so the commits are larger and each groups swaps that share an upstream capability and a test surface โ€” a row is now "this much of the engine moved", not "this one function moved".

No commit both delegates and moves a frozen value, which is what ยง6.5's rule protects and what the split buys. Four rows re-freeze and none of them delegates: WP-154 carries what upstream's rebuilt synthetic fixtures force, WP-154b carries the two conventions this project chooses to give up, WP-155b carries whatever the letterbox resampling moves on the evaluation path, and WP-158 closes. The delegation rows โ€” 155, 156, 157 โ€” are then held to the strict reading: a tier-A, B or C value that moves inside one of them is a defect, and the row that moved it is where the defect is.

WP Commit subject Scope DoD Dep Status
154 chore(deps): fuse 0.12 becomes the pinned engine The dependency bump and the re-freeze it forces, with nothing delegated. R21 advances from fedde0c1 to a5a276a, the commit v0.12.0 is tagged from once this roll-out is clean. Two breaks are loud and repaired here: tests/fixtures/synthetic.py imports animal_shapes, which upstream's restructure removed, and the synthetic fixtures themselves moved against goldens whose tolerance is zero. Every changed golden traces to a named upstream change or stops the row ยท scope scripts/check_goldens.py green with each changed value traced case by case, tests/fixtures/synthetic.py collecting; make gate green 153c โœ…
154c fix(goldens): freeze-goldens stops re-copying what it cannot re-freeze WP-154 hit WP-132's wall a second time โ€” goldens/frozen/0.5 and 0.6 both re-acquired the two generator-derived goldens make freeze-goldens was told never to keep, because the target was a blind cp with no notion of the distinction. A "freezable": false schema field on the two live goldens, scripts/freeze_goldens.py skipping them at copy time, and check_goldens.py rejecting one found under frozen/ regardless, so 0.7 cannot repeat what 0.5 and 0.6 both did ยท scope scripts/_tests/test_freeze_goldens.py, scripts/_tests/test_check_goldens.py โ€” a freezable: false golden is skipped at freeze time and rejected if found frozen anyway; make gate green 154 โœ…
154b refine(data): fuse's geometric conventions replace this project's Absorbs FA-1, FA-2 and FA-3, and gives up the two conventions ยง8.12 measured as genuinely different โ€” the composition centre and the mirror axis โ€” deliberately before any delegation, so the frozen literals they move travel with a local edit rather than with a swap. Asserts what is not changed: the datamodule's generator reaching fuse's own draws, and constant fill arriving as a fill= parameter that requires padding_mode="zeros" ยท scope tests/data/test_aug_determinism.py, tests/data/test_aug_frozen.py with the moved literals traced to the two convention changes; make gate green 154 โœ…
155 refine(data): letterbox and instance filter come from upstream Absorbs FA-4 and FA-5. Letterbox keeps inverse_map and forward_affine unchanged in signature and semantics, so its six evaluation and prediction consumers are untouched, and delegates the resize-and-pad internals. RandomAffine and MosaicAssembly consume one upstream keep mask instead of two local copies that must agree, with both thresholds passed explicitly because upstream's defaults drop nothing. Deletes _resize_pad, _LetterboxGeom and both _keep_mask ยท scope tests/data/test_letterbox.py, tests/data/test_affine.py, tests/data/test_mosaic.py, with one case per call site asserting both thresholds reach upstream; make gate green 154b โœ…
155b refine(eval): the letterbox resampling delta measured and re-frozen The row decision 6 asked for, holding no swap. Delegating the letterbox keeps its geometry exactly and changes its pixels, so only image-derived metrics move โ€” on the evaluation path, behind a green coordinate suite. This row measures that delta against the 0.6 frozen goldens, records the number, and re-freezes what moved; nothing moving is a legitimate outcome and is recorded as one ยท scope scripts/check_goldens.py, tests/data/test_letterbox.py; the measured delta recorded in docs/ENGINEERING_LOG.md; make gate green 155 โœ…
156 refine(data): the affine reduces to ranges over a fuse segment Absorbs FA-3 and FA-5 into the warp itself and takes the local fusion with it; what remains locally is R1 Table S3's ranges and the sampling of them. Polygon rings ride the composed matrix via return_matrix=True rather than a duplicate warp path, which makes operation ordering and the single-geometric-segment condition requirements of this row rather than incidental to it. Deletes _warp_image, _theta_from_pixel_matrix, _transform_box_corners and FusedAffineLetterbox ยท scope tests/data/test_affine.py, tests/data/test_fused_warp.py, tests/data/test_aug_contract.py, the single-geometric-segment and not-None conditions both asserted as tier-C invariants; make gate green 155b โœ…
157 refine(data): rotated boxes and flip pairs delegate upstream Absorbs FA-7 and FA-8, the two task-convention modalities, together because each passes a table or a callback upstream rather than moving a rule there. rotated_aug.py and _mirror_targets both shrink to what upstream declines to own rather than being deleted outright, canonicalize is passed as a callback since angle periodicity is a task convention and not a resampling concern, and rotated_geom.py is untouched ยท scope tests/data/test_rotated_aug.py, tests/data/test_photometric.py, tests/data/test_aug_frozen.py, with a mirrored-visibility case asserting the flags follow their points; make gate green 154b โœ…
158 docs: the augmentation stack after upstreaming The closing sweep, and the last of the four rows that re-freeze โ€” like the other three, it holds no swap. data/__init__.py's 54-symbol export list is reduced to what survives, and docs/ASSUMPTIONS.md, docs/PROVENANCE.md, the model cards and README.md are rewritten to describe the boundary rather than a stack this project no longer implements. Structural fixture metrics the swaps moved are re-frozen; tiers A through D are not ยท scope scripts/lint/audit_docs_site.py, scripts/check_goldens.py, tests/data/test_targets.py; make gate green 156,157 โœ…
159 release: v0.7.0 augmentation engine upstream [PRINCIPAL] The swaps released. __version__ to 0.7.0, the ## [Unreleased] section closed, make freeze-goldens into goldens/frozen/0.7, and fuse-augmentations promoted from a development pin to a runtime dependency with a version ceiling, under the four conditions ADR-005 records scripts/release_guard.py; make gate green 158 โœ…

๐Ÿงญ Phase 15 โ€” Audit remediation, release 0.8.0 (WP-160โ€ฆ176)

Phase 14 closed at its own release; what followed it was rolling work, and it belongs here. The six rows already landed โ€” the dependency-tier guard, the nominative README, the golden sweep's move off the commit path, and the three validation-path measurements โ€” open this phase rather than trail the last one.

The rest is remediation. An adversarial read of the whole tree on 2026-09-04 โ€” seven readers over separate dimensions plus one external reviewer over all of it โ€” returned 125 gaps and 26 boundaries that are correct but under-stated. The rows below group those gaps by the file surface each touches rather than by the reader that found them, so a row is one revertable change to one part of the tree and not one reader's list. Two findings block this phase's own release and are its first two rows: the scorer lucid-eval selects by default implements neither COCO's crowd-ignore rule nor its area buckets, and nothing schedules the accelerator half of the gate. The audit itself is .plans/active/audit_adversarial_2026-09-04.md, and its per-finding ids appear in the Scope cells below.

Two rows 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 are dated exceptions rather than precedent. Neither row starts without its own docs/ESCALATION.md entry and a ruling, and the DoD of each says so. Three further rows need an accelerator and cannot go green offline.

Ordering follows what moves goldens: the numerics rows land before WP-167 re-freezes the goldens/gpu/ tree, and the documentation sweep lands after the code it describes.

WP Commit subject Scope DoD Dep Status
160 ci(release): the guard reads dependency tiers The release guard checked the tag, the changelog section and the gate, and nothing about what a consumer installing the distribution receives โ€” which is what a release is. WP-159 found the gap by walking into it: Phase 14 moved five modules under data/ onto fuse-augmentations while its requirement sat in the dev dependency group, so an install omitting that group raised ImportError from lucid_yolo.data, and the fact sat legible in pyproject.toml for five work packages with nothing to surface it. A fourth check walks the shipped package's imports, resolves each to its distribution, and refuses a tag when one is absent from [project].dependencies โ€” naming the group that does declare it, so the refusal diagnoses. Publishability is deliberately not checked: the runtime table carries a direct reference by decision (D20), and a check refusing it would re-litigate rather than protect ยท log scripts/_tests/test_release_guard.py, the group-only case failing against the pre-WP-159 tier layout and passing after it; make gate green 159 โœ…
161 docs: the README names the paper nominatively PROVENANCE.md's usage discipline admits exactly one form โ€” a nominative reference to the YOLO26 paper, arXiv:2606.03748 โ€” and the README's own banner had drifted to the Ultralytics YOLO26 paper, attaching a vendor's name to the method rather than to the citation, which is the conflation sec. 3.5 exists to prevent. The id becomes the link it always described. The name stays in the two non-affiliation sentences, where naming the party is what makes a disclaimer one ยท log pre-commit run license-headers --all-files with both disclaimer fragments intact; scripts/lint/audit_docs_present.py; make gate green 158 โœ…
162 ci: the golden sweep leaves the commit path WP-130b put golden-check on every commit calling it "one already-fast golden recompute"; measured it is 34.6 s, and make gate charged it twice โ€” once inside precommit, once via golden. The hook goes to stages: [manual], leaving this file to the linting and quick checks it is for. lint.yml's --all-files run was CI's only full sweep, so the check moves to a ci-tests.yml step rather than disappearing with the stage ยท log pre-commit run --all-files no longer invokes check_goldens.py; pre-commit run --hook-stage manual golden-check still does; ci-tests.yml carries the 43/43 sweep; make gate green 160 โœ…
163 perf(ptl): validation ground truth crosses to the host once Every validation accumulator moved its ground truth one image at a time โ€” two tensors per image for detection, three for oriented and keypoints, and a mask stack for segmentation โ€” so a batch of 16 paid the accelerator's fixed per-transfer latency 32 to 48 times for a payload that fits in one transfer. _host_split concatenates along the batch axis, moves once, and slices the result back into the same per-image pieces. Values, dtypes and order are unchanged, so no metric and no golden moves. Ported from a survey of RF-DETR's per-image device-to-host sync fixes; measured here rather than assumed ยท log tests/ptl/test_module.py::TestHostSplit, tests/ptl/test_module.py::test_validation_ground_truth_matches_the_per_target_transfer; make gate green 158 โœ…
164 perf(ptl): the mask stack is not copied to drop nothing Validation decoded all 100 detection rows and then wrote masks[keep], a second full copy of a 100ร—160ร—160 boolean stack per image. In the deployed configuration that copy drops nothing: TopKDecoder() takes conf_threshold=0.0 and the anchor count exceeds k, so every row carries a live score. Counting the live rows on the host copy already present and slicing to that count leaves the copy out and, where a caller does set a threshold, shrinks the decode too. Values and order are unchanged, so no metric and no golden moves. Corrects WP-163's rejection of this candidate, whose "MPS regresses" reading was three measurement faults compounding ยท log tests/decode/test_topk_e2e.py::test_the_scored_rows_are_a_leading_run, tests/ptl/test_seg_training.py::test_zero_scored_rows_are_dropped_before_the_decode_rather_than_after; measured +11.7% CPU and +15.4% MPS on the deployed all-rows-live shape, every trial positive on both devices; make gate green 163 โœ…
165 perf(ptl): validation scores reach COCO once per image torchmetrics' epoch-to-COCO conversion hoists boxes and labels to one .cpu().tolist() per image and then reads each detection's score individually, one tensor index and transfer per annotation. The parameter is already optional upstream, so _HoistedScoreBackend passes scores=None, lets super() build the document, and attaches the scores from one read per image. It delegates rather than reimplements โ€” every field but score is upstream's own โ€” and tests/ptl/test_coco_backend.py asserts document equality against the stock backend on the installed version, so drift fails the gate instead of moving a metric. The private-attribute swap is confined to one factory. Accepted deliberately at a coupling cost flagged and overruled: a private method carries no compatibility promise ยท log tests/ptl/test_coco_backend.py (box, mask-with-skipped-image, ground-truth-without-scores document equality, plus end-to-end mAP equality); measured compute() over 320 images x 100 detections 187.7 ms to 169.5 ms (+9.7%), every trial faster; make gate green 164 โœ…
165b docs: Phase 15 opened and the audit grouped into rows Lettered off 165: the phase and the rows it runs. Phase 14 ends at its own release and its six rolling successors move here. An adversarial read of the tree returned 125 gaps grouped below by file surface rather than by the reader that found them, so each row stays one revertable change. Two rows move a frozen value and open with an escalation entry instead of a commit ยท log scripts/lint/audit_docs_present.py with _WP_FLOOR at 176 and its six references raised; docs-only, no code touched, gate not re-run 165 โœ…
166 fix(eval): crowd and area reach the default scorer C-01 and C-02. _accumulate_boxes hardcodes iscrowd: 0 and recomputes area as box width times height, while annotations.py parses both from the file and hands them over โ€” so the engine lucid-eval picks by default implements neither COCO's crowd-ignore rule nor its area buckets, and the faster_coco_eval path fed identical targets implements both. Measured on synthetic cases: map 1.0000 against 0.5050 for one crowd annotation, and the size buckets inverted. The defect is this project's, not the library's โ€” hotcoco returns COCO's answer when the fields reach it. Carries the module's own consistency items with it: the two engines' maxDets, the OKS category set, and the redirect that discards native warnings ยท log tests/eval/test_coco_eval.py::TestHotcocoParity with a case carrying iscrowd=[0, 1] and an area that is not the box area, agreeing on map and on all three size buckets โ€” failing before the fix; make gate green 165b โœ…
167 ci(gates): the accelerator half gets a schedule and the floors a gate [GPU] C-03, H-01, H-02. The Makefile says gate-gpu "is that schedule"; nothing invokes it โ€” no cron, no dispatch, no tag hook โ€” and RESEARCH_LOG.md already records shapes_regression_det.json outstanding. The four task floors are compared once, inside main(), so three of them are enforced only by a human at a prompt, and two gpu tolerances admit values the published DoD rejects: OKS AP down to 0.2857 against a 0.30 floor, rotated mAP50 down to 0.8890 against 0.90. The comparison moves into run_overfit so producer and floor cannot disagree, and each band clamps at its own floor ยท log a scheduled or dispatchable gate-gpu workflow, or a run recorded under .reports/; scripts/_tests/test_overfit_micro.py floor cases for segmentation, oriented detection and keypoints; goldens/gpu/ re-frozen with every band above its floor; make gate-gpu green 170,173 โœ…
168 ci(gates): the gates and the ratchets certify what they claim H-03, H-04, H-08, H-10 and the checker items under them. CI runs 1544 tests where make gate runs 2661 โ€” no --doctest-modules, no src scripts paths โ€” so every scripts/_tests/ test and some 879 doctests never run remotely. Nothing verifies a frozen golden is unmodified: edit its values and tolerances together and the gate is green, which is the rule two principal overrides were granted against. _ASSUMPTION_FLOOR sits at 26 against 73 rows and tests max(ids) where the work-package floor tests len. The licence audit matches the GPL family against a policy that bans six further classes ยท log ci-tests.yml invoking make test; goldens/frozen/MANIFEST.sha256 with a hook asserting every digest and freeze_goldens.py the only writer; _ASSUMPTION_FLOOR at 73 on a len predicate; scripts/_tests/test_audit_licenses.py rejecting an SSPL distribution and one declaring no licence; release_guard asserting tag against __version__ and a non-empty frozen set; make gate green 165b โœ…
169 fix(assign): alignment weights survive zero-metric ties and low overlap H-16 and H-17, both measured. topk over an all-zero alignment metric fills from the global ties, so intersecting with the candidate mask can return no positive for a ground truth that had four โ€” the state a head is in at initialization, decode_ltrb applying no non-negativity and oriented_loss recording that raw distances routinely decode inverted. Separately eps = 1e-9 is an absolute floor under t_max = sยทuโถ, so at IoU 0.01 the best anchor's weight is 2000 times too small and a positive trains as background while fg_mask says foreground. Both move assignment_cases.json, live and frozen from 0.2 โ€” measured, neither does ยท log a real ground truth with c candidates and an all-zero metric yielding min(k, c) positives; tests/assign/test_tal.py weight assertion parametrized over u_max in {1.0, 0.5, 0.1, 0.01}; a principal ruling recorded in docs/ESCALATION.md before any frozen value moves; make gate green 165b โœ…
170 fix(losses): degenerate geometry stops answering plausibly [GPU] H-18, H-19 and the degenerate-input medium block. A collapsed box draws a 3e5 CIoU gradient through atan(w/(h + eps)) and an inverted one draws none at all, so the term can never un-invert a box; atan2 removes both. A NaN rotated box scores IoU exactly 0, so suppression never fires and it is emitted as a detection with a real score. Negative extents canonicalize into a valid pair and return IoU 1 against a documented zero. Invisible keypoints reach the RealNVP flow before the mask does, so one non-finite unlabeled coordinate makes every gradient NaN while the loss reads healthy ยท log gradient-magnitude bounds rather than finiteness in tests/losses/test_ciou.py; NaN and Inf cases for rotated_iou and RotatedNMSDecoder; a gradient-independence assertion on the masked keypoint; float16 value and gradient cases for ProbIoU; goldens/gpu/ re-frozen with each moved value traced; make gate green 165b โœ…
171 fix(cli): entry points validate values, not only types H-20 and the eight siblings the audit grouped under it. Nine public entry points check a type and not a domain, so an out-of-range value reaches arithmetic or dispatch and answers plausibly instead of failing. --limit -5 scores every image except the last five and prints the truncated count as though it were the request; --batch_size 0 raises from a progress-bar total; a misspelled decoder takes the one-to-many path with another branch's boxes and, on the oriented path, another branch's headings; img_size=641 crashes inside the neck; an unknown task is scored as detection ยท log tests/cli/test_cli_surface.py refusing a negative limit, a zero batch size, an out-of-range confidence threshold, an unknown decoder spelling, a canvas not divisible by the maximum stride, and an unsupported task โ€” each with a message naming the flag; make gate green 165b โœ…
172 fix(data): every merge path keeps every modality The data-layer block. CopyPaste._merge rebuilds Targets without rboxes or difficult, so a rotated destination loses its modality through the apply seam WP-147 made public โ€” the exact failure _reject_rboxes exists to prevent โ€” and a difficult flag A48 makes load-bearing is refilled all-False. HorizontalFlip mirrors about W-1 and never re-clips, so the last stage of the train pipeline hands the assigner a coordinate a pixel outside the canvas. Train and val COCO label spaces are derived independently and never compared ยท log tests/data/test_mixup_copypaste.py driving apply directly for both the rotated refusal and difficult survival; a train/val category-map assertion raising on the symmetric difference; the flip's convention stated and asserted; aug_invariants.json's move ruled on in docs/ESCALATION.md before it lands; make gate green 165b โœ…
173 fix(models): named branch outputs and a schedule that reaches its floor [GPU] H-21 and the model-surface block. _DetectionBranch.forward returns six tensors as a bare positional tuple destructured in five places, four of them with underscore placeholders โ€” the failure its own docstring argues against, one layer below the dataclass built because a tuple was judged unsafe there. count_flops on a deploy() view flips the parent into train mode through shared submodule references. The LR factor's last real step is total - 1, so a 100-step run floors at 0.0209 rather than lrf, and warmup_steps >= total_steps never decays at all ยท log a frozen BranchOutput dataclass read by attribute at all five call sites; tests/optim/test_schedule.py asserting the factor at total_steps - 1 equals lrf and that warmup is clamped below the total; a mode-restoration case for count_flops; goldens/gpu/ re-frozen for the schedule change; make gate green 165b โœ…
174 test(eval): the keypoint tier's shipped surface is executed H-05, H-06 and the checkpoint block. predict_keypoints has zero executed lines in the gate โ€” its Example asserts callable, and the one test naming it replaces it with a stub โ€” while pose_eval.py, the keypoint tier's acceptance scorer, sits at 37% behind the same Example pattern. _overlay_ema is unexecuted too, and resolves its callback key by substring in one place and exactly in another, copying only the shadow's own keys so a module tensor missing from the shadow silently keeps its raw value ยท log tests/predict/test_predict_kp.py planting a known point set and asserting the letterbox inverse against hand-computed coordinates; tests/eval/test_pose_eval.py covering the K != 17 refusal; _overlay_ema unit-tested against a hand-built payload including the shadow-less raise; an unconditional hotcoco_available() == (True, None) assertion; make gate green 165b โœ…
175 docs: the record describes the repository that exists The documentation sweep, and the register work under it. The README names 0.4.0 as current against __version__ 0.7.0, offers an install line that resolves to nothing, and says the paper publishes no oriented parameter table while test_obb_vs_tableS11 gates exactly that table on every commit. Five documents count three tasks where four ship, and the vendor-bound paper phrase WP-161 corrected in the README survives in NOTICE, docs/index.md and all four model cards. The blueprint, the source of 49 in-code citations, has no register row โ€” measured, 133 across 36 files ยท log a register row for the blueprint stating authorship, publicity and licence status; a second audit row covering 975ff50..HEAD; the corrected paper phrase added to audit_license_headers.py's required fragments; a README-version check beside audit_version_single_source.py; scripts/lint/audit_docs_site.py clean; make gate green 166,168,169,170,171,172,173,174 โœ…
176 release: v0.8.0 the audit remediations [PRINCIPAL] The phase released. __version__ to 0.8.0, the ## [Unreleased] section closed, make freeze-goldens into goldens/frozen/0.8. 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 ยท log scripts/release_guard.py; make gate green 175,167 โœ…

โ›“๏ธ Critical path and parallelism

Strictly sequential: 001 -> 007 -> 015 -> 023 -> 030 -> 033 -> 040 -> 046. WP-016โ€ฆ019 (blocks), WP-024 (CIoU), and WP-031 (Newtonโ€“Schulz) have no interdependencies and may be executed in any order once WP-003 lands. Phases 7 and 8 are strictly gated on releases 0.1.0 and 0.2.0 respectively โ€” task heads never land on an unproven detector.

โš ๏ธ Failure budget

If a WP's DoD cannot be met after two documented assumption iterations, escalate (AGENTS.md sec. 4). Fidelity-gate WPs (023, 052, 062) are the expected escalation sites: they are where the papers' block-level ambiguity meets a hard published number, and where an honest reproduction earns its credibility.