syntaxai/tdd.md · commit b5e4e66

SAMA: ship as an agent-installable SKILL.md

The /sama pages explain the discipline to a human. This commit adds
the same content in obra/superpowers SKILL.md format so an agent can
load SAMA on demand from ~/.claude/skills/sama.md.

  content/sama/skill.md
    YAML frontmatter (name: sama-architecture, description: ...) so
    skill loaders pick it up. Body covers: when to use, the iron rule
    + verification grep, the four letters with examples, picking the
    right layer (decision tree), good/bad code blocks, common
    rationalizations table, red-flags-stop list, and a pre-merge
    verification checklist.

  src/c21_app.ts
    Two routes:
      /skills/sama.md   raw markdown with frontmatter + correct
                        text/markdown content-type, so an agent can
                        curl -fsSL it straight into its skills dir.
      /sama/skill       HTML viewer that strips the YAML frontmatter
                        and prepends an install-instructions box with
                        the curl one-liner.
    The /sama landing gets a "drop into your agent" section pointing
    at both URLs. Sitemap includes /sama/skill (the HTML page); the
    raw .md is intentionally excluded.

Install:
    mkdir -p ~/.claude/skills
    curl -fsSL https://tdd.md/skills/sama.md -o ~/.claude/skills/sama.md

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
author
syntaxai <[email protected]>
date
2026-05-09 16:37:08 +01:00
parent
0159bbb
commit
b5e4e661277176c07022f588f8cae8e4a20bdbbd

2 files changed · +233 −0

added content/sama/skill.md +186 −0
@@ -0,0 +1,186 @@
1+---
2+name: sama-architecture
3+description: Use when creating, moving, or refactoring any source file in a SAMA codebase. Encodes the four-layer-prefix convention (Sorted, Architecture, Modeled, Atomic) with one mechanical verification grep.
4+---
5+
6+# SAMA — Sorted, Architecture, Modeled, Atomic
7+
8+## Overview
9+
10+A four-property file-naming and module-organisation convention so files sort by their dependency layer and one grep proves the structure.
11+
12+**Core principle:** pick the layer first, then the name, then the code.
13+
14+**The Iron Rule:** lower-numbered layers NEVER import from higher-numbered ones.
15+
16+## When to Use
17+
18+**Always, when:**
19+- Adding a new source file
20+- Moving a function between files
21+- Splitting a file that has grown past ~700 lines
22+- Reviewing a diff for layer violations
23+
24+**Exceptions (ask your human partner):**
25+- Generated code
26+- Vendored third-party files
27+
28+Thinking "this one helper doesn't need a prefix"? Stop. That's how the rule erodes.
29+
30+## The Iron Rule
31+
32+```
33+LOWER LAYERS NEVER IMPORT FROM HIGHER LAYERS
34+```
35+
36+Verify with one grep:
37+
38+```bash
39+grep -rE 'from "\./c[5-9]' src/c1*.ts src/c2*.ts src/c3*.ts
40+```
41+
42+Empty output = rule holds. Any output = a lower layer reaches into a higher one. Either move the function or rename the file. Do not "fix" the violation by deleting the import without understanding what broke.
43+
44+## The Four Letters
45+
46+### S — Sorted
47+
48+Alphabetical sort = dependency direction. `ls src/` is the architecture diagram. The file system is the contract.
49+
50+### A — Architecture
51+
52+The prefix number is the layer; the layer is the contract.
53+
54+| prefix | layer | what's allowed | what's not |
55+|---|---|---|---|
56+| `c11_*` | server entry | env, `Bun.serve()`, port wiring | route logic, SQL, HTML |
57+| `c13_*` | database | SQLite queries, schema | HTTP, HTML, route handling |
58+| `c14_*` | secondary I/O | HTTP clients (GitHub, mail, etc.) | SQL, business logic |
59+| `c21_*` | handlers | route handlers, composes lower | direct SQL, raw HTTP, model defs |
60+| `c31_*` | models | types, parsers, pure data helpers | I/O of any kind, side effects |
61+| `c32_*` | logic | pure business logic, deterministic | I/O, randomness, time without injection |
62+| `c51_*` | UI | HTML rendering, page chrome | data fetching, mutations |
63+
64+Numbers are spaced. Future layers land between existing ones without renaming the world.
65+
66+### M — Modeled
67+
68+Tests live next to source. Types and parse-functions live in `c31_*`.
69+
70+```
71+src/c32_session.ts impl
72+src/c32_session.test.ts proof
73+```
74+
75+External input (JSON, env, request body) goes through a parser in `c31_*` before any logic touches it. Typed values flow downstream from there. **No `as` casts at I/O boundaries** — a cast is a lie the type system promised not to question.
76+
77+### A — Atomic
78+
79+One responsibility per module. When a layer file passes ~700 lines, split per UI/data domain using the same prefix:
80+
81+```
82+c51_render_layout.ts chrome (renderPage, escape, ...)
83+c51_render_projects.ts /projects body builders
84+c51_render_reports.ts /reports body builders
85+```
86+
87+**No barrel re-exports.** Consumers import directly from the atom.
88+
89+## Picking the Right Layer
90+
91+Decide in this order:
92+
93+1. Does it perform I/O? → `c13` (SQL) or `c14` (HTTP).
94+2. Is it pure types or parsers? → `c31`.
95+3. Is it pure logic deriving one value from others? → `c32`.
96+4. Does it produce HTML? → `c51`.
97+5. Is it a route handler that composes the above? → `c21`.
98+
99+Cannot fit? The file does more than one job. Split it.
100+
101+## Good vs Bad
102+
103+<Good>
104+```ts
105+// src/c21_handlers_projects.ts
106+import { parseProjectConfig } from "./c31_project_config.ts";
107+import { upsertProject } from "./c13_database.ts";
108+
109+const config = parseProjectConfig(await req.json());
110+await upsertProject(viewer.login, config);
111+```
112+Handler composes lower layers. Parser in c31 handles untyped input. SQL stays in c13.
113+</Good>
114+
115+<Bad>
116+```ts
117+// src/c21_handlers_projects.ts
118+const raw = await req.json() as { test_runner: string };
119+const stmt = db.prepare("INSERT INTO projects ...");
120+stmt.run(raw.test_runner);
121+```
122+Handler does its own SQL (belongs in c13). `as` cast at the boundary (belongs as a parser in c31). Two layer violations in three lines.
123+</Bad>
124+
125+## Common Rationalizations
126+
127+| Excuse | Reality |
128+|---|---|
129+| "This helper is generic, no specific layer" | Every helper has callers. The lowest caller's layer is its home. |
130+| "I'll add the prefix later" | You won't. Add it now. |
131+| "A barrel makes imports cleaner" | A barrel hides the dependency direction the grep proves. |
132+| "It's still under 700 lines" | The threshold isn't the only signal. If you'd want a TOC, split. |
133+| "Tests in a parallel tree are fine" | Then deletes orphan their tests. Move them next to source. |
134+| "An `as` cast is faster than a parser" | Until the input lies and you debug for hours. Write the parser. |
135+| "I'll re-export through an index for ergonomics" | The five extra characters in the import path are not a real cost. The barrel is. |
136+
137+## Red Flags — STOP
138+
139+- A `from "./cXX_..."` import where the source's prefix is HIGHER than the target's prefix.
140+- A `cXX_*.ts` without a sibling `cXX_*.test.ts` and it's not pure data.
141+- A file over 700 lines with multiple unrelated functions.
142+- A `c51_render.ts` or similar that re-exports from per-domain files.
143+- An `as Foo` cast on data from outside the process.
144+- A function defined in one file but logically belonging to another layer.
145+
146+**All of these mean: stop, fix the layout before adding more code.**
147+
148+## Verification Checklist
149+
150+Before merging:
151+
152+- [ ] `grep -rE 'from "\./c[5-9]' src/c1*.ts src/c2*.ts src/c3*.ts` returns empty
153+- [ ] every non-data `cXX_*.ts` has a sibling `cXX_*.test.ts`
154+- [ ] no file in `src/` over 700 lines without a per-domain split rationale
155+- [ ] no barrel re-export files (`cXX_render.ts`, `cXX_handlers.ts`, `index.ts` in src/)
156+- [ ] no `as` cast on data from outside the process
157+
158+## Examples
159+
160+### Adding a new feature
161+
162+A new "/exports" feature that lets a user download their kata results as JSON.
163+
164+1. **Type + parser** for the export shape → `c31_export.ts` (with `c31_export.test.ts` next to it).
165+2. **Logic** that turns runs into the export shape → `c32_export.ts` (pure, no I/O).
166+3. **Handler** at `/exports` → add to `c21_app.ts` (or new `c21_handlers_exports.ts` if it grows).
167+4. **Render** the JSON page → if there's an HTML index of past exports, that goes in `c51_render_exports.ts`.
168+
169+Four files, four layers, every dependency points DOWN. Run the grep. Empty? Done.
170+
171+### Refactoring an oversized file
172+
173+`c51_render_reports.ts` has grown to 850 lines covering `/reports`, `/reports/demo`, and `/reports/live`. Split per sub-domain:
174+
175+```
176+c51_render_reports.ts → kept: shared helpers (sparkline, tile, bars)
177+c51_render_reports_demo.ts ← new: demo-specific body builders
178+c51_render_reports_live.ts ← new: live-specific body builders
179+```
180+
181+Or, if the live and demo body builders are mostly the same shape parameterised by a context object: factor the *context* into `c31_reports_context.ts` and keep one `c51_render_reports.ts`. Both are valid. The wrong answer is a barrel.
182+
183+## Reference
184+
185+- The four disciplines with examples: https://tdd.md/sama
186+- Why SAMA compounds with TDD and token-discipline: https://tdd.md/blog/three-constraints-agentic-coding
modified src/c21_app.ts +47 −0
@@ -307,6 +307,7 @@ ${url("https://tdd.md/guides", "0.9")}
307307 ${guideUrls}
308308 ${url("https://tdd.md/sama", "0.9")}
309309 ${samaUrls}
310+${url("https://tdd.md/sama/skill", "0.8")}
310311 ${url("https://tdd.md/blog", "0.7")}
311312 ${blogUrls}
312313 ${url("https://tdd.md/agents", "0.7")}
@@ -630,6 +631,41 @@ ${rows}
630631 return htmlResponse(html);
631632 },
632633
634+ "/skills/sama.md": async () => {
635+ const md = await Bun.file("./content/sama/skill.md").text();
636+ return new Response(md, {
637+ headers: {
638+ "Content-Type": "text/markdown; charset=utf-8",
639+ "Cache-Control": "public, max-age=300",
640+ },
641+ });
642+ },
643+
644+ "/sama/skill": async () => {
645+ const raw = await Bun.file("./content/sama/skill.md").text();
646+ // Strip the YAML frontmatter for the HTML render — the .md raw
647+ // download keeps it (that's the agent-installable format).
648+ const stripped = raw.replace(/^---\n[\s\S]*?\n---\n+/, "");
649+ const installNote = `> **Drop into your agent.** Save the raw markdown to your skills directory:
650+>
651+> \`\`\`bash
652+> mkdir -p ~/.claude/skills
653+> curl -fsSL https://tdd.md/skills/sama.md -o ~/.claude/skills/sama.md
654+> \`\`\`
655+>
656+> 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)
657+`;
658+ const body = `${installNote}\n\n${stripped}\n\n---\n\n[← /sama](/sama) · [the four disciplines](/sama) · [back to tdd.md](/)\n`;
659+ const html = await renderPage({
660+ title: "SAMA skill — drop into your agent — tdd.md",
661+ 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.",
662+ bodyMarkdown: body,
663+ ogPath: "https://tdd.md/sama/skill",
664+ active: "sama",
665+ });
666+ return htmlResponse(html);
667+ },
668+
633669 "/sama": async () => {
634670 const rows = ALL_SAMA
635671 .map((d) => `| **[${d.letter} — ${d.title}](/sama/${d.slug})** | ${d.rule} |`)
@@ -656,6 +692,17 @@ If you're new to this:
656692
657693 Each page is short, opinionated, and ends with the common mistakes you'll see if the discipline lapses.
658694
695+## drop into your agent
696+
697+For agents that load skills from \`~/.claude/skills/\` (Claude Code, obra/superpowers, etc.), grab the SKILL.md version:
698+
699+\`\`\`bash
700+mkdir -p ~/.claude/skills
701+curl -fsSL https://tdd.md/skills/sama.md -o ~/.claude/skills/sama.md
702+\`\`\`
703+
704+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)**
705+
659706 ## why these four together
660707
661708 Each property fixes a different failure mode: