Files
solopm-server/plans/07-issues-epics.md

3.7 KiB

07 — Issues & Epics

The two work-item levels. Schema in 02-data-model.md; routes in 04-api.md; board/list UI in 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 (completedcompleted_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