{ "index": 118, "slug": "editorial-2024-09-field-adr-decisions", "title": "ADR устарел: как проверить решение и не переписать его историю", "excerpt": "Старый ADR не становится неверным только потому, что изменился код. Разбираем drift, successor-запись, confirmation и безопасный переход от Accepted к Superseded.", "contentHtml": "

Проблема в актуальности ADR проявляется, когда репозиторий открывают перед изменением сервиса. В записи зафиксирован синхронный экспорт: клиент ждёт ответ с готовым файлом. В коде уже появился worker, а endpoint возвращает jobId и состояния pending, completed и failed. Возникает соблазн заменить в старом файле пару слов и оставить прежнюю дату.

\n

Такой diff скрывает две разные операции. Первая — уточнить evidence: решение всё ещё подходит, а наблюдение или ссылка стали точнее. Вторая — изменить архитектурную границу: теперь клиент управляет длительной операцией и должен уметь повторно получить её результат. Во втором случае правка старой записи стирает причину прежнего выбора и лишает команду точки сравнения. Разберём, как отличить эти случаи и оформить successor — новую запись, которая заменяет прежнюю.

\n

ADR хранит решение в его контексте

\n

Architectural Decision Record — запись одного архитектурно значимого решения. Официальный сайт ADR описывает три опорные идеи: решение отвечает значимому требованию, сохраняет rationale и показывает trade-offs и последствия. Это не снимок текущего кода и не обещание, что выбранный вариант навсегда останется лучшим.

\n

Практический минимальный каркас выглядит так: Status, Context, Decision и Consequences. В Context попадает проблема и условия выбора. Decision называет выбранную границу. Consequences показывает, что стало легче, а что — дороже или рискованнее. MADR 4.0.0 расширяет каркас decision drivers, рассмотренными вариантами и способом confirmation — подтверждения того, что реализация соответствует записи.

\n

Из этого следует полезное разделение. ADR фиксирует rationale. Исходный код и контракт показывают, что система делает сейчас. Тест, запрос к метрике или результат review отвечает на конкретный вопрос о соответствии. План изменения описывает rollout, наблюдение и rollback. Ссылка из ADR на файл не заменяет ни проверку, ни план.

\n

Drift не равен устаревшему решению

\n

Сначала выпишите исходное условие, а не название технологии. В нашем примере оно звучит так: «маленький экспорт завершается в рамках запроса, а вызывающая сторона получает файл сразу». Затем соберите новый факт: «время выполнения превышает допустимое ожидание, поэтому сервер возвращает идентификатор операции, а клиент опрашивает её состояние».

\n

Если новый факт только уточняет ссылку, владельца или измерение, accepted ADR можно дополнить датированной заметкой по правилам команды. Если изменились требование, assumption или граница ответственности, нужен новый ADR. В нём следует сослаться на старый, описать варианты и объяснить цену перехода. Статус старой записи меняется на superseded только после принятия successor. Это правило статьи — безопасная политика append-only; конкретный проект может выбрать другую процедуру и обязан описать её заранее.

\n
Диагностика расхождения между ADR и текущей системой
НаблюдениеЧто могло изменитьсяПроверкаСледующий артефакт
В ADR синхронный ответ, в контракте jobIdГраница времени и владелец состояния операцииСравнить версию контракта, обработчик и retry-путьSuccessor в статусе Proposed
Ссылка ведёт на удалённый модульАртефакт переехал, но решение не изменилосьНайти новый устойчивый путь и проверить тот же инвариантДатированное обновление evidence
Появился новый класс данныхИзменились требования безопасности или храненияПроверить threat model, права и срок храненияНовый ADR либо отклонённая альтернатива
При review нет факта о consequenceКалендарная дата подменила проверкуНазначить owner, источник, среду и критерий опроверженияValidation task, а не новый статус
Команда хочет «починить текст» после откатаОткат реализации перепутан с отменой решенияСверить принятый successor и фактический rollbackСохранённый Accepted ADR или явный Rejected ADR
\n

Четыре вопроса перед созданием successor

\n

Что изменилось? Запишите наблюдаемый сигнал: новый endpoint, лимит времени, обязанность хранить статус, класс данных или исчезнувший владелец. Формула «код стал другим» недостаточна, потому что код мог изменить реализацию внутри прежней границы.

\n

Какая assumption нарушена? Assumption — условие, на котором держался выбор. Для синхронного экспорта это может быть верхняя граница времени ответа. Для очереди — наличие владельца retry и понятного срока хранения. Назовите условие числом или проверяемым правилом, если это возможно.

\n

Какие варианты остаются? Сравните минимум два реалистичных варианта. Для экспорта это синхронный путь с жёстким лимитом, очередь с видимым статусом или передача работы внешнему сервису. Критерии должны быть связаны с проблемой: время ответа, восстановление после сбоя, права доступа, стоимость сопровождения и обратная совместимость.

\n

Что подтвердит выбор? Confirmation должен отвечать на один конкретный вопрос. Например: «контракт позволяет клиенту получить итог после временного сетевого сбоя, не создавая второй экспорт». Укажите тест, запрос или review, среду, владельца и результат, который заставит пересмотреть решение.

\n

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

\n

Ниже — самодостаточная fixture на Node.js. Она не читает репозиторий и не доказывает свойства реальной очереди. Её задача — сделать правило перехода явным: изменение протокола считается сигналом для successor, но старый Accepted ADR не переводится автоматически.

\n
node --input-type=module <<'NODE'\nconst oldAdr = Object.freeze({\n  id: 'ADR-0012',\n  status: 'accepted',\n  boundary: 'small synchronous export',\n  maxDurationMs: 3000,\n});\n\nconst observedContract = {\n  transport: 'job',\n  returns: ['jobId', 'pending', 'completed', 'failed'],\n};\n\nconst boundaryChanged =\n  observedContract.transport !== 'sync' ||\n  !observedContract.returns.includes('jobId');\n\nconst review = {\n  signal: boundaryChanged ? 'successor-required' : 'reconfirm',\n  oldStatus: oldAdr.status,\n  oldAdrRemains: oldAdr.status === 'accepted',\n  confirmationQuestion:\n    'Can the caller resume one job and observe its final state?',\n};\n\nconsole.log(JSON.stringify(review, null, 2));\nNODE
\n

Ожидаемый результат — successor-required, а oldAdrRemains равен true. Это не проверка доступности worker, идемпотентности повторного запроса, авторизации или срока хранения. Для production эти свойства должны появиться в отдельном тесте или change plan. Учебная программа лишь защищает от логической ошибки «новый контракт найден — старую историю можно переписать».

\n
\"Схема
Сначала фиксируется сигнал и проверяется исходная assumption. Если граница выдержана, ADR подтверждают новыми данными. Если нет, создают Proposed successor; статус старой записи меняют только после явного принятия нового решения.
\n

Как оформить successor без потери истории

\n

Создайте новую запись рядом со старой и поставьте ей Proposed. В заголовке назовите решаемую проблему и выбранную границу, а не внутреннее имя очереди. Минимальная структура может выглядеть так:

\n
status: proposed\nsupersedes: ADR-0012\ndecision-makers: export team\n\n# Return a status for long-running exports\n\n## Context and Problem Statement\nThe synchronous request exceeds the caller timeout.\n\n## Decision Drivers\n- bounded request time\n- resumable result\n- authenticated access to job state\n\n## Considered Options\n- keep synchronous export\n- queue the export and expose job status\n\n## Decision Outcome\nChosen option: \"queue the export and expose job status\".\n\n## Consequences\n- The client handles pending, completed and failed states.\n- The service owns retention, retry and authorization rules.\n\n## Confirmation\nContract test: one job can be polled after a transient client timeout.
\n

Это пример полей, а не обязательный синтаксис для каждого репозитория. Если проект использует YAML front matter, другую нумерацию или отдельный каталог, сохраните локальный contract. Существенны не названия файлов, а обратная ссылка, граница решения, alternatives, consequences и проверка соответствия.

\n

После review возможны три результата. При принятии successor старую запись помечают Superseded by ADR-0013 и не меняют её исходные Context и Consequences. При отклонении нового варианта старый ADR остаётся Accepted, а причина отказа остаётся в новом Rejected ADR. При недостатке данных обе записи должны честно показать неопределённость: Proposed не является разрешением на rollout.

\n

Evidence не превращается в гарантию

\n

У evidence есть субъект, область и срок действия. Commit показывает состояние кода в конкретной версии. Контрактный тест показывает допустимые формы запроса и ответа. Метрика показывает измеренное поведение при выбранной нагрузке и окне наблюдения. Review подтверждает согласование, но не заменяет эксплуатационную проверку.

\n

Нельзя делать следующий скачок без отдельного доказательства: «jobId есть в схеме» не означает, что операция переживает повтор; «ошибок мало» не означает, что recovery безопасен; «ADR связан с PR» не означает, что rollout можно откатить. В ADR полезно писать и отрицательный результат: какой риск не проверен, кто его проверит и какое наблюдение остановит выпуск.

\n

Для локального поиска можно начать с таких команд, подставив пути своего проекта:

\n
rg -n '^status:|^supersedes:|^## (Context|Decision|Consequences|Confirmation)' docs/adr\nrg -n 'jobId|pending|completed|failed' src test\ngit diff -- docs/adr src test
\n

Команды показывают кандидатов для ручного сопоставления. Они не вычисляют архитектурный drift автоматически: совпадение слов не доказывает совпадение смысла, а отсутствие совпадения не доказывает, что решение нарушено.

\n

Порядок действий для команды

\n
  1. Сохраните копию исходного Accepted ADR и выпишите его Context, Decision, Consequences, assumptions, владельца и ссылки.
  2. Зафиксируйте один наблюдаемый сигнал и версию артефакта, в котором он обнаружен.
  3. Сопоставьте исходную границу с кодом, API-контрактом, правами и эксплуатационным маршрутом. Разделите факт, гипотезу и пробел evidence.
  4. Если граница не изменилась, обновите evidence датированной записью и назначьте следующую проверку. Не создавайте successor только из-за календарной даты.
  5. Если граница изменилась, опишите минимум два варианта, decision drivers, последствия, стоимость миграции и условия rollback.
  6. Создайте successor в Proposed, добавьте обратную ссылку и назначьте decision-makers и confirmation.
  7. Проведите review. Только после принятия свяжите записи и переведите старый ADR в Superseded по правилам проекта.
  8. Проведите реализацию отдельным change plan: тесты, rollout, наблюдение, stop condition и rollback.
\n

Ограничения применимости

\n

ADR не является RFC, тестовым раннером, threat model, benchmark, runbook или системой управления изменениями. Официальные шаблоны предлагают язык и поля, но не назначают универсальные сроки review, обязательный набор ролей или порог для создания записи. Статусы Accepted, Rejected и Superseded должны иметь однозначное значение в политике конкретного репозитория.

\n

Append-only подход снижает риск переписать rationale, но увеличивает число записей и требует навигации между ними. Живой документ может быть удобнее для команды, однако тогда нужны датированные изменения и видимая история. Ни один вариант не спасает от неверного Context или отсутствующего owner. Нельзя объявлять решение корректным по одной ссылке, одной метрике или успешному запуску fixture.

\n

Для описанного случая критерий готовности таков: видны исходная assumption, новый сигнал, выбранные варианты, consequence, владелец confirmation и результат, который опровергнет решение. Если не хватает хотя бы одного элемента, честный итог — открытая проверка или Proposed successor, а не отредактированная история.

\n

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

" }