GRAIN 1.1
The APERTURESyndicate code standard. Film grain — what identifies the medium before you read the signature.
GRAIN is not about indentation and quotes: that is the formatter's job, and it leaves no trace. GRAIN is about naming, contracts and shape — what remains after any formatter and reads like handwriting. Code written to GRAIN is recognisable in a stranger's repository within one screen.
Three principles
1. Closed vocabularies instead of taste. Verbs, units, prefixes, file roles — finite lists. Not "write clearly" but "here are 33 verbs, there are no others". An argument about a function name lasts exactly as long as it takes to read a table.
2. A name is part of the contract. findTrack and getTrack are different promises, not synonyms. From the name alone you know whether null can come back, whether the function crosses the network, whether it emits an event, and what unit a number is in. That removes half the reasons to open a function body.
3. Shape follows responsibility, not taste. A function is long not because "it turned out that way" but because it does two things. Limits on nesting, argument count and length are not aesthetics — they are an alarm: exceeded means split.
Levels
The standard is not adopted whole in one day. L1 and L2 differ in how many rules apply; L3 differs in how strongly they are enforced.
The level is declared in the project's configuration and confirmed by machine: you cannot claim L2 without passing L2 — the checker refuses to issue the badge.
bun grain --all --badge[](https://docs.aperturesyndicate.com/en/grain/overview)The five rules people start with
If there is no time to read further — this is L1 in full.
function handleTrackData(data) {
const timeout = 900
// increment counter
const flag = data.views > 0
}function publishTrack(draft) {
const refreshTtlSec = 900
// why: counter moves before the write — otherwise it races the worker
const hasViews = draft.viewCount > 0
}- A function starts with a verb from the lexicon.
handle,process,manage,check,init,updateare banned — they state no action. - A number carries its unit.
refreshTtlSec,priceCents,sizeBytes. A number without a unit is a class of production bugs, not a matter of taste. - A boolean starts with
is/has/can/should/was/will/must. - A comment answers "why" and carries a tag:
why:,perf:,safety:,spec:,ref:. A comment restating the code is deleted. - No file or directory named
utils,helpers,common,misc. "Utilities" have a legal home —*.pure.ts, next to their own domain.
Where to go next
| Page | About |
|---|---|
| One endpoint, before and after | A real handler rewritten, thirteen violations with the price of each |
| Verbs and names | The full lexicon of 33 verbs, units, booleans, banned words |
| Shape, boundaries, failures | The funnel, the limits, where time and randomness may live, two failure models |
| Files and comments | Ten file roles, domain-shaped directories, five comment tags |
| Anti-fingerprint | Why generated code is recognisable, and what the standard leaves alone |
| Enforcement | Linter, checker, pre-commit, levels, badge |
The machine-readable source of truth is a file called grain.synx: the same vocabularies, as data. A rule that is not in it is not checked by machine; a rule that is not in these documents has no meaning. They change together or not at all.