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— thezodsprint response schema + its field-by-field mapper (Amendment 2; thelib/api/v1/workItems/schema.tspattern).stateis a closed vocabulary and takes thesatisfies+AssertTotalcompile-time totality guard. The nullability is contract, not incidental:committedPointsandcommittedIssueCountare the immutable activation baselinestartSprintsnapshots, and they arenullon a never-started sprint and — forcommittedPoints— 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 to0. A sprint id is a cuid and stays one on the wire: a sprint has noMOTIR-<n>key, which §7's identifier rule covers work items only (11.2.2 records the same exception forsprintId).sprintsService.getById(id, ctx)— as above.GET /api/v1/projects/{projectKey}/sprints— every sprint with state, window, goal, issue count and the baseline fields, insequenceorder, cursor-paged via 11.3.2.listByProjectis 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 wayGET /api/v1/workspacesdoes. This is also the endpoint that answers "what is the active sprint?" — the row carriesstate, so no separate/sprints/activepath 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 declaredzodschema's output — no inline shaping. - A never-started sprint returns
committedPoints: nullandcommittedIssueCount: null, not0— asserted directly, and separately from the started-but-unestimated case where onlycommittedPointsis null. - A sprint holding N issues reports
issueCount: Non BOTH endpoints — the regression test for thegetActiveSprinthard-coded0, asserted specifically against the single-sprint read, which is where that trap lives. sprintsService.getByIdreturns the same DTO shape as alistByProjectrow 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
sequenceorder, defaults to 50, clamps at 100; an empty project is 200 with emptyitems. - A sprint in another workspace is a 404, not a 403, on the single-sprint read; an unknown
projectKeyis a 404 on the list. - No route calls Prisma or opens a transaction;
sprintRepositoryis 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.ts—listByProject(the correct per-row count),getActiveSprint(thetoSprintDto(row, 0)trap), and the absence this card fills.lib/repositories/sprintRepository.ts—findById(id, workspaceId), the workspace-gated leafgetByIdwraps.lib/dto/sprints.ts—SprintDto,SprintStateDto, and the recorded meaning of the twocommitted*baseline fields.lib/api/v1/workItems/schema.ts— the schema-module pattern, and its recordedsprintIdkeyless-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.