Files
clickstream-ch-kafka-supers…/.scratch/generator-model-time-startup-history/issues/13-backfill-top-up-from-snapshot.md
T
ddadminandClaude Fable 5 540989d358 docs(generator): постановка задачи 13 доработана после двойного ревью
- Зачем:
  - два слепых ревью постановки (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>
2026-07-12 22:30:56 +03:00

161 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
Все находки внесены решениями ниже; полные индексы — в обменном каталоге
сессии (в репо не хранятся).
1. **next-day — новый ограниченный режим запуска, а не переиспользование
существующего глагола.** Сейчас `backfill` — свежая генерация от `T0`
с чистым завершением, `continue` — бесконечный live
(`generator/src/clickstream_generator/launch.py:96-128`,
`service.py:96-128`). next-day = восстановление мира из state
+ генерация ровно одного дня с самозавершением + сдвиг манифеста.
Это основная работа задачи, «переиспользуется» только restore-механизм.
2. **Один модельный день = ровно 24 часа, полуоткрытый диапазон
`[T_end, T_end + 24h)` в модельном времени (UTC).** Календарные сутки и
`GEN_MODEL_TIMEZONE` в границах не участвуют; литералы времени в
проверках — по образцу `clickhouse_datetime_literal` (урок задачи 20).
3. **Предпроверка границы вместо 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`-режим не менять).
4. **Идемпотентность — через явный параметр.** У DAG-запуска next-day есть
необязательный параметр `expected_t_end`: если задан и не совпадает с
`T_end` манифеста — громкий отказ, в тексте обе границы (ожидаемая и
фактическая). Повторный триггер «того же дня» с прежним `expected_t_end`
после успешного прогона ловится этим же сравнением. Без параметра —
генерация от текущей границы (режим «жизни»); от двойного параллельного
запуска защищает `max_active_runs=1`.
5. **Точка фиксации — публикация манифеста, автоотката нет.** Порядок
публикации дня: топики данных -> state -> манифест последним. Сбой до
манифеста оставляет «хвост» за границей — его ловит проверка цепочки
(непарные счётчики), восстановление — переимпорт артефакта
(задокументировать в OPERATIONS.md). Отклонено: инкрементальный откат
через `rollback_import_topics` — он удаляет топики целиком
(`startup_history_artifact.py:250-270`) и для доливки непригоден.
6. **Цепочка границ живёт внутри единственной записи манифеста.**
Манифест остаётся одной записью compact-топика с ключом `default`
(`kafka_io.py:205-274`); добавляется необязательное поле `boundaries`
накопительный список границ `[T0, d1, ..., T_end]`. Старый манифест без
поля читается как `[T0, T_end]`. Итоговые счётчики и контрольные суммы
остаются накопительными за всю историю. Формат артефакта меняется только
этим необязательным полем — без новой версии и без изменения импорта.
7. **Проверка цепочки — новый батчевый режим, live-гейт не трогать.**
Текущий seam-гейт (`scripts/check_generated_analytics.sh:334-531`)
привязан к одной границе и живому генератору — литерально «расширить»
его нельзя. Новый режим (отдельная make-цель): по каждой границе из
`boundaries` — непарные счётчики и однородность per-event полей у
визитов, переживших границу (класс проверки задачи 09). Существующий
сценарий `make generated-history-runtime-check` остаётся зелёным как был.
8. **Расписание — вне этой задачи.** `schedule=None` у DAG не меняется;
задача гарантирует только готовность глагола к расписанию (пункты 4–5).
Включение «жизни» по расписанию — отдельная задача-продолжение (в ней же
решить: как расписание задаёт `operation=next-day`, ведь параметр DAG по
умолчанию — `backfill`, и планового запуска с дефолтом быть не должно).
9. **Исправлены ссылки:** запасная ветка рождения визита —
`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 по коду ветки.