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
annotationswith asource='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.
analyzeImagedocuments 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.listImageSuggestionstakes bothdatasetIdanddatasetSlug. 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.