8e72c3b810ef81c69ce002c701ca6c4c02a7ee44 diff --git a/src/c21_app.ts b/src/c21_app.ts index 00a236b64ac5ef3c51569a4ca3febf0077138674..1edd511e9abd582c3fa549c2fc0ef7509939d0a9 100644 --- a/src/c21_app.ts +++ b/src/c21_app.ts @@ -6,7 +6,6 @@ import { renderPage, renderNotFound, htmlResponse, - escape, } from "./c51_render_layout.ts"; import { renderDocsPage } from "./c51_render_docs_layout.ts"; import { @@ -14,37 +13,16 @@ import { projectRegisterMd, projectDetailMd, } from "./c51_render_projects.ts"; -import { - reportsLandingMd, - execSummaryMd, - agentDrilldownMd, - testsOverviewMd, -} from "./c51_render_reports.ts"; import { FORGEJO_URL, adminApiHeaders, proxyToForgejo, } from "./c14_forgejo.ts"; -import { - fetchProjectConfig, - fetchRepoTree, - fetchRepoRawFile, -} from "./c14_github.ts"; -import { verifySama, type SamaReport } from "./c32_sama_verify.ts"; +import { fetchProjectConfig } from "./c14_github.ts"; import { listGames, loadGame } from "./c31_games.ts"; import { ALL_POSTS } from "./c31_blog.ts"; import { ALL_GUIDES } from "./c31_guides.ts"; import { ALL_SAMA } from "./c31_sama.ts"; -import { - DEMO_REPORTS, - DEMO_PERIOD, - DEMO_ORG, - DEMO_REPOS, - DEMO_SNAPSHOTS, - DEMO_STABILITY, -} from "./c31_reports_demo.ts"; -import { buildLiveReports } from "./c32_real_reports.ts"; -import { buildLiveTestData } from "./c32_real_tests.ts"; import { parseRepoIdentifier } from "./c31_project_config.ts"; import { judge } from "./c32_judge.ts"; import { @@ -62,58 +40,27 @@ import { renderRepoView } from "./c21_handlers_repo_view.ts"; import { renderAgentsIndex, renderAgentDetail } from "./c21_handlers_agents.ts"; import { renderLeaderboard } from "./c21_handlers_leaderboard.ts"; import { startGithubOauth, handleGithubCallback } from "./c21_handlers_auth.ts"; +import { + reportsLandingHandler, + reportsDemoHandler, + reportsDemoTestsHandler, + reportsDemoAgentHandler, + reportsLiveHandler, + reportsLiveTestsHandler, + reportsLiveAgentHandler, +} from "./c21_handlers_reports.ts"; +import { + skillsSamaMdHandler, + samaCliResponse, + samaSkillHandler, + samaVerifyHandler, + samaLandingHandler, + samaSlugHandler, +} from "./c21_handlers_sama.ts"; const HOME_MD = "./content/home.md"; const GAME_DIR = "./content/games"; -// --------------------------------------------------------------------- -// Reports-context builders. The c51 builders take a ReportsContext — -// these tiny helpers assemble it for the synthetic /reports/demo and -// the live /reports/live (real data fetched from syntaxai/tdd.md). -// --------------------------------------------------------------------- - -const LIVE_REPO_OWNER = "syntaxai"; -const LIVE_REPO_NAME = "tdd.md"; -const LIVE_FETCH_COUNT = 100; - -const DEMO_BANNER_HTML = `
demo data — design preview with synthetic numbers. Want the real readout? /reports/live renders the same shape from live tdd.md commits. why tdd.md needs this
`; - -const LIVE_BANNER_HTML = `
live data — sourced from ${LIVE_REPO_OWNER}/${LIVE_REPO_NAME} via the public commits API (5-min cache). Agent attribution comes from Co-Authored-By: footers; commits without one are excluded. Phase coverage measures % of commits tagged red:/green:/refactor:.
`; - -const demoContext = () => ({ - reports: DEMO_REPORTS, - period: DEMO_PERIOD, - scopeLabel: `${DEMO_REPOS} repos · ${DEMO_ORG}`, - bannerHtml: DEMO_BANNER_HTML, - narrative: { - changedHeading: "what changed this quarter", - changedBody: - "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.", - doingHeading: "what we're doing", - doingBody: - "- **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.", - }, - footerLinks: - "[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)", -}); - -const liveContext = async () => { - const live = await buildLiveReports(LIVE_REPO_OWNER, LIVE_REPO_NAME, LIVE_FETCH_COUNT); - const period = live.earliest && live.latest - ? `${live.earliest.slice(0, 10)} → ${live.latest.slice(0, 10)}` - : "no commits fetched"; - const drillLinks = live.reports - .map((r) => `[${r.name}](/reports/live/agents/${r.slug})`) - .join(" · "); - return { - reports: live.reports, - period, - scopeLabel: `${LIVE_REPO_OWNER}/${LIVE_REPO_NAME} · ${live.totalCommits} commits sampled${live.unknownCount > 0 ? ` (${live.unknownCount} unattributed, excluded)` : ""}`, - bannerHtml: LIVE_BANNER_HTML, - footerLinks: `${drillLinks ? drillLinks + " · " : ""}[tests overview](/reports/live/tests) · [demo preview](/reports/demo) · [back to /reports](/reports)`, - }; -}; - const HOME_DESCRIPTION = "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."; @@ -479,119 +426,13 @@ ${rows} return htmlResponse(html); }, - "/reports": async () => { - const html = await renderPage({ - title: "Reports — tdd.md", - description: "Per-agent TDD-discipline reporting over real project repos: trend, failure-mode breakdown, and an exec summary fit for a quarterly readout.", - bodyMarkdown: reportsLandingMd(), - ogPath: "https://tdd.md/reports", - noindex: true, - }); - return htmlResponse(html); - }, - - "/reports/demo": async () => { - const ctx = demoContext(); - const html = await renderPage({ - title: "TDD-discipline report · Q1 2026 (demo) — tdd.md", - description: "Mockup of the management-level TDD-discipline report — single page, three agents, with trend and narrative.", - bodyMarkdown: execSummaryMd(ctx), - ogPath: "https://tdd.md/reports/demo", - noindex: true, - }); - return htmlResponse(html); - }, - - "/reports/demo/tests": async () => { - const html = await renderPage({ - title: "Tests overview (demo) — tdd.md", - description: "Mockup of the per-test overview: current pass/fail snapshot per repo plus test stability over the quarter.", - bodyMarkdown: testsOverviewMd({ - period: DEMO_PERIOD, - bannerHtml: DEMO_BANNER_HTML, - snapshots: DEMO_SNAPSHOTS, - stability: DEMO_STABILITY, - }), - ogPath: "https://tdd.md/reports/demo/tests", - noindex: true, - }); - return htmlResponse(html); - }, - - "/reports/demo/agents/:slug": async (req) => { - const slug = req.params.slug as (typeof DEMO_REPORTS)[number]["slug"]; - const ctx = demoContext(); - const md = agentDrilldownMd(slug, ctx); - if (!md) { - const html = await renderNotFound(`/reports/demo/agents/${slug}`); - return htmlResponse(html, 404); - } - const entry = DEMO_REPORTS.find((r) => r.slug === slug)!; - const html = await renderPage({ - title: `${entry.name} drill-down (demo) — tdd.md`, - description: `Per-agent drill-down mockup for ${entry.name}: trend, failure-mode breakdown, recent flagged commits with coaching links.`, - bodyMarkdown: md, - ogPath: `https://tdd.md/reports/demo/agents/${slug}`, - noindex: true, - }); - return htmlResponse(html); - }, - - "/reports/live": async () => { - const ctx = await liveContext(); - const html = await renderPage({ - title: "TDD-discipline report · live — tdd.md", - description: `Live discipline report built from the real commit history of syntaxai/tdd.md (last ${LIVE_FETCH_COUNT} commits, 5-min cache).`, - bodyMarkdown: execSummaryMd(ctx), - ogPath: "https://tdd.md/reports/live", - noindex: true, - }); - return htmlResponse(html); - }, - - "/reports/live/tests": async () => { - const data = await buildLiveTestData(LIVE_REPO_OWNER, LIVE_REPO_NAME); - const ranOn = data.ranAt ? new Date(data.ranAt).toISOString().slice(0, 10) : null; - const period = data.runsCount === 0 - ? "no runs in bundle" - : `last run ${ranOn} · ${data.runsCount} run${data.runsCount === 1 ? "" : "s"} cumulative`; - const unavailableNote = data.runsCount === 0 - ? "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." - : undefined; - const html = await renderPage({ - title: "Tests overview · live — tdd.md", - description: `Live test snapshot of ${LIVE_REPO_OWNER}/${LIVE_REPO_NAME} — ${data.runsCount} run${data.runsCount === 1 ? "" : "s"} bundled.`, - bodyMarkdown: testsOverviewMd({ - period, - bannerHtml: LIVE_BANNER_HTML, - snapshots: data.snapshots, - stability: data.stability, - unavailableNote, - placeholderTests: data.placeholderTests, - }), - ogPath: "https://tdd.md/reports/live/tests", - }); - return htmlResponse(html); - }, - - "/reports/live/agents/:slug": async (req) => { - const ctx = await liveContext(); - const slug = req.params.slug as (typeof DEMO_REPORTS)[number]["slug"]; - const md = agentDrilldownMd(slug, ctx); - if (!md) { - const html = await renderNotFound(`/reports/live/agents/${slug}`); - return htmlResponse(html, 404); - } - const entry = ctx.reports.find((r) => r.slug === slug)!; - const html = await renderPage({ - title: `${entry.name} drill-down · live — tdd.md`, - description: `Live drill-down for ${entry.name} on syntaxai/tdd.md — trend, failure-mode breakdown, recent commits.`, - bodyMarkdown: md, - ogPath: `https://tdd.md/reports/live/agents/${slug}`, - noindex: true, - }); - return htmlResponse(html); - }, + "/reports": reportsLandingHandler, + "/reports/demo": reportsDemoHandler, + "/reports/demo/tests": reportsDemoTestsHandler, + "/reports/demo/agents/:slug": reportsDemoAgentHandler, + "/reports/live": reportsLiveHandler, + "/reports/live/tests": reportsLiveTestsHandler, + "/reports/live/agents/:slug": reportsLiveAgentHandler, "/guides": async () => { const rows = ALL_GUIDES @@ -645,352 +486,16 @@ ${rows} return htmlResponse(html); }, - "/skills/sama.md": async () => { - const md = await Bun.file("./content/sama/skill.md").text(); - return new Response(md, { - headers: { - "Content-Type": "text/markdown; charset=utf-8", - "Cache-Control": "public, max-age=300", - }, - }); - }, - - "/tools/sama-cli": new Response(Bun.file("./public/sama-cli"), { - headers: { - // text/javascript so browsers preview as code; the shebang at - // line 1 makes the file directly executable once chmod +x'd. - "Content-Type": "text/javascript; charset=utf-8", - "Content-Disposition": 'inline; filename="sama"', - "Cache-Control": "public, max-age=300", - }, - }), - - "/sama/skill": async () => { - const raw = await Bun.file("./content/sama/skill.md").text(); - // Strip the YAML frontmatter for the HTML render — the .md raw - // download keeps it (that's the agent-installable format). - const stripped = raw.replace(/^---\n[\s\S]*?\n---\n+/, ""); - const installNote = `> **Drop into your agent.** Save the raw markdown to your skills directory: -> -> \`\`\`bash -> mkdir -p ~/.claude/skills -> curl -fsSL https://tdd.md/skills/sama.md -o ~/.claude/skills/sama.md -> \`\`\` -> -> 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) -`; - const body = `${installNote}\n\n${stripped}\n\n---\n\n[← /sama](/sama) · [the four disciplines](/sama) · [back to tdd.md](/)\n`; - const html = await renderDocsPage({ - title: "SAMA skill — drop into your agent — tdd.md", - 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.", - bodyMarkdown: body, - ogPath: "https://tdd.md/sama/skill", - active: "sama", - pathForDocs: "/sama/skill", - }); - return htmlResponse(html); - }, - - "/sama/verify": async (req) => { - const url = new URL(req.url); - const repoArg = (url.searchParams.get("repo") ?? "").trim(); - const formMd = `# SAMA verify - -> 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. - -
- - -
- -Try it on this site: [\`syntaxai/tdd.md\`](/sama/verify?repo=syntaxai/tdd.md) · or any public repo of your own. - -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. - -[← /sama](/sama) -`; - - if (!repoArg) { - const html = await renderDocsPage({ - title: "SAMA verify — tdd.md", - 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).", - bodyMarkdown: formMd, - ogPath: "https://tdd.md/sama/verify", - active: "sama", - pathForDocs: "/sama/verify", - }); - return htmlResponse(html); - } - - const m = /^([^\/\s]+)\/([^\/\s]+)$/.exec(repoArg); - if (!m) { - const html = await renderDocsPage({ - title: "SAMA verify · bad input — tdd.md", - description: "SAMA verify expects an owner/name repo identifier.", - bodyMarkdown: `# SAMA verify\n\n> Couldn't parse \`${repoArg}\`. Use the form: \`owner/name\`.\n\n[← back](/sama/verify)\n`, - pathForDocs: "/sama/verify", - editPathOverride: null, - ogPath: "https://tdd.md/sama/verify", - active: "sama", - noindex: true, - }); - return htmlResponse(html, 400); - } - - const [, owner, name] = m; - let report: SamaReport; - try { - // Dogfood short-circuit: tdd.md is a private repo, so the GitHub - // API can't see it. When asked to verify ourselves, read the - // source from the bundled `./src/` directory inside the container. - // Same checks, same shape, same code path downstream. - const isSelf = owner === LIVE_REPO_OWNER && name === LIVE_REPO_NAME; - if (isSelf) { - const { readdirSync, readFileSync } = await import("node:fs"); - const srcDir = "./src"; - const tsFiles = readdirSync(srcDir, { withFileTypes: true }) - .filter((e) => e.isFile() && e.name.endsWith(".ts")) - .map((e) => e.name) - .sort(); - const contents = new Map(); - for (const f of tsFiles) { - if (/^c\d{2}_/.test(f)) { - contents.set(f, readFileSync(`${srcDir}/${f}`, "utf8")); - } - } - report = verifySama({ - repoOwner: owner!, - repoName: name!, - defaultBranch: "main", - srcPaths: tsFiles, - contents, - }); - } else { - const tree = await fetchRepoTree(owner!, name!); - const srcEntries = tree.entries - .filter((e) => e.type === "blob" && e.path.startsWith("src/") && e.path.endsWith(".ts")) - .slice(0, 200); - const srcPaths = srcEntries.map((e) => e.path.slice("src/".length)); - const samaPaths = srcPaths.filter((p) => /^c\d{2}_/.test(p)); - const contents = new Map(); - const fetches = await Promise.all( - samaPaths.map(async (p) => [p, await fetchRepoRawFile(owner!, name!, tree.defaultBranch, `src/${p}`)] as const), - ); - for (const [p, c] of fetches) { - if (c !== null) contents.set(p, c); - } - report = verifySama({ - repoOwner: owner!, - repoName: name!, - defaultBranch: tree.defaultBranch, - srcPaths, - contents, - }); - } - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - const html = await renderDocsPage({ - title: `SAMA verify · ${owner}/${name} · error — tdd.md`, - description: `SAMA verify could not inspect ${owner}/${name}.`, - 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`, - ogPath: `https://tdd.md/sama/verify?repo=${owner}/${name}`, - active: "sama", - noindex: true, - pathForDocs: "/sama/verify", - editPathOverride: null, - }); - return htmlResponse(html, 502); - } - - const summary = report.overallPassed - ? `> ✓ 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/).` - : `> ⚠ ${report.checks.filter((c) => !c.passed).length} of 4 checks failed for [\`${report.repoSlug}\`](https://github.com/${report.repoSlug}) on \`${report.defaultBranch}\`.`; - const checkBlocks = report.checks - .map((c) => { - const status = c.passed ? "✓ pass" : `✗ ${c.violations.length} violation${c.violations.length === 1 ? "" : "s"}`; - const violationsBlock = c.violations.length === 0 - ? "" - : `\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_` : ""}`; - const noteBlock = c.note ? `\n\n_${escape(c.note)}_` : ""; - return `### ${c.letter} — ${c.property} · ${status}\n\nExamined ${c.examined} file${c.examined === 1 ? "" : "s"}.${violationsBlock}${noteBlock}`; - }) - .join("\n\n"); - const reportMd = `# SAMA verify · \`${report.repoSlug}\` - -${summary} - -${checkBlocks} - ---- - -[← verify another repo](/sama/verify) · [the four SAMA disciplines →](/sama) · [SAMA skill for your agent →](/sama/skill) -`; - const html = await renderDocsPage({ - title: `SAMA verify · ${report.repoSlug} — tdd.md`, - description: `SAMA verification for ${report.repoSlug}: ${report.overallPassed ? "all four checks passed" : `${report.checks.filter((c) => !c.passed).length}/4 checks failed`}.`, - bodyMarkdown: reportMd, - ogPath: `https://tdd.md/sama/verify?repo=${report.repoSlug}`, - active: "sama", - pathForDocs: "/sama/verify", - editPathOverride: null, - }); - return htmlResponse(html); - }, - - "/sama": async () => { - const rows = ALL_SAMA - .map((d) => `| **[${d.letter} — ${d.title}](/sama/${d.slug})** | ${d.rule} |`) - .join("\n"); - const body = `# SAMA - -> **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. - -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. - -## the four disciplines - -| letter | discipline | one-line rule | -|---|---|---| -${rows} - -## reading order - -If you're new to this: -1. Start with **[Sorted](/sama/sorted)** — it has the verification grep that everything else is built around. -2. Then **[Architecture](/sama/architecture)** — what each layer prefix means. -3. Then **[Modeled](/sama/modeled)** — where types and tests live. -4. Then **[Atomic](/sama/atomic)** — the split rule that keeps the rest honest as the codebase grows. - -Each page is short, opinionated, and ends with the common mistakes you'll see if the discipline lapses. - -## drop into your agent - -For agents that load skills from \`~/.claude/skills/\` (Claude Code, obra/superpowers, etc.), grab the SKILL.md version: - -\`\`\`bash -mkdir -p ~/.claude/skills -curl -fsSL https://tdd.md/skills/sama.md -o ~/.claude/skills/sama.md -\`\`\` - -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)** - -## verify any public repo - -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). - -## the \`sama\` CLI + "/skills/sama.md": skillsSamaMdHandler, + "/tools/sama-cli": samaCliResponse(), -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: + "/sama/skill": samaSkillHandler, -\`\`\`bash -mkdir -p ~/.local/bin -curl -fsSL https://tdd.md/tools/sama-cli -o ~/.local/bin/sama -chmod +x ~/.local/bin/sama -sama --help -\`\`\` + "/sama/verify": samaVerifyHandler, -Two subcommands: + "/sama": samaLandingHandler, -\`\`\`bash -sama check # verify the current repo's src/ -sama check --json # JSON output for piping into CI tooling -sama verify-repo owner/name # verify a public GitHub repo (no token) -\`\`\` - -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\`. - -### pre-commit hook - -Add to \`.git/hooks/pre-commit\` (or via \`husky\`, \`pre-commit\`, \`lefthook\`): - -\`\`\`bash -#!/usr/bin/env bash -# Block commits that violate SAMA layer/atomic/modeled rules. -exec sama check -\`\`\` - -### GitHub Action - -\`\`\`yaml -# .github/workflows/sama.yml -name: sama -on: [push, pull_request] -jobs: - verify: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - - run: | - curl -fsSL https://tdd.md/tools/sama-cli -o sama - chmod +x sama - ./sama check -\`\`\` - -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. - -## the case behind it - -Two long-form pieces that argue *why* SAMA is shaped this way: - -- [**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.* -- [**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. - -If you're reading these for the first time, the order to take them is harness postmortem → corpus → back here. - -## why these four together - -Each property fixes a different failure mode: - -- *Sorted* fails when imports go in any direction → grep proves the rule. -- *Architecture* fails when responsibilities blur → the prefix is the contract. -- *Modeled* fails when types and tests scatter → siblings are mandatory. -- *Atomic* fails when files swell → the ~700-line split keeps atoms small. - -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. - -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. - -[← back to tdd.md](/) · [the blog](/blog) · [the guides](/guides) -`; - const html = await renderDocsPage({ - title: "SAMA — sorted, architecture, modeled, atomic — tdd.md", - 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.", - bodyMarkdown: body, - ogPath: "https://tdd.md/sama", - active: "sama", - pathForDocs: "/sama", - editPathOverride: null, - }); - return htmlResponse(html); - }, - - "/sama/:slug": async (req) => { - const slug = req.params.slug; - const entry = ALL_SAMA.find((d) => d.slug === slug); - if (!entry) { - const html = await renderNotFound(`/sama/${slug}`); - return htmlResponse(html, 404); - } - const file = Bun.file(`./content/sama/${slug}.md`); - if (!(await file.exists())) { - const html = await renderNotFound(`/sama/${slug}`); - return htmlResponse(html, 404); - } - const md = await file.text(); - const html = await renderDocsPage({ - title: `SAMA · ${entry.letter} — ${entry.title} — tdd.md`, - description: entry.description, - bodyMarkdown: md, - ogPath: `https://tdd.md/sama/${slug}`, - active: "sama", - pathForDocs: `/sama/${slug}`, - }); - return htmlResponse(html); - }, + "/sama/:slug": samaSlugHandler, "/games/:kata": async (req) => { const res = await renderKata(req.params.kata); diff --git a/src/c21_handlers_reports.ts b/src/c21_handlers_reports.ts new file mode 100644 index 0000000000000000000000000000000000000000..ea3b65998d679daba7a367e064493bd6b2d02519 --- /dev/null +++ b/src/c21_handlers_reports.ts @@ -0,0 +1,190 @@ +// c21 — handlers: the /reports cluster. Demo mockup pages plus the +// live readout assembled from the deploy-time commit + test bundles. +// Extracted from c21_app.ts per the SAMA Atomic rule. + +import { + renderPage, + renderNotFound, + htmlResponse, +} from "./c51_render_layout.ts"; +import { + reportsLandingMd, + execSummaryMd, + agentDrilldownMd, + testsOverviewMd, +} from "./c51_render_reports.ts"; +import { + DEMO_REPORTS, + DEMO_PERIOD, + DEMO_ORG, + DEMO_REPOS, + DEMO_SNAPSHOTS, + DEMO_STABILITY, +} from "./c31_reports_demo.ts"; +import { buildLiveReports } from "./c32_real_reports.ts"; +import { buildLiveTestData } from "./c32_real_tests.ts"; +import { + LIVE_REPO_OWNER, + LIVE_REPO_NAME, + LIVE_FETCH_COUNT, +} from "./c31_site_config.ts"; + +// -------- shared banners + context builders -------- + +const DEMO_BANNER_HTML = `
demo data — design preview with synthetic numbers. Want the real readout? /reports/live renders the same shape from live tdd.md commits. why tdd.md needs this
`; + +const LIVE_BANNER_HTML = `
live data — sourced from ${LIVE_REPO_OWNER}/${LIVE_REPO_NAME} via the public commits API (5-min cache). Agent attribution comes from Co-Authored-By: footers; commits without one are excluded. Phase coverage measures % of commits tagged red:/green:/refactor:.
`; + +const demoContext = () => ({ + reports: DEMO_REPORTS, + period: DEMO_PERIOD, + scopeLabel: `${DEMO_REPOS} repos · ${DEMO_ORG}`, + bannerHtml: DEMO_BANNER_HTML, + narrative: { + changedHeading: "what changed this quarter", + changedBody: + "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.", + doingHeading: "what we're doing", + doingBody: + "- **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.", + }, + footerLinks: + "[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)", +}); + +const liveContext = async () => { + const live = await buildLiveReports(LIVE_REPO_OWNER, LIVE_REPO_NAME, LIVE_FETCH_COUNT); + const period = live.earliest && live.latest + ? `${live.earliest.slice(0, 10)} → ${live.latest.slice(0, 10)}` + : "no commits fetched"; + const drillLinks = live.reports + .map((r) => `[${r.name}](/reports/live/agents/${r.slug})`) + .join(" · "); + return { + reports: live.reports, + period, + scopeLabel: `${LIVE_REPO_OWNER}/${LIVE_REPO_NAME} · ${live.totalCommits} commits sampled${live.unknownCount > 0 ? ` (${live.unknownCount} unattributed, excluded)` : ""}`, + bannerHtml: LIVE_BANNER_HTML, + footerLinks: `${drillLinks ? drillLinks + " · " : ""}[tests overview](/reports/live/tests) · [demo preview](/reports/demo) · [back to /reports](/reports)`, + }; +}; + +// -------- /reports landing -------- + +export const reportsLandingHandler = async (): Promise => { + const html = await renderPage({ + title: "Reports — tdd.md", + description: "Per-agent TDD-discipline reporting over real project repos: trend, failure-mode breakdown, and an exec summary fit for a quarterly readout.", + bodyMarkdown: reportsLandingMd(), + ogPath: "https://tdd.md/reports", + noindex: true, + }); + return htmlResponse(html); +}; + +// -------- /reports/demo -------- + +export const reportsDemoHandler = async (): Promise => { + const ctx = demoContext(); + const html = await renderPage({ + title: "TDD-discipline report · Q1 2026 (demo) — tdd.md", + description: "Mockup of the management-level TDD-discipline report — single page, three agents, with trend and narrative.", + bodyMarkdown: execSummaryMd(ctx), + ogPath: "https://tdd.md/reports/demo", + noindex: true, + }); + return htmlResponse(html); +}; + +export const reportsDemoTestsHandler = async (): Promise => { + const html = await renderPage({ + title: "Tests overview (demo) — tdd.md", + description: "Mockup of the per-test overview: current pass/fail snapshot per repo plus test stability over the quarter.", + bodyMarkdown: testsOverviewMd({ + period: DEMO_PERIOD, + bannerHtml: DEMO_BANNER_HTML, + snapshots: DEMO_SNAPSHOTS, + stability: DEMO_STABILITY, + }), + ogPath: "https://tdd.md/reports/demo/tests", + noindex: true, + }); + return htmlResponse(html); +}; + +export const reportsDemoAgentHandler = async (req: { params: { slug: string } }): Promise => { + const slug = req.params.slug as (typeof DEMO_REPORTS)[number]["slug"]; + const ctx = demoContext(); + const md = agentDrilldownMd(slug, ctx); + if (!md) { + const html = await renderNotFound(`/reports/demo/agents/${slug}`); + return htmlResponse(html, 404); + } + const entry = DEMO_REPORTS.find((r) => r.slug === slug)!; + const html = await renderPage({ + title: `${entry.name} drill-down (demo) — tdd.md`, + description: `Per-agent drill-down mockup for ${entry.name}: trend, failure-mode breakdown, recent flagged commits with coaching links.`, + bodyMarkdown: md, + ogPath: `https://tdd.md/reports/demo/agents/${slug}`, + noindex: true, + }); + return htmlResponse(html); +}; + +// -------- /reports/live -------- + +export const reportsLiveHandler = async (): Promise => { + const ctx = await liveContext(); + const html = await renderPage({ + title: "TDD-discipline report · live — tdd.md", + description: `Live discipline report built from the real commit history of syntaxai/tdd.md (last ${LIVE_FETCH_COUNT} commits, 5-min cache).`, + bodyMarkdown: execSummaryMd(ctx), + ogPath: "https://tdd.md/reports/live", + noindex: true, + }); + return htmlResponse(html); +}; + +export const reportsLiveTestsHandler = async (): Promise => { + const data = await buildLiveTestData(LIVE_REPO_OWNER, LIVE_REPO_NAME); + const ranOn = data.ranAt ? new Date(data.ranAt).toISOString().slice(0, 10) : null; + const period = data.runsCount === 0 + ? "no runs in bundle" + : `last run ${ranOn} · ${data.runsCount} run${data.runsCount === 1 ? "" : "s"} cumulative`; + const unavailableNote = data.runsCount === 0 + ? "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." + : undefined; + const html = await renderPage({ + title: "Tests overview · live — tdd.md", + description: `Live test snapshot of ${LIVE_REPO_OWNER}/${LIVE_REPO_NAME} — ${data.runsCount} run${data.runsCount === 1 ? "" : "s"} bundled.`, + bodyMarkdown: testsOverviewMd({ + period, + bannerHtml: LIVE_BANNER_HTML, + snapshots: data.snapshots, + stability: data.stability, + unavailableNote, + placeholderTests: data.placeholderTests, + }), + ogPath: "https://tdd.md/reports/live/tests", + }); + return htmlResponse(html); +}; + +export const reportsLiveAgentHandler = async (req: { params: { slug: string } }): Promise => { + const ctx = await liveContext(); + const slug = req.params.slug as (typeof DEMO_REPORTS)[number]["slug"]; + const md = agentDrilldownMd(slug, ctx); + if (!md) { + const html = await renderNotFound(`/reports/live/agents/${slug}`); + return htmlResponse(html, 404); + } + const entry = ctx.reports.find((r) => r.slug === slug)!; + const html = await renderPage({ + title: `${entry.name} drill-down · live — tdd.md`, + description: `Live drill-down for ${entry.name} on syntaxai/tdd.md — trend, failure-mode breakdown, recent commits.`, + bodyMarkdown: md, + ogPath: `https://tdd.md/reports/live/agents/${slug}`, + noindex: true, + }); + return htmlResponse(html); +}; diff --git a/src/c21_handlers_sama.ts b/src/c21_handlers_sama.ts new file mode 100644 index 0000000000000000000000000000000000000000..a7af480cf0f4130c57f688eae4b6e9d3e39b1b06 --- /dev/null +++ b/src/c21_handlers_sama.ts @@ -0,0 +1,390 @@ +// c21 — handlers: the /sama cluster. All routes that live under +// /sama/* plus the SKILL raw download and the bundled CLI download. +// Extracted from c21_app.ts per the SAMA Atomic rule (the dispatcher +// passed the 700-line split threshold). +// +// Each export is a handler function the dispatcher in c21_app.ts +// references inline so Bun.serve still sees literal route keys for +// path-parameter type inference. + +import { + renderNotFound, + htmlResponse, + escape, +} from "./c51_render_layout.ts"; +import { renderDocsPage } from "./c51_render_docs_layout.ts"; +import { ALL_SAMA } from "./c31_sama.ts"; +import { + fetchRepoTree, + fetchRepoRawFile, +} from "./c14_github.ts"; +import { verifySama, type SamaReport } from "./c32_sama_verify.ts"; +import { LIVE_REPO_OWNER, LIVE_REPO_NAME } from "./c31_site_config.ts"; + +// -------- /skills/sama.md (raw download) -------- + +export const skillsSamaMdHandler = async (): Promise => { + const md = await Bun.file("./content/sama/skill.md").text(); + return new Response(md, { + headers: { + "Content-Type": "text/markdown; charset=utf-8", + "Cache-Control": "public, max-age=300", + }, + }); +}; + +// -------- /sama/skill (HTML viewer of the SKILL.md) -------- + +export const samaSkillHandler = async (): Promise => { + const raw = await Bun.file("./content/sama/skill.md").text(); + // Strip the YAML frontmatter for the HTML render — the .md raw + // download keeps it (that's the agent-installable format). + const stripped = raw.replace(/^---\n[\s\S]*?\n---\n+/, ""); + const installNote = `> **Drop into your agent.** Save the raw markdown to your skills directory: +> +> \`\`\`bash +> mkdir -p ~/.claude/skills +> curl -fsSL https://tdd.md/skills/sama.md -o ~/.claude/skills/sama.md +> \`\`\` +> +> 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) +`; + const body = `${installNote}\n\n${stripped}\n\n---\n\n[← /sama](/sama) · [the four disciplines](/sama) · [back to tdd.md](/)\n`; + const html = await renderDocsPage({ + title: "SAMA skill — drop into your agent — tdd.md", + 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.", + bodyMarkdown: body, + ogPath: "https://tdd.md/sama/skill", + active: "sama", + pathForDocs: "/sama/skill", + }); + return htmlResponse(html); +}; + +// -------- /sama/verify (form + report + dogfood short-circuit) -------- + +const VERIFY_FORM_MD = `# SAMA verify + +> 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. + +
+ + +
+ +Try it on this site: [\`syntaxai/tdd.md\`](/sama/verify?repo=syntaxai/tdd.md) · or any public repo of your own. + +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. + +[← /sama](/sama) +`; + +const verifyLocalDogfood = async (owner: string, name: string): Promise => { + const { readdirSync, readFileSync } = await import("node:fs"); + const srcDir = "./src"; + const tsFiles = readdirSync(srcDir, { withFileTypes: true }) + .filter((e) => e.isFile() && e.name.endsWith(".ts")) + .map((e) => e.name) + .sort(); + const contents = new Map(); + for (const f of tsFiles) { + if (/^c\d{2}_/.test(f)) { + contents.set(f, readFileSync(`${srcDir}/${f}`, "utf8")); + } + } + return verifySama({ + repoOwner: owner, + repoName: name, + defaultBranch: "main", + srcPaths: tsFiles, + contents, + }); +}; + +const verifyRemoteRepo = async (owner: string, name: string): Promise => { + const tree = await fetchRepoTree(owner, name); + const srcEntries = tree.entries + .filter((e) => e.type === "blob" && e.path.startsWith("src/") && e.path.endsWith(".ts")) + .slice(0, 200); + const srcPaths = srcEntries.map((e) => e.path.slice("src/".length)); + const samaPaths = srcPaths.filter((p) => /^c\d{2}_/.test(p)); + const contents = new Map(); + const fetches = await Promise.all( + samaPaths.map(async (p) => [p, await fetchRepoRawFile(owner, name, tree.defaultBranch, `src/${p}`)] as const), + ); + for (const [p, c] of fetches) { + if (c !== null) contents.set(p, c); + } + return verifySama({ + repoOwner: owner, + repoName: name, + defaultBranch: tree.defaultBranch, + srcPaths, + contents, + }); +}; + +const renderVerifyReport = async (report: SamaReport): Promise => { + const summary = report.overallPassed + ? `> ✓ 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/).` + : `> ⚠ ${report.checks.filter((c) => !c.passed).length} of 4 checks failed for [\`${report.repoSlug}\`](https://github.com/${report.repoSlug}) on \`${report.defaultBranch}\`.`; + const checkBlocks = report.checks + .map((c) => { + const status = c.passed ? "✓ pass" : `✗ ${c.violations.length} violation${c.violations.length === 1 ? "" : "s"}`; + const violationsBlock = c.violations.length === 0 + ? "" + : `\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_` : ""}`; + const noteBlock = c.note ? `\n\n_${escape(c.note)}_` : ""; + return `### ${c.letter} — ${c.property} · ${status}\n\nExamined ${c.examined} file${c.examined === 1 ? "" : "s"}.${violationsBlock}${noteBlock}`; + }) + .join("\n\n"); + const reportMd = `# SAMA verify · \`${report.repoSlug}\` + +${summary} + +${checkBlocks} + +--- + +[← verify another repo](/sama/verify) · [the four SAMA disciplines →](/sama) · [SAMA skill for your agent →](/sama/skill) +`; + return renderDocsPage({ + title: `SAMA verify · ${report.repoSlug} — tdd.md`, + description: `SAMA verification for ${report.repoSlug}: ${report.overallPassed ? "all four checks passed" : `${report.checks.filter((c) => !c.passed).length}/4 checks failed`}.`, + bodyMarkdown: reportMd, + ogPath: `https://tdd.md/sama/verify?repo=${report.repoSlug}`, + active: "sama", + pathForDocs: "/sama/verify", + editPathOverride: null, + }); +}; + +export const samaVerifyHandler = async (req: { url: string }): Promise => { + const url = new URL(req.url); + const repoArg = (url.searchParams.get("repo") ?? "").trim(); + + if (!repoArg) { + const html = await renderDocsPage({ + title: "SAMA verify — tdd.md", + 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).", + bodyMarkdown: VERIFY_FORM_MD, + ogPath: "https://tdd.md/sama/verify", + active: "sama", + pathForDocs: "/sama/verify", + }); + return htmlResponse(html); + } + + const m = /^([^\/\s]+)\/([^\/\s]+)$/.exec(repoArg); + if (!m) { + const html = await renderDocsPage({ + title: "SAMA verify · bad input — tdd.md", + description: "SAMA verify expects an owner/name repo identifier.", + bodyMarkdown: `# SAMA verify\n\n> Couldn't parse \`${repoArg}\`. Use the form: \`owner/name\`.\n\n[← back](/sama/verify)\n`, + pathForDocs: "/sama/verify", + editPathOverride: null, + ogPath: "https://tdd.md/sama/verify", + active: "sama", + noindex: true, + }); + return htmlResponse(html, 400); + } + + const [, owner, name] = m; + let report: SamaReport; + try { + // Dogfood short-circuit: tdd.md is a private repo, so the GitHub + // API can't see it. When asked to verify ourselves, read the + // source from the bundled `./src/` directory inside the container. + const isSelf = owner === LIVE_REPO_OWNER && name === LIVE_REPO_NAME; + report = isSelf ? await verifyLocalDogfood(owner!, name!) : await verifyRemoteRepo(owner!, name!); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + const html = await renderDocsPage({ + title: `SAMA verify · ${owner}/${name} · error — tdd.md`, + description: `SAMA verify could not inspect ${owner}/${name}.`, + 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`, + ogPath: `https://tdd.md/sama/verify?repo=${owner}/${name}`, + active: "sama", + noindex: true, + pathForDocs: "/sama/verify", + editPathOverride: null, + }); + return htmlResponse(html, 502); + } + + const html = await renderVerifyReport(report); + return htmlResponse(html); +}; + +// -------- /sama (landing) -------- + +const SAMA_LANDING_MD = `# SAMA + +> **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. + +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. + +## the four disciplines + +| letter | discipline | one-line rule | +|---|---|---| +%ROWS% + +## reading order + +If you're new to this: +1. Start with **[Sorted](/sama/sorted)** — it has the verification grep that everything else is built around. +2. Then **[Architecture](/sama/architecture)** — what each layer prefix means. +3. Then **[Modeled](/sama/modeled)** — where types and tests live. +4. Then **[Atomic](/sama/atomic)** — the split rule that keeps the rest honest as the codebase grows. + +Each page is short, opinionated, and ends with the common mistakes you'll see if the discipline lapses. + +## drop into your agent + +For agents that load skills from \`~/.claude/skills/\` (Claude Code, obra/superpowers, etc.), grab the SKILL.md version: + +\`\`\`bash +mkdir -p ~/.claude/skills +curl -fsSL https://tdd.md/skills/sama.md -o ~/.claude/skills/sama.md +\`\`\` + +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)** + +## verify any public repo + +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). + +## the \`sama\` CLI + +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: + +\`\`\`bash +mkdir -p ~/.local/bin +curl -fsSL https://tdd.md/tools/sama-cli -o ~/.local/bin/sama +chmod +x ~/.local/bin/sama +sama --help +\`\`\` + +Two subcommands: + +\`\`\`bash +sama check # verify the current repo's src/ +sama check --json # JSON output for piping into CI tooling +sama verify-repo owner/name # verify a public GitHub repo (no token) +\`\`\` + +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\`. + +### pre-commit hook + +Add to \`.git/hooks/pre-commit\` (or via \`husky\`, \`pre-commit\`, \`lefthook\`): + +\`\`\`bash +#!/usr/bin/env bash +# Block commits that violate SAMA layer/atomic/modeled rules. +exec sama check +\`\`\` + +### GitHub Action + +\`\`\`yaml +# .github/workflows/sama.yml +name: sama +on: [push, pull_request] +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - run: | + curl -fsSL https://tdd.md/tools/sama-cli -o sama + chmod +x sama + ./sama check +\`\`\` + +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. + +## the case behind it + +Two long-form pieces that argue *why* SAMA is shaped this way: + +- [**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.* +- [**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. + +If you're reading these for the first time, the order to take them is harness postmortem → corpus → back here. + +## why these four together + +Each property fixes a different failure mode: + +- *Sorted* fails when imports go in any direction → grep proves the rule. +- *Architecture* fails when responsibilities blur → the prefix is the contract. +- *Modeled* fails when types and tests scatter → siblings are mandatory. +- *Atomic* fails when files swell → the ~700-line split keeps atoms small. + +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. + +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. + +[← back to tdd.md](/) · [the blog](/blog) · [the guides](/guides) +`; + +export const samaLandingHandler = async (): Promise => { + const rows = ALL_SAMA + .map((d) => `| **[${d.letter} — ${d.title}](/sama/${d.slug})** | ${d.rule} |`) + .join("\n"); + const body = SAMA_LANDING_MD.replace("%ROWS%", rows); + const html = await renderDocsPage({ + title: "SAMA — sorted, architecture, modeled, atomic — tdd.md", + 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.", + bodyMarkdown: body, + ogPath: "https://tdd.md/sama", + active: "sama", + pathForDocs: "/sama", + editPathOverride: null, + }); + return htmlResponse(html); +}; + +// -------- /sama/:slug (per-discipline content page) -------- + +export const samaSlugHandler = async (req: { params: { slug: string } }): Promise => { + const slug = req.params.slug; + const entry = ALL_SAMA.find((d) => d.slug === slug); + if (!entry) { + const html = await renderNotFound(`/sama/${slug}`); + return htmlResponse(html, 404); + } + const file = Bun.file(`./content/sama/${slug}.md`); + if (!(await file.exists())) { + const html = await renderNotFound(`/sama/${slug}`); + return htmlResponse(html, 404); + } + const md = await file.text(); + const html = await renderDocsPage({ + title: `SAMA · ${entry.letter} — ${entry.title} — tdd.md`, + description: entry.description, + bodyMarkdown: md, + ogPath: `https://tdd.md/sama/${slug}`, + active: "sama", + pathForDocs: `/sama/${slug}`, + }); + return htmlResponse(html); +}; + +// -------- /tools/sama-cli (binary download) -------- + +export const samaCliResponse = (): Response => + new Response(Bun.file("./public/sama-cli"), { + headers: { + "Content-Type": "text/javascript; charset=utf-8", + "Content-Disposition": 'inline; filename="sama"', + "Cache-Control": "public, max-age=300", + }, + }); diff --git a/src/c31_site_config.ts b/src/c31_site_config.ts new file mode 100644 index 0000000000000000000000000000000000000000..5a06b9715a7056eb904844aac3fbbfccf098dc32 --- /dev/null +++ b/src/c31_site_config.ts @@ -0,0 +1,10 @@ +// c31 — model: site-wide config constants. Pure data, no I/O. +// Lives here so handlers across clusters (sama-verify dogfood, +// reports/live, sitemap, etc.) reference the same values without +// circular imports between c21_handlers_*. + +export const LIVE_REPO_OWNER = "syntaxai"; +export const LIVE_REPO_NAME = "tdd.md"; +// Number of recent commits the live-reports view samples from the +// in-container git-history bundle. +export const LIVE_FETCH_COUNT = 100;