Правила ревью, которые ваш линтер проверить не может
Вот дифф из нашего учебного репозитория (на GitLab или на GitHub): ESLint молчит, семнадцать тестов проходят, TypeScript доволен — а скидка по купону считается как (net.amount * percent) / 100, и счёт на 9,99 € тихо обзаводится долей цента. Ревью нашло это за полторы минуты и оценило как 🔴 critical, потому что команда однажды записала одну строку: «Amounts live in integer minor units…» — деньги живут целыми в минорных единицах.
Эта строка и есть тема страницы. Не «настройте секцию правил», а какие из договорённостей вашей команды вообще стоит записывать, как их сформулировать, чтобы ревью по ним действительно работало, и чего оно с ними не сделает, как ни формулируй.
Перед началом: нужен репозиторий с .reviewgate/config.yml — если его ещё нет, откуда он берётся, показано в первом ревью на своей машине. Посмотреть правила: reviewgate rules.
Правило — не настройка, а фраза, которую вы отправляете модели
У правила ровно три поля: id, description, severity (без него — major). Текст уходит в системный промпт дословно, по строке на правило:
- [critical] money-in-minor-units: Amounts live in integer minor units…
- [major] no-customer-data-in-logs: Logs carry identifiers and outcomes…Два следствия, которых обычно не ждут. Модель видит написанный вами уровень — то есть это не просто пост-обработка, уровень влияет на то, как замечание будет оценено изначально. И тот же блок уходит генератору, каждому судье и арбитру: разросшийся свод правил оплачивается на каждом вызове каждого прогона, а не однажды.
Фильтр: три места, где может жить договорённость
Прежде чем писать правило, подумайте, что могло бы проверить эту договорённость. Ответов всего три — линтер, тесты или rules[], — и только последний проверяет, понимая смысл.
| Договорённость | Куда её | Почему |
|---|---|---|
«никакого any вне тестов», «===, а не ==», «промис без обработки» | линтер | это машина проверяет детерминированно, бесплатно и за миллисекунды — а часть ещё и чинит сама |
| «скидка 10% от 24,80 даёт 2,48», «счёт, выписанный вечером 31-го по времени клиента, попадает в этот месяц, а не в следующий по UTC» | тесты | это поведение, а поведение выражается утверждением, а не фразой |
| «деньги живут целыми в минорных единицах», «каждое чтение из базы отфильтровано по клиенту, от имени которого пришёл запрос», «в логах идентификаторы, а не данные клиента» | rules[] | этого не проверит ни один инструмент: нужно понимать, что делает код и зачем |
Граница проходит не по «важно или неважно», а по «может ли инструмент проверить это, не понимая смысла». Правило, повторяющее линтер, стоит денег на каждом вызове и не даёт ничего: линтер уже сказал то же самое, быстрее и бесплатно.
Есть и четвёртый источник — тот, который дублируют: пресет. preset: nestjs или preset: angular уже кладёт в промпт страницу инструкций — необработанные промисы, дефолты через || там, где валидны falsy-значения, ловушки конкретного фреймворка. Сам текст пресета наружу не отдаётся: команда ниже покажет только его имя, а что именно проверяет каждый пресет, перечислено в таблице пресетов справочника конфига.
reviewgate rulesИсключения оговаривайте в самом правиле, а не отдельной настройкой
Правило, написанное без исключений, спорит с вашей же архитектурой: ревью снова и снова ругает решение, которое команда приняла сознательно, и за каждое такое ложное замечание вы платите столько же, сколько за настоящее. Одна фраза внутри текста правила это исправляет — исключение теперь живёт там, где его читает модель. Этот приём заменит большую часть того, что вы положите в dont_flag.
- id: no-direct-db-access
description: "Services do not touch the database directly"- id: no-direct-db-access
description: "Services go through a repository. Exceptions: migrations and seed scripts
under db/, where direct access is intentional."Скажите, что считать доказательством
Правило про то, чего в коде не видно, — отсутствующий файл, ненаписанный тест, договорённость про дифф целиком, — модель проверить не может. Если правило не говорит, что считать доказательством, она решит это сама: в учебном репозитории правило «изменения ставок налога должны быть задокументированы для финансовой команды» дало замечание с требованием комментария со ссылкой на тикет — этого никто не просил, и проверить это нельзя. То же правило с названным доказательством даёт замечание ровно о том, чего не хватает: отсутствие README.md среди изменённых файлов и есть доказательство, а не гипотеза.
- id: pricing-changes-documented
description: "Every change to pricing rules (discounts, tax rates) must be documented
for the finance team."🟠 src/pricing/pricing.service.ts:18 — Добавлена новая налоговая ставка для Швейцарии
(CH: 0.081) без сопровождающей документации для финансовой команды.
(team:pricing-changes-documented)
Добавить комментарий с ссылкой на источник/тикет finance-команды, подтверждающий
ставку 8.1% для CH, либо приложить подтверждение в описании MR.- id: pricing-changes-documented
description: "Every change to src/pricing/ must come with a change to README.md in the
same diff. README need not be read: the absence of README.md among the changed files
IS the evidence, not a hypothesis."🟠 src/pricing/pricing.service.ts:19 — Изменение в src/pricing/ (добавлена налоговая
ставка для CH) внесено без сопутствующего изменения README.md в этом же диффе.
По правилу команды отсутствие README.md среди изменённых файлов само по себе
является нарушением. (team:pricing-changes-documented)
Добавить в этот же MR изменение README.md, описывающее добавленную ставку для CH.id — это связка, а не имя
Когда ruleId замечания точно совпадает с правилом, происходят три вещи: замечание получает ваш уровень, помечается в сводке как правило команды, и модели предписано не занимать ваш id ничем другим. Поэтому id должен совпадать буква в букву: лишний пробел или переименованное правило лишают вас всех трёх, и никакого предупреждения не будет.
severity — потолок, а не гарантия. Ваш уровень применяется до судейства, и судья вправе его понизить — повысить не вправе. Поэтому severity: blocker, написанный ради гейта, может приехать как minor и пропустить ветку. Если правило обязано держать гейт, его текст должен быть таким, чтобы судья, читающий файл целиком, согласился с уровнем, а не только с ярлыком.
Длина оплачивается на каждом вызове. Ограничения на число правил и длину текста нет. Есть счёт: блок едет генератору, каждому судье и арбитру. Десять точных фраз лучше сорока расплывчатых вдвойне — они и стоят меньше, и оставляют модели меньше простора для трактовки.
Чего правила не умеют
У правила нет области действия. В rules[] нельзя указать пути: правило применяется ко всему диффу или не применяется вовсе. Если оно касается только части кода, впишите границу в сам текст: «в слое api/…», «для файлов под legacy/ это не действует». Оговорка сработает: модель видит путь каждого файла в диффе.
Одно правило не покроет два стека. Пресет один на репозиторий, пресетов по каталогам нет. В монорепе пресет берут под тот стек, где ошибка дороже, а особенности второго описывают в review_prompt (mode: extend добавляет ваш текст к дефолтному гайдлайну, replace ставит его вместо дефолта).
Четыре способа сказать «не ругайся»
| Инструмент | Где применяется | Экономит токены | Знает пути |
|---|---|---|---|
| ignore | до вызова модели — файл вообще не попадает в промпт | да | да, globs |
| reviewgate-ignore-file | так же, но решает сам файл | да | по файлу |
| dont_flag | фраза в промпте с просьбой не поднимать класс замечаний | нет | нет |
| min_severity | после ответа: тихие замечания сворачиваются в сводку | нет | нет |
Строка, которую читают неправильно, — dont_flag. Это не фильтр: после ответа ничего не удаляется. Это текст в промпте: генератору сказано не поднимать замечания, попадающие под эти допущения, а судье — отбрасывать те, что им противоречат. Работает хорошо и стоит ровно столько же, сколько любая другая фраза в промпте. Если цель — деньги, помогают только две верхние строки.
Строка, которая удивляет, — min_severity. Кажется, что это фильтр шума в ленте, но приглушённые замечания уходят и из гейта: при min_severity: major прогон с пятью замечаниями 🔵 minor не остановит ни коммит, ни MR, какой бы порог гейта вы ни поставили. И считают их по-разному: строка вердикта CLI скажет 0, перечислив все пять с пометкой «тихая», а сводка бота в MR или PR посчитает все пять и свернёт в блок. Одна политика, два числа.
tests: optional велит модели не отмечать отсутствие тестов как проблему — чтобы не раздражать команду замечаниями о покрытии. Но правило команды сильнее: с этим флагом правило «новый сервис приходит с тестом» всё равно сработает — в учебном репозитории ревью подняло недостающий тест, и судья его оставил. Хотите тишины о тестах — не пишите таких правил; хотите проверять их выборочно — оставьте флаг и напишите правило.Как выглядит сработавшее правило команды
Тот же дифф со скидкой по купону, что в начале страницы, но в репозитории, где правила есть:
🔴 src/pricing/pricing.service.ts:52 — couponDiscount вычисляет сумму купона как
(net.amount * percent) / 100 напрямую, без округления через общий хелпер
multiply/roundHalfUp. Результат может быть нецелым числом минорных единиц
(например, net=999, percent=7 → 69.93), что нарушает инвариант Money.amount
(целые центы). (team:money-in-minor-units)
return multiply(net, percent / 100);Хвост в скобках — это и есть суть: замечание не мнение модели, а ваша договорённость, процитированная у строки, которая её нарушает. Обсуждения в ревью заканчиваются быстрее, когда не нужно спорить, важно это или нет: команда уже решила.
Прогоните reviewgate rules после каждой правки конфига по причине: сломанное правило выпадает достаточно тихо, вы этого даже не заметите. У правила два обязательных поля — id и description; нет любого из них или поле названо иначе — правило пропускается целиком. Заметка в сводке есть, но она одна строка среди многих, и больше ничего не меняется: ревью идёт, отчёт выглядит обычным, правила в нём просто нет. Первая строка вывода команды показывает, сколько правил загружено (rules=6); если число меньше, чем правил в файле, одно из них не разобралось — вот и вся проверка. Дубли id хуже: к модели уезжают оба правила, но побеждает severity последнего, без единого предупреждения.
Когда что-то пошло не так
Правило написано, но ни одно замечание на него не ссылается. Три возможные причины, по убыванию вероятности: правило выброшено при разборе — проверьте reviewgate rules; id не совпадает буква в букву с тем, что выдала модель; правило сформулировано так расплывчато, что модель не связала с ним замечание, — напишите в тексте правила, что именно считать нарушением.
Ревью всё время указывает на легаси, которое вы и не собирались чинить. У правила нет оговорки об исключениях, а области по путям у правил нет. Впишите границу в сам текст правила — «под legacy/ это не действует», — вместо ещё одной строки в dont_flag.
Каждый прогон предупреждает о вашем конфиге. У правила ровно три поля; любое другое — globs, prompt, files — даёт заметку при каждом прогоне. Чаще всего такие поля дописывает ИИ-помощник, которому поручили править конфиг: он придумывает схему, какой она «должна быть», а не берёт ту, что есть. Ключа для области по путям нет намеренно: правило действует на весь дифф, а границу пишут в его тексте.
Правило с blocker не держит гейт. Либо судья понизил уровень — понижать ваш уровень он вправе, повышать нет, — либо min_severity стоит выше порога гейта и убирает замечание из гейта.
Дальше
- Первое ревью на своей машине— откуда берётся конфиг
- Лесенка хуков— чтобы гейт действовал по этим правилам
- Справочник конфига— все ключи, включая dont_flag, min_severity и review_prompt