4.1.2 `sprintRepository` + `work_item` sprint/rank repo methods (single-op; writes require `tx`)
Estimate: 18m · Depends on: 4.1.1
The data-access leaf for sprints + the backlog rank, per the 4-layer rule (each method one Prisma op; no business logic, no transactions; writes take a required tx: Prisma.TransactionClient; reads used inside a write-guarding transaction take tx + SELECT … FOR UPDATE where a concurrent write could race).
sprintRepository (lib/repositories/sprintRepository.ts): create(data, tx), update(id, data, tx), delete(id, tx), findById(id), findActiveByProject(projectId) (the single state == active row — reads tx + FOR UPDATE in the variant the activation guard uses), listByProject(projectId) (ordered by sequence / state), countByProjectAndState / maxSequenceForProject(projectId) (for the next default name). NO cross-repo calls (repos are leaves).
work_item sprint/rank methods — extend workItemRepository (the entity owns them, not a new repo): setSprint(itemId, sprintId | null, tx) (the association write), setBacklogRank(itemId, rank, tx), and the bounded reads findBacklogPage(projectId, { cursor, limit }, ...) (sprint_id IS NULL, backlog_rank order, limit+1 for the next-cursor) + countBacklog(projectId) + findSprintIssues(sprintId) / countSprintIssues(sprintId) + the findRankNeighbours/boundary reads rankIssue needs (the prev/next backlog_rank around a target). Each is a single Prisma op ($queryRaw only where a grouped/aggregate read needs it).
Empty-input guards (the motir-core-coverage-gate lesson): any method that can be called with an empty id list / null cursor short-circuits with a direct unit test so the per-file branch-coverage gate stays green.
Acceptance criteria
sprintRepositoryexposes the create/update/delete + findById/findActiveByProject/listByProject/count/maxSequence methods; write methods requiretx;findActiveByProjecthas aFOR UPDATEvariant for the activation guard path.workItemRepositorygainssetSprint,setBacklogRank, the cursor-paginatedfindBacklogPage+countBacklog,findSprintIssues+countSprintIssues, and the rank-neighbour reads — each a single Prisma op; writes requiretx.- No repository method calls another repository or opens a transaction; aggregates/raw reads use
$queryRaw. Empty-input guards are directly unit-tested. pnpm typecheckpasses; methods return Prisma rows (mapping is the service's job).
Context refs
lib/repositories/boardRepository.ts/workItemRepository.ts— the single-op + required-tx+$queryRaw-aggregate patterns to mirror; where thework_itemsprint/rank methods landlib/workItems/positioning.ts—keyBetween(the service computes the rank; the repo just persists it)motir-core/CLAUDE.md(repository layer rules; entity-name-wins for method placement) +motir-core-coverage-gate(empty-input-guard tests)