[
{ "id": "F001", "description": "Migration: create comment_threads table with tenant/thread_id PK, polymorphic ticket_id/project_task_id (CHECK exactly one), root_comment_id, is_internal, reply_count, last_activity_at, email_message_id, email_references[], email_provider_thread_id, FKs to tickets and project_tasks", "implemented": true, "prdRefs": ["Data / API / Integrations"] },
{ "id": "F002", "description": "Migration: indexes on comment_threads — (tenant, ticket_id, last_activity_at DESC), (tenant, project_task_id, last_activity_at DESC), partial (tenant, email_message_id) WHERE email_message_id IS NOT NULL", "implemented": true, "prdRefs": ["Non-functional Requirements"] },
{ "id": "F003", "description": "Migration: add thread_id (nullable), parent_comment_id (nullable), deleted_at (nullable) columns to comments; FKs to comment_threads and self-FK to comments", "implemented": true, "prdRefs": ["Data / API / Integrations"] },
{ "id": "F004", "description": "Migration: add thread_id (nullable), parent_comment_id (nullable), deleted_at (nullable) columns to project_task_comments; analogous FKs", "implemented": true, "prdRefs": ["Data / API / Integrations"] },
{ "id": "F005", "description": "Migration: backfill — for each existing comments row, insert a comment_threads row (root_comment_id=comment_id, is_internal=root.is_internal, reply_count=0, last_activity_at=created_at, email_message_id=metadata->'email'->>'messageId') and set comment.thread_id; chunked + idempotent", "implemented": true, "prdRefs": ["Functional Requirements §3", "Rollout / Migration"] },
{ "id": "F006", "description": "Migration: backfill the same way for project_task_comments → comment_threads with project_task_id set; chunked + idempotent", "implemented": true, "prdRefs": ["Functional Requirements §3", "Rollout / Migration"] },
{ "id": "F007", "description": "Migration: enforce NOT NULL on comments.thread_id and project_task_comments.thread_id (separate migration after backfill)", "implemented": true, "prdRefs": ["Rollout / Migration"] },
{ "id": "F008", "description": "Migration: add comment_thread_id column on email_sending_logs (FK to comment_threads.thread_id); leave existing thread_id column as email_provider_thread_id (or rename in same migration if safe)", "implemented": true, "prdRefs": ["Functional Requirements §11"] },
{ "id": "F009", "description": "Types: create ICommentThread interface in packages/types/src/interfaces/commentThread.interface.ts covering all comment_threads columns", "implemented": true, "prdRefs": ["TypeScript Interfaces"] },
{ "id": "F010", "description": "Types: extend IComment with thread_id (string), parent_comment_id (string|null), deleted_at (string|null)", "implemented": true, "prdRefs": ["TypeScript Interfaces"] },
{ "id": "F011", "description": "Types: extend IProjectTaskComment with thread_id, parent_comment_id, deleted_at", "implemented": true, "prdRefs": ["TypeScript Interfaces"] },
{ "id": "F012", "description": "Model: comments.createComment — when parent_comment_id NULL, create a new comment_threads row in same transaction, set comment.thread_id to new id, root_comment_id to comment_id", "implemented": true, "prdRefs": ["Functional Requirements §4"] },
{ "id": "F013", "description": "Model: comments.createComment — when parent_comment_id set, look up parent (same tenant + ticket), inherit thread_id, validate parent not deleted, validate is_internal compatibility with thread root", "implemented": true, "prdRefs": ["Functional Requirements §5", "Security / Permissions"] },
{ "id": "F014", "description": "Model: comments — on insert of a reply, increment comment_threads.reply_count and bump last_activity_at in the same transaction", "implemented": true, "prdRefs": ["Functional Requirements §5", "Non-functional Requirements"] },
{ "id": "F015", "description": "Model: comments.deleteComment — if leaf (no children), hard-delete and decrement reply_count; if root-with-children or interior, soft-delete (set deleted_at, replace note with '[deleted]')", "implemented": true, "prdRefs": ["Functional Requirements §6"] },
{ "id": "F016", "description": "Model: comments.findCommentsByTicketId returns thread_id + parent_comment_id + deleted_at; ordered by created_at asc; includes soft-deleted comments so tree stays well-formed", "implemented": true, "prdRefs": ["Functional Requirements §6"] },
{ "id": "F017", "description": "Model: projectTaskComment.createTaskComment — same NULL-parent thread creation flow", "implemented": true, "prdRefs": ["Functional Requirements §4", "Functional Requirements §12"] },
{ "id": "F018", "description": "Model: projectTaskComment.createTaskComment — same parent-inheritance flow with validation", "implemented": true, "prdRefs": ["Functional Requirements §5"] },
{ "id": "F019", "description": "Model: projectTaskComment — reply_count + last_activity_at maintenance on insert", "implemented": true, "prdRefs": ["Functional Requirements §5"] },
{ "id": "F020", "description": "Model: projectTaskComment.deleteTaskComment — leaf vs root-with-children soft-delete behavior mirroring tickets", "implemented": true, "prdRefs": ["Functional Requirements §6"] },
{ "id": "F021", "description": "Model: projectTaskComment.getTaskComments returns thread_id + parent_comment_id + deleted_at", "implemented": true, "prdRefs": ["Functional Requirements §6"] },
{ "id": "F022", "description": "Action: commentActions.createComment accepts optional parent_comment_id in payload; passes through to model", "implemented": true, "prdRefs": ["Server Actions"] },
{ "id": "F023", "description": "Action: commentActions — TICKET_COMMENT_ADDED event payload includes thread_id, parent_comment_id, is_reply", "implemented": true, "prdRefs": ["Server Actions"] },
{ "id": "F024", "description": "Action: commentActions — RBAC reaffirms is_internal compatibility with thread root and rejects internal reply on client thread (or vice versa)", "implemented": true, "prdRefs": ["Security / Permissions"] },
{ "id": "F025", "description": "Action: projectTaskCommentActions.createTaskComment accepts optional parent_comment_id", "implemented": true, "prdRefs": ["Server Actions"] },
{ "id": "F026", "description": "Action: projectTaskCommentActions — TASK_COMMENT_ADDED event payload includes thread_id, parent_comment_id, is_reply", "implemented": true, "prdRefs": ["Server Actions"] },
{ "id": "F027", "description": "Action: projectTaskCommentActions.assertOwnCommentOrInternalUser handles replies (own reply or internal user)", "implemented": true, "prdRefs": ["Security / Permissions"] },
{ "id": "F028", "description": "Email inbound: reply-token resolution branch — when token resolves to a comment, derive thread_id from that comment and create reply as child of latest comment in thread", "implemented": true, "prdRefs": ["Functional Requirements §10.1"] },
{ "id": "F029", "description": "Email inbound: In-Reply-To resolution — look up rfc_message_id in email_sending_logs, get comment_thread_id, create reply as child of latest comment in thread", "implemented": true, "prdRefs": ["Functional Requirements §10.2"] },
{ "id": "F030", "description": "Email inbound: References[] chain walk — iterate from last to first; first match in email_sending_logs.rfc_message_id wins", "implemented": true, "prdRefs": ["Functional Requirements §10.3"] },
{ "id": "F031", "description": "Email inbound: provider thread id match — direct equality against comment_threads.email_provider_thread_id", "implemented": true, "prdRefs": ["Functional Requirements §10.4"] },
{ "id": "F032", "description": "Email inbound: fallback — existing ticket-level match creates a NEW top-level thread on the matched ticket (preserves today's UX)", "implemented": true, "prdRefs": ["Functional Requirements §10.5"] },
{ "id": "F033", "description": "Email inbound: comment creation populates thread_id + parent_comment_id from resolved thread (parent = latest comment in thread)", "implemented": true, "prdRefs": ["Functional Requirements §10"] },
{ "id": "F034", "description": "Email outbound: when sending a top-level comment-as-email, generate a fresh RFC Message-ID and persist into comment_threads.email_message_id; no In-Reply-To header", "implemented": true, "prdRefs": ["Functional Requirements §11"] },
{ "id": "F035", "description": "Email outbound: when sending a reply in an existing thread, set In-Reply-To to the latest outbound rfc_message_id for that thread (lookup email_sending_logs by comment_thread_id, ORDER BY created_at DESC LIMIT 1)", "implemented": true, "prdRefs": ["Functional Requirements §11"] },
{ "id": "F036", "description": "Email outbound: accumulate References[] header — append to comment_threads.email_references and emit the array as RFC References", "implemented": true, "prdRefs": ["Functional Requirements §11"] },
{ "id": "F037", "description": "Email outbound: issue a reply_token in email_reply_tokens scoped to the new outbound comment_id; embed [ALGA-REPLY-TOKEN:...] marker as today", "implemented": true, "prdRefs": ["Functional Requirements §11"] },
{ "id": "F038", "description": "Email outbound: persist email_sending_logs row with comment_thread_id, rfc_message_id, reply_token_hash, provider_message_id", "implemented": true, "prdRefs": ["Functional Requirements §11"] },
{ "id": "F039", "description": "Shared UI: new CommentThreadList component that takes flat comments[], groups by thread_id, renders one HybridThreadGroup per thread sorted by thread.last_activity_at", "implemented": true, "prdRefs": ["UX / UI Notes"] },
{ "id": "F040", "description": "Shared UI: new HybridThreadNode (recursive) — renders a comment, then thread bar (if has children), then indented children. Caps margin-left at depth 4", "implemented": true, "prdRefs": ["UX / UI Notes", "Functional Requirements §13"] },
{ "id": "F041", "description": "Shared UI: HybridThreadNode collapse state — Collapse button when expanded; Expand + Open in drawer when collapsed", "implemented": true, "prdRefs": ["Users and Primary Flows §2"] },
{ "id": "F042", "description": "Shared UI: HybridThreadNode 'Open in drawer' action — calls onOpenPanel(commentId) prop, lifted to parent container", "implemented": true, "prdRefs": ["Users and Primary Flows §3"] },
{ "id": "F043", "description": "Shared UI: InlineReplyComposer — small composer rendered under a comment when Reply is clicked; uses TextEditor (BlockNote); shows Mark-as-Internal switch only (no Mark-as-Resolution); inherits parent's is_internal as default", "implemented": true, "prdRefs": ["Functional Requirements §8", "Functional Requirements §9"] },
{ "id": "F044", "description": "Shared UI: CommentThreadDrawer — opens via existing packages/ui Drawer; shows thread head + all replies + bottom composer; close returns to inline view", "implemented": true, "prdRefs": ["Users and Primary Flows §3"] },
{ "id": "F045", "description": "Shared UI: CommentThread.module.css — indent left rail (2px #E5E7EB), thread-bar pill, depth-{1..4} indent classes (cap), sub-thread dashed-border style, hover Reply button styles", "implemented": true, "prdRefs": ["UX / UI Notes"] },
{ "id": "F046", "description": "Tickets UI: CommentItem — add Reply button (CornerUpLeft icon) to the hover action row, between React and Pencil; calls onReply(comment) prop", "implemented": true, "prdRefs": ["UX / UI Notes", "Users and Primary Flows §1"] },
{ "id": "F047", "description": "Tickets UI: TicketConversation — replace flat .map render with ; preserve existing tabs, composer, sort toggle", "implemented": true, "prdRefs": ["Users and Primary Flows §1"] },
{ "id": "F048", "description": "Tickets UI: TicketConversation — tab filtering operates on threads (All / Client / Internal / Resolution per PRD rules)", "implemented": true, "prdRefs": ["Functional Requirements §7"] },
{ "id": "F049", "description": "Tickets UI: TicketConversation — sort order toggle reorders threads by last_activity_at; within-thread replies stay chronological", "implemented": true, "prdRefs": ["UX / UI Notes"] },
{ "id": "F050", "description": "Tickets UI: TicketConversation — wire drawer state (openPanelRoot) and pass onOpenPanel down through CommentThreadList", "implemented": true, "prdRefs": ["Users and Primary Flows §3"] },
{ "id": "F051", "description": "Tickets UI: hovering a comment shows Reply; clicking opens InlineReplyComposer beneath the comment", "implemented": true, "prdRefs": ["Users and Primary Flows §1"] },
{ "id": "F052", "description": "Tickets UI: submitting an inline reply calls commentActions.createComment with parent_comment_id, refreshes the local conversation list, closes the composer", "implemented": true, "prdRefs": ["Users and Primary Flows §1"] },
{ "id": "F053", "description": "Tickets UI: when drawer is open, replying inside drawer also calls createComment with parent_comment_id; on close, inline list reflects the new reply", "implemented": true, "prdRefs": ["Users and Primary Flows §3"] },
{ "id": "F054", "description": "Project Tasks UI: TaskComment — add Reply button on hover", "implemented": true, "prdRefs": ["Functional Requirements §12"] },
{ "id": "F055", "description": "Project Tasks UI: TaskCommentThread — replace flat list render with ", "implemented": true, "prdRefs": ["Functional Requirements §12"] },
{ "id": "F056", "description": "Project Tasks UI: TaskCommentForm — pass parent_comment_id when invoked as reply; default inherits is_internal=false (tasks have no internal/client split)", "implemented": true, "prdRefs": ["Functional Requirements §12"] },
{ "id": "F057", "description": "Project Tasks UI: drawer wiring identical to ticket flow", "implemented": true, "prdRefs": ["Users and Primary Flows §3"] },
{ "id": "F058", "description": "UI: soft-deleted comments render as '[deleted]' placeholder with reduced opacity; Reply button suppressed; preserves tree structure", "implemented": true, "prdRefs": ["Functional Requirements §6"] },
{ "id": "F059", "description": "UI: depth-4 cap in CSS — .depth-4 .thread-children { margin-left: 16px; } and no further indent past it; data still nests", "implemented": true, "prdRefs": ["Functional Requirements §13"] },
{ "id": "F060", "description": "UI: subthread bars (depth >= 1) use dashed border + white background per design; indigo-violet count color", "implemented": true, "prdRefs": ["UX / UI Notes"] },
{ "id": "F061", "description": "Persistence: ticket conversation 'oldest/newest first' preference applies at thread granularity (existing ticketConversationOrderPreference helper continues to work; just consumed at thread sort time)", "implemented": true, "prdRefs": ["UX / UI Notes"] },
{ "id": "F062", "description": "Seed compatibility: update dev comment seed to create comment_threads rows and set comments.thread_id after NOT NULL enforcement", "implemented": true, "prdRefs": ["Rollout / Migration"] }
]