惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Microsoft Azure Blog
Microsoft Azure Blog
Cloudbric
Cloudbric
I
InfoQ
V
V2EX
博客园_首页
The Register - Security
The Register - Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
S
Secure Thoughts
Vercel News
Vercel News
Forbes - Security
Forbes - Security
云风的 BLOG
云风的 BLOG
PCI Perspectives
PCI Perspectives
L
LINUX DO - 最新话题
D
DataBreaches.Net
H
Hacker News: Front Page
Application and Cybersecurity Blog
Application and Cybersecurity Blog
B
Blog RSS Feed
A
About on SuperTechFans
N
News and Events Feed by Topic
Apple Machine Learning Research
Apple Machine Learning Research
Help Net Security
Help Net Security
Attack and Defense Labs
Attack and Defense Labs
N
Netflix TechBlog - Medium
Spread Privacy
Spread Privacy
F
Full Disclosure
Recorded Future
Recorded Future
AWS News Blog
AWS News Blog
博客园 - 【当耐特】
The Cloudflare Blog
T
Threatpost
T
Tor Project blog
Google DeepMind News
Google DeepMind News
C
CXSECURITY Database RSS Feed - CXSecurity.com
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Recent Announcements
Recent Announcements
M
MIT News - Artificial intelligence
A
Arctic Wolf
C
Check Point Blog
Stack Overflow Blog
Stack Overflow Blog
T
Threat Research - Cisco Blogs
Security Archives - TechRepublic
Security Archives - TechRepublic
Hacker News - Newest:
Hacker News - Newest: "LLM"
WordPress大学
WordPress大学
Cyberwarzone
Cyberwarzone
小众软件
小众软件
C
Cyber Attacks, Cyber Crime and Cyber Security
P
Proofpoint News Feed
Security Latest
Security Latest
The Last Watchdog
The Last Watchdog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Почему spec-driven development плохо работает на микросервисах: часть 1. Где теряется контекст
krus210 · 2026-05-12 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение12 мин

Охват и читатели1.1K

Кейс

Первая статья из цикла из трёх частей.

Часть 1 — где LLM теряет межсервисный контекст и почему локальных спек недостаточно.

Часть 2 — archspec: версионируемый архитектурный контракт для сервисов.

Часть 3 — archspec: исследование фичи, обновление контрактов и реализация.

1. Вступление

Я работаю в большой продуктовой компании с тысячей микросервисов. В такой системе даже небольшая фича часто проходит через несколько сервисов, событий и внутренних контрактов. Spec-driven development с LLM уже применяется в некоторых командах для планирования и ревью фич, поэтому мне было важно понять, где этот подход помогает, а где начинает ошибаться. Пока задача живёт внутри одного сервиса, всё обычно идёт быстро: спека короткая, описание и реализация помещаются в контекст модели. Но как только фича проходит через несколько сервисов, начинаются проблемы. По отдельности каждый кусок выглядит нормально: разбиение на слои, именование по код стайлу, прохождение тестов и ревью. Но в целом система не работает должным образом. Типичные ошибки: нет идемпотентности, LLM упускает сценарии и edge case-ы, появляются циклические вызовы сервисов. Чем больше делаешь правок, тем больше ошибок она допускает.

Для эксперимента я собрал отдельный стенд: Go-проект - платформа для поиска фрилансеров. Внутри 12 микросервисов, связанных через gRPC и брокер сообщений; в этом проекте брокером выступает NATS. Одни сервисы хранят задачи и профили исполнителей, другие подбирают кандидатов, считают расстояния, проверяют портфолио и отправляют уведомления. Проект специально спроектирован с шестью категориями архитектурных ловушек: они проявляются не внутри одного сервиса, а на границах между сервисами.

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

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

На это можно ответить, что нужен end-to-end тест на весь сценарий, но это не закрывает проблему целиком. End-to-end тесты есть не везде, их дорого поддерживать, и они не покрывают все развилки: особенно редкие edge case-ы, дубликаты событий, гонки и редкие комбинации условий. Главное же в другом: на этапе spec-driven разработки модель должна помочь собрать требования, ограничения и контекст, а именно там она часто ошибается.

Разработчик тоже не всегда заранее знает, где спрятана проблема. Он может помнить про Outbox, дедупликацию уведомлений или особые требования конкретного сервиса к входным данным, но не сформулировать это как ограничение для новой фичи. LLM читает документы по сервисам, задаёт уточняющие вопросы и всё равно может пропустить связь между ними.

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

2. Демо-Проект

Сетап такой: двенадцать Go-микросервисов, gRPC для синхронных вызовов и брокер сообщений для асинхронных событий. Каждый сервис собран по одной схеме Clean Architecture: domainusecaserepositorygatewayhandlerinfra. У task-service и matching-service есть Transactional Outbox, чтобы изменение состояния и событие записывались вместе.

Коротко по сервисам: api-gateway принимает клиентские запросы и проксирует их в gRPC, task-service хранит задачи и публикует task.created через Outbox, а matching-service оркестрирует подбор: вызывает skill-analyzer, затем worker-facade, затем review-service, ранжирует кандидатов и публикует match.found. Закрытые сервисы worker-profileportfolio-service и verification-service изолированы NetworkPolicy, поэтому ходить к ним напрямую нельзя; единственная разрешённая точка входа — worker-facadenotification-service слушает match.found и отправляет уведомления, geo-service считает расстояния, а config-service в этом сценарии не участвует.

В проекте заложены шесть архитектурных ловушек. Это обычные для микросервисов ограничения, но LLM легко пропускает их, когда читает сервисы по одному.

  1. Закрытые сервисы нельзя вызывать напрямуюworker-profileportfolio-service и verification-service доступны только через worker-facade; это закреплено NetworkPolicy, то есть правилом, которое ограничивает сетевой доступ к сервисам. Если LLM вызовет их напрямую, код может выглядеть нормальным, но архитектурная граница будет нарушена.

  2. У skill-analyzer один метод анализа текста. В proto есть только AnalyzeText. Методов вроде ExtractSkills или DetectUrgency нет, хотя LLM легко может их придумать по названию задачи.

  3. Данные о городе уже есть в профиле исполнителяworker-profile отдаёт city_nameregion_name и timezonegeo-service нужен не для этих данных, а для расчёта расстояния по city_id.

  4. Имя автора отзыва уже хранится в review-service. В отзыве есть поле author_name, поэтому review-service не должен ходить за именем автора обратно через worker-facade. Иначе легко получить цепочку вызовов review → worker-facade → review.

  5. Массовые запросы должны идти batch-методами. Для этого уже есть GetWorkersBatch и GetDistancesBatch. Иначе подбор кандидатов легко превращается в N+1: вместо одного запроса сервис делает отдельный запрос на каждого кандидата.

  6. Состояние и событие должны записываться вместе. Для этого используется Outbox, например CreateWithEvent и UpdateWithEvent, а получатель делает дедупликацию событий: запоминает уже обработанные ключи и отбрасывает повторы. Если запись состояния и публикацию события разнести, можно получить рассинхрон.

3. Задача

Задача для эксперимента называлась Smart Task Reassignment. По бизнес‑смыслу это автоматическое переназначение задачи: заказчик уже получил подходящего фрилансера, но фрилансер отказался от оффера. В этот момент платформа должна не бросать задачу в ручную обработку, а сама выбрать следующего кандидата и отправить ему новый оффер.

Правила переназначения:

  • отказ фрилансера запускает новый подбор;

  • кандидаты ранжируются по рейтингу, а при равном рейтинге — по расстоянию до города задачи;

  • заказчик получает уведомление о переназначении;

  • после трёх неудачных переназначений задача переходит в failed.

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

Именно эти детали потом решают, будет ли фича работать.

4. Как я запускал эксперимент

У каждого сервиса есть свой CLAUDE.md: зона ответственности, открытые RPC, события и список сервисов, к которым можно обращаться. Над ними лежит проектный CLAUDE.md со списком всех сервисов и ссылками на архитектурные доки. Идея была простая: перед реализацией Claude Code должен прочитать эти файлы и понять общую схему проекта.

Для планирования я использовал скилл superpowers:brainstorming на Sonnet 4.6. Я дал промпт с описанием фичи, Claude задал два уточняющих вопроса и предложил три варианта реализации. Я выбрал event-driven вариант, после чего Claude подготовил полную спеку.

Как на самом деле прошла сессия брейншторма

Claude выдал план примерно на 180 строк, а затем реализовал фичу по этому плану.

Только затем я прогнал два независимых ревью: Claude Opus в отдельной сессии и Codex со сверкой по эталонному решению и проектному чек-листу. Оба ревью показали одно и то же: полный сценарий отказа и переназначения работает не так, как должен.

5. Межсервисные ошибки в реализации

К плану Claude приложил sequence-диаграмму:

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

5.1. Коллизия ключа идемпотентности

notification-service дедуплицирует события match.found по match_id: если событие с таким ключом уже приходило, сервис его отбрасывает. Поэтому у каждого нового оффера должен быть свой match_id: первичный оффер и каждое переназначение — это разные события для потребителя.

В реализации Claude все переназначения получают один и тот же match_id. Модель хранит одну запись MatchResult и заново публикует её после каждого отказа, не создавая новый ключ для новой попытки.

HandleMatchFound
func (uc *NotificationUseCase) HandleMatchFound(event domain.MatchFoundEvent) {
    dedupKey := event.MatchID
    if dedupKey == "" {
        dedupKey = event.TaskID
    }

    if !uc.dedup.MarkProcessed(dedupKey) {
        log.Printf("duplicate match.found for %s, skipping", dedupKey)
        return
    }

    log.Printf("[STUB] client notified: task %s reassigned to worker %s", event.TaskID, event.WorkerID)
}

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

Локальные тесты это не подсвечивают. notification-service правильно отбрасывает дубликаты, а matching-service правильно публикует событие о найденном матче. Ошибка появляется только в связке: разные попытки оффера не должны использовать один и тот же ключ идемпотентности.

На ревью такой баг легко пропустить, потому что правило "один match_id на одну попытку оффера" не записано ни у отправителя, ни у получателя события. Это правило относится к их взаимодействию, а не к логике одного сервиса.

5.2. Новый путь записи без Outbox

В проекте есть правило: если операция меняет состояние и должна отправить событие, состояние и событие записываются вместе через Outbox. Например, task-service делает это через CreateWithEvent и UpdateWithEvent.

В реализации Claude api-gateway публикует offer.declined напрямую в брокер сообщений. После этого matching-service получает событие и отдельным вызовом записывает отказ в task-service.

DeclineOffer
func (h *GatewayGRPCHandler) DeclineOffer(ctx context.Context, req *gatewayv1.DeclineOfferRequest) (*gatewayv1.DeclineOfferResponse, error) {
    if req.GetTaskId() == "" || req.GetWorkerId() == "" {
        return nil, status.Error(codes.InvalidArgument, "task_id and worker_id are required")
    }
    if h.nc == nil {
        return nil, status.Error(codes.Unavailable, "messaging unavailable")
    }
    payload, err := json.Marshal(offerDeclinedPayload{TaskID: req.GetTaskId(), WorkerID: req.GetWorkerId()})
    if err != nil {
        return nil, status.Errorf(codes.Internal, "marshal: %v", err)
    }
    if err := h.nc.Publish(natsSubjectOfferDeclined, payload); err != nil {
        return nil, status.Errorf(codes.Internal, "publish: %v", err)
    }
    return &gatewayv1.DeclineOfferResponse{Success: true}, nil
}

Проблема в том, что событие и изменение состояния больше не атомарны. Событие может уйти в брокер сообщений, а запись отказа в task-service не произойдёт. Или наоборот, повторное событие может быть обработано как новый отказ.

Локально это выглядит как нормальная event-driven схема: gateway принял запрос, отправил событие, matching-service его обработал. Но для этой операции нужен общий контракт: отказ от оффера меняет состояние задачи, значит событие отказа должно появляться через Outbox того сервиса, который владеет состоянием задачи.

5.3. N+1 при получении рейтингов

Для ранжирования нужны рейтинги всех кандидатов. В реализации Claude matching-service вызывает GetAverageRating внутри цикла: один кандидат — один запрос в review-service.

GetAverageRating
candidates := make([]candidateEntry, 0, len(workers))
for _, w := range workers {
    rating, err := uc.ratings.GetAverageRating(ctx, w.ID)
    if err != nil {
        log.Printf("failed to get rating for worker %s: %v", w.ID, err)
        continue
    }
    candidates = append(candidates, candidateEntry{
        workerID:   w.ID,
        name:       w.Name,
        cityID:     w.CityID,
        rating:     rating,
        distanceKm: math.Inf(1),
    })
}

Если кандидатов двадцать, сервис делает двадцать сетевых вызовов за рейтингами. Правильнее было добавить batch-метод и получить рейтинги одним запросом.

Это не ошибка компиляции и не нарушение существующего proto: в review-service есть только GetAverageRating для одного исполнителя. Ошибка в другом: при межсервисном вызове в цикле нужно явно проверять, нужен ли batch API. В этой спеке такое правило не было записано.

5.4. Потерянный переход состояния

Если кандидаты закончились раньше лимита в три переназначения, задачу нужно перевести в финальный статус failed. В реализации Claude matching-service только пишет лог и выходит, не меняя состояние задачи.

Log
if int(count) >= len(result.Candidates) {
    log.Printf("ProcessOfferDeclined: candidates exhausted for task %s (count=%d, candidates=%d)", taskID, count, len(result.Candidates))
    return
}

В результате задача остаётся в прежнем статусе, например open или assigned, хотя следующего кандидата уже нет. Клиент не получает уведомление, потому что событие task.failed не публикуется.

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

5.5. Ещё две ошибки, коротко

Несовместимые представления города

Для расчёта расстояния нужны идентификаторы городов. В task-service у задачи есть поле City, где лежит отображаемое имя города вроде Moscow. А geo-service в GetDistancesBatch работает с парами идентификаторов городов.

В реализации Claude matching-service передаёт в GetDistancesBatch пару из city_id исполнителя и строки City из задачи:

candidates
pairs := make([][2]string, len(candidates))
for i, c := range candidates {
    pairs[i] = [2]string{c.cityID, city}
}

Получаются разные форматы в одном запросе: например city-1 и Moscowgeo-service не может корректно посчитать расстояние, и система может выбрать не ближайшего кандидата.

Это не ошибка одного поля или одного метода. Контракт должен был явно сказать, какое представление города передаётся между task-servicematching-service и geo-service: отображаемое имя или city_id.

Проигнорированное уточнение

Во время брейншторма Claude спросил, как должен выглядеть внешний вызов отказа от оффера. Я ответил: DeclineOffer(taskId). Исполнителя нужно брать из auth-токена на сервере, а не из запроса клиента.

В реализации появился метод DeclineOffer(taskId, workerId), который принимает worker_id от клиента. В итоге клиент может передать чужой worker_id и отказаться от оффера за другого исполнителя.

Моё уточнение как раз должно было закрыть этот риск, но в финальной реализации оно потерялось.

6. Почему это происходит

Проблема не в том, что Claude не прочитал файлы. Он прочитал CLAUDE.md по сервисам, задал уточняющие вопросы и написал подробную спеку. Но эти документы описывали в основном каждый сервис отдельно, а не правила, которые связывают несколько сервисов.

Вот какие правила не были явно зафиксированы:

  • новый оффер должен получать новый match_id, иначе получатель события примет его за дубликат;

  • операция, которая меняет состояние задачи и отправляет событие, должна идти через Outbox;

  • если сервис вызывает другой сервис в цикле, нужно проверить, нужен ли batch API;

  • если сценарий дальше продолжить нельзя, задача должна перейти в финальный статус, а не просто записать лог;

  • в контракте должен быть указан формат города: отображаемое имя или city_id.

End-to-end тесты помогают поймать часть таких ошибок, но обычно уже после реализации. Мне хотелось сдвинуть проверку раньше: на момент, когда LLM собирает требования и предлагает план. Для этого нужен не только Markdown с описанием сервисов, а структурный контракт: граф вызовов, контракты ручек, ключи идемпотентности, правила Outbox, batch-вызовы и переходы состояния.

Такой контракт можно валидировать на коммите, показывать в PR как понятный диф и давать LLM как контекст перед реализацией. Свободный Markdown для этого не подходит: он легко устаревает, плохо показывает структурный диф и не заставляет явно описывать правила между сервисами.

7. Что дальше

Ответ, к которому я пришёл: нужен не ещё один свободный Markdown, а машиночитаемый контракт на каждый сервис. В нём должны быть не только endpoints и зависимости, но и правила, которые обычно теряются между сервисами: ключи идемпотентности, Outbox, batch-вызовы, переходы состояния и формат данных на границах.

Во второй части я покажу /archspec:init. Он проходит по всем двенадцати сервисам, вытаскивает из кода endpoints, зависимости и топики брокера сообщений, а затем собирает для каждого сервиса YAML-контракт архитектуры. На основании этой спеки archspec генерирует C1/C2 и sequence-диаграммы, которые легко читать человеку и ревьюить в PR вместе с дифом архитектурного контракта.

В третьей части я вернусь к Smart Task Reassignment — фиче автоматического переназначения задачи после отказа фрилансера — через /archspec:investigate. Там инструмент читает контракты затронутых сервисов до реализации, предлагает изменения в архитектурной спеке и выдаёт план, где уже учтены межсервисные ограничения из этой статьи.

Оба репозитория открыты:

archspec уже можно пробовать как плагин. Если найдёте баг, неудобный сценарий или правило, которого не хватает, заводите issue в репозитории.

Часть 2 — на подходе.