Files
progcode/editorial/agent-rewrites/072.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
18 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": 72,
"slug": "editorial-2026-01-practice-platform-api",
"title": "Платформенный API без скрытых обещаний: как проверить контракт до hand-off",
"excerpt": "Потребитель просит особый флаг или поле, а команда рискует превратить случайное поведение в обязательство. Разбираем контрактную поверхность, узкий escape hatch и проверяемый stop для несовместимых случаев.",
"contentHtml": "<p>Проблема часто начинается с безобидной просьбы: потребителю нужен ещё один флаг, сырой фрагмент ответа или особый порядок элементов. Платформенная команда добавляет параметр и закрывает задачу. Через месяц другой клиент начинает зависеть от этого поведения. Затем команда меняет внутренний формат, а клиент ломается на поле, которое никто не называл публичным.</p>\n<p>Цена ошибки — не только откат релиза. Клиент может принять неверное решение, сохранить неправильное состояние или повторить операцию. Владельцы API тратят время на спор: это баг, новая гарантия или локальный обход? Номер версии и проходящий schema-check не отвечают на этот вопрос.</p>\n<p>Тезис простой: платформенный API нужно проверять как договор между конкретным контрактом и конкретным потребителем. Сначала назовите операцию, вход, ответ, ошибки и гарантии. Потом сравните их с решением потребителя. Если требование выходит за поверхность, оформите узкое documented escape hatch или остановите hand-off с причиной.</p>\n<h2>Что считается контрактом</h2>\n<p>Контракт описывает не все детали реализации. Он описывает то, на чём потребитель вправе строить решение. Для API чтения это обычно операция, версия, обязательные поля запроса, обязательные поля ответа, допустимые ошибки и смысл значений. Дополнительное поле не становится гарантией только потому, что его видно в JSON.</p>\n<p>Отдельно фиксируйте поведение при расширении. Один reader игнорирует неизвестные поля. Другой закрыто десериализует объект. Третий строит хэш полного ответа. Для них одно и то же добавление имеет разный риск. Проверять нужно reader, а не только схему.</p>\n<p>То же относится к порядку. Если клиент берёт первый элемент массива, порядок стал частью его фактического ожидания. Но это ещё не значит, что API его обещал. Пока команда не записала такую гарантию и не проверила её, результатом должен быть stop, а не новая версия с более уверенным названием.</p>\n<figure><img src=\"/assets/editorial/2026/platform-api-2026-contract-surface.svg\" alt=\"Контрактная поверхность платформенного API: операция, запрос, ответ, ошибки и гарантии\" loading=\"lazy\" /><figcaption>Потребитель должен проходить через названную поверхность API. Скрытый параметр и неописанная гарантия не расширяют договор автоматически.</figcaption></figure>\n<h2>Сначала решение потребителя, потом форма ответа</h2>\n<p>Потребитель редко просит поле ради самого поля. Он хочет выбрать ветку: показать статус, повторить запрос, отобразить объяснение или передать запись дальше. Запишите это решение первым. Затем спросите, какой минимальный факт ему нужен.</p>\n<p>Например, fixed reader читает только <code>id</code> и <code>state</code>. Ему не нужен весь внутренний объект. Другой reader требует <code>legacyMode</code>. Это не повод молча добавить поле в общий договор. Сначала проверьте семью контракта, версию, обязательность поля и условие миграции.</p>\n<p>Такой порядок сдерживает две крайности. Команда не превращает ответ в бесконечный объект «на будущее». И она не отказывает потребителю общей фразой «так нельзя». Для каждого требования появляется конкретный путь: публичное расширение, отдельное исключение или stop с недостающим фактом.</p>\n<h2>Учебный пример проверки</h2>\n<p>Ниже — учебная модель. Она не отправляет HTTP-запросы, не читает production-трафик и не доказывает совместимость настоящего сервиса. Код показывает только порядок проверки и названия отрицательных результатов.</p>\n<pre><code>type Contract = {\n family: 'catalog-read';\n version: '1.3.0';\n requiredResponse: Array&lt;'id' | 'state'&gt;;\n errors: Array&lt;'not_found' | 'invalid_request'&gt;;\n guarantees: Array&lt;'order_is_irrelevant'&gt;;\n};\n\ntype Consumer = {\n family: string;\n requiredFields: string[];\n needsStableOrder: boolean;\n};\n\nfunction review(contract: Contract, consumer: Consumer) {\n if (consumer.family !== contract.family) {\n return 'stop-incomparable-family';\n }\n\n const missing = consumer.requiredFields.filter(\n (field) =&gt; !contract.requiredResponse.includes(field as 'id' | 'state'),\n );\n if (missing.length &gt; 0) return 'stop-missing-contract-field';\n if (consumer.needsStableOrder &amp;&amp; !contract.guarantees.includes('order_is_stable')) {\n return 'stop-undeclared-guarantee';\n }\n return 'bounded-review-hand-off';\n}</code></pre>\n<p>Положительный результат здесь узкий. Он означает, что учебный consumer относится к той же семье, его обязательные поля описаны, а требуемые гарантии не выходят за контракт. Он не означает, что реальный клиент работает, что API выдержит нагрузку или что выпуск безопасен.</p>\n<p>Отрицательный путь важнее. Для command API возвращается <code>stop-incomparable-family</code>. Для требования <code>legacyMode</code> — <code>stop-missing-contract-field</code>. Для непроверенного порядка — <code>stop-undeclared-guarantee</code>. Сохраняйте причину. Слово «совместимо» без причины не помогает следующему инженеру продолжить проверку.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class=\"table-scroll\"><table><caption>Типовые утечки платформенного API</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>Наблюдение приняли за гарантию</td><td>Сравнить reader с declared response fields</td><td>Остановить hand-off или объявить поле отдельным изменением</td></tr><tr><td>Добавление поля ломает старый reader</td><td>Клиент строго разбирает объект или хэширует ответ</td><td>Проверить декодер на unknown fields, отсутствие и null</td><td>Сохранить совместимую форму либо подготовить migration</td></tr><tr><td>Потребитель зависит от первого элемента</td><td>Порядок не назван, но стал скрытой гарантией</td><td>Найти сортировку и проверку порядка в коде consumer</td><td>Добавить явную гарантию и тест или убрать зависимость</td></tr><tr><td>Особый query-флаг нужен одному клиенту</td><td>Escape hatch не имеет владельца и границы</td><td>Проверить имя, версию, вход, ответ и negative boundary</td><td>Оформить узкий hatch или удалить скрытый обход</td></tr><tr><td>Read API сравнивают с command API</td><td>Не названа contract family</td><td>Сопоставить операцию, вход и побочный эффект</td><td>Вернуть incomparable и завести отдельный review</td></tr></tbody></table></div>\n<h2>Escape hatch — отдельный договор</h2>\n<p>Escape hatch полезен, когда общий контракт честно не покрывает ограниченную задачу. Он должен иметь имя, версию, разрешённый вход, форму результата и отрицательную границу. Например, <code>raw-envelope-v1</code> может дать одному названному consumer одну дополнительную representation. Это не обещает порядок, задержку, хранение, доступность или сохранение будущих полей.</p>\n<p>Скрытый <code>debug-wire</code> устроен иначе. У него нет понятного получателя и предела. Один клиент прочитает внутреннее поле, второй скопирует его в свою схему, третий начнёт рассчитывать на случайный порядок. Название debug не ограничивает зависимость. Ограничивает её только записанный договор и проверяемая граница.</p>\n<p>Не расширяйте общий response «на всякий случай». Если потребителю нужен raw envelope, это отдельная поверхность с отдельным риском. Если потребитель не может назвать решение, которое он принимает с помощью особого поля, сначала уточните задачу. Без этого команда не знает, что именно должна поддерживать.</p>\n<h2>Порядок действий</h2>\n<ol><li><strong>Зафиксируйте симптом.</strong> Запишите вход, ответ, ветку consumer и цену неверного решения до изменения кода.</li><li><strong>Назовите участников.</strong> Укажите владельца API, конкретный consumer, contract family и версию. Слово «клиенты» недостаточно.</li><li><strong>Опишите поверхность.</strong> Выпишите required fields, допустимые значения, ошибки, порядок и гарантии. Отделите наблюдение от обещания.</li><li><strong>Найдите фактическую зависимость.</strong> Проверьте decoder, fallback, сравнение полного объекта, чтение первого элемента и особые параметры.</li><li><strong>Сравните изменение.</strong> Проверьте удаление, переименование, новый enum, неизвестные поля, отсутствие и <code>null</code>. Для разных family остановите сравнение.</li><li><strong>Выберите форму.</strong> Расширьте public contract с правилами совместимости, оформите versioned escape hatch или верните stop с причиной.</li><li><strong>Передайте ограниченный результат.</strong> Приложите diff, профиль consumer, тест положительного пути и тест каждого ожидаемого stop. Не выдавайте учебную проверку за rollout.</li></ol>\n<h2>Ограничения</h2>\n<p>Метод не обнаруживает потребителя, которого нет в inventory. Если API доступен за пределами известной команды, список зависимостей может быть неполным. В таком случае неописанное поведение безопаснее считать риском до отдельной проверки.</p>\n<p>Метод не заменяет нагрузочное тестирование, проверку доступа, анализ данных, SLA и план отката. OpenAPI описывает форму интерфейса, но не подтверждает, что реализация ей соответствует. SemVer помогает назвать изменение после определения public API, но номер версии сам не создаёт гарантию. HTTP-стандарт задаёт общие семантики, но не решает прикладной вопрос совместимости конкретного reader.</p>\n<p>Положительный учебный пример также не даёт production-результата. Его граница — порядок мышления: назвать контракт, назвать потребителя, проверить нужный факт и остановиться там, где факта нет.</p>\n<h2>Критерий готовности</h2>\n<p>Проверка готова, когда другой инженер без устного пояснения находит в одном месте contract family, версию, required fields, допустимые ошибки, явные гарантии, профиль каждого проверенного consumer, diff изменения и автоматические проверки положительного и отрицательного путей.</p>\n<p>Тест должен падать, если удалили обязательное поле, добавили неподдерживаемое значение, нарушили объявленный порядок или вернули скрытый hatch без имени и границы. Если хотя бы одного элемента нет, результат — не «совместимо», а конкретный stop и имя следующего доказательства.</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 API. Спецификация описывает поверхность интерфейса, но не выбирает поведение reader при неизвестном поле.</li><li><a href=\"https://semver.org/spec/v2.0.0.html\" target=\"_blank\" rel=\"noopener noreferrer\">Semantic Versioning 2.0.0</a> — официальные правила версий для объявленного public API. Номер версии не заменяет inventory потребителей и проверку гарантий.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc9110.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9110: HTTP Semantics</a> — официальный стандарт общих семантик HTTP, включая request, response, status и representation. Он не задаёт прикладной escape hatch и не выносит verdict о совместимости конкретного клиента.</li></ul>"
}