Resource: annotation_suggestions

Generated from docs/api_standards/resources/annotation_suggestions.md. Edit that file, then regenerate: python docs/portfolio/_build/generate_reference_pages.py.


Resource: annotation_suggestions

The first design sheet, written after its handlers rather than before — deliberately, as the worked example. Every claim below was read from the spec and the code, so this doubles as a check on whether the shipped design holds together. It found two things that do not (see Open questions).

Resource

A model-produced candidate annotation on one image, awaiting human disposition. Never an annotation: the separate table is the whole point (20260809_ai_annotation_suggestions.md D1). If model output could land in annotations, every existing reader of that table would become responsible for filtering it, and one missed filter silently promotes a guess to a recorded finding.

Identity: uuid. Scope: the parent image’s dataset.

Lifecycle

(analysis run) ──▶ pending ──▶ accepted   ──▶ becomes an annotation
                          └──▶ rejected
                          └──▶ recategorized ──▶ becomes an annotation

pending is the only mutable state. Disposition is append-only: a decision is recorded in annotation_review_decisions, and an accepted suggestion is promoted to an annotation in the same transaction — so a reviewer never sees a state where the decision exists and the annotation does not.

A re-analysis produces new rows. It does not mutate old ones.

Operations

Method · Path operationId Purpose Auth Idempotent
POST /images/{id}/analyze analyzeImage Run the skill against one image, recording the attempt Cookie, Bearer no
GET /images/{id}/suggestions listImageAnnotationSuggestions The recorded suggestions for one image Cookie, Bearer yes
GET /images/suggestions listImageSuggestions Suggestions across a dataset Cookie, Bearer yes
POST /annotation-review-decisions appendAnnotationReviewDecisions Accept / reject / recategorize Cookie, Bearer yes — same body, same end state
GET /annotation-categories listAnnotationCategories The category vocabulary a decision may assign Cookie, Bearer yes

There is deliberately no batch analyze. A selection is analyzed by calling the single-image route once per image, sequentially, capped at 25, in the browser while the tab is open. A POST /images/analyze taking a list would look like a job and behave like a long HTTP request; durable collection runs are a separate design (analysis_runs).

Access

dataset-scoped: a caller who can see the parent dataset can see its suggestions. Not-permitted answers 404, per CONTRACT-API-001 — a 403 would confirm the image exists.

appendAnnotationReviewDecisions is the audit boundary: it is the moment model output becomes a human-owned finding, and it is human-only. An engine holding a bearer may read suggestions; it may not dispose of them.

Errors

Code When Client action
auth.unauthenticated No usable identity Refresh once, then surface
image.not_found No such image visible to this caller Treat as absent — do not say “not yours”
request.missing_parameter Decision body incomplete Caller bug; do not retry
upstream.failed The analysis backend failed or timed out Retry once with backoff
server.internal Unhandled Retry once, then surface

Pagination & limits

listImageAnnotationSuggestions and listImageSuggestions take limit. Per CONTRACT-API-002 rule 5 the target for new list operations is limit+offset with a documented ceiling; these predate that and take limit alone, which is the plurality scheme. Converge when touched.

Consistency

analyzeImage returns after the attempt is recorded. Suggestions are readable immediately on return — this is not an eventually-consistent surface.

The property that matters to a client: a suggestion recorded here survives a reload; an IMAGE_ANALYSIS_RESULT overlay from a chat turn does not. They look identical in the viewer and have different lifetimes, which is the single most confusable thing about this resource.

Agent notes

An engine with a bearer may call the read operations. It must not call appendAnnotationReviewDecisions — see Access.

What a model needs told that a human would not: a suggestion is not a finding. A model summarising an image’s suggestions must not phrase them as what is wrong with the equipment; they are what a detector proposed and nobody has accepted. The UI carries this distinction visually (dashed boxes, a colour outside the category palette); prose has to carry it in words.

Rejected shapes

  • annotations with a source='ai' column. Rejected: it makes every existing annotation reader responsible for filtering, and one missed filter promotes model output to a human finding silently. The failure is invisible at the point it happens.
  • Deleting suggestions on rejection. Rejected: the rejection is the training signal, and a review loop that discards its negatives cannot measure whether the detector is improving.
  • A batch analyze endpoint. Rejected — see Operations.

Open questions

Both found by writing this sheet against the shipped spec, which is what a sheet is for.

  1. analyzeImage documents both 403 and 404. Its siblings document 404 only. Under CONTRACT-API-001 a dataset-scoped resource answers 404 for not-permitted, so either the 403 is a different condition that should be named, or it is a disclosure the rule forbids. Needs a handler read.
  2. listImageSuggestions takes both datasetId and datasetSlug. Two identifiers for one scope means two code paths and a question — what happens when both are supplied and disagree? The spec does not say.