1.4 Work-item (issue) data model
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 withchangeKind = '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 confirmsexplanationSource = user_editedauto-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'sdescriptionHtml+explanationHtmlfields 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'}. HitGET /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_CYCLEin 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.