Самый частый вопрос про исполняемый стандарт: «это же просто SonarQube?». Нет. Но и не вместо. Эти инструменты живут на разных уровнях абстракции и дополняют друг друга, а не конкурируют.

Эта статья — таблица сравнения по 12 параметрам и разбор «когда что использовать».

Резюме за 30 секунд

  • SonarQube / ESLint / Detekt — для багов уровня кода и стиля. Регексы, AST-анализ, безопасность.
  • Code review тимлидом — для архитектуры и домена. Не масштабируется на команду 20+.
  • Исполняемые правила агента с набором правил — для архитектуры и домена. Масштабируются. Дополняют линтеры, не заменяют.

Все три должны работать одновременно в зрелой команде. Линтер ловит NPE, исполняемое правило ловит «событие зарегистрировано не в корне агрегата», человек смотрит на «правильно ли вообще выбрана граница агрегата».

Полная таблица сравнения

ПараметрSonarQube/ESLintТимлид-ревьюИсполняемые правила агента
Что проверяетстиль, security-баги, complexityархитектура, доменархитектура, домен
Глубина пониманиясинтаксис + ASTсемантика + контекстсемантика + контекст (через набор правил)
Кто исполняетстатический анализаторчеловекLLM-агент
Кодифицированоплагины + конфигв головеmarkdown в репо
Ссылка в обзореда (java:S1234)нетда (R-AGG-4)
Масштабируетсяданет (узкое место)да
Задержка обзорасекунды (CI)дни (тимлид reads)минуты (claude run)
False positivesсредненизкосредне
Понимает доменнетдада
Версионируетсярелиз плагинаустные правилаgit diff на наборе правил
Открытостьopen-source движок, часть проверок — только в платных редакцияхв головерепо команды
Lifecycle правилрелизы плагиновнеформальноgit, semantic diff

Где они НЕ конкурируют

Возьмём один и тот же PR — handler оплаты заказа:

public void handle(PayOrder cmd) {
    Order order = orders.findById(cmd.orderId()).get();
    order.setStatus(OrderStatus.PAID);
    order.setPaidAt(Instant.now());
    orders.save(order);
    events.publishEvent(new OrderPaid(order.id(), order.total()));
}
func (h *PayOrderHandler) Handle(ctx context.Context, cmd PayOrderCommand) error {
    order, err := h.orders.FindByID(ctx, cmd.OrderID)
    if err != nil {
        return err
    }
    order.Status = OrderStatusPaid
    order.PaidAt = time.Now()
    if err := h.orders.Save(ctx, order); err != nil {
        return err
    }
    return h.events.Publish(ctx, OrderPaidEvent{ID: order.ID, Total: order.Total})
}
async handle(cmd: PayOrderCommand): Promise<void> {
    const order = await this.orders.findById(cmd.orderId);
    order.status = OrderStatus.PAID;
    order.paidAt = new Date();
    await this.orders.save(order);
    await this.events.publish(new OrderPaidEvent(order.id, order.total));
}
from datetime import datetime, timezone

def handle(self, cmd: PayOrderCommand) -> None:
    order = self.orders.find_by_id(cmd.order_id)
    order.status = OrderStatus.PAID
    order.paid_at = datetime.now(tz=timezone.utc)
    self.orders.save(order)
    self.events.publish(OrderPaidEvent(id=order.id, total=order.total))

SonarQube скажет:

  • java:S3655 — Optional.get() без isPresent() — может бросить

И на этом всё. Что заказ переводят в оплаченный сеттерами, а событие уходит мимо транзакции, он не заметит: это не его уровень.

Исполняемое правило скажет:

  • R-AGG-X4 — событие зарегистрировано не в корне агрегата
  • R-ENT-X3 — состояние меняют публичным сеттером, а не бизнес-методом корня
  • R-EVT-X3 — событие публикуется прямо из хендлера, мимо корня и транзакционных гарантий

Тимлид скажет:

  • «Этот use case вообще должен быть Order.pay() или application service PaymentSaga? У нас же платёж асинхронный и приходит через webhook.»

Три уровня. Три инструмента. Все три нужны.

Когда что брать

Дальше — ориентиры, а не закон: размер команды тут удобная точка отсчёта, но решает не он, а то, насколько сложен домен и насколько договорённости успели устояться.

Только линтер

  • Команда небольшая, несколько человек.
  • Простой CRUD-проект, не доменно-сложный.
  • Архитектурных договорённостей ещё нет — кодифицировать пока нечего.

Линтер + тимлид-ревью

  • Команда до десятка человек.
  • Есть техлид с устоявшейся архитектурой.
  • Архитектура держится на людях, не формализована.

Линтер + исполняемые правила + тимлид

  • Команда больше десятка человек.
  • Несколько сервисов, риск drift между ними.
  • Архитектурные паттерны достаточно устоялись, чтобы их кодифицировать.

Линтер + тимлид (без исполняемых правил)

  • Ранний прототип, всё меняется еженедельно.
  • Команда не доверяет AI-обзорам.
  • Архитектура ещё не устоялась.

Что общего у всех трёх

Все три — это инструменты обзора, а не замены ответственности. Линтер не пишет код за вас. Тимлид не подписывает PR за разработчика. Исполняемое правило не принимает архитектурные решения. Все они — усилители, не подменители.

Качество ревью на любом из трёх уровней зависит от качества исходных стандартов:

  • ESLint без хорошей конфигурации — шум.
  • Тимлид без устоявшихся принципов — субъективные капризы.
  • Исполняемое правило без хорошего набора правил — обобщённый обзор уровня «улучши читаемость».

Качественный набор правил — то, что отличает зрелую команду от слепого копирования чужих практик.

Что исполняемые правила делают, чего не делают линтеры

Есть один класс правил, который традиционные линтеры принципиально не достанут:

  1. Правила про доменный слой. «Имя value object — существительное в единственном числе». Линтер не знает что такое value object и какое имя «доменное», а какое нет.
  2. Правила про границы. «Один use case изменяет один агрегат, остальные через события». Линтер не знает где проходят границы агрегатов в вашей доменной модели.
  3. Правила про намерение. «Имя класса соответствует доменному термину из ubiquitous language». Линтер не знает ubiquitous language.
  4. Правила про контекст. «В одних проектах маппинг идёт через отдельный mapper-класс, в других — через сгенерированный». Линтер не знает соглашения конкретного проекта и не выбирает правила в зависимости от них.

LLM умеет всё это потому что читает прозу правил вместе с прозой кода. Для LLM «доменный термин» и «не доменный» — это вопрос контекста, а не AST-узла.

Что линтеры делают, чего не делают исполняемые правила

И обратное:

  1. Скорость. Линтер по изменённым файлам отрабатывает за секунды, полный прогон по сотне тысяч строк — минуты. Исполняемое правило — десятки секунд на PR.
  2. Детерминированность. Линтер на одном и том же коде даст один и тот же результат. LLM — с вариациями.
  3. Низкоуровневые баги. NPE, deadlock, off-by-one. Это работа AST-анализатора.
  4. Security-сканирование. SQL-инъекции, XSS, зашитые в код секреты — паттерны более чёткие. Одна оговорка: инъекции ищут прослеживанием пути данных от входа до запроса, и в SonarQube это есть только в платных редакциях — бесплатная Community такого не умеет.

Дальше