Проблема обратной совместимости редко выглядит как удаление endpoint. Чаще команда добавляет «безобидное» поле, меняет порядок элементов или оставляет неописанный exception, а один строгий consumer уже превратил наблюдение в условие работы. Цена — несовместимость обнаруживается после того, как её причина растворилась между схемой, клиентом и версией: каждая сторона права локально, но никакая не может назвать прежнюю гарантию.
Механика должна сравнивать не два JSON-снимка, а четыре вещи: объявленную гарантию, допустимое исключение, идентичность consumer и нужную ему поверхность. Если связь не названа, fixture останавливает решение. Действие: проверять claim о совместимости против versioned fixed contract и named consumer, а не выводить его из слова optional, статуса 200 или красивого номера версии.
Поле описывает форму, гарантия разрешает вывод
Поле state в response говорит, что такое имя присутствует в заданной форме. Гарантия state-is-a-fixed-symbol уже сильнее: она разрешает consumer различать заранее названные symbolic values. А фраза «ответ всегда отсортирован» сильнее ещё раз: она добавляет порядок, которого в контракте нет. Ошибка в таких спорах возникает, когда все три уровня называют «схемой». Но менять каждый из них нужно по разным правилам и проверять разными контрпримерами.
У fixed contract есть required response fields id и state, optional label, один listed error и одна declared guarantee. Список маленький специально: его можно проверить полностью. Consumer с требованием legacyMode не становится совместимым оттого, что это поле когда-то наблюдалось. А claim о стабильном порядке не становится верным оттого, что текущий array выглядит упорядоченным. Оба случая должны сохранить причину stop, иначе следующий reviewer увидит уже только чужой итог.
| Объект | Разрешённый вопрос | Признак stop | Следующее действие |
|---|---|---|---|
| surface | названы ли request, response и errors? | неполная поверхность | дописать fixed contract |
| guarantee | есть ли claim в declared list? | implicit-guarantee | сузить claim или объявить гарантию |
| escape hatch | есть ли name и boundary? | undocumented-escape-hatch | оформить отдельный договор |
| consumer | это то же contract family? | incomparable-consumer | разделить review |
| consumer needs | все required fields доступны? | incompatible-consumer | сохранить surface или назвать migration |
Почему optional не означает обратно совместимо
Слово optional описывает отношение поля к одному валидатору или генератору. Оно не сообщает, как consumer обрабатывает отсутствующее поле, дополнительное поле, новые значения, порядок, ошибки и побочный переход. Даже равенство двух OpenAPI fragments не отвечает на этот вопрос, если не известна модель reader. Один reader игнорирует лишнее, другой использует закрытую десериализацию, третий считает отсутствие поля сигналом старого режима. Совместимость — это свойство пары «контракт и named consumer», а не метка около поля.
В RFC 9110 representation и ресурс разделены намеренно: передаваемая форма не обязана раскрывать внутренности. Для API это полезное напоминание. Наблюдаемая форма ответа не даёт права выводить, что внутренний порядок, способ вычисления или соседняя ошибка стали публичными. Внутреннее может измениться без нарушения договора; публичное нельзя менять молча. Граница определяется не тем, что видит trace, а тем, что команда записала как разрешённое ожидание.
Исполняемый отрицательный пример
import { createFixedPlatformApiReview, reviewFixedPlatformApi } from './upgrade-2026-01.mjs';
const review = createFixedPlatformApiReview('implicit-guarantee-v1');
const report = reviewFixedPlatformApi(review);
console.log({ status: report.status, reasons: report.reasons, next: report.nextAction });
// { status: 'stop-implicit-guarantee', reasons: ['implicit-guarantee'], next: 'write-the-guarantee-into-the-fixed-contract-or-remove-the-claim' }
Пример выполняется без API, parser, сети и случайного времени. Named input содержит один валидный field-level contract и дополнительный claim response-order-is-stable. Поскольку этот claim не входит в declared guarantees, функция не пытается угадать намерение и не повышает версию. Она выдаёт stop. Это fail-closed не из недоверия к автору, а потому что у review нет формального основания решить, обязуется ли API поддерживать порядок.
Исключение нельзя прятать внутри флага
Exception бывает законным: часть consumer действительно может нуждаться в представлении, которое не подходит широкому API. Но exception обязан быть меньше контракта, а не шире. Он называет получателя, форму, срок или версию, входные ограничения и то, что не гарантирует. Если документ говорит только «включите debug-wire для интеграций», это не exception, а канал для новых неявных зависимостей. Каждый такой вызов расширяет API без единой версии или проверки.
В mechanism fixture hatch проверяется отдельно от совместимости field. Даже reader, которому достаточно id и state, не может получить positive output рядом с debug-wire: у него нет documentation и boundary. Это важный порядок. Не надо сначала одобрять consumer, а потом «разобраться с документацией». Hatch меняет видимую поверхность, значит его граница — часть решения совместимости, а не сопроводительный текст после решения.
Версия — сводка изменений, а не доказательство
Semantic Versioning сформулирован вокруг public API: прежде чем связывать изменение с major или minor, нужно объявить, что считается public. Поэтому номер 1.3.0 в fixed object — идентификатор проверяемой поверхности, не формула её безопасности. Он помогает reader спросить «для какой версии заявлен этот contract», но не превращает новую семантику в compatible автоматически. Если public API не назван, любая арифметика версий лишь аккуратно упаковывает неясность.
С другой стороны, нельзя использовать это различие как повод никогда не выпускать изменения. Если новая потребность формулируется как самостоятельная гарантия и named tolerant consumer не зависит от несуществующих полей, её можно рассматривать как отдельное versioned предложение. Положительный ответ всё равно скромен: synthetic contract-review hand-off. В нём нет production effect, миграции или решения за реальную команду. Дальше потребуется материал конкретного API, которого в этом пакете намеренно нет.
Ошибка, отсутствие и порядок требуют разных доказательств
Есть ещё одна частая склейка: consumer видит fixed-not-found, делает fallback и начинает считать любую другую ошибку отсутствием записи. В contract это другой вид неявной гарантии. Названная ошибка описывает разрешённую ветку для конкретного состояния; она не делает остальные ошибки эквивалентными и не объявляет retry, доступность или timing. Так же и порядок: даже если fixed response сегодня содержит один элемент, из этого нельзя вывести стабильность списка. Каждое такое ожидание должно пройти через guarantee list отдельно.
Этот разбор полезен именно при изменении. Поле можно оставить на месте, но изменить допустимый набор symbolic values; error можно сохранить по имени, но изменить когда он возникает; hatch можно не удалить, но расширить область так, что старый consumer уже неверно понимает результат. Простое schema diff покажет часть формы, но не все переходы смысла. Поэтому review хранит declared guarantees рядом с surface и отказывается принимать claim, если ему не соответствует буквальная строка контракта. Это не полная спецификация мира, а минимальная защита от «мы думали, что это обещано».
Контрольный вопрос здесь намеренно приземлённый: какое условие должен проверить reader, чтобы безопасно принять следующее решение? Если ответ — «он видит поле», значит гарантии ещё нет. Если ответ — «нам всегда так отвечали», значит зафиксировано наблюдение, а не контракт. Если ответ можно записать как короткое условие с версией и named error, его уже можно положить в review и проверить против следующей версии.
Пять проверок перед словом compatible
- Разделить форму и смысл. Выписать field, error и guarantee разными строками, не заменяя один объект другим.
- Найти лишний claim. Отметить слова про порядок, время, повтор, доступность или future behavior, если их нет в guarantee list.
- Проверить hatch. Для любого исключения требовать name, version и отрицательную boundary; отсутствие любого поля — stop.
- Сравнить family. Не сравнивать read contract с command adapter только потому, что у них совпали поля или version string.
- Сохранить причину. Передать status и next action, а не единственное слово compatible или incompatible.
Граница механизма и следующий шаг
Этот механизм не заменяет тесты кода, переговоры об SLA, security review или поддержку старой версии. Он также не доказывает, что любой consumer честно описал свои потребности. Его роль уже: не дать явному отсутствию гарантии стать молчаливым решением. Так у команды появляется качественный вход для следующего инструмента — migration plan, schema diff или нагрузочного эксперимента — вместо набора предположений.
Для ближайшего review возьмите один compatibility claim и попробуйте разложить его на table выше. Если «совместимо» нельзя привязать к точной guarantee и конкретному consumer family, верните stop-incomparable-consumer или stop-implicit-guarantee. Это не задержка ради процесса. Это минимальный способ не включить чужую зависимость в public API задним числом.
Проверяемые источники
- RFC 9110: HTTP Semantics — версия: RFC 9110, June 2022, immutable RFC publication. RFC 9110 различает resource и transferable representation, а HTTP semantics связывает request, response, method, status и metadata. Граница: RFC не определяет application-level compatibility, order guarantee или migration policy для fixed consumer.
- Semantic Versioning 2.0.0, exact source commit — версия: Semantic Versioning 2.0.0, commit 7c834b3f3a4940d77ab593bc32583004d6a426a9, 18 June 2013, immutable commit pin. Pinned SemVer 2.0.0 требует precise public API и относит backward-incompatible public API change к major version. Граница: Правила версионирования не решают, является ли конкретный implicit claim гарантией и не заменяют consumer review.
- OpenAPI Specification v3.1.1 — версия: OpenAPI Specification 3.1.1, 24 October 2024, dated immutable publication. OAS 3.1.1 связывает schema с content request, response, parameter или header, поэтому полезен как vocabulary описания формы. Граница: OAS не устанавливает, как любой consumer обрабатывает unknown field, order или exception.