7  Annotations

8 operations.

7.0.1 listAnnotationCategories

GET /api/annotation-categories

Lists the whole vocabulary of annotation categories, so a reviewer can pick one when the AI’s wording matches nothing in it.

Why it exists. ‡ Offer the vocabulary of problem classes a reviewer picks from, so a mark on a photograph is labelled from an agreed list rather than free text.

Called by — components/workspaces/category-editor.tsx · components/workspaces/condition-class-picker.tsx · modules/gallery/core/hooks/use-suggestion-review.ts · modules/gallery/overlays/components/image-viewer.tsx …and 1 more

Reached from — /datasets/<slug> · /w/data-explorer/anomalies/map · /w/data-explorer/assets · /w/data-explorer/categories · /w/data-explorer/insights · /w/integrity/assets …and 2 more

Every annotation category, for the reviewer choosing one when a suggestion’s label maps to none. Distinct from listDatasetAnnotationCategories, which summarizes the categories a dataset’s existing annotations already use, with counts and thumbnails. The vocabulary is per-organization (20260818160000_annotation_categories_organization_scope.sql): the read runs under the caller’s RLS and adds no filter of its own, so two callers in different organizations get different vocabularies from this request. This is one of the two vocabularies createEquipmentDamageMechanism grounds a finding in — the other is listDamageMechanisms’s API-571 catalog.

Authentication: CookieAuth, SupabaseAuth

Returns — limit · truncated · includeRetired · categories

Field names as the handler returns them; the source states no types for them.

Effect — reads annotation_categories

Parameters

Name In Required Type Notes
limit query no integer
includeRetired query no boolean

Responses

Status Meaning Body
200 The annotation vocabulary.
401 Not authenticated. Error
500 Server error. Error

7.0.2 createAnnotationCategory

POST /api/annotation-categories

Adds a condition class the shared vocabulary does not yet contain, so an inspection can name what it actually found rather than picking the nearest wrong label.

Why it exists. ‡ Let an engineer name a condition the catalog does not yet contain, so the vocabulary can grow to fit what inspections actually find.

Called by — components/workspaces/category-editor.tsx

Reached from — /w/data-explorer/categories

Creates an annotation category in one organization’s vocabulary (P4 of the free-findings proposal, under D2: create, rename, retire — no delete, no merge). Sets no coco_category_id — nullable since 20260825160000 precisely so this operation would not have to invent one. A name already used in that organization is a 409, case-insensitively. Authorized by annotation_categories_insert_policy, not by this handler (CONTRACT-API-001): is_org_editor_secure admits an organization’s admins and members and excludes viewers.

Authentication: CookieAuth, SupabaseAuth

Returns — category

Field names as the handler returns them; the source states no types for them.

Effect — writes (insert) annotation_categories

Request body

Field Type Required
organization_id string (uuid) no
name string no
parent_category string no
subcategory string no

Responses

Status Meaning Body
201 The class was created.
400 Missing or invalid organization_id or name. Error
401 Not authenticated. Error
403 Forbidden — caller lacks the member/admin role in that organization. Error
409 A class with that name already exists in the organization. Error
500 Server error. Error

7.0.3 amendAnnotationCategory

PATCH /api/annotation-categories/{id}

Renames a condition class, or retires one that should no longer be offered — keeping every annotation and finding already recorded in its terms.

Why it exists. ‡ Rename a condition class, or retire one that should no longer be offered, so a shared vocabulary can be corrected and pruned without deleting the history recorded in its terms.

Called by — components/workspaces/category-editor.tsx

Reached from — /w/data-explorer/categories

Renames or retires an annotation category (P4 of the free-findings proposal, under D2: create, rename, retire — no delete, no merge). THERE IS NO DELETE, deliberately: edm_observation_category_id_fkey is ON DELETE CASCADE, so deleting a class would take every finding grounded in it; retirement is reversible and destroys nothing. A RENAME does more than it looks — handle_annotation_category_update() rewrites images.tags across the class’s images. A class in another organization is absent rather than forbidden — 404. Authorized by annotation_categories_update_policy (is_org_editor_secure), not by this handler.

Authentication: CookieAuth, SupabaseAuth

Returns — category · changed

Field names as the handler returns them; the source states no types for them.

Effect — writes (update) annotation_categories

Parameters

Name In Required Type Notes
id path yes string (uuid)

Request body

Field Type Required
name string no
parent_category string no
subcategory string no
retired boolean no

Responses

Status Meaning Body
200 The class as it now stands. changed is false when nothing differed.
400 No amendable field given, or an invalid one. Error
401 Not authenticated. Error
403 Forbidden — caller can read the class but lacks the member/admin role. Error
404 No such class, or it belongs to an organization the caller is not a member of. Error
409 Another class in the organization already has that name. Error
500 Server error. Error

7.0.4 appendAnnotationReviewDecisions

POST /api/annotation-review-decisions

Records what a reviewer decided about one or more AI proposals — accept, reject, or recategorize — and, for the ones they accepted, turns them into real annotations in the same step. Decisions are added, never overwritten: a rejection later changed to an acceptance leaves both, with who did it and when. A proposal whose wording matches no category cannot be accepted until someone picks one.

Why it exists. ‡ Record what a reviewer decided about a machine suggestion — accept, reclassify or reject — and promote an accepted one into a real mark, so the decision is auditable and the detector can be measured against it.

Called by — modules/gallery/core/hooks/use-suggestion-review.ts

Reached from — /datasets/<slug> · /w/data-explorer/anomalies/map · /w/data-explorer/assets · /w/integrity/assets · /w/integrity/evidence-explorer

Appends what a reviewer decided about one or more annotation suggestions and, for an accept or a reclassify, promotes the suggestion to an annotation in the same transaction (the Supabase client cannot express a transaction itself, so the work happens in review_annotation_suggestions, a SECURITY INVOKER function — a transaction boundary, not a privilege boundary; the editor-tier insert policies on both tables still decide). Decisions are appended, never edited: a reject later changed to an accept leaves both rows with their actors and times. Refusals are per item and arrive with a 200 — a reviewer who accepted nine good boxes does not lose all nine because the tenth carried a label that maps to no category. Read recorded, not the status code.

Authentication: CookieAuth, SupabaseAuth

Returns — recorded · refused · results

Field names as the handler returns them; the source states no types for them.

Responses

Status Meaning Body
200 What happened to each decision. Includes the case where every one of them was refused: the request itself was understood.
400 Malformed body, an unknown decision word, or more than 200 decisions in one request. Error
401 Not authenticated. Error
500 The decisions could not be recorded. Error

7.0.5 listAnnotations

GET /api/annotations

Lists the annotations drawn on a specific photo.

Why it exists. ‡ List the marks recorded on photographs, with their classes resolved, for the panels that show what was found.

Called by — components/bounding-box-visualizer.tsx · modules/gallery/overlays/components/image-viewer.tsx · modules/gallery/overlays/components/pairs-viewer.tsx

Reached from — /datasets/<slug> · /w/data-explorer/anomalies/map · /w/data-explorer/assets · /w/integrity/assets · /w/integrity/evidence-explorer

Retrieves all annotations for a specific image, with each annotation’s category resolved to a name. imageId accepts a UUID or, for a caller that only has one, a filename — the route resolves either to the underlying image before reading its annotations. Runs as the caller: images is readable only from the caller’s organizations’ datasets, and annotations only from those images.

Authentication: CookieAuth, SupabaseAuth

Returns — annotations · count

Field names as the handler returns them; the source states no types for them.

Effect — reads annotation_categories · annotations · images

Parameters

Name In Required Type Notes
imageId query yes string

Responses

Status Meaning Body
200 A list of annotations for the image. Annotation
400 Bad request, imageId is required. Error
401 Not authenticated. Error
500 Server error. Error

7.0.6 reclassifyAnnotation

PATCH /api/annotations/{id}

Reclassifies one stored annotation to another category. Only an editor of the owning organization may do it; the act is written to an append-only edit log.

Why it exists. ‡ Re-label a mark on a photograph when the class it carries is wrong, keeping who changed it and when.

Called by — modules/gallery/overlays/components/stored-annotations-panel.tsx

Reached from — /datasets/<slug> · /w/data-explorer/anomalies/map · /w/data-explorer/assets · /w/integrity/assets · /w/integrity/evidence-explorer

Changes the category of one stored annotation (user-editable anomalies, Step A). Authorization is the database’s: the editor-scoped RLS UPDATE policy on public.annotations governs the write, so an annotation outside the caller’s organizations is a 404 and a non-editor who can read it is a 403. The act is recorded in the append-only annotation_edits log.

Authentication: CookieAuth, SupabaseAuth

Returns — id · category_id

Field names as the handler returns them; the source states no types for them.

Effect — writes (insert, update) annotation_categories · annotation_edits · annotations

Parameters

Name In Required Type Notes
id path yes string

Request body

Field Type Required
category_id string (uuid) no

Responses

Status Meaning Body
200 The annotation was reclassified.
400 category_id missing, or names no category. Error
401 Not authenticated. Error
403 The caller may read the annotation but is not an editor. Error
404 No such annotation the caller may read. Error
500 Server error. Error

7.0.7 deleteAnnotation

DELETE /api/annotations/{id}

Removes one stored annotation. The removal is recorded in the append-only edit log first — keeping the category, box and photo — so the audit survives the annotation. Editors only.

Why it exists. ‡ Remove a mark from a photograph, whether a person or the assistant put it there.

Called by — modules/gallery/overlays/components/stored-annotations-panel.tsx

Reached from — /datasets/<slug> · /w/data-explorer/anomalies/map · /w/data-explorer/assets · /w/integrity/assets · /w/integrity/evidence-explorer

Removes one stored annotation (user-editable anomalies, Step A). A remove record is written to the append-only annotation_edits log first, so the audit preserves the removed annotation’s category, bbox and image even after the row is gone. Authorization is the database’s: the editor-scoped RLS DELETE policy governs it, so an annotation outside the caller’s organizations is a 404 and a non-editor who can read it is a 403.

Authentication: CookieAuth, SupabaseAuth

Returns — id · deleted

Field names as the handler returns them; the source states no types for them.

Effect — writes (delete, insert) annotation_edits · annotations

Parameters

Name In Required Type Notes
id path yes string

Responses

Status Meaning Body
200 The annotation was removed.
401 Not authenticated. Error
403 The caller may read the annotation but is not an editor. Error
404 No such annotation the caller may read. Error
500 Server error. Error

7.0.8 createAnnotation

POST /api/images/{id}/annotations

Adds a new human-drawn anomaly to a photo from a rectangle and a category, recorded as a human annotation. Only an editor of the owning organization may do it; the act is written to an append-only edit log.

Why it exists. ‡ Let an inspector draw a problem on a photograph by hand, rather than only accepting what the assistant proposed.

Called by — modules/gallery/overlays/components/image-viewer.tsx

Reached from — /datasets/<slug> · /w/data-explorer/anomalies/map · /w/data-explorer/assets · /w/integrity/assets · /w/integrity/evidence-explorer

Creates a human-drawn annotation on the image from a normalized bounding box and a category (user-editable anomalies, Step B). The row is stamped source: 'human' and created_by. Authorization is the database’s: the editor-scoped RLS INSERT policy on public.annotations governs the write, so a caller who can see the image but is not an editor of its organization is a 403. The act is recorded in the append-only annotation_edits log.

Authentication: CookieAuth, SupabaseAuth

Returns — id · category_id · bbox

Field names as the handler returns them; the source states no types for them.

Effect — writes (insert) annotation_categories · annotation_edits · annotations

Parameters

Name In Required Type Notes
id path yes string

Request body

Field Type Required
bbox array of number no
category_id string (uuid) no
area number no

Responses

Status Meaning Body
200 The annotation was created.
400 bbox invalid, or category_id missing / names no category. Error
401 Not authenticated. Error
403 The caller may view the image but is not an editor. Error
500 Server error. Error