v0.2.0: Git-native architecture (#1)
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
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"];
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user