← назад к разделу

Понятия записаны, словарь согласован — теперь их можно превратить в главный рабочий артефакт продукт-инженера: документ, по которому агент строит, а вы принимаете результат.

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

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

diagram
Обязательно

Спека — это модель понятий с обязательствами

Посмотрите, из чего состоит хорошая спецификация функциональности:

  • Сущности и их атрибуты — понятия модели: заказ, позиция, платёж.
  • Состояния и переходы — жизненный цикл: черновик → ожидает оплаты → оплачен → отправлен, «из „отменён“ переходов нет».
  • Команды — что можно сделать: «оформить заказ», «отменить», «оплатить» — с предусловиями («оплатить можно только заказ в статусе „ожидает оплаты“»).
  • События — что происходит в результате: «заказ оформлен», «платёж получен» — то, на что реагируют другие части системы.
  • Инварианты — правила, которые не нарушаются никогда: «сумма позиций равна сумме заказа», «у заказа ровно один клиент».
  • Критерии приёмки — проверяемые сценарии: дано → когда → тогда.

Первые пять пунктов — ровно элементы модели домена (объекты, состояния, события, связи, правила), только с обязательствами: спека не просто описывает понятия, она фиксирует, что система про них гарантирует. Поэтому попытка «написать спеку» без модели понятий и проваливается — нечего разворачивать. Получается пересказ пожеланий, где агенту всё равно приходится угадывать, что такое «заказ» и можно ли оплатить отменённый.

Порядок работы такой: понятия и связи → состояния (запрещённые переходы важнее разрешённых) → команды и события в едином словаре (CancelOrder и OrderCancelled, а не синонимы по настроению) → инварианты → критерии приёмки. На выходе — документ на несколько страниц, где нет ни слова про фреймворки и таблицы: только домен. Техника добавляется отдельным разделом и не смешивается с моделью — мы уже видели, что бывает при смешивании.

Контракт среза: рабочая единица

Спека описывает область целиком, а работа идёт срезами — кусками продукта, каждый из которых решает настоящую проблему пользователя и проверяет гипотезу минимальными силами (про то, как резать до такого среза, — в приоритизации наименьшего среза).

Один срез разворачивается в короткий контракт. Это не техническое задание на сто страниц, а четыре вещи, зафиксированные словами настолько точно, чтобы по ним можно было и собрать, и потом проверить.

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

2. Сценарий словами пользователя. Один случай использования, описанный так, как его проживает человек, а не как исполняет система: «покупатель нажимает „оплатить“, видит подтверждение, деньги списываются один раз». Не «контроллер принимает POST и вызывает сервис». Если сценарий не формулируется одной внятной фразой — срез ещё не дорезан.

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

4. Критерии приёмки — проверяемые. Не «работает корректно», а перечень утверждений, каждое из которых проверяется «да/нет». «При повторе с тем же ключом второго списания нет». «При недоступном шлюзе заказ остаётся неоплаченным, пользователь видит понятную ошибку». Если критерий нельзя проверить — это не критерий, а пожелание, и агент его тихо проигнорирует.

Как это выглядит

Контракт короткий и вне языка: ремесло агент знает сам, ваше дело — задать рамку.

UseCase: Оплатить заказ

Границы:
  входит    — оплата картой одного заказа, разовое списание
  не входит — сохранённые карты, рассрочка, частичная оплата, возврат

Сценарий:
  Покупатель подтверждает оплату заказа в статусе «ожидает оплаты»
  и получает подтверждение; деньги списываются ровно один раз.

Интерфейс:
  вход   — идентификатор заказа, ключ идемпотентности
  выход  — статус заказа, ссылка на платёж
  ошибки — заказ не найден; заказ уже оплачен; шлюз недоступен
  повтор — тот же ключ возвращает первый результат, второго списания нет

Критерии приёмки:
  1. Успешная оплата переводит заказ в «оплачен» и возвращает ссылку.
  2. Повтор с тем же ключом не списывает второй раз.
  3. Недоступный шлюз оставляет заказ «ожидает оплаты», ошибка понятна.
  4. Оплата уже оплаченного заказа отклоняется и не списывает.

Двадцать строк. Но по ним агент собирает предсказуемо: границы не дают расползтись, сценарий держит смысл, интерфейс фиксирует стык, критерии дают чем закончить.

А вот как выглядит та же задача в двух фразах

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

Сделай оплату заказа картой. Чтобы деньги списывались один раз.

Формально здесь есть и что делать, и главное требование. Что агент достроит на месте недосказанного — предсказуемо, и почти каждый пункт окажется решением, которого вы не принимали:

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

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

Самое дорогое в списке — предпоследняя и последняя строки. Свои коды ошибок всплывут, когда этот срез начнут использовать снаружи; своя таблица с номерами карт — когда до неё дойдёт проверка защищённости. И то и другое исправляется дороже, чем стоило написать двадцать строк контракта.

Проверочное упражнение на своей задаче: напишите её в двух фразах, а потом выпишите, что именно в ней не сказано. Если список получился длиннее пяти пунктов — контракт нужен.

Идемпотентность: откуда ключ и что возвращать

В контракте выше идемпотентность занимает одну строку, и это правильно для контракта. Но за этой строкой стоят четыре решения, и если их не принять, агент примет их сам — с последствиями из таблицы выше.

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

2. Ключ живёт ограниченное время. Хранить его вечно не нужно и дорого: смысл он имеет, пока клиент может повторить запрос. Ходовой срок — от суток до трёх. Это решение записывают явно, потому что от него зависит и объём хранилища, и поведение после истечения: запрос с истёкшим ключом — это уже новый запрос.

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

Отсюда же разбирается случай «тот же ключ, другое тело». Это не повтор, а ошибка на стороне клиента: либо переиспользовали ключ, либо перепутали запросы. Правильная реакция — отказать явно, а не выполнить и не вернуть старый ответ. Общепринятая форма этого ответа и коды разобраны в статье про заголовки REST API, где та же механика показана со стороны интерфейса.

4. Два одинаковых запроса одновременно. Случай, который забывают почти всегда, а происходит он регулярно: пользователь нажал кнопку дважды, или клиент повторил запрос, не дождавшись ответа, и первый ещё в работе. Тогда наивная проверка «поискали ключ — не нашли — обрабатываем» выполняется дважды, потому что оба запроса не нашли ключа.

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

  • Отказать признаком «запрос уже в работе» и предложить повторить позже. Проще и честнее.
  • Дождаться первого и вернуть его результат. Приятнее для клиента, но держит соединение и требует ограничения по времени ожидания.

Как это выглядит в контракте — три строки вместо одной:

повтор — тот же ключ возвращает первый результат (тот же статус и тело)
       — тот же ключ с другим телом отклоняется как ошибка клиента
       — тот же ключ, пока первый запрос в работе, отклоняется признаком «в работе»
       — ключ живёт 24 часа, после этого запрос считается новым

И отдельное правило, которое стоит знать про деньги: на списаниях идемпотентность делают в два слоя. Ключ от клиента защищает от повтора запроса, а ограничение на стороне данных (одно списание на заказ) защищает от всего остального: ошибки в коде, двух копий сервиса, повтора из очереди. Один слой на деньгах — это вопрос времени, а не надёжности.

Почему это работает

Три причины, и все три — про то, чего размытая задача не даёт.

Контракт проверяем. Инварианты и критерии приёмки — это готовые тесты, и приёмка результата превращается из «вроде похоже» в прогон сценариев. Написан контракт один раз, а работает дважды: как задание на входе и как основание приёмки на выходе.

Он устраняет угадывание. Агент не решает сам, можно ли оплатить отменённый заказ, — это записано. Чем меньше свободы в смысле, тем больше пользы от скорости агента: свобода остаётся там, где она уместна, — в реализации.

Он переживает сессии. Контекст сессии исчезает, документ остаётся. Следующая сессия — или другой агент, или вы через месяц — стартует от того же текста, и три сессии дают одно решение, а не три.

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

Дальше эта дисциплина масштабируется: контракт на срез — рабочая единица, набор контрактов — модель сервиса. По этому принципу устроена методология Use Case Pattern, на которой стоит весь этот сайт: спецификация живёт рядом с кодом, исполняется агентом и проверяется на каждом изменении.

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

Где контракт лежит и как попадает к агенту

Последний шаг, без которого всё предыдущее остаётся упражнением: контракт должен физически где-то лежать, и агент должен его читать.

Правильное место — файл в репозитории рядом с кодом. Не сообщение в чате, не описание задачи в трекере, не документ в вики. Причины ровно те, по которым контракт вообще заводят:

  • Он версионируется вместе с кодом. Правка поведения и правка контракта едут одним изменением, и на ревью видно обе. В трекере и в вики этой связи нет.
  • Он попадает в контекст автоматически. Агент читает репозиторий; в вики он не ходит, а описание задачи ему надо пересказывать руками каждый раз.
  • Его видно из будущего. Через полгода вопрос «почему повтор возвращает старый ответ» решается чтением файла рядом с кодом, а не поиском по переписке.

Раскладка, которая работает:

docs/spec/
  orders-spec.md              — модель контекста: понятия, состояния, инварианты
  usecases/
    pay-order.md              — контракт среза
    cancel-order.md
  changes/
    2026-09-25-partial-pay.md — изменение контракта, пока оно делается

Одна вещь здесь важнее раскладки: в файле правил проекта стоит указатель на этот каталог. Строка вида «источник правды по предметной области — docs/spec/» попадает в контекст каждой сессии, и агент идёт читать спецификацию сам, вместо того чтобы угадывать. Без этого указателя файлы лежат мёртвым грузом: агент не обязан догадываться, что они существуют. Про сам файл правил и про то, чем он отличается от памяти проекта, — в статье про настройку агента.

Как контракт попадает в работу. Порядок на каждой задаче:

  1. Контракт написан и лежит в файле — это отдельный коммит до начала работы над кодом.
  2. В начале сессии агенту явно называют путь: «работаем по docs/spec/usecases/pay-order.md». Указатель в правилах даёт ему возможность найти, прямое указание — уверенность, что он читает нужное.
  3. Критерии приёмки из контракта становятся именами тестов. Это связывает документ с проверкой: тест с именем «повтор с тем же ключом не списывает второй раз» находится по критерию и наоборот.
  4. Приёмка идёт по тому же файлу, а не по памяти о разговоре.

Чего делать не стоит. Держать контракт только в описании задачи в трекере: он умрёт вместе с задачей, а поведение останется. Складывать все контракты в один растущий документ: он перестанет влезать в контекст, и агент будет читать его целиком ради одного среза. И дублировать контракт в трекер и в репозиторий: две копии разойдутся, и никто не будет знать, какая главная. В трекере остаётся ссылка.

Когда приёмка провалилась

Критерий не выполнен. Статья доводит до того, что критерии дают чем закончить, и обрывается — а ведь дальше начинается самое интересное. Провал приёмки бывает четырёх видов, и лечатся они по-разному. Первое, что надо сделать, — понять, какой у вас.

Вид 1: критерий не выполнен, но понятно почему. Обычный случай: забыли ветку, не обработали ошибку, перепутали статус. Действие: вернуть агенту с тем же контекстом и точной формулировкой провала. Не «не работает», а «критерий 3: при недоступном шлюзе заказ переходит в „отменён“, а по контракту должен остаться в „ожидает оплаты“». Лучше всего — падающим тестом: он и формулировка провала, и критерий готовности одновременно.

Вид 2: критерий не выполнен, и попытки не помогают. Две правки подряд не привели к выполнению, и агент ходит по кругу. Это уже не приёмка, а тупик, и лечится он как тупик: откатить, сузить, начать с чистым контекстом или сделать руками. Продолжать приёмку в этом состоянии бессмысленно — контекст забит неудачными попытками.

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

Вид 4: критерий верный, но срез слишком крупный. Признак: критериев несколько, часть выполнена, часть нет, и невыполненные держатся на чём-то, чего в срезе не предполагалось (внешняя система ведёт себя иначе, нужна миграция данных, требуется решение, которого никто не принимал). Действие: дорезать срез по границе выполненного. То, что готово и имеет смысл само по себе, принимается и сливается; остальное становится следующим срезом с собственным контрактом. Это не поражение, а нормальный результат: граница проведена точнее, чем при планировании.

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

Дополнительно: при первом чтении можно пропустить

Глубже: спека и код по мере изменений: что делать при расхождениирасширенное

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

Изменение начинается со спеки. Любая правка поведения это сначала правка контракта среза (или отдельный документ изменения, если контекст большой), потом код. Это не бюрократия, а тот же принцип, что «тест до кода»: агент получает обновлённый контракт и делает по нему, а вы принимаете по нему же. Правка в две строки тоже проходит через спеку, потому что именно двухстрочные правки её и убивают.

Расхождение это ошибка, а не долг. Нашли на ревью или в приёмке, что код делает не то, что записано: чинят в том же pull request либо код, либо спеку, с записью, почему выбрали второе. «Доработаем спеку потом» запрещено тем же правилом, что и красный main: через неделю никто не вспомнит, что именно расходилось.

Чем это ловится машиной. Часть соответствия проверяется автоматически, и это стоит завести первым: список операций в спеке против списка обработчиков в коде, поля контракта API против схемы OpenAPI, состояния из спеки против перечисления в коде, критерии приёмки против имён тестов. Проверка падает в сборке при расхождении, и это те гейты, о которых говорит раздел про Use Case Pattern. Что машиной не проверить (смысл правила), проверяет ревью по чек-листу «спека, код, тест на каждое правило».

Периодическая сверка. Раз в несколько недель агенту дают задачу сравнить спеку с кодом и перечислить расхождения; он находит их лучше человека, потому что читает оба целиком и без усталости. Найденное это задачи, а не заметки.

Коротко

  • Размытая задача даёт правдоподобное «не то», и чем мощнее агент, тем лучше оно замаскировано.
  • Спека = модель понятий с обязательствами: сущности, состояния, команды, события, инварианты + критерии приёмки. Порядок: понятия → состояния (запрещённое важнее разрешённого) → команды и события в едином словаре → инварианты → критерии приёмки. Домен отдельно, техника отдельно.
  • Рабочая единица — контракт на один срез: границы (в том числе «не входит»), сценарий словами пользователя, интерфейс (вход, выход, ошибки, повтор), проверяемые критерии. Из четырёх частей первой пишут границы: незаписанное «не входит» агент считает разрешённым.
  • Контракт проверяем (критерии = тесты), устраняет угадывание (инварианты не интерпретируются) и переживает сессии. Написан один раз — работает дважды: как задание на входе и как основание приёмки на выходе.
  • Изменение начинается со спеки, расхождение чинят в том же pull request, соответствие ловят гейтами в сборке (операции, поля, состояния, критерии против тестов) и периодической сверкой агентом.
  • Та же задача в двух фразах заставляет агента принять за вас решения: границы, откуда ключ повтора, что возвращать на повтор, поведение при недоступном шлюзе, форму ошибок; если список недосказанного длиннее пяти пунктов — контракт нужен.
  • Идемпотентность это четыре решения: ключ генерирует клиент, срок жизни ограничен сутками-тремя, повтор получает сохранённый ответ, а тот же ключ с другим телом и одновременный повтор обрабатываются отдельно; на деньгах защита в два слоя.
  • Контракт лежит файлом в репозитории рядом с кодом, а указатель на каталог спецификаций стоит в файле правил проекта — иначе агент не обязан догадываться, что он есть.
  • Провал приёмки бывает четырёх видов: вернуть с точной формулировкой (лучше падающим тестом), признать тупик, переписать критерий с записью причины или дорезать срез по границе выполненного; «в целом работает, остальное потом» — не вариант.
  • Проверка на подгонку критерия: согласился бы я на эту формулировку до начала работы?

Что почитать дальше