Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-266Done

5.1.2 `commentsService` — add/edit/delete/list with 6.4-role permissions, server-side mention parsing + `comment_mention` rows, paged reads, job events

Estimate: 35m · Depends on: 5.1.1

The business-logic core. Per the 4-layer rule: lib/services/commentsService.ts owning validation, transactions, DTO mapping, and typed errors (lib/comments/errors.ts); HTTP-only routes; the repos from 5.1.1.

addComment(workItemId, { bodyMd, parentCommentId? }, ctx) — validates the caller can view the issue AND holds a commenting role (project admin/member via the 6.4 access logic; the read-only viewer gets a typed CommentForbiddenError — the Jira "Add comments" permission mapped onto the shipped role model). A parentCommentId must point at a ROOT comment on the SAME issue (a reply-to-a-reply is attached to the root by the UI, 5.1.5; the service rejects depth >1 with a typed error — single-level threading). In ONE transaction: create the comment, parse mentions (below), write comment_mention rows. After commit: emit the work-item/comment.created job event (workspaceId, workItemId, commentId, authorId, mentionedUserIds) via the 1.6 sendEvent — events NEVER fire inside the tx (a rollback must not have notified anyone).

Mention parsing — the service is the authority. A shared helper (lib/mentions/parse.ts) extracts [@Name](mention:<userId>) tokens from Markdown. For each id: workspace member AND can view this issue (private projects → project members; open/limited → workspace members — the 6.4 assignableMembersService scoping, reused not duplicated); failures are silently DROPPED from the mention set (the Jira rule — no view permission, no notification), never an error. Dedup repeated tokens. On EDIT, re-parse and diff: newly-added mentions get comment_mention rows + a follow-up event carrying ONLY the new ids (no re-notify on unchanged mentions).

editComment / deleteComment — the Jira permission split. Edit own = author; edit all / delete all = project admin or workspace admin/owner; delete own = author. Edit sets editedAt (the "Edited" tag). Delete is a hard delete — the row (and, for a root, its replies + mention rows, via the 5.1.1 cascade) is gone; in the same tx write a work_item_revision entry recording that a comment by X was deleted by Y (count of replies included) — the History trace Jira keeps and Story 5.5 renders. The deliberate root-delete-cascades decision (mirror behaviour unverifiable) is recorded here: confirm copy in 5.1.5 names the reply count.

listComments(workItemId, { cursor?, order? }, ctx) — view-gated, cursor-paged from the most recent (take 20) with totalCount, returning roots WITH their replies (a thread loads whole — replies are bounded by single-level threading) ordered per order (default oldest-first, the Jira default). NEVER a load-all (finding #57). DTOs carry author (id/name/image), body, editedAt, reply nesting, and mention metadata.

Routes (HTTP-only): GET/POST /api/work-items/[id]/comments, PATCH/DELETE /api/comments/[id] — parse → one service call → typed-error→status mapping (403 forbidden / 404 cross-workspace-invisible per finding #44 — never leak existence).

Acceptance criteria

  • commentsService ships add/edit/delete/list with the permission matrix: viewer cannot add; author edits/deletes own; project admin + workspace admin/owner edit/delete all; all writes view-gated; cross-workspace access reads as 404 (finding #44).
  • Threading: replies attach to roots only (depth >1 → typed error); a root delete cascades its thread and writes the revision-trail deletion record in the same tx.
  • Mention parsing: [@Name](mention:<id>) tokens → deduped, view-validated comment_mention rows in the same tx; non-viewable/non-member ids dropped silently; edit re-parse notifies only NEW mentions; work-item/comment.created (typed in JobEventDataMap) emits AFTER commit, never on rollback.
  • listComments is cursor-paged (take 20) + totalCount, threads load whole, order param flips oldest/newest-first; no unbounded read exists on any path.
  • Edit sets editedAt; the DTO exposes it; routes are HTTP-only; pnpm test:coverage keeps the new files ≥90% branch/fn/line with direct empty-input guards (the coverage gate).

Context refs

  • 5.1.1 repos + models; lib/services/workItemsService.ts — service conventions, the finding-#26 workspace gate, the finding-#44 404-not-403 rule
  • lib/services/assignableMembersService.ts (6.4) — the EXACT view-scoping the mention validation + candidate read reuse
  • lib/services/workItemRevisionsService.ts (1.4.6) — the revision-trail write for the deletion record
  • lib/jobs/sendEvent.ts + lib/jobs/types.ts (1.6) — typed event emission (extend JobEventDataMap); motir-core/CLAUDE.md — one service method = one transaction
  • Story 5.1 description — the Jira-verified permission set, threading, hard-delete + History-trace semantics