syntaxai/tdd.md · main · src / a31_commits.ts
export type Phase = "red" | "green" | "refactor" | "spike" | "init" | "untagged";
export interface ParsedCommit {
phase: Phase;
step: string | null;
subject: string;
}
const PHASE_RE = /^(red|green|refactor|spike)(?:\(([a-z][a-z0-9-]*)\))?:\s*(.*)$/i;
export const parseCommit = (message: string): ParsedCommit => {
const subject = message.split("\n")[0] ?? "";
const m = subject.match(PHASE_RE);
if (m) {
return {
phase: m[1]!.toLowerCase() as Phase,
step: m[2] ?? null,
subject: m[3] ?? "",
};
}
if (/^Initial commit$/i.test(subject)) {
return { phase: "init", step: null, subject };
}
return { phase: "untagged", step: null, subject };
};
export interface Progress {
verifiedSteps: Set<string>;
redCount: number;
greenCount: number;
refactorCount: number;
spikeCount: number;
untaggedCount: number;
}
// A step counts as "verified" when its red commit is followed by a green
// for the same step. Refactor and untagged commits are tallied separately
// for the score breakdown but don't move verification.
export const computeProgress = (commits: { commit: { message: string } }[]): Progress => {
const pendingRed = new Set<string>();
const verifiedSteps = new Set<string>();
let redCount = 0;
let greenCount = 0;
let refactorCount = 0;
let spikeCount = 0;
let untaggedCount = 0;
// Forgejo returns commits newest-first; walk oldest-first to get sequence.
for (const c of [...commits].reverse()) {
const p = parseCommit(c.commit.message);
if (p.phase === "red") {
redCount++;
if (p.step) pendingRed.add(p.step);
} else if (p.phase === "green") {
greenCount++;
if (p.step && pendingRed.has(p.step)) verifiedSteps.add(p.step);
} else if (p.phase === "refactor") {
refactorCount++;
} else if (p.phase === "spike") {
spikeCount++;
} else if (p.phase === "untagged") {
untaggedCount++;
}
}
return { verifiedSteps, redCount, greenCount, refactorCount, spikeCount, untaggedCount };
};