
Вторая статья из цикла из трёх частей.
Часть 1 — где LLM теряет межсервисный контекст и почему локальных спек недостаточно.
Часть 2 — archspec как контракт вместо свободного Markdown.
Часть 3 — archspec: исследование фичи, обновление контрактов и реализация.
1. Зачем понадобился архитектурный контракт
В первой части я разбирал, почему spec-driven development начинает ошибаться, когда фича проходит через несколько микросервисов. Проблема не в том, что LLM плохо читает код или не умеет писать спеку. На уровне отдельных сервисов всё может выглядеть аккуратно: есть описание, план, реализация и тесты. Но правила, которые связывают сервисы между собой, часто не записаны в одном месте. Часть таких правил спрятана в реализации, часть известна только команде, а часть всплывает уже на ревью. Обычный Markdown не решает эту проблему: его легко написать неполным, сложно проверить автоматически и почти невозможно ревьюить как структурный контракт.
Отсюда родилась идея: нужен машиночитаемый контракт на каждый сервис, который фиксирует межсервисные правила, проверяется на коммите и даёт LLM структурный контекст вместо набора Markdown-файлов. Для этого я собрал open source плагин для Claude Code — archspec.
В этой части я покажу, как работает /archspec:init на одном сервисе из демо-проекта freelance-marketplace, разберу сгенерированные артефакты и объясню, как archspec поддерживает их в актуальном состоянии. Напомню, это Go-проект с 12 микросервисами для поиска фрилансеров. Вот схема сервисов, которую я использую на протяжении всего цикла:
Схема сервисов

2. Как инициализируется архитектурный контракт
В качестве примера я беру task-service. В демо-проекте он отвечает за задачи: хранит их состояние и публикует task.created, после чего подбором кандидатов занимается matching-service. У сервиса есть важное ограничение: если операция меняет задачу и должна опубликовать событие, изменение состояния и событие должны записываться вместе через Outbox. В текущем стенде сервис специально упрощён: хранилище in-memory, а ключи идемпотентности пока не сохраняются.
2.1. Запуск /archspec:init
Скил /archspec:init добавляет в сервис четыре вещи: спеку, то есть машиночитаемый YAML-контракт архитектуры (docs/SERVICE_MAP.yaml); страницу архитектуры для разработчиков (docs/ARCHITECTURE.md); три диаграммы в docs/diagrams/; а также pre-commit хуки, которые запускаются перед коммитом и проверяют, что сгенерированные файлы не разъехались со спекой. Диаграммы нужны для разных уровней детализации: context показывает внешние связи сервиса, container — каналы взаимодействия и хранилища, а sequence — путь конкретного запроса. Главное правило здесь простое: руками правится только спека, а всё остальное генерируется из неё. Ниже ещё покажу, как это поддерживается хуками.

2.2. Проектная архитектура: подсказка, а не источник истины

Скил может взять проектный architecture.md как подсказку для общей схемы сервисов. В демо-проекте такого файла нет, поэтому я отвечаю skip; факты о самом сервисе скил всё равно проверит по репозиторию.
2.3. Базовые данные о сервисе

Дальше скил просит заполнить базовые данные: имя сервиса, команду-владельца, домен (bounded context — обособленная часть предметной области со своей моделью) и URL репозитория. Имя сервиса по умолчанию берётся из имени директории, потому что скил видит, где он запущен.
2.4. Что сервис делает и что гарантирует

На этом шаге важно коротко зафиксировать, за что отвечает сервис и какие правила он обязан соблюдать. Для task-service это хранение задач, публикация task.created через Outbox и контроль переходов статусов. Отдельно я записываю гарантии: запись должна идти через Outbox, а id задачи не должен меняться. Эти ответы потом попадут и в YAML-спеку, и в страницу архитектуры.
2.5. Что можно достать из кода автоматически
После ручных ответов скил читает репозиторий и собирает факты, которые не хочется переписывать вручную: какие RPC и HTTP-ручки есть у сервиса, какие события он публикует, какие хранилища использует и где находится основная модель записи. Это важно не только для удобства. Если контракт собирается из кода, меньше шансов случайно описать ручку, которой уже нет, или забыть событие, которое сервис реально отправляет.
2.6. Где нужна идемпотентность и какие SLA известны


Дальше скил проходит по найденным ручкам и уточняет то, что в коде часто не видно напрямую. Для команд вроде CreateTask важно понять, нужна ли идемпотентность, откуда берётся ключ и где он хранится. В моём примере ключ требуется, но хранилища для него ещё нет, поэтому я выбираю not-implemented. SLA тоже фиксируется явно: если метрика уже есть, её можно записать сразу; если нет, остаётся not-measured.
2.7. Хранилища, события и Outbox
На этом шаге в контракт попадают хранилища и события. Для task-service это in-memory:task-store и событие task.created версии 1. Здесь же становится видно, может ли сервис использовать Outbox: для этого у него должны быть и состояние, которое меняется, и событие, которое нужно опубликовать вместе с этим изменением.
2.8. Какие сервисы зависят от task-service
Контракту важно знать не только зависимости самого сервиса, но и тех, кто его вызывает. Поэтому я указываю корень монорепо, а скил ищет потребителей task-service. В этом примере он находит api-gateway. Это полезно на ревью: если меняется контракт task-service, сразу видно, какие сервисы потенциально затронуты.
2.9. Главный агрегат и путь записи

Скил также просит подтвердить главный агрегат и путь записи. В коде он видит техническое имя MemoryTask, но в контракте лучше использовать бизнес-имя task: так спеку проще читать не только автору кода. Паттерн outbox выбирается не по умолчанию, а по роли сервиса. Он нужен там, где сервис сам меняет своё состояние и вместе с этим должен опубликовать событие. У task-service как раз такой случай: он хранит задачи и публикует task.created. А stateless-шлюз вроде api-gateway состояние не хранит, поэтому ему нечего записывать через Outbox.
2.10. Правила между сервисами

Последний шаг — записать правила, которые относятся не к одному сервису, а к его взаимодействию с другими сервисами. Например, кто должен обработать task.created, как защищаться от дублей и какие гарантии ожидают потребители события. Это как раз тот слой, который плохо виден в локальном CLAUDE.md: сервис может быть описан правильно, но его договорённости с другими сервисами всё равно останутся неявными.
2.11. Что получилось после init

Итог сессии: 3 ручки, одно публикуемое событие, один потребитель, in-memory хранилище, outbox и агрегат task. В контракте также остаются явно отмеченные незакрытые места. Например, для CreateTask идемпотентность нужна, но хранилища ключей в сервисе пока нет (IDEMP-001). У события task.created пока нет отдельной схемы (DOC-002). Поэтому на ревью видно не только то, что уже есть в сервисе, но и то, что ещё нужно доделать. Весь init занял около трёх минут на сервис.
3. Что сгенерировал archspec
3.1. YAML-контракт сервиса
Спека — это машиночитаемый контракт сервиса в одном YAML-файле. В ней лежит именно то, что терялось в первой части: зона ответственности, инварианты, ручки с признаком идемпотентности, путь записи (тот самый Outbox), события и главные агрегаты. Файл получается небольшим, обычно на пару сотен строк, поэтому LLM быстро читает его целиком. У task-service спека выглядит так; показываю только одну ручку, чтобы не раздувать пример:
service:
responsibilities:
- "create and persist freelance tasks"
- "publish task.created events via outbox"
invariants:
- "every write goes through the outbox"
- "task IDs are unique and immutable"
api:
endpoints:
- name: CreateTask
protocol: gRPC
idempotency:
required: true
key_source: "metadata: x-idempotency-key"
storage: "not-implemented"
sla:
p99_latency: "500ms"
availability: "99.9%"
events:
published:
- topic: task.created
version: 1
consistency:
write_path:
pattern: outbox
concurrency:
aggregates:
- name: task
write_strategy: optimisticstorage: not-implemented — это не баг генерации, а явная отметка: ключ идемпотентности на ручке требуется, но хранилища ключей в коде ещё нет. Полный файл лежит здесь: SERVICE_MAP.yaml.
3.2. Страница архитектуры для разработчиков
Страница архитектуры содержит те же данные, что и спека, но в форме обычного Markdown: таблицу ручек с протоколами и SLA, списки зависимостей, событий, хранилищ и ссылки на диаграммы. По формату это обычный README, который GitHub показывает в PR без дополнительных инструментов.
Внутри файла стоят маркеры <!-- archspec:managed-region:start --> и <!-- archspec:managed-region:end -->. Всё, что находится между ними, при каждой регенерации перезаписывается из спеки. Всё за пределами этого блока можно редактировать вручную: добавить историю сервиса, runbook или заметки по эксплуатации. Pre-commit проверяет только сгенерированный блок и не трогает эти разделы.
Кусок таблицы ручек у task-service:
Endpoint | Protocol | Idempotent | SLA p99 |
|---|---|---|---|
CreateTask | gRPC | yes | 500ms |
GetTask | gRPC | no | not-measured |
ListTasks | gRPC | no | 100ms |
Полный файл лежит здесь: ARCHITECTURE.md.
3.3. Диаграммы, которые обновляются из спеки
Диаграммы хранятся в docs/diagrams/ отдельными файлами в Mermaid-формате. GitHub и Habr рендерят Mermaid прямо из Markdown, без дополнительных инструментов.
Context (C1). Общий вид: кто вызывает сервис снаружи, какие у него хранилища и какие события он публикует. Внутренних деталей здесь нет.
Container (C2). Тот же сервис, но связи помечены типом: sync — downstream gRPC, async — события, storage — хранилище, publish/consume — топики. По этой диаграмме видно, через какие каналы ходят данные.
Sequence. По одному обращению на каждую ручку, с чтением заголовка идемпотентности и записью в Outbox. Этого достаточно, чтобы понять основной путь запроса.
container.mmd для task-service:

Фрагмент sequence.mmd для CreateTask:

В PR ревьюер видит структурный диф архитектуры рядом с дифом кода. На больших монорепо этого часто не хватает: код меняется, а схема взаимодействия сервисов не попадает в ревью. Полные диаграммы лежат в docs/diagrams/.
3.4. Блок для Claude Code
Ещё один артефакт появляется в CLAUDE.md сервиса. Это небольшой сгенерированный блок, который объясняет Claude Code, как работать с архитектурным контрактом: сначала читать docs/SERVICE_MAP.yaml, перед нетривиальной задачей запускать /archspec:investigate, после изменения спеки запускать /archspec:sync, а перед merge проверять результат через /archspec:validate.
Смысл простой: CLAUDE.md остаётся входной точкой для агента, но теперь он не заменяет архитектурный контракт. Он направляет Claude Code к спеке и напоминает, что диаграммы и сгенерированную часть ARCHITECTURE.md нельзя править вручную.
4. Какие проблемы решает контракт
4.1. Документация не должна расходиться с контрактом
Хуки блокируют ручную правку сгенерированной страницы и диаграмм, потому что без такой защиты они быстро разойдутся со спекой. Кто-то поправит таблицу ручек в ARCHITECTURE.md, забудет про YAML, и в PR появятся изменения, которых на самом деле нет в контракте сервиса. В итоге документация начнёт описывать не тот сервис, который лежит в репозитории.
4.2. В контракте должны быть видны незакрытые места
Раньше в примере с CreateTask было видно: идемпотентность нужна, но хранилища ключей пока нет. В обычной документации это место легко сгладить или случайно описать так, будто всё уже реализовано. В контракте лучше оставить явную отметку not-implemented: так ревьюер сразу видит, что правило уже зафиксировано, но код ещё не закрывает его полностью.
Такие отметки нужны не только для идемпотентности. not-documented показывает, что код есть, но схемы в proto или OpenAPI ещё нет. not-measured означает, что SLA указан как требование, но метрика пока не снимается. Это не финальное состояние, а понятный список того, что ещё нужно доделать.
4.3. Outbox появляется только там, где он возможен
Паттерн outbox прописывается только тогда, когда у сервиса есть и публикуемые события, и долговременное хранилище. Например, api-gateway только принимает запросы и передаёт их дальше, но сам не хранит состояние задачи. Значит, ему не нужен Outbox: ему нечего записывать вместе с событием. Это закрывает один из частых классов ошибок из первой части: в спеке события выглядят «как бы атомарными» с записью, хотя в реальном коде этой атомарности нет.
4.4. Генерация должна быть предсказуемой
Один и тот же код должен давать один и тот же результат в спеке. Иначе разработчик запускает синхронизацию, ничего по сути не меняет, а в Git появляется лишний диф. После нескольких таких случаев хукам перестают доверять: они начинают выглядеть как шум, а не как проверка архитектуры. Поэтому генерация должна быть предсказуемой, чтобы в PR попадали только реальные изменения.
4.5. Потребителей сервиса лучше искать скриптом, а не просить LLM
Поиск потребителей сервиса лучше делать автоматически. Это механическая задача: нужно пройтись по монорепо и понять, кто вызывает task-service. LLM может помочь объяснить результат, но сам список потребителей должен собираться скриптом, чтобы не пропустить связь между сервисами. Перед применением изменений archspec показывает, что именно будет добавлено в спеку, и даёт выбор: применить всё, отредактировать вручную или пропустить.
5. Как контракт остаётся актуальным
5.1. Проверка запускается перед коммитом
В конце init archspec добавляет шаг перед git commit. Он не заменяет существующие pre-commit хуки проекта, а выполняется вместе с ними. Если спека, страница архитектуры или диаграммы расходятся между собой, разработчик увидит это до коммита.
5.2. Что проверяется перед коммитом
Перед коммитом archspec сверяет спеку, страницу архитектуры, диаграммы и найденные связи между сервисами. Проверка ловит несколько типичных проблем:
ручную правку сгенерированного блока в
ARCHITECTURE.mdили диаграммах;литерал
TODOв обязательных полях спеки;паттерн записи
outboxбез долговременного хранилища;сервис вызывает другой, но не записан как его потребитель;
идемпотентность задекларирована, а хранилища для ключей в спеке нет.
По умолчанию это предупреждения (WARN). Чтобы сделать их блокирующими (BLOCK), в спеке включается строгий режим. Обычно его включают не сразу: на старых сервисах сначала может появиться много предупреждений, и команде удобнее разобрать их постепенно, а не получить заблокированный коммит в первый же день.
5.3. Как выглядит обычный цикл обновления
Обычный цикл такой: разработчик меняет контракт сервиса, а затем запускает /archspec:sync. Из обновлённой спеки заново собираются страница архитектуры и диаграммы, поэтому их не нужно править вручную. В PR попадает один связанный набор изменений: код, контракт и сгенерированная документация. Ревьюер видит не только новый метод или событие, но и то, как из-за него изменилась схема сервиса.
5.4. Как проверить связи между сервисами
После init отдельных сервисов полезно посмотреть на проект целиком. Для этого есть /archspec:check-architecture: он читает все спеки сразу и ищет несостыковки между ними. Например, один сервис вызывает другой, но это не отражено в контракте; событие публикуется, но у него нет потребителя; в спеке указана инфраструктура, которой нет в зависимостях сервиса. Проверка ничего не правит сама, а только показывает список проблем, чтобы команда могла разобрать их отдельно.
6. Что дальше
После /archspec:init у сервиса появляется YAML-контракт, страница архитектуры и диаграммы. Эти файлы лежат рядом с кодом и обновляются из одной спеки, поэтому их уже можно использовать как контекст для LLM до начала реализации.
В третьей части я вернусь к задаче Smart Task Reassignment из первой статьи: если выбранный фрилансер отказался от оффера, система должна автоматически выбрать следующего кандидата и отправить новый оффер. На этой задаче проверю следующий шаг: что изменится, если дать LLM не набор локальных Markdown-файлов, а обновлённые архитектурные контракты сервисов. Посмотрю, какие из шести межсервисных ошибок получится поймать уже на этапе планирования.
Оба репозитория открыты:
archspec: https://github.com/krus210/archspecfreelance-marketplace(демо-проект): https://github.com/krus210/freelance-marketplace
Если найдёте баг или неудобный сценарий, создайте issue в репозитории archspec.
Часть 3 — на подходе.





















