modeled.md
raw
· source
M — Modeled
Rule: tests live next to source. Types and parse-functions live in
c31_*. The shape comes before the logic.
The third letter of SAMA. Where Sorted and Architecture tell the agent where files go, Modeled tells it what those files contain. Two concrete commitments:
- Every behaviour has a test file as its sibling —
c32_session.tsnext toc32_session.test.ts. - Every external input has a parser in a
c31_*model — types and parse-functions colocated, not scattered.
The shape of the data and the proof of the behaviour are first-class artefacts, never afterthoughts.
Tests next to source
Not in a separate tests/ tree. Not in a parallel __tests__ folder. The test for c32_session.ts is c32_session.test.ts in the same directory.
src/
├── c32_session.ts ← impl
├── c32_session.test.ts ← its sibling test
├── c32_judge.ts
└── c32_judge.test.ts
Why this matters for an agent:
- When you
ls src/c32_session.*, you see both the impl and its proof. - When you delete the impl, the test goes too — no orphan test files to chase.
- When you start a TDD red commit, the test file is already next to where the impl will land — no path-juggling.
- Coverage is a glance, not a tool: any
cXX_*.tswithout a sibling*.test.tsis unproven.
The tooling agrees: bun test discovers *.test.ts automatically with no path config.
Parsers live in c31
Anything that comes in from outside the process — JSON files, request bodies, environment variables, third-party API responses — gets a parser in c31_* before any logic touches it. The parser:
- Defines the type (the shape).
- Validates the input.
- Throws a clear error on bad input.
// src/c31_project_config.ts
export interface ProjectConfig {
test_runner: TestRunner;
tracked_branches: string[];
// ...
}
export const parseProjectConfig = (raw: unknown): ProjectConfig => {
// explicit shape check, then return a typed value
};
The rest of the codebase consumes ProjectConfig, never unknown and never any. By the time data leaves c31_*, it has a name and a shape that the type system can carry the rest of the way.
"The shape comes before the logic"
The order in which an agent should approach a feature:
- What's the input shape? →
c31_*type + parser. - What's the behaviour? → red test in the test file.
- What's the impl? → just enough to pass the test.
Not the other way around. Writing the impl first lets you smuggle assumptions about the input ("oh, this field is always a number") into code that should have refused to start without an explicit type.
The discipline lines up with TDD's iron law: no code without a failing test first. SAMA adds: no test without a typed input first. Together they force you to write down what the world looks like before you write down what to do with it.
Examples
Good — type + parser + sibling test:
src/
├── c31_project_config.ts type + parseProjectConfig
├── c31_project_config.test.ts parser-edge-case tests
├── c32_judge.ts logic that consumes ProjectConfig
└── c32_judge.test.ts judge-behaviour tests
Bad — type defined inline at point of use:
// src/c21_handlers_projects.ts
const config = await req.json() as { test_runner: string; ... };
// ↑ no parser, no validation, type lies
The fix: move the type to c31_project_config.ts, write a parseProjectConfig that validates, call it in the handler. The shape and its proof are now in one place; every consumer benefits.
Common mistakes
- Tests in a parallel tree. Breaks the "delete the impl, test goes too" property. Move them next to source.
ascasts at I/O boundaries. A cast is a lie the type system promises not to question. Replace casts with parsers inc31_*.- Inline
interfacein handlers / logic. If two files use the same shape, it belongs inc31_*, not duplicated at each consumer. - A test file with no sibling impl. The test is for something that no longer exists, or the impl drifted to another file. Either reattach it or delete it.
What this gives you
- An agent given the path
c32_session.tscan find the contract (sibling test) and the input shape (c31_*type) without searching. - The boundary between the typed inside and the untyped outside is one file (
c31_*) per concern, not a scatter ofascasts. - "What does this code do?" is answered by the test next to it, every time.