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-2065Done

11.3.8 The two RANKED work-item collections — `GET /api/v1/projects/{projectKey}/backlog` and `GET /api/v1/sprints/{sprintId}/work-items`, paged in `backlogRank` order

The two collections that answer "what is queued, and what did we commit to?" — and the only reads in 11.3 that are genuinely unbounded (this project's sprints have run to 117 issues and its backlog is far larger).

They ship as one card because they are one shape: backlogService.getBacklog and getSprintIssues both return RankedIssuePageDto, both window in the DATABASE, both order by backlogRank, both take the same optional filterAst, and both raise the same totalCount question. Two cards would mean writing the same cursor translation and the same envelope decision twice and letting them drift.

Both are already real database-level keyset reads — this is a true thin adapter

Unlike 11.2's work-item list (which had to add a keyset read), these two already do the right thing: findBacklogPage / findSprintIssues take { take, cursor, filter } and window in SQL, with a bounded COUNT alongside. Nothing new is needed at the service or repository layer. What the route owes is the translation between v1's signed, collection-scoped cursor (11.3.2) and the service's own cursor — the last row's id, in backlogRank order — and the RankedIssuePageDto → v1 envelope mapping.

Two shipped behaviours are contract and must be preserved rather than re-derived:

  • The backlog EXCLUDES done-category issues (backlogExcludedStatusKeys) — the backlog is the to-be-planned pile, matching the mirror. A sprint's members are NOT filtered that way: a done issue stays part of its sprint's scope. Same-shaped endpoints, deliberately different predicates. Say so in the module; a reader who assumes symmetry will "fix" one of them.
  • totalCount. Both reads compute it. Whether the v1 envelope carries it is 11.3.1 Q2's decision — implement whichever it records, and do not invent a third shape here.

Narrowing is the versioned FilterAST, decoded by the same codec /items and search_work_items ride and the shipped work-item collection route already uses — never an ad-hoc ?status=&assignee= axis. One query grammar; a second is the first place the API and the product start disagreeing about what a filter means.

Rows are the work-item summary shape 11.2.2 already pins (WorkItemSummaryDto is what both reads return) — this card adds no new work-item representation.

Acceptance criteria

  • Both endpoints exist, declare scope: 'read', and return the declared schema's output over the v1 list envelope.
  • A paged scan of a collection being written concurrently never skips or duplicates a row — the keyset property, driven with a real concurrent insert the way 11.1's pagination test does, on at least one of the two.
  • Paging preserves backlogRank order across page boundaries — asserted over a collection larger than one page, comparing the concatenated pages against the service's own order.
  • limit defaults to 50 and clamps to 100; a cursor issued by one of these two collections is refused (422) by the other.
  • The backlog omits done-category issues and the sprint read keeps them — asserted with the same fixture item in both, since this is the asymmetry a later reader is most likely to "correct".
  • The ?filter= parameter accepts the versioned FilterAST and produces the same narrowing the /items view does for an identical filter; an invalid filter is a 422 with the shipped code (INVALID_FILTER / UNSUPPORTED_FILTER_VERSION / FILTER_TOO_LARGE), not a 500.
  • totalCount is handled exactly as Amendment 3 Q2 records — carried, or documented as deliberately absent. Not both, and not silently dropped.
  • An empty backlog or an empty sprint is 200 with empty items, never a 404; an unknown projectKey / cross-workspace sprintId is a 404, not a 403.
  • No route calls Prisma or opens a transaction; each is key resolution plus one service call.
  • Unit tests ship with the routes; every new file holds the ≥90% per-file coverage floor.
  • ONE PR against motir-core.

Context refs

  • lib/services/backlogService.tsgetBacklog, getSprintIssues, backlogExcludedStatusKeys, resolveBacklogFilter, clampLimit, buildPage.
  • lib/repositories/workItemRepository.tsfindBacklogPage / findSprintIssues / countBacklog / countSprintIssues, the SQL-level windowing this adapts.
  • lib/dto/backlog.tsRankedIssuePageDto (items / nextCursor / totalCount).
  • lib/filters/ast.ts + lib/filters/registry.ts — the one query grammar and its field set.
  • app/api/v1/projects/[projectKey]/work-items/route.ts — the shipped route that already does this translation for a DB-windowed read, including parseFilterParam and its error codes.
  • lib/api/v1/workItems/schema.tspresentWorkItemSummary, the row shape both collections return.
  • Blockers: 11.3.1 (the totalCount + cursor contract), 11.3.2 (the cursor primitive), 11.3.4 (the /sprints/{sprintId} resource + its tenancy resolution). Parent story: 11.3.