8 lines
18 KiB
JSON
8 lines
18 KiB
JSON
{
|
||
"index": 64,
|
||
"slug": "editorial-2026-03-field-data-contracts",
|
||
"title": "Почему один verdict не описывает совместимость контракта данных",
|
||
"excerpt": "Один consumer прочитал новый JSON, но это не доказывает совместимость всей схемы. Разбираем проверку пары producer → consumer, строгие границы чтения и отказ при неизвестном сравнении.",
|
||
"contentHtml": "<p>После изменения JSON один consumer продолжает читать сообщения, и команда ставит схеме зелёный статус. Через день другой reader начинает отбрасывать объект: он запрещает новые поля. Ещё один consumer ждёт обязательный <code>state</code>, а producer уже отправляет только <code>phase</code>. Поле называется похоже, тест на sample проходит, но смысл и правила чтения различаются. Ошибка стоит дорого: сбой обнаруживается после раскатки, владелец находится вручную, а команда откатывает уже связанное изменение.</p>\n<p>Тезис простой: совместимость нельзя присвоить схеме в целом. Её проверяют для конкретной пары <code>producer → consumer</code>, конкретной версии и конкретного направления чтения. Для каждой пары нужно назвать contract family, baseline, candidate и capability reader. Неизвестный consumer не получает зелёный статус. Отсутствующая связь означает остановку и уточнение.</p>\n<h2>Минимальная единица решения</h2>\n<p>Список интеграций помогает найти владельцев, но не отвечает на вопрос о совместимости. Нужна одна строка review. В ней producer создаёт candidate schema, consumer читает эту форму, а gate сравнивает только заранее названные свойства. Такой scope ограничивает вывод и делает причину отказа адресной.</p>\n<pre><code>const 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};</code></pre>\n<p>Это учебный объект. Он не описывает реальный сервис, registry, сообщение или deployment. Он показывает форму решения: у comparison есть family, две формы, направление и правило reader. Если убрать любое из этих звеньев, результат нельзя расширять до общего обещания.</p>\n<figure><img src=\"/assets/editorial/2026/data-contracts-2026-producer-consumer-matrix.svg\" alt=\"Матрица связывает одну candidate schema с tolerant reader, strict reader и неизвестной парой; для каждой связи показан отдельный результат проверки.\" loading=\"lazy\" /><figcaption>Одна и та же candidate schema даёт разные результаты: tolerant reader принимает объявленное поле, strict reader останавливает проверку, неизвестная пара требует сначала назвать связь.</figcaption></figure>\n<h2>Что именно проверяет gate</h2>\n<p>Сначала gate проверяет structural слой. Обязательное поле baseline не должно исчезнуть из candidate. Его тип не должен измениться без отдельного решения. В учебном примере замена <code>state</code> на <code>phase</code> — не безопасный rename. Reader, который ищет <code>state</code>, видит удалённое required field. Близость слов не доказывает совпадение семантики.</p>\n<p>Затем gate проверяет объявленные additions. Добавление необязательного <code>priority</code> сохраняет старую обязательную поверхность, но всё равно требует проверки consumer. Tolerant reader может принимать declared additions. Strict reader может отвергать любое дополнительное поле. Тип данных сам по себе не говорит, какая политика действует на границе.</p>\n<p>Третья проверка связывает diff с manifest. Если candidate содержит <code>routingHint</code>, но карточка change его не называет, это не повод угадать намерение. Gate возвращает <code>stop-undocumented-schema-field</code>. Скрытое поле может влиять на маршрутизацию, размер сообщения или безопасность. Сначала его нужно объявить и проверить.</p>\n<p>Наконец, gate проверяет relation. Поля двух JSON-объектов нельзя сравнивать только потому, что оба объекта выглядят одинаково. Нужны family, producer, consumer и direction. Если направление не задано, sample не превращается в compatibility verdict. Результат — <code>stop-implicit-comparison</code>.</p>\n<h2>Пример fail-closed проверки</h2>\n<pre><code>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}</code></pre>\n<p>Пример синтетический. Он не читает сеть, не вызывает schema registry, не ищет consumers и не подтверждает результат в production. Функция демонстрирует порядок отказов. Сначала она требует relation, потом проверяет обязательную поверхность, затем manifest и только после этого применяет policy reader. Реальная система должна дополнить эти шаги своей схемой, тестами и наблюдаемыми входами.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class=\"table-scroll\"><table><caption>Диагностика неверного verdict</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Один consumer прочитал sample, схема объявлена совместимой</td><td>Проверили одну пару и свернули результат в общий статус</td><td>Перечислить named consumers и их policy</td><td>Разбить review на отдельные строки producer → consumer</td></tr><tr><td>Reader перестал находить <code>state</code></td><td>Required field заменили на похожее имя <code>phase</code></td><td>Сравнить обязательные поля baseline и candidate</td><td>Вернуть поле или оформить отдельную migration</td></tr><tr><td>Новый reader падает на поле <code>priority</code></td><td>Strict policy не принимает additions</td><td>Проверить capability reader, а не только тип поля</td><td>Удержать addition или расширить границу reader</td></tr><tr><td>В candidate есть <code>routingHint</code>, но в change его нет</td><td>Schema diff не связан с manifest</td><td>Сверить все добавленные поля с declared list</td><td>Остановить review и описать поле явно</td></tr><tr><td>В отчёте написано «совместимо», но direction пуст</td><td>Сравнение сделано по внешнему сходству JSON</td><td>Проверить family, producerId, consumerId и direction</td><td>Вернуть работу на описание relation</td></tr></tbody></table></div>\n<h2>Почему общий зелёный статус опасен</h2>\n<p>У change может быть пять consumers. Один принимает addition, второй запрещает его, третий относится к другой family, а четвёртый неизвестен. Общий статус «compatible» скрывает владельца решения и стирает отрицательные ветки. Такой статус допустим только как агрегат после того, как каждая известная пара получила собственный результат. Даже тогда рядом должны остаться причины stop и несопоставимые отношения.</p>\n<p>Strict reader не является неисправным. Его policy — часть контракта. Gate не должен менять её ради удобства producer. Если producer добавляет поле, есть три честных варианта: не добавлять его, мигрировать reader или выпустить отдельную форму. Пока выбор не сделан, stop полезнее зелёного предположения.</p>\n<p>Отдельно храните неизвестность. Отсутствие карточки consumer не означает tolerance. Скрытый reader нельзя объявить совместимым по умолчанию. Если связь только предполагается, сначала нужен владелец, подтверждение family и направление чтения. Это отрицательный путь механизма, а не исключение из него.</p>\n<h2>Порядок проверки</h2>\n<ol><li><strong>Зафиксируйте одну пару.</strong> Назовите producer, consumer, family и направление: кто пишет candidate и кто его читает.</li><li><strong>Сохраните baseline.</strong> Выпишите обязательные поля, типы и версию формы до изменения.</li><li><strong>Опишите candidate.</strong> Отделите сохранённые поля, удалённые поля и additions. Не трактуйте rename по сходству имён.</li><li><strong>Проверьте manifest.</strong> Каждое добавленное поле должно быть объявлено. Необъявленное поле возвращает stop.</li><li><strong>Назовите capability reader.</strong> Зафиксируйте supported version и policy для declared additions. Не выводите policy из того, что reader однажды прочитал sample.</li><li><strong>Запустите structural check.</strong> Сначала остановите удалённое required field и изменение типа. Только потом проверяйте additions.</li><li><strong>Сохраните отдельный verdict.</strong> Запишите точную причину: backward break, undocumented field, incompatible consumer или implicit comparison.</li><li><strong>Проверьте отрицательные случаи.</strong> Подайте объект без relation, с удалённым <code>state</code>, со скрытым <code>routingHint</code> и со strict reader. Каждый случай должен остановиться на своей причине.</li><li><strong>Передайте ограниченный результат.</strong> Успешная synthetic-проверка означает только hand-off на следующий review. Она не означает публикацию, раскатку или работоспособность внешней системы.</li></ol>\n<h2>Ограничения</h2>\n<p>Этот механизм не обнаруживает неизвестных consumers. Он не знает, кто хранит старую форму в архиве, какой proxy меняет payload и как асинхронная доставка обрабатывает повтор. Для этого нужны inventory, наблюдаемая маршрутизация и отдельные проверки. Compatibility gate не заменяет schema registry, consumer contract tests, миграцию данных и план возврата.</p>\n<p>Проверка required fields не покрывает всю семантику. Два поля могут иметь один тип и разные единицы измерения, часовые пояса или правила округления. Название family не доказывает значение поля. Такие условия нужно добавить в контракт отдельными правилами и тестовыми случаями. Нельзя получить полноту из короткой функции.</p>\n<p>Учебные literals не дают production-результата. Успешный вызов функции не говорит, что реальный consumer обработал candidate, что registry содержит нужную версию или что deployment завершился. Для реального изменения потребуется привязать проверку к фактическим схемам, версиям, данным и владельцам. Если вход невозможно подтвердить, результат должен остаться stop.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Проверка готова, когда независимый читатель без устных пояснений видит одну relation, baseline, candidate, required surface и policy consumer. Для каждого addition есть запись в manifest. Для каждого verdict есть причина и следующий адрес действия. Удаление обязательного поля, строгий reader, скрытое поле и пустое направление дают определённые stop-результаты. Ни один synthetic hand-off не назван разрешением на production.</p>\n<p>Практический тест готовности короткий: возьмите положительный case, удалите из него по одному звену и повторите проверку. Если объект без consumer или direction всё ещё получает зелёный результат, gate слишком либерален. Если <code>state → phase</code> проходит как косметическое изменение, structural слой слишком слаб. Если скрытый addition проходит, manifest не связан с diff. Готовность выражается не числом зелёных строк, а тем, что каждый разрыв даёт понятный stop.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://www.rfc-editor.org/rfc/rfc8927.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 8927: JSON Type Definition</a> — описывает required, optional и additional properties; не задаёт policy конкретного consumer.</li><li><a href=\"https://github.com/apache/avro/blob/8c27801dc8d42ccc00997f25c0b8f45f8d4a233e/doc/content/en/docs/%2B%2Bversion%2B%2B/Specification/_index.md#schema-resolution\" target=\"_blank\" rel=\"noopener noreferrer\">Apache Avro: Schema Resolution</a> — фиксирует различие writer и reader schema и правила resolution; не подтверждает эту учебную матрицу.</li><li><a href=\"https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-01\" target=\"_blank\" rel=\"noopener noreferrer\">JSON Schema Draft 2020-12</a> — даёт vocabulary для object properties и additionalProperties; не выдаёт общий compatibility verdict для произвольной группы readers.</li></ul>"
|
||
}
|