Files
progcode/editorial/agent-rewrites/066.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
18 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 66,
"slug": "editorial-2026-03-practice-data-contracts",
"title": "Изменение схемы без устных договорённостей: как проверить контракт данных",
"excerpt": "Практический маршрут для изменения JSON-контракта: зафиксировать baseline, candidate, manifest, направление чтения и конкретного consumer до передачи изменения дальше.",
"contentHtml": "<p>Продюсер добавляет поле в JSON-ответ. Потребитель узнаёт об этом из сообщения в чате. Через несколько часов строгий парсер отклоняет объект с новым ключом, а команда спорит, было ли поле обязательным, кто согласовал изменение и какую версию нужно откатить. Цена ошибки складывается из простоя, ручного восстановления данных и времени на поиск настоящего контракта.</p>\n<p>Проблема начинается не с синтаксиса схемы. Она начинается с неявного обещания: «новое поле необязательное, значит всё совместимо». Это утверждение неполно. Нужно назвать исходную форму, новую форму, направление чтения, producer и конкретного consumer. Только после этого можно решить, additive change это или несовместимое изменение.</p>\n<h2>Тезис: версия не заменяет проверку</h2>\n<p>Номер <code>v1.1</code> связывает две точки во времени, но не отвечает на главный вопрос. Может ли reader, рассчитанный на baseline, принять candidate? Ответ зависит от обязательных полей, типов, дополнительных ключей и правил самого reader. Один consumer игнорирует незнакомые поля. Другой отвергает их. Одинаковый JSON для них имеет разный результат.</p>\n<p>Контракт данных — это не только схема. Это схема вместе с владельцем записи, ожидаемым reader, направлением совместимости и правилом изменения. Для практической проверки достаточно начать с одной пары: producer создаёт candidate, named consumer читает его как продолжение baseline. Остальные потребители требуют отдельных проверок.</p>\n<h2>Механизм: сравнить пару, а не два файла</h2>\n<p>Сначала зафиксируйте <code>baseline</code> — форму, которую уже читает потребитель. Затем опишите <code>candidate</code> — форму после изменения. В manifest перечислите добавленные, удалённые и изменённые по типу поля. Направление <code>backward</code> в этом материале означает: старый reader получает новую запись. Это не означает, что новый reader обязательно прочитает старую запись.</p>\n<p>Проверка должна идти в том же порядке. Сначала она убеждается, что обязательная поверхность baseline не исчезла. Затем проверяет, что каждое новое поле названо в manifest. После этого она спрашивает capability конкретного consumer: принимает ли он дополнительные ключи. Если входные данные не называют направление или consumer, проверка останавливается. Пустое сравнение нельзя считать зелёным результатом.</p>\n<figure><img src=\"/assets/editorial/2026/data-contracts-2026-contract-evolution.svg\" alt=\"Схема проверки изменения контракта данных: baseline и candidate проходят через manifest и compatibility gate к tolerant или strict consumer\"><figcaption>Учебная иллюстрация показывает, почему additive change зависит не только от candidate, но и от правил reader. Красная ветка означает остановку до передачи изменения дальше.</figcaption></figure>\n<h2>Минимальный пример</h2>\n<p>Ниже — учебная функция. Она не подключается к registry, не читает реальные схемы и не доказывает совместимость сервиса. Её задача — сделать порядок решений видимым.</p>\n<pre><code>const baseline = {\n id: { type: 'string', required: true },\n state: { type: 'string', required: true },\n note: { type: 'string', required: false },\n};\n\nconst candidate = {\n ...baseline,\n priority: { type: 'number', required: false },\n};\n\nconst change = {\n direction: 'backward',\n producer: 'work-item-api',\n consumer: 'billing-worker-v1',\n added: ['priority'],\n removed: [],\n changed: [],\n};\n\nfunction review({ baseline, candidate, change, acceptsAdditional }) {\n const requiredLost = Object.entries(baseline)\n .filter(([name, field]) =&gt; field.required &amp;&amp; !candidate[name])\n .map(([name]) =&gt; name);\n\n if (!change.direction || !change.consumer) {\n return { status: 'stop-implicit-comparison' };\n }\n if (requiredLost.length &gt; 0) {\n return { status: 'stop-backward-incompatible-schema', requiredLost };\n }\n\n const actualAdded = Object.keys(candidate)\n .filter((name) =&gt; !baseline[name]);\n const undocumented = actualAdded\n .filter((name) =&gt; !change.added.includes(name));\n\n if (undocumented.length &gt; 0) {\n return { status: 'stop-undocumented-schema-field', undocumented };\n }\n if (actualAdded.length &gt; 0 &amp;&amp; !acceptsAdditional) {\n return { status: 'stop-incompatible-consumer' };\n }\n return { status: 'synthetic-compatibility-review-hand-off' };\n}\n\nconsole.log(review({\n baseline,\n candidate,\n change,\n acceptsAdditional: true,\n}));\n// { status: 'synthetic-compatibility-review-hand-off' }</code></pre>\n<p>В примере <code>priority</code> не удаляет <code>id</code> и <code>state</code>, поэтому структурная проверка проходит. Поле также записано в manifest. Tolerant consumer принимает дополнительные ключи, и функция возвращает ограниченный положительный статус. Этот статус означает только одно: фиксированная учебная пара прошла перечисленные правила. Он не означает deploy, миграцию базы или успешную обработку реального сообщения.</p>\n<p>Теперь измените candidate: замените <code>state</code> на <code>phase</code>. Функция вернёт <code>stop-backward-incompatible-schema</code>. Имена похожи, но старый consumer всё ещё ищет обязательное поле <code>state</code>. Не пытайтесь исправить этот результат добавлением номера версии. Здесь нужен отдельный план миграции или сохранение старого поля на период перехода.</p>\n<p>Третий случай — strict consumer. Оставьте additive candidate, но передайте <code>acceptsAdditional: false</code>. Результат станет <code>stop-incompatible-consumer</code>. Поле может быть корректным для одного reader и запрещённым для другого. Поэтому слово «optional» должно описывать не только schema declaration, но и поведение потребителя.</p>\n<h2>Симптомы и действия</h2>\n<table><thead><tr><th>Симптом</th><th>Причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>Старый reader падает на новом ключе</td><td>Consumer запрещает дополнительные поля</td><td>Проверить его parser policy и тест на candidate</td><td>Оставить поле вне старой формы, изменить reader или ввести отдельный контракт</td></tr><tr><td>После rename пропало значение</td><td>Удалено обязательное поле baseline</td><td>Сравнить required-поля baseline и candidate</td><td>Вернуть поле на переходный период или спроектировать миграцию</td></tr><tr><td>В review нет единого вердикта</td><td>Не задано направление или named consumer</td><td>Проверить manifest: direction, producer, consumer</td><td>Остановить изменение и сначала определить пару</td></tr><tr><td>Новый ключ появился без обсуждения</td><td>Diff шире заявленного manifest</td><td>Сравнить фактические ключи candidate со списком added</td><td>Назвать поле и его смысл либо удалить его из candidate</td></tr><tr><td>Тип остался строкой, но смысл изменился</td><td>Семантический breaking change не виден в structural diff</td><td>Сверить единицы, timezone, enum и документацию consumer</td><td>Дать новое имя или подготовить явную миграцию значения</td></tr></tbody></table>\n<h2>Почему schema validation недостаточно</h2>\n<p>JSON Schema описывает структуру экземпляра: свойства, типы и дополнительные свойства. Это полезная граница, но schema validation не знает, какой сервис владеет полем, кто читает объект и можно ли менять смысл значения без новой версии. Две схемы могут быть валидными по отдельности и всё равно не образовывать безопасную пару.</p>\n<p>Та же граница видна в JSON Type Definition. В RFC 8927 required properties и optionalProperties разделены явно. Режим дополнительных свойств тоже задаётся отдельно. Это хороший словарь для разговора о форме объекта. Но RFC не выбирает migration policy вашей команды и не сообщает, выдержит ли конкретный consumer изменение.</p>\n<p>В форматах с writer и reader schemas направление становится ещё заметнее. Apache Avro описывает schema resolution между схемой записи и схемой чтения. Для JSON-сервисов конкретные правила будут другими, но принцип переносим: нельзя обсуждать compatibility без указания стороны, которая пишет, и стороны, которая читает.</p>\n<h2>Порядок действий</h2>\n<ol><li><strong>Зафиксируйте baseline.</strong> Укажите идентификатор и версию формы. Выпишите обязательные поля и их типы.</li><li><strong>Опишите candidate.</strong> Покажите полную новую форму, а не только короткий diff. Не меняйте baseline задним числом.</li><li><strong>Составьте manifest.</strong> Перечислите <code>added</code>, <code>removed</code> и <code>changed</code>. Любое поле вне списка считается неоформленным.</li><li><strong>Назовите участников.</strong> Запишите producer, конкретного consumer, family контракта и направление: backward или другое явно определённое отношение.</li><li><strong>Проверьте обязательную поверхность.</strong> Убедитесь, что candidate сохраняет required-поля baseline и их типы.</li><li><strong>Проверьте новые поля.</strong> Сверьте фактический diff с manifest. Затем проверьте parser policy named consumer.</li><li><strong>Разберите отрицательный путь.</strong> Запустите тест на удаление обязательного поля, скрытый новый ключ и strict reader. Для каждой ветки сохраните отдельную причину остановки.</li><li><strong>Передайте результат с границей.</strong> Положительный synthetic verdict передаёт change на независимый review. Он не разрешает deploy без интеграционных проверок и наблюдаемого rollout.</li></ol>\n<h2>Ограничения</h2>\n<p>Учебный gate не видит неизвестных внешних клиентов. Он не проверяет кеши, очереди, сохранённые payload, SDK, базы и семантику бизнес-значений автоматически. Он также не определяет срок поддержки старой формы. Для этих вопросов нужны реальные инвентари потребителей, contract tests и план удаления.</p>\n<p>Добавление необязательного поля часто безопаснее удаления обязательного, но это не универсальное правило. Строгий parser, подпись payload или downstream-система с закрытым набором ключей превращают additive change в остановку. Не называйте поле безопасным только потому, что оно не помечено как required.</p>\n<p>Отдельный риск — изменение смысла без изменения типа. Строка <code>amount</code> может перейти с рублей на копейки. Timestamp может сменить timezone. Enum может получить другой смысл при том же наборе строк. Structural diff этого не докажет. Нужны доменное описание, тесты значений и проверка consumer.</p>\n<h2>Критерий готовности</h2>\n<p>Изменение готово к следующему review, если другая инженерная команда без устного пояснения может открыть одну карточку и ответить на пять вопросов: какая форма была baseline, какая стала candidate, что изменилось по manifest, кто читает результат и в каком направлении выполнялась проверка. Для additive change дополнительно нужен положительный тест tolerant consumer и отрицательный тест strict consumer. Для breaking change нужен отдельный migration или versioning decision.</p>\n<p>Если хотя бы один ответ неизвестен, итогом должен быть stop, а не зелёный комментарий. Такая остановка дешевле аварийного отката: она превращает неясное обещание в конкретный вопрос, который можно проверить.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://json-schema.org/draft/2020-12/json-schema-validation\" target=\"_blank\" rel=\"noopener\">JSON Schema Draft 2020-12: Validation vocabulary</a> — официальное описание structural keywords, включая <code>properties</code> и <code>additionalProperties</code>.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc8927.html\" target=\"_blank\" rel=\"noopener\">RFC 8927: JSON Type Definition</a> — нормативное описание <code>properties</code>, <code>optionalProperties</code> и режима дополнительных свойств.</li><li><a href=\"https://avro.apache.org/docs/1.12.0/specification/\" target=\"_blank\" rel=\"noopener\">Apache Avro 1.12.0 Specification</a> — официальное описание writer schema, reader schema и schema resolution.</li></ul>"
}