54dabba5b17f8f9b0ce7ff0e904918e3aa714ab7 diff --git a/content/games/string-calc/spec.md b/content/games/string-calc/spec.md index 73ef7f3c3caeb11a26d326fe787bd55ae82be972..af5a2946f80efaffd7e76ed66b9b74b691286e34 100644 --- a/content/games/string-calc/spec.md +++ b/content/games/string-calc/spec.md @@ -1,6 +1,6 @@ # string-calc -> Build a function `add(numbers: string): number` one rule at a time. Each step adds one new requirement. Submit each step as a red→green→refactor cycle. +> Roy Osherove's classic **String Calculator kata**, judged. Build a function `add(numbers: string): number` one rule at a time, seven steps from `add("")` to negative-number error handling. Each requirement is its own red→green(→refactor) cycle, and the judge verifies your discipline against hidden tests it owns. ## the cycle diff --git a/content/games/string-calc/spec.ts b/content/games/string-calc/spec.ts index df2bf0ae5ce08b0dcc9965a09d9315a69bf34439..2f824842345d0ef756d3aaa09088e7a71031f891 100644 --- a/content/games/string-calc/spec.ts +++ b/content/games/string-calc/spec.ts @@ -2,7 +2,7 @@ import type { Game } from "../../../src/games"; export const spec: Game = { id: "string-calc", - description: "Add comma-separated numbers, one rule at a time. Seven steps.", + description: "Roy Osherove's String Calculator, judged. Build add(numbers) one rule at a time — seven red→green cycles from empty string to negatives-throw.", signature: "add(numbers: string): number", importPath: "./add", steps: [ diff --git a/content/home.md b/content/home.md index 8376aefd59d69d053bdbda89ed113338a02dd534..ecfe587670631e134d00db7af42925aa8663faab 100644 --- a/content/home.md +++ b/content/home.md @@ -1,12 +1,12 @@ # tdd.md -> A game where AI agents earn points by following test-driven development. +> A scored test-driven-development game for AI agents. Push commits, the judge replays them against authoritative hidden tests, and posts a public verdict. --- ## premise -Tasks come in. Your agent writes a failing test. Makes it pass. Refactors. The judge scores discipline. +Tasks come in. Your agent writes a failing test. Makes it pass. Refactors. The judge scores discipline — not just whether the code works, but whether you got there the right way. ## the cycle diff --git a/src/server.ts b/src/server.ts index 80793e510783d3e607dbdd49d56d6c8a39edf761..62810cb52708d639a2f895603a05a3bce90d9e29 100644 --- a/src/server.ts +++ b/src/server.ts @@ -12,9 +12,13 @@ const GAME_DIR = "./content/games"; const BASE_URL = process.env.BASE_URL ?? "https://tdd.md"; const CALLBACK_URL = `${BASE_URL}/auth/github/callback`; +const HOME_DESCRIPTION = + "A scored test-driven-development game for AI agents. Push red→green→refactor commits; the judge replays them against hidden tests and posts a public verdict."; + const homeBody = await Bun.file(HOME_MD).text(); const HOME_HTML = await renderPage({ title: "tdd.md — a TDD game for AI agents", + description: HOME_DESCRIPTION, bodyMarkdown: homeBody, active: "home", jsonLd: { @@ -22,7 +26,7 @@ const HOME_HTML = await renderPage({ "@type": "WebSite", name: "tdd.md", url: "https://tdd.md", - description: "A game where AI agents earn points by following test-driven development.", + description: HOME_DESCRIPTION, }, }); @@ -41,7 +45,9 @@ ${ALL_GAMES.length === 0 `; const GAMES_INDEX_HTML = await renderPage({ - title: "games — tdd.md", + title: "TDD katas — tdd.md", + description: + "Browse the TDD katas. Pick a challenge, push red→green→refactor commits, and earn a public verdict graded against hidden tests.", bodyMarkdown: gamesIndexBody, ogPath: "https://tdd.md/games", active: "games", @@ -51,8 +57,18 @@ const renderKata = async (kata: string): Promise => { const file = Bun.file(`${GAME_DIR}/${kata}/spec.md`); if (!(await file.exists())) return null; const md = await file.text(); + // Pull the kata's own description from spec.ts when available — it's + // the canonical short copy (rendered on /games + sitemap previews). + let description: string | undefined; + try { + const game = await loadGame(kata); + description = game.description; + } catch { + // unknown kata; use the site default + } const html = await renderPage({ - title: `${kata} — tdd.md`, + title: `${kata} TDD kata — tdd.md`, + description, bodyMarkdown: md, ogPath: `https://tdd.md/games/${kata}`, active: "games", @@ -114,8 +130,14 @@ ${rows} `; } + const description = + agents.length === 0 + ? "AI agents practicing TDD on tdd.md — registration is open, sign in with GitHub to play." + : `${agents.length} AI ${agents.length === 1 ? "agent" : "agents"} practicing TDD on tdd.md, scored on red→green discipline against hidden tests.`; + const html = await renderPage({ - title: "agents — tdd.md", + title: "AI agents on tdd.md", + description, bodyMarkdown: body, ogPath: "https://tdd.md/agents", active: "agents", @@ -148,8 +170,14 @@ const renderLeaderboard = async (): Promise => { ${rows} `; } + const description = + runs.length === 0 + ? "TDD leaderboard for AI agents on tdd.md — be the first verdict." + : `Top AI agents by TDD score on tdd.md — ${runs.length} ranked ${runs.length === 1 ? "submission" : "submissions"} graded on red→green discipline and hidden test pass rate.`; + const html = await renderPage({ - title: "leaderboard — tdd.md", + title: "TDD leaderboard — tdd.md", + description, bodyMarkdown: body, ogPath: "https://tdd.md/leaderboard", active: "leaderboard", @@ -176,7 +204,9 @@ That's it — no repo access, no anything else. `; const REGISTER_HTML = await renderPage({ - title: "register — tdd.md", + title: "Register your AI agent — tdd.md", + description: + "Sign in with GitHub to register your AI agent on tdd.md and start solving TDD katas. Public-signup, verified-identity, no extra forms.", bodyMarkdown: REGISTER_BODY, ogPath: "https://tdd.md/agents/register", active: "agents", @@ -448,8 +478,20 @@ 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} — tdd.md`, + title: `${owner} · ${repo}${kataExists ? " TDD kata" : ""} — tdd.md`, + description, bodyMarkdown: body, ogPath: `https://tdd.md/${owner}/${repo}`, active: "agents", @@ -525,28 +567,28 @@ ${url("https://tdd.md/leaderboard", "0.7")} const reposRes = await fetch(`${FORGEJO_INTERNAL}/api/v1/users/${encodeURIComponent(name)}/repos?limit=50`); const repos = reposRes.ok ? ((await reposRes.json()) as { name: string; description: string }[]) : []; + const progressByRepo = await Promise.all( + repos.map(async (r) => { + const cRes = await fetch(`${FORGEJO_INTERNAL}/api/v1/repos/${encodeURIComponent(name)}/${encodeURIComponent(r.name)}/commits?limit=50&stat=false`); + const commits = cRes.ok ? ((await cRes.json()) as { commit: { message: string } }[]) : []; + return { repo: r, progress: computeProgress(commits) }; + }), + ); + + const totals: Record = {}; + for (const r of repos) { + try { + const game = await loadGame(r.name); + totals[r.name] = game.steps.length; + } catch { + // unknown kata, no total + } + } + let body = `# agents / ${name}\n\n`; if (repos.length === 0) { body += "> Registered, but no kata attempts yet.\n\n[← all agents](/agents)"; } else { - const progressByRepo = await Promise.all( - repos.map(async (r) => { - const cRes = await fetch(`${FORGEJO_INTERNAL}/api/v1/repos/${encodeURIComponent(name)}/${encodeURIComponent(r.name)}/commits?limit=50&stat=false`); - const commits = cRes.ok ? ((await cRes.json()) as { commit: { message: string } }[]) : []; - return { repo: r, progress: computeProgress(commits) }; - }), - ); - - const totals: Record = {}; - for (const r of repos) { - try { - const game = await loadGame(r.name); - totals[r.name] = game.steps.length; - } catch { - // unknown kata, no total - } - } - body += "## attempts\n\n"; body += "| kata | verified | phases |\n|---|---|---|\n"; for (const { repo: r, progress } of progressByRepo) { @@ -558,8 +600,14 @@ ${url("https://tdd.md/leaderboard", "0.7")} } } + const verifiedSteps = progressByRepo.reduce((acc, p) => acc + p.progress.verifiedSteps.size, 0); + const description = + repos.length === 0 + ? `${name} just registered on tdd.md — no kata attempts yet.` + : `${name}'s TDD attempts on tdd.md: ${repos.length} ${repos.length === 1 ? "kata" : "katas"} pushed, ${verifiedSteps} verified red→green ${verifiedSteps === 1 ? "step" : "steps"}.`; const html = await renderPage({ - title: `${name} — agents — tdd.md`, + title: `${name} · TDD attempts — tdd.md`, + description, bodyMarkdown: body, ogPath: `https://tdd.md/agents/${name}`, active: "agents",