📦 WP-099 — feat(data): YOLO-format dataset reader¶
Engineering log · Phase 9 — Inference and generalization
🔗 Depends on WP-098 · Needed by WP-099b
WP-099 — a format with no specification¶
The clean-room rule says the reader is written from the format as the datasets publish it, never from an implementation's reader. That is a clear instruction with a hole in the middle: the YOLO label format has no specification on the paper allowlist, and the obvious ways to learn it — a search, a docs page, somebody's loader — are all the thing the rule forbids. What closed it was a published dataset export already on the machine, CC BY 4.0, a data artifact with no implementation lineage: registered as R32, and read for what it actually contains rather than what a tutorial says it contains.
That got four facts and left four gaps, and the gaps are the useful part. The export's fields lie in (0, 1] and its clipped corners land on exactly 0 and 1; names is an index-ordered list beside nc; split entries are written ../train/images for a tree at <root>/train/images; every image has a label file and none is empty. What it could not settle: whether a ten-field oriented row exists (R18 appends a difficult flag to its own label lines, and the normalized variant has no published spelling for one), whether data.yaml may carry a path: key, which of the two directory spellings ranks first when a root satisfies both, and which image extensions are in scope. Each of those is rejected or documented as a precedence choice rather than guessed — A54 through A58 — because a guess that silently mis-parses is exactly the failure the clean-room clause exists to prevent, and it fails quietly by producing a dataset rather than an error.
Two decisions where the format's own limits set the line. A missing label file is an error; an empty one is a background image — writing zero bytes is a positive statement that the image holds no objects, while not writing one is silence, and a tree with no manifest cannot tell silence from a half-finished download. Reading silence as "no objects" turns a broken export into a dataset that trains on backgrounds without ever saying so. A coordinate outside [0, 1] is rejected, not clamped, and compared with no epsilon — it is not a slightly-off box but a file that was never normalized, typically written in pixels, and clamping turns it into a wall of degenerate edge boxes that would then be trained on.
The layout question was the one that looked smallest and was not. resolve_split requires a directory and an annotation file, and a YOLO split's annotations are a directory. Folding YOLO into the existing CANDIDATES would force that predicate to accept either kind of thing, and a root satisfying both conventions would then resolve by table order — handing a labels tree to the COCO reader, or an instances_*.json to this one. Which convention applies is a property of the reader asking, so the reader asks its own table.
What did not land: the datamodule still does not dispatch between the two readers, so a YOLO root is reachable from a library call and not yet from lucid-yolo fit. Recorded as 099b rather than absorbed into the row, because a package that reports itself done while a clause of its scope is unbuilt is how a roadmap stops describing the code.
Three details of the shipped surface, kept out of the roadmap row for length. The reader produces the same Targets container the COCO path does, which is the property that leaves assignment, loss and metric code untouched by a second dataset format entirely — the format question stops at the loader. Class names and split paths come from the dataset's own data.yaml rather than from a config, so a YOLO root carries its own label space. And a layout row was added alongside, so --data.data_root resolves a YOLO root the way it resolves every other, through resolve_yolo_split and from_root.
The clean-room instruction this was written under is AGENTS.md's prime directive, cited here because the register rows (A54-A58) record what was unknown and not what forbade looking it up.