The APERTURESyndicate code standard — and the checker that enforces it.
Film grain is 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 survives any formatter and reads like handwriting. Code written to GRAIN is recognisable in a stranger's repository within one screen.
- A closed lexicon of 33 verbs, each with a contract:
find*may returnnull,get*never does,list*is always a collection. - A number carries its unit (
refreshTtlSec,priceCents); a boolean carries its prefix (is,has,can, …). - One file — one role, and the role is in the name:
invoice.store.ts,token.policy.ts,cover.pure.ts. Noutils, nohelpers, noindex. - Comments answer why and start with a tag:
why:perf:safety:spec:ref:. NoTODO, no dividers, no retelling of the next line. - Time, randomness and
process.envlive only at the boundary; dependencies point inward; independentawaits run throughPromise.all.
The full standard: docs.aperturesyndicate.com/en/grain/overview (English) · docs/ (Russian, the canonical text). The one-page card is docs/CARD.md; the twelve laws that fit into an assistant's prompt are docs/SMALL.md.
The standard is held by a machine, not by memory. grain reads the same vocabulary file the standard is written from (grain.synx) and checks TypeScript, JavaScript, Rust, SQL, Kotlin, Swift and SYNX: names, contracts, file roles, directory shape, comments, event subjects, config keys. For TypeScript it runs its own 17 AST rules through ESLint as a library — the project needs no ESLint config of its own.
Requires Bun.
bun add -d @aperturesyndicate/grainbun grain # staged files — what the pre-commit hook runs
bun grain --all # the whole repository
bun grain src/billing # one path, no gate
bun grain --all --baseline # record the current debt
bun grain --audit # is the declared level actually earned
bun grain --all --stat # debt map by module
bun grain --explain <rule> # what a rule checks
bun grain --json # machine-readable output for CIA standard cannot be switched on "starting tomorrow": existing code breaks it in thousands of places, and a gate that fails on everything is removed by day two. So the debt is recorded as numbers:
bun grain --all --baseline # writes baseline.synx: <file> <rule> <count>From then on the gate lets exactly that debt through and fails on anything new — new code holds the standard from its first line, old code is repaid a module at a time. Line numbers are deliberately not stored: they shift on any edit above and would turn the debt into a source of false positives.
The package ships the full vocabulary. A project overrides only what differs, in its own grain.synx at the repository root — a key is replaced whole:
level 3
adopt src scripts
paths_ignored node_modules dist src/db/migrations
synx_config app.synx
units_domain Px Deg Hz Bpm Db Energy
Every key and its meaning is documented inline in grain.synx. Environment overrides: GRAIN_ROOT, GRAIN_CONFIG, GRAIN_BASELINE.
| Level | What it means |
|---|---|
| L1 Readability | Verbs, units, boolean prefixes, comment form, no dumping-ground files. Adopted in an evening. |
| L2 Contracts | The full standard: find/get contracts, file roles, boundaries, inward imports, dead exports, failure codes. |
| L3 Discipline | The rules of L2 plus a guarantee: a blocking pre-commit hook, a check in CI, adoption covering all the code. grain --audit verifies the guarantee and refuses to let a project claim a level it does not hold. |
# .githooks/pre-commit
#!/usr/bin/env bash
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
[ "${GRAIN_SKIP:-0}" = "1" ] && exit 0
bun graingit config core.hooksPath .githooks # once per clone# .github/workflows/grain.yml
name: grain
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun grain --all
- run: bun grain --auditA rule may be broken deliberately, but not silently. The comment covers the named rule on its own line and the next one; the reason is mandatory.
// grain:allow unit-suffix — field of an external Stripe contract, cannot be renamedFor editor highlighting the same rules are available as a flat config:
// eslint.config.js
import { grainConfig } from '@aperturesyndicate/grain'
export default [grainConfig]grainLevel1Config limits it to L1; grainMigrationConfig keeps GRAIN's own rules as errors and lowers the built-in shape limits to warnings for code that is migrating.
grain.synx the vocabularies as data — the single source the standard, the rules and the docs are built from
bin/ the checker entry point and the docs generator
src/ checks by language, the ESLint rules, the baseline ledger
docs/ the standard (Russian): overview, per-language chapters, glossary, walkthrough
test/ the rules' self-test: bun test
baseline.synx this repository's own recorded debt
docs/CARD.md and docs/SMALL.md are generated from grain.synx by bun run docs — do not edit them by hand. A rule that is not in grain.synx is not checked; a rule that is not in the docs makes no sense. Both change together or neither does.
GRAIN is versioned as a whole. Adding to a vocabulary is a minor version; removing a verb, a role or a unit is a major one, because it breaks existing code. See CHANGELOG.md.
MIT — the text of the standard and the checker alike.