Glossary
The words the standard uses. The examples come from the walkthrough, so that a term is a line of code rather than a definition.
Pure function
A function whose result depends only on its arguments and which changes nothing around it. Call it twice with the same arguments and you get the same answer, and nothing in the world has moved.
derivePublishedTrack(track, nowMs) // pure: only computes a new value
saveTrack(track) // not pure: writes to the databasePure functions need no setup to test: no database, no network, no faked clock. That is why GRAIN gives them their own file role — *.pure.ts.
Side effect
Anything a function does "outward": writing to a database, an HTTP request, a log line, changing a global, reading the system clock. Effects are not forbidden — they are collected in predictable places instead of scattered through the code.
Non-determinism
A source of values that will answer the same question differently tomorrow: Date.now(), Math.random(), crypto.randomUUID(), process.env. GRAIN keeps them at the boundary; inside the domain they arrive as arguments, because otherwise behaviour can be neither repeated nor verified.
Boundary
Where code talks to the outside world: an HTTP handler, a gRPC method, a queue consumer, a process entry point, a client for someone else's API. The roles entry, route, rpc, job, wire. A boundary may do what the inside may not: read the clock, read the environment, know the framework.
Domain
The meaningful part of the program: what would remain if you removed HTTP, the database and the framework. The rules for publishing a track are domain. The number 403 is not.
Contract
A promise a function makes to its caller before the caller reads the body. In GRAIN the name carries the contract: findTrack promises to return null when there is no track and never to throw; getTrack promises a value or an exception. Contracts must not be broken — code is written against them.
Invariant
A condition that must always hold, or the program is broken. "A published track has a cover" is an invariant. It is checked with assert*: if it is violated, that is a bug, not an expected situation.
Failure and exception
A failure is an expected outcome and part of the contract: not found, not allowed, insufficient funds. It is returned as a value carrying a code shaped domain.subject.reason.
An exception is what should not happen: the database is down, an invariant broke, there is a bug in the code. It is thrown and caught only at the boundary.
Confusing the two is the main source of code where one catch swallows everything and turns it into "something went wrong".
Idempotency
The property that calling an operation again changes nothing. Publishing an already published track should leave it published, not move the date. In GRAIN the verb ensure* carries this promise; in SQL it is if not exists in migrations.
Guard, early exit
A check at the top of a function that cuts off the impossible and leaves immediately. Instead of an if / else staircase, a flat list of conditions:
if (track === null) return makeFail('music.track.missing', '…')
if (!canPublish(track, actor.id, actor.isAdmin)) return makeFail('music.track.forbidden', '…')Each guard reads on its own, and none of them requires holding the others in your head.
Funnel
The shape of a function in GRAIN: guard → acquire → derive → effect. Cut off the impossible, fetch the data, compute, write. The beats do not interleave; two sets of beats mean two functions.
File role
One responsibility, declared in the filename: *.store.ts is the only place with SQL, *.policy.ts holds rules without I/O, *.pure.ts holds computation, *.route.ts is the HTTP boundary. The role is visible in a directory listing, so you do not open a file to learn what is in it.
Dependency direction
Who knows about whom. In GRAIN the arrows point inward: the boundary knows the domain, the domain never knows the boundary. The moment *.pure.ts imports *.store.ts, the pure part has stopped being pure.
Mock
A fake dependency in a test: a fake database, a frozen clock. A mock is not a tool but a symptom: if testing a rule requires a fake database, the rule is entangled with I/O and belongs in *.policy.ts or *.pure.ts.
Unit in the name
The suffix that says what a number measures: durationSec, timeoutMs, priceCents, sizeBytes. A number without a unit is an error in GRAIN, because it is an entire class of production bugs: one side sends seconds, the other expects milliseconds, and formally both are right.
Closed vocabulary
A finite list of permitted words: 33 verbs, seven boolean prefixes, ten file roles, five comment tags. Closed means a new word cannot be invented on the spot — it is added to the standard. This is the mechanism that makes the code look written by one hand.
AST, linter
An AST is the tree a compiler turns source text into: not "a string of characters" but "a function declaration with this name and these arguments". A linter walks that tree and checks rules. That is how a machine can tell the word find in a function name from the word find in a comment.
Escape hatch
The sanctioned way to break a rule deliberately: // grain:allow <rule> — <reason>. The reason is mandatory — it turns a violation from carelessness into a decision someone can argue with.