178 lines
5.5 KiB
TypeScript
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 };
|
|
}
|
|
|