{ "index": 64, "slug": "editorial-2026-03-field-data-contracts", "title": "Почему один verdict не описывает совместимость контракта данных", "excerpt": "Один consumer прочитал новый JSON, но это не доказывает совместимость всей схемы. Разбираем проверку пары producer → consumer, строгие границы чтения и отказ при неизвестном сравнении.", "contentHtml": "
После изменения JSON один consumer продолжает читать сообщения, и команда ставит схеме зелёный статус. Через день другой reader начинает отбрасывать объект: он запрещает новые поля. Ещё один consumer ждёт обязательный state, а producer уже отправляет только phase. Поле называется похоже, тест на sample проходит, но смысл и правила чтения различаются. Ошибка стоит дорого: сбой обнаруживается после раскатки, владелец находится вручную, а команда откатывает уже связанное изменение.
Тезис простой: совместимость нельзя присвоить схеме в целом. Её проверяют для конкретной пары producer → consumer, конкретной версии и конкретного направления чтения. Для каждой пары нужно назвать contract family, baseline, candidate и capability reader. Неизвестный consumer не получает зелёный статус. Отсутствующая связь означает остановку и уточнение.
Список интеграций помогает найти владельцев, но не отвечает на вопрос о совместимости. Нужна одна строка review. В ней producer создаёт candidate schema, consumer читает эту форму, а gate сравнивает только заранее названные свойства. Такой scope ограничивает вывод и делает причину отказа адресной.
\nconst review = {\n family: 'orders-v1',\n baseline: { id: 'string', state: 'string', note: 'optional' },\n candidate: { id: 'string', state: 'string', note: 'optional', priority: 'integer' },\n producerId: 'producer-1.1',\n consumerId: 'reader-tolerant-v1',\n direction: 'producer-writes-consumer-reads',\n policy: 'declared-additions-accepted'\n};\nЭто учебный объект. Он не описывает реальный сервис, registry, сообщение или deployment. Он показывает форму решения: у comparison есть family, две формы, направление и правило reader. Если убрать любое из этих звеньев, результат нельзя расширять до общего обещания.
\nСначала gate проверяет structural слой. Обязательное поле baseline не должно исчезнуть из candidate. Его тип не должен измениться без отдельного решения. В учебном примере замена state на phase — не безопасный rename. Reader, который ищет state, видит удалённое required field. Близость слов не доказывает совпадение семантики.
Затем gate проверяет объявленные additions. Добавление необязательного priority сохраняет старую обязательную поверхность, но всё равно требует проверки consumer. Tolerant reader может принимать declared additions. Strict reader может отвергать любое дополнительное поле. Тип данных сам по себе не говорит, какая политика действует на границе.
Третья проверка связывает diff с manifest. Если candidate содержит routingHint, но карточка change его не называет, это не повод угадать намерение. Gate возвращает stop-undocumented-schema-field. Скрытое поле может влиять на маршрутизацию, размер сообщения или безопасность. Сначала его нужно объявить и проверить.
Наконец, gate проверяет relation. Поля двух JSON-объектов нельзя сравнивать только потому, что оба объекта выглядят одинаково. Нужны family, producer, consumer и direction. Если направление не задано, sample не превращается в compatibility verdict. Результат — stop-implicit-comparison.
function reviewCompatibility(item) {\n if (!item.family || !item.producerId || !item.consumerId || !item.direction) {\n return {\n status: 'stop-implicit-comparison',\n nextAction: 'name-contract-relation'\n };\n }\n\n const removed = requiredFields(item.baseline)\n .filter((field) => !(field in item.candidate));\n\n if (removed.length > 0) {\n return {\n status: 'stop-backward-incompatible-schema',\n removedRequiredFields: removed\n };\n }\n\n if (hasUndeclaredAddedFields(item)) {\n return {\n status: 'stop-undocumented-schema-field',\n nextAction: 'update-change-manifest'\n };\n }\n\n if (item.policy === 'declared-additions-rejected' && hasAddedFields(item)) {\n return {\n status: 'stop-incompatible-consumer',\n nextAction: 'hold-addition-or-migrate-reader'\n };\n }\n\n return {\n status: 'synthetic-compatibility-review-hand-off'\n };\n}\nПример синтетический. Он не читает сеть, не вызывает schema registry, не ищет consumers и не подтверждает результат в production. Функция демонстрирует порядок отказов. Сначала она требует relation, потом проверяет обязательную поверхность, затем manifest и только после этого применяет policy reader. Реальная система должна дополнить эти шаги своей схемой, тестами и наблюдаемыми входами.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Один consumer прочитал sample, схема объявлена совместимой | Проверили одну пару и свернули результат в общий статус | Перечислить named consumers и их policy | Разбить review на отдельные строки producer → consumer |
Reader перестал находить state | Required field заменили на похожее имя phase | Сравнить обязательные поля baseline и candidate | Вернуть поле или оформить отдельную migration |
Новый reader падает на поле priority | Strict policy не принимает additions | Проверить capability reader, а не только тип поля | Удержать addition или расширить границу reader |
В candidate есть routingHint, но в change его нет | Schema diff не связан с manifest | Сверить все добавленные поля с declared list | Остановить review и описать поле явно |
| В отчёте написано «совместимо», но direction пуст | Сравнение сделано по внешнему сходству JSON | Проверить family, producerId, consumerId и direction | Вернуть работу на описание relation |
У change может быть пять consumers. Один принимает addition, второй запрещает его, третий относится к другой family, а четвёртый неизвестен. Общий статус «compatible» скрывает владельца решения и стирает отрицательные ветки. Такой статус допустим только как агрегат после того, как каждая известная пара получила собственный результат. Даже тогда рядом должны остаться причины stop и несопоставимые отношения.
\nStrict reader не является неисправным. Его policy — часть контракта. Gate не должен менять её ради удобства producer. Если producer добавляет поле, есть три честных варианта: не добавлять его, мигрировать reader или выпустить отдельную форму. Пока выбор не сделан, stop полезнее зелёного предположения.
\nОтдельно храните неизвестность. Отсутствие карточки consumer не означает tolerance. Скрытый reader нельзя объявить совместимым по умолчанию. Если связь только предполагается, сначала нужен владелец, подтверждение family и направление чтения. Это отрицательный путь механизма, а не исключение из него.
\nstate, со скрытым routingHint и со strict reader. Каждый случай должен остановиться на своей причине.Этот механизм не обнаруживает неизвестных consumers. Он не знает, кто хранит старую форму в архиве, какой proxy меняет payload и как асинхронная доставка обрабатывает повтор. Для этого нужны inventory, наблюдаемая маршрутизация и отдельные проверки. Compatibility gate не заменяет schema registry, consumer contract tests, миграцию данных и план возврата.
\nПроверка required fields не покрывает всю семантику. Два поля могут иметь один тип и разные единицы измерения, часовые пояса или правила округления. Название family не доказывает значение поля. Такие условия нужно добавить в контракт отдельными правилами и тестовыми случаями. Нельзя получить полноту из короткой функции.
\nУчебные literals не дают production-результата. Успешный вызов функции не говорит, что реальный consumer обработал candidate, что registry содержит нужную версию или что deployment завершился. Для реального изменения потребуется привязать проверку к фактическим схемам, версиям, данным и владельцам. Если вход невозможно подтвердить, результат должен остаться stop.
\nПроверка готова, когда независимый читатель без устных пояснений видит одну relation, baseline, candidate, required surface и policy consumer. Для каждого addition есть запись в manifest. Для каждого verdict есть причина и следующий адрес действия. Удаление обязательного поля, строгий reader, скрытое поле и пустое направление дают определённые stop-результаты. Ни один synthetic hand-off не назван разрешением на production.
\nПрактический тест готовности короткий: возьмите положительный case, удалите из него по одному звену и повторите проверку. Если объект без consumer или direction всё ещё получает зелёный результат, gate слишком либерален. Если state → phase проходит как косметическое изменение, structural слой слишком слаб. Если скрытый addition проходит, manifest не связан с diff. Готовность выражается не числом зелёных строк, а тем, что каждый разрыв даёт понятный stop.