feat(kernel): AnnotationService — schema-validated writes, attribute values, derived progress (#7) - #82
Merged
Merged
Conversation
…values, derived progress (#7) The one door to an Annotation. Every write is judged against the schema version its batch pinned at approval — never the project's active version — and the five refusals share an InvalidAnnotation base so a surface answers 422 without enumerating them. - domain: Annotation.attributes, keyed by Attribute.name exactly; Attribute.rejects as the single "does this attribute take this value" rule, shared with the default validator; progress_after_annotating as a pure domain function. - adapters: annotation.attributes JSON column, declared last and carrying a server_default because it arrives by ALTER in migration 5 (FORMAT_VERSION 4 -> 5). - services: JobService._require_job/_require_open_batch promoted to public uow-taking lookups, so the batch gate has one wording in both services. - delete carries no confirm=: removing a box is the annotator edit loop, and the batch gate is the guard.
JArmandoAnaya
added a commit
that referenced
this pull request
Aug 21, 2026
…values, derived progress (#7) (#82) The one door to an Annotation. Every write is judged against the schema version its batch pinned at approval — never the project's active version — and the five refusals share an InvalidAnnotation base so a surface answers 422 without enumerating them. - domain: Annotation.attributes, keyed by Attribute.name exactly; Attribute.rejects as the single "does this attribute take this value" rule, shared with the default validator; progress_after_annotating as a pure domain function. - adapters: annotation.attributes JSON column, declared last and carrying a server_default because it arrives by ALTER in migration 5 (FORMAT_VERSION 4 -> 5). - services: JobService._require_job/_require_open_batch promoted to public uow-taking lookups, so the batch gate has one wording in both services. - delete carries no confirm=: removing a box is the annotator edit loop, and the batch gate is the guard.
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.
The kernel's fifth service, and the one all the others exist to protect: until now an
annotation could only be written by reaching past the services straight into
uow.annotations. Vision-doc decision F says schema violations are a hard reject at writetime, in the kernel — so
AnnotationServiceis the one door, and every write is judgedagainst the schema version its batch pinned at approval.
Surface
get(annotation_id)Annotationfor_asset(job_id, asset_id)list[Annotation]add(job_id, annotations)list[Annotation]— stored, with ids and the pinupdate(job_id, annotations)list[Annotation]delete(job_id, annotation_ids)intEvery write is all-or-nothing: one transaction, everything validated before anything is
stored.
Decisions
schema_versionfromBatch.schema_version, the way it letsidgenerate itself.
updatedoes the same withasset_id— the stored one wins, becausemoving a label between assets is a delete and an add, not an edit.
progress_after_annotatingindomain/task.pyis the rule:unannotated ↔ annotatedand nothing else, becauseskipped/review_pending/acceptedare people's decisions and stay withJobService.mark. Applied through this service's own unit of work — callingJobService.markwould open a second session and write from it.deletehas noconfirm=. Deleting a box is the annotator edit loop, not lifecycledestruction; the batch gate is the guard. Documented as the exception where the standing
rule lives.
InvalidProvenance.provenance='model'needing amodel_refandconfidence ∈ [0, 1]are already validators on the model, so a bad annotation cannot be constructed andnever reaches a service. Proved with
pytest.raises(ValidationError).Also in here
Annotation.attributes— a new field, a new JSON column, and migration 5(
FORMAT_VERSION4 → 5). Declared last onAnnotationRowand carrying aserver_default,because
ALTER TABLEappends and SQLite refusesADD COLUMN … NOT NULLwithout a valuefor the rows already there.
_downgrade_to_version_onelearned its undo.Attribute.rejects— extracted from_default_matches_kind, so a value and a default arejudged by one rule rather than two that can drift.
JobService._require_job/_require_open_batchpromoted to public, uow-taking lookups(the
WorkspaceService.require_project_nameprecedent), soBatchNotInAnnotationhas onewording in both services.
tests/kernel/test_geometry.py: the geometryrule is per-class equality against
LabelClass.geometry, not membership inSchemaService.allowed_geometries(which is the union across classes and would let apolygon through under a bbox class).
LabelClass.allowed_geometriesnever shipped.docs/annotations.md, plus updates toREADME,jobs,schemasandpersistence.Checks
End-to-end, through nothing but services: open → project → schema → assets → batch → approve
→ start →
annotations.add(...)→ the assets reachannotatedwith nojobs.mark→ jobcompletes → batch completes.
Closes #7