Files and directories
The role lives in the filename
<domain>.<role>.ts — the role is visible in a directory listing, so the file does not have to be opened.
| Role | Contents | Promise |
|---|---|---|
*.entry.ts | Process entry point | The only place where the dependency graph is assembled |
*.route.ts | HTTP endpoints | Request parsing and a call into the domain, nothing else |
*.rpc.ts | gRPC methods | The same, for RPC |
*.store.ts | Database access | The only place that contains SQL or an ORM |
*.wire.ts | A client for someone else's API | The only place a foreign format lives |
*.policy.ts | Access rules and invariants | Pure, no I/O — rules are testable without a database |
*.shape.ts | Types and validation schemas | Nothing executable |
*.event.ts | Event contracts, producers and consumers | Subject names appear only here |
*.job.ts | Background work, cron | Idempotency is mandatory |
*.pure.ts | Pure domain computation | Zero I/O. The legal home for what used to be dumped in utils |
*.tsx is a component by definition and needs no role. Framework-mandated names (page, layout, route, middleware, mod) stay as they are. A package's public entry is public.ts.
Banned filenames
utils util helpers helper common shared misc lib
main types constants service manager handler indexEvery one of them promises a dumping ground: within six months a file with that name holds two unrelated things, and nobody can delete it because nobody knows who uses it.
Directories are vertical slices
src/controllers/track.ts
src/services/track.ts
src/types/track.ts
src/utils/track.tssrc/track/track.route.ts
src/track/track.store.ts
src/track/track.shape.ts
src/track/track.pure.tsA directory is named after a domain, not after a technical nature. Directories named utils helpers common shared misc lib core services controllers managers handlers are banned. A listing of src/ should show what the product does, not which framework it is written in.
Comments
A comment answers "why", never "what". The "what" is already written on the line below, and unlike the comment it does not lie.
Five tags are allowed:
// why: Stripe sends the webhook before the object is readable through the API
// perf: 40k rows — a Map beats a linear scan by two orders of magnitude
// safety: constant-time comparison, the value comes from outside
// spec: RFC 8628 §3.5
// ref: incident 2026-08-04, production rollbackA continuation line of a multi-line comment does not repeat the tag. Block comments are JSDoc above an export only; /* ... */ around code means the code should be deleted, not commented out.
Banned
// increment the counter
count += 1
// ============================
// Step 1: validate the input
// TODO: rewrite thiscount += 1
// why: the counter moves before the write — otherwise it races the worker
// grain:allow unit-suffix — field of an external Stripe contractComments that restate code, TODO / FIXME / HACK, numbered steps, ASCII dividers, emoji — in comments and in logs alike: a log is read by grep, not by a person.
A deliberate exception to any rule is written down, with a reason:
// grain:allow unit-suffix — field of an external Stripe contract, cannot be renamedThe reason is mandatory — without it the checker complains about the escape itself. That turns a violation from carelessness into a decision someone can argue with.