Третья часть цикла о spec-driven development закрывает эксперимент, начатый в двух предыдущих материалах. Стенд — Go-проект из 12 микросервисов для поиска фрилансеров: gRPC для синхронных вызовов, NATS как брокер асинхронных событий, Clean Architecture в каждом сервисе. Тестовая фича — Smart Task Reassignment: если фрилансер отказался от оффера, платформа сама находит следующего кандидата, уведомляет заказчика и переводит задачу в failed после трёх неудачных попыток.

Первый прогон (task_1) прошёл без archspec: Claude Sonnet 4.6 читал локальные CLAUDE.md по каждому сервису, построил план примерно на 180 строк и реализовал фичу. Два независимых ревью — Claude в отдельной сессии и Codex со сверкой по эталонному решению — нашли одну и ту же группу проблем. Итог: 6/10, чеклист закрыт примерно на 64%. Ошибки оказались типичными для работы LLM с распределёнными системами: прямые вызовы закрытых сервисов в обход worker-facade, придуманные методы в proto, город передавался как строка-имя вместо city_id, N+1 вместо batch-запросов, публикация событий мимо Transactional Outbox. Критическим оказался баг с match_id: все переназначения получали один идентификатор, и notification-service отбрасывал повторные офферы как дубликаты — сквозной сценарий не проходил.

ПрогонИнструментРезультат ревьюЧеклистКритический баг
task_1Локальные CLAUDE.md, свободное планирование6/10~64%Один match_id на все переназначения — дубликаты в notification-service
task_3SERVICE_MAP.yaml + /archspec:investigateОшибки закрыты на этапе планаКоллизия match_id устранена до кода

Причина системная: правила, которые управляют поведением на границах между сервисами, нигде не были записаны как единое ограничение. LLM видел каждый сервис по отдельности и не мог восстановить межсервисный контекст из разрозненных Markdown-файлов. Во второй части цикла для каждого из 12 сервисов был сгенерирован машиночитаемый контракт SERVICE_MAP.yaml — структурированный документ, где явно прописаны публичные интерфейсы, события, владельцы данных и ограничения на границах.

Во втором прогоне (task_3) изменились ровно две вещи: у каждого сервиса появился контракт, и вместо свободного планирования запустился /archspec:investigate. Промпт фичи и модель остались прежними. Инструмент начинает не с плана, а с clarify-gate: читает срезы контрактов затронутых сервисов и задаёт уточняющие вопросы по измерениям неоднозначности — откуда приходит триггер, кто владеет состоянием, как резолвить тай-брейк по геолокации, что именно считает лимит переназначений. Это read-only этап: код и контракты не трогаются. На этом шаге были заданы именно те вопросы, на которых task_1 молча принял неверное решение: worker_id взят из тела запроса вместо токена, город передан как имя, лимит посчитан с ошибкой на единицу.

После уточнений investigate сохраняет план как отдельный файл-артефакт, который затем читает этап implement. Структура плана включает: цитаты конкретных строк из контрактов для каждого утверждения, список открытых вопросов с решениями до кода, diff правок в API (например, добавить метод DeclineOffer и поле city_id в proto), диаграмму последовательности по всем трём веткам сценария, проверку по всем 12 сервисам на предмет того, кто публикует каждое событие и кто его слушает, а также список крайних случаев с привязкой к тестам.

Диаграмма последовательности из плана охватывает три ветки: обычный отказ (фрилансер отказывается → api-gateway достаёт worker_id из токена → task-service публикует offer.declined через outbox → matching-service берёт следующего кандидата → уведомление заказчику), исчерпанный лимит (третий отказ → task.failed → уведомление) и ситуацию, когда кандидаты кончились раньше лимита (matching-service шлёт match.exhausted → task-service переводит задачу в failed). Синхронные gRPC-вызовы и асинхронные NATS-события на схеме различаются визуально, у каждой ветки есть терминальное состояние.

Во втором прогоне коллизия match_id закрыта на уровне плана: каждая попытка переназначения получает новый идентификатор, а TaskID-fallback дедупликации в notification-service убран. city_id сделан обязательным полем и протянут в payload события task.created. Это именно те места, где task_1 не проходил сквозной сценарий — и они были исправлены до написания кода, а не обнаружены в ревью после реализации.

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