You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
No layer compares an annotation's geometry to the bounds of the asset it is on. The kernel's write gate — _validate in src/visionset/kernel/services/annotation_service.py — checks that the class exists in the pinned schema version, that the geometry's kind matches what that class declares, and that the attributes are declared, required-if-required and well-typed. It never looks at where the shape is. Nothing downstream does either: an annotation whose box lies entirely off the picture is storable today, and it exports.
This surfaced while closing the same hole one door over. POST /inference/suggest used to accept prompt points at any coordinate; #520 recorded the decision to refuse them and #523 shipped it, with the editor's own half having landed earlier in #514. Prompts are now bounded at both ends, and the question of what a stored shape may claim was deliberately left out of that change.
Nothing in the current flow produces such an annotation. The editor draws on the asset, and suggest is bounded. This is a latent gap in what the kernel will accept rather than an active defect, which is why it is filed as a decision and not as a fix.
Why the line is not obvious
A partially out-of-frame shape is legitimate and common: an object cut by the edge of the frame has a true extent that continues past it, and annotators are routinely taught to draw the whole thing. A fully disjoint shape — no overlap with the picture at all — is storable garbage under any reading. So the rule cannot be "geometry must fit", and any check has to be about disjointness rather than containment.
Assets complicate it further. Asset.width and Asset.height are both int | None, so an asset whose dimensions were never recorded has nothing to be checked against — the same nuance #523 handled for prompts by skipping the check rather than refusing, on the grounds that refusing punishes the caller for a gap in the asset's own metadata.
And the answer touches three surfaces whose costs differ:
The write gate. Refusing fully-disjoint geometry is the cheapest place to enforce it and the only one that keeps bad data out of the store, but it is also the surface where a wrong rule is most expensive: every client, importer and future tool inherits the refusal.
Importers, which arrive with external-dataset support in 0.2.0 (0.2.0 scope: import of external datasets #411). External datasets carry everything, including coordinates in another convention. Refusing at import has quite different ergonomics from refusing at annotation time — one blocks a person mid-gesture, the other fails a batch job somebody has to triage.
Exporters. What each format does with a shape hanging off the frame is already a question the format capability model can express: clamp it to the frame, carry it as-is, or declare it as loss under the lossy capability and the allow_lossy consent gate that goes with it.
Options
(a) The write gate refuses fully-disjoint geometry only. Partial stays legal, assets of unrecorded size are skipped, and importers and exporters decide their own halves when those surfaces are designed. Keeps the invariant where the data enters and commits to the narrowest rule that is defensible.
(b) No write-gate change; the invariant is enforced at export or release time as a declared check. The store stays permissive and the problem is caught where a dataset is being handed to somebody else, alongside the checks that already run there.
(c) Document the absence as intended. The kernel stores what the caller asserts, and bounds are a client concern — which is a coherent position given that the two clients that exist are both already bounded.
No layer compares an annotation's geometry to the bounds of the asset it is on. The kernel's write gate —
_validateinsrc/visionset/kernel/services/annotation_service.py— checks that the class exists in the pinned schema version, that the geometry's kind matches what that class declares, and that the attributes are declared, required-if-required and well-typed. It never looks at where the shape is. Nothing downstream does either: an annotation whose box lies entirely off the picture is storable today, and it exports.This surfaced while closing the same hole one door over.
POST /inference/suggestused to accept prompt points at any coordinate; #520 recorded the decision to refuse them and #523 shipped it, with the editor's own half having landed earlier in #514. Prompts are now bounded at both ends, and the question of what a stored shape may claim was deliberately left out of that change.Nothing in the current flow produces such an annotation. The editor draws on the asset, and suggest is bounded. This is a latent gap in what the kernel will accept rather than an active defect, which is why it is filed as a decision and not as a fix.
Why the line is not obvious
A partially out-of-frame shape is legitimate and common: an object cut by the edge of the frame has a true extent that continues past it, and annotators are routinely taught to draw the whole thing. A fully disjoint shape — no overlap with the picture at all — is storable garbage under any reading. So the rule cannot be "geometry must fit", and any check has to be about disjointness rather than containment.
Assets complicate it further.
Asset.widthandAsset.heightare bothint | None, so an asset whose dimensions were never recorded has nothing to be checked against — the same nuance #523 handled for prompts by skipping the check rather than refusing, on the grounds that refusing punishes the caller for a gap in the asset's own metadata.And the answer touches three surfaces whose costs differ:
lossycapability and theallow_lossyconsent gate that goes with it.Options
(a) The write gate refuses fully-disjoint geometry only. Partial stays legal, assets of unrecorded size are skipped, and importers and exporters decide their own halves when those surfaces are designed. Keeps the invariant where the data enters and commits to the narrowest rule that is defensible.
(b) No write-gate change; the invariant is enforced at export or release time as a declared check. The store stays permissive and the problem is caught where a dataset is being handed to somebody else, alongside the checks that already run there.
(c) Document the absence as intended. The kernel stores what the caller asserts, and bounds are a client concern — which is a coherent position given that the two clients that exist are both already bounded.