4.4.2 Backend — schema (scope-lock baseline cols) + `startSprint` flow (transition + one-active guard + window + scope-lock + ensure-scrum-board)
Estimate: 30m · Depends on: 4.1.3, 4.1.4, 3.7.3
The start half of the lifecycle service — the head of Story 4.4 (all its deps are done, so it is ready immediately). Composes Story 4.1's rules; adds the small scope-lock baseline.
Migration add_sprint_lifecycle_fields — two additive plain scalars on the existing Sprint model: committedPoints Int? @map("committed_points") + committedIssueCount Int? @map("committed_issue_count") — the immutable scope-lock baseline, set once by startSprint and never mutated (the Jira "Committed" line). NO new FK (so the migration is drift-free — a second migrate dev reports "No difference detected"; startDate/endDate/completedAt already exist from 4.1.1). No enum/index change.
sprintsService.startSprint(sprintId, { name?, startDate, endDate }) (extends the 4.1.3 service; one method = one prisma.$transaction):
- Load the sprint (
findById); assert it isplannedand compose 4.1's pureassertSprintTransition(planned, active)for the transition rule (do NOT re-derive it). - One-active guard (friendly path): read
findActiveByProject(projectId)(theFOR UPDATEvariant, inside the tx); if one exists, throw a NEW typedSprintAlreadyActiveError(→ 409) BEFORE the write — so the UI gets an explainable error rather than a raw unique-violation. 4.1.1'ssprint_one_active_per_projectpartial-unique index remains the data-layer backstop (defence in depth). - Window: validate
endDate ≥ startDate(reuse 4.1.3SprintWindowInvalidError); stampstartDate(default now) +endDate; optionally updatename. - Scope-lock: compute the committed baseline at start —
committedIssueCount= the count of the sprint's issues;committedPoints=SUM(storyPoints)over them (REUSE 4.3.3rollupForSprint/the points aggregate if available; if 4.3 is not yet wired on these issues the sum is 0/null — graceful) — and write them (immutable thereafter). - "Board opens" — ensure the scrum board exists: in the same transaction, ensure the project has a
type == scrumboard — read the project's boards; if none isscrum, call the shippedboardsService.createBoard(projectId, { name: 'Sprint board', type: 'scrum' })(3.7.3, which seeds default columns). Idempotent: a second start does not create a duplicate. (A service calling a service is allowed; repos stay leaves.) - Flip
statetoactive; record a 1.4.6work_item_revision/sprint revision in the same tx; return the updatedSprintDto. Enforce the finding-#26workspaceIdgate.
Typed errors: add SprintAlreadyActiveError, SprintNotStartableError (not in planned) to lib/sprints/errors.ts. Route: POST /api/sprints/[id]/start — HTTP-only, one service call + error→status mapping (409 already-active, 422 window/state).
Acceptance criteria
- The
add_sprint_lifecycle_fieldsmigration addscommitted_points+committed_issue_countas nullable scalars onsprint;pnpm prisma migrate devapplies cleanly and a SECOND run reports "No difference detected" (no FK drift). startSprintcomposesassertSprintTransition(planned→active), throwsSprintAlreadyActiveErrorwhen the project already has an active sprint (before the partial-unique backstop), validates the window, stampsstartDate/endDate+ the immutablecommittedPoints/committedIssueCountbaseline, ensures ascrumboard exists viacreateBoard(idempotent), flips state toactive, and records a revision — all in ONE transaction; returns aSprintDto.- A different project can start its own sprint concurrently; an
endDate < startDatewindow is rejected; starting a non-planned sprint throwsSprintNotStartableError. - New typed errors live in
lib/sprints/errors.ts;POST /api/sprints/[id]/startis HTTP-only (one service call + error mapping); the finding-#26workspaceIdgate covers the reads/writes. pnpm test:coveragekeeps the new/changed service file ≥90% branch/fn/line (the coverage gate); complete/report ORCHESTRATION is explicitly absent (subtasks 4.4.3 / 4.4.4).
Context refs
lib/services/sprintsService.ts+lib/sprints/errors.ts+lib/mappers/sprintMappers.ts+lib/dto/sprints.ts(Story 4.1.3) — the service/errors/DTOs this extends; the pureassertSprintTransitionto compose;lib/repositories/sprintRepository.tsfindActiveByProject(theFOR UPDATEvariant)lib/services/boardsService.tscreateBoard(projectId, { name, type })(Story 3.7.3) — the shipped scrum-board provisioning primitive to call for "board opens"- Story 4.3.3
rollupForSprint/ theSUM(storyPoints)aggregate (for the committed baseline; graceful 0 when unestimated) — read here, defined there prisma/schema.prismamodel Sprint(4.1.1) — where the two baseline columns land;motir-core/CLAUDE.md(4-layer: service owns the tx + DTO mapping; FK-as-@relation/no-drift rule); finding #26 (workspaceIdgate);motir-core-coverage-gate