5.4.1 Schema — `label` + `work_item_label`, `component` + `work_item_component`, `watcher` (+ Epic-6 join-predicate contract, repos)
Estimate: 22m
The persistence for all three features. Schema + migration + repo skeletons only.
Label — id, workspaceId, projectId, name (display casing as first typed), nameLower (the case-insensitive uniqueness key: @@unique([projectId, nameLower]) — the wart-fix decision), createdAt. WorkItemLabel — workItemId (Cascade) + labelId (Cascade), @@unique([workItemId, labelId]), @@index([labelId]) (the Epic-6 by-label join). Labels are rows so the join is FK-clean, but they live ONLY while used (the service deletes a label row when its last join row goes — folksonomy semantics; no orphan-label GC needed by design).
Component — id, workspaceId, projectId, name + nameLower (@@unique([projectId, nameLower])), description?, defaultAssigneeId? (FK → User, SetNull — a departed user clears the default, never blocks), timestamps. WorkItemComponent — workItemId (Cascade) + componentId (Restrict — the service must run the move-or-remove flow before a component deletes; the DB backstops), @@unique([workItemId, componentId]), @@index([componentId]).
Watcher — workItemId (Cascade), userId (Cascade — a deleted user stops watching), createdAt, @@unique([workItemId, userId]), @@index([userId]) (the "issues I watch" read 5.7/6.x will want). Every FK a two-sided @relation (CLAUDE.md). The Epic-6 contract documented (schema comment + note): JOIN work_item_label/work_item_component ON … predicate sketches over the [labelId]/[componentId] indexes.
Repo skeletons (single-op, writes require tx): label find-or-create reads (findByNameLower, searchByPrefix(projectId, q, take)), join add/remove, countUsesByLabel; component CRUD + listByProject + join add/remove + countItemsByComponent + reassignItems(fromId, toId, tx); watcher add/remove/listByWorkItem(paged)/existsFor.
Acceptance criteria
- The five models exist with the exact uniques/indexes/onDelete actions above (label joins Cascade; component join Restrict; defaultAssignee SetNull; watcher Cascade both sides); relations two-sided;
prisma migrate devre-run reports no drift. - The Epic-6 join-predicate contract is documented beside the 5.3.1 one.
- Repo methods exist as single ops; Vitest verifies the uniques (case-insensitive label/component names), the Restrict backstop, the cascades, and empty-input guards (coverage gate).
Context refs
prisma/schema.prismaconventions + the 5.3.1 contract-comment pattern;motir-core/CLAUDE.md(FK rule, required-tx)- The verified mirror semantics in the Story 5.4 description (folksonomy delete-on-last-use; move-or-remove; watcher view-access)
lib/repositories/conventions (workItemRepository paging shapes)