diff --git a/.scratch/generator-model-time-startup-history/issues/13-backfill-top-up-from-snapshot.md b/.scratch/generator-model-time-startup-history/issues/13-backfill-top-up-from-snapshot.md index 7bcc35b..c7aea31 100644 --- a/.scratch/generator-model-time-startup-history/issues/13-backfill-top-up-from-snapshot.md +++ b/.scratch/generator-model-time-startup-history/issues/13-backfill-top-up-from-snapshot.md @@ -1,6 +1,6 @@ -Status: needs-triage +Status: needs-info -# Доливка стартовой истории кусочком от слепка +# Глагол next-day: собрать следующий модельный день ## Parent @@ -8,40 +8,87 @@ Status: needs-triage ## Why -Сейчас backfill всегда стартует с чистого состояния от `T0` -(`service.py:178-185`): удлинить существующую историю нельзя — есть 2 суток, -нужно 7, генерируй все 7 заново. Идея — «долить» продолжение от сохранённого -слепка `T_end`. +Рамка пересмотрена 2026-07-07 (обсуждение с пользователем). Было: «удлинить +готовую историю» (2 суток -> 7) — этот мотив закрыл портативный артефакт +(задача 07), приоритет был низкий. Стало: **режим кормления стенда порциями**. +Ценность не в длине истории, а в дискретности подачи: -Приоритет низкий (решение 2026-07-04): с портативным артефактом -(задача 07) длинную историю можно сгенерировать один раз и переиспользовать, -так что потребность в доливке может и не возникнуть. Вернуться, если возникнет. +- **Урок.** Менти триггерит «следующий день» -> в Kafka падает батч за новый + модельный день -> прогон `etl_pipeline` -> витрины и дашборды сдвинулись. + Полный цикл DWH виден за один шаг; повторяемо и управляемо, в отличие от + непрерывного live. +- **Имитация жизни.** Тот же механизм по расписанию (`@daily` или чаще) — + стенд «живёт»: мониторинги бегут, метрики копятся, нагрузка видна. +- Удлинение истории остаётся как частный случай (несколько next-day подряд). -## What to build +## Интерфейс -- Режим backfill, продолжающий от слепка: новая порция истории от `T_end` до - нового `T_end'`, манифест обновляется. +Новый глагол `next-day` в существующем DAG `generator_control` +(`airflow/dags/generator_control_dag.py`), не отдельный DAG: менти уже знает, +где ручки генератора; pause-check `etl_pipeline` и guard чистого стенда +переиспользуются. Ручной триггер — для урока, расписание — для «жизни». + +## Развилка реализации (решить до реализации) + +Интерфейс `next-day` одинаков для двух начинок, начинку можно менять, не +трогая то, чему учим: + +1. **Резать готовый артефакт на дневные порции.** История сгенерирована одним + прогоном (задача 07), швов нет вообще — границы дней внутри одного мира + естественные. Дёшево, если механизм импорта умеет подавать историю частями + (см. «Открытые вопросы»). Ограничение: горизонт равен длине артефакта. +2. **Доливка от слепка `T_end`.** Настоящая генерация продолжения. Нужна, + когда жизнь стенда должна быть неограниченной. Каждый батч добавляет шов; + restore-механизм общий с рестартом и закалён задачей 09, но открыт хвост + про запасную ветку рождения визита (см. критерии). + +Разумный путь: вариант 1 как MVP, вариант 2 — когда упрёмся в конец артефакта +(возможно, отдельной задачей). + +## Открытые вопросы (блокируют ready-for-agent) + +- [x] Умеет ли текущий импорт артефакта (`startup-history-import`) подавать + историю частями? **Нет, «всё или ничего»** (проверено 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 + (для учебного сценария этого достаточно). +- [ ] Для варианта 2 — решение по запасной ветке рождения визита + (`generation.py`, ветка без донора-кандидата): либо закрепить проверкой, + что она не срабатывает, либо ограничить длину визита так, чтобы fallback + не менял фактуру на стыке. Унаследовано из прежней редакции задачи. ## Acceptance criteria (черновые) -- [ ] Доливка продолжает мир слепка: без дублей и смешения на границе. -- [ ] Однородность визитов, переживших границу доливки, — включая per-event - браузерные поля (`dds.event`), см. дефект - `09-seam-browser-fixture-not-preserved.md`: доливка создаёт новые стыки, - тот же механизм восстановления. -- [ ] Манифест после доливки отражает новый `T_end'`; антисмешивание работает. -- [ ] До реализации доливки принято решение по fallback-ветке восстановления - визита: либо свойство донорского сида закреплено проверкой, либо длина визита - ограничена так, чтобы fallback не менял фактуру на стыке. +- [ ] Глагол `next-day` в `generator_control`: батч ровно за один следующий + модельный день от текущей границы манифеста. +- [ ] Идемпотентность: повторный триггер того же дня — громкий отказ по + манифесту, а не второй мир и не дубли. +- [ ] Манифест двигает `T_end` атомарно; антисмешивание работает на **цепочке** + границ (30 батчей = 30 границ), проверки + (`scripts/check_generated_analytics.sh` и родня) расширены с одной границы + на цепочку. +- [ ] Учебный цикл целиком: после `next-day` прогон `etl_pipeline` даёт новые + строки в DM-витринах; проверка повторяемая, не разовая. +- [ ] Для варианта 2: визиты, пережившие границу батча, однородны по всем + per-event полям (браузер, referer, utm) — тот же класс проверки, что в + задаче 09. ## Blocked by -- `15-world-boundary-after-cross-review.md` — доливка продолжает мир от слепка; - сначала нужен закрытый инвариант «старый state и консольные пути не создают - второй мир». -- `17-trusted-checks-startup-history-superset.md` — доливка создаёт новые - стыки; сначала нужна доверенная проверка фактической границы по манифесту. +- Открытые вопросы выше (статус needs-info до их закрытия). +- Исторические блокеры закрыты: `15-world-boundary-after-cross-review.md` + (граница миров) и `17-trusted-checks-startup-history-superset.md` + (доверенные проверки) — done. -Исторический контекст: `09-seam-browser-fixture-not-preserved.md` закрыла -фактуру визита на стыке восстановления, но кросс-линейное ревью 2026-07-05 -нашло более широкие риски вокруг границы мира и проверки стыка. +Исторический контекст: `09-seam-browser-fixture-not-preserved.md` — шов +рождается при любом прохождении мира через сериализованный state (рестарт, +сбой, доливка — один код восстановления); фикс 09 положил донора фактуры в +state и закрыл тихие fallback'и. Прежняя редакция этой задачи называлась +«Доливка стартовой истории кусочком от слепка». diff --git a/.scratch/generator-model-time-startup-history/issues/20-flaky-runtime-seam-check.md b/.scratch/generator-model-time-startup-history/issues/20-flaky-runtime-seam-check.md new file mode 100644 index 0000000..04a4673 --- /dev/null +++ b/.scratch/generator-model-time-startup-history/issues/20-flaky-runtime-seam-check.md @@ -0,0 +1,66 @@ +Status: needs-triage + +# Runtime-проверка стыка нестабильна: фактура меняется через раз + +## Parent + +`.scratch/generator-model-time-startup-history/PRD.md` + +## Что нашли + +Два подряд прогона `make generated-history-runtime-check` на одном и том же +стенде (2026-07-07, проверка пути менти) дали разный результат: + +- Прогон 1 (20:18–20:20): **красный** — + `Ошибка: per-event фактура меняется на стыке: 8/19` + (у 8 из 19 визитов, переживших границу backfill->live, поменялась + per-event фактура). При этом `duplicate_events=0`, events=2776. +- Прогон 2 (21:5x, та же команда, без изменений кода): **зелёный** — + «runtime-проверка startup-history/live seam прошла», конфликтов 0, + events=2815. + +Конфигурация проверки: `GEN_LAUNCH_PROFILE=daily-wave`, +`GEN_HISTORY_DURATION=1h`, `GEN_MODEL_T_END=2026-01-01T01:00:00+00:00` +(зашита в `scripts/run_generated_history_runtime_check.sh`). + +## Почему это важно + +- Это гейт, которому мы доверяем стык backfill->live (задача 17); флаки-гейт + ничего не гарантирует: красный пугает зря, зелёный ничего не доказывает. +- Смена фактуры на стыке — класс дефекта задачи 09, который считается + закрытым (донор фактуры сохранён в state, тихие fallback'и заменены на + ошибки). Либо фикс неполон, либо есть второй источник расхождения. +- Задача 13 (глагол next-day) навешивает на этот же механизм цепочку границ — + ей нужен доверенный, стабильный гейт. + +## Гипотезы (проверить при диагнозе) + +1. **Тихая запасная ветка рождения визита** (`generation.py`, ветка без + донора-кандидата): при истории в 1 час словарь визитов маленький, донора + с достаточным числом событий может не найтись. Задача 09 требовала + «молча оставить как есть нельзя» — проверить, что сделал фикс. +2. Недетерминизм live-части: реальное время тиков решает, какие визиты + пересекут границу; сам факт «через раз» указывает на зависимость от + wall-clock, а не от сида. +3. Проверочный SQL: убедиться, что сравнение полей не цепляет поля, + легитимно различающиеся (по образцу исключения `event_id` в задаче 09). + +## Acceptance criteria (черновые) + +- [ ] Причина расхождения 8/19 найдена и названа (код, не догадка). +- [ ] Либо генератор починен (фактура на стыке стабильна при любом профиле), + либо проверка исправлена (если ловила легитимные различия) — с объяснением. +- [ ] `make generated-history-runtime-check` стабилен: N подряд прогонов + зелёные (N >= 3), зафиксировано в задаче. +- [ ] Если срабатывает запасная ветка рождения — она стала громкой или + воспроизводимой при восстановлении (наследие задачи 09 и открытый вопрос + задачи 13 закрываются согласованно). + +## Blocked by + +- Нет. Диагноз можно начинать сразу; стенд воспроизводит через раз. + +Связано: `09-seam-browser-fixture-not-preserved.md` (класс дефекта и решение +про донора), `13-backfill-top-up-from-snapshot.md` (нуждается в доверенном +гейте на цепочке границ), `17-trusted-checks-startup-history-superset.md` +(появление этого гейта).