syntaxai/tdd.md · commit c94f94e

Batch 4: dynamic kata listing, narrow cookie path, drop noisy HEALTHCHECK

- Game now carries a description field; spec.ts holds the canonical
  one-liner. listGames() walks content/games/<id>/ and loads each kata
  via loadGame, so the games index, sitemap, and any future kata-aware
  pages stop hardcoding "string-calc".
- /games index renders the kata table from listGames() instead of a
  hand-written row. Adding a kata is now: drop a folder under
  content/games/, restart.
- /sitemap.xml emits a <url> entry per loaded kata.
- OAuth state cookie scope tightened from Path=/ to Path=/auth — same
  HttpOnly/Secure/SameSite=Lax/Max-Age=600. The cookie was never read
  outside /auth/github/callback anyway.
- Containerfile drops HEALTHCHECK. OCI builds were rejecting it with
  warnings on every build, and we already probe /healthz from
  cloudflared upstream + the deploy script.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
author
syntaxai <[email protected]>
date
2026-05-03 18:43:50 +01:00
parent
8ecc5c7
commit
c94f94e93c10adfbc3ee82a86f8abaa216297808

4 files changed · +45 −10

modified Containerfile +3 −3
@@ -21,8 +21,8 @@ COPY public ./public
2121 ENV PORT=3000
2222 EXPOSE 3000
2323
24-# Healthcheck via /healthz (Bun's HTTP server start <100ms).
25-HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
26- CMD wget -q -O- http://localhost:3000/healthz || exit 1
24+# No HEALTHCHECK — podman's OCI builds drop it with a warning, and our
25+# external probes (cloudflared upstream + the deploy script's /healthz
26+# poll) already cover liveness.
2727
2828 CMD ["bun", "src/server.ts"]
modified content/games/string-calc/spec.ts +1 −0
@@ -2,6 +2,7 @@ import type { Game } from "../../../src/games";
22
33 export const spec: Game = {
44 id: "string-calc",
5+ description: "Add comma-separated numbers, one rule at a time. Seven steps.",
56 signature: "add(numbers: string): number",
67 importPath: "./add",
78 steps: [
modified src/games.ts +26 −0
@@ -10,6 +10,8 @@ export interface Step {
1010
1111 export interface Game {
1212 id: string;
13+ // One-line summary shown on the games index and OG previews.
14+ description: string;
1315 // Human-readable function signature the agent must export. Documented
1416 // on the kata page so authors know what to build.
1517 signature: string;
@@ -19,6 +21,30 @@ export interface Game {
1921 steps: Step[];
2022 }
2123
24+import { readdir } from "node:fs/promises";
25+
26+// Reads every kata under content/games/ and returns the loaded specs in
27+// alphabetical order. Used to build the games index and sitemap without
28+// hard-coding individual kata ids.
29+export async function listGames(): Promise<Game[]> {
30+ let entries;
31+ try {
32+ entries = await readdir("./content/games", { withFileTypes: true });
33+ } catch {
34+ return [];
35+ }
36+ const ids = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
37+ const games: Game[] = [];
38+ for (const id of ids) {
39+ try {
40+ games.push(await loadGame(id));
41+ } catch {
42+ // skip katas that fail to load (missing spec.ts, etc.)
43+ }
44+ }
45+ return games;
46+}
47+
2248 export async function loadGame(id: string): Promise<Game> {
2349 const file = Bun.file(`./content/games/${id}/spec.ts`);
2450 if (!(await file.exists())) {
modified src/server.ts +15 −7
@@ -2,7 +2,7 @@ import { renderPage, renderNotFound } from "./render";
22 import * as github from "./github_oauth";
33 import * as forgejo from "./forgejo";
44 import { parseCommit, computeProgress, type Phase } from "./commits";
5-import { loadGame } from "./games";
5+import { loadGame, listGames } from "./games";
66 import { judge } from "./judge";
77 import { latestRun, allLatestRuns } from "./db";
88
@@ -26,11 +26,16 @@ const HOME_HTML = await renderPage({
2626 },
2727 });
2828
29+const ALL_GAMES = await listGames();
30+
2931 const gamesIndexBody = `# games
3032
31-| kata | description | language |
32-|---|---|---|
33-| [string-calc](/games/string-calc) | Add comma-separated numbers, one rule at a time. Seven steps. | TypeScript |
33+${ALL_GAMES.length === 0
34+ ? "_No katas registered yet._"
35+ : `| kata | description | steps |\n|---|---|---|\n${ALL_GAMES.map(
36+ (g) => `| [${g.id}](/games/${g.id}) | ${g.description} | ${g.steps.length} |`,
37+ ).join("\n")}`
38+}
3439
3540 > Ready to play? [Register your agent →](/agents/register)
3641 `;
@@ -472,11 +477,14 @@ const server = Bun.serve({
472477 const today = new Date().toISOString().slice(0, 10);
473478 const url = (loc: string, priority: string) =>
474479 `<url><loc>${loc}</loc><lastmod>${today}</lastmod><priority>${priority}</priority></url>`;
480+ const kataUrls = ALL_GAMES.map((g) =>
481+ url(`https://tdd.md/games/${g.id}`, "0.8"),
482+ ).join("\n");
475483 const xml = `<?xml version="1.0" encoding="UTF-8"?>
476484 <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
477485 ${url("https://tdd.md/", "1.0")}
478486 ${url("https://tdd.md/games", "0.9")}
479-${url("https://tdd.md/games/string-calc", "0.8")}
487+${kataUrls}
480488 ${url("https://tdd.md/agents", "0.7")}
481489 ${url("https://tdd.md/leaderboard", "0.7")}
482490 </urlset>`;
@@ -625,7 +633,7 @@ ${url("https://tdd.md/leaderboard", "0.7")}
625633 status: 302,
626634 headers: {
627635 Location: github.authorizeUrl(nonce, CALLBACK_URL),
628- "Set-Cookie": `tdd_oauth_state=${nonce}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=600`,
636+ "Set-Cookie": `tdd_oauth_state=${nonce}; Path=/auth; HttpOnly; Secure; SameSite=Lax; Max-Age=600`,
629637 },
630638 });
631639 },
@@ -712,7 +720,7 @@ When you push, the judge replays your commits and posts the verdict at [/agents/
712720 return new Response(html, {
713721 headers: {
714722 "Content-Type": "text/html; charset=utf-8",
715- "Set-Cookie": "tdd_oauth_state=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0",
723+ "Set-Cookie": "tdd_oauth_state=; Path=/auth; HttpOnly; Secure; SameSite=Lax; Max-Age=0",
716724 },
717725 });
718726 },