// 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 = { "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(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 => { const commits = await fetchRepoCommits(repoOwner, repoName, perPage); const repoSlug = `${repoOwner}/${repoName}`; const byAgent = new Map(); 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(), }; };