Внедрение

Last updated: 2026-08-18Как стандарт перестаёт быть текстом: словари как данные, ESLint-плагин, чекер для любого языка, блокирующий pre-commit, уровни и бейдж.

Внедрение

Стандарт, который держится на памяти автора, умирает за месяц. GRAIN проверяется машинно на трёх уровнях, и все три читают один и тот же файл словарей.

grain.synx            словари как данные: глаголы, единицы, роли, пределы, уровни
   ├── @as/style      ESLint-плагин: 17 правил по AST для TypeScript
   ├── grain          чекер: имена файлов, комментарии, Rust, SQL — любой язык
   └── pre-commit     блокирует коммит по списку внедрения

Уровни и бейдж

Проект объявляет уровень ключом level в своём grain.synx. Чекер проверяет ровно правила этого уровня — и не выдаст бейдж, если проект заявил больше, чем держит.

bash
bun grain --all --level 1   # проверить только L1
bun grain --all --badge     # markdown-бейдж, если чисто

L3 отличается от L2 не правилами, а гарантией: проверка в CI, блокирующий pre-commit, adopt покрывает весь код, ни одного GRAIN_SKIP в истории. Это то, что нельзя проверить одним запуском линтера, но видно в репозитории.

Команды

bash
bun grain                  # проиндексированные файлы — то же, что делает хук
bun grain --all            # весь репозиторий по списку внедрения
bun grain --all --force    # весь репозиторий, игнорируя список внедрения
bun grain src/billing      # конкретный путь, без гейта
bun grain --json           # машинный вывод для CI

ESLint

js
// eslint.config.js
import { grainConfig } from '@as/style'

export default [grainConfig]

Для сервиса, который мигрируется постепенно, — grainMigrationConfig: собственные правила GRAIN остаются ошибками, встроенные ограничения формы понижаются до предупреждений.

ПравилоЧто ловит
grain/verb-lexiconГлагол не из словаря или запрещённый
grain/find-get-contractfind* без 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-actcatch, который логирует и продолжает
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

bash
git config core.hooksPath .githooks   # один раз на клон

Хук проверяет только проиндексированные файлы и только пути из списка внедрения. Аварийный обход — GRAIN_SKIP=1 git commit …; обход остаётся в истории оболочки, а не в коде.

Список внедрения

Легаси не переписывается разом. В grain.synx есть ключ adopt — список путей, на которых GRAIN обязателен:

synx
adopt src/billing

Правило простое: новый модуль создаётся сразу по GRAIN и добавляется в список в том же коммите. Существующий переезжает, когда до него доходят руки, — сервис целиком, не по файлу: полумигрированный каталог хуже немигрированного, потому что перестаёт быть предсказуемым.

Порядок миграции сервиса:

  1. Переименовать файлы по ролям и разложить по доменам — механическая правка, её видно в git mv.
  2. Прогнать grain <путь> --force, починить имена и комментарии.
  3. Подключить grainConfig в eslint.config.js, починить контракты find/get и форму функций.
  4. Добавить путь в adopt, закоммитить одним изменением.

Escape-хатч

Правило можно нарушить осознанно, но не молча:

ts
// grain:allow unit-suffix — поле внешнего контракта Stripe, переименовать нельзя

Причина обязательна — без неё чекер ругается на сам escape. Escape без причины и GRAIN_SKIP в CI приравниваются к нарушению.

Изменение стандарта

grain.synx и документы стандарта меняются одним коммитом. Пополнение словаря — минорная версия; удаление глагола, роли или единицы — мажорная, потому что ломает существующий код.

Внедрение | AS Docs