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

4.5.2 Projection — sprint scope + `SprintSummaryDto` (committed/completed/remaining points + per-column totals) in `getBoard` for scrum boards

Estimate: 24m · Depends on: 3.1.4, 4.1, 4.3

The one backend change: teach the Story-3.1.4 boardsService.getBoard projection to render a scrum-type board scoped to its active sprint, and to carry the sprint summary the header needs — without regressing the bounded, never-load-all shape (finding #57) and without touching the kanban path.

Depends on Stories 4.1 + 4.3 (Epic-4 siblings, story-level forward refs while they are unexpanded stubs). Story 4.1 provides the sprint entity (goal, startDate, endDate, state planned·active·complete) + the issue→sprint association (workItem.sprintId or a join — whatever 4.1 ships); Story 4.3 provides the story-point field on issues. Both are unexpanded stubs today, so these are story-level deps; this subtask reads those fields, it does not define them. (When 4.1/4.3 expand, a re-plan can retarget to the exact subtasks.)

Sprint scope. For a board whose type == scrum, resolve the board's active sprint (the single state == active sprint for the board — the 4.4 one-active-per-board guard makes this unambiguous; resolve via a single repository read, SELECT … WHERE boardId/projectId AND state = active). FILTER every column query to issues associated with that sprint (an extra WHERE issue.sprintId = :activeSprintId on the SAME 3.1.4 column/page/count queries — not a new projection path). A type == kanban board is unchanged (no sprint scope — guard the new branch on board type). When a scrum board has no active sprint, return sprint: null and empty columns (the UI renders the empty state) — never silently fall back to the unscoped backlog as if it were a sprint.

SprintSummaryDto (drives the header — bounded aggregates). When an active sprint exists, getBoard returns sprint: { id, name, goal, startDate, endDate, state, daysRemaining, points: { committed, completed, remaining }, columnPoints: Record<columnId, number> }. The point figures are aggregate SUM(storyPoints) queries scoped to the sprint: committed = all sprint issues; completed = sprint issues in a terminal/done status; remaining = committed − completed; columnPoints = the per-column point sum (a grouped aggregate by the column's mapped statuses — the Jira "sprint health" number). These are computed by aggregate queries, NOT by summing the loaded card page (a sum over the bounded page would undercount — the board only loads a page per column). daysRemaining = calendar days from today to endDate, floored at 0 (an overdue sprint → 0, surfaced as "Ended" by the UI, never negative). Issues with a NULL story-point estimate contribute 0 to sums; if the WHOLE sprint is unestimated the UI shows "—" (the DTO still returns 0/0/0 — the UI decides the "—" presentation, finding-#57-style the data layer stays total).

Pagination + swimlanes stay intact (finding #57). Cards still page per column via the 3.1.4 first-page + loadColumnCards cursor; loadColumnCards ALSO takes the sprint scope so "load more" stays sprint-filtered. The 3.3.4 swimlaneGroupBy + swimlanes[] + per-card swimlaneKey continue to work (the sprint filter composes with the lane grouping — lanes are computed over the scoped issue set). The projection never loads every card to total points or discover lanes; both are aggregates.

4-layer + tenant gate. The active-sprint resolution + the point aggregates are repository single-op reads (the SUM/grouped aggregates via $queryRaw where needed); the service owns the orchestration + DTO mapping (mapper in lib/mappers/boardMappers.ts, DTO in lib/dto/boards.ts); the explicit application-layer workspaceId gate (finding #26) already on getBoard covers the new reads. No route change beyond the projection it already returns.

Out of scope here: any UI (4.5.3); the sprint entity / lifecycle / start-complete (Stories 4.1/4.4); the burndown chart aggregate (Story 4.6 — only the numeric remaining is here).

Acceptance criteria

  • For a scrum board getBoard resolves the active sprint and returns columns filtered to that sprint's issues; a kanban board is byte-for-byte the 3.1.4 shape (no regression); a scrum board with no active sprint returns sprint: null + empty columns (no backlog fallback).
  • getBoard returns a SprintSummaryDto (id, name, goal, startDate, endDate, state, daysRemaining, points {committed, completed, remaining}, columnPoints) when a sprint is active; daysRemaining is floored at 0; point figures come from SUM/grouped aggregates scoped to the sprint, NOT from the loaded card page.
  • loadColumnCards (load-more) carries the sprint scope; per-column cursor pagination + the 3.3.4 swimlane grouping compose with the sprint filter; the projection never returns every card.
  • NULL-estimate issues contribute 0 to point sums (no NaN); the DTO stays total (returns numbers, the UI owns the "—" presentation).
  • Returns DTOs mapped in lib/mappers/boardMappers.ts; the new reads are repository single-ops; the finding-#26 workspaceId gate covers them.
  • Vitest (real Postgres) covers: scrum scope (only active-sprint issues), the kanban no-op, no-active-sprint → sprint: null, the committed/completed/remaining + columnPoints aggregates (incl. an unestimated sprint → 0s), daysRemaining floor, and that pagination/bounding/swimlanes are preserved (no load-all).

Context refs

  • lib/services/boardsService.ts getBoard + loadColumnCards + lib/mappers/boardMappers.ts + lib/dto/boards.ts (Story 3.1.4) — the projection + BoardProjectionDto/BoardCardDto this extends with sprint + the scope filter
  • Story 3.3.4 — the swimlaneGroupBy + swimlanes[] + per-card swimlaneKey projection the sprint filter must compose with
  • Story 4.1 (sprint entity + issue→sprint association) + Story 4.3 (story-point field) — the sibling Epic-4 model this reads (story-level dep; fields defined there, read here)
  • lib/repositories/boardRepository.ts / workItemRepository.ts — where the active-sprint read + the SUM/grouped point aggregates land (single-op, $queryRaw for aggregates)
  • finding #57 — bounded projection (aggregates, not load-all); finding #26 — the app-layer workspaceId gate; motir-core/CLAUDE.md — 4-layer (service owns DTO mapping, repo single-op)