← назад к разделу

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

Для этого в Use Case Pattern есть Уровень 0 — первая ступень лестницы уровней, по которой сервис растёт от слоёного монолита до полной изоляции ядра.

Обязательно

Когда нет документации, а сервис уже есть

В реальных проектах часто так: сервис написали быстро, документацию отложили на потом — и «потом» не пришло. Новый разработчик тратит недели, чтобы понять, какие таблицы к чему относятся, какие статусы у заказа и откуда берётся ограничение на сумму.

Уровень 0 решает именно эту проблему: восстановить структурированное описание сервиса прямо из кода — без встреч с аналитиками, без бизнес-брифа. Читаем миграции базы данных, контроллеры, тесты, конфигурацию — и собираем из этого понятную картину.

Это не про «правильную» архитектуру. Это про то, чтобы зафиксировать как есть — честно и без прикрас.

Что входит в такое описание

Описание уровня 0 охватывает то, что реально видно в коде:

  • Таблицы и поля — из миграций базы данных. Какие сущности хранятся, какие у них поля и статусы.
  • Точки входа — HTTP-эндпоинты, команды, очереди сообщений. Что сервис принимает снаружи.
  • Логика обработки — что происходит с данными: проверки, вычисления, переходы статусов.
  • Интеграции — какие внешние сервисы вызываются, какие события публикуются в брокер.

Всё, чего в коде не выражено — например, кто является инициатором операции или какое максимальное время ответа допустимо — помечается явно: not-declared. Это честный сигнал: «мы это не нашли, надо уточнить».

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

Снимок фиксирует и ошибки тоже

Главное ограничение уровня 0, и его стоит проговорить до того, как описание начнут читать как истину. «Как есть» включает и то, что работает неправильно. Код, который по ошибке разрешает отменить отгруженный заказ, попадёт в описание как разрешённый переход. Проверка, которую забыли, попадёт как её отсутствие. Скилл честно описывает поведение и не знает, задумано оно или случилось.

Чем это опасно практически: описание выглядит как спецификация, а спецификацию читают как решение. Через месяц кто-то скажет «так у нас написано, что отменять после отгрузки можно», и ошибка получит статус договорённости.

Что с этим делать — три приёма, все дешёвые:

  • Пометить сам документ как снимок. Пометка уровня во frontmatter для этого и нужна, но её мало: первая строка файла говорит словами, что это описание текущего поведения, а не требования, и что расхождения с ожидаемым — задачи, а не норма.
  • Разделить вычитку на два прохода. Первый — «так ли работает на самом деле» (сверка с кодом, где сомнительно). Второй — «так ли должно работать». Второй проход и есть то место, где находят баги: человек читает описание перехода и говорит «постойте, этого быть не должно».
  • Найденное расхождение писать рядом, а не вместо. Пометка вида «текущее поведение: разрешено; ожидаемое: запрещено, задача такая-то» сохраняет и факт, и решение. Заменить описание на желаемое в момент вычитки — потерять снимок, который и был целью.

Полезный побочный эффект: уровень 0 находит баги, которые никто не искал. Матрица переходов, восстановленная из кода, показывает разрешённые пары, о которых не думали; список эндпоинтов — забытые точки входа; интеграции — вызовы, о которых никто не помнил. Это одна из главных причин делать снимок, и её редко называют.

Зачем это нужно

Снимок «как есть» даёт три практических преимущества.

Быстрое введение в сервис. Новый участник команды видит структуру сервиса сразу из описания — вместо того чтобы неделями собирать её по коду.

Точка отсчёта для изменений. Перед тем как что-то менять в старом сервисе, полезно зафиксировать текущее состояние. Тогда видно, от чего отталкиваемся и что именно меняем.

Основа для аудита. Когда описание есть, видно и то, чего нет: неочевидные зависимости, дублирующаяся логика, забытые эндпоинты.

Как получается спецификация

Скилл ucp-spec-tier-0 читает исходники сервиса и собирает описание в стандартном формате: корневой файл контекста плюс по одному файлу на каждый домен-юнит (агрегат).

ucp-spec-tier-0 миграции таблицы и поля контроллеры точки входа конфигурация правила и маршруты тесты ожидаемое поведение снимок спеки корень + aggregates/

Снимок собирают из четырёх источников сразу: слева видно, что даёт каждый, справа то, во что это складывается.

Важные правила, которым следует скилл:

  • Только факты из кода. Скилл не предполагает и не выдумывает. Файл домен-юнита создаётся всегда, даже когда агрегатов в коде нет: просто внутри будет таблица полей центральной сущности и пометка «агрегаты в коде не выделены», а не «возможный агрегат».
  • Без оценки «правильно/неправильно». Цель — зафиксировать, а не критиковать. Критика будет позже, когда есть от чего отталкиваться.
  • level: 0 во frontmatter — чтобы отличать снимок от проектного описания.

Как это выглядит

Кусок восстановленного описания — единственный ощутимый артефакт уровня, и его полезно увидеть. Вот фрагмент корневого файла:

---
context: billing
bounded-context: not-declared
level: 0
---

# Billing (снимок из кода, 2026-09-26)

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

## 1. Bounded Context

Миссия: not-declared
Субдомен: not-declared
Владелец: not-declared
Что внутри границы: таблицы invoices, invoice_lines, payments_log;
                    эндпоинты /invoices/*; публикация в топик billing.events
Агрегаты в коде не выделены: доменной логики в моделях нет,
                    логика в InvoiceService (14 методов).

## 3. Ubiquitous Language

| Термин в коде | Что означает | Комментарий |
|---|---|---|
| Invoice | счёт | статусы NEW, SENT, PAID, VOID |
| InvoiceLine | позиция счёта | ссылка на invoice_id |
| dueDate | срок оплаты | not-declared: правило расчёта не найдено |
| legacyFlag | not-declared | используется в 3 местах, смысл неясен |

И фрагмент файла домен-юнита с восстановленным жизненным циклом:

## 2. Жизненный цикл

Статусы найдены в enum InvoiceStatus: NEW, SENT, PAID, VOID

| Из | В | Триггер | Где в коде | Условие |
|---|---|---|---|---|
| — | NEW | POST /invoices | InvoiceController:42 | — |
| NEW | SENT | POST /invoices/{id}/send | InvoiceController:61 | проверок не найдено |
| SENT | PAID | обработчик payment.captured | PaymentListener:28 | сверка суммы отсутствует |
| PAID | VOID | POST /invoices/{id}/void | InvoiceController:88 | **проверок не найдено** |
| NEW | PAID | — | не найдено | переход в коде отсутствует |

Замечания снимка:
- переход PAID → VOID доступен без проверок и без роли: вероятно, дефект
- сверки суммы платежа со суммой счёта в коде нет
- политик и таймаутов не найдено

Обратите внимание на три вещи, которые делают такой снимок полезным. Ссылки на места в коде — по ним проверяют утверждение, не доверяя ему. Формулировка «не найдено» вместо «нет» — скилл не утверждает отсутствие, он сообщает, что не нашёл. И отдельный блок замечаний, где подозрительное собрано в одном месте, но не выдаётся за требование.

Когда логика живёт не только в коде сервиса

Типичный унаследованный сервис устроен хуже, чем предполагает простая формулировка «читаем код». Логика размазана, и уровень 0 надо снимать со всех мест, где она есть.

Несколько репозиториев. Сервис в одном, общие библиотеки в другом, схема базы в третьем, настройки развёртывания в четвёртом. Порядок такой: снимок делают на границе контекста, а не репозитория, и первым шагом составляют список источников — какие репозитории, какие каталоги, какие ветки. Список идёт в сам файл снимка: без него через месяц непонятно, что именно было прочитано, и повторить нельзя. Если репозитории не открыть все сразу, честнее снять по частям и пометить, что источник такой-то не читался.

Логика в хранимых процедурах и триггерах базы. Частый и самый недооценённый случай: в коде видно вызов, а что происходит — в базе. Такое описывают как часть поведения, а не как деталь хранения: процедура, меняющая статус, — это переход в матрице, и в колонке «Где в коде» стоит имя процедуры. Триггеры особенно важно вытащить наружу: они меняют данные незаметно для кода, и их отсутствие в снимке делает описание неверным, а не неполным.

Логика в конфигурации. Правила в настройках оркестратора процессов, маршруты в шлюзе, условия в планировщике, значения в таблицах-справочниках, которые ведут руками. Признак того, что это поведение, а не настройка: если поменять значение, изменится результат для пользователя. Ставка комиссии в таблице — это поведение; размер пула соединений — нет.

Логика в чужом сервисе. Часть правил выполняет внешняя система, и внутри её не видно. Тогда в снимке это фиксируется как стык: что мы отправляем, что получаем, какое поведение предполагаем, и явная пометка, что правило не наше. Именно здесь not-declared уместнее всего.

Логика в задачах по расписанию. Ночные сценарии, которые меняют статусы, начисляют, рассылают. Они не видны из эндпоинтов и потому пропускаются чаще всего, а в матрице переходов это как раз политики. Искать их надо отдельно: по расписанию, по точкам входа планировщика, по журналам.

Порядок работы с размазанной логикой. Сначала список источников. Потом снимок по каждому, отдельно. Потом сборка в один документ, где переход или правило указывает, где оно живёт. Сборку делает человек, а не скилл: именно на ней обнаруживается, что одно и то же правило реализовано в двух местах по-разному, — и это самая ценная находка уровня 0.

Куда двигаться дальше

Уровень 0 — это стартовая точка, а не финальное состояние. Дальше команда:

  1. Читает описание и договаривается о том, что сервис реально должен делать.
  2. Заменяет not-declared на реальные бизнес-факты.
  3. Переводит описание на целевой уровень: 1 Слоёный, 2 Use Case Pattern или 3 DDD + Hexagonal.
  4. Постепенно подтягивает код к новому описанию.

Такой порядок помогает избежать ситуации, когда рефакторинг начинается без понимания того, что именно меняется.

Про эти четыре пункта стоит сказать, сколько они занимают и кто их делает, — иначе «дальше команда» остаётся без исполнителя.

ШагКто делаетСколько занимает
Генерация снимкаскилл, запускает разработчикчасы, чаще меньше
Первая вычитка: «так ли работает»тот, кто знает сервис лучше всехдень на средний сервис
Договориться, что сервис должен делатьвладелец домена и разработчики вместеодна-две встречи по два часа
Заменить not-declared на фактыразработчик после встречдень-два
Перевод на целевой уровеньтот, кто будет менять кодотдельная работа, недели

Про встречу, на которой договариваются. Это главный шаг, и он единственный требует людей помимо разработчиков. Формат, который работает:

  • Вход — снимок, а не чистый лист. Читать готовое описание и возражать легко; сочинять требования с нуля — трудно. Именно поэтому снимок делается первым.
  • Участники — владелец домена, тот, кто отвечает за продукт, и один-два разработчика. Больше пяти человек превращают встречу в обсуждение.
  • Предмет — не весь снимок, а спорные места. До встречи их отмечают: разрешённые переходы, которые выглядят странно, not-declared в важных местах, замечания скилла. Обычно спорного на полтора часа, а не на день.
  • Результат — решения, записанные в тот же документ. Не протокол отдельно: правка идёт в снимок, с пометкой «решено на такую-то дату».
  • Что не решается за одну встречу — становится задачей с владельцем. Часть вопросов требует выяснения у внешних потребителей или у бухгалтерии; они не должны блокировать остальное.

Порядок, который стоит соблюдать: сначала договориться о поведении, потом менять код. Соблазн начать с рефакторинга сразу после снимка велик, и он возвращает к тому, от чего уровень 0 защищает: изменение без понимания того, что именно меняется.

Как понять, что снимок получился плохой

Скилл всегда что-то возвращает, и результат выглядит убедительно. Признаки, по которым видно, что читать его нельзя:

Много not-declared в местах, где данные обязаны быть. Сама доля ни о чём не говорит: not-declared в нефункциональных требованиях и в миссии — норма, их в коде нет по определению. Тревожно другое — пометка там, где ответ в коде есть:

Где стоит not-declaredЧто это значит
Миссия, владелец, целевые значениянорма: этого в коде и не бывает
Роли и праваподозрительно: проверки прав обычно в коде есть
Статусы и переходыскилл не нашёл перечисление или матрицу — почти всегда ошибка чтения
Публикуемые событияесли в коде есть отправка в брокер, это пропуск
Бизнес-правила у командсамый частый настоящий пробел: правила размазаны по условиям

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

Нет ни одной задачи по расписанию и ни одного обработчика событий. В сервисе старше года они почти всегда есть. Отсутствие — признак, что смотрели только на эндпоинты.

Таблицы есть, а переходов нет. Значит статусы описаны как поля, а жизненный цикл не восстановлен. Это самый бесполезный вид снимка: структура данных без поведения не отвечает ни на один вопрос, ради которого снимок делали.

Всё выглядит слишком хорошо. Снимок без замечаний, без not-declared и без странностей в унаследованном сервисе — признак того, что скилл описал модель кода, а не поведение. В живом сервисе странности есть всегда.

Проверка, которая занимает пятнадцать минут. Возьмите три вещи, которые знаете точно, — одно правило, один переход, одну интеграцию — и найдите их в снимке. Не нашли хотя бы одну: снимок неполный, и дальше надо не вычитывать, а разбираться с источниками, о чём раздел выше.

Что делать с плохим снимком. Не выбрасывать и не править руками целиком. Сначала понять причину: не все источники прочитаны, логика не в коде, или постановка была слишком общей. Потом перегенерировать с уточнением — указать модули, назвать способ объявления точек входа, дать список репозиториев. Правка руками уместна там, где пробелов единицы; при системном пропуске она маскирует причину, и следующий снимок получится таким же.

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

Глубже: когда Use Case Pattern не братьрасширенное

У каждого уровня есть «когда не стоит», у методологии целиком нет, и это нечестно: она тоже не для всех.

Не брать, когда сервис это прокси или CRUD без правил, который переживёт год и умрёт: спека, карточки и гейты стоят больше, чем всё его поведение. Когда команда из одного-двух человек делает прототип для проверки гипотезы и половина кода будет выброшена: методология защищает от дрейфа на длинной дистанции, а у прототипа её нет. Когда организация не готова считать спеку источником правды: если менеджер меняет требования в чате, а разработчик правит код по чату, спека станет музеем через месяц, и лучше не заводить её, чем завести и не вести. Когда домен не ваш: интеграционный клей поверх чужой системы описывают её терминами, и своя модель понятий там лишняя.

Брать частично. Уровень 0 и корневой файл спеки полезны почти любому сервису старше года как карта, даже без UseCase в коде. Уровень 2 без спеки на весь контекст, только с карточками на операции, которые меняют, окупается быстро. Корпус правил берут не целиком, а десяток правил под свои грабли, о чём статья про третий уровень.

Признаки, что пора. Один и тот же вопрос («а можно ли оплатить отменённый заказ?») задают третий раз; правка одной операции ломает соседнюю; агент генерирует код, который каждый раз устроен иначе; новый разработчик неделю читает код, чтобы понять, что делает сервис. Любой из них означает, что цена отсутствия модели уже выше цены её ведения, и тогда начинают с уровня 0 в эту же неделю.

Коротко

  • Уровень 0 — это описание сервиса, восстановленное из кода, когда документации нет.
  • В него входят таблицы, эндпоинты, логика, интеграции — всё, что видно в исходниках. Чего нет в коде — помечается not-declared, а не просто пропускается.
  • Скилл ucp-spec-tier-0 собирает это описание из кода — но это черновик, который команда вычитывает.
  • Уровень 0 — отправная точка, от которой движутся к целевой архитектуре, а не конечная цель.
  • Методологию не берут для короткоживущего CRUD, прототипов на выброс, команд, где спеку не будут вести, и клея над чужим доменом; берут частично картой уровня 0 и карточками на меняемые операции, а целиком когда цена отсутствия модели уже видна.
  • Снимок «как есть» фиксирует и ошибки: документ помечают как описание текущего поведения, вычитывают в два прохода («так ли работает» и «так ли должно») и пишут расхождение рядом, а не вместо.
  • Побочная польза уровня 0 — найденные баги: восстановленная матрица показывает разрешённые пары, о которых не думали, забытые эндпоинты и вызовы, о которых никто не помнил.
  • Логика живёт не только в коде сервиса: несколько репозиториев, хранимые процедуры и триггеры, конфигурация оркестратора и справочники, чужой сервис, задачи по расписанию; список источников пишут в сам снимок.
  • Договорённость о должном поведении — отдельная встреча на входе со снимком, с владельцем домена и парой разработчиков, по заранее отмеченным спорным местам, с решениями прямо в документ.
  • Плохой снимок узнают по пометкам в местах, где ответ есть в коде, по короткому списку эндпоинтов, по отсутствию задач по расписанию и переходов и по подозрительной гладкости; лечится перегенерацией с уточнением источников, а не правкой руками.

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