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
144
planned
1,362
shipped

Motir · Work items

MOTIR-156Done

3.1 Board data model + column-from-workflow projection

The data-model + projection floor under every board surface in Epic 3. A project owns one or more boards; each board owns an ordered set of columns; each column maps to a SET of the project's workflow statuses (Story 2.2). The board is a pure read projection over issues — a card's column is derived from its work_item.status, and the board stores no per-card placement state of its own. The load-bearing principle (carried from the stub): moving a card = a workflow transition, never a board-local write. A cross-column drop resolves to issuesService.updateStatus (validated by workflowsService.canTransition under the project's policy mode — Story 2.2.4); an in-column drop is a pure rank change on work_item.position. This story builds the backend for both; Story 3.2 builds the drag-drop UI that calls it (illegal drops snap back on the typed error this story raises).

Durable shape — why a real board entity, not "column == status". The mirror product (Jira; decision-ladder rung 1) does NOT hardwire one column per status: a column maps to many statuses (the canonical example — merge In Progress + In Review into one "In Progress" column), and a status mapped to no column is unmapped (hidden from the board until an admin maps it). So the schema is a board + board_column + a board_column_status mapping (many statuses → one column; a status maps to ≤1 column per board). The "column == one status, 1:1" form is the shortcut the no-shortcuts rule forbids ("simpler X now, migrate later"). The default board generated for a new project IS one-column-per-status (the column-from-workflow projection) — but that is a seeded default over the durable mapping, not a hardcoded shape. v1 auto-creates exactly ONE Kanban board per project; the board.projectId FK is non-unique, so multiple boards per project is a non-breaking addition planned as Story 3.7 (board CRUD + switcher). The Scrum (sprint-scoped) board is Story 4.5 (moved to Epic 4 per mistake #32 — see data/story-4.5.ts).

Scale shape — the projection is bounded, never "load every row" (finding #57). A real team's project has thousands of issues; a board that reads them all to render is prototype- thinking. The projection paginates per column: each column returns its first N cards (ordered by work_item.position), a per-column total count, and a cursor for lazy "load more" (Story 3.2 virtualizes within a column). Done/terminal columns are additionally bounded to a recent window (Jira hides done issues older than ~14 days) with the full count surfaced. The projection reuses Story 2.5's issue-read path + filter shape (workItemRepository flat reads, the AND-across-facet / OR-within-facet filter), not a new full-table scan.

Prerequisites & contracts. Story 2.2 ships workflow_status (the columns derive from these, in status.position order) + project.workflow_policy_mode + the transition validation the move path delegates to (2.2.4). Story 1.4 ships work_item with the status string + the position fractional index (finding #18) the within-column reorder writes. Story 2.5 ships the paginated issue-read precedent (2.5.12). All work follows motir-core/CLAUDE.md's 4-layer architecture (Route → Service → Repository → Prisma); every new route carries an explicit workspaceId application-layer gate (finding #26 — RLS is the backstop, not the sole gate, because the dev/CI superuser bypasses RLS).

Verification

  • Pull the Story branch, pnpm install && pnpm prisma generate && pnpm prisma migrate dev against a fresh local DB.
  • pnpm test — vitest covers the schema RLS, the default-board seed (one column per status, in workflow order), the repository reads, the projection (grouping + per-column pagination + count + unmapped statuses), and the move/reorder service (cross-column move = transition, illegal-move rejection, in-column rank change).
  • pnpm test:e2e --grep board-projection — the closing API-level suite drives the real stack through the board endpoints (no UI yet — that arrives in 3.2).
  • Manual API check: create a workspace + project (the default board auto-seeds). GET /api/projects/[key]/board → six columns (To Do / Blocked / In Progress / In Review / Done / Cancelled, in workflow order), each with its cards grouped by status, per-column counts, and an empty unmappedStatuses. Create ~10 issues across statuses → they appear in the right columns.
  • Unmapped-status check (finding #57 / mirror-product): add a custom status via the workflow editor (Story 2.2.5). GET …/board → the new status appears in unmappedStatuses (NOT a new column, and NOT silently dropped) — proving the projection surfaces, rather than hides, statuses with no column mapping.
  • Scale check (finding #57): pnpm db:seed:large (Story 2.5.16), open a column with hundreds of cards → the projection returns a bounded first page + a total count + a cursor; GET …/board/columns/[id]/cards?cursor=… returns the next page. The board never loads every row.
  • Move = transition check: POST …/board/move a card from To Do → In Progress → it returns the moved card with the new status, and the issue's status is updated via the workflow path. Attempt an illegal cross-column move under restricted policy (e.g. To Do → Done if no such transition) → a typed IllegalBoardMoveError (HTTP 409), issue status unchanged — the snapback contract 3.2 relies on.
  • In-column reorder check: move a card within the same column (no status change) → only work_item.position changes, no transition is attempted, column membership is unchanged.
  • RLS proof: open a psql session as prodect_app, SET app.workspace_id = '<workspace-A>', query board / board_column / board_column_status — see only workspace A's rows.