fix(formats): dropped is not degraded, and one word for both was a lie (#158) - #165
Merged
Merged
Conversation
#158) `check_export` reported polygon classes as excluded from a YOLO or VOC export while both exporters wrote those polygons anyway, as their axis-aligned bounding boxes. A user was told two annotations would be lost, consented, and received two extra boxes in their training labels under the polygon's own class name — well formed in every way a validator can check, and not what the report promised. `Exporter.supported_geometries` carried one meaning and was read with two intents, each internally consistent: `_compatibility` read "not supported" as *absent from the output*; the exporters read it as *convert to something I can write*, which is the capability #62 and #64 deliberately included. The model had no word for **carried, but reduced**. Option B, as decided on the issue: - `Exporter` gains `degraded_geometries` — `{polygon}` for yolo and voc, empty for coco (which writes a polygon as a polygon) and dummy (which writes nothing). - `ClassCompatibility.supported: bool` becomes `status`, one of `supported` / `degraded` / `dropped`; `supported` and `carried` survive as properties, so no report can carry a pair that disagree. - `excluded_annotations` / `excluded_assets` count **dropped only**, with `degraded_annotations` / `degraded_assets` beside them. Every `reason` now says what happens to that class rather than "cannot write" for a class that is written. - `compatible` is still false for either, so the `allow_lossy` gate did not move. Only the accounting became true. The guard ships with it, and it is the part that was missing: a new `tests/formats/test_report_agreement.py` exports one release holding all three geometries through **every installed format** and counts the annotations in the written label files, XML documents and COCO JSON — asserting the report against the bytes rather than against an expected number. Verified by mutation: restoring yolo's old declaration turns three named tests red. A fourth exporter either lands a counter there or is declared non-writing, so the divergence cannot come back unnoticed. `GET /formats` publishes `degraded_geometries` for the same reason at that surface. `openapi.json` and the generated client are regenerated; `FORMAT_VERSION` stays 12 and no migration is added. `docs/releases.md` records the distinction and the argument, and `docs/tutorial.md` shows the two CLI lines. Closes #158
This was referenced Aug 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #158. First of the
0.0.1-beta.2defect fixes, and the one that silently corrupts a training set.The defect
check_exportreported polygon classes as excluded from a YOLO or VOC export. Both exporters then wrote those polygons anyway, as their axis-aligned bounding boxes. A user consented to losing two annotations and received two extra boxes in their training labels under the polygon's own class name — every row well-formed, every class index real, every coordinate in range, and not what the report said.Exporter.supported_geometriescarried one meaning and was read with two intents, both defensible:_compatibilityread "not insupported_geometries" as will not be present in the output.yolo/_as_boxandvoc/_as_boxread it as convert to something I can write — the capability formats: YOLO exporter — rewritten with v1's math as reference, classes from the schema (not snapshots), read errors never swallowed #62 and formats: VOC exporter (if budget allows; otherwise post-beta) #414 deliberately included, documented in both module docstrings.So this was never a typo. The model was missing a state: there was no vocabulary for carried, but degraded.
The decision
Option B, argued on the issue: give the port a third state rather than delete the conversion. Option A would have made the report true by removing polygon-derived boxes from YOLO and VOC, which is a real detection-training use.
What changed
Exporterdegraded_geometries—{polygon}foryolo/voc, empty forcoco(writes a polygon as a polygon) anddummy(writes nothing). Disjoint fromsupported_geometries;supportedwins if a plugin says both.ClassCompatibilitysupported: bool→status:supported/degraded/dropped.supportedandcarriedsurvive as properties, so no report carries a pair that disagree.ExportCompatibilityexcluded_annotations/excluded_assetscount dropped only; newdegraded_annotations/degraded_assets; newdegradedproperty besideexcluded.reason"yolo writes a polygon as its bounding box; the shape is lost"versus"yolo cannot place a classification_tag and drops it".compatibleallow_lossygate did not move. Only the accounting became true.GET /formatsdegraded_geometries, becausegeometries: ["bbox"]alone reads as the whole answer and foryoloit left out that a polygon is written at all.visionset exportThe kernel does not know how a plugin reduces a geometry, so the specific sentence is written only when the format's own declarations make it inevitable (it reduces a shape and can write a box). Anything else gets the general sentence rather than a guess.
The guard, which is the point
tests/formats/test_report_agreement.py(new) exports one release holding 3 boxes, 2 polygons and 1 tag through every installed exporter and counts what each actually wrote — label rows resolved throughdata.yaml's own index,<object>elements by<name>, COCO instances bycategory_id. It asserts:excluded_annotationsequals everything held minus everything on disk — arithmetic over the artifacts, never a restated expectation;reasondoes not say "cannot".Verified by mutation: restoring yolo's old declaration (
degraded_geometries = frozenset()) turns three named tests red, including the two acceptance-criterion ones.COUNTERSplus a declaredNON_WRITINGset must cover every registered format, so a fourth exporter either gets compared against its own output or is deliberately exempted — criterion 5.Ledger
FORMAT_VERSIONstays 12;VERSIONstays0.0.1b1.openapi.jsonandfrontend/ui-core/src/generated/api.tsregenerated together, never hand-edited. This is the milestone's one permitted spec move.ClassCompatibilityOut.supported→status, andExportCompatibilityOutgains two fields. Noted inCHANGELOG.md.Checks run locally
uv run pytest tests/— exit 0, 2059 collecteduv run mypy src/visionset— no issues, 116 filesuv run ruff check/ruff format— cleanuv run lint-imports— 2 contracts kept, 0 brokenpnpm -r build,pnpm test(711 annotator + 107 ui-core vitest, 22 node:test),pnpm -r lint,pnpm -r typecheck— all greenDocs
docs/releases.mdgains "Dropped is not degraded, and one word for both was a lie" with the argument and the guard, so it is not re-litigated;docs/tutorial.mdshows both CLI lines;CHANGELOG.mdgains an Unreleased entry.