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('') + '
/v1/posting будет удалён после февраля. В день, когда дата наступает, в pull request легко написать «старый endpoint больше никому не нужен» и вырезать обработчик. Симптом понятен: календарь даёт один ясный сигнал, а список потребителей разбросан между контрактом, владельцами и документами. Цена ошибки тоже конкретна: один неизвестный клиент получает отказ, а команда не может объяснить, кому сообщили, какой replacement предложили и где безопасно остановить изменение.'),
p('Обратная ошибка не лучше: путь держат бесконечно, потому что никто не хочет отвечать за слово «последний consumer». Дата сама по себе не решает этот спор. Она полезна как boundary для планирования, но не как доказательство отсутствия вызовов. Удаление начинается не с календаря, а с карты: какой именно контракт устаревает, кто отвечает за миграцию, какое объявление видит потребитель и какой факт разрешает перейти к отдельному change на удаление.'),
h2('Симптом → причина → проверка → действие'),
ol([
'Симптом. В задаче есть дедлайн, но нет перечня контрактов и ответственных за переход.',
'Причина. Endpoint принимают за один объект, хотя вокруг него есть public contract, client integrations, authorizations, documentation и старый migration path.',
'Проверка. Для каждого известного или неизвестного потребителя заполните одну строку: contract, owner, migration path, deadline, announcement и evidence boundary.',
'Действие. До закрытия карты не удаляйте путь. После карты подготовьте отдельный review: что доказано, что неизвестно, какой риск остаётся и какие условия остановят удаление.',
]),
h2('Сначала ограничить предмет удаления'),
p('Фраза «удаляем v1» слишком широкая. В ней не видно, речь о write route, одной операции в OpenAPI, SDK method, callback или полном наборе ресурсов. Начните с имени contract: HTTP method, URI template, request and response shape, authentication boundary и replacement. Если replacement меняет semantics, одной замены URL мало. Нужно записать, кто проверяет compatibility: например, сохраняется ли idempotency key, как кодируются ошибки и может ли старый client понять ответ.'),
p('OpenAPI 3.1.0 даёт полезную, но узкую декларацию: у Operation Object есть deprecated: true, и consumers должны воздерживаться от этой операции. Это не инвентаризация. Пометка в schema не рассказывает, какой generated client уже выпущен, какой сервис спрятал вызов за adapter и какая интеграция вообще не читает OpenAPI. Поэтому declaration и map — разные артефакты. Первое говорит, что контракт больше не рекомендован; второе делает migration работой с владельцами.'),
table('Consumer map для учебного контракта', ['Потребитель', 'Класс сведения', 'Contract и owner', 'Migration path', 'Deadline', 'Announcement и evidence boundary'], [
['synthetic-web-checkout', 'source usage', 'v1 write route; synthetic-checkout-owner', 'заменить fixed v1 call на fixed v2 contract', 'synthetic-2025-01-13', 'synthetic note; label учебный, не code search'],
['synthetic-sdk-package', 'declared dependency', 'v1 write route; synthetic-sdk-owner', 'сначала выпустить fixed v2 SDK surface', 'synthetic-2025-01-20', 'synthetic release note; не package inventory'],
['synthetic-unknown-integrator', 'unknown consumer', 'v1 write route; synthetic-api-owner', 'оставить notice discoverable и открыть scope question', 'synthetic-2025-02-03', 'synthetic deprecation page; не customer list'],
]),
figure('/assets/editorial/2024/deprecation-2024-consumer-map.svg', 'Схема consumer map: от точно названного API-контракта идут шесть обязательных полей — потребитель, владелец, путь миграции, дедлайн, объявление и граница доказательства. Отдельная ветка unknown consumer блокирует автоматическое удаление.', 'Карта показывает порядок инженерного разговора. Имена, даты и статусы в статье — fixed synthetic model, а не сведения о реальной системе или клиентах.'),
h2('Карта не обязана притворяться полным реестром'),
p('У карты есть два честных типа строк. Известная строка связывает конкретного consumer с owner и migration path. Неизвестная строка фиксирует противоположное: область ещё не доказана. Она не портит отчёт. Она не позволяет календарю выдать желаемый ответ. Если в карте есть unknown consumer, следующий вопрос звучит не «когда удалить?», а «какой разрешённый способ ограничит неизвестность и кто примет остаточный риск?».'),
p('Это особенно важно для внешнего API. Нельзя получать список потребителей из одной телеметрической витрины и объявлять его полным: часть клиентов может не попадать в выбранное окно, использовать другой credential class, идти через proxy или вообще не иметь доступного механизма учёта. В этой статье нет ни telemetry, ни customer list. Учебная строка synthetic-unknown-integrator нужна только для того, чтобы не потерять этот класс риска в дизайне.'),
h2('Deprecation record: короткий контракт перехода'),
p('Уведомление полезно, когда у него есть контекст. IETF draft deprecation header в версии 09, доступной к ноябрю 2024, описывает signal о том, что resource уже устарел или устареет, и link на документацию. RFC 8594 отдельно описывает Sunset как hint о предполагаемой недоступности. Из этого не следует, что два header автоматически мигрируют client. Они лишь делают решение discoverable. Migration guide, replacement, owner и scope должны быть отдельными полями записи.'),
code([
'{',
' "contract": "synthetic-ledger-v1-posting-path",',
' "owner": "synthetic-ledger-owner",',
' "replacement": "synthetic-ledger-v2-posting-path",',
' "deprecationDate": "synthetic-2024-11-04",',
' "sunsetBoundary": "synthetic-2025-02-03",',
' "announcement": "synthetic-public-deprecation-page",',
' "knownConsumerRule": "every row names owner and migration path",',
' "unknownConsumerRule": "unknown blocks automatic removal",',
' "restoreBoundary": "stop proposal before a real removal change"',
'}',
].join('\n')),
p('Это не production configuration и не готовый header. Здесь нет реальной даты HTTP, токена, route table или client data. Запись проверяет другую вещь: нельзя провести строку к removal gate, пока не названы replacement, owner, объявление и реакция на unknown. Если public API ведётся по SemVer, versioning contract можно включить рядом: SemVer 2.0.0 требует объявить public API, а deprecated public functionality отражать minor version. Но версия пакета не заменяет communication plan для конкретного endpoint.'),
h2('Как читать доказательство, не подменяя его обещанием'),
p('В consumer map колонка evidence должна называть не результат «никого нет», а способ и границу. Например, «объявленная зависимость» сообщает, что чей-то manifest или contract сказал о зависимости; «source usage» — что разрешённая проверка кода нашла конкретный call; «authorization» — что определён набор credential class. Каждая строка отвечает на свой вопрос. Ни одна не равна population всех пользователей.'),
table('Сведения, которые нельзя склеивать в один зелёный статус', ['Сведение', 'На какой вопрос отвечает', 'Чего не доказывает', 'Безопасная формулировка'], [
['Deprecated в OpenAPI', 'операция больше не рекомендована в описании контракта', 'что consumer уже перестали её вызывать', 'declaration опубликована, миграция не подтверждена'],
['Deprecation / Sunset signal', 'resource сообщил о lifecycle и boundary', 'что клиент получил, понял или применил notice', 'signal доступен в указанной response boundary'],
['Declared dependency', 'потребитель объявил связь с API или SDK', 'что этот путь выполняется сейчас', 'зависимость требует owner и migration path'],
['Unknown consumer', 'полнота scope не установлена', 'что отсутствует риск', 'автоматическое удаление заблокировано'],
]),
h2('Removal gate — отдельное решение'),
p('Когда строки карты заполнены, удаление всё ещё не становится автоматическим. Нужен gate с отрицательными условиями: нет active row, unknown scope обработан отдельным решением, replacement contract читаем, announcement и дедлайн сохранены, а у change есть restore boundary. Это делает цену решения видимой. Можно ускорить удаление, сузив scope до одной operation, или отложить, если replacement ещё не может принять нужный сценарий. Нельзя компенсировать незнание более ранней датой.'),
p('Если перед реальным удалением появляется active consumer, безопасное действие — остановить proposal. Не менять одновременно documentation, route, authorization и fallback. Сначала сохранить, где обнаружен конфликт и какой compatibility boundary сохраняется. В synthetic fixture восстановление означает только discard in-memory draft: оно не возвращает endpoint, не меняет header и не обещает rollback production. В настоящем проекте restore plan должен быть отдельным, с owner, ограничением данных и проверкой после возврата.'),
h2('Ограничения и следующий проверяемый шаг'),
p('Материал не выполняет source search, API call, анализ трафика, чтение access logs, customer list, Git или CI. Fixed rows не доказывают существование реальных клиентов, а synthetic deadlines не задают policy. RFC 8594 называет Sunset hint и не гарантирует, что ресурс станет недоступен ровно в дату. IETF draft-09 на дату исторической границы был draft, поэтому нельзя выдавать его за завершённый стандарт ноября 2024.'),
p('Следующий шаг: выберите одну operation, а не весь API. Создайте consumer map из шести колонок этой статьи. В первой же строке, где не удаётся назвать owner, evidence boundary или replacement, поставьте unknown. Ожидаемый результат — не преждевременное удаление, а понятное решение: какую проверку запросить, кто её владелец и почему до неё route остаётся совместимым.'),
h2('Историческая граница ноября 2024'),
p('Для терминов lifecycle использованы RFC 8594 (May 2019) и IETF draft-ietf-httpapi-deprecation-header-09 (September 2024). Draft показывает deprecation signal и documentation link, но на ноябрь 2024 ещё не был RFC. OpenAPI Specification v3.1.0 подтверждает декларацию deprecated operation, а SemVer 2.0.0 — правила для заявленного public API. Все имена, строки карты, даты, outcomes и действия в этом материале — fixed synthetic in-memory model; это не реальная телеметрия, customer list, исходный код, incident или production evidence.'),
]);
const mechanism = revision({
slug: 'editorial-2024-11-mechanism-deprecation',
title: 'Почему telemetry не равна списку пользователей',
categories: ['Рефакторинг', 'API'],
cover: '/assets/editorial/2024/deprecation-2024-timeline.svg',
excerpt: 'Механизм deprecation: разделить source usage, declared dependency, exposure or traffic, authorization и unknown consumer, а затем держать warning, sunset и removal как разные границы.',
readingMinutes: 13,
}, [
p('Перед удалением API часто появляется уверенная фраза: «в telemetry, то есть в данных наблюдения, за две недели не видно вызовов». Она звучит как ответ на вопрос о последнем consumer, но отвечает только на вопрос о выбранном наборе наблюдений. Симптом — одно число или пустой график превращают в список пользователей. Цена ошибки — удалить путь для client, который не попал в окно, идёт по другой authorization boundary или не присылает нужный signal, а затем спорить, была ли это «неожиданная» зависимость.'),
p('Другой риск возникает раньше. Команда ставит deprecated в OpenAPI или отправляет warning, но называет это миграцией. Declaration, runtime signal и removal — разные состояния. Если смешать их, можно начать возврат только после поломки: replacement не описан, owner не назначен, Sunset date трактуется как жёсткая гарантия, а неизвестный consumer исчезает из таблицы. Практичнее держать отдельные evidence types и timeline с boundary для каждого шага.'),
h2('Симптом → причина → проверка → действие'),
ol([
'Симптом. Нулевой график или один access report объявлен доказательством, что у API больше нет пользователей.',
'Причина. Source usage, declared dependency, exposure or traffic, authorization и unknown consumer измеряют разные поверхности, но их сложили в один статус.',
'Проверка. Для каждой строки map укажите evidence type, scope, period, owner и то, чего этот тип не может доказать.',
'Действие. Unknown оставьте отдельным состоянием. Warning, deprecation, sunset и removal проводите по timeline, где у каждой boundary есть entry condition и stop condition.',
]),
h2('Пять разных источников сведений'),
p('Source usage отвечает на локальный вопрос: известен ли вызов в разрешённой кодовой области. Это полезный сигнал для named service, но не доказательство всех deployed clients. Declared dependency отвечает на другой вопрос: кто явно объявил SDK, schema или API contract. Такая запись может пережить migration и не говорить о runtime execution. Эти два слоя помогают найти owner, но не позволяют объявить population полной.'),
p('Exposure or traffic отвечает на вопрос об observed requests в конкретном инструменте, периоде и маршруте. Здесь особенно опасно слово «пользователь». Request не всегда равен человеку, а отсутствие request в окне не равно отсутствию caller. Authorization описывает credential class, permission или gateway policy: она может показать, какие классы способны использовать resource, но не гарантирует, что каждый класс реально делает вызов. Unknown consumer остаётся, когда scope нельзя честно замкнуть.'),
table('Evidence types для lifecycle API', ['Тип', 'Полезный вопрос', 'Типичная ложная подмена', 'Что записать рядом'], [
['Source usage', 'есть ли known call в согласованной code scope?', 'не нашли call → никто не использует API', 'repository or module scope, revision, owner, blind zones'],
['Declared dependency', 'кто объявил SDK, schema или contract?', 'зависимость → текущий runtime call', 'artifact, version, owner, migration target'],
['Exposure / traffic', 'что observed в заданном route and period?', 'нет samples → нет users', 'instrument, interval, sampling, auth and cache boundary'],
['Authorization', 'какой credential class может обратиться?', 'credential exists → active consumer', 'policy scope, owner, exceptions, review date'],
['Unknown consumer', 'какая часть population не доказана?', 'unknown → zero', 'reason, risk owner, next authorized check or decision'],
]),
figure('/assets/editorial/2024/deprecation-2024-timeline.svg', 'Timeline deprecation разделяет четыре состояния: documentation warning, declared deprecation, sunset boundary и отдельный removal gate. Под каждой стадией отмечены требуемые поля: replacement, owner, evidence scope, stop condition и restore boundary.', 'Временная шкала показывает порядок состояний, а не календарную политику. Даты и пункты в статье — fixed synthetic values; схема не читает telemetry и не указывает реальный срок отключения.'),
h2('Warning, deprecation, sunset и removal не являются одним событием'),
p('Warning — это discoverable documentation: consumer может увидеть replacement и условия перехода, но сам resource продолжает работать. OpenAPI 3.1.0 позволяет объявить operation устаревшей в contract. IETF draft-09 описывает Deprecation header как runtime signal и допускает Link на deprecation documentation. Ничто из этого не выключает endpoint. Это правильно: notice должен уменьшать появление новых зависимостей, не меняя semantics незаметно.'),
p('Sunset — следующий уровень риска. RFC 8594 описывает timestamp, после которого URI ожидаемо станет unresponsive. Документ специально отделяет стадию «не preferred» от decommission и называет Sunset hint. Поэтому нельзя использовать timestamp как доказательство того, что все callers успели перейти. Он задаёт boundary для plan, например: после неё compatibility может закончиться, но до actual removal всё равно нужен check, owner и безопасный stop.'),
table('Lifecycle timeline и условия перехода', ['Состояние', 'Что становится видимым', 'Что ещё запрещено утверждать', 'Boundary для следующего шага'], [
['Warning', 'replacement, owner, migration guide и scope notice', 'что caller увидел notice или начал migration', 'declaration and documentation reviewed'],
['Deprecated', 'OpenAPI flag or Deprecation signal для resource', 'что response semantics уже изменились или consumer ушли', 'named consumer map and evidence boundary recorded'],
['Sunset boundary', 'планируемая дата возможной недоступности URI', 'что endpoint обязательно выключится именно в момент timestamp', 'human review of residual risk and restore boundary'],
['Removal gate', 'отдельный decision о change', 'что hidden consumer невозможен', 'authorized evidence, stop condition, safe rollback or restore plan'],
]),
h2('Ограниченная воспроизводимая модель'),
p('Ниже нет HTTP call, file read, code search, customer record, CI or production. Функции принимают только один case id и возвращают fixed objects из этого модуля. Это намеренное ограничение. Оно позволяет проверить строгие input and report contracts: extra key, sparse array, forged decision и cyclic JSON не превращаются в «зелёный» план. Но fixture не доказывает ни один факт о реальном API.'),
code(fixtureExample),
p('В модели есть три case. fixed-active-consumer-v1 блокирует removal, потому что одна fixed row active и другая unknown. fixed-unknown-consumer-v1 показывает более неприятную ветку: visible row migrated, но authorization scope remains unknown. fixed-migrated-consumer-v1 разрешает только human review proposal — не deletion — потому что named rows migrated, а undiscovered client не доказан отсутствующим.'),
h2('Почему строгая форма важнее красивого summary'),
p('Если report можно дополнить произвольным полем traffic: 0, следующий reviewer может принять внешний факт без scope and method. Если array consumer map sparse, «пустое место» выглядит как строка, но у него нет owner and evidence. Если forged decision заменяет block-removal на remove-now, description перестаёт соответствовать fixed case. Поэтому fixture требует exact keys, dense arrays, canonical JSON и безопасно отвергает cycle.'),
p('Это не security control для реальных data. Это дисциплина учебного примера: model should not create its own fake evidence. В реальной работе содержимое report нужно получать только в разрешённой процедуре и хранить с method, period, scope and owner. Даже тогда его следует читать как evidence for a question, not universal consumer list.'),
h2('Как построить timeline без ложной автоматизации'),
ol([
'Назовите resource scope. Method, URI template, operationId, version and replacement должны описывать один предмет, а не «весь API».',
'Опубликуйте warning. Добавьте migration guide, owner and communication path. Это уменьшает новые dependencies, но не подтверждает чтение notice.',
'Объявите deprecation. Сверьте OpenAPI contract и, если выбран HTTP signal, его resource scope. Не меняйте functional behavior под видом notice.',
'Поставьте sunset boundary. Назовите дату как planned availability boundary и document, что client must not treat it as hard promise.',
'Соберите evidence by type. Source, dependency, exposure, authorization and unknown rows не склеивайте. Для каждой запишите blind zone.',
'Откройте removal review. До отдельного change назовите active blockers, residual unknown, stop condition and restore boundary.',
]),
h2('Stop condition и restore boundary'),
p('Stop condition должен быть короче, чем план удаления. Пример: «появилась active row в разрешённой проверке» или «owner replacement contract не может подтвердить compatibility». В этот момент proposal останавливают, а не «дочищают» ещё два слоя, чтобы сохранить дату. Restore boundary отвечает на следующий вопрос: куда команда возвращается, пока реальное удаление не началось? Для API это обычно last documented compatible contract, а не магическое «вернуть всё назад».'),
p('В synthetic model restoreSyntheticDeprecationReview умеет только выбросить canonical in-memory draft. Это специально слабая операция. Она не открывает route, не трогает headers and configuration и не возвращает traffic. Настоящий rollback должен описывать authorization, schema and data compatibility отдельно. If a new client contract already writes irreversible state, a timestamp cannot be its restore plan.'),
h2('Ограничения и следующий проверяемый шаг'),
p('Эта статья не предлагает метод собирать customer list и не описывает реальную telemetry. В ней нет access-log query, real request count, credential, incident, metric или active account. Fixed labels созданы для проверки формата, а не для оценки нагрузки. SemVer и OpenAPI задают versioning and contract vocabulary, а RFC 8594 и IETF draft-09 задают lifecycle signals; ни один источник не делает один график доказательством отсутствия всех consumers.'),
p('Следующий шаг: возьмите один endpoint и создайте пять строк evidence types из таблицы. В каждой добавьте самый опасный blind zone. Если для строки exposure нельзя назвать tool and period, она остаётся unknown. Ожидаемый результат — timeline без фальшивого зелёного статуса: команда знает, что именно объявлено, что observed, что не доказано и какой факт остановит removal review.'),
h2('Историческая граница ноября 2024'),
p('Источники зафиксированы до ноября 2024: RFC 8594 (May 2019), IETF draft-ietf-httpapi-deprecation-header-09 (September 2024), OpenAPI Specification v3.1.0 и Semantic Versioning 2.0.0. Draft-09 в этот момент не был RFC. Его signal и RFC Sunset — information about lifecycle, не telemetry contract. Все labels, states, dates, actions and outcomes в fixture — fixed synthetic in-memory records; script не обращается к network, files, Git, CI, production, telemetry, authorization или customer data.'),
]);
const field = revision({
slug: 'editorial-2024-11-field-deprecation',
title: 'Последний consumer: доказуемое удаление',
categories: ['Рефакторинг', 'API'],
cover: '/assets/editorial/2024/deprecation-2024-removal-gate.svg',
excerpt: 'Три fixed synthetic case для removal gate: active, unknown и migrated. Как остановить удаление, зафиксировать residual risk и не назвать учебную карту доказательством отсутствия скрытых потребителей.',
readingMinutes: 13,
}, [
p('В конце migration часто остаётся один вопрос: «кто последний consumer?». Он опасен не потому, что на него нельзя ответить, а потому, что звучит как просьба о полном списке пользователей. Команда находит несколько migrated integrations, открывает задачу удаления и считает путь свободным. Цена ошибки — hidden client получает 4xx после change, а у владельца нет сохранённой boundary: что именно было проверено, кто принял residual risk и можно ли безопасно остановиться до разрушения compatibility.'),
p('Противоположная крайность — не удалять ничего, пока не появится невозможное доказательство отсутствия всех неизвестных. Так старый контракт навсегда остаётся в code, documentation и test matrix. Практический выход не в обещании «скрытых consumers нет». Он в removal gate, то есть наборе условий допуска к удалению: separate known evidence from unknown, назвать stop condition, оставить restore boundary и дать human owner принять решение по ограниченному scope.'),
h2('Симптом → причина → проверка → действие'),
ol([
'Симптом. Все известные integrations migrated, но никто не может доказать, что hidden caller невозможен.',
'Причина. Migration status named rows перепутали с complete population, а removal change не имеет отдельного gate и restore boundary.',
'Проверка. Разложите результат на three cases: active, unknown and migrated. Для каждого укажите, что blocked, кто owner и где изменение должно остановиться.',
'Действие. Active and unknown блокируют automatic removal. Migrated открывает только human review с residual risk; actual deletion остаётся отдельным authorized change.',
]),
h2('Три fixed synthetic case'),
p('Первый case — fixed-active-consumer-v1. В нём fixed map содержит active source-usage row для synthetic-web-checkout и unknown integrator. Важно не то, как эти строки получены: они не получены из реального code search. Важно, что модель не разрешает спорить с собственным фактом. Пока active row есть, removal gate закрыт. Next step — не новое окно наблюдения, а owner and replacement discussion для указанной operation.'),
p('Второй case — fixed-unknown-consumer-v1. Named declared dependency уже migrated, но authorization class остаётся unknown, а exposure label говорит только not-observed-in-model. Это не ноль. Такое состояние сложнее active: нельзя дать migration task конкретному consumer, но и нельзя безопасно вычеркнуть риск. Gate требует human decision: ограничить scope authorised evidence, отложить removal или сохранить compatibility.'),
p('Третий case — fixed-migrated-consumer-v1. Два named rows migrated, replacement contract and announcement записаны, но synthetic-undiscovered-client остаётся unknown-not-proved-absent. Модель поэтому не возвращает safe-to-delete. Она разрешает только allow-human-removal-review-only: сформировать proposal с residual risk and restore boundary. Это честнее, чем объявить map полным без способа проверить population.'),
table('Три case и честный вердикт removal gate', ['Fixed case', 'Что известно в model', 'Что остаётся неизвестным', 'Вердикт', 'Следующий шаг'], [
['active', 'одна named row active; contract and owner defined', 'полнота неизвестной внешней scope', 'block removal', 'владелец active row согласует replacement and migration'],
['unknown', 'одна visible dependency migrated', 'authorization scope and exposure population', 'block removal', 'human owner ограничивает authorised check or keeps compatibility'],
['migrated', 'named rows migrated; replacement, deadline and notice recorded', 'hidden consumer not proved absent', 'human review only', 'оформить residual risk, stop condition and separate removal change'],
]),
figure('/assets/editorial/2024/deprecation-2024-removal-gate.svg', 'Removal gate с тремя ветками: active ведёт к миграции, unknown — к ограничению scope или сохранению совместимости, migrated — к human review. Все ветки сходятся только в отдельный authorized removal change с stop condition и restore boundary.', 'Схема не обещает обнаружить всех hidden consumers. Она показывает, почему active, unknown и migrated должны оставаться разными состояниями до реального change.'),
h2('Removal gate проверяет отрицательные условия'),
p('Gate полезен, когда сформулирован как набор причин не удалять. Нет active named row. Нет replacement without owner. Announcement and deadline сохранены. Unknown не спрятан под нулём. Restore boundary существует. Такой список звучит медленнее, чем deadline, но делает стоимость решения видимой. Если один пункт не выполнен, scope удаления нельзя расширять.'),
table('Removal gate перед отдельным change', ['Проверка', 'Пройти можно, если', 'Stop condition', 'Безопасное действие'], [
['Contract scope', 'одна operation и replacement описаны без двусмысленности', 'route, method or response semantics спорны', 'остановить proposal и сузить contract'],
['Known consumers', 'каждая named row имеет owner and migration path', 'active row или owner missing', 'не удалять; вернуть migration к owner'],
['Unknown boundary', 'unknown явно записан и residual risk имеет owner', 'unknown назван «нулём» без method', 'заблокировать automatic removal'],
['Announcement', 'migration guide, deadline and affected scope доступны', 'notice не связан с replacement', 'обновить communication record before review'],
['Restore boundary', 'есть last compatible contract и criterion остановки', 'rollback требует неизвестных data or auth changes', 'не начинать removal change'],
]),
h2('Воспроизводимый учебный прогон'),
p('Пример ниже проверяет pure in-memory object. Он не показывает real API status. Его ценность в другом: input принимает только exact case id; planner принимает только canonical report; forged map, extra key, sparse array and cyclic JSON не могут пройти как valid removal plan. Это минимальная защита от того, чтобы красивый summary подменил структуру доказательства ещё до настоящего review.'),
code([
"import {",
" createFixedSyntheticDeprecationInput,",
" inspectSyntheticDeprecation,",
" planSyntheticDeprecationReview,",
" restoreSyntheticDeprecationReview,",
"} from './upgrade-2024-11.mjs';",
'',
"const report = inspectSyntheticDeprecation(",
" createFixedSyntheticDeprecationInput('fixed-migrated-consumer-v1'),",
");",
'const proposal = planSyntheticDeprecationReview(report);',
'const stopped = restoreSyntheticDeprecationReview(proposal);',
'',
'console.log(report.decision.code); // allow-human-removal-review-only',
'console.log(stopped.restored); // true: only draft is discarded',
'',
'// The model does not query an endpoint or restore a deployed route.',
].join('\n')),
p('Если заменить fixed report на report с extra traffic, or make consumer map sparse, planner returns rejected object. Если добавить cyclic decision, comparison also rejects it without throwing. Fixture covers these negative branches. PASS means only that the three artificial cases retain their shape and boundaries. It does not mean v1 has no consumers, that v2 is compatible or that a production rollback would succeed.'),
h2('Stop condition нужно назвать до удаления'),
p('Stop condition — это не «если что-то пойдёт не так». Он должен быть наблюдаемым и привязанным к scope: active consumer found in an authorized check; replacement contract cannot preserve an agreed error or idempotency boundary; unknown risk cannot be accepted by named owner; migration guide has no reachable communication path. После stop condition change не расширяют и не склеивают с redesign. Возвращаются к last documented compatible contract и открывают отдельный вопрос.'),
p('Restore boundary ограничивает обещание. Для status before a real change достаточно сказать: removal proposal stopped, old compatibility stays documented, draft discarded. После реального route removal это уже другой plan: кто может re-enable, какие credentials, data and schema remain compatible, как проверить response and client recovery. Если этих фактов нет, нельзя называть operation reversible. Пустая строка «rollback available» хуже, чем явное unknown.'),
h2('Checklist для human review'),
ol([
'Сверьте один contract. Method, URI, operationId, authentication, request and response boundary должны совпадать между notice, map and replacement.',
'Прочитайте rows по классу. Не складывайте source usage, declared dependency, observed traffic and authorization в единый count.',
'Оставьте unknown видимым. Укажите reason, owner and next authorised question. Не называйте его отсутствием consumer.',
'Проверьте replacement. Migration path должен вести к contract, который имеет owner and compatibility scope, а не просто к новому URL.',
'Проверьте announcement. Documentation link, deadline and affected scope должны быть доступны до Sunset boundary.',
'Назовите stop condition. Один факт должен прекращать proposal без попытки одновременно исправить route, data and policy.',
'Отделите removal change. Только после review отдельное изменение получает tests, approvals, deployment and restore plan; deprecation record не выполняет эти действия сам.',
]),
h2('Почему header не закрывает поле доказательств'),
p('Deprecation header и OpenAPI flag полезны для communication. IETF draft-09 говорит, что deprecation itself does not change resource behavior. RFC 8594 говорит, что Sunset timestamp is a hint and does not tell which response follows afterwards. Эти свойства как раз защищают migration: client получает notice before path becomes unavailable. Но client can ignore notice, cache a contract or be outside the chosen delivery boundary. Therefore headers belong to announcement row, not to final proof of removal.'),
p('SemVer 2.0.0 similarly helps communicate public API change when it applies: deprecating public functionality increments minor version, incompatible deletion requires major version. It does not choose an acceptable residual risk or inventory consumer. Versioning makes contract evolution explicit; removal gate makes the operational decision reviewable. These layers should reinforce each other, not impersonate each other.'),
h2('Ограничения и следующий проверяемый шаг'),
p('В статье нет real client names, calls, traffic, authorization records, customer accounts, incidents or metrics. The three cases are fixed synthetic records embedded in one JS module. They do not read files, Git, network, CI, clock, production or telemetry. Unknown is deliberately preserved as a limitation, so the material does not promise that hidden consumers are absent.'),
p('Следующий шаг: для одного deprecated operation проведите этот checklist с владельцем replacement. Укажите one active, one unknown or one migrated verdict — whichever is honest — и отдельно зафиксируйте stop condition. Ожидаемый результат: team either blocks removal for a concrete reason or opens a narrowly scoped human review with an explicit residual risk, rather than deleting an endpoint because the calendar reached a date.'),
h2('Историческая граница ноября 2024'),
p('Материал использует RFC 8594 (May 2019) для Sunset, IETF draft-ietf-httpapi-deprecation-header-09 (September 2024) для deprecation signal, OpenAPI Specification v3.1.0 для declaration и Semantic Versioning 2.0.0 для versioning vocabulary. На историческую дату draft-09 был draft, не RFC. Fixed consumer names, dates, evidence labels, outcomes and restore actions — teaching values. Они не являются результатом real telemetry, source search, authorization review, customer communication, incident or production deletion.'),
]);
export const revisions = [practice, mechanism, field].map(({ proseLength, ...item }) => item);
function verifyFixture() {
const report = runDeprecationFixture();
const failed = Object.entries(report.assertions).filter(([, value]) => value !== true).map(([key]) => key);
if (failed.length > 0) {
process.stderr.write('FAIL fixture: ' + failed.join(', ') + '\n');
process.exitCode = 1;
return;
}
const count = Object.keys(report.assertions).length;
process.stdout.write('PASS fixture: ' + count + '/' + count + ' assertions\n');
}
if (process.argv.includes('--verify-fixture')) verifyFixture();
if (process.argv.includes('--print-revisions')) process.stdout.write(JSON.stringify(revisions) + '\n');