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

3.3.4 Projection — swimlane grouping (resolved `swimlaneKey` + bounded lane list) + WIP count, in `getBoard`

Estimate: 24m · Depends on: 3.3.3, 3.1.4

Extend the Story-3.1.4 boardsService.getBoard projection so the UI can render swimlanes and the WIP state — without regressing the bounded, never-load-all shape (finding #57). 3.1.4 already returns ordered columns, each with a bounded first page of BoardCardDto, a per-column total + cursor, and the wipLimit on each column. This subtask adds the lane dimension.

swimlaneGroupBy on the projection. getBoard returns the board's active swimlaneGroupBy. When it is none, the projection is exactly the 3.1.4 flat shape (no behaviour change). When it is assignee / epic / priority:

  • Per-card swimlaneKey (resolved server-side). Each BoardCardDto is stamped with a swimlaneKey for the active group-by so the client never re-derives it: assigneeassigneeId (or the catch-all key for unassigned); priority → the priority value; epic → the card's ancestor epic id (walk the parent chain to the epic, NOT the immediate parent — a task under a story under an epic groups by the epic), or the catch-all key when there is no epic ancestor.
  • The lane list (bounded aggregate, NOT load-all). A top-level swimlanes: BoardSwimlaneDto[]{ key, label, kind, count } — built from a grouped/distinct aggregate query over the project's issues (lanes that actually have cards + the catch-all), ordered by the documented lane-order rule (assignee alpha / priority rank / epic position; catch-all last). This is an aggregate, so it does NOT load every card to discover lanes. The label/kind let the UI render the lane header (assignee summary / epic key+title / priority) without extra fetches.

Pagination stays per column (finding #57). Cards continue to page per column via the 3.1.4 first-page + loadColumnCards cursor; the client buckets the loaded page into (lane, column) cells by swimlaneKey, and "load more" on a column pulls the rest. The projection does NOT page per (lane × column) cell (that CxL explosion is over-engineering with no stated need); per-lane TOTAL counts come from the aggregate, per-column totals from 3.1.4. Document this as the durable bounded shape.

Epic-ancestor resolution. Resolve each card's epic ancestor efficiently (a single batched lookup over the page's cards' ancestor chains / a recursive CTE), not an N+1 walk; reuse any Story-1.4 / 2.5 ancestor helper if one exists.

Out of scope here: WIP enforcement (there is none — it stays a UI soft warning, 3.3.6; the projection just carries wipLimit + the per-column count); any UI (3.3.5/3.3.6); the config writes (3.3.3).

Acceptance criteria

  • getBoard returns the active swimlaneGroupBy; with none the projection is byte-for-byte the 3.1.4 flat shape (no regression).
  • With a non-none group-by, each BoardCardDto carries a correct swimlaneKey (assignee / priority value / ancestor-epic id / catch-all), and a top-level swimlanes: { key, label, kind, count }[] lists lanes-with-cards + the catch-all, in the documented order.
  • The lane list is built from a bounded grouped/distinct aggregate (no all-cards fetch); per-card cursor pagination per column is unchanged; the projection never returns every card.
  • Epic grouping uses the ancestor epic (not the immediate parent), resolved without N+1.
  • wipLimit + the per-column total count remain on each column (carried for 3.3.6; the projection does NOT enforce WIP).
  • Vitest (real Postgres) covers: each group-by resolving the right swimlaneKey (incl. epic-ancestor + each catch-all), the lane list + per-lane counts, the none no-op, and that pagination/bounding is preserved (no load-all).

Context refs

  • lib/services/boardsService.ts getBoard + lib/mappers/boardMappers.ts + lib/dto/boards.ts (Story 3.1.4) — the projection + BoardProjectionDto/BoardCardDto this extends
  • lib/repositories/workItemRepository.tsfindProjectIssuesFlat / count + any ancestor/parent-chain helper (Story 1.4 / 2.5) for epic resolution
  • lib/dto/workItems.ts — the assignee-summary / priority / parent fields the lane labels reuse
  • finding #57 — bounded projection (no load-all); motir-core/CLAUDE.md — service owns DTO mapping, mappers in lib/mappers/*