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

11.3.4 The v1 SPRINT resource — its schema, the missing by-id service read, `GET /api/v1/projects/{projectKey}/sprints` and `GET /api/v1/sprints/{sprintId}`

The sprint as a v1 resource: the schema every sprint response in 11.3 returns, and the two reads. This is the card the sprint writes, the lifecycle moves and the membership moves all compose, so it lands before them.

⚠️ One endpoint here has NO service path — that is the substance of this card

Verified against origin/main @ 94a65035: sprintsService exposes createSprint / updateSprint / deleteSprint / startSprint / completeSprint / getActiveSprint / listByProject / validateSprint / getSprintReport — and no by-id read returning a SprintDto. Only sprintRepository.findById exists, a Prisma row ADR §9 forbids a route from touching.

getActiveSprint cannot stand in for it, and the reason is a live trap rather than a style objection: it returns toSprintDto(row, 0)issueCount hard-coded to 0. A GET /api/v1/sprints/{sprintId} built on it would report every sprint as empty, and nothing would fail: the field is present, well-typed, and wrong. listByProject is the read that gets it right, computing the count per row via workItemRepository.countSprintIssues.

So this card adds sprintsService.getById — the shipped repository read, the shipped toSprintDto mapper, the shipped workspaceId gate, and the real issue count — under the by-id re-presentation carve-out 11.3.1 Q3 records. It adds no field, no gate and no filter axis. If Amendment 3 did not admit that carve-out, stop and re-plan rather than adding the method anyway.

What to build

  • lib/api/v1/sprints/schema.ts — the zod sprint response schema + its field-by-field mapper (Amendment 2; the lib/api/v1/workItems/schema.ts pattern). state is a closed vocabulary and takes the satisfies + AssertTotal compile-time totality guard. The nullability is contract, not incidental: committedPoints and committedIssueCount are the immutable activation baseline startSprint snapshots, and they are null on a never-started sprint and — for committedPoints — on a started sprint that was wholly unestimated. Under §8, nullability cannot change later without a major, so the schema declares them nullable and the mapper never coerces a null to 0. A sprint id is a cuid and stays one on the wire: a sprint has no MOTIR-<n> key, which §7's identifier rule covers work items only (11.2.2 records the same exception for sprintId).
  • sprintsService.getById(id, ctx) — as above.
  • GET /api/v1/projects/{projectKey}/sprints — every sprint with state, window, goal, issue count and the baseline fields, in sequence order, cursor-paged via 11.3.2. listByProject is a bounded read (a project's sprints are a cadence, not a collection) that fans out one count per sprint; page over it in memory the way GET /api/v1/workspaces does. This is also the endpoint that answers "what is the active sprint?" — the row carries state, so no separate /sprints/active path is added.
  • GET /api/v1/sprints/{sprintId} — one sprint, through the new service read.

Both reads are read-scoped and, per the shipped services, available to any project member — the sprint-admin gate guards sprint MANAGEMENT writes, not reads.

Acceptance criteria

  • Both endpoints exist, declare scope: 'read', and return the declared zod schema's output — no inline shaping.
  • A never-started sprint returns committedPoints: null and committedIssueCount: null, not 0 — asserted directly, and separately from the started-but-unestimated case where only committedPoints is null.
  • A sprint holding N issues reports issueCount: N on BOTH endpoints — the regression test for the getActiveSprint hard-coded 0, asserted specifically against the single-sprint read, which is where that trap lives.
  • sprintsService.getById returns the same DTO shape as a listByProject row for the same sprint — asserted by reading one sprint through both paths and comparing, so the two reads cannot drift.
  • The sprint list pages by cursor in sequence order, defaults to 50, clamps at 100; an empty project is 200 with empty items.
  • A sprint in another workspace is a 404, not a 403, on the single-sprint read; an unknown projectKey is a 404 on the list.
  • No route calls Prisma or opens a transaction; sprintRepository is reached only through the service.
  • Unit tests ship with the code; every new file holds the ≥90% per-file coverage floor.
  • ONE PR against motir-core.

Context refs

  • lib/services/sprintsService.tslistByProject (the correct per-row count), getActiveSprint (the toSprintDto(row, 0) trap), and the absence this card fills.
  • lib/repositories/sprintRepository.tsfindById(id, workspaceId), the workspace-gated leaf getById wraps.
  • lib/dto/sprints.tsSprintDto, SprintStateDto, and the recorded meaning of the two committed* baseline fields.
  • lib/api/v1/workItems/schema.ts — the schema-module pattern, and its recorded sprintId keyless-identifier exception.
  • docs/decisions/public-api-conventions.md §7 (identifiers), §8 (nullability is not changeable later), Amendment 1's permitted/forbidden table + Amendment 3 Q3 (the carve-out this leans on).
  • Blockers: 11.3.1, 11.3.2. Parent story: 11.3.