// c21 (repo-view) — handler that renders the bare /:owner/:repo page. // Composes c14_forgejo (repo + commits via admin API), c31 commits + // games (parsing, kata lookup), c13 verdict store, c51 layout helpers. // Exposed via the c21_app.ts fallback fetch — reserved top-level routes // are matched first, this is the catch-all for //. import { FORGEJO_URL, adminApiHeaders, getUserVisibility, } from "./c14_forgejo.ts"; import { parseCommit, computeProgress } from "./a31_commits.ts"; import { loadGame } from "./a31_games.ts"; import { latestRun } from "./c13_database.ts"; import { renderPage, renderNotFound, htmlResponse, phaseSpan, relativeTime, } from "./b51_render_layout.ts"; interface ForgejoRepoSummary { description: string; clone_url: string; empty: boolean; private: boolean; } interface ForgejoCommit { sha: string; commit: { message: string; author: { name: string; date: string } }; } export const renderRepoView = async ( owner: string, repo: string, viewer: string | null, ): Promise => { // Private/limited owners get a 404 to anonymous visitors — but the // owner themselves (verified via session cookie) can always see // their own pages. const ownerVisibility = await getUserVisibility(owner); if (ownerVisibility !== null && ownerVisibility !== "public" && viewer !== owner) { const html = await renderNotFound(`/${owner}/${repo}`); return htmlResponse(html, 404); } const repoApi = `${FORGEJO_URL}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; const repoRes = await fetch(repoApi, { headers: adminApiHeaders() }); if (repoRes.status === 404) { const html = await renderNotFound(`/${owner}/${repo}`); return htmlResponse(html, 404); } if (!repoRes.ok) { const html = await renderPage({ title: `${owner}/${repo} — tdd.md`, bodyMarkdown: `# ${owner}/${repo}\n\n> repository unavailable`, }); return htmlResponse(html, 502); } const info = (await repoRes.json()) as ForgejoRepoSummary; const cloneUrl = info.clone_url || `https://tdd.md/${owner}/${repo}.git`; const isPrivate = info.private === true; // The repo name is by convention the kata id. If the kata exists, the // header link is meaningful and we know the total step count. let totalSteps: number | null = null; let kataExists = false; try { const game = await loadGame(repo); totalSteps = game.steps.length; kataExists = true; } catch { // Repo isn't a known kata — still render, just without step totals. } let commits: ForgejoCommit[] = []; if (!info.empty) { const commitsRes = await fetch(`${repoApi}/commits?limit=50&stat=false`, { headers: adminApiHeaders(), }); if (commitsRes.ok) commits = (await commitsRes.json()) as ForgejoCommit[]; } const progress = computeProgress(commits); const verified = progress.verifiedSteps.size; let status: string; if (commits.length === 0) { status = "awaiting first push"; } else if (totalSteps !== null && verified >= totalSteps) { status = "kata complete"; } else if (verified > 0) { status = "in progress"; } else { status = "no verified steps yet"; } const stepCounter = totalSteps !== null ? `${verified} / ${totalSteps}` : `${verified} / ?`; let phaseLog: string; if (commits.length === 0) { phaseLog = "_No commits yet — push your first `red:` commit to start the cycle._"; } else { const rows = commits.map((c) => { const sha = c.sha.slice(0, 7); const p = parseCommit(c.commit.message); const subject = (p.subject || c.commit.message.split("\n")[0] || "").replace(/\|/g, "\\|"); const stepCell = p.step ? `\`${p.step}\`` : "—"; return `| \`${sha}\` | ${phaseSpan(p.phase)} | ${stepCell} | ${subject} | ${relativeTime(c.commit.author.date)} |`; }); phaseLog = `| sha | phase | step | message | when |\n|---|---|---|---|---|\n${rows.join("\n")}`; } const kataLink = kataExists ? `[\`${repo}\` →](/games/${repo})` : `\`${repo}\``; const privateBadge = isPrivate ? ` [private]` : ""; const verdict = latestRun(owner, repo); const headSha = commits[0]?.sha ?? null; const verdictStale = verdict !== null && headSha !== null && verdict.headSha !== headSha; let scoreSection: string; if (verdict === null) { scoreSection = `> Not yet judged. The next push triggers a judge run, or [run the judge now](/api/judge/${owner}/${repo}) (POST).\n\nPhase tally: red ${progress.redCount} · green ${progress.greenCount} · refactor ${progress.refactorCount}${progress.untaggedCount > 0 ? ` · untagged ${progress.untaggedCount}` : ""}.`; } else { const stale = verdictStale ? ` · stale — newer commits not yet judged` : ""; const sign = verdict.totalScore >= 0 ? "+" : ""; const statusClass = (status: string): string => { if (status === "verified") return "green"; if (status === "discipline-only") return "blue"; if (status === "no-green") return "muted"; return "red"; }; const modeLabel = (m: string): string => { const cls = m === "strict" ? "red" : m === "pragmatic" ? "blue" : "green"; return `${m}`; }; const rows = verdict.steps.length === 0 ? "_No red→green pairs found yet._" : `| step | red | green | hidden | status | points | explanation |\n|---|---|---|---|---|---|---|\n` + verdict.steps.map((s) => { const cls = statusClass(s.status); const sign = s.scoreDelta >= 0 ? "+" : ""; const hiddenCell = s.hiddenPassed === true ? `pass` : s.hiddenPassed === false ? `fail` : ``; const explanation = (s.explanation ?? "").replace(/\|/g, "\\|"); return `| \`${s.stepId}\` | \`${s.redSha?.slice(0, 7) ?? "—"}\` | \`${s.greenSha?.slice(0, 7) ?? "—"}\` | ${hiddenCell} | ${s.status} | ${sign}${s.scoreDelta} | ${explanation} |`; }).join("\n"); const refactorRows = (verdict.refactors ?? []).length === 0 ? "" : `\n\n### refactors\n\n| sha | step | tests | points | explanation |\n|---|---|---|---|---|\n` + verdict.refactors.map((r) => { const sign = r.scoreDelta >= 0 ? "+" : ""; const cls = r.testsPassed ? "green" : "red"; const verb = r.testsPassed ? "green" : "broke tests"; const explanation = (r.explanation ?? "").replace(/\|/g, "\\|"); return `| \`${r.sha.slice(0, 7)}\` | ${r.stepId ? `\`${r.stepId}\`` : "—"} | ${verb} | ${sign}${r.scoreDelta} | ${explanation} |`; }).join("\n"); const modeLine = verdict.mode ? `**mode: ${modeLabel(verdict.mode)}** · ` : ""; scoreSection = `${modeLine}**total: ${sign}${verdict.totalScore}** · judged ${relativeTime(new Date(verdict.judgedAt).toISOString())}${stale}\n\n${rows}${refactorRows}`; } const body = `# ${owner} · playing ${kataLink}${privateBadge} > ${status} > **${stepCounter}** steps verified ## phase log ${phaseLog} ## score ${scoreSection} ## clone \`\`\` git clone ${cloneUrl} \`\`\` [← /agents/${owner}](/agents/${owner})${kataExists ? ` · [kata spec →](/games/${repo})` : ""} `; // Dynamic description tailored to this attempt — gives every agent // run a unique snippet for search results and social previews instead // of falling back to the site default. const totalSnippet = verdict !== null ? `, score ${verdict.totalScore >= 0 ? "+" : ""}${verdict.totalScore}` : ""; const description = kataExists ? `${owner}'s ${repo} TDD kata attempt on tdd.md — ${verified}${totalSteps !== null ? `/${totalSteps}` : ""} steps verified${totalSnippet}.` : `${owner}/${repo} on tdd.md — ${commits.length} ${commits.length === 1 ? "commit" : "commits"} in the phase log${totalSnippet}.`; const html = await renderPage({ title: `${owner} · ${repo}${kataExists ? " TDD kata" : ""} — tdd.md`, description, bodyMarkdown: body, ogPath: `https://tdd.md/${owner}/${repo}`, active: "agents", }); return htmlResponse(html); };