diff --git a/CONTEXT.md b/CONTEXT.md index 1f5b847..7f80b4e 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -44,6 +44,25 @@ user_domain_id (пользователь, постоянный) Здоровые данные дают пирамиду `users ≤ sessions ≤ events`. +## Термины потоковой генерации (steady-stream) + +Относятся к синтетическому потоку из генератора (режим `steady-stream`), не к +статическому сиду. Решение и контекст — [ADR-0004](./docs/adr/0004-steady-stream-synthetic-generator.md), +форма доработки — [спека генератора](./docs/specs/2026-06-09-generator-rework-hierarchical.md). + +### Популяция пользователей (user population) + +Множество пользователей с **постоянными** `user_domain_id`, которые генератор +держит во времени и из которых разыгрывает активность. В отличие от сида (где +пул `user_domain_id` плоский и 1:1 с визитами), популяция — это источник +возвратов: один и тот же пользователь порождает несколько визитов. + +### Возвращающийся пользователь (returning user) + +Пользователь, открывающий **более одного** визита (`click_id`) во времени, с +межсессионными паузами. Именно возвраты дают расхождение `users < sessions` — +то, чего нет на сиде (`users == sessions`) и что отличает поток от статики. + ## Почему на демо `Unique Users == Unique Sessions` В демо-датасете (`data/*.jsonl`) каждый пользователь имеет **ровно один** diff --git a/docs/adr/0004-steady-stream-synthetic-generator.md b/docs/adr/0004-steady-stream-synthetic-generator.md new file mode 100644 index 0000000..54aad28 --- /dev/null +++ b/docs/adr/0004-steady-stream-synthetic-generator.md @@ -0,0 +1,78 @@ +# ADR-0004: Steady-stream источник — синтетический иерархический генератор, не реплей + +Принято: 2026-06-09 +Статус: accepted +Связано: [`generator/KNOWN_ISSUES.md`](../../generator/KNOWN_ISSUES.md) (диагноз +дефекта), [`CONTEXT.md`](../../CONTEXT.md), +[ADR-0002](./0002-specs-as-durable-design-docs.md) (спеки как durable design-доки), +спека [`docs/specs/2026-06-09-generator-rework-hierarchical.md`](../specs/2026-06-09-generator-rework-hierarchical.md) +(форма доработки). + +## Решение + +Режим `steady-stream` (живой поток на стенде) питается **синтетическим +генератором, переписанным с нуля по иерархической модели** «популяция +пользователей → сессии → события», а **не реплеем статического сида**. Модель +интенсивности текущего генератора (Poisson по тикам + дневной коэффициент + +jitter) сохраняется. Режим `bootstrap` (статический сид `data/*.jsonl`) и уроки +0–6 остаются прежними — генератор **сосуществует** с сидом, не заменяет его. + +## Контекст + +Проект сменил назначение на учебный стенд с курсом. Базовый курс стоит на +статическом сиде, и это осознанно: сид **честен внутри визита** (`click_id` +группирует 1..7 событий — настоящая воронка). Но у сида два потолка, которые +сид принципиально не закрывает: + +- **`users == sessions`** — каждый пользователь имеет ровно один `click_id` (1:1), + возвращающихся пользователей нет (см. `CONTEXT.md`). +- **одноразовость** — сид заливается батчем; не видно, как ClickHouse и витрины + ведут себя на непрерывном живом потоке. + +Нужен режим, в котором стенд **живёт и движется** на правдоподобных данных +(воронка + жизнеподобные колебания интенсивности), при **скромном объёме** (у +менти может не быть мощного железа — на объём/стресс не закладываемся). + +Двойная учебная ценность — ключевой критерий выбора: ценны не только *данные* +(живой стенд + честная пирамида `users < sessions < events`), но и **сам +генератор как объект изучения** — его генеративная модель достойна того, чтобы её +разбирать в курсе. + +Текущая реализация генератора не дорабатывается инкрементально: у неё +концептуальный дефект модели сущностей (свежий `click_id` на каждое событие +схлопывает иерархию — см. `generator/KNOWN_ISSUES.md`), переписываем с нуля. + +## Рассмотренные варианты + +- **A — синтетический иерархический генератор (принято).** Популяция юзеров с + постоянным `user_domain_id` → 1..N сессий (`click_id` на сессию) → 1..M + упорядоченных по времени событий с правдоподобной воронкой. Единственный + вариант, дающий возвращающихся пользователей (`users < sessions < events`) и + учебную ценность самого моделирования. Цена — самый большой объём работы и + риск ошибиться в статистической модели. +- **C — реплей честного сида на часах.** Лить реальные записи сида в Kafka во + времени, переписывая `event_timestamp` в «сейчас», зацикливая пул и модулируя + rate. **Отклонено.** Дёшев и даёт гарантированно честную воронку почти без + риска, **но**: (1) обходит ровно ту генеративно-модельную часть, ради учебной + ценности которой всё и затевается; (2) наследует вырождение `users == sessions` + из сида — полную пирамиду не даёт никогда; (3) «бесконечность» = зацикленный + конечный пул. +- **B — минимальный «живой» генератор с грубой воронкой.** Тот же rewrite, но + воронка на фиксированных вероятностях, без глубины. Отклонено как + половинчатое: числа воронки менее убедительны, а вопрос возвратов всё равно + надо решать — то есть основной сложности не избегает. + +## Последствия + +- Режим `steady-stream` фиксируется как синтетическая генерация; направление + переоткрывать не нужно (типовой вопрос «почему не реплей?» закрыт здесь). +- `bootstrap`-сид и уроки 0–6 не трогаем; живой поток подаётся отдельным уроком 7. +- Контракт данных потока: здоровая пирамида `users < sessions < events`, + монотонное время внутри сессии, `click_id` переиспользуется внутри сессии. + Этот контракт наследуют будущие артефакты (спека доработки, урок 7, возможные + правки витрин). +- Детальная архитектура/требования — в спеке `2026-06-09-generator-rework-hierarchical.md`. +- **Статистическая модель** (распределения событий/сессия, сессий/пользователь, + межсессионные паузы; нужен ли настоящий session-timeout; персистентность + популяции через рестарты) **выносится в отдельную follow-up-спеку** и здесь + намеренно не фиксируется. diff --git a/docs/specs/2026-06-09-generator-rework-hierarchical.md b/docs/specs/2026-06-09-generator-rework-hierarchical.md new file mode 100644 index 0000000..7e3d4f3 --- /dev/null +++ b/docs/specs/2026-06-09-generator-rework-hierarchical.md @@ -0,0 +1,173 @@ +# Переработка генератора: иерархическая модель сущностей для 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`), где до терминального шага доходит + правдоподобно малая доля — согласованно с дашбордом и сидом. +- Сохранена модель интенсивности (жизнеподобные колебания нагрузки). +- Встраивается в курс как **урок 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`), где до + терминального шага доходит малая доля. (Вводить ли разнообразие `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` + правдоподобно мала), терминальный шаг — `/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-спека по математике) + +- Конкретные распределения: события на сессию, сессии на пользователя за период, + межсессионные интервалы. +- **Вводить ли разнообразие `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`.