Skip to content

feat(kernel): AnnotationService — schema-validated writes, attribute values, derived progress (#7) - #82

Merged
JArmandoAnaya merged 1 commit into
mainfrom
feat/annotation-service
Jul 27, 2026
Merged

feat(kernel): AnnotationService — schema-validated writes, attribute values, derived progress (#7)#82
JArmandoAnaya merged 1 commit into
mainfrom
feat/annotation-service

Conversation

@JArmandoAnaya

Copy link
Copy Markdown
Contributor

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 write
time, in the kernel — so AnnotationService is the one door, and every write is judged
against the schema version its batch pinned at approval.

Surface

Method Returns
get(annotation_id) Annotation
for_asset(job_id, asset_id) list[Annotation]
add(job_id, annotations) list[Annotation] — stored, with ids and the pin
update(job_id, annotations) list[Annotation]
delete(job_id, annotation_ids) int

Every write is all-or-nothing: one transaction, everything validated before anything is
stored.

Decisions

  • The service stamps schema_version from Batch.schema_version, the way it lets id
    generate itself. update does the same with asset_id — the stored one wins, because
    moving a label between assets is a delete and an add, not an edit.
  • Progress is derived in the same transaction. progress_after_annotating in
    domain/task.py is the rule: unannotated ↔ annotated and nothing else, because
    skipped / review_pending / accepted are people's decisions and stay with
    JobService.mark. Applied through this service's own unit of work — calling
    JobService.mark would open a second session and write from it.
  • delete has no confirm=. Deleting a box is the annotator edit loop, not lifecycle
    destruction; the batch gate is the guard. Documented as the exception where the standing
    rule lives.
  • No InvalidProvenance. provenance='model' needing a model_ref and confidence ∈ [0, 1] are already validators on the model, so a bad annotation cannot be constructed and
    never reaches a service. Proved with pytest.raises(ValidationError).

Also in here

  • Annotation.attributes — a new field, a new JSON column, and migration 5
    (FORMAT_VERSION 4 → 5). Declared last on AnnotationRow and carrying a server_default,
    because ALTER TABLE appends and SQLite refuses ADD COLUMN … NOT NULL without a value
    for the rows already there. _downgrade_to_version_one learned its undo.
  • Attribute.rejects — extracted from _default_matches_kind, so a value and a default are
    judged by one rule rather than two that can drift.
  • JobService._require_job / _require_open_batch promoted to public, uow-taking lookups
    (the WorkspaceService.require_project_name precedent), so BatchNotInAnnotation has one
    wording in both services.
  • Corrected the stale comment on issue kernel: AnnotationService — schema-validated writes (hard reject: unknown class, disallowed geometry, missing required attribute), provenance, UUID identity #7 in tests/kernel/test_geometry.py: the geometry
    rule is per-class equality against LabelClass.geometry, not membership in
    SchemaService.allowed_geometries (which is the union across classes and would let a
    polygon through under a bbox class). LabelClass.allowed_geometries never shipped.
  • docs/annotations.md, plus updates to README, jobs, schemas and persistence.

Checks

uv run ruff format . && uv run ruff check .   All checks passed!
uv run mypy src/visionset/kernel             Success: no issues found in 40 source files
uv run lint-imports                          Contracts: 2 kept, 0 broken.
uv run pytest                                429 passed, 1 warning in 66.46s
uv run python scripts/export_openapi.py      no diff in openapi.json
pnpm version:check                           All frontend packages are at 0.0.1-dev.0.

End-to-end, through nothing but services: open → project → schema → assets → batch → approve
→ start → annotations.add(...) → the assets reach annotated with no jobs.mark → job
completes → batch completes.

Closes #7

…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
JArmandoAnaya merged commit 4b6b70a into main Jul 27, 2026
3 checks passed
@JArmandoAnaya
JArmandoAnaya deleted the feat/annotation-service branch July 27, 2026 05:35
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant