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

11.3.5 The sprint WRITE pair — `POST /api/v1/projects/{projectKey}/sprints` and `PATCH /api/v1/sprints/{sprintId}`, gated on `sprints:write` AND the sprint-admin role

Creating and editing a sprint over the API — the first sprints:write operations, and the pair that turns 11.3 from a read surface into a cadence a script can actually run.

Thin adapters over sprintsService.createSprint and updateSprint, returning the sprint schema 11.3.4 pins.

Path correction

The story originally wrote this pair as "POST / PATCH /api/v1/projects/{projectKey}/sprints". PATCH on a collection path is not the edit operation — the edit addresses one sprint, so it is PATCH /api/v1/sprints/{sprintId}, matching §7's rule that an identifier in a path names the resource, and matching how 11.2 placed PATCH /api/v1/work-items/{key} beside the collection POST. Create stays on the collection, where the project scopes it.

⚠️ Two gates, not one — and the second is the one a client will be surprised by

sprints:write is the SCOPE. Every sprint write additionally calls assertSprintAdmin(ctx.userId, projectId, workspaceId) and raises NotSprintAdminError (403). A token carrying sprints:write whose OWNER is an ordinary project member is refused — a scope NARROWS the owner's role and never widens it (ADR §3). That is shipped behaviour this card exposes rather than changes, and it must be asserted, because "my token has the scope and I still get 403" is the single most confusing thing this endpoint can do to an integrator. Make the error's code and message carry that distinction rather than reading identically to a missing scope.

What to build

  • POST /api/v1/projects/{projectKey}/sprints — create. Every field of CreateSprintInput is optional (name defaults to "Sprint <n>" from the project's max sequence; goal, startDate, endDate are nullable planned-sprint metadata). The request body is validated by a zod body schema; dates arrive as ISO-8601 strings and the SERVICE parses and validates them — do not re-implement window validation at the route. Returns 201 with the created sprint and a Location header naming the new resource, the pattern 11.2.6 set.
  • PATCH /api/v1/sprints/{sprintId} — edit name / goal / the planned window. Preserve the shipped tri-state exactly: an absent key leaves the field unchanged, an explicit null clears it, a value sets it. exactOptionalPropertyTypes makes { x: undefined } and {} different types, so the body must be picked key-by-key rather than spread — the same pick discipline the shipped work-item route uses, and for the same reason: spreading blurs precisely the distinction the schema drew.

Acceptance criteria

  • Both endpoints exist and declare scope: 'sprints:write'; a read-only token is refused 403 on each.
  • A token with sprints:write whose owner is NOT a sprint admin is refused 403, with an error code distinguishable from the insufficient-scope refusal — asserted on both endpoints.
  • Create returns 201, the sprint schema's output, and a Location header; an omitted name comes back as the service's "Sprint <n>" default rather than empty.
  • A created-but-never-started sprint reads back with committedPoints: null / committedIssueCount: null and state: 'planned'.
  • PATCH's tri-state is asserted per field: absent leaves the value, explicit null clears goal / a date, a value sets it — three cases, not one.
  • An invalid window (endDate before startDate) surfaces the service's typed error as a 422 with { code, error }, not a 500; an invalid name surfaces its own typed error; both are raised by the service, not re-checked at the route.
  • A sprint in another workspace is a 404 on PATCH, and an unknown projectKey a 404 on POST — never a 403.
  • No route calls Prisma or opens a transaction; each write is one service call after key resolution.
  • Unit tests ship with the routes; every new file holds the ≥90% per-file coverage floor.
  • ONE PR against motir-core.

Context refs

  • lib/services/sprintsService.tscreateSprint (sequence derivation, maxSequenceForProject), updateSprint, assertSprintAdmin / NotSprintAdminError, validateName, assertWindow, parseNullableDate.
  • lib/dto/sprints.tsCreateSprintInput, UpdateSprintInput, and the recorded meaning of undefined vs explicit null.
  • app/api/v1/projects/[projectKey]/work-items/route.ts — the shipped create pattern: parseV1Body, 201 + Location, and the pick helper that preserves the absent/null distinction.
  • lib/api/v1/errors.ts — the domain-error → status map a new typed error must have a deliberate row in.
  • docs/decisions/public-api-conventions.md §3 (scope narrows the role, never widens it), §4 (the status table), §7 (naming).
  • Blocker: 11.3.4 (the sprint schema + the resource path). Pattern precedent: 11.2.6. Parent story: 11.3.