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

4.4 Sprint lifecycle (start / complete)

The sprint lifecycle flows — the verbs that take a sprint from planned to active to complete — built on the entity + rules Story 4.1 shipped. Three things: start a sprint (scope-lock the committed work, stamp the window, ensure the scrum board exists so it "opens"), complete a sprint (move the unfinished issues somewhere via carry-over, close the sprint), and the sprint report (what got done vs. what did not, in issues and points). The one-active-sprint guard rail is enforced at the flow level (on top of 4.1's data-layer partial-unique backstop). 4.4 owns no new entity beyond a small scope-lock baseline; it COMPOSES 4.1's assertSprintTransition guard, one-active index, and assignToSprint/moveToBacklog association writes into real flows.

What 4.4 owns vs. what Stories 4.1 / 4.2 / 4.3 / 4.5 own (the clean seam). Story 4.1 owns the ENTITY + RULES — the Sprint model + SprintState enum, the pure assertSprintTransition (planned→active→complete, one-way), the sprint_one_active_per_project partial-unique index, the sprint CRUD (4.1.3), and the issue↔sprint association assignToSprint/moveToBacklog (4.1.4). 4.4 COMPOSES them: startSprint and completeSprint are orchestrations that call the guard, flip the state, and (for complete) drive the carry-over moves. Story 4.2 (backlog) MOUNTS the Start-sprint entry-point button as a seam; 4.4 wires the start modal to it. Story 4.5 (scrum board) MOUNTS the Complete-sprint entry point and REUSES 4.4's complete flow (4.5.3 dependsOn 4.4 — the one-way arrow). Story 4.3 exposes rollupForSprint (committed/completed/remaining points); 4.4's report REUSES it. 4.4 does NOT re-implement the state machine, the association, the point roll-up, the backlog UI, or the scrum render — it consumes them.

Start a sprint (scope-lock + window + "board opens"). From the backlog's Start-sprint entry point on a planned sprint that has at least one issue (the button is disabled on an empty sprint — 4.2.1's rule), open the start-sprint modal: confirm/edit the name, pick a duration (1 / 2 / 3 / 4 weeks / custom — the Jira durations, mirror rung 1), which derives the start date (now) + end date, and the sprint goal. On confirm, startSprint (a) asserts the planned→active transition via 4.1's assertSprintTransition; (b) enforces one active sprint per project — a friendly typed SprintAlreadyActiveError before hitting 4.1's partial-unique backstop (the index is the data-layer guard; the service turns it into a 409 a UI can explain); (c) stamps startDate/endDate + flips state to active; (d) scope-locks the committed baseline (below); (e) ensures the project's scrum board exists (create-if-missing via the shipped 3.7.3 boardsService.createBoard(projectId, { type: scrum })) so the started sprint has a board to render on; all in ONE transaction, recording a 1.4.6 revision. "Board opens" = the start UI then navigates to /boards (the scrum board renders the active sprint once Story 4.5 lands; until then it renders as Kanban — graceful).

Scope-lock = an immutable committed baseline (the durable shape, not a shortcut). Jira's sprint report shows a fixed Committed line — the issues/points in the sprint at start — that does NOT move as scope changes afterward, so the report can flag work added during the sprint. The faithful durable shape is to STORE that baseline at start (an immutable snapshot), not to re-derive it later (re-derivation is fragile once points are edited retroactively). So 4.4 adds two small, set-once-at-start columns — sprint.committedPoints + sprint.committedIssueCount — stamped by startSprint and never mutated again. Combined with the 1.4.6 revision trail (which timestamps every sprint association), the report derives "added after start" = associations created after startDate. (completedAt already exists from 4.1.1; the start window columns startDate/endDate too — 4.4 only adds the two baseline columns.)

Complete a sprint (carry-over + close). From the Complete-sprint entry point (scrum header — Story 4.5 — AND the backlog active-sprint container — self-mounted here), open the complete-sprint modal: it shows the completed count (issues whose workflow status is in a category = 'done' terminal status — reuse workflowsService.getTerminalStatusKeys, the Epic-3 done-category set) and the incomplete count, and asks where the incomplete issues go — the carry-over destination: the Backlog (default) or an existing planned sprint (a future sprint to roll them into). On confirm, completeSprint(sprintId, { carryOverTo }) (a) asserts active→complete; (b) moves every unfinished issue — moveToBacklog (4.1.4) for the backlog, or assignToSprint into the chosen planned sprint (same-project guarded) — leaving the DONE issues on the completed sprint; (c) sets completedAt + flips state to complete; all in ONE transaction, recording 1.4.6 revisions. Carry-over into a NEW sprint = create it first (4.1.3 createSprint) then pick it — no inline sprint-create in the complete modal (no complexity for nothing; the backlog already creates sprints). Completing the sprint frees the one-active slot so the next sprint can start.

The sprint report (what got done). getSprintReport(sprintId) returns, for a completed (or active, for a live preview) sprint: the completed vs not-completed issue lists (the done-category split), the points summarycommitted (the locked baseline) / completed (SUM over done-category issues) / not completed — REUSING Story 4.3's bounded rollupForSprint rather than re-summing, and the scope change ("N issues added after start", derived from the 1.4.6 revisions vs startDate). It degrades gracefully when issues are unestimated (points show "—", never NaN, exactly as the 4.5 sprint header does). The burndown CHART is Story 4.6 (it reads this same completed-sprint history + the committed baseline); the report here is the textual / numeric summary + the issue lists, with a documented seam for 4.6's chart. The report is reachable right after completion (rendered by the complete modal's success state) and later (a sprint's report stays viewable — Jira keeps closed-sprint reports).

Completeness / scale (finding #57 — bounded, not load-all). The completed/incomplete COUNTS and the point figures are grouped aggregates scoped to the sprint (the same shape as 4.5.2's SprintSummaryDto + 4.3's rollupForSprint), never a load-every-issue-then-sum. The report's issue LISTS are cursor-paginated (a real sprint can hold hundreds of issues) — the report shows the counts + the first bounded page of each list, "view all" deep-links to the /issues navigator (Story 2.5) filtered to the sprint (the 4.2 "View all issues" mirror pattern), never a full in-report dump. The carry-over MOVE is bounded too: it is the same bounded-batch transaction shape 4.2.2 uses for bulk assignment (move the unfinished set in one tx, not N client round-trips, with rollback on partial failure).

The real-product states. Start: an empty sprint can't be started (entry point disabled — 4.2.1); a second active sprint is refused with the friendly SprintAlreadyActiveError (the modal explains "Project X already has an active sprint"); an invalid window (endDate < startDate) is rejected (reuse 4.1.3's SprintWindowInvalidError). Complete: a sprint with no incomplete issues skips the carry-over chooser (nothing to move); a sprint that is all incomplete still completes (everything carries over); the carry-over target must be a planned sprint in the SAME project (the 4.1.4 same-project guard backstops it). Loading / error on each modal reuse the shipped Modal + ErrorState idioms. Every state is drawn in 4.4.1, not improvised.

4-layer + tenancy (CLAUDE.md). The two baseline columns are modelled as plain scalars on the existing Sprint model (no new FK; the add_sprint_lifecycle_fields migration is additive and drift-free — a second migrate dev reports "No difference detected"). startSprint / completeSprint / getSprintReport are sprintsService methods (each write = one prisma.$transaction), composing the 4.1 repository single-ops (the scrum-board ensure goes through the shipped boardsService.createBoard — a service composing a service is fine; repos stay leaves); typed errors in lib/sprints/errors.ts; DTOs via lib/mappers/sprintMappers.ts; the finding-#26 application-layer workspaceId gate on every read/write; the sprint RLS policy (4.1.1) already covers the table. Routes are HTTP-only one-service-call handlers. Client UI uses --el-* colour + element-shape tokens only (no Tier-0 --color-* / raw rounded-*).

Out of scope (Epic-4 siblings / Epic 6 / later): the sprint entity / state-machine guard / one-active index / association + rank writes / bounded reads (Story 4.1 — consumed, not built); the backlog UI + the Start-sprint entry-point button + sprint CRUD UI (Story 4.2 — 4.4 wires the flow into the mounted button); story-point estimation + the rollupForSprint engine (Story 4.3 — consumed by the report); the Scrum BOARD render + the sprint header + the Complete-sprint entry point placement (Story 4.5 — which REUSES 4.4's flow); the burndown + velocity CHARTS (Story 4.6 — the report shows numeric/list summary + a chart seam); board CRUD / multi-board nav (Story 3.7 — 4.4 only CALLS the shipped createBoard to provision the scrum board); the combined at-scale Scrum journey E2E (Story 4.7 — 4.4 ships its own focused lifecycle E2E); multiple parallel active sprints (the guard is one active per project — no multi-sprint selector, matching the planned guard, no complexity for nothing); a multi-value sprint history field on issues (Jira keeps one active sprint_id + the revision trail records moves — a later reporting concern).

Verification

  • Pull the Story branch, pnpm install, pnpm prisma migrate dev (applies add_sprint_lifecycle_fields — the two scope-lock baseline columns), pnpm db:seed, pnpm dev. (Requires Story 4.1 merged for the sprint entity + guard + association; Story 4.3 merged for rollupForSprint; Story 4.2 merged for the backlog Start/Complete entry points the flows wire into.)
  • Migration is clean (no drift): a second pnpm prisma migrate dev reports "No difference detected" — the baseline columns are plain additive scalars on Sprint (no FK, no raw-SQL-only constraint). pnpm prisma migrate status is up to date.
  • Design exists first: design/sprints/sprint-lifecycle.mock.html + a PNG export + design/sprints/design-notes.md exist (subtask 4.4.1), built from components/ui/* + --el-*/element-shape tokens only, AA-safe, passing the render checklist — drawing the start-sprint modal, the complete-sprint modal (completed/incomplete split + carry-over chooser), and the sprint report (lists + points + scope change + the 4.6 chart seam).
  • pnpm test:coverage — Vitest (real Postgres) over the lifecycle service stays ≥90% per-file branch/fn/line on the new/changed service/repository files (the CI coverage gate, motir-core-coverage-gate); empty-input guards on any new repo method have a direct test.
  • Start flow: startSprint on a planned sprint with ≥1 issue flips it to active, stamps startDate/endDate (from the chosen duration) + the committedPoints/committedIssueCount baseline, ensures a type == scrum board exists for the project (created if missing, via createBoard), and records a 1.4.6 revision — all in one transaction. Starting a sprint while another is active in the same project throws SprintAlreadyActiveError (the friendly 409, before the partial-unique backstop); a different project may start its own concurrently. An endDate < startDate window is rejected.
  • One-active rail (defence in depth): with the service guard bypassed, the sprint_one_active_per_project index (4.1.1) still refuses a second active sprint — the data layer is the backstop, the service error is the friendly path.
  • Complete flow + carry-over: completeSprint(sprintId, { carryOverTo: 'backlog' }) moves every NON-done-category issue back to the backlog (in backlog_rank order), leaves the done issues on the sprint, sets completedAt, flips state to complete, and frees the one-active slot — one transaction. { carryOverTo: { sprintId } } instead assigns the unfinished issues into the chosen PLANNED sprint (same-project guarded; a cross-project or non-planned target is rejected). A sprint with no incomplete issues completes without a carry-over step.
  • Sprint report: getSprintReport returns the completed vs not-completed issue lists (the done-category split), the points summary (committed = the locked baseline, completed = SUM over done issues via 4.3 rollupForSprint, not completed = the remainder), and the scope-change count ("added after start" from the 1.4.6 revisions vs startDate); an unestimated sprint shows "—" for points (no NaN). The lists are cursor-paginated (first bounded page + a "view all" deep-link to /issues filtered to the sprint), NOT a full dump (finding #57).
  • Start-sprint UI: sign in as zhuyue@motir.co, open the motir project → /backlog; a planned sprint with issues shows an enabled Start sprint button → the start modal (name / duration / dates / goal) → confirm flips the sprint active and navigates to /boards. An empty sprint's Start button is disabled; starting a second sprint shows the "already active" message. Matches design/sprints/sprint-lifecycle.mock.html.
  • Complete-sprint UI: on the active sprint (from the backlog active-sprint container — and, once Story 4.5 lands, the scrum header) the Complete sprint action opens the complete modal showing the completed/incomplete counts and the carry-over chooser (Backlog · a planned sprint) → confirm completes the sprint, moves the unfinished issues, and shows the sprint report (completed/incomplete lists + committed/completed points + "N added during sprint"). Matches the design.
  • Scale check (finding #57): pnpm db:seed:large with a large active sprint → the report COUNTS + point figures come from grouped aggregates (not a load-all), the issue lists render one bounded page with a "view all" link, and the carry-over move is ONE bounded transaction (a forced mid-batch failure rolls back — none moved, not a partial set).
  • Tenancy: a cross-workspace start/complete/report call is denied by the finding-#26 workspaceId gate; the sprint RLS policy rejects access outside the active workspace context.
  • a11y: the modals are labelled dialogs with focus trap + escape; the report's completed/incomplete and points are read as text+number (not colour alone — finding #35); the carry-over chooser is keyboard-operable.