{"record":{"id":"0aa0ff6d1a66999c","repo":"nexu-io/open-design","slug":"comment-note-required","errorCode":null,"errorMessage":"comment note required","messagePattern":"comment note required","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/db.ts","lineNumber":2917,"sourceCode":"    typeof input?.id === 'string' && input.id.trim()\n      ? input.id.trim()\n      : null;\n  const now = Date.now();\n  const existing = requestedId\n    ? db\n        .prepare(\n          `SELECT id, created_at AS createdAt, attachments_json AS attachmentsJson\n             FROM preview_comments\n            WHERE id = ? AND project_id = ? AND conversation_id = ?`,\n        )\n        .get(requestedId, projectId, conversationId) as DbRow | undefined\n    : undefined;\n  const id = existing?.id ?? requestedId ?? randomCommentId();\n  const createdAt = existing?.createdAt ?? now;\n  const existingAttachments = normalizePreviewCommentAttachments(parseJsonOrUndef(existing?.attachmentsJson));\n  const attachments = attachmentsProvided ? incomingAttachments : existingAttachments;\n  // A comment must carry either a note or at least one image attachment.\n  if (!note && attachments.length === 0) throw new Error('comment note required');\n  // pin_seq / pin_seq_confirmed / sort_key are assigned exactly once, on the\n  // INSERT branch, and are absent from the ON CONFLICT SET clause below so an\n  // edit (existing !== undefined) never rewrites them — see\n  // recvq5BVsolIxi / UpsertPreviewCommentOptions above. Computed against THIS\n  // db file only: safe as the initial guess even when a sibling device\n  // concurrently computes the same number for its own new comment, because a\n  // team-shared project's pin_seq_confirmed=0 row gets reconciled to the\n  // collab-cloud's globally-serialized seq by confirmPreviewCommentPinSeq\n  // once its push resolves (never by recomputing locally again).\n  let pinSeq: number | null = null;\n  let sortKey: number | null = null;\n  let pinSeqConfirmed = 1;\n  if (!existing) {\n    const pinScope = db\n      .prepare(\n        `SELECT COALESCE(MAX(pin_seq), 0) AS maxPinSeq\n           FROM preview_comments\n          WHERE project_id = ? AND file_path = ?`,","sourceCodeStart":2899,"sourceCodeEnd":2935,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/db.ts#L2899-L2935","documentation":"Thrown by upsertPreviewComment() when a comment being inserted/updated has no note text AND zero image attachments. The invariant: a preview comment must carry either text or at least one image; an empty comment is rejected as invalid. Existing comments being edited also pass through this gate (attachments default to existingAttachments when not provided, so clearing both note and attachments on edit trips it too).","triggerScenarios":"Calling the comment upsert with an empty/whitespace note and no attachments array (or an empty one); editing a comment and stripping both the text and all attachments.","commonSituations":"UI submitted before the user typed anything; an agent created a pin-only comment without attaching a screenshot; an edit request cleared the note but did not preserve existing attachments.","solutions":["Provide non-empty note text, OR at least one image attachment in the attachments array.","When editing, pass attachments explicitly if you want to clear the note (and vice-versa).","Validate client-side before POST: require (note?.trim() || attachments.length > 0)."],"exampleFix":"// before: empty comment\nupsertPreviewComment(db, p, c, { note: '', attachments: [] });\n\n// after: require note or at least one attachment\nif (!note?.trim() && attachments.length === 0) {\n  throw new Error('comment needs a note or an attachment');\n}\nupsertPreviewComment(db, p, c, { note, attachments });","handlingStrategy":"validation","validationCode":"function validateCommentInput(note: string | undefined, attachments: unknown[]) {\n  if (!note?.trim() && attachments.length === 0) {\n    throw new Error('comment needs a note or at least one attachment');\n  }\n}","typeGuard":"function commentHasContent(note: string | undefined, attachments: unknown[]): boolean {\n  return !!note?.trim() || attachments.length > 0;\n}","tryCatchPattern":"try { upsertPreviewComment(db, p, c, input); }\ncatch (e) {\n  if (e instanceof Error && /comment note required/.test(e.message)) {\n    // surface 'note or attachment required' to the UI; do not silent-drop\n  } else throw e;\n}","preventionTips":["Disable the submit button until the comment has a note or an attachment.","Validate (note?.trim() || attachments.length > 0) client-side before POST.","When editing, pass both note and attachments explicitly to avoid clearing one accidentally."],"tags":["db","comments","validation","data-integrity"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}