( wsDir: string, id: string, author: string, text: string, )
| 176 | * Returns the re-validated record, or `not_found` when the file is absent. |
| 177 | */ |
| 178 | export async function appendIssueComment( |
| 179 | wsDir: string, |
| 180 | id: string, |
| 181 | author: string, |
| 182 | text: string, |
| 183 | ): Promise<MutateResult> { |
| 184 | if (!ID_RE.test(id)) return { ok: false, reason: 'not_found' } |
| 185 | const raw = await readWorkspaceFile(wsDir, relFor(id)) |
| 186 | if (raw === null) return { ok: false, reason: 'not_found' } |
| 187 | |
| 188 | const split = splitFrontmatter(raw) |
| 189 | if (!split) return { ok: false, reason: 'invalid', error: 'missing YAML frontmatter' } |
| 190 | // Validate the existing file so we don't grow a comment onto a broken issue. |
| 191 | const current = parseIssueContent(id, raw) |
| 192 | if (!current.ok) return { ok: false, reason: 'invalid', error: current.error } |
| 193 | |
| 194 | const stamp = new Date().toISOString() |
| 195 | const block = `**${author}** · ${stamp}\n\n${text.trim()}` |
| 196 | |
| 197 | const body = split.body |
| 198 | const hasSection = /^##\s+Comments\s*$/m.test(body) |
| 199 | const base = hasSection ? body : `${body}${body ? '\n\n' : ''}${COMMENTS_HEADING}` |
| 200 | const newBody = `${base}\n\n${block}` |
| 201 | |
| 202 | // Preserve the frontmatter raw (no re-serialize) — a comment is body-only. |
| 203 | const content = `---\n${split.frontmatter}\n---\n\n${newBody}\n` |
| 204 | const reparsed = parseIssueContent(id, content) |
| 205 | if (!reparsed.ok) return { ok: false, reason: 'invalid', error: reparsed.error } |
| 206 | await writeWorkspaceFile(wsDir, relFor(id), content) |
| 207 | return { ok: true, issue: reparsed.issue } |
| 208 | } |
| 209 | |
| 210 | /** |
| 211 | * Create a new issue file. Derives a kebab slug from `title` when `id` is |
no test coverage detected