Жизнь приложения, написанного за выходные
Каждый пост в этом блоге препарирует один орган платформы — карту, базу данных, морозилку. А этот наблюдает за всем организмом целиком, вживую. Это правдивая история: одно приложение, собранное на платформе в пятничный вечер, с настоящими промптами, настоящими временными метками и настоящим счётом. Ничто в этом посте не является макетом — включая ту часть, где у сборки заканчивается бюджет, и ту, где первый баг находит человек.
Пятница, 18:42 — абзац вместо спецификации
Всё техзадание, ровно как было набрано:
Мы с 7 друзьями едем кататься на лыжах в феврале, и я устал от таблицы. Хочу приложение, где любой из группы может добавить расход, который он оплатил (ски-пассы, продукты, бензин, ужины), отметить, на кого он распространяется, и в любой момент увидеть сводку «кто кому должен», чтобы в конце мы могли рассчитаться с минимальным числом переводов. Всё в EUR. Должно хорошо работать на телефонах, потому что добавлять записи будем прямо на склонах.
Ни схемы, ни страниц, ни технологического стека. В тексте прячутся три требования, и ни одно из них не сформулировано как требование: mobile-first («на склонах»), общие данные («любой из группы») и настоящий алгоритм — рассчитаться с минимальным числом переводов — это задача минимизации денежных потоков, а не CRUD-эндпоинт.
18:46 — четыре минуты спустя возвращается план от архитектора. Шесть фич, одиннадцать подзадач:
| # | Фича |
|---|---|
| 0 | Каркас приложения и навигация — mobile-first, нижние вкладки |
| 1 | Поездка и участники — 8 друзей, настраиваемые имена |
| 2 | Добавить расход — кто оплатил, за что, сумма, на кого распространяется |
| 3 | Лента расходов и балансы |
| 4 | Расчёт — минимум переводов, чтобы обнулить все балансы |
| 5 | Доводка и проверка — засеять демо-данные, протестировать математику от и до |
Все три неявных требования вошли в план, плюс одно, о котором никто не просил: протестировать математику от и до. План — это контракт, а не вайб: каждая подзадача объявляет файлы, которые создаст, и критерии приёмки, по которым её будут судить. Судить объективно: компиляторами и пробами, а не моделью, которая оценивает сама себя.
18:52 — план утверждён одним словом («да»).
18:52:15 — инфраструктура появляется раньше первой строчки кода
Через шесть секунд после утверждения, ещё до того как агент что-либо написал, у проекта есть: собственный namespace в Kubernetes с квотами, собственная ветка PostgreSQL (копирование при записи, мгновенная, с масштабированием до нуля), скаффолд React + tRPC и схема базы данных из плана. Provisioning — это детерминированный код платформы, а не работа агента: агент здесь — наименее доверенный компонент, и ему выдают меблированную комнату, а не ящик с инструментами рядом с дата-центром.
18:52 → 21:11 — сборка, как всё было на самом деле
Вот что агент на самом деле делает со своим бюджетом, согласно журналу инструментов этого запуска:
| Что | Доля шагов |
|---|---|
| Написание кода | ~24% |
| Проверка — проверки типов, живые контрактные пробы, прогоны тестов внутри пода, headless-браузинг | ~27% |
| Чтение контекста | ~29% |
| Ведение плана (которое заодно и запускает проверку) | ~15% |
| Прямые SQL-проверки против собственной базы данных | ~4% |
На каждые два шага написания приходилось два шага проверки. Это соотношение — не добродетель агента, а позиция платформы, навязанная инструментами: подзадача считается выполненной только тогда, когда так говорит пайплайн верификации. Три момента из журнала показывают, что это значит на практике:
Обрезанный файл перехватили на проверке. На шаге наполнения данными агент однажды выдал db/seed.ts длиной 176 символов — сбой модели, который «успешно прошёл» бы как запись файла. Контроль на выдаче файла отбраковал его как вырожденный; повтор записал настоящий файл. Работа платформы — следить за тем, чтобы плохой день модели не скомпилировался в продукт.
Подзадача честно провалилась. Страница управления участниками сожгла три шага, не записав ни одного из объявленных файлов, и scope-gate отказался её засчитывать: статус failing, доказательство — «completed 3 steps without writing any of its declared files». Никакого ложного зелёного. На следующем проходе анти-спиральный толчок выбил агента из диагностической петли на шаге 11 — и страница приземлилась: пять файлов, тесты зелёные.
Математику проверили в SQL. В плане было сказано протестировать расчёт от и до, поэтому агент засеял восемь участников и дюжину расходов, а затем прогнал запросы WITH balances AS (...) напрямую против своей ветки, чтобы убедиться, что план переводов обнуляет каждый баланс — после чего пропатчил собственный набор тестов там, где они расходились. Приложение поставляется вместе с этим набором тестов; оно родилось тестируемым.
Ещё один честный момент: на полпути по плану первый запуск упёрся в бюджет по шагам. Бюджет — это предохранитель, а не обещание, что каждый план уместится в один проход — поэтому запуск отложил пять проверенных подзадач, закоммитил их и встал на паузу в 19:53, прямо об этом сообщив. Затем он ждал — не вычисляя, не выставляя счёт, не притворяясь, что работает — 35 минут, пока в 20:28 не забрёл человек и не набрал «continue». Второй запуск подхватил цепочку там, где журнал зафиксировал остановку, пропустил всё уже проверенное и доделал оставшиеся шесть. (Он также повторил страницу, которая провалилась — там и случилось анти-спиральное спасение, описанное выше.)
21:11 — готово. Одиннадцать из одиннадцати подзадач прошли. Часы на стене показывают 2 ч 19 мин от утверждения до готовности, но полчаса из этого платформа просто ждала, замороженная посреди плана, пока человек не вернётся с ужина. Фактическое рабочее время агента — около 1 ч 45 мин. Релизный коммит привязан к точной позиции в логе базы данных (bdc2258 @ LSN 0/21933A8), что звучит как мелочь, пока вам не понадобится, чтобы код и данные путешествовали во времени вместе.
Вот это приложение, в продакшене, сфотографированное тем же headless-браузером Chromium, который платформа использует для собственных runtime-проверок:
Экран расчёта — это та самая недоописанная пятничная фраза, доведённая до продукта: «кто кому должен… с минимальным числом переводов» превратилось в восемь балансов и план из 7 переводов — минимум для восьми человек, поскольку любой граф расчётов требует не более n−1 рёбер. А если присмотреться к отрицательным суммам, то прямо в кадре уже видна следующая итерация: удвоенный знак минус. Баг-репорт во вторник будет одним предложением, как и прошлый.
21:28 — первый баг находит человек
Тыкая в превью: два странных участника с именем «x», у которых есть балансы, и нет никакой возможности их удалить. Сообщено так, как человек реально сообщает о таком:
I see some strange users x which have a non zero balance but I don't see any expenses associated to them. And I can't delete them.
Диагноз — это история про автоматический QA: во время runtime-верификации headless-браузер агента вводил данные в настоящую форму добавления участника, и тестовые участники там застряли. Их удаление вскрыло реальный пробел в дизайне — участники, на которых ссылаются разбивки расходов, были защищены внешним ключом, а у интерфейса не было ответа.
Что вернулось девять минут спустя, было лучше, чем просто разблокировка удаления: дизайн с перераспределением — при удалении участника его доли распределяются между оставшимися — плюс регрессионный тест ровно на тот случай, который существующий набор не покрывал (это собственное наблюдение агента, из его рассказа о ходе работы). Двадцать один тест зелёный, закоммичено.
Это и есть тот цикл итераций, вокруг которого построена платформа: баг приходит как предложение, фикс приходит как проверенный коммит, а человек на склонах так и не увидел ни одного стек-трейса.
23:00 — во сколько обходится готовое приложение выходного дня
Счёт за вечер, из таблиц учёта:
- Сборка: ~4,8 млн токенов через кодовую модель за два запуска и итерацию с фиксом — примерно $3,50 чистой стоимости модели за фулстек-приложение с тестами и засеянными данными.
- Простой: через 148 секунд после последнего keepalive платформа заморозила контейнер приложения на уровне cgroup — это самое приложение, из этой самой истории, в свой первый тихий вечер освободило 201 МБ RAM в своп. Его compute базы данных масштабировался до нуля отдельно. Простаивающая идея выходного дня по умолчанию стоит фактически ноль, сколь угодно долго.
Таблица умерла в 18:42. К ужину было приложение с проверенной математикой, собственной веткой базы данных, набором тестов и нулевой стоимостью простоя — и никому из участников поездки не нужно знать, что такое LSN.
Чего превью пока не обещает
Всё вышеописанное — это пока ещё превью одного пользователя, а превью почти ничего не обещает. Интересная часть жизни этого приложения начинается, когда URL получат семь других людей: продакшен-деплой, который форкает таймлайн данных, реальные расходы реальных друзей, которые следующий деплой не должен затронуть, рискованная миграция и — когда что-то пойдёт не так — откат с явными правилами о том, что ему позволено удалять.
Это вторая часть истории этого приложения. Механизмы уже описаны в Акте 2 — просто это приложение их ещё не прожило. Проживёт к воскресенью.