Эта статья — про review-сторону методологии: corpus правил + AI-проверка каждого PR. У той же методологии есть и design-сторона — генерация целых сервисов из спецификации задачи, но это отдельная тема.

У вас уже есть линтер для запятых. У вас нет линтера для агрегатов, доменных событий и границ транзакций. Вот эту дыру и закрывает AI-агент с corpus правил.

Тезис: те решения, которые годами жили в головах техлидов и терялись с их уходом, теперь могут быть версионируемым corpus в репо команды. AI-агент применяет их на каждом PR и цитирует конкретное правило. Не PDF, который никто не читает. Не вики, которая устаревает за квартал. Исполняемый стандарт.

В этой статье:

  • что такое executable engineering standard и чем он отличается от обычного style guide;
  • сравнение с SonarQube, ESLint, традиционным code review (таблица);
  • архитектура «rule corpus + executor» — два слоя, один источник истины;
  • пример AI-обзора PR со ссылками на правила;
  • когда такой стандарт не нужен (честно).

Обязательно

1. Что не работает в существующих подходах

Посмотрим как обычно выглядит «архитектурный стандарт» в команде из 20+ инженеров:

Вариант A — Confluence-страница на 40 экранов. Кто-то техлид написал в 2022. Команда читает один раз при погружении в проект. Через год правила устарели — стек поменялся, появились новые инструменты и подходы. Страницу никто не обновляет. Через два года новые сотрудники ссылаются на неё как на устаревшую документацию.

Вариант B — code review глазами тимлида. Работает в команде из 5. На команде 25 — тимлид становится узким местом. На каждом PR одни и те же комментарии: «инварианты в агрегате, не в Handler-е», «это не value object, добавь equals», «timestamp без TZ — поправь». Через полгода тимлид выгорает или уходит — знание уходит с ним.

Вариант C — линтер (SonarQube, ESLint, Detekt). Ловит стилевые проблемы и баги уровня кода: cyclomatic complexity, unused variables, null pointers. Не ловит архитектурные: границы агрегатов, naming доменных событий, разделение Domain Service vs Application Service. Не должен ловить — линтер для этого слишком плоский.

Все три варианта реальны, ни один не масштабируется.


2. Executable engineering standard как ответ

Идея проста: возьмём то, что хорошо себя показало в линтерах — кодифицированные правила с уникальными идентификаторами — и применим к архитектурным решениям, которые линтеры обычно не достают.

правило = {уникальный код, краткая формулировка, объяснение, пример ОК, пример НЕ ОК}

Где раньше тимлид писал в комменте: «вынеси регистрацию события из Handler-а в корень агрегата», теперь AI-агент пишет:

Нарушение R-AGG-X4 — событие OrderPaid зарегистрировано в OrderHandler, должно регистрироваться в самом корне Order через registerEvent(...). Источник: правило R-AGG-X4 в corpus стандарта команды.

Разработчик кликает ссылку — попадает на правило с примерами и обоснованием. Спорить с правилом → открывает PR в репо стандарта, обсуждает в команде, правило обновляется.

Ключевое отличие от обычного style guide: правило исполняется при каждом PR, цитируется в обзоре с прямой ссылкой, версионируется в git вместе с кодом.


3. Сравнение: SonarQube / ESLint / тимлид-ревью / executable standard

SonarQube / ESLintCode review тимлидомExecutable standard + AI
Что проверяетстиль, security-баги, уровень кодаархитектура, домен, корнер-кейсыархитектура, домен, корнер-кейсы
Кто исполняетстатический анализаторчеловекLLM-агент
Кодифицированоплагины, regex-правилав голове тимлидаmarkdown corpus в репо
Citeable в обзорахда (java:S1234)нет (свободный текст)да (R-AGG-X4)
Масштабируетсяданет (тимлид — узкое место)да
Понимает доменнетдада (через corpus)
Версионируетсячерез релиз linter-аникакчерез git
Открыто/закрытоopen-sourceприватно в головерепо команды
Lifecycle правилрелизы плагиновустные договорённостиgit diff на corpus

Executable standard заполняет пересечение трёх свойств, которое одиночные подходы не дают: понимает архитектуру (как тимлид) + масштабируется (как линтер) + видим/редактируем командой (как git-репозиторий).


4. Архитектура: rule corpus + executor

Стандарт состоит из двух слоёв, один источник истины.

Слой 1: корпус правил markdown в репозитории команды читает Слой 2: ИИ-агент тонкие промпты по этим правилам применяется к Ревью в PR замечания со ссылками на правила

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

Слой 1 — это правила с кодами (R-AGG-3, PG-T-013): прозой для людей, структурой для ИИ, в репозитории команды и под git-ом. Слой 2 — тонкие промпты по полсотни строк, которые вызываются на diff и цитируют правила по коду.

Что важно архитектурно:

  • Правила не зашиты в промпт. Промпт-исполнитель тонкий, 40–50 строк. Правила — толстый corpus: на сентябрь 2026 это семьсот с лишним требований и больше тысячи кодов правил. Это позволяет менять правила, не трогая исполнитель. Аналог — ESLint плагин, где конфиг отдельно от движка.
  • Правила — обычный markdown, не формат «для машины». Тот же файл читает агент как текст и открывает человек как текст. Правило и его объяснение лежат в одном файле, поэтому разойтись между собой не могут.
  • Проверка запускается разработчиком явно. Не «AI ревьюит всё подряд». Разработчик сам зовёт нужный скилл ревью — /ucp-ddd-tactical-review, /ucp-pattern-review — или вешает его на хук. Контроль остаётся у человека.

6. Когда executable standard НЕ нужен

Честно. Подход не для всех.

Не нужен если:

  • Команда меньше ~5 инженеров. Тимлид-ревью покрывает всё, формализация — overhead. Возьмёте, когда команда вырастет.
  • Ранний прототип с частой сменой направления. Архитектура меняется слишком быстро. Кодификация устареет за неделю. Сначала проверьте жизнеспособность продукта, потом стандартизируйте.
  • Кодовая база на 80% — унаследованный код без планов рефакторинга. Применять новые правила к коду 5-летней давности — генератор раздражения. Вводить нужно вместе с новыми сервисами.
  • Команда не доверяет AI-обзорам. Сначала культурная подготовка, потом автоматическая проверка. Иначе проверку выключат, corpus застынет.

Ясно нужен когда:

  • Команда 10+, мульти-сервисный проект. Тимлид-bottleneck реальный, у каждого сервиса архитектура расходится без формализации.
  • Greenfield/новый сервис. Самое выгодное место — записать паттерны, пока они свежие в голове.
  • Текучка/растущая команда. Знание уходит с людьми. Corpus — память команды, которую не унесут с собой.
  • Жёсткий compliance/safety домен. Авто, медицина, финансы — где «у нас принято» недостаточно, нужна аудит-trail почему ревью прошло.

7. Что executable standard НЕ делает

Чтобы избежать завышенных ожиданий:

  • Не пишет архитектуру за вас. Решение про границы агрегатов, выбор глубины моделирования, какой стек — за человеком.
  • Не заменяет архитектора. Обзор — это поддержка решения, не замена; решает человек.
  • Не объявляет себя истиной. Каждое правило в репо — обсуждаемо. Несогласен — открой PR в репо стандарта.
  • Не работает на унаследованном коде без адаптации. Применять R-AGG-X3 к жёстко связанному сервису 2018 года = генерировать боль. Вводить с границы новых модулей.
  • Не ловит баги уровня кода (NPE, deadlock, off-by-one). Это работа SonarQube/ESLint. AI-стандарт — слой ВЫШЕ.

Дополнительно: при первом чтении можно пропустить

Глубже: пример: разбор настоящего PRрасширенное

Допустим, разработчик запушил код, в котором OrderHandler сам публикует событие OrderPaid и изменяет состояние Order через публичные сеттеры вместо бизнес-метода:

public class OrderHandler {
    private final OrderRepository orders;
    private final ApplicationEventPublisher events;

    @Transactional
    public void handle(PayOrder cmd) {
        Order order = orders.findById(cmd.orderId()).orElseThrow();
        order.setStatus(OrderStatus.PAID);
        order.setPaidAt(Instant.now());
        orders.save(order);
        events.publishEvent(new OrderPaid(order.id(), order.total()));
    }
}
func (h *OrderHandler) Handle(ctx context.Context, cmd PayOrder) error {
    order, err := h.orders.FindByID(ctx, cmd.OrderID)
    if err != nil {
        return err
    }
    order.Status = OrderStatusPaid
    order.PaidAt = time.Now().UTC()
    if err := h.orders.Save(ctx, order); err != nil {
        return err
    }
    return h.events.Publish(ctx, OrderPaid{OrderID: order.ID, Total: order.Total})
}
class OrderHandler {
  constructor(
    private readonly orders: OrderRepository,
    private readonly events: EventBus,
  ) {}

  async handle(cmd: PayOrder): 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 OrderPaid(order.id, order.total));
  }
}
from datetime import datetime, timezone

class OrderHandler:
    def __init__(self, orders: OrderRepository, events: EventBus) -> None:
        self._orders = orders
        self._events = events

    async def handle(self, cmd: PayOrder) -> None:
        order = await self._orders.find_by_id(cmd.order_id)
        order.status = OrderStatus.PAID
        order.paid_at = datetime.now(tz=timezone.utc)
        await self._orders.save(order)
        await self._events.publish(OrderPaid(order_id=order.id, total=order.total))

AI-агент после /ucp-ddd-tactical-review выдаст:

Findings (3 critical, 1 minor)

R-AGG-X4 — события регистрирует корень агрегата, а не обработчик (строка 11) Событие OrderPaid опубликовано в OrderHandler, а не в самом Order. Должно: order.pay(...), внутри которого registerEvent(...). Источник: правило R-AGG-X4 в corpus стандарта.

R-ENT-X5 — анемичная модель: состояние меняют снаружи через сеттеры (строки 8–9) setStatus() и setPaidAt() оставляют Order набором полей: правила оплаты живут в обработчике, а не в самой сущности. Изменение состояния — только через бизнес-метод (order.pay(amount)). Источник: правило R-ENT-X5 в corpus стандарта.

R-EVT-X4 — критичный эффект уходит без гарантии доставки (строка 11) ApplicationEventPublisher раздаёт событие подписчикам в памяти. Процесс упал сразу после фиксации транзакции — событие исчезло, и следов не осталось. Такие эффекты идут через надёжную публикацию: событие пишется в таблицу-outbox в той же транзакции, а отправляет его отдельный процесс. Источник: правило R-EVT-X4 в corpus стандарта.

Разработчик видит три замечания, кликает первую ссылку, читает обоснование, переписывает:

public class OrderHandler {
    private final OrderRepository orders;

    @Transactional
    public void handle(PayOrder cmd) {
        Order order = orders.findById(cmd.orderId()).orElseThrow();
        order.pay(cmd.amount());
        orders.save(order);  // save складывает события в outbox в той же транзакции
    }
}

// в Order.java
public void pay(Money amount) {
    if (status == OrderStatus.CANCELLED) {
        throw new IllegalStateException("Cannot pay a cancelled order");
    }
    this.status = OrderStatus.PAID;
    this.paidAt = Instant.now();
    registerEvent(new OrderPaid(id, amount, paidAt));
}
func (h *OrderHandler) Handle(ctx context.Context, cmd PayOrder) error {
    order, err := h.orders.FindByID(ctx, cmd.OrderID)
    if err != nil {
        return err
    }
    if err := order.Pay(cmd.Amount); err != nil {
        return err
    }
    return h.orders.Save(ctx, order) // Save сбрасывает накопленные события через outbox
}

// в order.go
func (o *Order) Pay(amount Money) error {
    if o.Status == OrderStatusCancelled {
        return fmt.Errorf("cannot pay a cancelled order")
    }
    o.Status = OrderStatusPaid
    o.PaidAt = time.Now().UTC()
    o.events = append(o.events, OrderPaid{OrderID: o.ID, Amount: amount, PaidAt: o.PaidAt})
    return nil
}
class OrderHandler {
  constructor(private readonly orders: OrderRepository) {}

  async handle(cmd: PayOrder): Promise<void> {
    const order = await this.orders.findById(cmd.orderId);
    order.pay(cmd.amount);
    await this.orders.save(order); // save сбрасывает накопленные события через outbox
  }
}

// в order.ts
pay(amount: Money): void {
  if (this.status === OrderStatus.CANCELLED) {
    throw new Error('Cannot pay a cancelled order');
  }
  this.status = OrderStatus.PAID;
  this.paidAt = new Date();
  this.recordEvent(new OrderPaid(this.id, amount, this.paidAt));
}
from datetime import datetime, timezone

class OrderHandler:
    def __init__(self, orders: OrderRepository) -> None:
        self._orders = orders

    async def handle(self, cmd: PayOrder) -> None:
        order = await self._orders.find_by_id(cmd.order_id)
        order.pay(cmd.amount)
        await self._orders.save(order)  # save сбрасывает накопленные события через outbox

# в order.py
def pay(self, amount: Money) -> None:
    if self.status == OrderStatus.CANCELLED:
        raise ValueError("Cannot pay a cancelled order")
    self.status = OrderStatus.PAID
    self.paid_at = datetime.now(tz=timezone.utc)
    self._record_event(OrderPaid(order_id=self.id, amount=amount, paid_at=self.paid_at))

Цикл «правило → AI-обзор → ссылка → понимание → правка» занимает ~10 минут вместо «спор в комменте PR с тимлидом → ожидание ответа на 2 дня → разворот → конфликт-merge». Тимлид появляется только если разработчик хочет обсудить само правило.


Коротко

  • Страница в Confluence устаревает, тимлид на команде из двадцати пяти становится узким местом, линтер не видит архитектуры: ни один из трёх способов держать стандарт не масштабируется.
  • Исполняемый стандарт берёт у линтеров кодифицированные правила с идентификаторами и применяет их к архитектурным решениям: код, формулировка, объяснение, пример ОК и пример НЕ ОК.
  • Правило исполняется на каждом PR, цитируется в замечании со ссылкой (R-AGG-X4) и версионируется в git вместе с кодом; спор с правилом это PR в репозиторий стандарта.
  • Два слоя и один источник истины: толстый корпус правил обычным markdown и тонкий промпт-исполнитель на полсотни строк; правила меняют, не трогая исполнитель.
  • Проверку запускает разработчик явно, нужным скиллом ревью или хуком, а не «AI ревьюит всё подряд».
  • Не нужен команде меньше пяти человек, раннему прототипу, унаследованному коду без планов рефакторинга и команде без доверия к AI-обзорам; нужен от десяти инженеров, на новых сервисах, при текучке и в доменах с аудитом.
  • Стандарт не проектирует архитектуру, не заменяет архитектора, не ловит баги уровня кода и не объявляет себя истиной.

Что почитать дальше