MOTIR-237Done
4.4.4 Backend — `getSprintReport` (completed/incomplete lists + points via 4.3 roll-up + scope-change, bounded)
Estimate: 22m · Depends on: 4.4.3, 4.3.3
The sprint report read — what got done vs. what did not — built to real-product SCALE (finding #57: bounded aggregates + paginated lists, never load-all). Powers the complete modal's success state (4.4.6) and the standalone closed-sprint report view.
sprintsService.getSprintReport(sprintId, { completedCursor?, incompleteCursor?, limit }) → a SprintReportDto:
- Points summary —
{ committed, completed, notCompleted }:committed= the lockedcommittedPointsbaseline (4.4.2);completed=SUM(storyPoints)over the sprint's done-category issues;notCompleted= the remainder — REUSE Story 4.3.3rollupForSprint(the bounded grouped aggregate) rather than re-summing. Unestimated → the DTO returns the numbers (0/null), the UI renders "—" (the data layer stays total — the 4.5.2 pattern). - Counts —
{ completedCount, incompleteCount }from grouped aggregates scoped to the sprint (viagetTerminalStatusKeys), NOT a loaded-page sum. - Issue lists —
completed: WorkItemSummaryDto[]+incomplete: WorkItemSummaryDto[], each cursor-paginated (the first bounded page + anextCursor); a real sprint can hold hundreds of issues, so the report shows the counts + a bounded page and a "view all" deep-link to the/issuesnavigator (Story 2.5) filtered to the sprint (the 4.2 "View all issues" mirror), never a full in-report dump. - Scope change —
{ addedAfterStart }: the count of issues associated with the sprint AFTERstartDate(derived from the 1.4.6 revision trail — the immutablecommittedIssueCountbaseline anchors it), the Jira "issues added during sprint" figure. Bounded (an aggregate over revisions, not a load-all). - Works for a
completesprint (the report) AND anactivesprint (a live preview the complete modal can show before confirming). Enforce the finding-#26workspaceIdgate.
Route: GET /api/sprints/[id]/report — HTTP-only, one service call. Mapper in lib/mappers/sprintMappers.ts → SprintReportDto in lib/dto/sprints.ts.
Acceptance criteria
getSprintReportreturns aSprintReportDtowith the points summary (committed = the locked baseline; completed/notCompleted viarollupForSprint), the completed/incomplete counts (grouped aggregates, not page sums), the cursor-paginated completed + incomplete issue lists (first bounded page + nextCursor + a "view all" deep-link target), and theaddedAfterStartscope-change count (from the 1.4.6 revisions vsstartDate).- An unestimated sprint returns 0/null points (the DTO stays total; the UI owns the "—"); the report works for both a completed and an active sprint; the lists are NEVER an unbounded dump (finding #57).
GET /api/sprints/[id]/reportis HTTP-only (one service call); DTOs mapped inlib/mappers/sprintMappers.ts; the finding-#26workspaceIdgate covers the reads.pnpm test:coveragekeeps the changed service/mapper files ≥90% branch/fn/line (the coverage gate).
Context refs
- Story 4.4.3 (
completeSprint— setscompletedAt+ the done/unfinished split) + 4.4.2 (thecommittedPoints/committedIssueCountbaseline) — the data this reports on - Story 4.3.3
rollupForSprint(sprintId)(committed/completed/remaining points, bounded aggregate) — the points source to REUSE, not re-sum lib/services/workflowsService.tsgetTerminalStatusKeys— the done-category split; the 1.4.6workItemRevisionsService— the sprint-association revisions the scope-change count reads- Story 2.5
/issuesnavigator — the "view all" deep-link target (filtered to the sprint); finding #57 (bounded aggregates + paginated lists);motir-core/CLAUDE.md(service owns DTO mapping);motir-core-coverage-gate