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]>
4 files changed · +621 −526
src/c21_app.ts
+31
−526
| @@ -6,7 +6,6 @@ import { | ||
| 6 | 6 | renderPage, |
| 7 | 7 | renderNotFound, |
| 8 | 8 | htmlResponse, |
| 9 | - escape, | |
| 10 | 9 | } from "./c51_render_layout.ts"; |
| 11 | 10 | import { renderDocsPage } from "./c51_render_docs_layout.ts"; |
| 12 | 11 | import { |
| @@ -14,37 +13,16 @@ import { | ||
| 14 | 13 | projectRegisterMd, |
| 15 | 14 | projectDetailMd, |
| 16 | 15 | } from "./c51_render_projects.ts"; |
| 17 | -import { | |
| 18 | - reportsLandingMd, | |
| 19 | - execSummaryMd, | |
| 20 | - agentDrilldownMd, | |
| 21 | - testsOverviewMd, | |
| 22 | -} from "./c51_render_reports.ts"; | |
| 23 | 16 | import { |
| 24 | 17 | FORGEJO_URL, |
| 25 | 18 | adminApiHeaders, |
| 26 | 19 | proxyToForgejo, |
| 27 | 20 | } 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"; | |
| 34 | 22 | import { listGames, loadGame } from "./c31_games.ts"; |
| 35 | 23 | import { ALL_POSTS } from "./c31_blog.ts"; |
| 36 | 24 | import { ALL_GUIDES } from "./c31_guides.ts"; |
| 37 | 25 | 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"; | |
| 48 | 26 | import { parseRepoIdentifier } from "./c31_project_config.ts"; |
| 49 | 27 | import { judge } from "./c32_judge.ts"; |
| 50 | 28 | import { |
| @@ -62,58 +40,27 @@ import { renderRepoView } from "./c21_handlers_repo_view.ts"; | ||
| 62 | 40 | import { renderAgentsIndex, renderAgentDetail } from "./c21_handlers_agents.ts"; |
| 63 | 41 | import { renderLeaderboard } from "./c21_handlers_leaderboard.ts"; |
| 64 | 42 | 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"; | |
| 65 | 60 | |
| 66 | 61 | const HOME_MD = "./content/home.md"; |
| 67 | 62 | const GAME_DIR = "./content/games"; |
| 68 | 63 | |
| 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 | - | |
| 117 | 64 | const HOME_DESCRIPTION = |
| 118 | 65 | "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."; |
| 119 | 66 | |
| @@ -479,119 +426,13 @@ ${rows} | ||
| 479 | 426 | return htmlResponse(html); |
| 480 | 427 | }, |
| 481 | 428 | |
| 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, | |
| 595 | 436 | |
| 596 | 437 | "/guides": async () => { |
| 597 | 438 | const rows = ALL_GUIDES |
| @@ -645,352 +486,16 @@ ${rows} | ||
| 645 | 486 | return htmlResponse(html); |
| 646 | 487 | }, |
| 647 | 488 | |
| 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(), | |
| 885 | 491 | |
| 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, | |
| 887 | 493 | |
| 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, | |
| 894 | 495 | |
| 895 | -Two subcommands: | |
| 496 | + "/sama": samaLandingHandler, | |
| 896 | 497 | |
| 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, | |
| 994 | 499 | |
| 995 | 500 | "/games/:kata": async (req) => { |
| 996 | 501 | const res = await renderKata(req.params.kata); |
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 | +}; | |
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 | + }); | |
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; | |