Update report generation to dynamically discover and display author commit data

Refactor `timmy-report.ts` to dynamically collect and display author commit samples from git log, update `context.md` to reflect dynamic author data, and adjust `timmy-report.md` to use the new dynamic contributor summary.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 90c7a60b-2c61-4699-b5c6-6a1ac7469a4d
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: cf2341e4-4927-4087-a7c9-a93340626de0
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
alexpaynex
2026-03-19 23:54:15 +00:00
parent f4243b516c
commit 1a268353f9
3 changed files with 164 additions and 123 deletions

View File

@@ -61,9 +61,6 @@ process.stdout.write("Collecting git data…\n");
const shortlog = git("shortlog -sn HEAD");
const logOneline = git("log --oneline HEAD");
const alexSample = git(`log HEAD --author="alexpaynex" --pretty=format:"%h %s" --stat -10`);
const replitAgentSample = git(`log HEAD --author="Replit Agent" --pretty=format:"%h %s" --stat -10`);
// Validate that git data is non-empty — fail loudly rather than commit blank sections
if (!shortlog || shortlog === "(git command failed)") {
throw new Error(`git shortlog returned empty output. ROOT=${ROOT}`);
@@ -72,7 +69,24 @@ if (!logOneline || logOneline === "(git command failed)") {
throw new Error(`git log returned empty output. ROOT=${ROOT}`);
}
process.stdout.write(` ✓ git data collected (${shortlog.split("\n").length} contributors, ${logOneline.split("\n").length} commits)\n`);
// Derive author list dynamically from shortlog output
// Each line looks like: " 127 Author Name"
const authors: string[] = shortlog
.split("\n")
.map((line) => line.trim().replace(/^\d+\s+/, ""))
.filter((name) => name.length > 0 && name !== "(git command failed)");
// Exclude Replit system identities (no meaningful code to sample)
const SYSTEM_IDENTITIES = new Set(["replit", "agent"]);
const codeAuthors = authors.filter((a) => !SYSTEM_IDENTITIES.has(a.toLowerCase()));
// Collect per-author stat samples for all code contributors
const authorSamples: Record<string, string> = {};
for (const author of codeAuthors) {
authorSamples[author] = git(`log HEAD --author="${author}" --pretty=format:"%h %s" --stat -10`);
}
process.stdout.write(` ✓ git data collected (${authors.length} contributors, ${logOneline.split("\n").length} commits)\n`);
// ── Collect source file excerpts ──────────────────────────────────────────────
@@ -154,21 +168,13 @@ ${logOneline}
---
## alexpaynex — Sample commits with diff stats (last 10)
${Object.entries(authorSamples).map(([author, sample]) => `## ${author} — Sample commits with diff stats (last 10)
\`\`\`
${alexSample}
${sample}
\`\`\`
---
## Replit Agent — Sample commits with diff stats (last 10)
\`\`\`
${replitAgentSample}
\`\`\`
---
---`).join("\n\n")}
## Key Source File Excerpts
@@ -221,11 +227,9 @@ ${shortlog}
FULL COMMIT LOG:
${logOneline}
ALEXPAYNEX — LAST 10 COMMITS WITH STATS:
${alexSample}
REPLIT AGENT — LAST 10 COMMITS WITH STATS:
${replitAgentSample}
${Object.entries(authorSamples).map(([author, sample]) =>
`${author.toUpperCase()} — LAST 10 COMMITS WITH STATS:\n${sample}`
).join("\n\n")}
KEY SOURCE FILES:
@@ -302,7 +306,7 @@ async function callClaude(systemPrompt: string, userContent: string): Promise<st
async function main(): Promise<void> {
if (STUB_MODE) {
process.stdout.write(
"\nWarning: AI_INTEGRATIONS_ANTHROPIC_BASE_URL / ANTHROPIC_API_KEY not set — writing stub Timmy report.\n",
"\nWarning: AI_INTEGRATIONS_ANTHROPIC_BASE_URL / AI_INTEGRATIONS_ANTHROPIC_API_KEY not set — writing stub Timmy report.\n",
);
const stubReport = `# Timmy's Rubric Report (Stub Mode)
@@ -321,6 +325,25 @@ pnpm --filter @workspace/scripts timmy-report
process.stdout.write("\nCalling Claude (claude-haiku-4-5) for Timmy's report…\n");
const timmyReport = await callClaude(TIMMY_SYSTEM, userPrompt);
// Post-generation sanity check — catch malformed or truncated model outputs early
// Match case-insensitively since Claude may use "PART 1" or "Part 1"
const REQUIRED_SECTIONS = ["part 1", "part 2", "part 3"];
const lowerReport = timmyReport.toLowerCase();
const missingSections = REQUIRED_SECTIONS.filter((s) => !lowerReport.includes(s));
const MIN_LINES = 30;
const actualLines = timmyReport.split("\n").length;
if (missingSections.length > 0) {
process.stderr.write(
`Warning: timmy-report.md is missing sections: ${missingSections.join(", ")} — model output may be malformed.\n`,
);
}
if (actualLines < MIN_LINES) {
process.stderr.write(
`Warning: timmy-report.md has only ${actualLines} lines (expected ≥${MIN_LINES}) — model output may be truncated.\n`,
);
}
const header = `# Timmy's Rubric Report
## Repo: \`replit/token-gated-economy\` (Timmy Tower World)
@@ -333,7 +356,7 @@ pnpm --filter @workspace/scripts timmy-report
`;
writeFileSync(join(ROOT, "reports/timmy-report.md"), header + timmyReport, "utf8");
process.stdout.write(" ✓ reports/timmy-report.md written\n\nDone. Both reports are in reports/\n");
process.stdout.write(` ✓ reports/timmy-report.md written (${actualLines} lines)\n\nDone. Both reports are in reports/\n`);
}
main().catch((err) => {