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

4.1.4 Issue↔sprint association + backlog rank ordering + bounded backlog/sprint read API (the data layer Story 4.2 binds to)

Estimate: 28m · Depends on: 4.1.3

The association + ranking + read API that powers the Story-4.2 backlog UI — the part of the "data model" that moves issues between the backlog and sprints and orders them, built to real-product SCALE (finding #57).

Association writes (in sprintsService, or a focused backlogService if cleaner — same 4-layer rules):

  • assignToSprint(itemId, sprintId, { rank? }) — set work_item.sprint_id; same-project guard: the sprint and the issue MUST share a project (throw CrossProjectSprintAssignmentError otherwise). Optionally place it at a rank within the sprint.
  • moveToBacklog(itemId) — null sprint_id; the issue re-appears in the backlog in its existing backlog_rank order.
  • rankIssue(itemId, { beforeId? , afterId? }) — compute the new backlog_rank via positioning.ts keyBetween(prevRank, nextRank) and write the SINGLE row (no N-row renumber). Works for both a backlog issue and an issue within a sprint (one global rank field). Guards the degenerate "no neighbours" (append/prepend) cases.
  • Create-time rank: new issues get a backlog_rank appended (keyForAppend) when created — wire this into the issue-create path (workItemsService.create) so the backfill (4.1.1) stays total going forward. Keep the touch on the create path minimal.
  • Every association/rank write records a work_item_revision row (reuse the 1.4.6 audit service) in the SAME transaction.

Bounded reads (the finding-#57 shape):

  • getBacklog(projectId, { cursor?, limit }){ items: WorkItemSummaryDto[], nextCursor, totalCount }sprint_id IS NULL, backlog_rank order, cursor-paginated (NEVER load-all), with the aggregate count for the "N issues" header.
  • getSprintIssues(sprintId) → the sprint's ranked issues (+ count) for the planning view; bounded the same way (a sprint is smaller, but the read is still paged-capable, not unbounded). Routes: POST /api/work-items/[id]/sprint (assign/clear), POST /api/work-items/[id]/rank, GET /api/projects/[id]/backlog — HTTP-only, one service call each. Mapping via lib/mappers/*.

Acceptance criteria

  • assignToSprint (same-project guarded), moveToBacklog, and rankIssue (single-row keyBetween write, append/prepend edge cases handled) exist, own their transaction, record a 1.4.6 revision, and enforce the workspaceId gate.
  • New issues receive a backlog_rank at creation (the create path appends one); cross-project sprint assignment is rejected with the typed error.
  • getBacklog is cursor-paginated in rank order with a total count and never selects every row; getSprintIssues returns the sprint's ranked issues + count; both return DTOs.
  • pnpm db:seed:large → the backlog read returns one bounded page + the count and rank writes stay O(1) (finding #57); routes are HTTP-only one-service-call handlers.
  • pnpm test:coverage keeps the new files ≥90% branch/fn/line.

Context refs

  • Story 4.1.2 (work_item sprint/rank repo methods) + 4.1.3 (sprintsService + DTOs/errors) — the layers this composes
  • lib/workItems/positioning.ts (keyBetween / keyForAppend / keyForPrepend) — the single-row rank computation
  • lib/services/workItemsService.ts + the 1.4.6 workItemRevisionsService — the create path to append the rank into + the audit-trail write to reuse
  • Story 4.2 (backlog UI) — the consumer of getBacklog / assignToSprint / rankIssue; finding #57 (bounded reads); finding #26 (workspaceId gate); motir-core/CLAUDE.md