# 07 — Issues & Epics The two work-item levels. Schema in [02-data-model.md](02-data-model.md); routes in [04-api.md](04-api.md); board/list UI in [13-frontend.md](13-frontend.md). ## Issues - [ ] Create: title required; mints `project_issue_id` via the counter UPDATE inside the insert tx (concurrent creates never duplicate or skip visibly); initial `board_rank` = top of its status column - [ ] Fields editable via PATCH: title, description, status, priority, estimate, assignee_id, epic_id, parent_issue_id, start_date, target_date - [ ] Status automation: - [ ] → `done` sets `completed_at = now()`; leaving `done` clears it - [ ] `duplicate` and `canceled` count as "closed" (with `done`) for epic progress and UI dimming - [ ] Assignment: single assignee; assigning emits `issue.assigned` (notification + auto-watch); unassigning emits `issue.unassigned` - [ ] Estimate: story points ∈ {1, 2, 3, 5, 8, 13}, nullable; displayed as a badge; summed on epics - [ ] Sub-issues: `parent_issue_id`, same project only; UI nests one level (data allows deeper); parent detail lists children with status; deleting a parent does **not** delete children (they keep the dangling parent until cull nulls it) - [ ] Blockers: `issue_blockers` with same-project constraint and DFS cycle detection (409); blocked issues show a blocked indicator in list/board/detail - [ ] Labels: apply/remove from the project's label set; emit `issue.label_added/removed` - [ ] Soft delete; emits `issue.deleted` ## Kanban ordering (board_rank) - [ ] `board_rank` is a **fractional lexicographic rank** (LexoRank-style base-36 strings, e.g. `"hzzz"`, midpoint insertion `between(a, b)`); implement `rank.Between/Before/After` helpers with unit tests - [ ] Move = PATCH with `{status?, after_rank?}`: server computes the new rank between neighbors; ties never occur because ranks are always unique strings per column (append-suffix on exhaustion) - [ ] `?view=board` returns issues grouped by status, each group ordered by board_rank; the 7 fixed statuses are the columns - [ ] Periodic rebalance is NOT needed in v1 (rank strings grow slowly at this scale); note as future work if strings exceed 64 chars - [ ] Rank-only moves emit `issue.moved` (audited, hidden from activity feeds, still broadcast over SSE for live board sync) ## List view semantics - [ ] Filters (combinable): status, assignee, label, priority, epic, parent, free-text `q` over title - [ ] Sort: project_issue_id (default desc), title, status, priority, estimate, assignee, target_date, updated_at - [ ] Pagination per API conventions; the board view is not paginated (whole project board ≤ a few hundred issues at target scale) ## Epics - [ ] Fields: title, description (markdown), status (`backlog…canceled`), color (palette hex) - [ ] Status automation mirrors projects (`completed` ↔ `completed_at` is not stored for epics — progress is computed, keep it simple) - [ ] Issue membership: `issues.epic_id` + `epic_rank` orders issues within the epic - [ ] `PUT /epics/{epicId}/issues/{num}` attaches (or moves, with `after_rank`); `DELETE` detaches (epic_id NULL) - [ ] An issue belongs to at most one epic; same project only - [ ] Progress (computed in the list/detail queries, no denormalized counters): - [ ] issue counts: total, closed (`done`+`canceled`+`duplicate`), open - [ ] story points: total estimated, closed estimated (unestimated issues counted separately as "unestimated: N") - [ ] percent = closed/total issues; UI progress bar shows both counts and points - [ ] Epic list sortable by status/title/progress; filter by status - [ ] Soft delete: epic disappears, issues keep `epic_id` until cull nulls it; UI treats a soft-deleted epic reference as none - [ ] Emit `epic.created/updated/status_changed/deleted`