Эта статья — про 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 / ESLint | Code 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
Стандарт состоит из двух слоёв, один источник истины.
Правила живут текстом рядом с кодом, агент читает их и разбирает по ним 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-обзорам; нужен от десяти инженеров, на новых сервисах, при текучке и в доменах с аудитом.
- Стандарт не проектирует архитектуру, не заменяет архитектора, не ловит баги уровня кода и не объявляет себя истиной.
Что почитать дальше
- Где граница между линтером, тимлидом и AI-проверкой — почему это разные слои
- Как ревьюить код, который написал AI — обратная сторона того же цикла
- AI пишет код. Зачем тогда методология? — почему corpus правил важнее генерации