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

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 is planned and compose 4.1's pure assertSprintTransition(planned, active) for the transition rule (do NOT re-derive it).
  • One-active guard (friendly path): read findActiveByProject(projectId) (the FOR UPDATE variant, inside the tx); if one exists, throw a NEW typed SprintAlreadyActiveError (→ 409) BEFORE the write — so the UI gets an explainable error rather than a raw unique-violation. 4.1.1's sprint_one_active_per_project partial-unique index remains the data-layer backstop (defence in depth).
  • Window: validate endDate ≥ startDate (reuse 4.1.3 SprintWindowInvalidError); stamp startDate (default now) + endDate; optionally update name.
  • Scope-lock: compute the committed baseline at start — committedIssueCount = the count of the sprint's issues; committedPoints = SUM(storyPoints) over them (REUSE 4.3.3 rollupForSprint/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 == scrum board — read the project's boards; if none is scrum, call the shipped boardsService.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 state to active; record a 1.4.6 work_item_revision/sprint revision in the same tx; return the updated SprintDto. Enforce the finding-#26 workspaceId gate.

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_fields migration adds committed_points + committed_issue_count as nullable scalars on sprint; pnpm prisma migrate dev applies cleanly and a SECOND run reports "No difference detected" (no FK drift).
  • startSprint composes assertSprintTransition(planned→active), throws SprintAlreadyActiveError when the project already has an active sprint (before the partial-unique backstop), validates the window, stamps startDate/endDate + the immutable committedPoints/committedIssueCount baseline, ensures a scrum board exists via createBoard (idempotent), flips state to active, and records a revision — all in ONE transaction; returns a SprintDto.
  • A different project can start its own sprint concurrently; an endDate < startDate window is rejected; starting a non-planned sprint throws SprintNotStartableError.
  • New typed errors live in lib/sprints/errors.ts; POST /api/sprints/[id]/start is HTTP-only (one service call + error mapping); the finding-#26 workspaceId gate covers the reads/writes.
  • pnpm test:coverage keeps 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 pure assertSprintTransition to compose; lib/repositories/sprintRepository.ts findActiveByProject (the FOR UPDATE variant)
  • lib/services/boardsService.ts createBoard(projectId, { name, type }) (Story 3.7.3) — the shipped scrum-board provisioning primitive to call for "board opens"
  • Story 4.3.3 rollupForSprint / the SUM(storyPoints) aggregate (for the committed baseline; graceful 0 when unestimated) — read here, defined there
  • prisma/schema.prisma model 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 (workspaceId gate); motir-core-coverage-gate