almdev Технический блог

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

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

Подкатегория: Лаборатория

Чтение
5 мин
Технологии / версии
Symfony · Coding agent · PHP · Лаборатория
Серия
Лаборатория · часть 3 из 9
25 июн 2026 · 5 мин · AI
Вокруг небольшой функции построен чрезмерно сложный механизм
Лаборатория 06/2026

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

Третий заход. Тот же проект: Symfony 8, 96 тысяч строк, модули Core, Tasks, Docs, 784 теста.

Задача

Подписка на задачу. Участник может подписаться на задачу и получать уведомления об изменениях, не будучи исполнителем. Отписка, список подписчиков на карточке, автоматическая подписка автора комментария.

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

Заход без границ

Формулировка: «добавь подписку на задачи с уведомлениями».

Через двадцать две минуты я получил дифф на 640 строк в 19 файлах. Что в нём было.

Сущность подписки — правильно. Сервис подписки и отписки — правильно. Кнопка на карточке — правильно.

Дальше начинается интересное. Абстрактный интерфейс канала уведомлений с тремя реализациями: почта, внутреннее уведомление, веб-хук. Веб-хуков в проекте нет вообще, ни одного. Настройки частоты уведомлений на пользователя: сразу, дайджест раз в час, дайджест раз в день — со своей таблицей и командой отправки дайджестов. Шаблон письма с настраиваемым текстом на трёх языках.

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

И главное: механизм уведомлений в проекте уже есть. NotificationService в модуле Core, с очередью, шаблонами и настройками. Агент его не нашёл, потому что искал по слову «subscription», а не «notification», и построил второй.

Контракт задачи

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

ЦЕЛЬ
Участник может подписаться на задачу и получать существующие уведомления
об изменениях этой задачи, не будучи исполнителем.

В ОБЪЁМЕ
- Сущность IssueWatcher: задача, участник, дата подписки
- Подписка и отписка через SubscriptionService
- Кнопка на карточке задачи (Turbo Frame, без перезагрузки)
- Автор комментария подписывается автоматически, если ещё не подписан
- Подписчики попадают в получателей существующих уведомлений

ВНЕ ОБЪЁМА
- Новые каналы доставки. Используем Core\NotificationService как есть.
- Настройки частоты, дайджесты, тихие часы
- Изменение схемы уведомлений и их шаблонов
- Массовая подписка, подписка на проект или на спринт

ОГРАНИЧЕНИЯ
- Модуль Tasks. Зависимость только на Core, обратной быть не должно
- UUID v7 через HasUuidIdentity, метки времени через HasTimestamps
- Права: подписаться может только участник организации задачи
- Миграция аддитивная, без изменения существующих таблиц

ГОТОВО, КОГДА
- Функциональный тест: подписка, отписка, повторная подписка не дублирует
- Функциональный тест: участник чужой организации получает 404
- Уведомление об изменении задачи приходит подписчику, не являющемуся исполнителем
- make check проходит, базовая линия PHPStan не выросла

Результат: 180 строк в 7 файлах, 34 минуты. Ни одного лишнего механизма.

Какой блок работает лучше всех

«Вне объёма». С большим отрывом.

Цель и критерий готовности задают направление и проверку — они нужны, но они и так примерно понятны из формулировки. А вот перечисление того, куда нельзя расширяться, — единственное, что останавливает достройку.

Механика простая. Агент, дойдя до места, где нужно решение, выбирает разумный вариант. Разумных вариантов много, и каждый тянет за собой следующий. «Вне объёма» — единственный сигнал, что расширение не является улучшением.

Строчка про использование существующего сервиса уведомлений сэкономила в этом заходе примерно 400 строк кода.

Второй по полезности блок — ограничения, и конкретно строка про направление зависимости. Без неё в первом заходе Core\NotificationService получил метод, знающий про подписчиков задач, — то есть ядро стало зависеть от модуля.

Ревью плана до кода

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

Прежде чем писать код: верни план.
- какие файлы создаются, какие изменяются
- схема новой таблицы
- где встраиваемся в существующий поток уведомлений (file:line)
- какие тесты пишутся
Код не пиши, жду подтверждения.

План на этой задаче занял 30 строк, прочитал я его за две минуты. И нашёл в нём две вещи.

Первая: подписчики должны были подмешиваться в получателей внутри NotificationService. Это опять зависимость ядра от модуля. Поправил на подписчика события в модуле Tasks.

Вторая: в плане не было уникального ограничения на пару «задача — участник». Повторная подписка создала бы дубликат, и один из критериев готовности провалился бы. Дописал одну строку.

Две правки в плане против правки 180 строк кода и миграции. Соотношение цены ошибки на этих двух этапах отличается на порядок, и весь смысл шага в этом.

Где план не спасает

Честно про границы метода.

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

Проверяется это только чтением диффа. Никакой контракт не отменяет ревью, он лишь делает дифф достаточно маленьким, чтобы ревью было реальным.

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

Замер

ЗаходВремяДиффФайловЛишних подсистем
Без границ22 мин640 строк193
Контракт + ревью плана34 мин + 2 мин на план180 строк70

Второй заход дольше по времени агента. Разница в моём времени несопоставима: дифф на 640 строк с тремя незапрошенными подсистемами я читал бы час и почти наверняка отправил бы переделывать.

По моей оценке, 180 строк — примерно тот объём, который я написал бы руками для этой задачи. Агент здесь не сократил код, а быстрее привёл реализацию к ожидаемому масштабу.

Что дальше

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

Серия

Лаборатория

#symfony #coding-agent #php #lab #postanovka-zadachi