Внедрение
Стандарт, который держится на памяти автора, умирает за месяц. GRAIN проверяется машинно на трёх уровнях, и все три читают один и тот же файл словарей.
grain.synx словари как данные: глаголы, единицы, роли, пределы, уровни
├── @as/style ESLint-плагин: 17 правил по AST для TypeScript
├── grain чекер: имена файлов, комментарии, Rust, SQL — любой язык
└── pre-commit блокирует коммит по списку внедренияУровни и бейдж
Проект объявляет уровень ключом level в своём grain.synx. Чекер проверяет ровно правила этого уровня — и не выдаст бейдж, если проект заявил больше, чем держит.
bun grain --all --level 1 # проверить только L1
bun grain --all --badge # markdown-бейдж, если чистоL3 отличается от L2 не правилами, а гарантией: проверка в CI, блокирующий pre-commit, adopt покрывает весь код, ни одного GRAIN_SKIP в истории. Это то, что нельзя проверить одним запуском линтера, но видно в репозитории.
Команды
bun grain # проиндексированные файлы — то же, что делает хук
bun grain --all # весь репозиторий по списку внедрения
bun grain --all --force # весь репозиторий, игнорируя список внедрения
bun grain src/billing # конкретный путь, без гейта
bun grain --json # машинный вывод для CIESLint
// eslint.config.js
import { grainConfig } from '@as/style'
export default [grainConfig]Для сервиса, который мигрируется постепенно, — grainMigrationConfig: собственные правила GRAIN остаются ошибками, встроенные ограничения формы понижаются до предупреждений.
| Правило | Что ловит |
|---|---|
grain/verb-lexicon | Глагол не из словаря или запрещённый |
grain/find-get-contract | find* без null в типе, get* с null |
grain/boolean-prefix | Булево без is/has/can/should/was/will/must |
grain/unit-suffix | Число без единицы измерения |
grain/no-vague-name | Пустые слова, сокращения, самопохвала в именах |
grain/comment-form | Комментарий без тега, TODO, разделители, эмодзи |
grain/no-emoji-log | Эмодзи в логах |
grain/catch-must-act | catch, который логирует и продолжает |
grain/no-redundant-temp | Переменная на одну строку до return (автофикс) |
grain/no-default-export | Экспорт по умолчанию |
grain/fail-code | Код отказа не в форме домен.предмет.причина |
grain/file-role | Роль в имени файла и kebab-case |
grain/no-dump-file | Файлы и каталоги-свалки (utils, helpers, common) |
grain/dir-depth | Иерархия каталогов глубже предела |
grain/boundary-effect | Время, случайность, process.env внутри домена |
grain/inward-import | Импорт наружу: домен узнал про I/O или про свою границу |
grain/explicit-parallel | Независимые await по очереди вместо Promise.all |
Плюс встроенные правила ESLint, задающие форму: max-depth, max-params, max-lines-per-function, max-lines, no-else-return, no-nested-ternary, no-lonely-if — их пределы берутся из grain.synx, а не пишутся руками.
Чекер grain
Работает без установленных зависимостей (собственный ридер SYNX) — поэтому годится для pre-commit в свежем клоне. Покрывает то, чего ESLint не видит: .rs, .sql, имена файлов и каталогов, длину файлов, комментарии в любом языке.
Что машина проверить не может
Стандарт, который врёт про свои возможности, разваливается на первом ложном срабатывании. Поэтому честно:
| Правило | Состояние проверки |
|---|---|
| Мёртвый код | Частично. no-dead-export находит экспорт, который никто не упоминает в этом же репозитории, и только в режиме --all. Имя, вызываемое из другого сервиса или динамически, здесь неотличимо от мёртвого |
| Публичная поверхность по требованию | Нет. Требует знания всех потребителей пакета — проверяется на ревью |
| Комментарий отвечает «почему», а не «что» | Частично. Машина видит отсутствие тега, но не может отличить осмысленное «почему» от бессмысленного |
Воронка guard → acquire → derive → effect | Косвенно. Через вложенность, длину и число аргументов |
find* / get* без аннотации типа | Нет. Без объявленного типа возврата контракт не виден машине |
Pre-commit
git config core.hooksPath .githooks # один раз на клонХук проверяет только проиндексированные файлы и только пути из списка внедрения. Аварийный обход — GRAIN_SKIP=1 git commit …; обход остаётся в истории оболочки, а не в коде.
Список внедрения
Легаси не переписывается разом. В grain.synx есть ключ adopt — список путей, на которых GRAIN обязателен:
adopt src/billingПравило простое: новый модуль создаётся сразу по GRAIN и добавляется в список в том же коммите. Существующий переезжает, когда до него доходят руки, — сервис целиком, не по файлу: полумигрированный каталог хуже немигрированного, потому что перестаёт быть предсказуемым.
Порядок миграции сервиса:
- Переименовать файлы по ролям и разложить по доменам — механическая правка, её видно в
git mv. - Прогнать
grain <путь> --force, починить имена и комментарии. - Подключить
grainConfigвeslint.config.js, починить контрактыfind/getи форму функций. - Добавить путь в
adopt, закоммитить одним изменением.
Escape-хатч
Правило можно нарушить осознанно, но не молча:
// grain:allow unit-suffix — поле внешнего контракта Stripe, переименовать нельзяПричина обязательна — без неё чекер ругается на сам escape. Escape без причины и GRAIN_SKIP в CI приравниваются к нарушению.
Изменение стандарта
grain.synx и документы стандарта меняются одним коммитом. Пополнение словаря — минорная версия; удаление глагола, роли или единицы — мажорная, потому что ломает существующий код.