function escapeHtml(value) { return String(value) .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } const p = (text) => '
' + text + '
'; const h2 = (text) => '' + escapeHtml(text) + '';
const ol = (items) => '| ' + item + ' | ').join('') + '
|---|
| ' + item + ' | ').join('') + '
id и state. Если другой reader требует legacyMode, это не аргумент тихо добавить поле в обещание. Сначала нужно выяснить, является ли это тем же семейством контрактов, сохранилось ли поле в целевой версии и может ли потребитель назвать условие миграции.'),
p('Такой порядок защищает и от чрезмерной универсальности. Не надо заранее превращать ответ в бесконечный объект «на всякий случай». Необязательное поле без правила расширения часто хуже отсутствующего поля: один потребитель не замечает его, другой делает из него обязательное условие, третий копирует его в собственную схему. Небольшая явная поверхность дешевле потому, что будущий спор имеет объект: можно сравнить предложение с объявленным контрактом, а не с памятью участников.'),
h2('Исполняемый обзор поверхности'),
code("import { createFixedPlatformApiReview, reviewFixedPlatformApi, summarizeFixedContractSurface } from './upgrade-2026-01.mjs';\n\nconst review = createFixedPlatformApiReview('documented-compatible-v1');\nconst report = reviewFixedPlatformApi(review);\nconst surface = summarizeFixedContractSurface(review);\nconsole.log({ status: report.status, fields: surface.responseFields, hatch: surface.documentedEscapeHatches });\n// { status: 'synthetic-contract-review-hand-off', fields: ['id', 'state', 'label'], hatch: ['raw-envelope-v1'] }"),
p('Этот код импортирует только public exports и работает с named fixed literal. Он не отправляет request, не открывает сеть и не меняет API. Принятый status означает лишь, что в учебном объекте названы поверхность, версия, consumer и граница hatch. Именно поэтому output заканчивается hand-off, а не «обновить контракт»: пакет не получает права менять реальную схему, выпуск или сервис.'),
h2('Escape hatch — отдельный договор, а не пароль для своих'),
p('Escape hatch нужен, когда общий контракт честно не покрывает задачу, но потребность всё же ограничена и проверяема. Хороший hatch имеет имя, версию, разрешённый вход, форму результата и отрицательную границу. В fixed example raw-envelope-v1 возвращает только одно named representation. Он не обещает порядок, фильтрацию, задержку, хранение, доступность или сохранение будущих полей. Такая граница может показаться сухой, но она не даёт одному наблюдению стать пятнадцатью неявными гарантиями.'),
p('Скрытый debug-wire опаснее не потому, что слово debug запрещено. У него нет документации и даже собственного предела. Значит, reviewer не знает, можно ли потребителю строить на нём парсер, повторять его после версии или передавать дальше. В данном fixture это fail-closed: незадокументированный hatch возвращает stop, хотя все остальные поля похожи на успешный review. Это полезный сигнал: сначала оформите отдельный договор либо уберите зависимость, не компенсируйте неопределённость красивым названием.'),
h2('Короткая последовательность перед hand-off'),
ol([
'Записать решение. Назвать, какое действие должен выполнить именно этот потребитель, а не какой внутренний объект он хочет увидеть.',
'Собрать поверхность. Зафиксировать operation, version, request, required response, errors и только проверяемые guarantees.',
'Найти утечку. Отметить поле, порядок, исключение или режим, которого нет в поверхности, но без которого потребитель не работает.',
'Выбрать форму. Либо расширить публичный контракт с правилами совместимости, либо оформить узкий documented escape hatch, либо вернуть stop.',
'Сравнить consumer. Проверить family, version и требуемые response fields; результатом может быть только synthetic review hand-off.',
]),
h2('Почему номер версии не заменяет этот разговор'),
p('Semantic Versioning полезен после того, как объявлен public API: он связывает несовместимое изменение с major version, а совместимое добавление — с minor version. Но номер не говорит, что именно public. Если скрытый параметр никогда не был назван поверхностью, один потребитель может считать его контрактом, а другой — случайностью. Сначала требуется конкретная запись обязательств; потом уже возможно обсуждать, какой номер соответствует их изменению.'),
p('OpenAPI решает ещё более раннюю часть задачи: даёт форму описания HTTP API, по которой человек или инструмент может увидеть способности без чтения исходного кода. Из этого не следует, что спецификация равна поведению. Документ может быть точным, неполным или устаревшим относительно реализации. Поэтому рядом с описанием нужен contract review: какой consumer сравнивался, какой факт проверялся и какое следующее действие разрешено. В этом пакете все эти объекты synthetic, так что вывод не распространяется на чужие документы.'),
h2('Наблюдение потребителя ещё не является новым обязательством'),
p('Особый consumer часто приносит правильное наблюдение и слишком широкий вывод. Он может честно сказать: «мне сейчас нужен raw envelope, иначе я не вижу дополнительный marker». Из этого не следует, что каждый consumer должен увидеть весь envelope или что marker стабилен между версиями. В contract card нужно разделить три предложения: что увидел consumer, какое решение он не может принять без этого факта и какой минимальный интерфейс достаточен. Пока второе или третье предложение не записано, нельзя понять, нужно ли расширение public surface или локальный adapter.'),
p('Эта разница снижает стоимость дизайна. Вместо двух крайностей — добавить всё в основной response или отказать без объяснения — появляется третья: назвать узкое исключение и обеспечить его границу. В fixed fixture hatch не переносит гарантию state-is-a-fixed-symbol на raw форму и не добавляет обещание долговечности. Если новый reader захочет построить parser на безымянном поле, review должен остановиться раньше, чем этот parser станет внутренним стандартом команды. Контракт не запрещает потребность; он заставляет назвать её цену и владельца.'),
h2('Ограничение практики и следующий шаг'),
p('Эта практика не классифицирует все будущие изменения автоматически. Она не выбирает формат документа, не строит migration и не измеряет влияние на команду. Особенно важно не путать явный escape hatch с гарантией надёжности: у hatch есть ровно та граница, которая записана. Любая производительность, безопасность, долговечность или поведение при неизвестных полях требует отдельного контракта и отдельной проверки.'),
p('Возьмите один существующий «особый параметр» и напишите одну карточку без общих слов: кто потребитель, какое решение он принимает, какая версия, какие поля обязательны и что hatch точно не обещает. Если хотя бы один ответ не удаётся записать, не расширяйте API на доверии. Верните стоп с недостающим фактом. Это делает ближайший обсуждаемый шаг меньше, но не превращает внутренний случай в пожизненное обязательство.'),
], [
{ key: 'oas', use: 'OAS 3.1.1 определяет language-agnostic interface description для HTTP API и описывает schema как описание request, response, parameter или header content.', boundary: 'Спецификация не доказывает, что implementation ей соответствует, и не задаёт политику migration для synthetic API.' },
{ key: 'semver', use: 'Pinned SemVer 2.0.0 требует объявить public API и связывает incompatible public API change с major version.', boundary: 'SemVer не определяет, какие поля данного fixed contract являются публичными, и не подтверждает совместимость consumer.' },
{ key: 'http', use: 'RFC 9110 описывает uniform interface и representation как передаваемую информацию о ресурсе, а не внутреннюю реализацию.', boundary: 'RFC не задаёт application-level escape hatch, future-field policy или результат contract review.' },
]);
const mechanism = revision({
slug: 'editorial-2026-01-mechanism-platform-api',
title: 'Гарантия не равна полю: механизм обратной совместимости для платформенного API',
categories: ['Архитектура', 'API'],
cover: '/assets/editorial/2026/platform-api-2026-guarantee-exception-matrix.svg',
excerpt: 'Механика контрактного решения: отделить описанное поле от гарантии, исключение от обхода и совместимость от номера версии; остановить неявное обязательство до интеграции.',
readingMinutes: 14,
}, [
p('Проблема обратной совместимости редко выглядит как удаление endpoint. Чаще команда добавляет «безобидное» поле, меняет порядок элементов или оставляет неописанный exception, а один строгий consumer уже превратил наблюдение в условие работы. Цена — несовместимость обнаруживается после того, как её причина растворилась между схемой, клиентом и версией: каждая сторона права локально, но никакая не может назвать прежнюю гарантию.'),
p('Механика должна сравнивать не два JSON-снимка, а четыре вещи: объявленную гарантию, допустимое исключение, идентичность consumer и нужную ему поверхность. Если связь не названа, fixture останавливает решение. Действие: проверять claim о совместимости против versioned fixed contract и named consumer, а не выводить его из слова optional, статуса 200 или красивого номера версии.'),
h2('Поле описывает форму, гарантия разрешает вывод'),
p('Поле state в response говорит, что такое имя присутствует в заданной форме. Гарантия state-is-a-fixed-symbol уже сильнее: она разрешает consumer различать заранее названные symbolic values. А фраза «ответ всегда отсортирован» сильнее ещё раз: она добавляет порядок, которого в контракте нет. Ошибка в таких спорах возникает, когда все три уровня называют «схемой». Но менять каждый из них нужно по разным правилам и проверять разными контрпримерами.'),
p('У fixed contract есть required response fields id и state, optional label, один listed error и одна declared guarantee. Список маленький специально: его можно проверить полностью. Consumer с требованием legacyMode не становится совместимым оттого, что это поле когда-то наблюдалось. А claim о стабильном порядке не становится верным оттого, что текущий array выглядит упорядоченным. Оба случая должны сохранить причину stop, иначе следующий reviewer увидит уже только чужой итог.'),
figure('/assets/editorial/2026/platform-api-2026-guarantee-exception-matrix.svg', 'Матрица с двумя осями: гарантия явно записана или нет, а потребитель сравним с семейством контракта или нет. Зелёная клетка ведёт только к synthetic hand-off; остальные клетки дают отдельные статусы stop.', 'Совместимость появляется лишь в одной клетке: когда потребность выражена объявленной гарантией и consumer относится к тому же именованному семейству.'),
table('Что именно проверяет compatibility review', ['Объект', 'Разрешённый вопрос', 'Признак 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'],
]),
h2('Почему optional не означает обратно совместимо'),
p('Слово optional описывает отношение поля к одному валидатору или генератору. Оно не сообщает, как consumer обрабатывает отсутствующее поле, дополнительное поле, новые значения, порядок, ошибки и побочный переход. Даже равенство двух OpenAPI fragments не отвечает на этот вопрос, если не известна модель reader. Один reader игнорирует лишнее, другой использует закрытую десериализацию, третий считает отсутствие поля сигналом старого режима. Совместимость — это свойство пары «контракт и named consumer», а не метка около поля.'),
p('В RFC 9110 representation и ресурс разделены намеренно: передаваемая форма не обязана раскрывать внутренности. Для API это полезное напоминание. Наблюдаемая форма ответа не даёт права выводить, что внутренний порядок, способ вычисления или соседняя ошибка стали публичными. Внутреннее может измениться без нарушения договора; публичное нельзя менять молча. Граница определяется не тем, что видит trace, а тем, что команда записала как разрешённое ожидание.'),
h2('Исполняемый отрицательный пример'),
code("import { createFixedPlatformApiReview, reviewFixedPlatformApi } from './upgrade-2026-01.mjs';\n\nconst review = createFixedPlatformApiReview('implicit-guarantee-v1');\nconst report = reviewFixedPlatformApi(review);\nconsole.log({ status: report.status, reasons: report.reasons, next: report.nextAction });\n// { status: 'stop-implicit-guarantee', reasons: ['implicit-guarantee'], next: 'write-the-guarantee-into-the-fixed-contract-or-remove-the-claim' }"),
p('Пример выполняется без API, parser, сети и случайного времени. Named input содержит один валидный field-level contract и дополнительный claim response-order-is-stable. Поскольку этот claim не входит в declared guarantees, функция не пытается угадать намерение и не повышает версию. Она выдаёт stop. Это fail-closed не из недоверия к автору, а потому что у review нет формального основания решить, обязуется ли API поддерживать порядок.'),
h2('Исключение нельзя прятать внутри флага'),
p('Exception бывает законным: часть consumer действительно может нуждаться в представлении, которое не подходит широкому API. Но exception обязан быть меньше контракта, а не шире. Он называет получателя, форму, срок или версию, входные ограничения и то, что не гарантирует. Если документ говорит только «включите debug-wire для интеграций», это не exception, а канал для новых неявных зависимостей. Каждый такой вызов расширяет API без единой версии или проверки.'),
p('В mechanism fixture hatch проверяется отдельно от совместимости field. Даже reader, которому достаточно id и state, не может получить positive output рядом с debug-wire: у него нет documentation и boundary. Это важный порядок. Не надо сначала одобрять consumer, а потом «разобраться с документацией». Hatch меняет видимую поверхность, значит его граница — часть решения совместимости, а не сопроводительный текст после решения.'),
h2('Версия — сводка изменений, а не доказательство'),
p('Semantic Versioning сформулирован вокруг public API: прежде чем связывать изменение с major или minor, нужно объявить, что считается public. Поэтому номер 1.3.0 в fixed object — идентификатор проверяемой поверхности, не формула её безопасности. Он помогает reader спросить «для какой версии заявлен этот contract», но не превращает новую семантику в compatible автоматически. Если public API не назван, любая арифметика версий лишь аккуратно упаковывает неясность.'),
p('С другой стороны, нельзя использовать это различие как повод никогда не выпускать изменения. Если новая потребность формулируется как самостоятельная гарантия и named tolerant consumer не зависит от несуществующих полей, её можно рассматривать как отдельное versioned предложение. Положительный ответ всё равно скромен: synthetic contract-review hand-off. В нём нет production effect, миграции или решения за реальную команду. Дальше потребуется материал конкретного API, которого в этом пакете намеренно нет.'),
h2('Ошибка, отсутствие и порядок требуют разных доказательств'),
p('Есть ещё одна частая склейка: consumer видит fixed-not-found, делает fallback и начинает считать любую другую ошибку отсутствием записи. В contract это другой вид неявной гарантии. Названная ошибка описывает разрешённую ветку для конкретного состояния; она не делает остальные ошибки эквивалентными и не объявляет retry, доступность или timing. Так же и порядок: даже если fixed response сегодня содержит один элемент, из этого нельзя вывести стабильность списка. Каждое такое ожидание должно пройти через guarantee list отдельно.'),
p('Этот разбор полезен именно при изменении. Поле можно оставить на месте, но изменить допустимый набор symbolic values; error можно сохранить по имени, но изменить когда он возникает; hatch можно не удалить, но расширить область так, что старый consumer уже неверно понимает результат. Простое schema diff покажет часть формы, но не все переходы смысла. Поэтому review хранит declared guarantees рядом с surface и отказывается принимать claim, если ему не соответствует буквальная строка контракта. Это не полная спецификация мира, а минимальная защита от «мы думали, что это обещано».'),
p('Контрольный вопрос здесь намеренно приземлённый: какое условие должен проверить reader, чтобы безопасно принять следующее решение? Если ответ — «он видит поле», значит гарантии ещё нет. Если ответ — «нам всегда так отвечали», значит зафиксировано наблюдение, а не контракт. Если ответ можно записать как короткое условие с версией и named error, его уже можно положить в review и проверить против следующей версии.'),
h2('Пять проверок перед словом compatible'),
ol([
'Разделить форму и смысл. Выписать 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.',
]),
h2('Граница механизма и следующий шаг'),
p('Этот механизм не заменяет тесты кода, переговоры об SLA, security review или поддержку старой версии. Он также не доказывает, что любой consumer честно описал свои потребности. Его роль уже: не дать явному отсутствию гарантии стать молчаливым решением. Так у команды появляется качественный вход для следующего инструмента — migration plan, schema diff или нагрузочного эксперимента — вместо набора предположений.'),
p('Для ближайшего review возьмите один compatibility claim и попробуйте разложить его на table выше. Если «совместимо» нельзя привязать к точной guarantee и конкретному consumer family, верните stop-incomparable-consumer или stop-implicit-guarantee. Это не задержка ради процесса. Это минимальный способ не включить чужую зависимость в public API задним числом.'),
], [
{ key: 'http', use: 'RFC 9110 различает resource и transferable representation, а HTTP semantics связывает request, response, method, status и metadata.', boundary: 'RFC не определяет application-level compatibility, order guarantee или migration policy для fixed consumer.' },
{ key: 'semver', use: 'Pinned SemVer 2.0.0 требует precise public API и относит backward-incompatible public API change к major version.', boundary: 'Правила версионирования не решают, является ли конкретный implicit claim гарантией и не заменяют consumer review.' },
{ key: 'oas', use: 'OAS 3.1.1 связывает schema с content request, response, parameter или header, поэтому полезен как vocabulary описания формы.', boundary: 'OAS не устанавливает, как любой consumer обрабатывает unknown field, order или exception.' },
]);
const field = revision({
slug: 'editorial-2026-01-field-platform-api',
title: 'Совместимость не решается номером: полевой цикл платформенной команды для API',
categories: ['Платформы', 'Практика'],
cover: '/assets/editorial/2026/platform-api-2026-consumer-compatibility-loop.svg',
excerpt: 'Полевой цикл для API-платформы: собрать именованных потребителей, отсеять несопоставимые контракты, передать точный synthetic status и не объявлять версию совместимой на основании одного удачного вызова.',
readingMinutes: 14,
}, [
p('Проблема в поле выглядит как простая координация: у платформенной команды есть новая версия, у нескольких потребителей — разные привычки, и хочется назвать всё compatible одним сообщением. Цена такой экономии — невидимый потребитель обнаруживается после hand-off, а команда чинит не контракт, а следы разных ожиданий: кто-то ожидал поле, кто-то порядок, кто-то секретный режим, а кто-то вообще сравнивал другой тип операции.'),
p('Рабочий цикл начинается не с массового уведомления, а с небольшого inventory: один named contract, один named consumer, одна цель сравнения и один status. Сначала отбрасываем несопоставимые family, затем проверяем требуемую поверхность и исключения, после чего передаём только ограниченный результат. Действие: сохранять stop как полезный выход review, а не маскировать его версией или словами «должно работать».'),
h2('Инвентарь consumer — это модель решения, не список команд'),
p('Слово consumer слишком широкое. Для compatibility review важны не владельцы и не названия систем, а наблюдаемые условия использования: какой contract family ожидается, какую version consumer способен читать, какие response fields обязательны и как он трактует errors. В этом пакете все profiles — fixed synthetic literals. Они не обозначают реальные сервисы, пользователей или трассы. Поэтому их можно безопасно сравнить, не создавая видимость, что мы обследовали производство.'),
p('У такого inventory есть приятная строгость. fixed-tolerant-reader-v1 относится к family fixed-catalog-read-v1, поддерживает 1.3.0 и требует id с state. fixed-legacy-reader-v1 требует ещё legacyMode; для него результат — incompatible, а не «попробуем». fixed-command-adapter-v1 вообще относится к command family. Его нельзя использовать как плохой пример read compatibility: сравнение прекращается раньше, на сопоставимости объекта.'),
figure('/assets/editorial/2026/platform-api-2026-consumer-compatibility-loop.svg', 'Замкнутый вертикальный цикл: named contract и named consumer проходят проверку family, surface, guarantee и escape hatch; зелёная ветка ведёт к synthetic hand-off, красные ветки возвращают конкретный stop в inventory.', 'Цикл не выпускает версию и не меняет API. Он сохраняет причину, по которой следующий review должен продолжить работу или остановиться.'),
table('Полевой inventory перед сравнениями', ['Поле карточки', 'Почему нужно', 'Fixed пример', 'Ошибочный заменитель'], [
['contract family', 'не смешать разные операции', 'fixed-catalog-read-v1', 'одинаковый version string'],
['version', 'назвать поверхность во времени', '1.3.0', 'latest или устная договорённость'],
['required fields', 'увидеть минимальное чтение', 'id, state', 'полный снимок response'],
['error boundary', 'понять условие ветвления', 'fixed-not-found', 'любая ошибка равна отсутствию'],
['escape hatch', 'отделить исключение от общего пути', 'raw-envelope-v1', 'секретный query flag'],
['status', 'сохранить решение review', 'stop-incomparable-consumer', 'общая фраза compatible'],
]),
h2('Сначала проверить, что сравниваем один вид контракта'),
p('Самая дешёвая проверка — family. Read operation и command adapter могут иметь похожие поля и одинаковую строку версии, но отвечают на разные действия и риски. Если сравнить их как два reader, можно ошибочно объявить набор полей достаточным. Если сравнить их как несовместимые, можно ошибочно потребовать migration там, где связи вообще не было. Поэтому incomparable-consumer — не мягкая форма incompatibility. Это отдельный verdict: основания для сравнения отсутствуют.'),
p('Это особенно важно для платформенной абстракции. Чем лучше общий API скрывает детали, тем сильнее соблазн привести разнородных потребителей к одному знаменателю. Но совместимость — не отношение между всеми сущностями с JSON. Это отношение выбранного contract family и конкретной потребности. Сначала нужно назвать ось сравнения, а уже потом обсуждать поля, errors и version. Такая последовательность обычно укорачивает meeting: часть спорных примеров уходит в другой review вместо того, чтобы загрязнять текущий verdict.'),
h2('Исполняемая остановка для несопоставимого consumer'),
code("import { createFixedConsumerCompatibilityCase, reviewFixedConsumerCompatibility, runFixedPlatformApiFixture } from './upgrade-2026-01.mjs';\n\nconst item = createFixedConsumerCompatibilityCase('incomparable-consumer-v1');\nconst result = reviewFixedConsumerCompatibility(item);\nconst fixture = runFixedPlatformApiFixture();\nconsole.log({ status: result.status, next: result.nextAction, assertions: Object.keys(fixture.assertions).length });\n// { status: 'stop-incomparable-consumer', next: 'separate-contract-review-by-family', assertions: 15 }"),
p('Фрагмент вызывает только public exports. Он не проверяет настоящий client и не создаёт release note. Его смысл в другом: case получает status до сравнения полей, потому что family не совпадает. Fixture дополнительно подтверждает fail-closed пути для undocumented hatch, implicit guarantee и incompatible reader. Положительный путь в этом модуле заканчивается synthetic contract-review hand-off, поэтому код не может случайно показать изменение API как результат проверки.'),
h2('Как передать совместимость, не передавая уверенность'),
p('Хороший hand-off состоит из четырёх коротких строк: идентификатор contract, version, consumer, status с next action. Например, у compatible fixed reader разрешён только hand-off hand-off-named-consumer-and-fixed-contract. Это не означает «развернуть» или «потребитель доказанно работает». Это значит, что названная пара прошла правила synthetic fixture и следующий reviewer может продолжать с явной областью. Любое более сильное решение потребовало бы данных и прав, которых здесь нет.'),
p('Для stop format ещё важнее. Вместо «потребитель старый» сохранить incompatible-consumer и missing field. Вместо «надо договориться» сохранить undocumented-escape-hatch и требование name plus boundary. Вместо «похоже, не подходит» сохранить incomparable-consumer. Точный status удерживает ответственность на объекте контракта, не на человеке и не на эмоциональной оценке команды.'),
h2('Последовательность полевого review'),
ol([
'Назвать единицу. Выбрать одну operation и одну version, не смешивая её с набором соседних endpoint.',
'Завести карточку consumer. Записать family, supported version, required fields и error boundary без догадки о будущих ожиданиях.',
'Отсечь несопоставимое. При разных family вернуть отдельный review, не вычисляя совместимость по похожим именам полей.',
'Проверить surface. Сопоставить нужные поля и declared guarantees с фиксированным contract; отсутствующее поле остаётся incompatibility.',
'Проверить исключение. Для hatch требовать versioned name и negative boundary; секретный маршрут не проходит review.',
'Передать ограниченно. Сохранить status, reasons и next action; positive output не меняет API и не заменяет rollout.',
]),
h2('Где в цикле живёт обратная совместимость'),
p('Обратная совместимость не живёт в одной функции сравнения и не в changelog. Она поддерживается на переходах: public surface объявлена до версии, consumer profile относится к тому же family, exception имеет отдельный предел, а status доступен следующему участнику. SemVer даёт полезную дисциплину именования изменения после того, как public API определён. OpenAPI может описать форму операции. Но ни стандарт, ни document не знают, какой именно synthetic reader держит закрытый parser или нуждается в missing field.'),
p('Поэтому цикл намеренно не агрегирует разные verdict в один процент совместимости. Процент скрывает, какой consumer нельзя сравнивать, какой требует absent field и какой использует неоформленный обход. Полевая команда должна сохранить эти разные причины до тех пор, пока не появится отдельное решение: сохранить surface, оформить migration или разделить family. В хорошей документации такой список выглядит менее гладким, зато не превращает неизвестность в командное обязательство.'),
h2('Неизвестный consumer не равен нулевому риску'),
p('Инвентарь почти всегда неполон. Ошибка здесь — подставить вместо отсутствующей карточки удобный verdict: «значит, зависимостей нет». Честнее хранить неизвестность отдельно. Если у потребителя не назван family, его нельзя включить ни в compatible, ни в incompatible список. Если он известен только по устному описанию, нельзя выводить required fields. Такой объект возвращается в очередь исследования, а не в числитель успешных проверок.'),
p('Это меняет разговор о гибкости платформы. Команда может выпускать узкий contract, не обещая покрыть каждый будущий случай, но она не должна объявлять неизвестные случаи безопасными. Когда новый reader появляется, ему не требуется оправдывать существование; требуется принести минимальную карточку решения. Затем его можно сравнить по обычному циклу или признать отдельным family. Так abstraction остаётся развиваемой: неизвестный спрос не утаскивает весь API в общий режим, но и не исчезает из истории решения.'),
h2('Ограничения и следующий шаг'),
p('Inventory не является реестром всех интеграций и не гарантирует отсутствие неизвестных consumer. В пакете нет сетевых вызовов, file reads, production traces, токенов, людей или реальных данных; нет и политики выпуска. Поэтому review нельзя использовать как сертификат compatibility, security или availability. Его результат — дисциплинированная форма вопроса, а не готовая операция над системой.'),
p('Следующий практический шаг — выбрать одну неизвестную зависимость и не угадывать её смысл. Создайте отдельную карточку с family, version и минимальным decision, который она должна принять. Если family неизвестна, это уже полезный статус; если поле не описано, это повод оформить контракт; если всё сравнимо, можно передать ограниченный hand-off в независимое ревью. Так платформа сохраняет гибкость без того, чтобы каждый нестандартный запрос навсегда растягивал публичный API.'),
], [
{ key: 'semver', use: 'Pinned SemVer 2.0.0 требует заявить precise public API и различает backward-compatible additions и backward-incompatible public changes.', boundary: 'SemVer не создаёт inventory consumer и не устанавливает, что любой version string означает compatibility.' },
{ key: 'oas', use: 'OAS 3.1.1 описывает возможности HTTP API без необходимости читать исходный код или сетевой трафик.', boundary: 'Описание не является evidence поведения конкретного consumer и не выполняет migration или rollout.' },
{ key: 'http', use: 'RFC 9110 определяет HTTP как uniform interface с request/response semantics и representations.', boundary: 'RFC не задаёт contract family, field tolerance или итог compatibility review прикладного API.' },
]);
export const revisions = deepFreeze([practice, mechanism, field]);
if (process.argv.includes('--verify-fixture')) {
const result = runFixedPlatformApiFixture();
const failed = Object.entries(result.assertions)
.filter(([, value]) => value !== true)
.map(([key]) => key);
if (failed.length) {
process.stderr.write('FAIL fixture: ' + failed.join(', ') + '\n');
process.exitCode = 1;
} else {
const count = Object.keys(result.assertions).length;
process.stdout.write('PASS fixture: ' + count + '/' + count + ' assertions\n');
}
}
if (process.argv.includes('--print-revisions')) {
process.stdout.write(JSON.stringify(revisions) + '\n');
}