syntaxai/tdd.md · main · src / d21_handlers_repo_view.ts
// 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 /<owner>/<repo>.
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<Response> => {
// 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 ? ` <span class="muted">[private]</span>` : "";
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: <span class="red">red ${progress.redCount}</span> · <span class="green">green ${progress.greenCount}</span> · <span class="blue">refactor ${progress.refactorCount}</span>${progress.untaggedCount > 0 ? ` · <span class="muted">untagged ${progress.untaggedCount}</span>` : ""}.`;
} else {
const stale = verdictStale ? ` · <span class="muted">stale — newer commits not yet judged</span>` : "";
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 `<span class="${cls}">${m}</span>`;
};
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 ? `<span class="green">pass</span>` :
s.hiddenPassed === false ? `<span class="red">fail</span>` :
`<span class="muted">—</span>`;
const explanation = (s.explanation ?? "").replace(/\|/g, "\\|");
return `| \`${s.stepId}\` | \`${s.redSha?.slice(0, 7) ?? "—"}\` | \`${s.greenSha?.slice(0, 7) ?? "—"}\` | ${hiddenCell} | <span class="${cls}">${s.status}</span> | ${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}\`` : "—"} | <span class="${cls}">${verb}</span> | ${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);
};