DarkRiDDeR15 мин

Backward совместимость как направление: механизм compatibility gate для схемы данных

АрхитектураДанные

Поломка схемы часто маскируется под обновление версии: в карточке написано v2, поля похожи, а значит якобы можно двигаться дальше. Техническая ошибка в другом месте — не задано отношение между старой формой, новой формой и reader. Цена такой неопределённости высока: любое позднее несовпадение превращается в спор о трактовке слова compatible, а не в проверку конкретного условия. Compatibility gate нужен, чтобы вернуть сравнению направление и наблюдаемые причины отказа.

Механизм начинается с fail-closed правила: если direction, baseline, candidate или consumer не названы, сравнение не выполняется. Нельзя вычислять совместимость по пересечению имён или по удачному serialisation sample. Дальше gate разнимает три вопроса, которые обычно склеивают: сохранил ли candidate required surface baseline, оформлены ли все новые поля и может ли этот consumer принять declared additions. Только после этих ответов возможен synthetic hand-off.

Backward — не направление стрелки в changelog

Слово backward звучит знакомо, но без субъекта оно пустое. В этом модуле оно значит: fixed producer создаёт candidate, а fixed consumer, чья точка отсчёта baseline, получает эту форму. Отношение несимметрично. Можно отдельно исследовать, способен ли новый reader разобрать старые данные; это будет другая карточка с другой семантикой. Склеить оба вопроса в один boolean удобно для отчёта, но опасно для решения: неизвестно, что именно можно сохранить при stop.

Поэтому comparison содержит пять значений: direction, contract family, baseline version, candidate version и consumerId. Family отсекает случайное сравнение одинаковых JSON-объектов, версии закрепляют две точки, consumerId запрещает заменить проверяемого участника во время обсуждения. Если хотя бы одно значение не совпадает с карточкой, status становится stop-implicit-comparison. Это не syntax error и не слабая форма false. Это признание, что механизм пока не знает, какое отношение он должен вычислять.

Диаграмма цикла compatibility gate: fixed case проходит явное направление, diff обязательных полей, manifest additions и capability consumer. Зелёная ветка заканчивается synthetic hand-off, четыре красные ветки возвращают разные stop statuses к уточнению карточки.
Gate удерживает причины раздельно: сначала точность сравнения, затем схема, затем declared fields и только потом capability named consumer.
Слои механизма и их отдельные вердикты
СлойВопросУдачный fixed ответFail-closed status
отношениечто и в какую сторону сопоставляем?backward producer → consumerstop-implicit-comparison
required surfaceсохранился ли baseline required field?id и state на местеstop-backward-incompatible-schema
manifestназвано ли каждое новое поле?priority указанstop-undocumented-schema-field
capabilityпринимает ли reader declared additions?tolerant reader: даstop-incompatible-consumer
выходкакое право даёт результат?synthetic hand-offне deploy и не migration

Diff схемы должен сохранять тип и обязательность

Первый технический слой строит индексы полей baseline и candidate. Затем он ищет две опасные разницы: required field baseline исчез в candidate или остался с другим type. В fixed case state заменяют на phase. Человек может увидеть близкий смысл, но gate не интерпретирует семантику имён. Для reader, которому нужен state, поле отсутствует. Статус stop-backward-incompatible-schema даёт короткий следующий шаг: восстановить required field либо честно назвать отдельную migration, а не обновлять таблицу версий.

Почему проверять именно required baseline fields, а не всё подряд? Потому что цель этой карточки узкая. Необязательное поле может иметь собственный риск, но его отсутствие не должно автоматически приравниваться к нарушению обязательной поверхности. Если команде нужно защищать и optional semantic contracts, это надо добавить новым явным правилом и тестом. Механизм не становится надёжнее от безымянной строгости. Он становится надёжнее, когда каждое правило можно указать в report и воспроизвести на fixed case.

Исполняемая остановка на разрушенной поверхности

import {
  reviewFixedDataContractCompatibility as review,
  createFixedDataContractCase as fixedCase,
} from './upgrade-2026-03.mjs';

const item = fixedCase('backward-incompatible-v2');
const report = review(item);
console.log({ status: report.status, removed: report.removedRequiredFields, next: report.nextAction });
// { status: 'stop-backward-incompatible-schema', removed: ['state'], next: 'retain-required-baseline-field-or-name-a-separate-migration' }

Здесь нет сериализации, registry client или внешнего schema file. Literal содержит обе формы и named consumer, а exported function только сравнивает их по правилам module. Это намеренно ограничивает доказательство. Мы можем буквально выполнить ветку fail-closed и увидеть, что исчезновение state не проходит как простое rename. Мы не можем из этого вывода делать заявление о совместимости реального формата или о результате какого-либо release.

Почему необязательное поле всё ещё требует consumer review

Следующий слой кажется парадоксальным. Candidate с необязательным priority не удаляет id и state, значит structural check проходит. Но producer может всё равно передать объект, где priority присутствует. Tolerant reader заранее объявил готовность к declared additions; strict reader объявил, что такие additions не принимает. Это не противоречие между двумя версиями schema. Это два разных требования к границе consumer, которые нельзя вывести из одного лишь слова optional.

Именно здесь появляется отдельный status stop-incompatible-consumer. Gate не изменяет candidate, не пытается удалить поле на лету и не предполагает адаптер. Он возвращает, что для данной named пары positive hand-off невозможен. Возможны разные инженерные ответы: изменить reader, разделить форму, задержать candidate или завести отдельную migration. Выбор остаётся за следующим решением. Качество gate в том, что он не маскирует эту развилку под зелёный version badge.

Manifest связывает фактический diff с намерением

Поле можно добавить двумя способами: как declared element change и как побочный след реализации. Для формата это одинаковые байты или ключи. Для контракта это разные состояния знания. Manifest не пытается предсказать смысл priority; он лишь перечисляет, что команда сознательно добавила priority. При сравнении candidate с hidden routingHint обнаруживается поле, которого нет в manifest. Gate возвращает stop-undocumented-schema-field до проверки consumer capability.

Этот порядок важен. Если сначала спросить tolerant reader, он мог бы сказать, что дополнительные поля допустимы, и скрытое изменение получило бы ложный положительный знак. Но acceptability consumer не заменяет обязательство producer объяснить новую поверхность. Сначала field становится предметом change, затем мы спрашиваем, может ли конкретный reader его получить. Получается небольшая, но полезная последовательность ответственности: producer называет изменение; review проверяет diff; consumer задаёт границу принятия.

Внутренний порядок gate

  1. Проверить известность case. Модуль принимает только clone named fixed literal; неизвестный объект получает stop unknown fixed contract case.
  2. Проверить relation. Сверить direction, family, версии и consumerId с обеими schema cards и профилем reader.
  3. Построить field maps. Вычислить additions, отсутствующие required baseline fields и type changes без сетевых или файловых зависимостей.
  4. Сверить manifest. Остановить case, если candidate содержит addition вне declaredAddedFields или manifest указывает несуществующее новое поле.
  5. Проверить capability. Сопоставить required fields consumer, supported candidate version и policy declared additions.
  6. Вернуть строго ограниченный output. Хранить reasons и next action; accepted result означает только synthetic compatibility-review hand-off.

Почему gate не должен вычислять процент совместимости

Процент быстро сглаживает нужную информацию. В одной корзине оказываются неизвестное направление, удалённый required field, скрытый addition и strict reader. У каждого состояния другой владелец следующего шага и другой риск. Если сказать «совместимо на 75 процентов», никто не понимает, можно ли уточнить manifest, восстановить field, завести migration или просто изменить consumer policy. Число выглядит нейтрально, но фактически стирает причины.

Для M9-практики полезнее один небольшой status на один единичный review. Это не означает, что в реальном процессе нельзя агрегировать итоговые данные. Но агрегировать следует после того, как сохраняется исходная структура: family, direction, baseline, candidate, consumer, reason. Иначе дашборд успокаивает команду ровно в тот момент, когда ей нужна конкретная карточка работы. Synthetic module намеренно не имеет общего счётчика и не измеряет успех.

Граница источников и следующий механизм

JSON Schema и JTD дают vocabulary для описания object fields и additional properties. Avro формулирует relation writer and reader schemas и правила resolution. Ни один из этих документов не определяет statuses этого overlay и не обещает, что конкретный parser перенесёт change. Поэтому код не объявляет себя реализацией стандарта. Он показывает минимальный механизм принятия решения: различить форму, намерение и capability reader, а неизвестность остановить раньше успешного hand-off.

Следующий шаг для команды — явно выбрать, какой второй direction требуется отдельно: новый consumer читает baseline или old consumer читает candidate. Не пытайтесь расширить текущую функцию без новой карточки и fixtures. Сначала назовите relation, затем добавьте один fixed boundary case, status и next action. Так compatibility gate растёт как контракт собственных решений, а не как накопление неявных if вокруг версий.

Проверяемые источники

  • Apache Avro Specification 1.12.0, exact source commit — версия: Apache Avro 1.12.0, commit 8c27801dc8d42ccc00997f25c0b8f45f8d4a233e, release tag dated 5 August 2024, immutable commit pin. Exact Avro 1.12.0 source distinguishes writer schema from reader schema and describes schema resolution, supporting the article distinction between directions of comparison. Граница: Pinned source does not define the overlay statuses, synthetic field map or a result for any external schema registry.
  • JSON Schema Core, draft-bhutton-json-schema-01 — версия: draft-bhutton-json-schema-01, published 10 June 2022, immutable versioned IETF draft. Draft 2020-12 defines vocabulary for object properties and additionalProperties, which is used only to explain why a field boundary must be explicit. Граница: The dated document does not establish backward compatibility for this fixed comparison or a named reader policy.
  • RFC 8927: JSON Type Definition — версия: RFC 8927, November 2020, immutable RFC publication. RFC 8927 describes properties, optionalProperties and additionalProperties as distinct schema concepts, supporting the separate treatment of required and added fields. Граница: RFC 8927 does not supply a producer inventory, migration decision or deployment approval.