Part of #202.
Build
Add an endpoint listing the captures of a project that await the reviewer.
Adding an endpoint is allowed; the OpenAPI source is the truth — edit
apps/server/api/src/ and regenerate, never the bundled
apps/server/api/openapi.yaml.
Today the client can ask for cases by state (listCases, filter state,
apps/server/api/src/paths/cases.yaml) and then for one case's grid
(getCaseCaptures, apps/server/api/src/paths/case-captures.yaml). Walking a
project's work therefore costs one request per case and returns whole grids to
find two captures. Nothing lists the captures themselves.
Shape. One GET under the project, taking an optional categoryId, and
returning the queue in the order the reviewer walks it: grouped by case, cases
in catalogue order, steps in position order, variants in label order. With a
category, the queue covers that category and every one beneath it — the tree is
of unrestricted depth (docs/adr/0014-server-generated-case-identity-and-catalogue-tree.md),
so the descent is recursive. Without one, the whole project. Each entry carries enough to open the carousel
on it and to say what it is waiting for — at minimum the case, the step, the
variant, and the capture with its status — to-review or moved, the two that
await the reviewer once #192 lands. No field says why beyond that status: a
delivered fix reads to-review like anything else, and its detail is read from
the comment. Reuse the existing evidence schemas
(apps/server/api/src/components/evidence.yaml) rather than inventing a
parallel vocabulary for the same things.
Read-only. The queue is computed from stored statuses at request time. It
stores nothing, and no endpoint accepts it as input
(docs/adr/0002-server-owns-the-review-lifecycle.md).
One query, no scan. The statuses are stored precisely so this filters
without walking every capture (docs/design/product.md §3.1). It belongs with
the other aggregate reads, apps/server/db/queries/aggregates.sql. Archived
cases are never listed, exactly as listCases already excludes them.
Layering is linter-enforced (docs/backend/adr/0001-hexagonal-layers.md): a
violation is fixed with an interface, never an import shortcut.
Validation
- The generated client exposes the operation and the web app can call it
without a hand-written type.
- A postgres adapter test builds a project holding both statuses — an unjudged
capture, a delivered fix (which reads to-review), a moved capture — plus
captures that must not appear: accepted with an unchanged image, refused
still with the dev, an archived case, a case at not-instrumented.
- The order is asserted, not incidental: two cases, several steps each, the
result grouped by case and ordered by step position.
- The scope is asserted at depth: a category two levels up returns its own
cases plus its descendants', and excludes a sibling branch's.
- A project with nothing to review answers an empty list, never an error.
Depends on #194
The queue selects on to-review and moved, and moved only exists as a
status once #194 lands.
Out of scope
Filtering beyond the category scope (reviewer, age, axis value), pagination
beyond what the queue naturally needs, and any write path. The carousel and its route are the web sub-issue.
Part of #202.
Build
Add an endpoint listing the captures of a project that await the reviewer.
Adding an endpoint is allowed; the OpenAPI source is the truth — edit
apps/server/api/src/and regenerate, never the bundledapps/server/api/openapi.yaml.Today the client can ask for cases by state (
listCases, filterstate,apps/server/api/src/paths/cases.yaml) and then for one case's grid(
getCaseCaptures,apps/server/api/src/paths/case-captures.yaml). Walking aproject's work therefore costs one request per case and returns whole grids to
find two captures. Nothing lists the captures themselves.
Shape. One
GETunder the project, taking an optionalcategoryId, andreturning the queue in the order the reviewer walks it: grouped by case, cases
in catalogue order, steps in position order, variants in label order. With a
category, the queue covers that category and every one beneath it — the tree is
of unrestricted depth (
docs/adr/0014-server-generated-case-identity-and-catalogue-tree.md),so the descent is recursive. Without one, the whole project. Each entry carries enough to open the carousel
on it and to say what it is waiting for — at minimum the case, the step, the
variant, and the capture with its status —
to-reviewormoved, the two thatawait the reviewer once #192 lands. No field says why beyond that status: a
delivered fix reads
to-reviewlike anything else, and its detail is read fromthe comment. Reuse the existing evidence schemas
(
apps/server/api/src/components/evidence.yaml) rather than inventing aparallel vocabulary for the same things.
Read-only. The queue is computed from stored statuses at request time. It
stores nothing, and no endpoint accepts it as input
(
docs/adr/0002-server-owns-the-review-lifecycle.md).One query, no scan. The statuses are stored precisely so this filters
without walking every capture (
docs/design/product.md§3.1). It belongs withthe other aggregate reads,
apps/server/db/queries/aggregates.sql. Archivedcases are never listed, exactly as
listCasesalready excludes them.Layering is linter-enforced (
docs/backend/adr/0001-hexagonal-layers.md): aviolation is fixed with an interface, never an import shortcut.
Validation
without a hand-written type.
capture, a delivered fix (which reads
to-review), a moved capture — pluscaptures that must not appear:
acceptedwith an unchanged image,refusedstill with the dev, an archived case, a case at
not-instrumented.result grouped by case and ordered by step position.
cases plus its descendants', and excludes a sibling branch's.
Depends on #194
The queue selects on
to-reviewandmoved, andmovedonly exists as astatus once #194 lands.
Out of scope
Filtering beyond the category scope (reviewer, age, axis value), pagination
beyond what the queue naturally needs, and any write path. The carousel and its route are the web sub-issue.