- Зачем:
- два слепых ревью постановки (Codex и Claude) вернули CHANGES_REQUIRED:
guard чистого стенда противоречил доливке, не были заданы механизм
идемпотентности, точка фиксации, формат цепочки границ и проверки.
- Что:
- добавлен раздел «Решения по ревью постановки»: next-day как новый
ограниченный режим, день = [T_end, T_end+24h), предпроверка границы,
параметр expected_t_end, манифест как точка фиксации, поле boundaries,
батчевый режим проверки цепочки, расписание вынесено в отдельную задачу.
- критерии приёмки финализированы (были черновыми), добавлен раздел
«Границы (что не трогать)», исправлены ссылки на строки кода.
- Проверка:
- вычитка задачи; факты сверены обоими ревьюерами по коду ветки
(индексы находок — в обменном каталоге сессии).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
14 KiB
Status: ready-for-agent
Глагол next-day: собрать следующий модельный день
Parent
.scratch/generator-model-time-startup-history/PRD.md
Why
Рамка пересмотрена 2026-07-07 (обсуждение с пользователем). Было: «удлинить готовую историю» (2 суток -> 7) — этот мотив закрыл портативный артефакт (задача 07), приоритет был низкий. Стало: режим кормления стенда порциями. Ценность не в длине истории, а в дискретности подачи:
- Урок. Менти триггерит «следующий день» -> в Kafka падает батч за новый
модельный день -> прогон
etl_pipeline-> витрины и дашборды сдвинулись. Полный цикл DWH виден за один шаг; повторяемо и управляемо, в отличие от непрерывного live. - Имитация жизни. Тот же механизм по расписанию — стенд «живёт». В этой задаче делаем глагол готовым к расписанию (идемпотентность, громкие отказы), но само включение расписания — отдельная задача (решение по скоупу 2026-07-12, см. «Решения по ревью постановки»).
- Удлинение истории остаётся как частный случай (несколько next-day подряд).
Интерфейс
Новый глагол next-day в существующем DAG generator_control
(airflow/dags/generator_control_dag.py), не отдельный DAG: менти уже знает,
где ручки генератора. Pause-check etl_pipeline переиспользуется
(generator_control_dag.py:153-164 — подтверждено ревью). Guard чистого
стенда не переиспользуется — у next-day своя предпроверка границы
(см. решение 3 ниже).
Развилка реализации — решена (пользователь, 2026-07-07)
Выбран вариант 2: генерация следующего дня от слепка T_end.
Обоснование. Диагноз задачи 20 показал, что генератор на стыке ничего не ломает — красный гейт был гонкой в самой проверке. Оба варианта одинаково требуют цепочку границ и расширение проверок, а вариант 1 сверх того требует новый режим среза в импорте («всё или ничего» сейчас). Вариант 2 не дороже, честнее (жизнь стенда не ограничена длиной артефакта) и переиспользует закалённый restore-механизм (задача 09: донор в state, громкие ошибки, детерминированная запасная ветка).
Отклонено: вариант 1 (резать готовый артефакт на дневные порции) — не даёт неограниченной жизни и требует своей новой механики импорта.
Решения по ревью постановки (2026-07-12, два слепых ревью)
Оба ревью (Codex и Claude, свежие сессии) вернули CHANGES_REQUIRED. Все находки внесены решениями ниже; полные индексы — в обменном каталоге сессии (в репо не хранятся).
- next-day — новый ограниченный режим запуска, а не переиспользование
существующего глагола. Сейчас
backfill— свежая генерация отT0с чистым завершением,continue— бесконечный live (generator/src/clickstream_generator/launch.py:96-128,service.py:96-128). next-day = восстановление мира из state- генерация ровно одного дня с самозавершением + сдвиг манифеста. Это основная работа задачи, «переиспользуется» только restore-механизм.
- Один модельный день = ровно 24 часа, полуоткрытый диапазон
[T_end, T_end + 24h)в модельном времени (UTC). Календарные сутки иGEN_MODEL_TIMEZONEв границах не участвуют; литералы времени в проверках — по образцуclickhouse_datetime_literal(урок задачи 20). - Предпроверка границы вместо guard'а чистого стенда.
assert_stand_cleanв DAG требует пустые Kafka и STG (airflow/dags/lib/airflow_control.py:99-109) — next-day по построению доливает в непустой стенд. Своя предпроверка: манифест существует, state согласован сT_endманифеста, live-генератор остановлен. Образец «непустого» режима —assert_stand_clean.sh continue(в Python-версии guard'а такого режима нет — добавить именно для next-day,clean-режим не менять). - Идемпотентность — через явный параметр. У DAG-запуска next-day есть
необязательный параметр
expected_t_end: если задан и не совпадает сT_endманифеста — громкий отказ, в тексте обе границы (ожидаемая и фактическая). Повторный триггер «того же дня» с прежнимexpected_t_endпосле успешного прогона ловится этим же сравнением. Без параметра — генерация от текущей границы (режим «жизни»); от двойного параллельного запуска защищаетmax_active_runs=1. - Точка фиксации — публикация манифеста, автоотката нет. Порядок
публикации дня: топики данных -> state -> манифест последним. Сбой до
манифеста оставляет «хвост» за границей — его ловит проверка цепочки
(непарные счётчики), восстановление — переимпорт артефакта
(задокументировать в OPERATIONS.md). Отклонено: инкрементальный откат
через
rollback_import_topics— он удаляет топики целиком (startup_history_artifact.py:250-270) и для доливки непригоден. - Цепочка границ живёт внутри единственной записи манифеста.
Манифест остаётся одной записью compact-топика с ключом
default(kafka_io.py:205-274); добавляется необязательное полеboundaries— накопительный список границ[T0, d1, ..., T_end]. Старый манифест без поля читается как[T0, T_end]. Итоговые счётчики и контрольные суммы остаются накопительными за всю историю. Формат артефакта меняется только этим необязательным полем — без новой версии и без изменения импорта. - Проверка цепочки — новый батчевый режим, live-гейт не трогать.
Текущий seam-гейт (
scripts/check_generated_analytics.sh:334-531) привязан к одной границе и живому генератору — литерально «расширить» его нельзя. Новый режим (отдельная make-цель): по каждой границе изboundaries— непарные счётчики и однородность per-event полей у визитов, переживших границу (класс проверки задачи 09). Существующий сценарийmake generated-history-runtime-checkостаётся зелёным как был. - Расписание — вне этой задачи.
schedule=Noneу DAG не меняется; задача гарантирует только готовность глагола к расписанию (пункты 4–5). Включение «жизни» по расписанию — отдельная задача-продолжение (в ней же решить: как расписание задаётoperation=next-day, ведь параметр DAG по умолчанию —backfill, и планового запуска с дефолтом быть не должно). - Исправлены ссылки: запасная ветка рождения визита —
generation.py:196-209(строка 194 — основная ветка); донор сериализуется вruntime.py:178-226.
Acceptance criteria
- Глагол
next-dayвgenerator_control: батч ровно за[T_end, T_end + 24h)отT_endманифеста; после прогона манифест содержит новую границу вboundariesи новыйT_end. - Предпроверка границы: на пустом стенде (нет манифеста) next-day
громко падает; при работающем live-генераторе — громко падает;
clean-guard и существующие глаголыbackfill|import|checkне изменены. - Идемпотентность: запуск с
expected_t_end, не равнымT_endманифеста, — громкий отказ с обеими границами в тексте; повторный запуск того же дня после успеха — тот же отказ, дублей и второго мира нет. - Проверка цепочки (новая make-цель): проходит по всем границам из
boundaries; после N прогонов next-day в цепочке N новых границ; непарные счётчики нулевые, per-event поля визитов через каждую границу однородны (браузер, referer, utm). - Учебный цикл целиком и повторяемо: next-day -> прогон
etl_pipeline-> в DM-витринах появились строки ровно за новый день (счётчики до/после, прирост только в диапазоне нового дня); проверено на двух next-day подряд. - Регрессий нет: тесты генератора и контракты корня зелёные
(
make test), сценарийmake generated-history-runtime-checkзадачи 20 остаётся зелёным. - Документация: OPERATIONS.md — глагол, предпроверка, поведение при сбое до фиксации манифеста (восстановление переимпортом артефакта).
Границы (что не трогать)
- Поведение существующих глаголов
backfill,import,checkиclean-guard'а. - Формат артефакта — кроме необязательного поля
boundaries(решение 6); механику импорта не менять. - Live seam-гейт задачи 20 (
generated-history-runtime-check) — не редактировать, только прогонять как регрессию.
Blocked by
- Нет. Последний блокер снят 2026-07-12:
20-flaky-runtime-seam-check.mdзакрыта, гейт стыка стабилен (3 подряд зелёных прогона, красный сценарий ловится). - Исторические блокеры закрыты:
15-world-boundary-after-cross-review.md(граница миров) и17-trusted-checks-startup-history-superset.md(доверенные проверки) — done.
Исторический контекст: 09-seam-browser-fixture-not-preserved.md — шов
рождается при любом прохождении мира через сериализованный state (рестарт,
сбой, доливка — один код восстановления); фикс 09 положил донора фактуры в
state и закрыл тихие fallback'и. Прежняя редакция этой задачи называлась
«Доливка стартовой истории кусочком от слепка». Проверенные 2026-07-07
факты (import «всё или ничего», донор задачи 09) подтверждены обоими ревью
2026-07-12 по коду ветки.