043b835baaa42fc2be8fcbbbd890b96f0a1fef77 diff --git a/content/sama/skill.md b/content/sama/skill.md index 542fe8288efa5d58496a5bb90c2dc3c12554e275..255eb0101800d78cfdc83c748ca6831b553851ae 100644 --- a/content/sama/skill.md +++ b/content/sama/skill.md @@ -1,191 +1,193 @@ ---- -name: sama-architecture -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. ---- - -# SAMA — Sorted, Architecture, Modeled, Atomic - -## Overview - -A four-property file-naming and module-organisation convention so files sort by their dependency layer and one grep proves the structure. - -**Core principle:** pick the layer first, then the name, then the code. - -**The Iron Rule:** lower-numbered layers NEVER import from higher-numbered ones. - -## When to Use - -**Always, when:** -- Adding a new source file -- Moving a function between files -- Splitting a file that has grown past ~700 lines -- Reviewing a diff for layer violations - -**Exceptions (ask your human partner):** -- Generated code -- Vendored third-party files - -Thinking "this one helper doesn't need a prefix"? Stop. That's how the rule erodes. - -## The Iron Rule - -``` -UI SITS AT THE EDGE — FOUNDATION, DATA AND LOGIC LAYERS NEVER DEPEND ON UI -``` - -Foundation/data/logic (`c1*`, `c3*`) must never import UI (`c5*+`). Handlers (`c21`) are the orchestration layer and may compose UI; UI itself may read models for the data it renders. - -Verify with one grep: - -```bash -grep -rE 'from "\./c[5-9]' src/c1*.ts src/c3*.ts -``` - -Empty output = rule holds. Any output = a UI dependency has leaked into foundation/data/logic. Move the function or rename the file. Do not "fix" the violation by deleting the import without understanding what broke. - -## The Four Letters - -### S — Sorted - -Alphabetical sort = dependency direction. `ls src/` is the architecture diagram. The file system is the contract. - -### A — Architecture - -The prefix number is the layer; the layer is the contract. - -| prefix | layer | what's allowed | what's not | -|---|---|---|---| -| `c11_*` | server entry | env, `Bun.serve()`, port wiring | route logic, SQL, HTML | -| `c13_*` | database | SQLite queries, schema | HTTP, HTML, route handling | -| `c14_*` | secondary I/O | HTTP clients (GitHub, mail, etc.) | SQL, business logic | -| `c21_*` | handlers | route handlers, composes lower | direct SQL, raw HTTP, model defs | -| `c31_*` | models | types, parsers, pure data helpers | I/O of any kind, side effects | -| `c32_*` | logic | pure business logic, deterministic | I/O, randomness, time without injection | -| `c51_*` | UI | HTML rendering, page chrome | data fetching, mutations | - -Numbers are spaced. Future layers land between existing ones without renaming the world. - -### M — Modeled - -Tests live next to source. Types and parse-functions live in `c31_*`. - -``` -src/c32_session.ts impl -src/c32_session.test.ts proof -``` - -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. - -### A — Atomic - -One responsibility per module. When a layer file passes ~700 lines, split per UI/data domain using the same prefix: - -``` -c51_render_layout.ts chrome (renderPage, escape, ...) -c51_render_projects.ts /projects body builders -c51_render_reports.ts /reports body builders -``` - -**No barrel re-exports.** Consumers import directly from the atom. - -## Picking the Right Layer - -Decide in this order: - -1. Does it perform I/O? → `c13` (SQL) or `c14` (HTTP). -2. Is it pure types or parsers? → `c31`. -3. Is it pure logic deriving one value from others? → `c32`. -4. Does it produce HTML? → `c51`. -5. Is it a route handler that composes the above? → `c21`. - -Cannot fit? The file does more than one job. Split it. - -## Good vs Bad - - -```ts -// src/c21_handlers_projects.ts -import { parseProjectConfig } from "./c31_project_config.ts"; -import { upsertProject } from "./c13_database.ts"; - -const config = parseProjectConfig(await req.json()); -await upsertProject(viewer.login, config); -``` -Handler composes lower layers. Parser in c31 handles untyped input. SQL stays in c13. - - - -```ts -// src/c21_handlers_projects.ts -const raw = await req.json() as { test_runner: string }; -const stmt = db.prepare("INSERT INTO projects ..."); -stmt.run(raw.test_runner); -``` -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. - - -## Common Rationalizations - -| Excuse | Reality | -|---|---| -| "This helper is generic, no specific layer" | Every helper has callers. The lowest caller's layer is its home. | -| "I'll add the prefix later" | You won't. Add it now. | -| "A barrel makes imports cleaner" | A barrel hides the dependency direction the grep proves. | -| "It's still under 700 lines" | The threshold isn't the only signal. If you'd want a TOC, split. | -| "Tests in a parallel tree are fine" | Then deletes orphan their tests. Move them next to source. | -| "An `as` cast is faster than a parser" | Until the input lies and you debug for hours. Write the parser. | -| "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. | - -## Red Flags — STOP - -- A `from "./cXX_..."` import where the source's prefix is HIGHER than the target's prefix. -- A `cXX_*.ts` without a sibling `cXX_*.test.ts` and it's not pure data. -- A file over 700 lines with multiple unrelated functions. -- A `c51_render.ts` or similar that re-exports from per-domain files. -- An `as Foo` cast on data from outside the process. -- A function defined in one file but logically belonging to another layer. - -**All of these mean: stop, fix the layout before adding more code.** - -## Verification Checklist - -Before merging: - -- [ ] `grep -rE 'from "\./c[5-9]' src/c1*.ts src/c2*.ts src/c3*.ts` returns empty -- [ ] every non-data `cXX_*.ts` has a sibling `cXX_*.test.ts` -- [ ] no file in `src/` over 700 lines without a per-domain split rationale -- [ ] no barrel re-export files (`cXX_render.ts`, `cXX_handlers.ts`, `index.ts` in src/) -- [ ] no `as` cast on data from outside the process - -## Examples - -### Adding a new feature - -A new "/exports" feature that lets a user download their kata results as JSON. - -1. **Type + parser** for the export shape → `c31_export.ts` (with `c31_export.test.ts` next to it). -2. **Logic** that turns runs into the export shape → `c32_export.ts` (pure, no I/O). -3. **Handler** at `/exports` → add to `c21_app.ts` (or new `c21_handlers_exports.ts` if it grows). -4. **Render** the JSON page → if there's an HTML index of past exports, that goes in `c51_render_exports.ts`. - -Four files, four layers, every dependency points DOWN. Run the grep. Empty? Done. - -### Refactoring an oversized file - -`c51_render_reports.ts` has grown to 850 lines covering `/reports`, `/reports/demo`, and `/reports/live`. Split per sub-domain: - -``` -c51_render_reports.ts → kept: shared helpers (sparkline, tile, bars) -c51_render_reports_demo.ts ← new: demo-specific body builders -c51_render_reports_live.ts ← new: live-specific body builders -``` - -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. - -## Reference - -- The four disciplines with examples: https://tdd.md/sama -- Why SAMA compounds with TDD and token-discipline: https://tdd.md/blog/three-constraints-agentic-coding -- The Reddit harness postmortem this skill is a response to: https://tdd.md/blog/claude-code-harness-postmortem -- Ten more threads, three patterns, mitigation tables: https://tdd.md/blog/agentic-coding-corpus-three-patterns -- Mechanically verify any public repo against these rules: https://tdd.md/sama/verify +--- +name: sama-architecture +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. +--- + +# SAMA — Sorted, Architecture, Modeled, Atomic + +## Overview + +A four-property file-naming and module-organisation convention so files sort by their dependency layer and one grep proves the structure. + +**Core principle:** pick the layer first, then the name, then the code. + +**The Iron Rule:** lower-numbered layers NEVER import from higher-numbered ones. + +## When to Use + +**Always, when:** +- Adding a new source file +- Moving a function between files +- Splitting a file that has grown past ~700 lines +- Reviewing a diff for layer violations + +**Exceptions (ask your human partner):** +- Generated code +- Vendored third-party files + +Thinking "this one helper doesn't need a prefix"? Stop. That's how the rule erodes. + +## The Iron Rule + +``` +UI SITS AT THE EDGE — FOUNDATION, DATA AND LOGIC LAYERS NEVER DEPEND ON UI +``` + +Foundation/data/logic (`c1*`, `c3*`) must never import UI (`c5*+`). Handlers (`c21`) are the orchestration layer and may compose UI; UI itself may read models for the data it renders. + +Verify with one grep: + +```bash +grep -rE 'from "\./c[5-9]' src/c1*.ts src/c3*.ts +``` + +Empty output = rule holds. Any output = a UI dependency has leaked into foundation/data/logic. Move the function or rename the file. Do not "fix" the violation by deleting the import without understanding what broke. + +## The Four Letters + +### S — Sorted + +Alphabetical sort = dependency direction. `ls src/` is the architecture diagram. The file system is the contract. + +### A — Architecture + +The prefix number is the layer; the layer is the contract. + +| prefix | layer | what's allowed | what's not | +|---|---|---|---| +| `c11_*` | server entry | env, `Bun.serve()`, port wiring | route logic, SQL, HTML | +| `c13_*` | database | SQLite queries, schema | HTTP, HTML, route handling | +| `c14_*` | secondary I/O | HTTP clients (GitHub, mail, etc.) | SQL, business logic | +| `c21_*` | handlers | route handlers, composes lower | direct SQL, raw HTTP, model defs | +| `c31_*` | models | types, parsers, pure data helpers | I/O of any kind, side effects | +| `c32_*` | logic | pure business logic, deterministic | I/O, randomness, time without injection | +| `c51_*` | UI | HTML rendering, page chrome | data fetching, mutations | + +Numbers are spaced. Future layers land between existing ones without renaming the world. + +### M — Modeled + +Tests live next to source. Types and parse-functions live in `c31_*`. + +``` +src/c32_session.ts impl +src/c32_session.test.ts proof +``` + +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. + +### A — Atomic + +One responsibility per module. When a layer file passes ~700 lines, split per UI/data domain using the same prefix: + +``` +c51_render_layout.ts chrome (renderPage, escape, ...) +c51_render_projects.ts /projects body builders +c51_render_reports.ts /reports body builders +``` + +**No barrel re-exports.** Consumers import directly from the atom. + +## Picking the Right Layer + +Decide in this order: + +1. Does it perform I/O? → `c13` (SQL) or `c14` (HTTP). +2. Is it pure types or parsers? → `c31`. +3. Is it pure logic deriving one value from others? → `c32`. +4. Does it produce HTML? → `c51`. +5. Is it a route handler that composes the above? → `c21`. + +Cannot fit? The file does more than one job. Split it. + +## Good vs Bad + + +```ts +// src/c21_handlers_projects.ts +import { parseProjectConfig } from "./c31_project_config.ts"; +import { upsertProject } from "./c13_database.ts"; + +const config = parseProjectConfig(await req.json()); +await upsertProject(viewer.login, config); +``` +Handler composes lower layers. Parser in c31 handles untyped input. SQL stays in c13. + + + +```ts +// src/c21_handlers_projects.ts +const raw = await req.json() as { test_runner: string }; +const stmt = db.prepare("INSERT INTO projects ..."); +stmt.run(raw.test_runner); +``` +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. + + +## Common Rationalizations + +| Excuse | Reality | +|---|---| +| "This helper is generic, no specific layer" | Every helper has callers. The lowest caller's layer is its home. | +| "I'll add the prefix later" | You won't. Add it now. | +| "A barrel makes imports cleaner" | A barrel hides the dependency direction the grep proves. | +| "It's still under 700 lines" | The threshold isn't the only signal. If you'd want a TOC, split. | +| "Tests in a parallel tree are fine" | Then deletes orphan their tests. Move them next to source. | +| "An `as` cast is faster than a parser" | Until the input lies and you debug for hours. Write the parser. | +| "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. | + +## Red Flags — STOP + +- A `from "./cXX_..."` import where the source's prefix is HIGHER than the target's prefix. +- A `cXX_*.ts` without a sibling `cXX_*.test.ts` and it's not pure data. +- A file over 700 lines with multiple unrelated functions. +- A `c51_render.ts` or similar that re-exports from per-domain files. +- An `as Foo` cast on data from outside the process. +- A function defined in one file but logically belonging to another layer. + +**All of these mean: stop, fix the layout before adding more code.** + +## Verification Checklist + +Before merging: + +- [ ] `grep -rE 'from "\./c[5-9]' src/c1*.ts src/c2*.ts src/c3*.ts` returns empty +- [ ] every non-data `cXX_*.ts` has a sibling `cXX_*.test.ts` +- [ ] no file in `src/` over 700 lines without a per-domain split rationale +- [ ] no barrel re-export files (`cXX_render.ts`, `cXX_handlers.ts`, `index.ts` in src/) +- [ ] no `as` cast on data from outside the process + +## Examples + +### Adding a new feature + +A new "/exports" feature that lets a user download their kata results as JSON. + +1. **Type + parser** for the export shape → `c31_export.ts` (with `c31_export.test.ts` next to it). +2. **Logic** that turns runs into the export shape → `c32_export.ts` (pure, no I/O). +3. **Handler** at `/exports` → add to `c21_app.ts` (or new `c21_handlers_exports.ts` if it grows). +4. **Render** the JSON page → if there's an HTML index of past exports, that goes in `c51_render_exports.ts`. + +Four files, four layers, every dependency points DOWN. Run the grep. Empty? Done. + +### Refactoring an oversized file + +`c51_render_reports.ts` has grown to 850 lines covering `/reports`, `/reports/demo`, and `/reports/live`. Split per sub-domain: + +``` +c51_render_reports.ts → kept: shared helpers (sparkline, tile, bars) +c51_render_reports_demo.ts ← new: demo-specific body builders +c51_render_reports_live.ts ← new: live-specific body builders +``` + +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. + +## Reference + +- The four disciplines with examples: https://tdd.md/sama +- Why SAMA compounds with TDD and token-discipline: https://tdd.md/blog/three-constraints-agentic-coding +- The Reddit harness postmortem this skill is a response to: https://tdd.md/blog/claude-code-harness-postmortem +- Ten more threads, three patterns, mitigation tables: https://tdd.md/blog/agentic-coding-corpus-three-patterns +- Mechanically verify any public repo against these rules: https://tdd.md/sama/verify + +