Files
progcode/editorial/agent-rewrites/010.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
16 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 10,
"slug": "editorial-2027-09-field-mentor-series",
"title": "API-diff в code review: как найти несовместимость и сохранить откат",
"excerpt": "Практический разбор API-изменений: классифицируем риск, ищем потребителей, проверяем частичный rollout и удаляем старый контракт только после измеримого сигнала.",
"contentHtml": "<p>Ошибка в API-изменении часто выглядит безобидно: сервер собирается, локальный тест получает 200, diff занимает несколько строк. Затем старый клиент отправляет прежний запрос, получает новый обязательный ответ или неизвестное значение enum и ломается на успешном пути. Цена ошибки растёт быстро: приходится восстанавливать потребителей, задерживать rollout и решать, как вернуть сервер, если он уже записал данные в новом формате.</p>\n<p>Тезис простой: review API-diff должен проверять не только код сервера. Он должен связать форму контракта, потребителей, данные и порядок выката. Сначала классифицируйте изменение. Затем найдите тех, кто читает и пишет старую форму. После этого выберите расширение, версию или expand/contract. Такой порядок превращает спор о «безопасном» diff в набор проверяемых условий.</p>\n<h2>Механизм: контракт живёт по обе стороны границы</h2>\n<p>API состоит как минимум из запроса, ответа и иногда события. У каждого есть форма, значения и смысл. Добавление необязательного поля в ответ обычно расширяет контракт: старый клиент может его проигнорировать. Удаление поля сужает контракт. Новое обязательное поле в запросе ломает старого отправителя. Сужение enum ломает ветвление, которое раньше обрабатывало удалённое значение.</p>\n<p>Тип изменения недостаточен. Поле может остаться строкой, но поменять часовой пояс, единицу измерения или правило пустого значения. Формальная схема пропустит такой ответ, а клиент изменит поведение. Поэтому отдельно фиксируйте структурную совместимость и семантическую совместимость. Первая отвечает на вопрос «можно ли распарсить данные». Вторая — «можно ли продолжить прежнюю операцию с тем же смыслом».</p>\n<table><caption>Симптом → причина → проверка → действие</caption><thead><tr><th scope='col'>Симптом</th><th scope='col'>Причина</th><th scope='col'>Проверка</th><th scope='col'>Действие</th></tr></thead><tbody><tr><td>Старый клиент не создаёт ресурс</td><td>В запрос добавили required-поле</td><td>Найти builders, SDK и fixtures старой версии</td><td>Оставить поле optional, задать совместимое значение или выпустить версию</td></tr><tr><td>Клиент падает на успешном ответе</td><td>Удалили свойство или изменили тип</td><td>Поиск чтения свойства и contract-тест старого клиента</td><td>Сначала deprecated-окно и новое поле, затем удаление</td></tr><tr><td>Новая ветка обработки не срабатывает</td><td>Сузили enum или добавили неизвестное значение</td><td>Прогнать значения через старые switch и парсеры</td><td>Сохранить старые значения либо сменить версию</td></tr><tr><td>Откат приложения не восстанавливает работу</td><td>Новый сервер записал только новый формат</td><td>Проверить чтение старой версией после частичного rollout</td><td>Сначала expand, затем switch, потом contract</td></tr><tr><td>Тесты зелёные, внешний потребитель сломан</td><td>Потребитель не попал в репозиторий</td><td>Проверить registry, документацию, логи и владельцев интеграций</td><td>Остановить удаление и оставить совместимый путь</td></tr></tbody></table>\n<h2>Пример: классификатор как первая страховка</h2>\n<p>Ниже учебная функция получает уже выделенные признаки diff. Она не читает OpenAPI, не ищет клиентов и не разрешает pull request автоматически. Её граница полезна именно поэтому: генератор или reviewer передаёт факты, а функция одинаково маркирует очевидный breaking-риск. Реальная проверка должна дополнить её consumer-тестами и проверкой данных.</p>\n<pre><code>function classifyApiChange(change) {\n const removed = Array.isArray(change.removedProperties)\n ? change.removedProperties\n : [];\n const required = Array.isArray(change.addedRequiredProperties)\n ? change.addedRequiredProperties\n : [];\n const narrowedEnum = Boolean(change.narrowedEnum);\n const breaking = removed.length &gt; 0\n || required.length &gt; 0\n || narrowedEnum;\n\n return {\n status: breaking ? 'breaking' : 'compatible',\n action: breaking\n ? 'version-or-expand-compatibility-window'\n : 'run-consumer-contract-tests',\n };\n}\n\nconst result = classifyApiChange({\n removedProperties: ['displayName'],\n addedRequiredProperties: [],\n narrowedEnum: false,\n});\n\nconsole.log(result);\n// { status: 'breaking',\n// action: 'version-or-expand-compatibility-window' }</code></pre>\n<p>Функция намеренно не считает любое добавление опасным. Необязательное поле в ответе обычно можно выпустить сразу, если клиенты действительно игнорируют неизвестные свойства. Но это условие нужно проверить. Клиент с жёстким декодером, схемой с <code>additionalProperties: false</code> или сравнением полного JSON может сломаться даже на расширении. Статус <code>compatible</code> здесь означает «нет очевидного структурного breaking-признака», а не «изменение доказанно безопасно».</p>\n<h2>Обратимость: сначала данные, потом бинарник</h2>\n<p>Rollback приложения не возвращает базу, очередь и уже отправленные события. Если новый код записал только <code>displayNameV2</code>, старый код может не знать, как его читать. Если событие получило новое обязательное поле, повторная доставка старому consumer не станет безопасной от одного переключения образа. Поэтому опасные изменения проводите в несколько фаз.</p>\n<p>На фазе expand добавьте новую колонку, поле или форму так, чтобы старый код продолжал работать. На фазе совместимости научите новый код читать старую и новую формы и, при необходимости, писать обе. На фазе switch переведите потребителей и проверьте сигнал использования старого пути. Только после этого выполняйте contract: удаляйте поле, старый writer или колонку. Для каждой фазы нужна обратная операция и условие перехода.</p>\n<figure><img src='/assets/editorial/2027/mentor-series-2027-autonomy-handoff-loop.svg' alt='Маршрут API review: классификация diff, проверка потребителей и окно обратимой миграции перед удалением старой формы.' loading='lazy' /><figcaption>Схема связывает локальный diff с потребителями и данными. Если старый формат ещё нужен, путь к удалению должен остановиться.</figcaption></figure>\n<h2>Порядок действий для одного изменения</h2>\n<ol><li>Назовите endpoint, метод, статус, media type и версию клиента. Не смешивайте request, response и внутреннюю модель в одном описании.</li><li>Снимите старую и новую формы. Выпишите required-поля, типы, nullable, enum, значения по умолчанию и изменение смысла.</li><li>Запустите классификацию. Для каждого breaking-признака укажите конкретное поле и потребителя, которого он затрагивает.</li><li>Найдите потребителей по сгенерированным типам, сериализаторам, SDK, fixture, документации, очередям и логам. Отдельно отметьте внешние интеграции, которых нет в монорепозитории.</li><li>Составьте матрицу чтения и записи: старая версия против старой формы, старая против новой, новая против старой и новая против новой. Добавьте частичный rollout и повторную доставку события.</li><li>Напишите отрицательный contract-тест для старого клиента и положительный тест для нового. Ошибка должна называть поле, значение и ожидаемую форму.</li><li>Выберите способ перехода: совместимое расширение, новая версия или expand/contract. Для rollout запишите точку остановки и способ возврата.</li><li>Удаляйте старую форму только после измеримого сигнала: старый consumer не обращается к полю, истёк согласованный срок хранения, а восстановление проверено.</li></ol>\n<h2>Где автоматическая проверка не помогает</h2>\n<p>Классификатор не знает, что внешний клиент использует поле чаще внутреннего. Он не видит подписанный payload, кэш, задержанную очередь и SDK, который обновляется отдельно. Он также не проверяет авторизацию, конкурентное изменение ресурса и идемпотентность повторного запроса. Эти риски нужно описать в своих тестах и процедуре rollout.</p>\n<p>Схема не доказывает корректность операции. Ответ может быть валидным по JSON, но устареть между чтением и записью. Для такого случая нужны версия ресурса, условный запрос вроде <code>If-Match</code>, правило конфликта и отдельный статус. Не прячьте доменный инвариант в описании поля: форма данных и допустимость перехода состояния — разные проверки.</p>\n<p>Пример синтетический. Он показывает границу классификатора и порядок миграции, но не заявляет замеры, production-результаты или совместимость с конкретным сервисом. Перед выпуском замените вымышленные поля реальным diff и приложите доказательства по вашим потребителям.</p>\n<h2>Критерий готовности</h2>\n<p>Изменение готово к выпуску, если reviewer может открыть одну страницу и увидеть старую и новую формы, список потребителей, результат классификации, матрицу частичного rollout, contract-тесты и процедуру возврата. Для breaking-изменения дополнительно указан владелец старого пути, сигнал его использования и условие удаления. Если хотя бы один потребитель неизвестен, не переходите к contract-фазе: оставьте совместимое расширение или выпустите отдельную версию.</p>\n<h2>Проверяемые источники</h2><ul><li><a href='https://spec.openapis.org/oas/v3.1.1.html' target='_blank' rel='noopener noreferrer'>OpenAPI Specification 3.1.1</a> — официальная спецификация описания HTTP-интерфейсов. Граница: описание не доказывает фактическое поведение сервера.</li><li><a href='https://json-schema.org/draft/2020-12/json-schema-core.html' target='_blank' rel='noopener noreferrer'>JSON Schema Core 2020-12</a> — официальный документ о языке схем JSON. Граница: схема не знает бизнес-состояние и права доступа.</li><li><a href='https://www.rfc-editor.org/rfc/rfc9110.html' target='_blank' rel='noopener noreferrer'>RFC 9110 — HTTP Semantics</a> — стандарт IETF о методах, статусах и семантике HTTP. Граница: стандарт не описывает внутреннее хранилище конкретного сервиса.</li></ul>"
}