syntaxai/tdd.md · commit 8e72c3b

SAMA Atomic: split c21_app.ts per-domain (handlers_sama + handlers_reports)

Fixes the last Atomic violation that the verifier had been flagging on
this codebase since before the SAMA-native pass: c21_app.ts had grown
to 1145 lines, well over the 700-line split threshold. Per the SAMA
convention's per-domain rule, the dispatcher stays in c21_app.ts and
each cluster moves to c21_handlers_<domain>.ts.

  src/c31_site_config.ts (new)
    LIVE_REPO_OWNER, LIVE_REPO_NAME, LIVE_FETCH_COUNT extracted so
    the new handler files reference the same values without forcing
    circular imports between c21_handlers_*.

  src/c21_handlers_sama.ts (new, ~330 lines)
    skillsSamaMdHandler        /skills/sama.md (raw download)
    samaCliResponse            /tools/sama-cli (binary download)
    samaSkillHandler           /sama/skill (HTML viewer)
    samaVerifyHandler          /sama/verify (form + report + dogfood)
    samaLandingHandler         /sama (the four-property page)
    samaSlugHandler            /sama/:slug (per-discipline content)
    Plus internal helpers verifyLocalDogfood, verifyRemoteRepo,
    renderVerifyReport, and the SAMA_LANDING_MD body template.

  src/c21_handlers_reports.ts (new, ~190 lines)
    reportsLandingHandler        /reports
    reportsDemoHandler           /reports/demo
    reportsDemoTestsHandler      /reports/demo/tests
    reportsDemoAgentHandler      /reports/demo/agents/:slug
    reportsLiveHandler           /reports/live
    reportsLiveTestsHandler      /reports/live/tests
    reportsLiveAgentHandler      /reports/live/agents/:slug
    Plus the demoContext/liveContext builders and DEMO/LIVE banners.

  src/c21_app.ts
    1145 lines -> 649 lines. Routes literal stays as the dispatcher
    so Bun.serve still sees literal keys (path-parameter inference
    flows through). Each affected route body collapses to a one-line
    handler reference.

Verifier dogfood after the split (sama check from this repo's root):
  S — Sorted: ✓ pass (34 files)
  A — Architecture: ✓ pass (34 files)
  M — Modeled: ✗ 4 violations (the four remaining c32_* logic files
                                without sibling tests — c32_judge,
                                c32_session, c32_real_reports,
                                c32_real_tests. Separate sliver.)
  A — Atomic: ✓ pass (34 files) — the 700-line violation is gone.

Verified end-to-end: /sama/* and /reports/* (14 routes) all return
200. /sama/verify?repo=syntaxai/tdd.md self-report mirrors the CLI.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
author
syntaxai <[email protected]>
date
2026-05-10 07:42:10 +01:00
parent
bd8f3aa
commit
8e72c3b810ef81c69ce002c701ca6c4c02a7ee44

4 files changed · +621 −526

modified src/c21_app.ts +31 −526
@@ -6,7 +6,6 @@ import {
66 renderPage,
77 renderNotFound,
88 htmlResponse,
9- escape,
109 } from "./c51_render_layout.ts";
1110 import { renderDocsPage } from "./c51_render_docs_layout.ts";
1211 import {
@@ -14,37 +13,16 @@ import {
1413 projectRegisterMd,
1514 projectDetailMd,
1615 } from "./c51_render_projects.ts";
17-import {
18- reportsLandingMd,
19- execSummaryMd,
20- agentDrilldownMd,
21- testsOverviewMd,
22-} from "./c51_render_reports.ts";
2316 import {
2417 FORGEJO_URL,
2518 adminApiHeaders,
2619 proxyToForgejo,
2720 } from "./c14_forgejo.ts";
28-import {
29- fetchProjectConfig,
30- fetchRepoTree,
31- fetchRepoRawFile,
32-} from "./c14_github.ts";
33-import { verifySama, type SamaReport } from "./c32_sama_verify.ts";
21+import { fetchProjectConfig } from "./c14_github.ts";
3422 import { listGames, loadGame } from "./c31_games.ts";
3523 import { ALL_POSTS } from "./c31_blog.ts";
3624 import { ALL_GUIDES } from "./c31_guides.ts";
3725 import { ALL_SAMA } from "./c31_sama.ts";
38-import {
39- DEMO_REPORTS,
40- DEMO_PERIOD,
41- DEMO_ORG,
42- DEMO_REPOS,
43- DEMO_SNAPSHOTS,
44- DEMO_STABILITY,
45-} from "./c31_reports_demo.ts";
46-import { buildLiveReports } from "./c32_real_reports.ts";
47-import { buildLiveTestData } from "./c32_real_tests.ts";
4826 import { parseRepoIdentifier } from "./c31_project_config.ts";
4927 import { judge } from "./c32_judge.ts";
5028 import {
@@ -62,58 +40,27 @@ import { renderRepoView } from "./c21_handlers_repo_view.ts";
6240 import { renderAgentsIndex, renderAgentDetail } from "./c21_handlers_agents.ts";
6341 import { renderLeaderboard } from "./c21_handlers_leaderboard.ts";
6442 import { startGithubOauth, handleGithubCallback } from "./c21_handlers_auth.ts";
43+import {
44+ reportsLandingHandler,
45+ reportsDemoHandler,
46+ reportsDemoTestsHandler,
47+ reportsDemoAgentHandler,
48+ reportsLiveHandler,
49+ reportsLiveTestsHandler,
50+ reportsLiveAgentHandler,
51+} from "./c21_handlers_reports.ts";
52+import {
53+ skillsSamaMdHandler,
54+ samaCliResponse,
55+ samaSkillHandler,
56+ samaVerifyHandler,
57+ samaLandingHandler,
58+ samaSlugHandler,
59+} from "./c21_handlers_sama.ts";
6560
6661 const HOME_MD = "./content/home.md";
6762 const GAME_DIR = "./content/games";
6863
69-// ---------------------------------------------------------------------
70-// Reports-context builders. The c51 builders take a ReportsContext —
71-// these tiny helpers assemble it for the synthetic /reports/demo and
72-// the live /reports/live (real data fetched from syntaxai/tdd.md).
73-// ---------------------------------------------------------------------
74-
75-const LIVE_REPO_OWNER = "syntaxai";
76-const LIVE_REPO_NAME = "tdd.md";
77-const LIVE_FETCH_COUNT = 100;
78-
79-const DEMO_BANNER_HTML = `<div class="report-mockup-banner">demo data — design preview with synthetic numbers. Want the real readout? <a href="/reports/live">/reports/live</a> renders the same shape from live tdd.md commits. <a href="/blog/tweag-handbook-tdd">why tdd.md needs this</a></div>`;
80-
81-const LIVE_BANNER_HTML = `<div class="report-mockup-banner">live data — sourced from <a href="https://github.com/${LIVE_REPO_OWNER}/${LIVE_REPO_NAME}">${LIVE_REPO_OWNER}/${LIVE_REPO_NAME}</a> via the public commits API (5-min cache). Agent attribution comes from <code>Co-Authored-By:</code> footers; commits without one are excluded. Phase coverage measures % of commits tagged <code>red:/green:/refactor:</code>.</div>`;
82-
83-const demoContext = () => ({
84- reports: DEMO_REPORTS,
85- period: DEMO_PERIOD,
86- scopeLabel: `${DEMO_REPOS} repos · ${DEMO_ORG}`,
87- bannerHtml: DEMO_BANNER_HTML,
88- narrative: {
89- changedHeading: "what changed this quarter",
90- changedBody:
91- "Cursor's score dropped 15 points after agent-mode became default in March; test-deletion incidents climbed from 2% to 14% of refactor commits, concentrated in the `api-gateway` repo. Claude Code's score rose after a phase-tagged commit prefix was added to CLAUDE.md at the end of January. Aider stays steadily high — auto-commit-per-edit prevents most cross-phase cheating on its own.",
92- doingHeading: "what we're doing",
93- doingBody:
94- "- **Cursor in `api-gateway`**: agent-mode disabled for refactor prompts, CONVENTIONS rule \"never delete a test in a refactor commit\" pinned ([details →](/reports/demo/agents/cursor)).\n- **Roll out Claude Code**: copy the CLAUDE.md template that worked in `billing-service` to the other three repos.\n- **Next reading**: 2026-04-30, mid-Q2, to check whether the Cursor fix holds.",
95- },
96- footerLinks:
97- "[per-agent drill-down: Claude Code](/reports/demo/agents/claude-code) · [Cursor](/reports/demo/agents/cursor) · [Aider](/reports/demo/agents/aider) · [tests overview](/reports/demo/tests) · [back to /reports](/reports)",
98-});
99-
100-const liveContext = async () => {
101- const live = await buildLiveReports(LIVE_REPO_OWNER, LIVE_REPO_NAME, LIVE_FETCH_COUNT);
102- const period = live.earliest && live.latest
103- ? `${live.earliest.slice(0, 10)} → ${live.latest.slice(0, 10)}`
104- : "no commits fetched";
105- const drillLinks = live.reports
106- .map((r) => `[${r.name}](/reports/live/agents/${r.slug})`)
107- .join(" · ");
108- return {
109- reports: live.reports,
110- period,
111- scopeLabel: `${LIVE_REPO_OWNER}/${LIVE_REPO_NAME} · ${live.totalCommits} commits sampled${live.unknownCount > 0 ? ` (${live.unknownCount} unattributed, excluded)` : ""}`,
112- bannerHtml: LIVE_BANNER_HTML,
113- footerLinks: `${drillLinks ? drillLinks + " · " : ""}[tests overview](/reports/live/tests) · [demo preview](/reports/demo) · [back to /reports](/reports)`,
114- };
115-};
116-
11764 const HOME_DESCRIPTION =
11865 "Test-driven development for agentic coding. Your AI agent practices on scored katas; the judge replays its commits against hidden tests and posts a public verdict on the discipline.";
11966
@@ -479,119 +426,13 @@ ${rows}
479426 return htmlResponse(html);
480427 },
481428
482- "/reports": async () => {
483- const html = await renderPage({
484- title: "Reports — tdd.md",
485- description: "Per-agent TDD-discipline reporting over real project repos: trend, failure-mode breakdown, and an exec summary fit for a quarterly readout.",
486- bodyMarkdown: reportsLandingMd(),
487- ogPath: "https://tdd.md/reports",
488- noindex: true,
489- });
490- return htmlResponse(html);
491- },
492-
493- "/reports/demo": async () => {
494- const ctx = demoContext();
495- const html = await renderPage({
496- title: "TDD-discipline report · Q1 2026 (demo) — tdd.md",
497- description: "Mockup of the management-level TDD-discipline report — single page, three agents, with trend and narrative.",
498- bodyMarkdown: execSummaryMd(ctx),
499- ogPath: "https://tdd.md/reports/demo",
500- noindex: true,
501- });
502- return htmlResponse(html);
503- },
504-
505- "/reports/demo/tests": async () => {
506- const html = await renderPage({
507- title: "Tests overview (demo) — tdd.md",
508- description: "Mockup of the per-test overview: current pass/fail snapshot per repo plus test stability over the quarter.",
509- bodyMarkdown: testsOverviewMd({
510- period: DEMO_PERIOD,
511- bannerHtml: DEMO_BANNER_HTML,
512- snapshots: DEMO_SNAPSHOTS,
513- stability: DEMO_STABILITY,
514- }),
515- ogPath: "https://tdd.md/reports/demo/tests",
516- noindex: true,
517- });
518- return htmlResponse(html);
519- },
520-
521- "/reports/demo/agents/:slug": async (req) => {
522- const slug = req.params.slug as (typeof DEMO_REPORTS)[number]["slug"];
523- const ctx = demoContext();
524- const md = agentDrilldownMd(slug, ctx);
525- if (!md) {
526- const html = await renderNotFound(`/reports/demo/agents/${slug}`);
527- return htmlResponse(html, 404);
528- }
529- const entry = DEMO_REPORTS.find((r) => r.slug === slug)!;
530- const html = await renderPage({
531- title: `${entry.name} drill-down (demo) — tdd.md`,
532- description: `Per-agent drill-down mockup for ${entry.name}: trend, failure-mode breakdown, recent flagged commits with coaching links.`,
533- bodyMarkdown: md,
534- ogPath: `https://tdd.md/reports/demo/agents/${slug}`,
535- noindex: true,
536- });
537- return htmlResponse(html);
538- },
539-
540- "/reports/live": async () => {
541- const ctx = await liveContext();
542- const html = await renderPage({
543- title: "TDD-discipline report · live — tdd.md",
544- description: `Live discipline report built from the real commit history of syntaxai/tdd.md (last ${LIVE_FETCH_COUNT} commits, 5-min cache).`,
545- bodyMarkdown: execSummaryMd(ctx),
546- ogPath: "https://tdd.md/reports/live",
547- noindex: true,
548- });
549- return htmlResponse(html);
550- },
551-
552- "/reports/live/tests": async () => {
553- const data = await buildLiveTestData(LIVE_REPO_OWNER, LIVE_REPO_NAME);
554- const ranOn = data.ranAt ? new Date(data.ranAt).toISOString().slice(0, 10) : null;
555- const period = data.runsCount === 0
556- ? "no runs in bundle"
557- : `last run ${ranOn} · ${data.runsCount} run${data.runsCount === 1 ? "" : "s"} cumulative`;
558- const unavailableNote = data.runsCount === 0
559- ? "No test runs bundled yet. The next deploy will run `bun test --reporter=junit` on the current HEAD and publish the result here. Stability (flaky %, deletion) builds up as more runs land in the bundle — the demo at [/reports/demo/tests](/reports/demo/tests) shows where this is heading."
560- : undefined;
561- const html = await renderPage({
562- title: "Tests overview · live — tdd.md",
563- description: `Live test snapshot of ${LIVE_REPO_OWNER}/${LIVE_REPO_NAME} — ${data.runsCount} run${data.runsCount === 1 ? "" : "s"} bundled.`,
564- bodyMarkdown: testsOverviewMd({
565- period,
566- bannerHtml: LIVE_BANNER_HTML,
567- snapshots: data.snapshots,
568- stability: data.stability,
569- unavailableNote,
570- placeholderTests: data.placeholderTests,
571- }),
572- ogPath: "https://tdd.md/reports/live/tests",
573- });
574- return htmlResponse(html);
575- },
576-
577- "/reports/live/agents/:slug": async (req) => {
578- const ctx = await liveContext();
579- const slug = req.params.slug as (typeof DEMO_REPORTS)[number]["slug"];
580- const md = agentDrilldownMd(slug, ctx);
581- if (!md) {
582- const html = await renderNotFound(`/reports/live/agents/${slug}`);
583- return htmlResponse(html, 404);
584- }
585- const entry = ctx.reports.find((r) => r.slug === slug)!;
586- const html = await renderPage({
587- title: `${entry.name} drill-down · live — tdd.md`,
588- description: `Live drill-down for ${entry.name} on syntaxai/tdd.md — trend, failure-mode breakdown, recent commits.`,
589- bodyMarkdown: md,
590- ogPath: `https://tdd.md/reports/live/agents/${slug}`,
591- noindex: true,
592- });
593- return htmlResponse(html);
594- },
429+ "/reports": reportsLandingHandler,
430+ "/reports/demo": reportsDemoHandler,
431+ "/reports/demo/tests": reportsDemoTestsHandler,
432+ "/reports/demo/agents/:slug": reportsDemoAgentHandler,
433+ "/reports/live": reportsLiveHandler,
434+ "/reports/live/tests": reportsLiveTestsHandler,
435+ "/reports/live/agents/:slug": reportsLiveAgentHandler,
595436
596437 "/guides": async () => {
597438 const rows = ALL_GUIDES
@@ -645,352 +486,16 @@ ${rows}
645486 return htmlResponse(html);
646487 },
647488
648- "/skills/sama.md": async () => {
649- const md = await Bun.file("./content/sama/skill.md").text();
650- return new Response(md, {
651- headers: {
652- "Content-Type": "text/markdown; charset=utf-8",
653- "Cache-Control": "public, max-age=300",
654- },
655- });
656- },
657-
658- "/tools/sama-cli": new Response(Bun.file("./public/sama-cli"), {
659- headers: {
660- // text/javascript so browsers preview as code; the shebang at
661- // line 1 makes the file directly executable once chmod +x'd.
662- "Content-Type": "text/javascript; charset=utf-8",
663- "Content-Disposition": 'inline; filename="sama"',
664- "Cache-Control": "public, max-age=300",
665- },
666- }),
667-
668- "/sama/skill": async () => {
669- const raw = await Bun.file("./content/sama/skill.md").text();
670- // Strip the YAML frontmatter for the HTML render — the .md raw
671- // download keeps it (that's the agent-installable format).
672- const stripped = raw.replace(/^---\n[\s\S]*?\n---\n+/, "");
673- const installNote = `> **Drop into your agent.** Save the raw markdown to your skills directory:
674->
675-> \`\`\`bash
676-> mkdir -p ~/.claude/skills
677-> curl -fsSL https://tdd.md/skills/sama.md -o ~/.claude/skills/sama.md
678-> \`\`\`
679->
680-> The frontmatter at the top of the file (\`name\`, \`description\`) is what your agent's loader keys off — don't edit it. [View raw markdown →](/skills/sama.md)
681-`;
682- const body = `${installNote}\n\n${stripped}\n\n---\n\n[← /sama](/sama) · [the four disciplines](/sama) · [back to tdd.md](/)\n`;
683- const html = await renderDocsPage({
684- title: "SAMA skill — drop into your agent — tdd.md",
685- description: "An obra/superpowers-style SKILL.md for the SAMA file-naming convention. Save it to ~/.claude/skills/sama.md and your agent will load the layer-prefix discipline on demand.",
686- bodyMarkdown: body,
687- ogPath: "https://tdd.md/sama/skill",
688- active: "sama",
689- pathForDocs: "/sama/skill",
690- });
691- return htmlResponse(html);
692- },
693-
694- "/sama/verify": async (req) => {
695- const url = new URL(req.url);
696- const repoArg = (url.searchParams.get("repo") ?? "").trim();
697- const formMd = `# SAMA verify
698-
699-> Paste a public GitHub repo. tdd.md will run the four [SAMA disciplines](/sama) against the default branch — *Sorted* (lower never imports higher), *Architecture* (known layer prefixes), *Modeled* (sibling tests, types in c31_*), *Atomic* (~700-line split + placeholder-test detection) — and return a report. No clone, no token; just one tree-listing API call plus raw-content reads. Cached for an hour per repo.
700-
701-<form method="get" action="/sama/verify" class="sama-verify-form">
702- <label>
703- public GitHub repo:
704- <input type="text" name="repo" placeholder="owner/name" required pattern="[^/\\s]+/[^/\\s]+" />
705- </label>
706- <button type="submit">verify</button>
707-</form>
708-
709-Try it on this site: [\`syntaxai/tdd.md\`](/sama/verify?repo=syntaxai/tdd.md) · or any public repo of your own.
710-
711-Limits: anonymous GitHub API quota is 60 requests/hour per IP. Each verify uses one tree-listing call; the rest of the work goes through raw.githubusercontent.com (uncapped). If the verifier returns "rate limit", come back later or use a token-authenticated proxy.
712-
713-[← /sama](/sama)
714-`;
715-
716- if (!repoArg) {
717- const html = await renderDocsPage({
718- title: "SAMA verify — tdd.md",
719- description: "Paste a public GitHub repo, get the four SAMA disciplines verified mechanically: sorted (lower never imports higher), architecture (known layer prefixes), modeled (sibling tests), atomic (700-line + placeholder-test detection).",
720- bodyMarkdown: formMd,
721- ogPath: "https://tdd.md/sama/verify",
722- active: "sama",
723- pathForDocs: "/sama/verify",
724- });
725- return htmlResponse(html);
726- }
727-
728- const m = /^([^\/\s]+)\/([^\/\s]+)$/.exec(repoArg);
729- if (!m) {
730- const html = await renderDocsPage({
731- title: "SAMA verify · bad input — tdd.md",
732- description: "SAMA verify expects an owner/name repo identifier.",
733- bodyMarkdown: `# SAMA verify\n\n> Couldn't parse \`${repoArg}\`. Use the form: \`owner/name\`.\n\n[← back](/sama/verify)\n`,
734- pathForDocs: "/sama/verify",
735- editPathOverride: null,
736- ogPath: "https://tdd.md/sama/verify",
737- active: "sama",
738- noindex: true,
739- });
740- return htmlResponse(html, 400);
741- }
742-
743- const [, owner, name] = m;
744- let report: SamaReport;
745- try {
746- // Dogfood short-circuit: tdd.md is a private repo, so the GitHub
747- // API can't see it. When asked to verify ourselves, read the
748- // source from the bundled `./src/` directory inside the container.
749- // Same checks, same shape, same code path downstream.
750- const isSelf = owner === LIVE_REPO_OWNER && name === LIVE_REPO_NAME;
751- if (isSelf) {
752- const { readdirSync, readFileSync } = await import("node:fs");
753- const srcDir = "./src";
754- const tsFiles = readdirSync(srcDir, { withFileTypes: true })
755- .filter((e) => e.isFile() && e.name.endsWith(".ts"))
756- .map((e) => e.name)
757- .sort();
758- const contents = new Map<string, string>();
759- for (const f of tsFiles) {
760- if (/^c\d{2}_/.test(f)) {
761- contents.set(f, readFileSync(`${srcDir}/${f}`, "utf8"));
762- }
763- }
764- report = verifySama({
765- repoOwner: owner!,
766- repoName: name!,
767- defaultBranch: "main",
768- srcPaths: tsFiles,
769- contents,
770- });
771- } else {
772- const tree = await fetchRepoTree(owner!, name!);
773- const srcEntries = tree.entries
774- .filter((e) => e.type === "blob" && e.path.startsWith("src/") && e.path.endsWith(".ts"))
775- .slice(0, 200);
776- const srcPaths = srcEntries.map((e) => e.path.slice("src/".length));
777- const samaPaths = srcPaths.filter((p) => /^c\d{2}_/.test(p));
778- const contents = new Map<string, string>();
779- const fetches = await Promise.all(
780- samaPaths.map(async (p) => [p, await fetchRepoRawFile(owner!, name!, tree.defaultBranch, `src/${p}`)] as const),
781- );
782- for (const [p, c] of fetches) {
783- if (c !== null) contents.set(p, c);
784- }
785- report = verifySama({
786- repoOwner: owner!,
787- repoName: name!,
788- defaultBranch: tree.defaultBranch,
789- srcPaths,
790- contents,
791- });
792- }
793- } catch (e) {
794- const msg = e instanceof Error ? e.message : String(e);
795- const html = await renderDocsPage({
796- title: `SAMA verify · ${owner}/${name} · error — tdd.md`,
797- description: `SAMA verify could not inspect ${owner}/${name}.`,
798- bodyMarkdown: `# SAMA verify · \`${owner}/${name}\`\n\n> Couldn't fetch the repo: ${escape(msg)}\n\nMost common causes: the repo is private, the name is wrong, or you've hit GitHub's anonymous rate limit (60/hour). [← try another repo](/sama/verify)\n`,
799- ogPath: `https://tdd.md/sama/verify?repo=${owner}/${name}`,
800- active: "sama",
801- noindex: true,
802- pathForDocs: "/sama/verify",
803- editPathOverride: null,
804- });
805- return htmlResponse(html, 502);
806- }
807-
808- const summary = report.overallPassed
809- ? `> ✓ All four checks passed for [\`${report.repoSlug}\`](https://github.com/${report.repoSlug}) on \`${report.defaultBranch}\` (${report.samaFiles} SAMA files / ${report.testFiles} tests / ${report.totalSrcFiles} total in src/).`
810- : `> ⚠ ${report.checks.filter((c) => !c.passed).length} of 4 checks failed for [\`${report.repoSlug}\`](https://github.com/${report.repoSlug}) on \`${report.defaultBranch}\`.`;
811- const checkBlocks = report.checks
812- .map((c) => {
813- const status = c.passed ? "✓ pass" : `✗ ${c.violations.length} violation${c.violations.length === 1 ? "" : "s"}`;
814- const violationsBlock = c.violations.length === 0
815- ? ""
816- : `\n\n${c.violations.slice(0, 20).map((v) => `- \`${escape(v.file)}\` — ${escape(v.detail)}`).join("\n")}${c.violations.length > 20 ? `\n- _...and ${c.violations.length - 20} more_` : ""}`;
817- const noteBlock = c.note ? `\n\n_${escape(c.note)}_` : "";
818- return `### ${c.letter} — ${c.property} · ${status}\n\nExamined ${c.examined} file${c.examined === 1 ? "" : "s"}.${violationsBlock}${noteBlock}`;
819- })
820- .join("\n\n");
821- const reportMd = `# SAMA verify · \`${report.repoSlug}\`
822-
823-${summary}
824-
825-${checkBlocks}
826-
827----
828-
829-[← verify another repo](/sama/verify) · [the four SAMA disciplines →](/sama) · [SAMA skill for your agent →](/sama/skill)
830-`;
831- const html = await renderDocsPage({
832- title: `SAMA verify · ${report.repoSlug} — tdd.md`,
833- description: `SAMA verification for ${report.repoSlug}: ${report.overallPassed ? "all four checks passed" : `${report.checks.filter((c) => !c.passed).length}/4 checks failed`}.`,
834- bodyMarkdown: reportMd,
835- ogPath: `https://tdd.md/sama/verify?repo=${report.repoSlug}`,
836- active: "sama",
837- pathForDocs: "/sama/verify",
838- editPathOverride: null,
839- });
840- return htmlResponse(html);
841- },
842-
843- "/sama": async () => {
844- const rows = ALL_SAMA
845- .map((d) => `| **[${d.letter} — ${d.title}](/sama/${d.slug})** | ${d.rule} |`)
846- .join("\n");
847- const body = `# SAMA
848-
849-> **Sorted, Architecture, Modeled, Atomic.** Four properties of a codebase that an AI agent can navigate, change, and verify without drift. The acronym is the rule set; each letter has a one-paragraph definition and a verification you can run.
850-
851-This is the file-naming and module-organisation convention this site is built on, shared across two other projects in my workspace. It exists to give an AI agent **one obvious place** for every change — and one mechanical check for every layer rule.
852-
853-## the four disciplines
854-
855-| letter | discipline | one-line rule |
856-|---|---|---|
857-${rows}
858-
859-## reading order
860-
861-If you're new to this:
862-1. Start with **[Sorted](/sama/sorted)** — it has the verification grep that everything else is built around.
863-2. Then **[Architecture](/sama/architecture)** — what each layer prefix means.
864-3. Then **[Modeled](/sama/modeled)** — where types and tests live.
865-4. Then **[Atomic](/sama/atomic)** — the split rule that keeps the rest honest as the codebase grows.
866-
867-Each page is short, opinionated, and ends with the common mistakes you'll see if the discipline lapses.
868-
869-## drop into your agent
870-
871-For agents that load skills from \`~/.claude/skills/\` (Claude Code, obra/superpowers, etc.), grab the SKILL.md version:
872-
873-\`\`\`bash
874-mkdir -p ~/.claude/skills
875-curl -fsSL https://tdd.md/skills/sama.md -o ~/.claude/skills/sama.md
876-\`\`\`
877-
878-The skill is the same content as the four pages here, written in obra/superpowers SKILL.md format with frontmatter, an iron-rule statement, and a verification checklist your agent can run before merging. **[Read it formatted →](/sama/skill)** · **[Raw markdown →](/skills/sama.md)**
879-
880-## verify any public repo
881-
882-Want to know whether a repo follows SAMA without reading its source? Paste the \`owner/name\` and tdd.md will run all four checks against the default branch — *Sorted* (the import-direction grep), *Architecture* (known layer prefixes), *Modeled* (sibling tests), *Atomic* (700-line + placeholder-test detection). Pass/fail per discipline, with violation lists. **[verify a repo on the web →](/sama/verify)** · or try it on this site: [\`syntaxai/tdd.md\`](/sama/verify?repo=syntaxai/tdd.md).
883-
884-## the \`sama\` CLI
489+ "/skills/sama.md": skillsSamaMdHandler,
490+ "/tools/sama-cli": samaCliResponse(),
885491
886-The web verifier is good for ad-hoc checks. For CI and pre-commit, install the standalone CLI — same checks, no network needed for local repos:
492+ "/sama/skill": samaSkillHandler,
887493
888-\`\`\`bash
889-mkdir -p ~/.local/bin
890-curl -fsSL https://tdd.md/tools/sama-cli -o ~/.local/bin/sama
891-chmod +x ~/.local/bin/sama
892-sama --help
893-\`\`\`
494+ "/sama/verify": samaVerifyHandler,
894495
895-Two subcommands:
496+ "/sama": samaLandingHandler,
896497
897-\`\`\`bash
898-sama check # verify the current repo's src/
899-sama check --json # JSON output for piping into CI tooling
900-sama verify-repo owner/name # verify a public GitHub repo (no token)
901-\`\`\`
902-
903-Exit codes: \`0\` on pass, \`1\` if any check fails, \`2\` on error. The CLI is a single Bun bundle (~14 KB). [Bun](https://bun.sh) needs to be on \`PATH\`.
904-
905-### pre-commit hook
906-
907-Add to \`.git/hooks/pre-commit\` (or via \`husky\`, \`pre-commit\`, \`lefthook\`):
908-
909-\`\`\`bash
910-#!/usr/bin/env bash
911-# Block commits that violate SAMA layer/atomic/modeled rules.
912-exec sama check
913-\`\`\`
914-
915-### GitHub Action
916-
917-\`\`\`yaml
918-# .github/workflows/sama.yml
919-name: sama
920-on: [push, pull_request]
921-jobs:
922- verify:
923- runs-on: ubuntu-latest
924- steps:
925- - uses: actions/checkout@v4
926- - uses: oven-sh/setup-bun@v2
927- - run: |
928- curl -fsSL https://tdd.md/tools/sama-cli -o sama
929- chmod +x sama
930- ./sama check
931-\`\`\`
932-
933-If the rule lives in a hook or an action that fails the build, the harness can't talk the agent out of it. That is the whole point of the [corpus post](/blog/agentic-coding-corpus-three-patterns) and the next step from the [from-rules-to-checks](/blog/from-rules-to-checks) wrap-up.
934-
935-## the case behind it
936-
937-Two long-form pieces that argue *why* SAMA is shaped this way:
938-
939-- [**The Claude Code harness postmortem read through TDD + SAMA**](/blog/claude-code-harness-postmortem) — ThePaSch's r/ClaudeAI audit (40+ hidden reminders, 5 gag-order sites, 158 prompt versions in 11 days) read against the iron law and the verification grep. *The harness is loud; the diff doesn't have to be.*
940-- [**Three patterns ten threads converge on**](/blog/agentic-coding-corpus-three-patterns) — a six-month corpus of r/ClaudeAI, r/ClaudeCode, r/AgentsOfAI failure-mode threads. Per-pattern mitigation tables map each thread to the SAMA / iron-law rule that catches or prevents it.
941-
942-If you're reading these for the first time, the order to take them is harness postmortem → corpus → back here.
943-
944-## why these four together
945-
946-Each property fixes a different failure mode:
947-
948-- *Sorted* fails when imports go in any direction → grep proves the rule.
949-- *Architecture* fails when responsibilities blur → the prefix is the contract.
950-- *Modeled* fails when types and tests scatter → siblings are mandatory.
951-- *Atomic* fails when files swell → the ~700-line split keeps atoms small.
952-
953-Pick one and you'll claw back some clarity. Pick all four and the codebase becomes the kind an agent can be left alone with — there is exactly one right place for any change, and a one-line shell command that proves the layer rule.
954-
955-The blog post [*Red, tokens, atoms*](/blog/three-constraints-agentic-coding) argues SAMA also compounds with TDD and Claude Code's token-saving discipline; the four properties on this page are the *Atomic* / *Modeled* / *Architecture* / *Sorted* halves of that story.
956-
957-[← back to tdd.md](/) · [the blog](/blog) · [the guides](/guides)
958-`;
959- const html = await renderDocsPage({
960- title: "SAMA — sorted, architecture, modeled, atomic — tdd.md",
961- description: "SAMA is a four-property file-naming and module convention for codebases that AI agents work in: sorted by layer prefix, architecture as a contract, models with siblings, atomic files. One page per discipline.",
962- bodyMarkdown: body,
963- ogPath: "https://tdd.md/sama",
964- active: "sama",
965- pathForDocs: "/sama",
966- editPathOverride: null,
967- });
968- return htmlResponse(html);
969- },
970-
971- "/sama/:slug": async (req) => {
972- const slug = req.params.slug;
973- const entry = ALL_SAMA.find((d) => d.slug === slug);
974- if (!entry) {
975- const html = await renderNotFound(`/sama/${slug}`);
976- return htmlResponse(html, 404);
977- }
978- const file = Bun.file(`./content/sama/${slug}.md`);
979- if (!(await file.exists())) {
980- const html = await renderNotFound(`/sama/${slug}`);
981- return htmlResponse(html, 404);
982- }
983- const md = await file.text();
984- const html = await renderDocsPage({
985- title: `SAMA · ${entry.letter} — ${entry.title} — tdd.md`,
986- description: entry.description,
987- bodyMarkdown: md,
988- ogPath: `https://tdd.md/sama/${slug}`,
989- active: "sama",
990- pathForDocs: `/sama/${slug}`,
991- });
992- return htmlResponse(html);
993- },
498+ "/sama/:slug": samaSlugHandler,
994499
995500 "/games/:kata": async (req) => {
996501 const res = await renderKata(req.params.kata);
added src/c21_handlers_reports.ts +190 −0
@@ -0,0 +1,190 @@
1+// c21 — handlers: the /reports cluster. Demo mockup pages plus the
2+// live readout assembled from the deploy-time commit + test bundles.
3+// Extracted from c21_app.ts per the SAMA Atomic rule.
4+
5+import {
6+ renderPage,
7+ renderNotFound,
8+ htmlResponse,
9+} from "./c51_render_layout.ts";
10+import {
11+ reportsLandingMd,
12+ execSummaryMd,
13+ agentDrilldownMd,
14+ testsOverviewMd,
15+} from "./c51_render_reports.ts";
16+import {
17+ DEMO_REPORTS,
18+ DEMO_PERIOD,
19+ DEMO_ORG,
20+ DEMO_REPOS,
21+ DEMO_SNAPSHOTS,
22+ DEMO_STABILITY,
23+} from "./c31_reports_demo.ts";
24+import { buildLiveReports } from "./c32_real_reports.ts";
25+import { buildLiveTestData } from "./c32_real_tests.ts";
26+import {
27+ LIVE_REPO_OWNER,
28+ LIVE_REPO_NAME,
29+ LIVE_FETCH_COUNT,
30+} from "./c31_site_config.ts";
31+
32+// -------- shared banners + context builders --------
33+
34+const DEMO_BANNER_HTML = `<div class="report-mockup-banner">demo data — design preview with synthetic numbers. Want the real readout? <a href="/reports/live">/reports/live</a> renders the same shape from live tdd.md commits. <a href="/blog/tweag-handbook-tdd">why tdd.md needs this</a></div>`;
35+
36+const LIVE_BANNER_HTML = `<div class="report-mockup-banner">live data — sourced from <a href="https://github.com/${LIVE_REPO_OWNER}/${LIVE_REPO_NAME}">${LIVE_REPO_OWNER}/${LIVE_REPO_NAME}</a> via the public commits API (5-min cache). Agent attribution comes from <code>Co-Authored-By:</code> footers; commits without one are excluded. Phase coverage measures % of commits tagged <code>red:/green:/refactor:</code>.</div>`;
37+
38+const demoContext = () => ({
39+ reports: DEMO_REPORTS,
40+ period: DEMO_PERIOD,
41+ scopeLabel: `${DEMO_REPOS} repos · ${DEMO_ORG}`,
42+ bannerHtml: DEMO_BANNER_HTML,
43+ narrative: {
44+ changedHeading: "what changed this quarter",
45+ changedBody:
46+ "Cursor's score dropped 15 points after agent-mode became default in March; test-deletion incidents climbed from 2% to 14% of refactor commits, concentrated in the `api-gateway` repo. Claude Code's score rose after a phase-tagged commit prefix was added to CLAUDE.md at the end of January. Aider stays steadily high — auto-commit-per-edit prevents most cross-phase cheating on its own.",
47+ doingHeading: "what we're doing",
48+ doingBody:
49+ "- **Cursor in `api-gateway`**: agent-mode disabled for refactor prompts, CONVENTIONS rule \"never delete a test in a refactor commit\" pinned ([details →](/reports/demo/agents/cursor)).\n- **Roll out Claude Code**: copy the CLAUDE.md template that worked in `billing-service` to the other three repos.\n- **Next reading**: 2026-04-30, mid-Q2, to check whether the Cursor fix holds.",
50+ },
51+ footerLinks:
52+ "[per-agent drill-down: Claude Code](/reports/demo/agents/claude-code) · [Cursor](/reports/demo/agents/cursor) · [Aider](/reports/demo/agents/aider) · [tests overview](/reports/demo/tests) · [back to /reports](/reports)",
53+});
54+
55+const liveContext = async () => {
56+ const live = await buildLiveReports(LIVE_REPO_OWNER, LIVE_REPO_NAME, LIVE_FETCH_COUNT);
57+ const period = live.earliest && live.latest
58+ ? `${live.earliest.slice(0, 10)} → ${live.latest.slice(0, 10)}`
59+ : "no commits fetched";
60+ const drillLinks = live.reports
61+ .map((r) => `[${r.name}](/reports/live/agents/${r.slug})`)
62+ .join(" · ");
63+ return {
64+ reports: live.reports,
65+ period,
66+ scopeLabel: `${LIVE_REPO_OWNER}/${LIVE_REPO_NAME} · ${live.totalCommits} commits sampled${live.unknownCount > 0 ? ` (${live.unknownCount} unattributed, excluded)` : ""}`,
67+ bannerHtml: LIVE_BANNER_HTML,
68+ footerLinks: `${drillLinks ? drillLinks + " · " : ""}[tests overview](/reports/live/tests) · [demo preview](/reports/demo) · [back to /reports](/reports)`,
69+ };
70+};
71+
72+// -------- /reports landing --------
73+
74+export const reportsLandingHandler = async (): Promise<Response> => {
75+ const html = await renderPage({
76+ title: "Reports — tdd.md",
77+ description: "Per-agent TDD-discipline reporting over real project repos: trend, failure-mode breakdown, and an exec summary fit for a quarterly readout.",
78+ bodyMarkdown: reportsLandingMd(),
79+ ogPath: "https://tdd.md/reports",
80+ noindex: true,
81+ });
82+ return htmlResponse(html);
83+};
84+
85+// -------- /reports/demo --------
86+
87+export const reportsDemoHandler = async (): Promise<Response> => {
88+ const ctx = demoContext();
89+ const html = await renderPage({
90+ title: "TDD-discipline report · Q1 2026 (demo) — tdd.md",
91+ description: "Mockup of the management-level TDD-discipline report — single page, three agents, with trend and narrative.",
92+ bodyMarkdown: execSummaryMd(ctx),
93+ ogPath: "https://tdd.md/reports/demo",
94+ noindex: true,
95+ });
96+ return htmlResponse(html);
97+};
98+
99+export const reportsDemoTestsHandler = async (): Promise<Response> => {
100+ const html = await renderPage({
101+ title: "Tests overview (demo) — tdd.md",
102+ description: "Mockup of the per-test overview: current pass/fail snapshot per repo plus test stability over the quarter.",
103+ bodyMarkdown: testsOverviewMd({
104+ period: DEMO_PERIOD,
105+ bannerHtml: DEMO_BANNER_HTML,
106+ snapshots: DEMO_SNAPSHOTS,
107+ stability: DEMO_STABILITY,
108+ }),
109+ ogPath: "https://tdd.md/reports/demo/tests",
110+ noindex: true,
111+ });
112+ return htmlResponse(html);
113+};
114+
115+export const reportsDemoAgentHandler = async (req: { params: { slug: string } }): Promise<Response> => {
116+ const slug = req.params.slug as (typeof DEMO_REPORTS)[number]["slug"];
117+ const ctx = demoContext();
118+ const md = agentDrilldownMd(slug, ctx);
119+ if (!md) {
120+ const html = await renderNotFound(`/reports/demo/agents/${slug}`);
121+ return htmlResponse(html, 404);
122+ }
123+ const entry = DEMO_REPORTS.find((r) => r.slug === slug)!;
124+ const html = await renderPage({
125+ title: `${entry.name} drill-down (demo) — tdd.md`,
126+ description: `Per-agent drill-down mockup for ${entry.name}: trend, failure-mode breakdown, recent flagged commits with coaching links.`,
127+ bodyMarkdown: md,
128+ ogPath: `https://tdd.md/reports/demo/agents/${slug}`,
129+ noindex: true,
130+ });
131+ return htmlResponse(html);
132+};
133+
134+// -------- /reports/live --------
135+
136+export const reportsLiveHandler = async (): Promise<Response> => {
137+ const ctx = await liveContext();
138+ const html = await renderPage({
139+ title: "TDD-discipline report · live — tdd.md",
140+ description: `Live discipline report built from the real commit history of syntaxai/tdd.md (last ${LIVE_FETCH_COUNT} commits, 5-min cache).`,
141+ bodyMarkdown: execSummaryMd(ctx),
142+ ogPath: "https://tdd.md/reports/live",
143+ noindex: true,
144+ });
145+ return htmlResponse(html);
146+};
147+
148+export const reportsLiveTestsHandler = async (): Promise<Response> => {
149+ const data = await buildLiveTestData(LIVE_REPO_OWNER, LIVE_REPO_NAME);
150+ const ranOn = data.ranAt ? new Date(data.ranAt).toISOString().slice(0, 10) : null;
151+ const period = data.runsCount === 0
152+ ? "no runs in bundle"
153+ : `last run ${ranOn} · ${data.runsCount} run${data.runsCount === 1 ? "" : "s"} cumulative`;
154+ const unavailableNote = data.runsCount === 0
155+ ? "No test runs bundled yet. The next deploy will run `bun test --reporter=junit` on the current HEAD and publish the result here. Stability (flaky %, deletion) builds up as more runs land in the bundle — the demo at [/reports/demo/tests](/reports/demo/tests) shows where this is heading."
156+ : undefined;
157+ const html = await renderPage({
158+ title: "Tests overview · live — tdd.md",
159+ description: `Live test snapshot of ${LIVE_REPO_OWNER}/${LIVE_REPO_NAME} — ${data.runsCount} run${data.runsCount === 1 ? "" : "s"} bundled.`,
160+ bodyMarkdown: testsOverviewMd({
161+ period,
162+ bannerHtml: LIVE_BANNER_HTML,
163+ snapshots: data.snapshots,
164+ stability: data.stability,
165+ unavailableNote,
166+ placeholderTests: data.placeholderTests,
167+ }),
168+ ogPath: "https://tdd.md/reports/live/tests",
169+ });
170+ return htmlResponse(html);
171+};
172+
173+export const reportsLiveAgentHandler = async (req: { params: { slug: string } }): Promise<Response> => {
174+ const ctx = await liveContext();
175+ const slug = req.params.slug as (typeof DEMO_REPORTS)[number]["slug"];
176+ const md = agentDrilldownMd(slug, ctx);
177+ if (!md) {
178+ const html = await renderNotFound(`/reports/live/agents/${slug}`);
179+ return htmlResponse(html, 404);
180+ }
181+ const entry = ctx.reports.find((r) => r.slug === slug)!;
182+ const html = await renderPage({
183+ title: `${entry.name} drill-down · live — tdd.md`,
184+ description: `Live drill-down for ${entry.name} on syntaxai/tdd.md — trend, failure-mode breakdown, recent commits.`,
185+ bodyMarkdown: md,
186+ ogPath: `https://tdd.md/reports/live/agents/${slug}`,
187+ noindex: true,
188+ });
189+ return htmlResponse(html);
190+};
added src/c21_handlers_sama.ts +390 −0
@@ -0,0 +1,390 @@
1+// c21 — handlers: the /sama cluster. All routes that live under
2+// /sama/* plus the SKILL raw download and the bundled CLI download.
3+// Extracted from c21_app.ts per the SAMA Atomic rule (the dispatcher
4+// passed the 700-line split threshold).
5+//
6+// Each export is a handler function the dispatcher in c21_app.ts
7+// references inline so Bun.serve still sees literal route keys for
8+// path-parameter type inference.
9+
10+import {
11+ renderNotFound,
12+ htmlResponse,
13+ escape,
14+} from "./c51_render_layout.ts";
15+import { renderDocsPage } from "./c51_render_docs_layout.ts";
16+import { ALL_SAMA } from "./c31_sama.ts";
17+import {
18+ fetchRepoTree,
19+ fetchRepoRawFile,
20+} from "./c14_github.ts";
21+import { verifySama, type SamaReport } from "./c32_sama_verify.ts";
22+import { LIVE_REPO_OWNER, LIVE_REPO_NAME } from "./c31_site_config.ts";
23+
24+// -------- /skills/sama.md (raw download) --------
25+
26+export const skillsSamaMdHandler = async (): Promise<Response> => {
27+ const md = await Bun.file("./content/sama/skill.md").text();
28+ return new Response(md, {
29+ headers: {
30+ "Content-Type": "text/markdown; charset=utf-8",
31+ "Cache-Control": "public, max-age=300",
32+ },
33+ });
34+};
35+
36+// -------- /sama/skill (HTML viewer of the SKILL.md) --------
37+
38+export const samaSkillHandler = async (): Promise<Response> => {
39+ const raw = await Bun.file("./content/sama/skill.md").text();
40+ // Strip the YAML frontmatter for the HTML render — the .md raw
41+ // download keeps it (that's the agent-installable format).
42+ const stripped = raw.replace(/^---\n[\s\S]*?\n---\n+/, "");
43+ const installNote = `> **Drop into your agent.** Save the raw markdown to your skills directory:
44+>
45+> \`\`\`bash
46+> mkdir -p ~/.claude/skills
47+> curl -fsSL https://tdd.md/skills/sama.md -o ~/.claude/skills/sama.md
48+> \`\`\`
49+>
50+> The frontmatter at the top of the file (\`name\`, \`description\`) is what your agent's loader keys off — don't edit it. [View raw markdown →](/skills/sama.md)
51+`;
52+ const body = `${installNote}\n\n${stripped}\n\n---\n\n[← /sama](/sama) · [the four disciplines](/sama) · [back to tdd.md](/)\n`;
53+ const html = await renderDocsPage({
54+ title: "SAMA skill — drop into your agent — tdd.md",
55+ description: "An obra/superpowers-style SKILL.md for the SAMA file-naming convention. Save it to ~/.claude/skills/sama.md and your agent will load the layer-prefix discipline on demand.",
56+ bodyMarkdown: body,
57+ ogPath: "https://tdd.md/sama/skill",
58+ active: "sama",
59+ pathForDocs: "/sama/skill",
60+ });
61+ return htmlResponse(html);
62+};
63+
64+// -------- /sama/verify (form + report + dogfood short-circuit) --------
65+
66+const VERIFY_FORM_MD = `# SAMA verify
67+
68+> Paste a public GitHub repo. tdd.md will run the four [SAMA disciplines](/sama) against the default branch — *Sorted* (lower never imports higher), *Architecture* (known layer prefixes), *Modeled* (sibling tests, types in c31_*), *Atomic* (~700-line split + placeholder-test detection) — and return a report. No clone, no token; just one tree-listing API call plus raw-content reads. Cached for an hour per repo.
69+
70+<form method="get" action="/sama/verify" class="sama-verify-form">
71+ <label>
72+ public GitHub repo:
73+ <input type="text" name="repo" placeholder="owner/name" required pattern="[^/\\s]+/[^/\\s]+" />
74+ </label>
75+ <button type="submit">verify</button>
76+</form>
77+
78+Try it on this site: [\`syntaxai/tdd.md\`](/sama/verify?repo=syntaxai/tdd.md) · or any public repo of your own.
79+
80+Limits: anonymous GitHub API quota is 60 requests/hour per IP. Each verify uses one tree-listing call; the rest of the work goes through raw.githubusercontent.com (uncapped). If the verifier returns "rate limit", come back later or use a token-authenticated proxy.
81+
82+[← /sama](/sama)
83+`;
84+
85+const verifyLocalDogfood = async (owner: string, name: string): Promise<SamaReport> => {
86+ const { readdirSync, readFileSync } = await import("node:fs");
87+ const srcDir = "./src";
88+ const tsFiles = readdirSync(srcDir, { withFileTypes: true })
89+ .filter((e) => e.isFile() && e.name.endsWith(".ts"))
90+ .map((e) => e.name)
91+ .sort();
92+ const contents = new Map<string, string>();
93+ for (const f of tsFiles) {
94+ if (/^c\d{2}_/.test(f)) {
95+ contents.set(f, readFileSync(`${srcDir}/${f}`, "utf8"));
96+ }
97+ }
98+ return verifySama({
99+ repoOwner: owner,
100+ repoName: name,
101+ defaultBranch: "main",
102+ srcPaths: tsFiles,
103+ contents,
104+ });
105+};
106+
107+const verifyRemoteRepo = async (owner: string, name: string): Promise<SamaReport> => {
108+ const tree = await fetchRepoTree(owner, name);
109+ const srcEntries = tree.entries
110+ .filter((e) => e.type === "blob" && e.path.startsWith("src/") && e.path.endsWith(".ts"))
111+ .slice(0, 200);
112+ const srcPaths = srcEntries.map((e) => e.path.slice("src/".length));
113+ const samaPaths = srcPaths.filter((p) => /^c\d{2}_/.test(p));
114+ const contents = new Map<string, string>();
115+ const fetches = await Promise.all(
116+ samaPaths.map(async (p) => [p, await fetchRepoRawFile(owner, name, tree.defaultBranch, `src/${p}`)] as const),
117+ );
118+ for (const [p, c] of fetches) {
119+ if (c !== null) contents.set(p, c);
120+ }
121+ return verifySama({
122+ repoOwner: owner,
123+ repoName: name,
124+ defaultBranch: tree.defaultBranch,
125+ srcPaths,
126+ contents,
127+ });
128+};
129+
130+const renderVerifyReport = async (report: SamaReport): Promise<string> => {
131+ const summary = report.overallPassed
132+ ? `> ✓ All four checks passed for [\`${report.repoSlug}\`](https://github.com/${report.repoSlug}) on \`${report.defaultBranch}\` (${report.samaFiles} SAMA files / ${report.testFiles} tests / ${report.totalSrcFiles} total in src/).`
133+ : `> ⚠ ${report.checks.filter((c) => !c.passed).length} of 4 checks failed for [\`${report.repoSlug}\`](https://github.com/${report.repoSlug}) on \`${report.defaultBranch}\`.`;
134+ const checkBlocks = report.checks
135+ .map((c) => {
136+ const status = c.passed ? "✓ pass" : `✗ ${c.violations.length} violation${c.violations.length === 1 ? "" : "s"}`;
137+ const violationsBlock = c.violations.length === 0
138+ ? ""
139+ : `\n\n${c.violations.slice(0, 20).map((v) => `- \`${escape(v.file)}\` — ${escape(v.detail)}`).join("\n")}${c.violations.length > 20 ? `\n- _...and ${c.violations.length - 20} more_` : ""}`;
140+ const noteBlock = c.note ? `\n\n_${escape(c.note)}_` : "";
141+ return `### ${c.letter} — ${c.property} · ${status}\n\nExamined ${c.examined} file${c.examined === 1 ? "" : "s"}.${violationsBlock}${noteBlock}`;
142+ })
143+ .join("\n\n");
144+ const reportMd = `# SAMA verify · \`${report.repoSlug}\`
145+
146+${summary}
147+
148+${checkBlocks}
149+
150+---
151+
152+[← verify another repo](/sama/verify) · [the four SAMA disciplines →](/sama) · [SAMA skill for your agent →](/sama/skill)
153+`;
154+ return renderDocsPage({
155+ title: `SAMA verify · ${report.repoSlug} — tdd.md`,
156+ description: `SAMA verification for ${report.repoSlug}: ${report.overallPassed ? "all four checks passed" : `${report.checks.filter((c) => !c.passed).length}/4 checks failed`}.`,
157+ bodyMarkdown: reportMd,
158+ ogPath: `https://tdd.md/sama/verify?repo=${report.repoSlug}`,
159+ active: "sama",
160+ pathForDocs: "/sama/verify",
161+ editPathOverride: null,
162+ });
163+};
164+
165+export const samaVerifyHandler = async (req: { url: string }): Promise<Response> => {
166+ const url = new URL(req.url);
167+ const repoArg = (url.searchParams.get("repo") ?? "").trim();
168+
169+ if (!repoArg) {
170+ const html = await renderDocsPage({
171+ title: "SAMA verify — tdd.md",
172+ description: "Paste a public GitHub repo, get the four SAMA disciplines verified mechanically: sorted (lower never imports higher), architecture (known layer prefixes), modeled (sibling tests), atomic (700-line + placeholder-test detection).",
173+ bodyMarkdown: VERIFY_FORM_MD,
174+ ogPath: "https://tdd.md/sama/verify",
175+ active: "sama",
176+ pathForDocs: "/sama/verify",
177+ });
178+ return htmlResponse(html);
179+ }
180+
181+ const m = /^([^\/\s]+)\/([^\/\s]+)$/.exec(repoArg);
182+ if (!m) {
183+ const html = await renderDocsPage({
184+ title: "SAMA verify · bad input — tdd.md",
185+ description: "SAMA verify expects an owner/name repo identifier.",
186+ bodyMarkdown: `# SAMA verify\n\n> Couldn't parse \`${repoArg}\`. Use the form: \`owner/name\`.\n\n[← back](/sama/verify)\n`,
187+ pathForDocs: "/sama/verify",
188+ editPathOverride: null,
189+ ogPath: "https://tdd.md/sama/verify",
190+ active: "sama",
191+ noindex: true,
192+ });
193+ return htmlResponse(html, 400);
194+ }
195+
196+ const [, owner, name] = m;
197+ let report: SamaReport;
198+ try {
199+ // Dogfood short-circuit: tdd.md is a private repo, so the GitHub
200+ // API can't see it. When asked to verify ourselves, read the
201+ // source from the bundled `./src/` directory inside the container.
202+ const isSelf = owner === LIVE_REPO_OWNER && name === LIVE_REPO_NAME;
203+ report = isSelf ? await verifyLocalDogfood(owner!, name!) : await verifyRemoteRepo(owner!, name!);
204+ } catch (e) {
205+ const msg = e instanceof Error ? e.message : String(e);
206+ const html = await renderDocsPage({
207+ title: `SAMA verify · ${owner}/${name} · error — tdd.md`,
208+ description: `SAMA verify could not inspect ${owner}/${name}.`,
209+ bodyMarkdown: `# SAMA verify · \`${owner}/${name}\`\n\n> Couldn't fetch the repo: ${escape(msg)}\n\nMost common causes: the repo is private, the name is wrong, or you've hit GitHub's anonymous rate limit (60/hour). [← try another repo](/sama/verify)\n`,
210+ ogPath: `https://tdd.md/sama/verify?repo=${owner}/${name}`,
211+ active: "sama",
212+ noindex: true,
213+ pathForDocs: "/sama/verify",
214+ editPathOverride: null,
215+ });
216+ return htmlResponse(html, 502);
217+ }
218+
219+ const html = await renderVerifyReport(report);
220+ return htmlResponse(html);
221+};
222+
223+// -------- /sama (landing) --------
224+
225+const SAMA_LANDING_MD = `# SAMA
226+
227+> **Sorted, Architecture, Modeled, Atomic.** Four properties of a codebase that an AI agent can navigate, change, and verify without drift. The acronym is the rule set; each letter has a one-paragraph definition and a verification you can run.
228+
229+This is the file-naming and module-organisation convention this site is built on, shared across two other projects in my workspace. It exists to give an AI agent **one obvious place** for every change — and one mechanical check for every layer rule.
230+
231+## the four disciplines
232+
233+| letter | discipline | one-line rule |
234+|---|---|---|
235+%ROWS%
236+
237+## reading order
238+
239+If you're new to this:
240+1. Start with **[Sorted](/sama/sorted)** — it has the verification grep that everything else is built around.
241+2. Then **[Architecture](/sama/architecture)** — what each layer prefix means.
242+3. Then **[Modeled](/sama/modeled)** — where types and tests live.
243+4. Then **[Atomic](/sama/atomic)** — the split rule that keeps the rest honest as the codebase grows.
244+
245+Each page is short, opinionated, and ends with the common mistakes you'll see if the discipline lapses.
246+
247+## drop into your agent
248+
249+For agents that load skills from \`~/.claude/skills/\` (Claude Code, obra/superpowers, etc.), grab the SKILL.md version:
250+
251+\`\`\`bash
252+mkdir -p ~/.claude/skills
253+curl -fsSL https://tdd.md/skills/sama.md -o ~/.claude/skills/sama.md
254+\`\`\`
255+
256+The skill is the same content as the four pages here, written in obra/superpowers SKILL.md format with frontmatter, an iron-rule statement, and a verification checklist your agent can run before merging. **[Read it formatted →](/sama/skill)** · **[Raw markdown →](/skills/sama.md)**
257+
258+## verify any public repo
259+
260+Want to know whether a repo follows SAMA without reading its source? Paste the \`owner/name\` and tdd.md will run all four checks against the default branch — *Sorted* (the import-direction grep), *Architecture* (known layer prefixes), *Modeled* (sibling tests), *Atomic* (700-line + placeholder-test detection). Pass/fail per discipline, with violation lists. **[verify a repo on the web →](/sama/verify)** · or try it on this site: [\`syntaxai/tdd.md\`](/sama/verify?repo=syntaxai/tdd.md).
261+
262+## the \`sama\` CLI
263+
264+The web verifier is good for ad-hoc checks. For CI and pre-commit, install the standalone CLI — same checks, no network needed for local repos:
265+
266+\`\`\`bash
267+mkdir -p ~/.local/bin
268+curl -fsSL https://tdd.md/tools/sama-cli -o ~/.local/bin/sama
269+chmod +x ~/.local/bin/sama
270+sama --help
271+\`\`\`
272+
273+Two subcommands:
274+
275+\`\`\`bash
276+sama check # verify the current repo's src/
277+sama check --json # JSON output for piping into CI tooling
278+sama verify-repo owner/name # verify a public GitHub repo (no token)
279+\`\`\`
280+
281+Exit codes: \`0\` on pass, \`1\` if any check fails, \`2\` on error. The CLI is a single Bun bundle (~14 KB). [Bun](https://bun.sh) needs to be on \`PATH\`.
282+
283+### pre-commit hook
284+
285+Add to \`.git/hooks/pre-commit\` (or via \`husky\`, \`pre-commit\`, \`lefthook\`):
286+
287+\`\`\`bash
288+#!/usr/bin/env bash
289+# Block commits that violate SAMA layer/atomic/modeled rules.
290+exec sama check
291+\`\`\`
292+
293+### GitHub Action
294+
295+\`\`\`yaml
296+# .github/workflows/sama.yml
297+name: sama
298+on: [push, pull_request]
299+jobs:
300+ verify:
301+ runs-on: ubuntu-latest
302+ steps:
303+ - uses: actions/checkout@v4
304+ - uses: oven-sh/setup-bun@v2
305+ - run: |
306+ curl -fsSL https://tdd.md/tools/sama-cli -o sama
307+ chmod +x sama
308+ ./sama check
309+\`\`\`
310+
311+If the rule lives in a hook or an action that fails the build, the harness can't talk the agent out of it. That is the whole point of the [corpus post](/blog/agentic-coding-corpus-three-patterns) and the next step from the [from-rules-to-checks](/blog/from-rules-to-checks) wrap-up.
312+
313+## the case behind it
314+
315+Two long-form pieces that argue *why* SAMA is shaped this way:
316+
317+- [**The Claude Code harness postmortem read through TDD + SAMA**](/blog/claude-code-harness-postmortem) — ThePaSch's r/ClaudeAI audit (40+ hidden reminders, 5 gag-order sites, 158 prompt versions in 11 days) read against the iron law and the verification grep. *The harness is loud; the diff doesn't have to be.*
318+- [**Three patterns ten threads converge on**](/blog/agentic-coding-corpus-three-patterns) — a six-month corpus of r/ClaudeAI, r/ClaudeCode, r/AgentsOfAI failure-mode threads. Per-pattern mitigation tables map each thread to the SAMA / iron-law rule that catches or prevents it.
319+
320+If you're reading these for the first time, the order to take them is harness postmortem → corpus → back here.
321+
322+## why these four together
323+
324+Each property fixes a different failure mode:
325+
326+- *Sorted* fails when imports go in any direction → grep proves the rule.
327+- *Architecture* fails when responsibilities blur → the prefix is the contract.
328+- *Modeled* fails when types and tests scatter → siblings are mandatory.
329+- *Atomic* fails when files swell → the ~700-line split keeps atoms small.
330+
331+Pick one and you'll claw back some clarity. Pick all four and the codebase becomes the kind an agent can be left alone with — there is exactly one right place for any change, and a one-line shell command that proves the layer rule.
332+
333+The blog post [*Red, tokens, atoms*](/blog/three-constraints-agentic-coding) argues SAMA also compounds with TDD and Claude Code's token-saving discipline; the four properties on this page are the *Atomic* / *Modeled* / *Architecture* / *Sorted* halves of that story.
334+
335+[← back to tdd.md](/) · [the blog](/blog) · [the guides](/guides)
336+`;
337+
338+export const samaLandingHandler = async (): Promise<Response> => {
339+ const rows = ALL_SAMA
340+ .map((d) => `| **[${d.letter} — ${d.title}](/sama/${d.slug})** | ${d.rule} |`)
341+ .join("\n");
342+ const body = SAMA_LANDING_MD.replace("%ROWS%", rows);
343+ const html = await renderDocsPage({
344+ title: "SAMA — sorted, architecture, modeled, atomic — tdd.md",
345+ description: "SAMA is a four-property file-naming and module convention for codebases that AI agents work in: sorted by layer prefix, architecture as a contract, models with siblings, atomic files. One page per discipline.",
346+ bodyMarkdown: body,
347+ ogPath: "https://tdd.md/sama",
348+ active: "sama",
349+ pathForDocs: "/sama",
350+ editPathOverride: null,
351+ });
352+ return htmlResponse(html);
353+};
354+
355+// -------- /sama/:slug (per-discipline content page) --------
356+
357+export const samaSlugHandler = async (req: { params: { slug: string } }): Promise<Response> => {
358+ const slug = req.params.slug;
359+ const entry = ALL_SAMA.find((d) => d.slug === slug);
360+ if (!entry) {
361+ const html = await renderNotFound(`/sama/${slug}`);
362+ return htmlResponse(html, 404);
363+ }
364+ const file = Bun.file(`./content/sama/${slug}.md`);
365+ if (!(await file.exists())) {
366+ const html = await renderNotFound(`/sama/${slug}`);
367+ return htmlResponse(html, 404);
368+ }
369+ const md = await file.text();
370+ const html = await renderDocsPage({
371+ title: `SAMA · ${entry.letter} — ${entry.title} — tdd.md`,
372+ description: entry.description,
373+ bodyMarkdown: md,
374+ ogPath: `https://tdd.md/sama/${slug}`,
375+ active: "sama",
376+ pathForDocs: `/sama/${slug}`,
377+ });
378+ return htmlResponse(html);
379+};
380+
381+// -------- /tools/sama-cli (binary download) --------
382+
383+export const samaCliResponse = (): Response =>
384+ new Response(Bun.file("./public/sama-cli"), {
385+ headers: {
386+ "Content-Type": "text/javascript; charset=utf-8",
387+ "Content-Disposition": 'inline; filename="sama"',
388+ "Cache-Control": "public, max-age=300",
389+ },
390+ });
added src/c31_site_config.ts +10 −0
@@ -0,0 +1,10 @@
1+// c31 — model: site-wide config constants. Pure data, no I/O.
2+// Lives here so handlers across clusters (sama-verify dogfood,
3+// reports/live, sitemap, etc.) reference the same values without
4+// circular imports between c21_handlers_*.
5+
6+export const LIVE_REPO_OWNER = "syntaxai";
7+export const LIVE_REPO_NAME = "tdd.md";
8+// Number of recent commits the live-reports view samples from the
9+// in-container git-history bundle.
10+export const LIVE_FETCH_COUNT = 100;