feat(generator): добавлена стартовая история через backfill

- Зачем:
  - стенду нужна повторяемая история с живым продолжением от модельной границы без дублей и разрыва визитов.
- Что:
  - добавлен backfill-режим с `GEN_MODEL_T_END`, manifest и state на `T_end`.
  - live-запуск восстанавливается из manifest без настенной дельты и проверяет совместимость state.
  - добавлены SQL-проверки формы данных, повторяемости и стыка backfill с live.
- Проверка:
  - make generator-test.
  - два чистых ClickHouse-прогона backfill дали одинаковые manifest checksums и digest.
  - reviewer gate issue 05 пройден после исправлений state/manifest.
This commit is contained in:
2026-06-14 19:45:58 +03:00
parent fcb06da16e
commit d5408f28e9
11 changed files with 1509 additions and 21 deletions
@@ -1,4 +1,4 @@
Status: ready-for-agent
Status: ready-for-human
# Стартовая история до ClickHouse
@@ -19,30 +19,200 @@ Status: ready-for-agent
## Acceptance criteria
- [ ] Промотка прошлого создаёт события за `[T0, T_end)`, слепок состояния и
- [x] Промотка прошлого создаёт события за `[T0, T_end)`, слепок состояния и
манифест с контрольными данными.
- [ ] При одинаковых `GEN_SEED`, `T0`, `T_end` и настройках артефакт промотки
- [x] При одинаковых `GEN_SEED`, `T0`, `T_end` и настройках артефакт промотки
прошлого повторяем точно; проверки в ClickHouse следуют правилам допуска из
контракта задачи 1.
- [ ] История загружается в ClickHouse штатной или явно описанной командой.
- [ ] SQL-проверка показывает здоровую пирамиду: пользователей меньше, чем
- [x] История загружается в ClickHouse штатной или явно описанной командой.
- [x] SQL-проверка показывает здоровую пирамиду: пользователей меньше, чем
визитов, визитов меньше, чем событий.
- [ ] SQL-проверка показывает возвраты: у части пользователей больше одного
- [x] SQL-проверка показывает возвраты: у части пользователей больше одного
визита.
- [ ] SQL-проверка длины визита проверяет форму, а не только среднее: долю
- [x] SQL-проверка длины визита проверяет форму, а не только среднее: долю
коротких визитов, медиану и долю срезов о потолок.
- [ ] SQL-проверка воронки `/home -> товары -> /cart -> /payment ->
- [x] SQL-проверка воронки `/home -> товары -> /cart -> /payment ->
/confirmation` монотонно убывает, а доля дошедших до `/confirmation` в
согласованном коридоре.
- [ ] Живое продолжение после `T_end` не создаёт дублей на границе и не выглядит
- [x] Живое продолжение после `T_end` не создаёт дублей на границе и не выглядит
как независимый второй мир.
- [ ] Визиты, которые переходят через `T_end`, остаются однородными: контекст
- [x] Визиты, которые переходят через `T_end`, остаются однородными: контекст
визита не меняется на стыке стартовой истории и живого продолжения.
- [ ] Подготовлены данные, команды и SQL-проверки, достаточные для внешнего
- [x] Подготовлены данные, команды и SQL-проверки, достаточные для внешнего
review gate по распределениям и двум путям генерации из `PRD.md`.
- [ ] Worker даёт промежуточный статус, если промотка, загрузка в ClickHouse или
- [x] Worker даёт промежуточный статус, если промотка, загрузка в ClickHouse или
распределительные проверки занимают заметное время.
## Blocked by
- `.scratch/generator-model-time-startup-history/issues/04-state-v2-model-resume.md`
## Фактический прогон worker-а
Дата проверки: 2026-06-14.
Настройки стенда:
- `GEN_SEED=4242`
- `GEN_MODEL_T0=2026-01-01T00:00:00+00:00`
- `GEN_MODEL_T_END=2026-01-01T06:00:00+00:00`
- `GEN_MODEL_TIMEZONE=UTC`
- `GEN_MODEL_TIME_SPEED=1`
- `GEN_TICK_SECONDS=60`
- `GEN_LAMBDA_BASE_PER_MIN=120`
- `GEN_JITTER_PCT=0`
- `GEN_MIN_EVENTS_PER_TICK=1`
- `GEN_MAX_EVENTS_PER_TICK=1000`
Команды проверки:
```bash
make clean
docker compose up -d clickhouse kafka
make ddl
docker compose build generator
docker compose run --rm --no-deps \
-e GEN_RUN_MODE=backfill \
-e GEN_STATE_RESET=true \
-e GEN_SEED=4242 \
-e GEN_MODEL_T0=2026-01-01T00:00:00+00:00 \
-e GEN_MODEL_T_END=2026-01-01T06:00:00+00:00 \
-e GEN_MODEL_TIMEZONE=UTC \
-e GEN_MODEL_TIME_SPEED=1 \
-e GEN_TICK_SECONDS=60 \
-e GEN_LAMBDA_BASE_PER_MIN=120 \
-e GEN_JITTER_PCT=0 \
-e GEN_MIN_EVENTS_PER_TICK=1 \
-e GEN_MAX_EVENTS_PER_TICK=1000 \
generator
sleep 10
bash scripts/run_batch.sh
```
Повторяемость проверена двумя чистыми прогонами с полным сбросом
ClickHouse/Kafka/state через `make clean`. В обоих прогонах manifest и ClickHouse
дали одинаковые контрольные числа:
- manifest `browser_events.checksum_sha256`:
`3b1946bd7fd3d669e2461fcbae88d8aed6980bb5f126779fb4e7480ce15a3269`
- manifest `location_events.checksum_sha256`:
`370816c8d0b0311592282b9b75372e18862db9ef4984f7032885e935f9436522`
- manifest `device_events.checksum_sha256`:
`37e157ac7d5a3203c1c27b2a11abc1b9cb9fc7f74f79c77645faa09412e08b33`
- manifest `geo_events.checksum_sha256`:
`ec0e36e4613e05b9a5e72490c7565cf7e5f767d39a7a58b383e56e2eb191fc20`
- ClickHouse digest:
`CC1A73E65E897D1F1FC982CFA4237A07`
Backfill в ClickHouse:
- `events=31825`
- `unique_events=31825`
- `visits=3020`
- `users=1048`
- `min_event_ts=2026-01-01 00:00:00.000000`
- `max_event_ts=2026-01-01 05:59:59.521599`
- `pyramid_ok=1`
- `half_open_ok=1`
Проверка возвратов:
- `users=1048`
- `returning_users=708`
- `returning_share=0.6755725190839694`
- `max_visits_per_user=11`
Форма длины визита:
- `visits=3020`
- `short_visit_share=0.16490066225165562`
- `median_events_per_visit=8`
- `avg_events_per_visit=10.538079470198676`
- `capped_visit_share=0.0619205298013245`
- `median_duration_sec=202`
- `p95_duration_sec=823`
- `max_events_per_visit=30`
Воронка:
- строгая упорядоченная:
`home=2743`, `products=1626`, `cart=995`, `payment=673`,
`confirmation=394`, `monotonic_ok=1`,
`ordered_confirmation_share=0.14363835216915785`;
- калибровочная по наличию страницы в визите:
`home=2743`, `products=2706`, `cart=2003`, `payment=1368`,
`confirmation=824`, `monotonic_ok=1`,
`confirmation_share_all_visits=0.2728476821192053`,
`confirmation_share_from_home=0.3004010207801677`.
Live-продолжение:
```bash
docker compose run -d --name issue05-live --no-deps \
-e GEN_RUN_MODE=live \
-e GEN_STATE_RESET=false \
-e GEN_SEED=4242 \
-e GEN_MODEL_T0=2026-01-01T00:00:00+00:00 \
-e GEN_MODEL_T_END=2026-01-01T06:00:00+00:00 \
-e GEN_MODEL_TIMEZONE=UTC \
-e GEN_MODEL_TIME_SPEED=1 \
-e GEN_TICK_SECONDS=60 \
-e GEN_LAMBDA_BASE_PER_MIN=120 \
-e GEN_JITTER_PCT=0 \
-e GEN_MIN_EVENTS_PER_TICK=1 \
-e GEN_MAX_EVENTS_PER_TICK=1000 \
generator
```
Лог live-запуска подтвердил восстановление из стартовой истории:
`model_time=2026-01-01T06:00:00+00:00`, затем прошли тики `361`-`364`.
После live и повторного `bash scripts/run_batch.sh`:
- `users=1079`
- `visits=3069`
- `events=32145`
- `min_event_ts=2026-01-01 00:00:00.000000`
- `max_event_ts=2026-01-01 06:03:00.000000`
- `history_events=31825`
- `live_events=320`
- `duplicate_events=0`
- `boundary_events=13`
`boundary_events=13` — это live-события ровно на `T_end`; они не входили в
backfill `[T0, T_end)`.
Визиты через `T_end`:
- `crossing_visits=36`
- `visits_with_both_sides=36`
- `homogeneous_visits=36`
- `context_ok=1`
- `min_before_events=2`
- `min_after_events=1`
- `max_after_events=10`
## Риски для review gate
- Manifest, state и события связаны метаданными (`GEN_SEED`, `T0`, `T_end`,
настройки генерации, `last_batch_id`) и checksum событий по топикам. Полной
криптографической связки `state + events + manifest` пока нет.
- Для воронки есть две SQL-формы. Строгая ordered-форма проверяет порядок
`/home -> товары -> /cart -> /payment -> /confirmation`. Калибровочная
contains-форма проверяет, была ли страница в визите. Для review gate
`confirmation_share` сравнивается с коридором мат-модели по contains-форме.
## Review gate
- Саморевью worker-а нашло блокирующий риск: backfill не должен сохранять
state/manifest при частичной публикации. Исправлено fail-fast поведением и
тестом.
- Первый reviewer gate нашёл два блокера: startup-history state мог остаться без
manifest и восстановиться как live-state, а startup-history restore не сверял
поля самого state. Оба пункта исправлены до коммита.
- Повторный reviewer gate: `gate pass`, новых блокирующих находок нет.
- Проверки после исправлений: `make generator-test` — 134 passed.
- Остаточный риск процесса: reviewer был со свежим контекстом, но той же
родословной, а не внешней моделью.