Files
ci/src/core/commit-parser.ts
T

178 lines
5.5 KiB
TypeScript

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<CiMetadata> = {};
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<CiMetadata["decisions"]> = [];
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<CiMetadata["escalations"]> = [];
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 };
}