revise January 2026 platform API articles
Build and deploy / deploy (push) Successful in 18s

This commit is contained in:
2026-07-31 18:49:24 +03:00
parent 50d783d0d5
commit fd381cd2f5
7 changed files with 898 additions and 1 deletions
+606
View File
@@ -0,0 +1,606 @@
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
const p = (text) => '<p>' + text + '</p>';
const h2 = (text) => '<h2>' + text + '</h2>';
const code = (text) => '<pre><code>' + escapeHtml(text) + '</code></pre>';
const ol = (items) => '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
const figure = (src, alt, caption) => '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
const table = (caption, headers, rows) => '<div class="table-scroll"><table><caption>' + caption + '</caption><thead><tr>' + headers.map((item) => '<th scope="col">' + item + '</th>').join('') + '</tr></thead><tbody>' + rows.map((row) => '<tr>' + row.map((item) => '<td>' + item + '</td>').join('') + '</tr>').join('') + '</tbody></table></div>';
function plainText(content) {
return content
.replace(/<[^>]+>/g, ' ')
.replace(/&(?:quot|amp|lt|gt|#039);/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function bodyText(content) {
return plainText(content.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, ''));
}
function deepFreeze(value) {
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
Object.values(value).forEach(deepFreeze);
Object.freeze(value);
}
return value;
}
function cloneFixed(value) {
return JSON.parse(JSON.stringify(value));
}
const REFERENCES = deepFreeze({
oas: {
title: 'OpenAPI Specification v3.1.1',
url: 'https://spec.openapis.org/oas/v3.1.1.html',
version: 'OpenAPI Specification 3.1.1, 24 October 2024, dated immutable publication',
},
semver: {
title: 'Semantic Versioning 2.0.0, exact source commit',
url: 'https://github.com/semver/semver/blob/7c834b3f3a4940d77ab593bc32583004d6a426a9/semver.md',
version: 'Semantic Versioning 2.0.0, commit 7c834b3f3a4940d77ab593bc32583004d6a426a9, 18 June 2013, immutable commit pin',
},
http: {
title: 'RFC 9110: HTTP Semantics',
url: 'https://www.rfc-editor.org/rfc/rfc9110.html',
version: 'RFC 9110, June 2022, immutable RFC publication',
},
});
function sourceList(entries) {
return '<ul>' + entries.map(({ key, use, boundary }) => {
const reference = REFERENCES[key];
return '<li><a href="' + reference.url + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(reference.title) + '</a> — версия: ' + escapeHtml(reference.version) + '. ' + escapeHtml(use) + ' Граница: ' + escapeHtml(boundary) + '</li>';
}).join('') + '</ul>';
}
const FIXED_PLATFORM_API_REVIEWS = deepFreeze({
'documented-compatible-v1': {
id: 'documented-compatible-v1',
api: {
name: 'fixed-catalog-read',
family: 'fixed-catalog-read-v1',
version: '1.3.0',
operation: 'readFixedRecord',
},
contract: {
request: { required: ['recordId'], optional: [] },
response: { required: ['id', 'state'], optional: ['label'], allowsAdditiveFields: false },
errors: ['fixed-not-found'],
guarantees: ['state-is-a-fixed-symbol'],
escapeHatches: [{
name: 'raw-envelope-v1',
documented: true,
boundary: 'Returns one fixed representation only; no ordering, filtering, latency, persistence, future-field or availability guarantee is added.',
}],
},
consumer: {
id: 'fixed-tolerant-reader-v1',
contractFamily: 'fixed-catalog-read-v1',
supportedVersions: ['1.3.0'],
requiredResponseFields: ['id', 'state'],
acceptsOnlyListedErrors: true,
},
claimedGuarantees: ['state-is-a-fixed-symbol'],
boundary: 'fixed synthetic literals in memory; no network, filesystem, Git, CI, production service, telemetry, user data or API change',
},
'undocumented-escape-hatch-v1': {
id: 'undocumented-escape-hatch-v1',
api: {
name: 'fixed-catalog-read',
family: 'fixed-catalog-read-v1',
version: '1.3.0',
operation: 'readFixedRecord',
},
contract: {
request: { required: ['recordId'], optional: [] },
response: { required: ['id', 'state'], optional: ['label'], allowsAdditiveFields: false },
errors: ['fixed-not-found'],
guarantees: ['state-is-a-fixed-symbol'],
escapeHatches: [{
name: 'debug-wire',
documented: false,
boundary: '',
}],
},
consumer: {
id: 'fixed-tolerant-reader-v1',
contractFamily: 'fixed-catalog-read-v1',
supportedVersions: ['1.3.0'],
requiredResponseFields: ['id', 'state'],
acceptsOnlyListedErrors: true,
},
claimedGuarantees: ['state-is-a-fixed-symbol'],
boundary: 'fixed synthetic literals in memory; no network, filesystem, Git, CI, production service, telemetry, user data or API change',
},
'implicit-guarantee-v1': {
id: 'implicit-guarantee-v1',
api: {
name: 'fixed-catalog-read',
family: 'fixed-catalog-read-v1',
version: '1.3.0',
operation: 'readFixedRecord',
},
contract: {
request: { required: ['recordId'], optional: [] },
response: { required: ['id', 'state'], optional: ['label'], allowsAdditiveFields: false },
errors: ['fixed-not-found'],
guarantees: ['state-is-a-fixed-symbol'],
escapeHatches: [{
name: 'raw-envelope-v1',
documented: true,
boundary: 'Returns one fixed representation only; no ordering, filtering, latency, persistence, future-field or availability guarantee is added.',
}],
},
consumer: {
id: 'fixed-tolerant-reader-v1',
contractFamily: 'fixed-catalog-read-v1',
supportedVersions: ['1.3.0'],
requiredResponseFields: ['id', 'state'],
acceptsOnlyListedErrors: true,
},
claimedGuarantees: ['state-is-a-fixed-symbol', 'response-order-is-stable'],
boundary: 'fixed synthetic literals in memory; no network, filesystem, Git, CI, production service, telemetry, user data or API change',
},
'incompatible-consumer-v1': {
id: 'incompatible-consumer-v1',
api: {
name: 'fixed-catalog-read',
family: 'fixed-catalog-read-v1',
version: '1.3.0',
operation: 'readFixedRecord',
},
contract: {
request: { required: ['recordId'], optional: [] },
response: { required: ['id', 'state'], optional: ['label'], allowsAdditiveFields: false },
errors: ['fixed-not-found'],
guarantees: ['state-is-a-fixed-symbol'],
escapeHatches: [{
name: 'raw-envelope-v1',
documented: true,
boundary: 'Returns one fixed representation only; no ordering, filtering, latency, persistence, future-field or availability guarantee is added.',
}],
},
consumer: {
id: 'fixed-legacy-reader-v1',
contractFamily: 'fixed-catalog-read-v1',
supportedVersions: ['1.3.0'],
requiredResponseFields: ['id', 'state', 'legacyMode'],
acceptsOnlyListedErrors: true,
},
claimedGuarantees: ['state-is-a-fixed-symbol'],
boundary: 'fixed synthetic literals in memory; no network, filesystem, Git, CI, production service, telemetry, user data or API change',
},
'incomparable-consumer-v1': {
id: 'incomparable-consumer-v1',
api: {
name: 'fixed-catalog-read',
family: 'fixed-catalog-read-v1',
version: '1.3.0',
operation: 'readFixedRecord',
},
contract: {
request: { required: ['recordId'], optional: [] },
response: { required: ['id', 'state'], optional: ['label'], allowsAdditiveFields: false },
errors: ['fixed-not-found'],
guarantees: ['state-is-a-fixed-symbol'],
escapeHatches: [{
name: 'raw-envelope-v1',
documented: true,
boundary: 'Returns one fixed representation only; no ordering, filtering, latency, persistence, future-field or availability guarantee is added.',
}],
},
consumer: {
id: 'fixed-command-adapter-v1',
contractFamily: 'fixed-catalog-command-v1',
supportedVersions: ['1.3.0'],
requiredResponseFields: ['id', 'state'],
acceptsOnlyListedErrors: true,
},
claimedGuarantees: ['state-is-a-fixed-symbol'],
boundary: 'fixed synthetic literals in memory; no network, filesystem, Git, CI, production service, telemetry, user data or API change',
},
});
const FIXED_CONSUMER_COMPATIBILITY_CASES = deepFreeze({
'compatible-consumer-v1': {
id: 'compatible-consumer-v1',
reviewId: 'documented-compatible-v1',
comparisonPurpose: 'one fixed reader compared with one fixed read contract',
boundary: 'fixed synthetic comparison only; not a release, approval or API mutation',
},
'incompatible-consumer-v1': {
id: 'incompatible-consumer-v1',
reviewId: 'incompatible-consumer-v1',
comparisonPurpose: 'one fixed legacy reader compared with one fixed read contract',
boundary: 'fixed synthetic comparison only; not a release, approval or API mutation',
},
'incomparable-consumer-v1': {
id: 'incomparable-consumer-v1',
reviewId: 'incomparable-consumer-v1',
comparisonPurpose: 'fixed read contract and fixed command adapter intentionally have different families',
boundary: 'fixed synthetic comparison only; not a release, approval or API mutation',
},
});
function isKnownFixed(value, collection) {
return Object.values(collection).some((candidate) => JSON.stringify(candidate) === JSON.stringify(value));
}
function isNonEmptyString(value) {
return typeof value === 'string' && value.trim().length > 0;
}
function includesAll(haystack, needles) {
return needles.every((needle) => haystack.includes(needle));
}
export function createFixedPlatformApiReview(id) {
const review = FIXED_PLATFORM_API_REVIEWS[id];
return review ? deepFreeze(cloneFixed(review)) : null;
}
export function summarizeFixedContractSurface(review) {
if (!isKnownFixed(review, FIXED_PLATFORM_API_REVIEWS)) {
return deepFreeze({
accepted: false,
status: 'stop-unknown-fixed-review',
reasons: ['review-is-not-a-named-fixed-literal'],
productionEffect: 'not-attempted',
});
}
return deepFreeze({
api: review.api.name,
family: review.api.family,
version: review.api.version,
operation: review.api.operation,
requestFields: deepFreeze([...review.contract.request.required, ...review.contract.request.optional]),
responseFields: deepFreeze([...review.contract.response.required, ...review.contract.response.optional]),
listedErrors: deepFreeze([...review.contract.errors]),
listedGuarantees: deepFreeze([...review.contract.guarantees]),
documentedEscapeHatches: deepFreeze(review.contract.escapeHatches.filter((item) => item.documented).map((item) => item.name)),
boundary: review.boundary,
productionEffect: 'not-attempted',
});
}
export function reviewFixedPlatformApi(review) {
if (!isKnownFixed(review, FIXED_PLATFORM_API_REVIEWS)) {
return deepFreeze({
accepted: false,
status: 'stop-unknown-fixed-review',
reasons: ['review-is-not-a-named-fixed-literal'],
nextAction: 'use-a-named-fixed-review',
productionEffect: 'not-attempted',
});
}
const reasons = [];
const { api, contract, consumer, claimedGuarantees } = review;
const responseFields = [...contract.response.required, ...contract.response.optional];
if (!isNonEmptyString(api.name) || !isNonEmptyString(api.family) || !isNonEmptyString(api.version) || !isNonEmptyString(api.operation)) {
reasons.push('incomplete-contract-identity');
}
if (!contract.request.required.length || !contract.response.required.length || !contract.errors.length) {
reasons.push('incomplete-contract-surface');
}
if (!contract.escapeHatches.every((hatch) => hatch.documented && isNonEmptyString(hatch.boundary))) {
reasons.push('undocumented-escape-hatch');
}
if (!includesAll(contract.guarantees, claimedGuarantees)) {
reasons.push('implicit-guarantee');
}
if (consumer.contractFamily !== api.family) {
reasons.push('incomparable-consumer');
}
if (!consumer.supportedVersions.includes(api.version) || !includesAll(responseFields, consumer.requiredResponseFields)) {
reasons.push('incompatible-consumer');
}
if (!consumer.acceptsOnlyListedErrors) {
reasons.push('consumer-error-boundary-is-not-explicit');
}
let status = 'synthetic-contract-review-hand-off';
let nextAction = 'hand-off-fixed-contract-review';
if (reasons.includes('undocumented-escape-hatch')) {
status = 'stop-undocumented-escape-hatch';
nextAction = 'document-name-boundary-and-version-or-remove-hatch';
} else if (reasons.includes('implicit-guarantee')) {
status = 'stop-implicit-guarantee';
nextAction = 'write-the-guarantee-into-the-fixed-contract-or-remove-the-claim';
} else if (reasons.includes('incomparable-consumer')) {
status = 'stop-incomparable-consumer';
nextAction = 'separate-contract-review-by-family';
} else if (reasons.includes('incompatible-consumer')) {
status = 'stop-incompatible-consumer';
nextAction = 'preserve-required-surface-or-name-a-separate-migration';
} else if (reasons.length > 0) {
status = 'stop-incomplete-contract';
nextAction = 'repair-fixed-contract-surface';
}
return deepFreeze({
accepted: reasons.length === 0,
status,
reasons: deepFreeze(reasons),
nextAction,
api: api.name,
version: api.version,
consumer: consumer.id,
boundary: review.boundary,
productionEffect: 'not-attempted',
});
}
export function createFixedConsumerCompatibilityCase(id) {
const item = FIXED_CONSUMER_COMPATIBILITY_CASES[id];
return item ? deepFreeze(cloneFixed(item)) : null;
}
export function reviewFixedConsumerCompatibility(caseInput) {
if (!isKnownFixed(caseInput, FIXED_CONSUMER_COMPATIBILITY_CASES)) {
return deepFreeze({
accepted: false,
status: 'stop-unknown-fixed-compatibility-case',
reasons: ['case-is-not-a-named-fixed-literal'],
nextAction: 'use-a-named-fixed-compatibility-case',
productionEffect: 'not-attempted',
});
}
const review = createFixedPlatformApiReview(caseInput.reviewId);
const report = reviewFixedPlatformApi(review);
if (!report.accepted) {
return deepFreeze({
accepted: false,
status: report.status,
reasons: report.reasons,
nextAction: report.nextAction,
comparisonPurpose: caseInput.comparisonPurpose,
boundary: caseInput.boundary,
productionEffect: 'not-attempted',
});
}
return deepFreeze({
accepted: true,
status: 'synthetic-contract-review-hand-off',
reasons: deepFreeze([]),
nextAction: 'hand-off-named-consumer-and-fixed-contract',
comparisonPurpose: caseInput.comparisonPurpose,
boundary: caseInput.boundary,
productionEffect: 'not-attempted',
});
}
export function runFixedPlatformApiFixture() {
const compatible = reviewFixedPlatformApi(createFixedPlatformApiReview('documented-compatible-v1'));
const undocumented = reviewFixedPlatformApi(createFixedPlatformApiReview('undocumented-escape-hatch-v1'));
const implicit = reviewFixedPlatformApi(createFixedPlatformApiReview('implicit-guarantee-v1'));
const incompatible = reviewFixedPlatformApi(createFixedPlatformApiReview('incompatible-consumer-v1'));
const incomparable = reviewFixedPlatformApi(createFixedPlatformApiReview('incomparable-consumer-v1'));
const compatibleCase = reviewFixedConsumerCompatibility(createFixedConsumerCompatibilityCase('compatible-consumer-v1'));
const incomparableCase = reviewFixedConsumerCompatibility(createFixedConsumerCompatibilityCase('incomparable-consumer-v1'));
const summary = summarizeFixedContractSurface(createFixedPlatformApiReview('documented-compatible-v1'));
const unknown = reviewFixedPlatformApi({ id: 'invented' });
return deepFreeze({
assertions: deepFreeze({
documentedContractHandsOffOnly: compatible.status === 'synthetic-contract-review-hand-off' && compatible.productionEffect === 'not-attempted',
documentedContractIsAccepted: compatible.accepted,
summaryHasNamedOperation: summary.operation === 'readFixedRecord',
summaryDoesNotInventField: !summary.responseFields.includes('legacyMode'),
undocumentedHatchStops: undocumented.status === 'stop-undocumented-escape-hatch',
undocumentedHatchNamesReason: undocumented.reasons.includes('undocumented-escape-hatch'),
implicitGuaranteeStops: implicit.status === 'stop-implicit-guarantee',
implicitGuaranteeNamesReason: implicit.reasons.includes('implicit-guarantee'),
incompatibleConsumerStops: incompatible.status === 'stop-incompatible-consumer',
incompatibleConsumerNamesMissingField: incompatible.reasons.includes('incompatible-consumer'),
incomparableConsumerStops: incomparable.status === 'stop-incomparable-consumer',
comparableCaseHandsOffOnly: compatibleCase.status === 'synthetic-contract-review-hand-off' && compatibleCase.productionEffect === 'not-attempted',
incomparableCaseStops: incomparableCase.status === 'stop-incomparable-consumer',
unknownReviewStops: unknown.status === 'stop-unknown-fixed-review',
fixtureIsFrozen: Object.isFrozen(FIXED_PLATFORM_API_REVIEWS) && Object.isFrozen(FIXED_CONSUMER_COMPATIBILITY_CASES),
}),
});
}
function revision(meta, parts, sources) {
const contentHtml = parts.join('\n') + '\n' + h2('Проверяемые источники') + sourceList(sources);
const proseLength = bodyText(contentHtml).length;
if (proseLength < 5000 || proseLength > 15000) {
throw new Error(meta.slug + ': body length ' + proseLength);
}
return deepFreeze({ ...meta, contentHtml, proseLength });
}
const practice = revision({
slug: 'editorial-2026-01-practice-platform-api',
title: 'Контракт до особого случая: как не превратить платформенный API в меню скрытых параметров',
categories: ['API', 'Инженерная практика'],
cover: '/assets/editorial/2026/platform-api-2026-contract-surface.svg',
excerpt: 'Практика для платформенного API: выделить контрактную поверхность, описать узкий escape hatch и проверить именованного потребителя до того, как особый случай станет скрытой зависимостью.',
readingMinutes: 14,
}, [
p('Проблема начинается не с поломки, а с вежливой просьбы: нестандартному потребителю нужен «ещё один флаг», сырой ответ или порядок, который раньше никто не называл. Если команда отвечает скрытым параметром, API получает вторую, неописанную поверхность. Цена — следующий потребитель начинает зависеть от случайного поведения, а изменение внутренностей превращается в расследование: это баг, обязательство или чей-то локальный обход?'),
p('Полезнее не спорить, достаточно ли случай особый. Сначала назвать контракт: операция, допустимый запрос, обязательный ответ, список ошибок, гарантия и граница расширения. Затем проверить конкретного именованного потребителя против этого списка. Действие короткое: если потребность нельзя объяснить через поверхность, создайте documented escape hatch с версией и пределом или остановите hand-off.'),
h2('Абстракция протекает там, где нет названной границы'),
p('Платформенная абстракция нужна не для того, чтобы скрыть всю реальность. Она скрывает детали до тех пор, пока потребителю достаточно объявленного результата. Протекание начинается, когда потребитель вынужден угадывать представление: читать незафиксированное поле, различать внутренний режим по тексту ошибки, рассчитывать на сортировку или передавать особый флаг. Ни один из этих сигналов сам по себе не плох. Опасно другое: команда уже дала доступ, но не решила, является ли это интерфейсом.'),
p('Контракт полезно читать как список разрешённых ожиданий, а не как снимок реализации. У операции есть имя и версия. У запроса — требуемые и допустимые поля. У ответа — обязательные поля, известные необязательные поля и правило, можно ли расширять форму. У ошибки — различимые состояния. У гарантии — только то, что команда готова повторять в пределах версии. Всё остальное остаётся деталью, даже если сегодня его легко наблюдать.'),
figure('/assets/editorial/2026/platform-api-2026-contract-surface.svg', 'Вертикальная схема контрактной поверхности: сигнал потребителя проходит через операцию, запрос, ответ, ошибки и гарантию; documented escape hatch имеет имя и границу, а скрытый обход отмечен красным стопом.', 'Поверхность не обязана быть большой. Её задача — сделать видимым, что именно потребитель вправе ожидать, а что ещё не стало обязательством.'),
table('Минимальная контрактная поверхность', ['Часть', 'Вопрос к автору API', 'Пример fixed review', 'Что не следует додумывать'], [
['операция', 'какое действие названо?', 'readFixedRecord', 'внутренний способ чтения'],
['запрос', 'какой вход обязателен?', 'recordId', 'скрытые параметры отладки'],
['ответ', 'что потребитель может читать?', 'id и state', 'поле legacyMode отсутствует'],
['ошибки', 'какое исключение различимо?', 'fixed-not-found', 'полный список внутренних причин'],
['гарантия', 'какой смысл обещан?', 'state is a fixed symbol', 'порядок, скорость и будущие поля'],
['escape hatch', 'что выдано сверх поверхности?', 'raw-envelope-v1 с границей', 'неограниченный доступ к wire shape'],
]),
h2('Начать не с типа, а с решения потребителя'),
p('Тип ответа отвечает на вопрос «какие байты или поля возможны». Контракт отвечает на другой вопрос: «какое решение потребитель вправе принять на их основе». Для одного fixed reader достаточно различать <code>id</code> и <code>state</code>. Если другой reader требует <code>legacyMode</code>, это не аргумент тихо добавить поле в обещание. Сначала нужно выяснить, является ли это тем же семейством контрактов, сохранилось ли поле в целевой версии и может ли потребитель назвать условие миграции.'),
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 <code>raw-envelope-v1</code> возвращает только одно named representation. Он не обещает порядок, фильтрацию, задержку, хранение, доступность или сохранение будущих полей. Такая граница может показаться сухой, но она не даёт одному наблюдению стать пятнадцатью неявными гарантиями.'),
p('Скрытый <code>debug-wire</code> опаснее не потому, что слово debug запрещено. У него нет документации и даже собственного предела. Значит, reviewer не знает, можно ли потребителю строить на нём парсер, повторять его после версии или передавать дальше. В данном fixture это fail-closed: незадокументированный hatch возвращает stop, хотя все остальные поля похожи на успешный review. Это полезный сигнал: сначала оформите отдельный договор либо уберите зависимость, не компенсируйте неопределённость красивым названием.'),
h2('Короткая последовательность перед hand-off'),
ol([
'<strong>Записать решение.</strong> Назвать, какое действие должен выполнить именно этот потребитель, а не какой внутренний объект он хочет увидеть.',
'<strong>Собрать поверхность.</strong> Зафиксировать operation, version, request, required response, errors и только проверяемые guarantees.',
'<strong>Найти утечку.</strong> Отметить поле, порядок, исключение или режим, которого нет в поверхности, но без которого потребитель не работает.',
'<strong>Выбрать форму.</strong> Либо расширить публичный контракт с правилами совместимости, либо оформить узкий documented escape hatch, либо вернуть stop.',
'<strong>Сравнить consumer.</strong> Проверить 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 не переносит гарантию <code>state-is-a-fixed-symbol</code> на 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('Поле <code>state</code> в response говорит, что такое имя присутствует в заданной форме. Гарантия <code>state-is-a-fixed-symbol</code> уже сильнее: она разрешает consumer различать заранее названные symbolic values. А фраза «ответ всегда отсортирован» сильнее ещё раз: она добавляет порядок, которого в контракте нет. Ошибка в таких спорах возникает, когда все три уровня называют «схемой». Но менять каждый из них нужно по разным правилам и проверять разными контрпримерами.'),
p('У fixed contract есть required response fields <code>id</code> и <code>state</code>, optional <code>label</code>, один listed error и одна declared guarantee. Список маленький специально: его можно проверить полностью. Consumer с требованием <code>legacyMode</code> не становится совместимым оттого, что это поле когда-то наблюдалось. А 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 <code>response-order-is-stable</code>. Поскольку этот claim не входит в declared guarantees, функция не пытается угадать намерение и не повышает версию. Она выдаёт stop. Это fail-closed не из недоверия к автору, а потому что у review нет формального основания решить, обязуется ли API поддерживать порядок.'),
h2('Исключение нельзя прятать внутри флага'),
p('Exception бывает законным: часть consumer действительно может нуждаться в представлении, которое не подходит широкому API. Но exception обязан быть меньше контракта, а не шире. Он называет получателя, форму, срок или версию, входные ограничения и то, что не гарантирует. Если документ говорит только «включите debug-wire для интеграций», это не exception, а канал для новых неявных зависимостей. Каждый такой вызов расширяет API без единой версии или проверки.'),
p('В mechanism fixture hatch проверяется отдельно от совместимости field. Даже reader, которому достаточно <code>id</code> и <code>state</code>, не может получить positive output рядом с <code>debug-wire</code>: у него нет 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 видит <code>fixed-not-found</code>, делает 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([
'<strong>Разделить форму и смысл.</strong> Выписать field, error и guarantee разными строками, не заменяя один объект другим.',
'<strong>Найти лишний claim.</strong> Отметить слова про порядок, время, повтор, доступность или future behavior, если их нет в guarantee list.',
'<strong>Проверить hatch.</strong> Для любого исключения требовать name, version и отрицательную boundary; отсутствие любого поля — stop.',
'<strong>Сравнить family.</strong> Не сравнивать read contract с command adapter только потому, что у них совпали поля или version string.',
'<strong>Сохранить причину.</strong> Передать status и next action, а не единственное слово compatible или incompatible.',
]),
h2('Граница механизма и следующий шаг'),
p('Этот механизм не заменяет тесты кода, переговоры об SLA, security review или поддержку старой версии. Он также не доказывает, что любой consumer честно описал свои потребности. Его роль уже: не дать явному отсутствию гарантии стать молчаливым решением. Так у команды появляется качественный вход для следующего инструмента — migration plan, schema diff или нагрузочного эксперимента — вместо набора предположений.'),
p('Для ближайшего review возьмите один compatibility claim и попробуйте разложить его на table выше. Если «совместимо» нельзя привязать к точной guarantee и конкретному consumer family, верните <code>stop-incomparable-consumer</code> или <code>stop-implicit-guarantee</code>. Это не задержка ради процесса. Это минимальный способ не включить чужую зависимость в 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 есть приятная строгость. <code>fixed-tolerant-reader-v1</code> относится к family <code>fixed-catalog-read-v1</code>, поддерживает 1.3.0 и требует <code>id</code> с <code>state</code>. <code>fixed-legacy-reader-v1</code> требует ещё <code>legacyMode</code>; для него результат — incompatible, а не «попробуем». <code>fixed-command-adapter-v1</code> вообще относится к 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 там, где связи вообще не было. Поэтому <code>incomparable-consumer</code> — не мягкая форма 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 <code>hand-off-named-consumer-and-fixed-contract</code>. Это не означает «развернуть» или «потребитель доказанно работает». Это значит, что названная пара прошла правила synthetic fixture и следующий reviewer может продолжать с явной областью. Любое более сильное решение потребовало бы данных и прав, которых здесь нет.'),
p('Для stop format ещё важнее. Вместо «потребитель старый» сохранить <code>incompatible-consumer</code> и missing field. Вместо «надо договориться» сохранить <code>undocumented-escape-hatch</code> и требование name plus boundary. Вместо «похоже, не подходит» сохранить <code>incomparable-consumer</code>. Точный status удерживает ответственность на объекте контракта, не на человеке и не на эмоциональной оценке команды.'),
h2('Последовательность полевого review'),
ol([
'<strong>Назвать единицу.</strong> Выбрать одну operation и одну version, не смешивая её с набором соседних endpoint.',
'<strong>Завести карточку consumer.</strong> Записать family, supported version, required fields и error boundary без догадки о будущих ожиданиях.',
'<strong>Отсечь несопоставимое.</strong> При разных family вернуть отдельный review, не вычисляя совместимость по похожим именам полей.',
'<strong>Проверить surface.</strong> Сопоставить нужные поля и declared guarantees с фиксированным contract; отсутствующее поле остаётся incompatibility.',
'<strong>Проверить исключение.</strong> Для hatch требовать versioned name и negative boundary; секретный маршрут не проходит review.',
'<strong>Передать ограниченно.</strong> Сохранить 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');
}