{ "index": 65, "slug": "editorial-2026-03-mechanism-data-contracts", "title": "Совместимость схемы — это направление, а не номер версии", "excerpt": "Как compatibility gate отделяет направление чтения, обязательные поля, объявленные additions и возможности consumer — и почему неопределённость должна останавливать проверку.", "contentHtml": "
Симптом обычно выглядит безобидно: producer выпускает схему v2, consumer видит знакомые поля, а в ревью появляется короткое слово compatible. Затем старый reader получает данные с новым полем, не находит обязательный state или встречает поле другого типа. Ошибка проявляется уже на границе сервисов. Цена — отклонённые сообщения, неверные значения по умолчанию, ручная миграция и спор о том, что именно обещала версия.
Тезис простой: совместимость нельзя вычислять по номеру версии, пересечению имён или удачному примеру сериализации. Сначала нужно назвать направление, две точки схемы и конкретного consumer. Затем отдельно проверить обязательную поверхность, объявленные additions и capability reader. Если хотя бы одна часть неизвестна, gate должен остановиться. Такой отказ полезнее зелёного статуса без объяснения.
\nНазовём baseline старой схемой и candidate новой схемой. В выбранном направлении фиксированный producer создаёт candidate, а фиксированный consumer читает эту форму, опираясь на baseline как на точку отсчёта. Это не единственное возможное направление. Новый reader может читать старые данные, но это уже другой вопрос и другая карточка сравнения.
\nМинимальная запись отношения содержит пять значений: direction, family, baselineVersion, candidateVersion и consumerId. family не даёт сравнить случайные JSON-объекты только потому, что у них совпали ключи. Версии закрепляют обе точки. consumerId не позволяет заменить проверяемого reader абстрактным «клиентом». Пустое или изменённое значение даёт stop-implicit-comparison.
Первая проверка смотрит на обязательную поверхность baseline. Если required-поле исчезло из candidate или сменило тип, старый reader больше не получает обещанную форму. Например, замена state на phase может казаться переименованием с тем же смыслом. Gate не угадывает смысл имён. Для reader поле state отсутствует, поэтому результат — stop-backward-incompatible-schema.
Вторая проверка смотрит на новые поля. Candidate может сохранить id и state, но добавить priority. Это не разрушает обязательную поверхность. Однако producer должен явно назвать addition в manifest. Скрытое routingHint, появившееся в candidate без записи в manifest, даёт stop-undocumented-schema-field. Gate сначала требует объяснить новую поверхность, а потом спрашивает, принимает ли её reader.
Третья проверка смотрит на capability consumer. Tolerant reader может разрешать declared additions. Strict reader может отклонять неизвестные поля. Слово optional в схеме producer не меняет policy reader автоматически. Если strict consumer не принимает priority, результат — stop-incompatible-consumer. Gate не удаляет поле на лету и не придумывает adapter. Команда отдельно выбирает изменение reader, разделение формы, задержку candidate или миграцию.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Для двух схем написано compatible, но не указано направление | Один boolean склеил разные отношения producer и reader | Проверить direction, family, baseline, candidate и consumerId | Остановить как stop-implicit-comparison и оформить relation |
В candidate нет обязательного state | Required-поле baseline удалили или переименовали | Построить field map и сравнить required surface baseline | Вернуть поле или назвать отдельную migration |
Новый routingHint есть в данных, но нет в описании изменения | Фактический diff шире declared manifest | Сверить additions candidate с declaredAddedFields | Остановить как stop-undocumented-schema-field |
| Tolerant reader проходит, strict reader падает | Capability зависит от конкретного consumer, а не от версии producer | Проверить policy declared additions у named reader | Изменить reader, форму или завести migration |
| Отчёт говорит «75% совместимо» | Агрегат скрыл разные причины и владельцев следующего шага | Проверить исходный status и reason одного сравнения | Сохранить конкретный stop-status вместо процента |
Ниже — учебный JavaScript-подобный пример. Он не подключается к registry, сети, файловой системе, CI или production data. fixedCase возвращает заранее известный объект, а review выполняет только описанные проверки. Пример показывает форму решения, но не доказывает совместимость реального формата.
const baseline = {\n version: '1.0',\n required: { id: 'string', state: 'string' }\n};\n\nconst candidate = {\n version: '2.0',\n required: { id: 'string', phase: 'string' },\n additions: []\n};\n\nconst relation = {\n direction: 'backward',\n family: 'orders',\n baselineVersion: '1.0',\n candidateVersion: '2.0',\n consumerId: 'orders-reader'\n};\n\nconst report = review({ baseline, candidate, relation });\nconsole.log(report);\n// {\n// status: 'stop-backward-incompatible-schema',\n// removedRequiredFields: ['state'],\n// nextAction: 'retain-required-baseline-field-or-name-a-separate-migration'\n// }\nВажна не длина функции, а граница вывода. Gate обнаружил отсутствие state. Он не объявил новый phase эквивалентом, не выдал разрешение на deploy и не выбрал стратегию миграции. Следующий шаг зависит от владельца контракта и требований старого reader.
Положительный учебный случай тоже ограничен. Если candidate сохраняет id и state, добавляет объявленный priority, а named reader допускает declared additions, gate может вернуть synthetic hand-off. Это означает только то, что фиксированная проверка закончилась положительно. Это не означает, что parser, registry, права, нагрузка и выпуск в реальной системе готовы.
Если сначала спросить reader, принимает ли он неизвестные поля, tolerant policy может скрыть неописанное изменение producer. Поэтому gate сначала устанавливает отношение, затем проверяет required surface, потом сверяет manifest и только после этого проверяет capability.
\nТак распределяется ответственность. Producer называет новую поверхность. Сравнение проверяет буквальную форму. Manifest связывает diff с намерением. Consumer описывает границу принятия. Ни один слой не подменяет другой. Если переставить шаги, зелёный результат может появиться раньше, чем команда поймёт, что именно она выпускает.
\nconsumerId и остановите сравнение при неизвестной или неполной связи.Нельзя считать gate работающим только по зелёному fixed case. Передайте объект без direction. Ожидайте stop-implicit-comparison. Удалите state из candidate. Ожидайте stop-backward-incompatible-schema. Добавьте routingHint без manifest. Ожидайте stop-undocumented-schema-field. Замените tolerant reader на strict reader. Ожидайте stop-incompatible-consumer.
Каждый отказ должен сохранять следующий шаг. Неизвестное отношение требует уточнить карточку. Удалённое required-поле требует вернуть поверхность или назвать миграцию. Скрытый addition требует обновить описание изменения или убрать поле. Несовместимый reader требует решения владельца consumer. Общий статус «не прошёл» не даёт команде достаточного действия.
\nЭтот gate проверяет узкое отношение между фиксированными описаниями. Он не извлекает схемы из registry, не знает все deployment-версии, не проверяет реальные payloads и не подтверждает, что consumer честно описал свои потребности. Он также не решает семантическое изменение: строка state=active может сохранить тип и имя, но начать означать другой бизнес-статус.
Он не заменяет contract tests, миграцию данных, нагрузочную проверку, security review, SLA и план отката. JSON Schema, JTD и Avro дают полезные понятия для формы и чтения, но не определяют статусы этого gate. Поэтому результат нужно читать узко: механизм сделал одно сравнение явным и остановил неизвестность. Он не управляет релизом.
\nДля выбранной пары есть заполненные family, direction, baseline, candidate и consumerId. Gate возвращает отдельные результаты для неизвестного отношения, разрушенной required surface, скрытого addition и несовместимого reader. Есть один положительный учебный случай и отрицательные случаи для каждой остановки. Каждый report содержит reason и next action. Положительный report прямо говорит synthetic hand-off и не выдаёт право на deploy.
Если команда не может воспроизвести эти статусы на фиксированных входах или не знает, какой reader проверяется, критерий не выполнен. Номер версии и зелёный процент не заменяют evidence. Готовность здесь означает, что вопрос о совместимости имеет направление, named участников, отдельную причину и проверяемый следующий шаг.
\n