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>
This commit is contained in:
+107
-42
@@ -17,67 +17,130 @@ Status: ready-for-agent
|
|||||||
модельный день -> прогон `etl_pipeline` -> витрины и дашборды сдвинулись.
|
модельный день -> прогон `etl_pipeline` -> витрины и дашборды сдвинулись.
|
||||||
Полный цикл DWH виден за один шаг; повторяемо и управляемо, в отличие от
|
Полный цикл DWH виден за один шаг; повторяемо и управляемо, в отличие от
|
||||||
непрерывного live.
|
непрерывного live.
|
||||||
- **Имитация жизни.** Тот же механизм по расписанию (`@daily` или чаще) —
|
- **Имитация жизни.** Тот же механизм по расписанию — стенд «живёт».
|
||||||
стенд «живёт»: мониторинги бегут, метрики копятся, нагрузка видна.
|
В этой задаче делаем глагол готовым к расписанию (идемпотентность,
|
||||||
|
громкие отказы), но само включение расписания — отдельная задача
|
||||||
|
(решение по скоупу 2026-07-12, см. «Решения по ревью постановки»).
|
||||||
- Удлинение истории остаётся как частный случай (несколько next-day подряд).
|
- Удлинение истории остаётся как частный случай (несколько next-day подряд).
|
||||||
|
|
||||||
## Интерфейс
|
## Интерфейс
|
||||||
|
|
||||||
Новый глагол `next-day` в существующем DAG `generator_control`
|
Новый глагол `next-day` в существующем DAG `generator_control`
|
||||||
(`airflow/dags/generator_control_dag.py`), не отдельный DAG: менти уже знает,
|
(`airflow/dags/generator_control_dag.py`), не отдельный DAG: менти уже знает,
|
||||||
где ручки генератора; pause-check `etl_pipeline` и guard чистого стенда
|
где ручки генератора. Pause-check `etl_pipeline` переиспользуется
|
||||||
переиспользуются. Ручной триггер — для урока, расписание — для «жизни».
|
(`generator_control_dag.py:153-164` — подтверждено ревью). Guard чистого
|
||||||
|
стенда **не** переиспользуется — у next-day своя предпроверка границы
|
||||||
|
(см. решение 3 ниже).
|
||||||
|
|
||||||
## Развилка реализации — решена (пользователь, 2026-07-07)
|
## Развилка реализации — решена (пользователь, 2026-07-07)
|
||||||
|
|
||||||
**Выбран вариант 2: генерация следующего дня от слепка `T_end`.**
|
**Выбран вариант 2: генерация следующего дня от слепка `T_end`.**
|
||||||
|
|
||||||
Обоснование. Диагноз задачи 20 показал, что генератор на стыке ничего не
|
Обоснование. Диагноз задачи 20 показал, что генератор на стыке ничего не
|
||||||
ломает — красный гейт был гонкой в самой проверке. Страх «швов» был страхом
|
ломает — красный гейт был гонкой в самой проверке. Оба варианта одинаково
|
||||||
перед артефактом верификатора. При этом оба варианта одинаково требуют
|
требуют цепочку границ и расширение проверок, а вариант 1 сверх того требует
|
||||||
цепочку манифестов и расширение проверок, а вариант 1 сверх того требует
|
новый режим среза в импорте («всё или ничего» сейчас). Вариант 2 не дороже,
|
||||||
новый режим среза в импорте («всё или ничего» сейчас). То есть вариант 2 не
|
честнее (жизнь стенда не ограничена длиной артефакта) и переиспользует
|
||||||
дороже, честнее (жизнь стенда не ограничена длиной артефакта) и переиспользует
|
закалённый restore-механизм (задача 09: донор в state, громкие ошибки,
|
||||||
уже закалённый restore-механизм (задача 09: донор в state, громкие ошибки,
|
|
||||||
детерминированная запасная ветка).
|
детерминированная запасная ветка).
|
||||||
|
|
||||||
Отклонено: вариант 1 (резать готовый артефакт на дневные порции) — не даёт
|
Отклонено: вариант 1 (резать готовый артефакт на дневные порции) — не даёт
|
||||||
неограниченной жизни и требует своей новой механики импорта.
|
неограниченной жизни и требует своей новой механики импорта.
|
||||||
|
|
||||||
## Открытые вопросы (блокируют ready-for-agent)
|
## Решения по ревью постановки (2026-07-12, два слепых ревью)
|
||||||
|
|
||||||
- [x] Умеет ли текущий импорт артефакта (`startup-history-import`) подавать
|
Оба ревью (Codex и Claude, свежие сессии) вернули CHANGES_REQUIRED.
|
||||||
историю частями? **Нет, «всё или ничего»** (проверено 2026-07-07):
|
Все находки внесены решениями ниже; полные индексы — в обменном каталоге
|
||||||
скрипт требует чистый стенд (`import_startup_history_artifact.sh:50`,
|
сессии (в репо не хранятся).
|
||||||
`assert_stand_clean.sh`), CLI артефакта воспроизводит все топики целиком и
|
|
||||||
не имеет опций среза, а валидация жёстко связывает manifest и state
|
|
||||||
(`startup_history_artifact.py`, `_validate_manifest_state`). Для варианта 1
|
|
||||||
нужен новый режим импорта: срез по модельному дню, цепочка manifest'ов
|
|
||||||
вместо «manifest равен state», и ослабление guard'а чистого стенда для
|
|
||||||
последующих порций (граница по манифесту вместо пустоты). Нюанс: артефакт
|
|
||||||
хранит один state на конец полной истории, поэтому live-продолжение
|
|
||||||
возможно только после последней порции; промежуточные дни — batch-only
|
|
||||||
(для учебного сценария этого достаточно).
|
|
||||||
- [x] Для варианта 2 — решение по запасной ветке рождения визита: **уже
|
|
||||||
закрыто фиксом задачи 09** (проверено по коду 2026-07-07 при диагнозе
|
|
||||||
задачи 20): ветка хранит донора (`generation.py:194-209`) и громко падает
|
|
||||||
на неполных locations, восстановление воспроизводит её детерминированно.
|
|
||||||
|
|
||||||
## Acceptance criteria (черновые)
|
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`.
|
||||||
|
|
||||||
- [ ] Глагол `next-day` в `generator_control`: батч ровно за один следующий
|
## Acceptance criteria
|
||||||
модельный день от текущей границы манифеста.
|
|
||||||
- [ ] Идемпотентность: повторный триггер того же дня — громкий отказ по
|
- [ ] Глагол `next-day` в `generator_control`: батч ровно за
|
||||||
манифесту, а не второй мир и не дубли.
|
`[T_end, T_end + 24h)` от `T_end` манифеста; после прогона манифест
|
||||||
- [ ] Манифест двигает `T_end` атомарно; антисмешивание работает на **цепочке**
|
содержит новую границу в `boundaries` и новый `T_end`.
|
||||||
границ (30 батчей = 30 границ), проверки
|
- [ ] Предпроверка границы: на пустом стенде (нет манифеста) next-day
|
||||||
(`scripts/check_generated_analytics.sh` и родня) расширены с одной границы
|
громко падает; при работающем live-генераторе — громко падает;
|
||||||
на цепочку.
|
`clean`-guard и существующие глаголы `backfill|import|check` не изменены.
|
||||||
- [ ] Учебный цикл целиком: после `next-day` прогон `etl_pipeline` даёт новые
|
- [ ] Идемпотентность: запуск с `expected_t_end`, не равным `T_end`
|
||||||
строки в DM-витринах; проверка повторяемая, не разовая.
|
манифеста, — громкий отказ с обеими границами в тексте; повторный запуск
|
||||||
- [ ] Для варианта 2: визиты, пережившие границу батча, однородны по всем
|
того же дня после успеха — тот же отказ, дублей и второго мира нет.
|
||||||
per-event полям (браузер, referer, utm) — тот же класс проверки, что в
|
- [ ] Проверка цепочки (новая make-цель): проходит по всем границам из
|
||||||
задаче 09.
|
`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
|
## Blocked by
|
||||||
|
|
||||||
@@ -92,4 +155,6 @@ Status: ready-for-agent
|
|||||||
рождается при любом прохождении мира через сериализованный state (рестарт,
|
рождается при любом прохождении мира через сериализованный state (рестарт,
|
||||||
сбой, доливка — один код восстановления); фикс 09 положил донора фактуры в
|
сбой, доливка — один код восстановления); фикс 09 положил донора фактуры в
|
||||||
state и закрыл тихие fallback'и. Прежняя редакция этой задачи называлась
|
state и закрыл тихие fallback'и. Прежняя редакция этой задачи называлась
|
||||||
«Доливка стартовой истории кусочком от слепка».
|
«Доливка стартовой истории кусочком от слепка». Проверенные 2026-07-07
|
||||||
|
факты (import «всё или ничего», донор задачи 09) подтверждены обоими ревью
|
||||||
|
2026-07-12 по коду ветки.
|
||||||
|
|||||||
Reference in New Issue
Block a user