| 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 |