Модуль 2 · Строим · урок 5 из 13
Первый вертикальный срез
Соблазн велик: попросить агента «сделай весь бэкенд», потом «весь фронтенд». Получится много кода, который ни разу не запускался вместе. Вертикальный срез — обратный подход: маленькая функция, но через все слои и сразу работающая. В этом уроке делаем первый срез «Переговорок» и разбираем, что в нём читать глазами.
Что такое вертикальный срез
Срез — это сценарий, который пользователь может пройти от начала до конца. Для нас это пункт 2 плана: «список комнат и бронь слота на сегодня, без входа». В нём есть таблица в базе, миграция, два эндпоинта, экран и тесты. Нет авторизации, недели, отмены — они придут следующими срезами.
Почему так:
- агент получает обратную связь от работающей системы, а не от воображаемой;
- ошибки интеграции (формат дат между API и интерфейсом, CORS, миграции) всплывают в первый день;
- первый срез становится образцом, который агент будет копировать, — его стоит сделать хорошо.
Пока входа нет, автор брони — фиксированный сотрудник из переменной DEV_USER_EMAIL. Пометьте это в коде комментарием TODO(auth), чтобы агент в следующем срезе нашёл все места.
Сначала план
Один чат — одна задача. Начните новую сессию и попросите план, не код:
/plan Пункт 2 из docs/PLAN.md: список комнат и бронь слота на сегодня.
Правила R1, R2, R3 и сценарии S1, S2 из docs/SPEC.md.
Входа пока нет: автор брони — DEV_USER_EMAIL из окружения, пометь TODO(auth).
Составь план: модели Prisma, миграция, эндпоинты с Zod-схемами
в packages/shared, экран, тесты. Укажи, как будешь соблюдать R3
при одновременных запросах.Читайте план внимательнее, чем потом будете читать код: исправить абзац дешевле, чем откатывать двести строк. Последний вопрос промпта — проверка. Если в плане написано «перед созданием проверим, что слот свободен», агент собирается сделать гонку: два запроса одновременно проверят, оба увидят свободный слот, оба запишут.
Задача на реализацию
Когда план устраивает, ставьте задачу по схеме из best practices: цель, контекст, ограничения, «готово, когда».
Реализуй план выше.
Цель: сотрудник видит комнаты и занятость на сегодня, бронирует свободный слот.
Контекст: docs/SPEC.md (R1–R3, S1, S2), AGENTS.md, apps/api/src/routes/health.ts как образец маршрута.
Ограничения: R3 обеспечивает база (exclusion constraint), а не проверка в коде.
Пересечение → HTTP 409, код ошибки BOOKING_OVERLAP. Новых зависимостей нет.
Готово, когда: npm run check зелёный; есть интеграционный тест на S1, S2
и на два параллельных запроса одного слота (ровно один успешен).Что читать в диффе
Весь дифф первого среза читать необязательно — но три места обязательно. Посмотреть изменения можно командой /diff прямо в сессии.
Схема. Первое, что проверяем, — типы времени:
model Room {
id Int @id @default(autoincrement())
name String @unique
capacity Int
hasScreen Boolean @default(false)
archivedAt DateTime? @db.Timestamptz(3)
bookings Booking[]
}
model Booking {
id Int @id @default(autoincrement())
roomId Int
room Room @relation(fields: [roomId], references: [id])
title String
bookedBy String
startsAt DateTime @db.Timestamptz(3)
endsAt DateTime @db.Timestamptz(3)
cancelledAt DateTime? @db.Timestamptz(3)
createdAt DateTime @default(now()) @db.Timestamptz(3)
@@index([roomId, startsAt])
}По умолчанию Prisma хранит DateTime в PostgreSQL как время без зоны. Если агент не поставил @db.Timestamptz, через месяц вы будете искать, почему брони съехали на три часа.
Миграция. Prisma не умеет описывать ограничения исключения в схеме, поэтому их добавляют в SQL миграции вручную. Агент создаёт пустую миграцию (npx prisma migrate dev --create-only) и дописывает:
CREATE EXTENSION IF NOT EXISTS btree_gist;
ALTER TABLE "Booking"
ADD CONSTRAINT booking_no_overlap
EXCLUDE USING gist (
"roomId" WITH =,
tstzrange("startsAt", "endsAt", '[)') WITH &&
)
WHERE ("cancelledAt" IS NULL);Прочитайте это построчно. Полуоткрытый интервал [) означает, что встречи 14:00–15:00 и 15:00–16:00 не пересекаются. Условие WHERE исключает отменённые брони — иначе отменённый слот нельзя будет занять снова.
Обработка ошибки. Найдите место, где API превращает нарушение ограничения (код PostgreSQL 23P01) в ответ 409. Частая ошибка агента — ловить любую ошибку базы и отвечать «слот занят». Тогда упавшее соединение тоже выглядит как занятый слот.
Остальное — маршруты, разметку экрана — достаточно пробежать глазами: за них отвечают тесты и типы.
Проверьте руками
Отчёт агента «тесты прошли» — не то же самое, что работающий сервис. Запустите и попробуйте сломать:
docker compose up -d && npm run db:migrate && npm run dev
# два одновременных запроса на один слот
for i in 1 2; do
curl -s -o /dev/null -w "%{http_code}\n" -X POST localhost:3000/api/bookings \
-H 'content-type: application/json' \
-d '{"roomId":1,"title":"Планёрка","startsAt":"2026-10-23T11:00:00+03:00","endsAt":"2026-10-23T12:00:00+03:00"}' &
done; waitОжидаемый результат — один ответ 201 и один 409. Потом откройте экран в браузере и забронируйте слот мышкой: интеграционный тест не заметит, что кнопка не реагирует.
Закрываем срез
Попросите агента отметить пункт в docs/PLAN.md и коротко описать в нём, что сделано и что отложено (TODO(auth)). Закоммитьте одним коммитом с понятным сообщением. Этот срез — эталон: в следующих задачах пишите «сделай по образцу брони».
Попробуйте сами
10–15 мин на рабочем месте- Сделайте первый срез своего проекта: план через
/plan, затем задача по схеме «цель — контекст — ограничения — готово, когда». - В диффе найдите три самых рискованных места и прочитайте их построчно. Запишите, что нашли.
- Проверьте гонку параллельными запросами через
curlили напишите на неё интеграционный тест.
Коротко
- Срез — маленький сценарий через все слои, который сразу работает.
- План читайте внимательнее кода: его дешевле исправить.
- В первом срезе читайте схему, миграции и обработку ошибок; остальное страхуют тесты.
- Инварианты вроде «брони не пересекаются» держит база, а не проверка в коде.
- Проверяйте руками: параллельные запросы и клик в браузере.
- Первый срез — образец для всех следующих.