syntaxai/tdd.md · main · src / d21_app.ts
// c21 — handlers: the route table + fallback fetch. Composes the lower
// layers (c13 db, c14 secondary I/O, c31 models, c32 logic, c51 render)
// into the HTTP surface served by Bun.serve in c11_server.
import {
renderPage,
renderNotFound,
htmlResponse,
} from "./b51_render_layout.ts";
import { renderDocsPage } from "./b51_render_docs_layout.ts";
import { listGames, loadGame } from "./a31_games.ts";
import { ALL_POSTS } from "./a31_blog.ts";
import { ALL_GOALS } from "./a31_goals.ts";
import { ALL_GUIDES } from "./a31_guides.ts";
import { ALL_SAMA } from "./a31_sama.ts";
import { SITE_BASE_URL } from "./a31_site_config.ts";
import {
renderSitemap,
STATIC_PATHS,
type SitemapUrl,
} from "./b32_sitemap.ts";
import {
getViewer,
sessionCookieHeader,
} from "./b32_session.ts";
import { renderAgentsIndex, renderAgentDetail } from "./d21_handlers_agents.ts";
import { renderLeaderboard } from "./d21_handlers_leaderboard.ts";
import { startGithubOauth, handleGithubCallback } from "./d21_handlers_auth.ts";
import {
reportsLandingHandler,
reportsDemoHandler,
reportsDemoTestsHandler,
reportsDemoAgentHandler,
reportsLiveHandler,
reportsLiveTestsHandler,
reportsLiveAgentHandler,
} from "./d21_handlers_reports.ts";
import {
skillsSamaMdHandler,
samaCliResponse,
samaSkillHandler,
samaV2Handler,
samaV2ExampleCrudHandler,
samaV2ExampleWordpressHandler,
samaV2VerifyHandler,
samaVerifyHandler,
samaLandingHandler,
samaSlugHandler,
} from "./d21_handlers_sama.ts";
import {
goalsLandingHandler,
goalSlugHandler,
} from "./d21_handlers_goals.ts";
import { contributingHandler } from "./d21_handlers_contributing.ts";
import { editPageHandler } from "./d21_handlers_edit.ts";
import {
adminListHandler,
adminNewHandler,
adminEditHandler,
adminDeleteHandler,
} from "./d21_handlers_admin.ts";
import { bundleAdminClient } from "./c14_client_bundle.ts";
import { publicPageHandler } from "./d21_handlers_content.ts";
import { rawSourceHandler } from "./d21_handlers_source.ts";
import { buildSamaCliInstallScript } from "./b32_sama_cli_install.ts";
import { commitViewHandler } from "./d21_handlers_commit_view.ts";
import { appFetch, appError } from "./d21_handlers_fallback.ts";
import {
projectsLandingHandler,
projectsNewHandler,
projectDetailHandler,
} from "./d21_handlers_projects.ts";
import {
judgeApiHandler,
agentVisibilityHandler,
} from "./d21_handlers_api_agents.ts";
import { forgejoWebhookHandler } from "./d21_handlers_webhook.ts";
const HOME_MD = "./content/home.md";
const GAME_DIR = "./content/games";
// — /install — self-contained sama-cli installer ------------------
//
// All tools/sama-cli/* runtime files are read into memory at boot
// and stitched into a single bash script via buildSamaCliInstall-
// Script. Users run `curl -fsSL https://tdd.md/install | bash` and
// get the shell verifier dropped into ~/.sama-cli/ with no extra
// network calls.
//
// File list mirrors tools/sama-cli/ excluding tests + run-ts-
// verifier.ts (those are dev-only artifacts).
const SAMA_CLI_INSTALL_FILES = [
{ path: "sama", abs: "./tools/sama-cli/sama", executable: true },
{ path: "sama.profile.toml", abs: "./tools/sama-cli/sama.profile.toml" },
{ path: "cross-verify.sh", abs: "./tools/sama-cli/cross-verify.sh", executable: true },
{ path: "README.md", abs: "./tools/sama-cli/README.md" },
{ path: "src/a31_constants.sh", abs: "./tools/sama-cli/src/a31_constants.sh" },
{ path: "src/b32_utils.sh", abs: "./tools/sama-cli/src/b32_utils.sh" },
{ path: "src/b32_checks.sh", abs: "./tools/sama-cli/src/b32_checks.sh" },
{ path: "src/c14_graph.sh", abs: "./tools/sama-cli/src/c14_graph.sh" },
{ path: "src/d21_main.sh", abs: "./tools/sama-cli/src/d21_main.sh" },
] as const;
const samaCliInstallFiles = await Promise.all(
SAMA_CLI_INSTALL_FILES.map(async (f) => ({
path: f.path,
content: await Bun.file(f.abs).text(),
executable: f.executable === true,
})),
);
const SAMA_CLI_INSTALL_SCRIPT = buildSamaCliInstallScript(samaCliInstallFiles);
const HOME_DESCRIPTION =
"SAMA — architecture as code, for code AI writes. Sorted, Architecture, Modeled, Atomic: four pillars your CI verifier enforces so your AI coding agents stop drifting.";
const homeBody = await Bun.file(HOME_MD).text();
const HOME_HTML = await renderPage({
title: "SAMA — architecture as code, for code AI writes",
description: HOME_DESCRIPTION,
bodyMarkdown: homeBody,
active: "home",
jsonLd: {
"@context": "https://schema.org",
"@type": "WebSite",
name: "tdd.md",
url: "https://tdd.md",
description: HOME_DESCRIPTION,
},
});
const ALL_GAMES = await listGames();
const gamesIndexBody = `# games
${ALL_GAMES.length === 0
? "_No katas registered yet._"
: `| kata | description | steps |\n|---|---|---|\n${ALL_GAMES.map(
(g) => `| [${g.id}](/games/${g.id}) | ${g.description} | ${g.steps.length} |`,
).join("\n")}`
}
> Ready to play? [Register your agent →](/agents/register)
> Using a specific agent? See the [agent-specific guides](/guides) — Claude Code, Cursor, Aider.
`;
const GAMES_INDEX_HTML = await renderPage({
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",
});
const renderKata = async (kata: string): Promise<Response | null> => {
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 kata — tdd.md`,
description,
bodyMarkdown: md,
ogPath: `https://tdd.md/games/${kata}`,
active: "games",
});
return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8" } });
};
const REGISTER_BODY = `# register
> Sign in with GitHub to create your tdd.md agent.
## what we ask GitHub for
- your username
- your primary verified email
That's it — no repo access, no anything else.
## what you get
- a public agent account at \`git.tdd.md/<your-github-name>\`
- a push token (shown once)
- an empty repo for the first kata, ready to push to
[ sign in with github → ](/auth/github/start)
`;
const REGISTER_HTML = await renderPage({
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",
noindex: true,
});
// ---------------------------------------------------------------------
// App factory — c11 calls createApp(port) to start the server. The
// routes literal stays inline here so Bun's path-parameter inference
// (`:slug` → `req.params.slug`) flows through to the handler types.
// ---------------------------------------------------------------------
export const createApp = (port: number) => Bun.serve({
port,
error: appError,
fetch: appFetch,
routes: {
"/": htmlResponse(HOME_HTML),
"/raw": new Response(Bun.file(HOME_MD), {
headers: { "Content-Type": "text/markdown; charset=utf-8" },
}),
"/install": new Response(SAMA_CLI_INSTALL_SCRIPT, {
headers: {
"Content-Type": "text/x-shellscript; charset=utf-8",
// Five minutes — long enough to absorb a curl|bash burst,
// short enough that pushing a fix to the verifier propagates
// promptly.
"Cache-Control": "public, max-age=300",
},
}),
"/healthz": new Response("ok"),
"/robots.txt": new Response(
// tdd.md is built for AI agents to read, audit, and learn from. We
// explicitly ALLOW the major AI crawlers + training agents. The site's
// entire empirical-chain argument depends on those agents being able
// to fetch the spec, the verifier output, the /goals archive, and the
// measurement posts.
//
// NOTE on Cloudflare: if "Block AI Crawlers" or "AI Audit / Content
// Signals" is enabled at the Cloudflare edge, CF injects Disallow
// blocks for these bots BEFORE this response body. App-level Allows
// here are defense-in-depth; the canonical fix is to disable that CF
// setting (Dashboard → Security → Bots → "Block AI Crawlers" off, or
// Content Signals → ai-train=yes).
`# tdd.md welcomes AI crawlers, agents, and training bots.\n` +
`# The empirical chain is meant to be read.\n\n` +
`User-agent: *\nAllow: /\nDisallow: /auth/\nDisallow: /api/\n\n` +
`User-agent: ClaudeBot\nAllow: /\n\n` +
`User-agent: Claude-Web\nAllow: /\n\n` +
`User-agent: GPTBot\nAllow: /\n\n` +
`User-agent: ChatGPT-User\nAllow: /\n\n` +
`User-agent: CCBot\nAllow: /\n\n` +
`User-agent: Google-Extended\nAllow: /\n\n` +
`User-agent: Applebot-Extended\nAllow: /\n\n` +
`User-agent: Amazonbot\nAllow: /\n\n` +
`User-agent: Bytespider\nAllow: /\n\n` +
`User-agent: meta-externalagent\nAllow: /\n\n` +
`User-agent: PerplexityBot\nAllow: /\n\n` +
`User-agent: Perplexity-User\nAllow: /\n\n` +
`Sitemap: https://tdd.md/sitemap.xml\n`,
{ headers: { "Content-Type": "text/plain; charset=utf-8" } },
),
"/sitemap.xml": () => {
const staticUrls: SitemapUrl[] = STATIC_PATHS.map((p) => ({
loc: `${SITE_BASE_URL}${p}`,
}));
const blogUrls: SitemapUrl[] = ALL_POSTS.map((p) => ({
loc: `${SITE_BASE_URL}/blog/${p.date.slice(0, 7)}/${p.slug}`,
lastmod: p.date,
}));
const samaUrls: SitemapUrl[] = ALL_SAMA.map((d) => ({
loc: `${SITE_BASE_URL}/sama/discipline/${d.slug}`,
}));
const guideUrls: SitemapUrl[] = ALL_GUIDES.map((g) => ({
loc: `${SITE_BASE_URL}/guides/${g.slug}`,
}));
const goalUrls: SitemapUrl[] = ALL_GOALS.map((g) => ({
loc: `${SITE_BASE_URL}/goals/${g.slug}`,
lastmod: g.date,
}));
const xml = renderSitemap([
...staticUrls,
...blogUrls,
...samaUrls,
...guideUrls,
...goalUrls,
]);
return new Response(xml, {
headers: {
"Content-Type": "application/xml; charset=utf-8",
"Cache-Control": "public, max-age=3600",
},
});
},
"/og.svg": new Response(Bun.file("./public/og.svg"), {
headers: {
"Content-Type": "image/svg+xml",
"Cache-Control": "public, max-age=3600",
},
}),
"/og.png": new Response(Bun.file("./public/og.png"), {
headers: {
"Content-Type": "image/png",
"Cache-Control": "public, max-age=3600",
},
}),
"/sama-hero.svg": new Response(Bun.file("./public/sama-hero.svg"), {
headers: { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=3600" },
}),
"/sama-hero.png": new Response(Bun.file("./public/sama-hero.png"), {
headers: { "Content-Type": "image/png", "Cache-Control": "public, max-age=3600" },
}),
"/sama-layers.svg": new Response(Bun.file("./public/sama-layers.svg"), {
headers: { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=3600" },
}),
"/sama-layers.png": new Response(Bun.file("./public/sama-layers.png"), {
headers: { "Content-Type": "image/png", "Cache-Control": "public, max-age=3600" },
}),
"/sama-metrics.svg": new Response(Bun.file("./public/sama-metrics.svg"), {
headers: { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=3600" },
}),
"/sama-metrics.png": new Response(Bun.file("./public/sama-metrics.png"), {
headers: { "Content-Type": "image/png", "Cache-Control": "public, max-age=3600" },
}),
"/sitemap-layers.svg": new Response(Bun.file("./public/sitemap-layers.svg"), {
headers: { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=3600" },
}),
"/sitemap-layers.png": new Response(Bun.file("./public/sitemap-layers.png"), {
headers: { "Content-Type": "image/png", "Cache-Control": "public, max-age=3600" },
}),
"/sitemap-flow.svg": new Response(Bun.file("./public/sitemap-flow.svg"), {
headers: { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=3600" },
}),
"/sitemap-flow.png": new Response(Bun.file("./public/sitemap-flow.png"), {
headers: { "Content-Type": "image/png", "Cache-Control": "public, max-age=3600" },
}),
"/games": htmlResponse(GAMES_INDEX_HTML),
"/blog": async () => {
const rows = ALL_POSTS
.map((p) => `| ${p.date} | [${p.title}](/blog/${p.date.slice(0, 7)}/${p.slug}) |`)
.join("\n");
const body = `# blog
Notes on TDD, agentic coding, and the discipline that ties them together.
| date | post |
|---|---|
${rows}
> RSS feed coming when there's a second post.
[← back to tdd.md](/) · [the guides](/guides) · [the katas](/games)
`;
const html = await renderDocsPage({
title: "Blog — tdd.md",
description: "Posts on test-driven development for AI coding agents — how to apply TDD with Claude Code, Cursor, and Aider, what we learn from the verdicts.",
bodyMarkdown: body,
ogPath: "https://tdd.md/blog",
active: "blog",
pathForDocs: "/blog",
editPathOverride: null,
});
return htmlResponse(html);
},
"/blog/:yyyymm/:slug": async (req) => {
const { yyyymm, slug } = req.params;
const entry = ALL_POSTS.find((p) => p.slug === slug);
const fullPath = `/blog/${yyyymm}/${slug}`;
// 404 if slug doesn't exist, OR if the yyyymm in the URL doesn't
// match the post's actual date prefix (prevents URL-spoofing where
// someone constructs /blog/9999-99/<valid-slug> and gets a 200).
if (!entry || entry.date.slice(0, 7) !== yyyymm) {
const html = await renderNotFound(fullPath);
return htmlResponse(html, 404);
}
const file = Bun.file(`./content/blog/${slug}.md`);
if (!(await file.exists())) {
const html = await renderNotFound(fullPath);
return htmlResponse(html, 404);
}
const md = await file.text();
const html = await renderDocsPage({
title: `${entry.title} — tdd.md`,
description: entry.description,
bodyMarkdown: md,
ogPath: `https://tdd.md${fullPath}`,
active: "blog",
pathForDocs: fullPath,
jsonLd: {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: entry.title,
description: entry.description,
datePublished: entry.date,
url: `https://tdd.md${fullPath}`,
author: { "@type": "Organization", name: "tdd.md" },
},
});
return htmlResponse(html);
},
"/projects": projectsLandingHandler,
"/projects/new": projectsNewHandler,
"/projects/:repoOwner/:repoName": projectDetailHandler,
"/reports": reportsLandingHandler,
"/reports/demo": reportsDemoHandler,
"/reports/demo/tests": reportsDemoTestsHandler,
"/reports/demo/agents/:slug": reportsDemoAgentHandler,
"/reports/live": reportsLiveHandler,
"/reports/live/tests": reportsLiveTestsHandler,
"/reports/live/agents/:slug": reportsLiveAgentHandler,
"/guides": async () => {
const rows = ALL_GUIDES
.map((g) => `| [${g.title}](/guides/${g.slug}) | ${g.description} |`)
.join("\n");
const body = `# guides
Agent-specific walkthroughs for using tdd.md with the major agentic-coding tools. Each guide covers setup, prompt patterns that keep the agent in TDD, and the common pitfalls that cost score.
| guide | what it covers |
|---|---|
${rows}
> Missing your agent? [The mechanics are the same](/) — push commits tagged \`red:\` / \`green:\` / \`refactor:\` to your kata repo. Send a PR with a new guide and we'll list it here.
[← play a kata](/games) · [register your agent →](/you)
`;
const html = await renderDocsPage({
title: "TDD guides for agentic coding tools — tdd.md",
description: "Practical TDD walkthroughs for Claude Code, Cursor, Aider and other AI coding agents — keep your agent honest with red→green→refactor commits, scored by tdd.md.",
bodyMarkdown: body,
ogPath: "https://tdd.md/guides",
active: "guides",
pathForDocs: "/guides",
editPathOverride: null,
});
return htmlResponse(html);
},
"/guides/:slug": async (req) => {
const slug = req.params.slug;
const entry = ALL_GUIDES.find((g) => g.slug === slug);
if (!entry) {
const html = await renderNotFound(`/guides/${slug}`);
return htmlResponse(html, 404);
}
const file = Bun.file(`./content/guides/${slug}.md`);
if (!(await file.exists())) {
const html = await renderNotFound(`/guides/${slug}`);
return htmlResponse(html, 404);
}
const md = await file.text();
const html = await renderDocsPage({
title: `${entry.title} — tdd.md`,
description: entry.description,
bodyMarkdown: md,
ogPath: `https://tdd.md/guides/${slug}`,
active: "guides",
pathForDocs: `/guides/${slug}`,
});
return htmlResponse(html);
},
"/skills/sama.md": skillsSamaMdHandler,
"/tools/sama-cli": samaCliResponse(),
"/sama/skill": samaSkillHandler,
"/sama/v2": samaV2Handler,
"/sama/v2/verify": samaV2VerifyHandler,
"/sama/v2/example-crud": samaV2ExampleCrudHandler,
"/sama/v2/example-wordpress": samaV2ExampleWordpressHandler,
"/sama/verify": samaVerifyHandler,
"/sama": samaLandingHandler,
"/sama/discipline/:slug": samaSlugHandler,
"/goals": goalsLandingHandler,
"/goals/:slug": goalSlugHandler,
"/contributing": contributingHandler,
"/games/:kata": async (req) => {
const res = await renderKata(req.params.kata);
if (res) return res;
const html = await renderNotFound(`/games/${req.params.kata}`);
return htmlResponse(html, 404);
},
"/agents": () => renderAgentsIndex(),
"/agents/register": htmlResponse(REGISTER_HTML),
"/agents/:name": async (req) => {
const viewer = await getViewer(req);
return renderAgentDetail(req.params.name, viewer);
},
// Redirect the legacy URL to the canonical /:owner/:repo path —
// /agents/:name/:kata used to render a placeholder before the
// GitHub-style routing landed.
"/agents/:name/:kata": (req) =>
Response.redirect(`/${req.params.name}/${req.params.kata}`, 301),
"/leaderboard": () => renderLeaderboard(),
"/api/judge/:owner/:repo": judgeApiHandler,
"/api/agents/:name/visibility": agentVisibilityHandler,
"/api/forgejo/webhook": forgejoWebhookHandler,
"/you": async (req) => {
const viewer = await getViewer(req);
const target = viewer ? `/agents/${viewer}` : "/auth/github/start";
return new Response(null, { status: 302, headers: { Location: target } });
},
"/auth/logout": (_req) => {
// Clear the session cookie and bounce back home.
return new Response(null, {
status: 302,
headers: {
Location: "/",
"Set-Cookie": sessionCookieHeader("", 0),
},
});
},
"/edit/:section/:slug": editPageHandler,
// Admin UI — sxdoc-backed CRUD on pages + posts. Replaces the legacy
// /edit flow in Fase 6; both live alongside until migration cutover.
"/admin": adminListHandler,
"/admin/new": adminNewHandler,
"/admin/edit/:type/:slug": adminEditHandler,
"/admin/delete/:type/:slug": adminDeleteHandler,
// Public sxdoc-backed pages — single-segment fast path. Multi-segment
// slugs fall through to appFetch's regex matcher above.
"/p/:slug": publicPageHandler,
"/admin/assets/blockeditor.js": async (req) => {
const { code, etag } = await bundleAdminClient();
if (req.headers.get("if-none-match") === etag) {
return new Response(null, { status: 304, headers: { ETag: etag } });
}
return new Response(code, {
headers: {
"Content-Type": "application/javascript; charset=utf-8",
"ETag": etag,
"Cache-Control": "no-cache",
},
});
},
// Raw markdown source — replaces the previous git.tdd.md "view source"
// link so docs pages don't depend on the Forgejo subdomain. The
// route uses `:filename` (with trailing `.md` validated in the
// handler) because Bun's parser treats `:slug.md` as a single param.
"/content/:section/:filename": rawSourceHandler,
// SAMA-native commit view — Bun-rendered alternative to Forgejo's
// /<owner>/<repo>/commit/<sha> page. The :sha param may carry a
// trailing ".diff" which the handler handles inline.
"/GIT/:repo/commit/:sha": commitViewHandler,
"/auth/github/start": (req) => startGithubOauth(req),
"/auth/github/callback": async (req) => handleGithubCallback(req),
},
});