Часть 5 · ~11 мин

Сквозная фича: несколько сервисов и NuGet-пакет

Фича, которая не помещается в репозиторий

Задача: показывать покупателю трекинг доставки. Что затронуто:

  • orders-service — публикует событие OrderTrackingUpdated;
  • notifications-service — консьюмит его и шлёт уведомление;
  • Syn.Contracts (NuGet) — тип события живёт здесь, его потребляют оба сервиса.

Три репозитория, три PR, один контракт. В варианте «спека в каждом репо» этот контракт пришлось бы описать трижды — и три копии разъехались бы к концу квартала. Здесь начинает окупаться store из части 3.

Ставим store — один раз на команду

Кто-то один (у «Синтеза» — Даша) создаёт репозиторий планирования:

openspec store setup syn-specs --path ~/openspec/syn-specs \
  --remote git@github.com:sintez/syn-specs.git
git -C ~/openspec/syn-specs push -u origin main

--remote записывает URL клона в файл идентичности store — каждый будущий клон «знает», откуда он, и подсказки для коллег генерируются готовыми к копипасту.

Каждый инженер — один раз на машину:

git clone git@github.com:sintez/syn-specs.git ~/openspec/syn-specs
openspec store register ~/openspec/syn-specs

Важно понимать модель: store — это обычный git-репозиторий. OpenSpec никогда сам не клонирует, не пуллит и не пушит. Изменение спеки в store — это ветка, PR и ревью, ровно как код. Устаревший локальный клон показывает устаревшие спеки, пока вы не сделаете git pull — это не баг, это git.

Код-репо декларируют связь (мы выбрали гибрид, поэтому references, а не store:):

# orders-service/openspec/config.yaml — и так же в notifications-service, syn-libs
references:
  - syn-specs

Двухслойная схема: контракт наверху, реализация внизу

Слой 1. Общий контракт — изменение в store

openspec new change add-delivery-tracking --store syn-specs

Спека этого изменения описывает поведение на границе компонентов — и только его:

### Requirement: OrderTrackingUpdated event
Событие ДОЛЖНО содержать OrderId, TrackingNumber, Carrier
и Status (Registered | InTransit | Delivered).

#### Scenario: Смена статуса перевозчиком
- GIVEN заказ с зарегистрированным трек-номером
- WHEN перевозчик сообщает новый статус
- THEN orders-service публикует OrderTrackingUpdated
- AND notifications-service отправляет покупателю уведомление

Этот change ревьюится PR-ом в репозитории syn-specs. Ревьюеры — владельцы обоих сервисов и Настя как владелица Syn.Contracts: контракт согласован до того, как написана первая строка кода.

Слой 2. Реализация — локальные изменения в каждом репо

После мержа контракта в основные спеки store каждый репозиторий заводит свой маленький change:

cd ~/src/syn-libs
qwen> /opsx-propose add-tracking-event-contract   # тип события в Syn.Contracts

cd ~/src/orders-service
qwen> /opsx-propose implement-tracking-publisher

cd ~/src/notifications-service
qwen> /opsx-propose implement-tracking-consumer

Благодаря references агент в каждом репозитории видит индекс спек store — с готовой командой openspec show order-tracking --type spec --store syn-specs. Локальный proposal цитирует общий контракт, а его tasks.md описывает только работу этого компонента. Дальше в каждом репо — обычный цикл из части 4: apply, PR, archive. Три PR тестируются, ревьюятся и мержатся независимо.

Порядок раскатки диктует NuGet: сначала Syn.Contracts (новая версия пакета), затем publisher, затем consumer — или в любом порядке, если событие спроектировано обратно-совместимым. Это решение тоже место в спеке контракта, а не в чьей-то голове.

NuGet-библиотека: два вида требований — два адреса

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

ТребованиеПримерГде спека
Публичный контракт пакета«OrderTrackingUpdated обязан сериализоваться как raw JSON без envelope»store syn-specs — его читают все потребители
Реализация пакета«внутренний кеш резолвера типов не аллоцирует на горячем пути»openspec/ в репозитории syn-libs — это дело Насти и её ревью

Потребитель пакета никогда не должен ходить в чужой репозиторий, чтобы узнать, что ему обещано. Обещания — в store; как обещание выполнено — приватное дело библиотеки.

Гигиена процесса

  • openspec doctor — проверка здоровья: все ли referenced stores зарегистрированы на этой машине; каждая находка — с готовой командой-фиксом.
  • openspec context — «с чем я сейчас работаю»: корень + referenced stores; --json для агентов.
  • Worksets — личное удобство: openspec workset create tracking --member ~/openspec/syn-specs --member ~/src/orders-service --tool code открывает планирование и код в одном окне IDE. Не коммитится, ничего не решает — просто удобный вид.
  • Связывайте PR-ы: в описании каждого implement-PR — ссылка на PR контракта в store. Ревьюер должен видеть, какой версии контракта соответствует реализация.

Кейс «Синтез». Первый же прогон поймал классику: в контракте Status был enum из трёх значений, а перевозчик присылает семь. Настя увидела это на ревью контракта в store — до того, как тип уехал в NuGet-пакет и в два сервиса. Цена правки: один комментарий в PR. Цена той же правки после релиза пакета: три PR и миграция.

Что дальше

Механика работает end-to-end. Осталось самое сложное — люди: как раскатать процесс на команду так, чтобы через месяц он не умер. Часть 6, финальная.