syntaxai/tdd.md · main · content / sama / skill.md

skill.md 198 lines · 8158 bytes 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:

  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.
</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_*.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