Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-2038Done

11.2.1 Decision — amend the `/api/v1` ADR: keyset paging over an offset-only service read, and per-resource zod RESPONSE schemas as the wire contract

Two questions block every endpoint in this story, and both are CONVENTION questions the 11.1.1 ADR either answers wrongly or does not answer at all. Its own Consequences section says a convention found wrong later is an ADR amendment card, never a per-endpoint deviation — so this is that card. docs/decisions/public-api-conventions.md is amended in place (11.1.1 is done and is never re-opened); the amendment is dated, evidenced and carries its rejected alternatives, exactly like the original.

Question 1 — the flagship list endpoint cannot satisfy §5 and §9 at the same time

§5 mandates keyset cursors and explicitly rejects offset pagination ("simpler to implement and impossible to make correct"). §9's corollary forbids v1 adding a service or repository ("v1 is a new PRESENTATION of existing capability"). Verified on origin/main @ c4ec51b1, both cannot hold for GET /api/v1/projects/{projectKey}/work-items:

  • workItemsService.getProjectIssuesList is 1-based LIMIT/OFFSET ({ limit: pageSize, offset } into workItemRepository.findProjectIssuesFlat) and returns { items, total, page, pageSize }.
  • It clamps page size to ISSUE_LIST_PAGE_SIZE = 50 (lib/issues/issueListView.ts), below the ADR's own 100 ceiling — so a v1 client asking limit=100 would silently receive 50, a contract the wrapper's parsePageRequest says it honours.
  • paginateKeyset (lib/api/v1/pagination.ts) takes a fully-read collection and slices it in memory. Correct for GET /api/v1/workspaces (a user's own memberships); catastrophic for a project's work items — the Motir project alone holds 1800+.
  • grep over lib/repositories/ finds no keyset/cursor read of work items anywhere; the cursor-paged repositories are attachments, notifications, comments and saved filters.

Options weighed

  • (a) Wrap the offset page in an opaque cursor, as lib/mcp/searchCursor.ts does for search_work_items. Rejected. That cursor wraps a page NUMBER, which is legitimate there because the tool must page identically to the offset-paged /items view it claims parity with. v1 has no such constraint, and adopting it would reintroduce the skip/duplicate defect §5 rejects — on the busiest, most-written collection in the product — while making the word "cursor" mean two different things inside one API.
  • (b) Read every row, then paginateKeyset in memory. Rejected — it is the unbounded read this story's own completeness criterion forbids, and finding #57's prototype-thinking tell.
  • (c) Add a keyset-paged read at the repository + service layer over the SAME filter predicate. RECOMMENDED.

The amendment. §9's bar on a new service/repository is a bar on new behaviour — a new filter axis, a new gate, a new field, a new write. A page ADDRESSING over an unchanged predicate is not new behaviour: the result SET is byte-for-byte the set the /items view already returns for the same filter; only how a page within it is named changes. So the carve-out is recorded and BOUNDED: a v1-added read may reuse an existing predicate with a different page addressing (including an optional page-SIZE parameter where the shipped read has a fixed one), and may do nothing else — no new filter axis, no new access gate, no new response field, no migration. Anything beyond that remains a card in the owning feature's epic, exactly as §9 says.

Question 2 — where the v1 RESPONSE shapes live, and who owns them

The two shipped routes shape their rows inline (GET /api/v1/me: "the response is shaped explicitly rather than spread, because verify returns the raw Prisma User row and a public API must never leak one"). That instinct is right and must survive; the inline FORM does not scale to this story's ten endpoints, which return the same work-item shape from list, detail, create, update, transition and archive.

11.4 currently claims "one schema module per resource" as its own deliverable. That direction is backwards and is recorded here as corrected: it would have 11.2 and 11.3 ship routes with ad-hoc inline shapes and then retrofit every one of them — which is precisely the drift 11.4 exists to prevent, performed deliberately.

Decision: each resource story ships the zod RESPONSE schemas next to its own routes; 11.4 ASSEMBLES. 11.4 owns the shared envelope/error/pagination schemas, the OpenAPI 3.1 emission, the route↔spec CI guard, the published reference and the stability policy — it does not author per-resource shapes. Rung 1: this is how every schema-first HTTP stack does it (zod-to-openapi, FastAPI, NestJS) — the schema is declared with the operation, the document is assembled from the operations. The corollary is the load-bearing half: a v1 response is a v1 schema's output, never a service DTO passed through. A service DTO is internal and changes freely for the web app's needs; §8's additive-only promise cannot ride something nobody promised to keep still.

Scope BOUNDARY

Docs only — this card writes no route, no schema and no test, and changes no behaviour. It records what 11.2.2 and its siblings then implement. It does not re-open 11.1.1 and does not re-decide anything §1–§8 already settled.

Acceptance criteria

  • docs/decisions/public-api-conventions.md carries a dated amendment section recording BOTH decisions, each with its rung-1 / rung-2 evidence and its rejected alternatives, in the shape the original ADR uses.
  • The amendment states the read carve-out's bounds explicitly — reuse of an existing predicate with a different page addressing, and nothing else (no new filter axis, no new gate, no new response field, no migration) — so it cannot be read as licence for v1 to grow behaviour at the edge.
  • §5 and §9 each carry an in-place pointer to the amendment, so a reader arriving at either clause cannot act on the superseded reading.
  • The amendment names the schema OWNERSHIP split across this story, 11.3 and 11.4 in one sentence each, and states that a v1 response is never a service DTO passed through — citing app/api/v1/me/route.ts as the shipped precedent for explicit shaping.
  • Every claim about shipped code in the amendment cites the file it was verified in (getProjectIssuesList's offset paging, the 50-row clamp, paginateKeyset's in-memory slice, the absent keyset repository read).
  • No file outside docs/decisions/ changes.

Context refs

  • docs/decisions/public-api-conventions.md — the ADR amended, §5 (pagination) and §9 (architecture) in particular.
  • lib/services/workItemsService.tsgetProjectIssuesList, its offset window and clampIssuePageSize.
  • lib/issues/issueListView.tsISSUE_LIST_PAGE_SIZE = 50, the clamp that conflicts with the ADR's 100 ceiling.
  • lib/api/v1/pagination.tsparsePageRequest / paginateKeyset, and the comment recording the deliberate divergence from searchCursor.ts.
  • lib/mcp/searchCursor.ts — the page-number cursor rejected for v1, and why it is right where it lives.
  • app/api/v1/me/route.ts · app/api/v1/workspaces/route.ts — the shipped inline-shaping precedent this generalises.
  • The ADR's author card: 11.1.1. The pagination card it amends: 11.1.3.
  • Downstream of the schema-ownership half: 11.4 · 11.3. Parent story: 11.2.