DarkRiDDeR13 мин

Последний consumer: доказуемое удаление

РефакторингAPI

В конце migration часто остаётся один вопрос: «кто последний consumer?». Он опасен не потому, что на него нельзя ответить, а потому, что звучит как просьба о полном списке пользователей. Команда находит несколько migrated integrations, открывает задачу удаления и считает путь свободным. Цена ошибки — hidden client получает 4xx после change, а у владельца нет сохранённой boundary: что именно было проверено, кто принял residual risk и можно ли безопасно остановиться до разрушения compatibility.

Противоположная крайность — не удалять ничего, пока не появится невозможное доказательство отсутствия всех неизвестных. Так старый контракт навсегда остаётся в code, documentation и test matrix. Практический выход не в обещании «скрытых consumers нет». Он в removal gate, то есть наборе условий допуска к удалению: separate known evidence from unknown, назвать stop condition, оставить restore boundary и дать human owner принять решение по ограниченному scope.

Симптом → причина → проверка → действие

  1. Симптом. Все известные integrations migrated, но никто не может доказать, что hidden caller невозможен.
  2. Причина. Migration status named rows перепутали с complete population, а removal change не имеет отдельного gate и restore boundary.
  3. Проверка. Разложите результат на three cases: active, unknown and migrated. Для каждого укажите, что blocked, кто owner и где изменение должно остановиться.
  4. Действие. Active and unknown блокируют automatic removal. Migrated открывает только human review с residual risk; actual deletion остаётся отдельным authorized change.

Три fixed synthetic case

Первый 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.

Второй 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.

Третий 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.

Три case и честный вердикт removal gate
Fixed caseЧто известно в modelЧто остаётся неизвестнымВердиктСледующий шаг
activeодна named row active; contract and owner definedполнота неизвестной внешней scopeblock removalвладелец active row согласует replacement and migration
unknownодна visible dependency migratedauthorization scope and exposure populationblock removalhuman owner ограничивает authorised check or keeps compatibility
migratednamed rows migrated; replacement, deadline and notice recordedhidden consumer not proved absenthuman review onlyоформить residual risk, stop condition and separate removal change
Removal gate с тремя ветками: active ведёт к миграции, unknown — к ограничению scope или сохранению совместимости, migrated — к human review. Все ветки сходятся только в отдельный authorized removal change с stop condition и restore boundary.
Схема не обещает обнаружить всех hidden consumers. Она показывает, почему active, unknown и migrated должны оставаться разными состояниями до реального change.

Removal gate проверяет отрицательные условия

Gate полезен, когда сформулирован как набор причин не удалять. Нет active named row. Нет replacement without owner. Announcement and deadline сохранены. Unknown не спрятан под нулём. Restore boundary существует. Такой список звучит медленнее, чем deadline, но делает стоимость решения видимой. Если один пункт не выполнен, scope удаления нельзя расширять.

Removal gate перед отдельным change
ПроверкаПройти можно, еслиStop conditionБезопасное действие
Contract scopeодна operation и replacement описаны без двусмысленностиroute, method or response semantics спорныостановить proposal и сузить contract
Known consumersкаждая named row имеет owner and migration pathactive row или owner missingне удалять; вернуть migration к owner
Unknown boundaryunknown явно записан и residual risk имеет ownerunknown назван «нулём» без methodзаблокировать automatic removal
Announcementmigration guide, deadline and affected scope доступныnotice не связан с replacementобновить communication record before review
Restore boundaryесть last compatible contract и criterion остановкиrollback требует неизвестных data or auth changesне начинать removal change

Воспроизводимый учебный прогон

Пример ниже проверяет 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.

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.

Если заменить 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.

Stop condition нужно назвать до удаления

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 и открывают отдельный вопрос.

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.

Checklist для human review

  1. Сверьте один contract. Method, URI, operationId, authentication, request and response boundary должны совпадать между notice, map and replacement.
  2. Прочитайте rows по классу. Не складывайте source usage, declared dependency, observed traffic and authorization в единый count.
  3. Оставьте unknown видимым. Укажите reason, owner and next authorised question. Не называйте его отсутствием consumer.
  4. Проверьте replacement. Migration path должен вести к contract, который имеет owner and compatibility scope, а не просто к новому URL.
  5. Проверьте announcement. Documentation link, deadline and affected scope должны быть доступны до Sunset boundary.
  6. Назовите stop condition. Один факт должен прекращать proposal без попытки одновременно исправить route, data and policy.
  7. Отделите removal change. Только после review отдельное изменение получает tests, approvals, deployment and restore plan; deprecation record не выполняет эти действия сам.

Почему header не закрывает поле доказательств

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.

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.

Ограничения и следующий проверяемый шаг

В статье нет 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.

Следующий шаг: для одного 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.

Историческая граница ноября 2024

Материал использует 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.

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

  • RFC 8594: The Sunset HTTP Header Field, May 2019 — Sunset сообщает, что конкретный URI, вероятно, станет недоступен в указанную дату; это hint, а не гарантия доступности или доказательство миграции. Граница: RFC различает стадию «не рекомендуем» и decommission: Sunset относится ко второй. Он не перечисляет клиентов, не задаёт срок уведомления и не обещает ответ после даты.
  • IETF draft-ietf-httpapi-deprecation-header-09, 27.09.2024 — Deprecation header сообщает, что ресурс уже устарел или устареет; Link с relation deprecation может вести к документации и migration guide. Граница: На ноябрь 2024 это IETF Internet-Draft, не опубликованный RFC. Сам header не меняет поведение ресурса и не является списком его пользователей.
  • OpenAPI Specification v3.1.0: Operation Object — Поле deprecated в Operation Object объявляет операцию устаревшей; consumers SHOULD refrain from usage, default false. Граница: Спецификация описывает декларацию в контракте. Она не подтверждает, что generated client обновлён, вызовов нет или replacement совместим с каждым потребителем.
  • Semantic Versioning 2.0.0 — SemVer требует объявить public API; при пометке public API как deprecated увеличивается minor version, а при несовместимом изменении public API — major version. Граница: SemVer — схема версий для объявленного API, не protocol удаления. Она не даёт consumer map, calendar policy, telemetry model или authority удалить endpoint.