Эта статья — про review-сторону методологии (corpus правил + AI-проверка PR). Use Case Pattern также включает design-сторону — генерацию целых сервисов из бизнес-брифа через скиллы ucp-spec-design, ucp-pattern-design, ucp-ddd-tactical-design. См. Use Case Pattern для широкой картины.

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

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

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

  • что такое 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 — тимлид становится bottleneck. На каждой 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(...). Источник: https://vikulin-va.ru/standards/backend/ddd-tactical/#r-agg-x4

Разработчик кликает ссылку — попадает на правило с примерами и обоснованием. Спорить с правилом → открывает 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)
Масштабируетсяданет (тимлид-bottleneck)да
Понимает доменнетдада (через corpus)
Версионируетсячерез релиз linter-аникакчерез git
Открыто/закрытоopen-sourceприватно в головерепо команды
Lifecycle правилрелизы плагиновустные договорённостиgit diff на corpus

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


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

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

┌─────────────────────────────────────────────────┐
│  Слой 1: rule corpus (markdown)                 │
│                                                 │
│  • правила с кодами `R-AGG-3`, `PG-T-013`, ... │
│  • прозой для людей, structured для AI          │
│  • живут в репо: site/ + .claude/docs/          │
│  • версионируются git-ом                        │
└─────────────────────────────────────────────────┘
                       ↓ читает
┌─────────────────────────────────────────────────┐
│  Слой 2: AI-skills (executor)                   │
│                                                 │
│  • маленькие промпты-скиллы (~50 строк каждый)  │
│  • вызываются на git diff команды               │
│  • цитируют правила по коду                     │
│  • живут в .claude/skills/                      │
└─────────────────────────────────────────────────┘
                       ↓ применяется к
┌─────────────────────────────────────────────────┐
│  PR команды → review с конкретными ссылками    │
└─────────────────────────────────────────────────┘
                       ↓ кликаешь ссылку →
                  правило на сайте

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

  • Правила не зашиты в скилл. Скилл — тонкий, ~50 строк промпта. Правила — толстый corpus (~600+ кодов). Это позволяет менять правила, не трогая скиллы. Аналог — ESLint плагин, где конфиг отдельно от движка.
  • Один источник истины — два render-а. Тот же .md файл через парсер рендерится в HTML на сайте (для людей) и читается AI как plain text (для LLM). Никогда не расходятся.
  • Скиллы вызываются разработчиком явно. Не «AI ревьюит всё подряд». Команда claude review или хук — на выбор. Контроль остаётся у человека.

5. Пример: real PR review

Допустим, разработчик запушил код, в котором 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 после claude review выдаст:

Findings (3 critical, 1 minor)

R-AGG-X4 — антипаттерн: регистрировать события вне корня агрегата (line 12) Событие OrderPaid опубликовано в OrderHandler, а не в самом Order. Должно: order.pay(...) внутри которого registerEvent(...). Источник: https://vikulin-va.ru/standards/backend/ddd-tactical/#r-agg-x4

R-ENT-X3 — антипаттерн: публичные сеттеры для всех полей (line 11) setStatus() и setPaidAt() нарушают инкапсуляцию. Изменение состояния — только через бизнес-методы (order.pay(amount)). Источник: https://vikulin-va.ru/standards/backend/ddd-tactical/#r-ent-x3

R-EVT-X4 — антипаттерн: AFTER_COMMIT для критичных эффектов (line 13) Публикация события без транзакционных гарантий — событие потеряется при падении после commit. Нужен Outbox или синхронная доставка в той же транзакции. Источник: https://vikulin-va.ru/standards/backend/ddd-tactical/#r-evt-x4

Разработчик в IDE видит 3 finding-а, кликает первую ссылку, читает обоснование, переписывает:

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 публикует события через DomainEventPublisher
    }
}

// в Order.java
public void pay(Money amount) {
    if (status != OrderStatus.CONFIRMED) {
        throw new IllegalStateException("Cannot pay: " + status);
    }
    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 != OrderStatusConfirmed {
        return fmt.Errorf("cannot pay order in status %s", o.Status)
    }
    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.CONFIRMED) {
    throw new Error(`Cannot pay order in status ${this.status}`);
  }
  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.CONFIRMED:
        raise ValueError(f"Cannot pay order in status {self.status}")
    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». Тимлид появляется только если разработчик хочет обсудить само правило.


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

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

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

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

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

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

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

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

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

8. Дальше