MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-43

1.4 Work-item (issue) data model

Done
Description

The work_item table — the unit of tracked work in Motir's PM core. Every epic, story, task, bug, and subtask a team plans in a project is a row in this table. Self-referencing tree (parent_id) with DB-level kind-parent rules and a depth limit. Carries the two user-visible content axes — descriptionMd ("what to do") and explanationMd ("why this matters") — both Markdown-source, GFM-rendered, with explanation provenance tracked (user_authored / ai_draft / user_edited) so the UI can surface "AI-drafted, review me" hints to non-technical reviewers. Wires Story 1.3's per-project key counter to allocate human-readable identifiers (PROD-42). Ships the schema, repository, service, RLS, dependency join table (work_item_link), and revision-history audit trail that every PM-core Epic (2 through 6) and the AI Planning Layer (Epic 7) build on. Reframed from the May 2026 plan revision: this Story models the tracked work issue tree (Jira's domain), not the AI planning meta-tree (Epic 7's domain — see Scope).

Prerequisites: Story 1.3 (Projects) must be complete — work_item FKs against Project with onDelete: Cascade, the RLS policies key off the same app.workspace_id session GUC that 1.2.3 established, and the issue-key allocator (PROD-42) calls projectRepository.allocateWorkItemNumber shipped in 1.3.1. Story 1.2 (Workspaces)'s RLS pattern is the structural model this Story copies. Story 1.0.5 (Design system) must be complete before 1.4.1 (mockup) — the issue-detail view composes the canonical Button, Input, Textarea, Select, Card, Avatar, Badge primitives. All code follows motir-core/CLAUDE.md's 4-layer rule (Route → Service → Repository → Prisma).

Verification

Functional verification — run the data layer end-to-end through the test endpoint added in 1.4.8. No production UI yet (that's Epic 2); this recipe exercises the service via HTTP + Prisma Studio + inspection.

  • Pull the merged Story branch; pnpm install && pnpm prisma migrate dev && pnpm dev.
  • Sign in as user A (use the seeded account from Story 1.1's E2E fixtures); confirm a workspace + project exist.
  • Hit the test endpoint to create an epic: curl -X POST .../api/_test/work-items -d '{"kind":"epic","title":"Q3 launch","projectId":"..."}' --cookie session. Note the returned identifier (e.g. PROD-1).
  • Open Prisma Studio (pnpm prisma studio); confirm the work_item row exists with the expected fields and that a work_item_revision row exists with changeKind = 'created'.
  • Create a story under the epic; create a subtask under the story; try to create a story under the subtask — expect a 4xx with IllegalParentTypeError.
  • Try to create a 5th-level descendant — expect DepthLimitExceededError.
  • Update the epic's title via PATCH; refresh Prisma Studio — confirm a new revision row with diff { title: { from: "Q3 launch", to: ... } }.
  • Explanation-source check: PATCH the epic with {"explanationMd": "## Why this matters\n\nThis launch...", "explanationSource": "ai_draft"} (simulating Epic 7's AI-drafting endpoint). GET the epic — confirm both fields stored. PATCH again with only {"explanationMd": "## Updated\n\n..."} (no explicit source) — GET confirms explanationSource = user_edited auto-transitioned. Open Prisma Studio — confirm three revision rows for the epic with the right diffs.
  • Markdown render check: hit GET /api/_test/work-items/{id}?render=1 — response body's descriptionHtml + explanationHtml fields contain sanitized HTML; verify any <script> tags in the Markdown source are stripped from the HTML output. Open a code-block test fixture, confirm syntax highlighting markup is present.
  • Dependency check: create two more issues Y and Z; link the epic is_blocked_by Y and is_blocked_by Z via POST /api/_test/work-item-links {fromId: epic.id, toId: Y.id, kind: 'is_blocked_by'}. Hit GET /api/_test/work-item-links?workItemId=epic.id&ready=1 — expect {ready: false}. PATCH Y and Z to status="done". Re-hit the ready endpoint — expect {ready: true}.
  • Cycle check: try to link Y is_blocked_by the epic — expect a 4xx with WI_LINK_CYCLE in the body.
  • Sign out, sign in as user B (in a different workspace from the seed fixtures); try to GET the epic by its ID — expect 404. Try to GET any of A's links — expect empty results.
  • Optional: open two terminals, fire 20 concurrent POSTs against the test endpoint, confirm the resulting keys are 1..20 with no duplicates.
  • Mark accepted if every step matches; mark needs-changes with notes if not.
Status
Done
Type
Story