148 lines
5.5 KiB
TypeScript
148 lines
5.5 KiB
TypeScript
import matter from "gray-matter";
|
|
import type { Decision, Document, Task } from "../types/index.ts";
|
|
import { normalizeAssignee } from "../utils/assignee.ts";
|
|
import { AcceptanceCriteriaManager, getStructuredSections, updateStructuredSections } from "./structured-sections.ts";
|
|
|
|
export function serializeTask(task: Task): string {
|
|
normalizeAssignee(task);
|
|
const frontmatter = {
|
|
id: task.id,
|
|
title: task.title,
|
|
status: task.status,
|
|
assignee: task.assignee,
|
|
...(task.reporter && { reporter: task.reporter }),
|
|
created_date: task.createdDate,
|
|
...(task.updatedDate && { updated_date: task.updatedDate }),
|
|
labels: task.labels,
|
|
...(task.milestone && { milestone: task.milestone }),
|
|
dependencies: task.dependencies,
|
|
...(task.parentTaskId && { parent_task_id: task.parentTaskId }),
|
|
...(task.subtasks && task.subtasks.length > 0 && { subtasks: task.subtasks }),
|
|
...(task.priority && { priority: task.priority }),
|
|
...(task.ordinal !== undefined && { ordinal: task.ordinal }),
|
|
...(task.onStatusChange && { onStatusChange: task.onStatusChange }),
|
|
...(task.statusHistory && task.statusHistory.length > 0 && { status_history: task.statusHistory }),
|
|
};
|
|
|
|
let contentBody = task.rawContent ?? "";
|
|
if (typeof task.description === "string" && task.description.trim() !== "") {
|
|
contentBody = updateTaskDescription(contentBody, task.description);
|
|
}
|
|
if (Array.isArray(task.acceptanceCriteriaItems)) {
|
|
const existingCriteria = AcceptanceCriteriaManager.parseAllCriteria(task.rawContent ?? "");
|
|
const hasExistingStructuredCriteria = existingCriteria.length > 0;
|
|
if (task.acceptanceCriteriaItems.length > 0 || hasExistingStructuredCriteria) {
|
|
contentBody = AcceptanceCriteriaManager.updateContent(contentBody, task.acceptanceCriteriaItems);
|
|
}
|
|
}
|
|
if (typeof task.implementationPlan === "string") {
|
|
contentBody = updateTaskImplementationPlan(contentBody, task.implementationPlan);
|
|
}
|
|
if (typeof task.implementationNotes === "string") {
|
|
contentBody = updateTaskImplementationNotes(contentBody, task.implementationNotes);
|
|
}
|
|
|
|
const serialized = matter.stringify(contentBody, frontmatter);
|
|
// Ensure there's a blank line between frontmatter and content
|
|
return serialized.replace(/^(---\n(?:.*\n)*?---)\n(?!$)/, "$1\n\n");
|
|
}
|
|
|
|
export function serializeDecision(decision: Decision): string {
|
|
const frontmatter = {
|
|
id: decision.id,
|
|
title: decision.title,
|
|
date: decision.date,
|
|
status: decision.status,
|
|
};
|
|
|
|
let content = `## Context\n\n${decision.context}\n\n`;
|
|
content += `## Decision\n\n${decision.decision}\n\n`;
|
|
content += `## Consequences\n\n${decision.consequences}`;
|
|
|
|
if (decision.alternatives) {
|
|
content += `\n\n## Alternatives\n\n${decision.alternatives}`;
|
|
}
|
|
|
|
return matter.stringify(content, frontmatter);
|
|
}
|
|
|
|
export function serializeDocument(document: Document): string {
|
|
const frontmatter = {
|
|
id: document.id,
|
|
title: document.title,
|
|
type: document.type,
|
|
created_date: document.createdDate,
|
|
...(document.updatedDate && { updated_date: document.updatedDate }),
|
|
...(document.tags && document.tags.length > 0 && { tags: document.tags }),
|
|
};
|
|
|
|
return matter.stringify(document.rawContent, frontmatter);
|
|
}
|
|
|
|
export function updateTaskAcceptanceCriteria(content: string, criteria: string[]): string {
|
|
// Normalize to LF while computing, preserve original EOL at return
|
|
const useCRLF = /\r\n/.test(content);
|
|
const src = content.replace(/\r\n/g, "\n");
|
|
// Find if there's already an Acceptance Criteria section
|
|
const criteriaRegex = /## Acceptance Criteria\s*\n([\s\S]*?)(?=\n## |$)/i;
|
|
const match = src.match(criteriaRegex);
|
|
|
|
const newCriteria = criteria.map((criterion) => `- [ ] ${criterion}`).join("\n");
|
|
const newSection = `## Acceptance Criteria\n\n${newCriteria}`;
|
|
|
|
let out: string | undefined;
|
|
if (match) {
|
|
// Replace existing section
|
|
out = src.replace(criteriaRegex, newSection);
|
|
} else {
|
|
// Add new section at the end
|
|
out = `${src}\n\n${newSection}`;
|
|
}
|
|
return useCRLF ? out.replace(/\n/g, "\r\n") : out;
|
|
}
|
|
|
|
export function updateTaskImplementationPlan(content: string, plan: string): string {
|
|
const sections = getStructuredSections(content);
|
|
return updateStructuredSections(content, {
|
|
description: sections.description ?? "",
|
|
implementationPlan: plan,
|
|
implementationNotes: sections.implementationNotes ?? "",
|
|
});
|
|
}
|
|
|
|
export function updateTaskImplementationNotes(content: string, notes: string): string {
|
|
const sections = getStructuredSections(content);
|
|
return updateStructuredSections(content, {
|
|
description: sections.description ?? "",
|
|
implementationPlan: sections.implementationPlan ?? "",
|
|
implementationNotes: notes,
|
|
});
|
|
}
|
|
|
|
export function appendTaskImplementationNotes(content: string, notesChunks: string | string[]): string {
|
|
const chunks = (Array.isArray(notesChunks) ? notesChunks : [notesChunks])
|
|
.map((c) => String(c))
|
|
.map((c) => c.replace(/\r\n/g, "\n"))
|
|
.map((c) => c.trim())
|
|
.filter(Boolean);
|
|
|
|
const sections = getStructuredSections(content);
|
|
const appendedBlock = chunks.join("\n\n");
|
|
const existingNotes = sections.implementationNotes?.trim();
|
|
const combined = existingNotes ? `${existingNotes}\n\n${appendedBlock}` : appendedBlock;
|
|
return updateStructuredSections(content, {
|
|
description: sections.description ?? "",
|
|
implementationPlan: sections.implementationPlan ?? "",
|
|
implementationNotes: combined,
|
|
});
|
|
}
|
|
|
|
export function updateTaskDescription(content: string, description: string): string {
|
|
const sections = getStructuredSections(content);
|
|
return updateStructuredSections(content, {
|
|
description,
|
|
implementationPlan: sections.implementationPlan ?? "",
|
|
implementationNotes: sections.implementationNotes ?? "",
|
|
});
|
|
}
|