skill.md
raw
· source
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:
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:
- Does it perform I/O? →
c13(SQL) orc14(HTTP). - Is it pure types or parsers? →
c31. - Is it pure logic deriving one value from others? →
c32. - Does it produce HTML? →
c51. - Is it a route handler that composes the above? →
c21.
Cannot fit? The file does more than one job. Split it.
Good vs Bad
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.
</Good>
<Bad>
```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_*.tswithout a siblingcXX_*.test.tsand it's not pure data. - A file over 700 lines with multiple unrelated functions.
- A
c51_render.tsor similar that re-exports from per-domain files. - An
as Foocast 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*.tsreturns empty - every non-data
cXX_*.tshas a siblingcXX_*.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.tsin src/) - no
ascast 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.
- Type + parser for the export shape →
c31_export.ts(withc31_export.test.tsnext to it). - Logic that turns runs into the export shape →
c32_export.ts(pure, no I/O). - Handler at
/exports→ add toc21_app.ts(or newc21_handlers_exports.tsif it grows). - 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/2026-05/three-constraints-agentic-coding
- The Reddit harness postmortem this skill is a response to: https://tdd.md/blog/2026-05/claude-code-harness-postmortem
- Ten more threads, three patterns, mitigation tables: https://tdd.md/blog/2026-05/agentic-coding-corpus-three-patterns
- Mechanically verify any public repo against these rules: https://tdd.md/sama/verify