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

4.6.4 Backend — `reportsService.getVelocity({ projectId, lastN })`: last-N completed sprints committed (4.4.2 baseline) vs completed (4.3.3 roll-up) + average, bounded

Estimate: 22m · Depends on: 4.4.2, 4.4.3, 4.3.3

The velocity aggregate — simpler than the burndown, purely a bounded read over the completed-sprint history. Pure backend, no new write model. Lives in the same new reportsService as 4.6.3 (the Epic-6.3 home), with a HTTP-only route + a VelocityDto.

reportsService.getVelocity({ projectId, lastN = 7 }) → a VelocityDto:

  • Enumerate the project's completed sprints (state = 'complete', ordered by completedAt / sequence desc), LIMIT lastN (default 7, the Jira default) — a bounded read via sprintRepository.listByProject filtered to complete (extend it with a state filter + limit if needed; do NOT load every sprint).
  • Per sprint: committed = the locked committedPoints baseline (4.4.2, O(1) stored) and completed = estimationService.rollupForSprint(sprintId).completed (4.3.3 — the same done-category aggregate the scrum header + the sprint report use, so the bars match those surfaces). Resolve the configured statistic the same way 4.3.3 does.
  • Average completed across the returned sprints (the planning forecast). Return the sprints oldest→newest for the X axis + the average + the statistic label.
  • Low-history — 0 completed sprints → an empty VelocityDto the UI renders as "not enough history yet"; 1 sprint returns the single bar + that sprint as the average (the UI may note "need ≥2 for a trend"). Unestimated sprints contribute 0 (or count, per the statistic), never NaN.

Layering + bound. The service does LIMIT N sprints, then N bounded rollupForSprint calls (N ≤ 7) — a bounded fan-out, not an all-issues scan; it never iterates every issue of every sprint in Node. Repo reads are single ops; the service owns the DTO + the average; the route is HTTP-only; the finding-#26 workspaceId/project gate covers it. Any new repo method (e.g. a state-filtered + limited sprint list) gets a direct empty-input-guard test (coverage gate).

Acceptance criteria

  • reportsService.getVelocity({ projectId, lastN }) returns the last N completed sprints (ordered oldest→newest) with committed (the 4.4.2 locked baseline) + completed (4.3.3 rollupForSprint, same done-category predicate as the scrum header) per sprint, plus the average completed + the statistic label.
  • The read is bounded: a LIMIT N sprint query + N (≤7) bounded roll-up aggregates — NOT a load of every sprint or every issue; db:seed:large (many completed sprints) stays bounded.
  • 0 completed sprints → an empty/low-history DTO (no crash, no axis-of-one); 1 sprint → the single bar; unestimated sprints contribute 0 / the issue-count value, never NaN.
  • GET /api/projects/[id]/velocity (or /api/boards/[id]/velocity) is HTTP-only; cross-workspace access denied (finding #26); any new repo method has a direct empty-input-guard test; pnpm test:coverage keeps the new files ≥90% branch/fn/line.

Context refs

  • Story 4.4.3 completeSprint (sets state = complete + completedAt) — the completed-sprint history this enumerates; Story 4.4.2 (committedPoints baseline) — the committed bar
  • Story 4.3.3 rollupForSprint(sprintId).completed — the completed bar (REUSE, do not re-sum); resolve the statistic identically so the bars match the scrum header + report
  • Story 4.1.2 sprintRepository.listByProject / countByProjectAndState — the sprint list to filter to complete + limit (extend minimally if a state-filter+limit variant is needed)
  • reportsService (introduced in 4.6.3) — the shared service home; motir-core/CLAUDE.md (4-layer); findings #57 (bounded), #26 (workspaceId gate); motir-core-coverage-gate