5.4 Labels, components, watchers
The issue-organisation layer: labels (a project-scoped folksonomy — type to create, multi-valued, no admin ceremony), components (an admin-managed taxonomy with default assignees, the "which part of the product" axis), and watchers (follow an issue for notifications, distinct from assignee). All three persist as indexed join rows so Epic 6 filters on them with real predicates (the stub's constraint — the contract is documented in the schema subtask, the 5.3.1 pattern).
Labels (the Jira-verified folksonomy, project-scoped by deliberate deviation). Jira labels are site-global — and project scoping is an OPEN JIRA SUGGESTION (JRACLOUD-23656): the global pool leaking across projects is the mirror's documented pain. The stub pins project scope; recorded as the justified deviation users ask the mirror for. Mechanics mirrored exactly: labels are created by typing in the issue field (autocomplete over the project's existing labels + a "create" row), multi-valued, no spaces (hyphens — the Jira rule), and there is NO admin UI (rename/merge is Jira's documented gap; ours too — the Epic-6 extension). A label exists only while used: removing its last use deletes the row (unused labels disappear, the verified behaviour). One wart-fix: case-insensitive uniqueness per project (the filed 'Performance'/'performance' duplicate complaint), first-typed casing displayed. Second justified deviation — label chips are COLOURED (product owner, 2026-06-10): Jira's labels are colourless-enterprise; Motir is deliberately more colourful. The tint is auto-assigned deterministically from the label name (FNV-1a over nameLower, mod 6 — the seed-loader hash family) into the existing --el-tint-{peach,rose,mint,lavender,sky,yellow} pastels with --el-text-strong text (finding #35 AA) — NO colour column, NO picker, NO admin: the folksonomy stays type-to-create and the same label is the same colour on every surface (rail, picker, Epic-6 filters). User-picked colours = the documented extension.
Components (company-managed Jira is the shape mirror). Project-scoped, admin-managed at Project settings → Components (the 6.4 two-tier gate): name (required, case-insensitively unique), description?, defaultAssigneeId? — the verified Jira five-way default-assignee enum collapsed to a nullable user (no project-lead concept here; component lead = the documented extension). Issues carry multiple components. The default-assignee rule (verified): an issue CREATED with components and no assignee takes the default assignee of its first-alphabetical component that has one. Deleting a component with issues forces the verified choice: move those issues to another component, or just remove the association — issues untouched either way. Label/component changes write revision-trail diffs (the links-diff precedent: { labels: { added/removed } }).
Watchers (the verified contract). The eye + count control at the detail header's right toggles self-watch (keyboard W — the Jira shortcut) and opens the watchers popover (avatars + names; add/remove). Permission mapping: anyone who can VIEW the issue sees the list and watches themselves (watching is not editing — a viewer may watch); project admin + workspace admin/owner manage others (Jira's "Manage watchers"). A watcher MUST hold view access — Jira silently drops violators (a documented trap); we reject with a typed error. Auto-watch: creating or commenting on an issue watches it (the verified Jira personal-setting behaviour, constant-on; the opt-out preference is Story 5.7's). Watching writes no history (mirror). Watcher emails ride the 1.6 jobs on the channel-agnostic events: work-item/comment.created (5.1.6's event) and a new work-item/transitioned emitted post-commit by updateStatus (which today writes only the revision row — rung-2 fact). Scope: those two highest-signal events; the full notification-scheme matrix is the documented extension. The actor is never self-notified, and a watcher who was also @mentioned gets ONE email — the mention wins (cross-job dedupe: the watcher job skips the comment's mentioned users).
Bounded everywhere (finding #57). Label autocomplete is a bounded prefix read (take 20); the rail cards render this issue's rows (≤ caps); watcher fan-out pages through watchers; components list is project-scoped admin data. Adopted caps as guards: 100 labels per issue is absurd — per-issue labels capped at 20 (Jira has no documented per-issue cap; a sanity guard, recorded), components per project uncapped but listed bounded, watchers paged.
Completeness — the real-product states. Labels: the create-row in the picker ("Create 'perf-q3'"), the no-spaces inline error, the empty rail placeholder. Components: empty admin state, the in-use delete dialog (move-or-remove with a target picker + counts), default-assignee at-create visible in the revision trail. Watchers: count updates optimistically, the manage-others affordance only for admins, the typed no-view-access error surfaced inline, viewer read-only states everywhere. All drawn by 5.4.6/5.4.7, asserted in 5.4.11.
Out of scope (documented extension slots, each justified): label rename/merge admin (Jira's own gap; Epic-6 admin territory if a use case lands); component lead + the five-way default-assignee enum (no project-lead concept; the nullable user covers the use case); voting (Jira's sibling feature — no use case for a small-team tool, "no complexity for nothing"); per-user notification preferences + the in-app bell (Story 5.7); watcher notifications on every field edit (the full scheme matrix — extension); user-PICKED label colours (the auto name-hash tint ships in this story — the recorded less-enterprise deviation; a color column + picker is the additive extension).
Verification
- Pull the Story branch,
pnpm install,pnpm prisma migrate dev(applies the 5.4.1 label/component/watcher migration cleanly; re-run reports "No difference detected"),pnpm db:seed,pnpm dev. pnpm test:coverage— Vitest (real Postgres) over the three services (folksonomy lifecycle, component CRUD + default-assignee + move-or-remove, watcher permissions + auto-watch) ≥90% per-file branch/fn/line; empty-input guards on new repo methods.- Labels flow: sign in as
zhuyue@motir.co/!QAZ1qaz, open an issue → the Labels rail card (matchingdesign/work-items/labels-components-watch.mock.html). Typeperf-q3→ the "Create" row adds it as a chip; typePERF-Q3on another issue → autocomplete offers the SAME label (case-insensitive, original casing shown); a label with a space is rejected inline; remove the chip from every issue → it stops appearing in autocomplete; the revision trail records added/removed. - Components flow: Project settings → Components (matching
design/projects/components.mock.html): create "API" (default assignee Bo) and "Web" (no default); assign both to an issue via the rail card; create a NEW issue with component API and no assignee → it lands assigned to Bo (first-alphabetical rule when multiple); delete "API" while in use → the dialog forces move-to-"Web" or remove, counts shown, issues survive. Non-admin member: the admin page is read-only. - Watchers flow: the eye + count sits in the detail header; click (or press
W) → you watch, count bumps; the popover lists watchers; as project admin addodie@motir.co→ listed; adding a user who cannot view a private-project issue is rejected with the inline error (not silently dropped); creating an issue auto-watches you; commenting auto-watches you; aviewercan watch themselves but sees no manage affordances. - Notifications: with Bo watching, a comment by the PM → the dev email console shows the watcher email to Bo and NONE to the PM (actor) — and if Bo was also @mentioned, ONLY the mention email arrives (dedupe); a status transition fires the transition email to watchers, never the actor; replaying the Inngest event double-sends nothing (idempotent).
- Epic-6 seam: the documented join-predicate contract exists (label/component/watcher joins + indexes); a raw SQL spot-check filters issues by label and by component using the indexes.
pnpm test:e2e --grep labels-components-watch— Playwright over the real stack: the three flows above end-to-end.- a11y check: the chip picker (combobox-with-chips), the watchers popover, and the Components admin pass the strict axe sweep;
Wshortcut documented; state conveyed as text; colour via--el-*, shape via element tokens.