Skip to content

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 — и как мы его закрываем

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

  1. consumed-contract извлекается автоматически — это схема, а не пример: сравнивается структура целиком, а не один удачный случай. Ручной поддержки нет.
  2. Заглушка генерируется из спеки поставщика обязательным шагом конвейера — дисциплину проверяет механический гейт CI, а не добрая воля. Заглушка честная и свежая по построению.
  3. Provenance штампуется при генерации — netlist знает, какие проверки устарели и кого перепроверять.
  4. 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.