{ "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. Возникает соблазн заменить в старом файле пару слов и оставить прежнюю дату.
Такой diff скрывает две разные операции. Первая — уточнить evidence: решение всё ещё подходит, а наблюдение или ссылка стали точнее. Вторая — изменить архитектурную границу: теперь клиент управляет длительной операцией и должен уметь повторно получить её результат. Во втором случае правка старой записи стирает причину прежнего выбора и лишает команду точки сравнения. Разберём, как отличить эти случаи и оформить successor — новую запись, которая заменяет прежнюю.
\nArchitectural Decision Record — запись одного архитектурно значимого решения. Официальный сайт ADR описывает три опорные идеи: решение отвечает значимому требованию, сохраняет rationale и показывает trade-offs и последствия. Это не снимок текущего кода и не обещание, что выбранный вариант навсегда останется лучшим.
\nПрактический минимальный каркас выглядит так: Status, Context, Decision и Consequences. В Context попадает проблема и условия выбора. Decision называет выбранную границу. Consequences показывает, что стало легче, а что — дороже или рискованнее. MADR 4.0.0 расширяет каркас decision drivers, рассмотренными вариантами и способом confirmation — подтверждения того, что реализация соответствует записи.
Из этого следует полезное разделение. ADR фиксирует rationale. Исходный код и контракт показывают, что система делает сейчас. Тест, запрос к метрике или результат review отвечает на конкретный вопрос о соответствии. План изменения описывает rollout, наблюдение и rollback. Ссылка из ADR на файл не заменяет ни проверку, ни план.
\nСначала выпишите исходное условие, а не название технологии. В нашем примере оно звучит так: «маленький экспорт завершается в рамках запроса, а вызывающая сторона получает файл сразу». Затем соберите новый факт: «время выполнения превышает допустимое ожидание, поэтому сервер возвращает идентификатор операции, а клиент опрашивает её состояние».
\nЕсли новый факт только уточняет ссылку, владельца или измерение, accepted ADR можно дополнить датированной заметкой по правилам команды. Если изменились требование, assumption или граница ответственности, нужен новый ADR. В нём следует сослаться на старый, описать варианты и объяснить цену перехода. Статус старой записи меняется на superseded только после принятия successor. Это правило статьи — безопасная политика append-only; конкретный проект может выбрать другую процедуру и обязан описать её заранее.
| Наблюдение | Что могло измениться | Проверка | Следующий артефакт |
|---|---|---|---|
В ADR синхронный ответ, в контракте jobId | Граница времени и владелец состояния операции | Сравнить версию контракта, обработчик и retry-путь | Successor в статусе Proposed |
| Ссылка ведёт на удалённый модуль | Артефакт переехал, но решение не изменилось | Найти новый устойчивый путь и проверить тот же инвариант | Датированное обновление evidence |
| Появился новый класс данных | Изменились требования безопасности или хранения | Проверить threat model, права и срок хранения | Новый ADR либо отклонённая альтернатива |
| При review нет факта о consequence | Календарная дата подменила проверку | Назначить owner, источник, среду и критерий опровержения | Validation task, а не новый статус |
| Команда хочет «починить текст» после отката | Откат реализации перепутан с отменой решения | Сверить принятый successor и фактический rollback | Сохранённый Accepted ADR или явный Rejected ADR |
Что изменилось? Запишите наблюдаемый сигнал: новый endpoint, лимит времени, обязанность хранить статус, класс данных или исчезнувший владелец. Формула «код стал другим» недостаточна, потому что код мог изменить реализацию внутри прежней границы.
\nКакая assumption нарушена? Assumption — условие, на котором держался выбор. Для синхронного экспорта это может быть верхняя граница времени ответа. Для очереди — наличие владельца retry и понятного срока хранения. Назовите условие числом или проверяемым правилом, если это возможно.
\nКакие варианты остаются? Сравните минимум два реалистичных варианта. Для экспорта это синхронный путь с жёстким лимитом, очередь с видимым статусом или передача работы внешнему сервису. Критерии должны быть связаны с проблемой: время ответа, восстановление после сбоя, права доступа, стоимость сопровождения и обратная совместимость.
\nЧто подтвердит выбор? Confirmation должен отвечать на один конкретный вопрос. Например: «контракт позволяет клиенту получить итог после временного сетевого сбоя, не создавая второй экспорт». Укажите тест, запрос или review, среду, владельца и результат, который заставит пересмотреть решение.
\nНиже — самодостаточная fixture на Node.js. Она не читает репозиторий и не доказывает свойства реальной очереди. Её задача — сделать правило перехода явным: изменение протокола считается сигналом для successor, но старый Accepted ADR не переводится автоматически.
\nnode --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. Учебная программа лишь защищает от логической ошибки «новый контракт найден — старую историю можно переписать».
Создайте новую запись рядом со старой и поставьте ей Proposed. В заголовке назовите решаемую проблему и выбранную границу, а не внутреннее имя очереди. Минимальная структура может выглядеть так:
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.
У evidence есть субъект, область и срок действия. Commit показывает состояние кода в конкретной версии. Контрактный тест показывает допустимые формы запроса и ответа. Метрика показывает измеренное поведение при выбранной нагрузке и окне наблюдения. Review подтверждает согласование, но не заменяет эксплуатационную проверку.
\nНельзя делать следующий скачок без отдельного доказательства: «jobId есть в схеме» не означает, что операция переживает повтор; «ошибок мало» не означает, что recovery безопасен; «ADR связан с PR» не означает, что rollout можно откатить. В ADR полезно писать и отрицательный результат: какой риск не проверен, кто его проверит и какое наблюдение остановит выпуск.
\nДля локального поиска можно начать с таких команд, подставив пути своего проекта:
\nrg -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 автоматически: совпадение слов не доказывает совпадение смысла, а отсутствие совпадения не доказывает, что решение нарушено.
\nADR не является RFC, тестовым раннером, threat model, benchmark, runbook или системой управления изменениями. Официальные шаблоны предлагают язык и поля, но не назначают универсальные сроки review, обязательный набор ролей или порог для создания записи. Статусы Accepted, Rejected и Superseded должны иметь однозначное значение в политике конкретного репозитория.
Append-only подход снижает риск переписать rationale, но увеличивает число записей и требует навигации между ними. Живой документ может быть удобнее для команды, однако тогда нужны датированные изменения и видимая история. Ни один вариант не спасает от неверного Context или отсутствующего owner. Нельзя объявлять решение корректным по одной ссылке, одной метрике или успешному запуску fixture.
\nДля описанного случая критерий готовности таков: видны исходная assumption, новый сигнал, выбранные варианты, consequence, владелец confirmation и результат, который опровергнет решение. Если не хватает хотя бы одного элемента, честный итог — открытая проверка или Proposed successor, а не отредактированная история.
\n