pinout: верификация контрактов до прода
Типичная поломка. Поставщик изменил формат ответа /balance: поле из integer стало string. Тесты
поставщика зелёные — он соответствует своей новой спеке. Тесты потребителя тоже зелёные — они гонялись
против старой заглушки. Мержим оба изменения. В проде интеграция падает на парсинге. Несовместимость
нашлась в рантайме — поздно и недетерминированно.
pinout ловит такую несовместимость до мержа, в CI — по машиночитаемым спецификациям, которые и так
лежат в Git. Клиентские библиотеки генерировать и публиковать не нужно.
Аналогия из электроники
В дизайне печатных плат pinout — это распиновка микросхемы: какие сигналы на каких ножках. Чтобы две
микросхемы заработали вместе, их распиновки должны сойтись. pinout делает то же, что проверка целостности
цепи (continuity test) на плате: убеждается, что «распиновки» потребителя и поставщика совместимы до
того, как сервисы уедут в прод.
Роль распиновки играет спецификация сервиса: OpenAPI для синхронного REST, AsyncAPI для асинхронного обмена сообщениями через брокер.
Ключевое условие
Спецификация безопасна как источник истины ровно при одном условии:
Каждый сервис действительно соответствует своей спеке — и это доказано его компонентными тестами.
Компонентные тесты проверяют сервис как чёрный ящик против его OpenAPI/AsyncAPI и README, а внешние зависимости заменяют заглушками, которые говорят по настоящему протоколу: HTTP-поставщик — HTTP-заглушка, брокер-поставщик — брокер-заглушка. Отсюда следствие, на котором держится весь pinout:
если оба сервиса соответствуют своим спекам, то совместимость спек — это реальная совместимость сервисов, а не совместимость пожеланий.
Откуда берётся ожидание потребителя
Главный вопрос: с чем сравнивать спеку поставщика? На первый взгляд — с «ожидаемой спекой» потребителя. Но реальный потребитель (например, брокер-интеграция, которая под капотом ходит по REST) отдельную «ожидаемую спеку» не ведёт. Его ожидание уже записано — в заглушках его компонентных тестов: вот какой запрос я отправляю, вот такой ответ жду.
Из этих заглушек мы автоматически извлекаем consumed-contract — схему того, что потребитель реально
использует: какие операции и каналы вызывает, какие поля отправляет и читает. Это не пример-заглушка и не
рукопись, которую надо поддерживать, — это машинно-сравнимая схема, и получается она с нулевой ручной
работой.
Модель: одинаково для sync и async
Синхронный — pinout-openapi |
Асинхронный — pinout-asyncapi |
|
|---|---|---|
| Спека поставщика (истина) | OpenAPI 3.x | AsyncAPI 3.0 |
| Единица контракта | операция (path+method) |
канал + операция (send/receive) |
| Что сверяем | схемы request / response | схемы payload сообщений |
| Направление сверки | запрос потребителя — против схемы запроса поставщика; ответ поставщика — против схемы, которую ждёт потребитель | send — как запрос, receive — как ответ |
У обоих валидаторов одно и то же ядро:
- источник истины — спека поставщика (её читает протокол-парсер);
- заглушка генерируется из спеки поставщика — обязательный шаг конвейера потребителя, поэтому заглушка всегда честная и свежая;
consumed-contractизвлекается из заглушек автоматически и несёт штамп происхождения (provenance): поставщик, версия его спекиprovider_versionи хеш этой версииcaptured_hash;- сравнение идёт схема-с-схемой, поле за полем — вложенность, типы, enum, format, nullable; глубина сравнения достаётся бесплатно от валидации схем.
Два направления
FORWARD — pinout-openapi / asyncapi REVERSE — pinout-netlist
(PR потребителя: «я ещё совместим?») (PR поставщика: «кого сломаю?»)
consumed-contract потребителя спека поставщика v_old
(из заглушек + provenance) │ диф версий:
│ схема против схемы ▼ oasdiff + R2/R3
▼ спека поставщика v_new
спека поставщика (истина) │
│ ▼
▼ граф зависимостей →
совместим? ✅ / ❌ кто из живых потребителей задет
- FORWARD (PR потребителя): «я ещё совместим с текущей спекой поставщика?» — статическая сверка
consumed-contractпротив схемы поставщика. - REVERSE (PR поставщика): «кого я сломаю этим изменением контракта?» — это задача
pinout-netlist. Обе версии спеки приходят прямо в запросе (сам netlist спеки не хранит), диф версий считается черезoasdiffплюс два собственных правила — удалённое поле ответа и новое обязательное поле запроса, которыхoasdiffне считает ломающими. Дальше по графу «кто кого потребляет» netlist показывает, какие живые потребители задеты. Если хешcaptured_hashна ребре не совпадает с текущей спекой поставщика, ребро помечается устаревшим (stale) — пару надо перепроверить.
Полные C4-диаграммы и алгоритмы — в концепте pinout.
Единый формат отчёта
Оба валидатора после проверки печатают JSON-отчёт в одном и том же формате — он называется канон 1.1 и
описан в pinout-openapi/docs/report-format.md.
В отчёте три главные вещи:
- результат: совместима пара или нет. Совместимость означает пустой список ошибок — это правило зашито в формат, «полусовместимых» отчётов не бывает;
- ошибки, и у каждой указан субъект — операция или канал, к которым она относится;
- provenance — штамп происхождения: какой поставщик, какая версия его спеки, хеш этой версии.
Из этих отчётов netlist строит граф: субъекты становятся рёбрами «потребитель → поставщик», а вердикт по ребру простой — «нет ошибки с этим субъектом». Есть один нюанс: если пара совместима, ошибок в отчёте нет, а вместе с ними нет и субъектов. Поэтому отчёт отправляют в netlist вместе со списком операций, которые потребитель реально использует, — в CI потребителя этот список и так известен из конфига пары.
Это индустрийная best-practice
Forward-направление pinout — это bi-directional contract testing. Индустрийный аналог — PactFlow Bi-Directional (pactflow.io): контракт потребителя генерируют из мок-инструмента (Wiremock/Pact), контракт поставщика — это OpenAPI, и они статически сверяются до уровня поля, без воспроизведения запросов против провайдера. PactFlow отчитываются о >50% экономии усилий против full-CDC (basic guide, Stoplight; getting started, Sngular).
Отличия pinout: open-source, живёт в Git, работает со спеками как есть, добавляет provenance и граф
зависимостей; consumed-contract извлекается автоматически и обязателен как шаг конвейера, а не
поддерживается руками.
Слабое место bi-directional — и как мы его закрываем
Слабое место подхода известно: контракт потребителя точен ровно настолько, насколько точны его заглушки. Мы закрываем это устройством процесса, а не дисциплиной разработчиков:
consumed-contractизвлекается автоматически — это схема, а не пример: сравнивается структура целиком, а не один удачный случай. Ручной поддержки нет.- Заглушка генерируется из спеки поставщика обязательным шагом конвейера — дисциплину проверяет механический гейт CI, а не добрая воля. Заглушка честная и свежая по построению.
- Provenance штампуется при генерации — netlist знает, какие проверки устарели и кого перепроверять.
- Self-conformance поставщика — обязательный CI-гейт. PactFlow этого требует, но не может заставить чужие организации — а внутри своей инфраструктуры можем.
Всё начинается с агента и процесса
Разработку каждого сервиса — и самих валидаторов pinout — ведёт AI-агент по стандарту разработки: сначала контракт и спецификация, потом код; компонентные тесты против спеки и README; модули с одним входом и одним выходом; короткоживущие ветки и мелкие PR; версии по совместимости. Стандарт исполняемый и не привязан к конкретному инструменту: правила доставляются в любой AI-харнес, а механические гейты не дают шаги обойти.
Когда каждый компонент разработан по стандарту, каждый сервис соответствует своей спецификации — и вся конструкция получается простой и гармоничной машинерией: спеки в Git доказаны тестами, валидаторы сверяют пары, netlist следит за изменениями во времени. Ключевое условие из начала статьи — не пожелание, а свойство конвейера.
Что уже работает
Обновлено в октябре 2026. На момент публикации это был план; сейчас это работающая связка:
- pinout-openapi — проверяет пары по OpenAPI (правила R1–R4), отчёты в формате 1.1;
- pinout-asyncapi — проверяет пары по AsyncAPI (правила R1–R9), первый выпуск — v0.0.1;
- pinout-netlist — строит граф «кто кого потребляет» и отвечает, кто сломается от изменения спеки поставщика; контракт API заморожен, выпуск v0.1.0;
- вся тройка прогоняется сквозным E2E-тестом: 7 сценариев, где валидаторы и netlist работают вместе, отдельной джобой в CI;
- весь код открыт под лицензией BSD-3-Clause.
Чего пока нет — и это честно записано в бэклоге: impact-анализ для асинхронных поставщиков, отдельная
CLI-утилита, автогенерация consumed-contract в харнесе и визуализация графа.
Итог. pinout — это спеки в Git, компонентные тесты и автоматическая сверка пары. FORWARD ловит
несовместимость пары здесь и сейчас (схема против схемы), REVERSE — ломающие изменения во времени (диф
версий + граф). Оба направления реализованы и выпущены, и стоят они на одном фундаменте — доказанной
соответственности каждого сервиса своей спеке. Несовместимость находится до мержа, на языке,
независимом от стека, без клиентских библиотек и лишней поверхности supply-chain.