syntaxai/tdd.md · main · src / c14_real_reports.ts
// c32 — logic: aggregate real GitHub commit history into the same
// AgentReport / RecentFlagged shape that c51_render_reports renders.
// Pure (given fetched commits in, produces report objects out); the
// I/O happens in c14_github.fetchRepoCommits which we call here.
//
// Attribution: Co-Authored-By footers are the agent-attribution channel
// the existing tdd.md commit history already uses. Anything without a
// recognised footer is bucketed as "unknown" and reported separately —
// it's still useful for volume context.
import { parseCommit } from "./a31_commits.ts";
import { fetchRepoCommits, type GithubCommit } from "./c14_github.ts";
import type {
AgentReport,
FailureSlice,
RecentFlagged,
} from "./a31_reports_demo.ts";
type LiveAgentSlug = AgentReport["slug"] | "unknown";
export const detectAgent = (msg: string): LiveAgentSlug => {
if (/Co-Authored-By:.*Claude/i.test(msg)) return "claude-code";
if (/Co-Authored-By:.*Cursor/i.test(msg)) return "cursor";
if (/Co-Authored-By:.*Aider/i.test(msg)) return "aider";
return "unknown";
};
const AGENT_NAMES: Record<AgentReport["slug"], string> = {
"claude-code": "Claude Code",
cursor: "Cursor",
aider: "Aider",
};
// 30-day daily commit-count series, oldest → newest. When there are no
// commits in a day, that day's value is 0 — the sparkline still renders
// but flat-lines, which honestly reflects the data.
export const buildTrend = (commits: GithubCommit[], days = 30): number[] => {
const out = new Array<number>(days).fill(0);
const today = new Date();
today.setUTCHours(0, 0, 0, 0);
for (const c of commits) {
const d = new Date(c.commit.author.date);
d.setUTCHours(0, 0, 0, 0);
const ageDays = Math.floor((today.getTime() - d.getTime()) / (24 * 60 * 60 * 1000));
if (ageDays < 0 || ageDays >= days) continue;
const idx = days - 1 - ageDays;
const cur = out[idx] ?? 0;
out[idx] = cur + 1;
}
return out;
};
const buildAgentReport = (
slug: AgentReport["slug"],
agentCommits: GithubCommit[],
repoSlug: string,
): AgentReport => {
const tagged = agentCommits.filter((c) => {
const phase = parseCommit(c.commit.message).phase;
return phase === "red" || phase === "green" || phase === "refactor";
});
const phaseCoveragePct = agentCommits.length === 0
? 0
: Math.round((tagged.length / agentCommits.length) * 100);
// Score is a proxy: phase-coverage is the only structural signal we
// can compute without running the test suite. When coverage is 0 the
// agent isn't attempting TDD, so the score is honestly low.
const score = phaseCoveragePct;
// Failure mix collapses to two slices for live data — phase-tagged vs
// not. Fine-grained failure modes (red-did-not-fail, test-deleted, etc)
// need the runner sliver before they're computable.
const failureMix: FailureSlice[] = [
{ label: "phase-tagged", pct: phaseCoveragePct, tone: "green" },
{ label: "no phase tag", pct: 100 - phaseCoveragePct, tone: "muted" },
];
const recent: RecentFlagged[] = agentCommits
.slice(0, 5)
.map((c) => {
const parsed = parseCommit(c.commit.message);
const phase = parsed.phase === "red" || parsed.phase === "green" || parsed.phase === "refactor"
? parsed.phase
: "green";
const failure = parsed.phase === "untagged" || parsed.phase === "init"
? "no phase tag"
: `${parsed.phase} (live judge not yet wired)`;
return {
date: c.commit.author.date.slice(0, 10),
repo: repoSlug,
sha: c.sha.slice(0, 7),
phase,
failure,
pts: 0,
};
});
const topIssueLabel = phaseCoveragePct === 100 ? "no current issues" : "no phase tag";
const topIssuePct = 100 - phaseCoveragePct;
return {
slug,
name: AGENT_NAMES[slug],
score,
delta: 0,
commits: agentCommits.length,
phaseCoveragePct,
streak: 0,
streakBroken: false,
topIssueLabel,
topIssuePct,
failureMix,
trend: buildTrend(agentCommits),
recent,
};
};
export interface LiveReports {
reports: AgentReport[];
unknownCount: number;
totalCommits: number;
earliest: string | null;
latest: string | null;
fetchedAt: number;
}
export const buildLiveReports = async (
repoOwner: string,
repoName: string,
perPage = 100,
): Promise<LiveReports> => {
const commits = await fetchRepoCommits(repoOwner, repoName, perPage);
const repoSlug = `${repoOwner}/${repoName}`;
const byAgent = new Map<AgentReport["slug"], GithubCommit[]>();
let unknownCount = 0;
for (const c of commits) {
const a = detectAgent(c.commit.message);
if (a === "unknown") {
unknownCount++;
continue;
}
const arr = byAgent.get(a) ?? [];
arr.push(c);
byAgent.set(a, arr);
}
const order: AgentReport["slug"][] = ["claude-code", "cursor", "aider"];
const reports = order
.map((slug) => {
const list = byAgent.get(slug);
if (!list || list.length === 0) return null;
return buildAgentReport(slug, list, repoSlug);
})
.filter((r): r is AgentReport => r !== null);
const dates = commits.map((c) => c.commit.author.date).sort();
const earliest = dates[0] ?? null;
const latest = dates[dates.length - 1] ?? null;
return {
reports,
unknownCount,
totalCommits: commits.length,
earliest,
latest,
fetchedAt: Date.now(),
};
};