# Переработка генератора: иерархическая модель сущностей для steady-stream Дата: 2026-06-09 Статус: Draft Связано: [ADR-0004](../adr/0004-steady-stream-synthetic-generator.md) (решение и почему), [`generator/KNOWN_ISSUES.md`](../../generator/KNOWN_ISSUES.md) (диагноз дефекта), [`CONTEXT.md`](../../CONTEXT.md) (доменный язык), [ADR-0002](../adr/0002-specs-as-durable-design-docs.md) ## Problem Курс на статическом сиде показывает только маленький одноразовый батч и не может показать, как стенд **живёт и движется**: непрерывный поток через пайплайн, шевелящиеся дашборды, поведение ClickHouse и витрин на живых данных. Текущий генератор источником быть не может — у него концептуальный дефект модели сущностей (свежий `click_id` на каждое событие схлопывает иерархию `пользователь → сессия → событие`, см. `KNOWN_ISSUES.md`). Нужен переписанный с нуля генератор, дающий правдоподобный живой поток. ## Context - **Решение направления уже принято** в [ADR-0004](../adr/0004-steady-stream-synthetic-generator.md): синтетический иерархический генератор, не реплей сида. Эта спека — про *форму* доработки, не про выбор «генератор vs реплей». - **Двойная учебная ценность** (критерий ADR-0004): ценны и данные (живой стенд + честная пирамида), и сам генератор как объект изучения. Дизайн должен быть пригоден для разбора в курсе, а не только «работать». - **Доменный язык** зафиксирован в `CONTEXT.md`: пользователь = `user_domain_id`, визит/сессия = `click_id`, событие = `event_id`; здоровые данные дают пирамиду `users ≤ sessions ≤ events`. Спека добавляет термины «популяция пользователей», «возвращающийся пользователь». - **Воронка в данных живёт в `page_url_path`, не в `event_type`.** В сиде `event_type` = 100% `pageview` (других типов нет); воронка реализована страницами: `/home → /product_a + /product_b → /cart → /payment → /confirmation` (терминальный шаг — `/confirmation`). Дашборд так и устроен (Page Funnel, конверсия `/confirmation ÷ /home`). Схема `event_type` *допускает* и другие значения (click/purchase/add_to_cart), но в данных их нет. `user_domain_id` — на уровне `click` (device_events), device/geo — сессионный контекст. См. `sql/ddl/dds/30_dds.sql`, `CONTEXT.md`. - **Что сохраняем без изменений:** модель интенсивности (`_calculate_events_count()` — Poisson + `_hour_factor()` + jitter) — `KNOWN_ISSUES.md` подтверждает её корректность. ## Goals - Поток порождает **здоровую пирамиду** `users < sessions < events`: популяция возвращающихся пользователей, у пользователя 1..N сессий, у сессии 1..M событий. - `click_id` **переиспользуется внутри сессии** (envelope из нескольких событий с общим device/geo), а не штампуется на каждое событие. - **Время событий внутри сессии монотонно растёт** (правдоподобный путь по страницам), а не равно `now()` для всего батча. - **Правдоподобная воронка** по `page_url_path`: путь по страницам (`/home → … → /confirmation`), где число визитов затухает от шага к шагу, а до последнего шага воронки доходит меньшинство (в сиде ~25%) — согласованно с дашбордом и сидом. - Сохранена модель интенсивности (жизнеподобные колебания нагрузки). - Встраивается в курс как **урок 7 «Система живёт»**, сосуществует с bootstrap-сидом и уроками 0–6. ## Non-goals - **Не закладываемся на объём/стресс.** Скромный rate, рассчитанный на слабое железо менти; цель — «видно жизнь и движение», а не нагрузочное тестирование. - **Не выбираем здесь распределения и прочую математику** — конкретные модели (события/сессия, сессии/пользователь, межсессионные паузы, нужен ли настоящий session-timeout, персистентность популяции через рестарты) выносятся в **отдельную follow-up-спеку**. - **Не трогаем** статический сид как bootstrap, базовые уроки 0–6, витрины и дашборд (строятся на сиде). - **Не дорабатываем текущую реализацию инкрементально** — переписываем с нуля (см. ADR-0004 и `KNOWN_ISSUES.md`). - **Не пишем сам урок 7** в рамках этой спеки — это материал по `LESSON_STANDARD` на этапе реализации. ## Current state `generator/generator.py`, `generate_batch()` — плоская выборка на каждое событие: свежий `click_id`, атрибуты копируются из случайной сид-строки, время ≈ `now()` для всего батча. Итог на потоке: `Sessions == Events` (сессия = одно событие), `user_domain_id` переиспользуется из сид-пула (~99) без привязки к сессиям. Полный разбор и таблица «сид vs поток» — в `generator/KNOWN_ISSUES.md`. Корректная часть, которую сохраняем: `_calculate_events_count()` (Poisson + дневной коэффициент + jitter). ## Recommended approach Иерархическая генерация вместо плоской выборки. На уровне требований (без выбора конкретных распределений — это follow-up-спека): 1. **Популяция пользователей.** Поддерживать множество пользователей с *постоянными* `user_domain_id`. Пользователь может **возвращаться** несколькими сессиями во времени. (Насколько device/geo стабильны между сессиями одного пользователя — моделирующее допущение; по схеме device/geo — сессионный контекст уровня `click`. Степень стабильности — в follow-up-спеке.) 2. **Сессии пользователя.** Пользователь со временем открывает 1..N сессий; каждая сессия — новый `click_id`, общий для всех её событий, с общим device/geo. Межсессионные паузы — модель «вернувшегося пользователя». 3. **События сессии.** Сессия порождает 1..M событий, разделяющих `click_id`, **упорядоченных по монотонно растущему времени**, образующих правдоподобный путь по воронке страниц (`page_url_path`: `/home → … → /confirmation`), где до последнего шага воронки доходит меньшинство визитов (~25%, как в сиде). (Вводить ли разнообразие `event_type` сверх `pageview` — открытый вопрос, см. ниже.) 4. **Интенсивность.** Поверх иерархии работает сохранённая модель интенсивности — сколько событий и когда (Poisson + дневной коэффициент + jitter). Связь с тиковой моделью генератора (как «развернуть» иерархию во времени по тикам, а не одним батчем) — открытый вопрос дизайна, см. ниже. ## Rationale - Иерархия — единственный способ получить возвраты и честную пирамиду (обоснование выбора против реплея — в ADR-0004). - Монотонное время и `click_id`-на-сессию делают воронку и сессии настоящими учебными объектами, а не артефактами. - Сохранение модели интенсивности минимизирует объём rewrite и риск: переписываем только модель сущностей, корректную математику интенсивности не трогаем. ## Risks and mitigations - **Стыковка иерархии с тиковой/потоковой моделью.** Иерархия естественно «батчевая» (сессия = последовательность), а генератор тиковый и стремится к `now()`. Риск снова получить время ≈ `now()` или порвать сессии между тиками. Митигейшн: в follow-up-спеке явно спроектировать, как сессия раскладывается во времени по тикам (например, активные сессии как состояние генератора). - **Правдоподобие против воспроизводимости.** `GEN_SEED` должен давать воспроизводимый поток и при иерархической модели. - **Персистентность популяции через рестарты.** Текущий state-в-Kafka хранит tick и RNG; популяция пользователей/активные сессии — новый вид состояния. Решается в follow-up-спеке (возможно, расширением `generator_state`). - **Скромное железо.** Держать rate низким по умолчанию; иерархия не должна раздувать память (ограничивать размер активной популяции/сессий). ## Validation Критерии приёмки переработанного генератора (на потоке, не на сиде): - `uniqExact(user_domain_id) < uniqExact(click_id) < count(event_id)` — пирамида не вырождена (есть возвраты и мультисобытийные сессии). - У `click_id` с несколькими событиями `event_ts` строго **возрастает**; мультисобытийные визиты составляют заметную долю потока (одностраничные визиты-отказы допустимы и правдоподобны). - Один `click_id` принадлежит ровно одному `user_domain_id`; у пользователя бывает несколько `click_id`. - Воронка по `page_url_path` затухает от шага к шагу; доля визитов, доходящих до `/confirmation`, согласована с сидом (~25%). `/confirmation` — последний шаг воронки, но визит может продолжаться и после него (как в сиде). - Поток крутится на слабом железе при дефолтном rate без деградации стенда. - `GEN_SEED` даёт воспроизводимый поток. ## Documentation impact - `generator/README.md` и `generator/KNOWN_ISSUES.md` — обновить **в момент реализации**: снять предупреждение о дефекте, описать новую модель. - `CONTEXT.md` — добавить «популяция пользователей» / «возвращающийся пользователь» (в этой итерации брейнсторма уже вносится). - `docs/course/` — добавить урок 7 «Система живёт» по `LESSON_STANDARD` (на этапе реализации); README курса — строку в таблицу уроков. - `README.md` (корень) — режим `steady-stream` уже описан; синхронизировать при реализации. ## Open questions (→ follow-up-спека по математике) > **Закрыто 2026-06-10:** все вопросы ниже решены в > [спеке математической модели](./2026-06-10-generator-math-model.md) > (распределения, ротация популяции, инвариант пауз вместо session-timeout, > активные сессии как состояние, персистентность, воспроизводимость; > `event_type` остаётся 100% `pageview`). Список сохранён для истории. - Конкретные распределения: события на сессию, сессии на пользователя за период, межсессионные интервалы. - **Вводить ли разнообразие `event_type`** (purchase/add_to_cart/click) сверх `pageview`. Сейчас и сид, и дашборд исходят из 100% `pageview` (воронка — страничная); добавление типов оживило бы выкинутые из дашборда колонки purchases/add_to_cart и требует согласованной правки урока 6/дашборда. То есть это не локальное решение генератора — взвесить в follow-up-спеке. - Вводить ли настоящий **session-timeout** (окно неактивности как граница сессии) — сейчас `CONTEXT.md` фиксирует `сессия ≡ визит ≡ click_id` без тайм-аута. - Как иерархия раскладывается во времени по тикам генератора (модель активных сессий как состояние). - Персистентность популяции пользователей и активных сессий через рестарты (расширение `generator_state`). - Воспроизводимость потока при иерархической модели и `GEN_SEED`.