import { CiMetadata, CommitType, CommitEscalation, ParsedCiCommit, parseCommitType, parseCommitScope, } from "../types/commit-meta.js"; const CI_BLOCK_START = "---ci---"; const CI_BLOCK_END = "---/ci---"; export function extractCiBlock(message: string): string | null { const startIdx = message.indexOf(CI_BLOCK_START); if (startIdx < 0) return null; const endIdx = message.indexOf(CI_BLOCK_END, startIdx); if (endIdx < 0) return null; return message.slice(startIdx + CI_BLOCK_START.length, endIdx).trim(); } export function parseCiBlock(yaml: string): CiMetadata | null { if (!yaml) return null; const result: Partial = {}; const phaseMatch = yaml.match(/^phase:\s*(.+)$/m); if (phaseMatch) result.phase = parseInt(phaseMatch[1], 10) || 0; const milestoneMatch = yaml.match(/^milestone:\s*(.+)$/m); if (milestoneMatch) result.milestone = milestoneMatch[1].trim(); const planMatch = yaml.match(/^plan:\s*(.+)$/m); if (planMatch) result.plan = planMatch[1].trim(); const taskMatch = yaml.match(/^task:\s*(.+)$/m); if (taskMatch) result.task = taskMatch[1].trim(); const statusMatch = yaml.match(/^status:\s*(.+)$/m); if (statusMatch) result.status = statusMatch[1].trim() as CiMetadata["status"]; const projectMatch = yaml.match(/^project:\s*(.+)$/m); if (projectMatch) result.project = projectMatch[1].trim(); result.decisions = parseDecisionsFromYaml(yaml); result.escalations = parseEscalationsFromYaml(yaml); result.requirements = parseRequirementsFromYaml(yaml); result.lessons = parseLessonsFromYaml(yaml); result.compound = parseCompoundFromYaml(yaml); if (result.phase !== undefined && result.milestone !== undefined && result.status !== undefined) { return result as CiMetadata; } return null; } function parseDecisionsFromYaml(yaml: string): CiMetadata["decisions"] { const decisions: NonNullable = []; const decisionRegex = /- id: (.+)\n\s+decision: (.+)\n\s+rationale: (.+)\n\s+confidence: (.+)\n\s+alternatives: \[([^\]]*)\]/g; let match; while ((match = decisionRegex.exec(yaml)) !== null) { decisions.push({ id: match[1].trim(), decision: match[2].trim(), rationale: match[3].trim(), confidence: parseFloat(match[4].trim()), alternatives: match[5].trim().split(",").map((a: string) => a.trim()).filter(Boolean), }); } return decisions.length > 0 ? decisions : undefined; } function parseEscalationsFromYaml(yaml: string): CiMetadata["escalations"] { const escalations: NonNullable = []; const escalationRegex = /- id: (.+)\n\s+type: (.+)\n\s+description: (.+)\n\s+resolution: (.+)/g; let match; while ((match = escalationRegex.exec(yaml)) !== null) { escalations.push({ id: match[1].trim(), type: match[2].trim(), description: match[3].trim(), resolution: match[4].trim() as CommitEscalation["resolution"], }); } return escalations.length > 0 ? escalations : undefined; } function parseRequirementsFromYaml(yaml: string): CiMetadata["requirements"] { const coveredMatch = yaml.match(/^\s+covered: \[([^\]]*)\]/m); const partialMatch = yaml.match(/^\s+partial: \[([^\]]*)\]/m); const covered = coveredMatch ? coveredMatch[1].split(",").map((s: string) => s.trim()).filter(Boolean) : []; const partial = partialMatch ? partialMatch[1].split(",").map((s: string) => s.trim()).filter(Boolean) : []; if (covered.length === 0 && partial.length === 0) return undefined; return { covered, partial }; } function parseLessonsFromYaml(yaml: string): CiMetadata["lessons"] { const lessonRegex = /^ - (.+)$/gm; const lessons: string[] = []; let inLessonsSection = false; for (const line of yaml.split("\n")) { if (/^lessons:/.test(line.trim())) { inLessonsSection = true; continue; } if (inLessonsSection && /^ - .+/.test(line)) { lessons.push(line.replace(/^ - /, "").trim()); } else if (inLessonsSection && !/^ - /.test(line) && !/^$/.test(line)) { inLessonsSection = false; } } return lessons.length > 0 ? lessons : undefined; } function parseCompoundFromYaml(yaml: string): CiMetadata["compound"] { const categoryMatch = yaml.match(/^\s+category: (.+)$/m); const problemMatch = yaml.match(/^\s+problem: (.+)$/m); const solutionMatch = yaml.match(/^\s+solution: (.+)$/m); if (!categoryMatch || !problemMatch || !solutionMatch) return undefined; return { category: categoryMatch[1].trim(), problem: problemMatch[1].trim(), solution: solutionMatch[1].trim(), }; } export function parseCommitMessage( hash: string, message: string ): ParsedCiCommit { const firstLine = message.split("\n")[0] || ""; const subjectMatch = firstLine.match(/^(\w+)(?:\(([^)]+)\))?: (.+)$/); let type: CommitType = "chore"; let scope = ""; let subject = firstLine; if (subjectMatch) { type = parseCommitType(subjectMatch[1]); scope = subjectMatch[2] || ""; subject = subjectMatch[3] || firstLine; } const ciBlock = extractCiBlock(message); const ci = ciBlock ? parseCiBlock(ciBlock) : null; const bodyStart = message.indexOf("\n"); let body = bodyStart >= 0 ? message.slice(bodyStart + 1).trim() : ""; if (ciBlock) { const blockStart = message.indexOf(CI_BLOCK_START); const blockEnd = message.indexOf(CI_BLOCK_END) + CI_BLOCK_END.length; const before = message.slice(bodyStart + 1, blockStart).trim(); const after = message.slice(blockEnd).trim(); body = [before, after].filter(Boolean).join("\n\n"); } return { hash, type, scope, subject, ci, body }; }