- Зачем:
- закрыть Open questions спеки формы доработки перед передачей на
реализацию; адверсариальное ревью показало, что прежние ориентиры
(популяция/паузы/интенсивность) взаимно несовместимы, а документы
опираются на неверный факт о сиде («1..7 событий на визит»).
- Что:
- добавлена docs/specs/2026-06-10-generator-math-model.md: марковская
цепочка по страницам, формула связи «популяция-интенсивность-пауза»
(λ по умолчанию 30/мин), кулдаун возврата, правило 30 минут на рестарт,
критерии приёмки.
- в CONTEXT.md добавлен профиль сид-датасета (полный замер: длины визитов
1..27, медиана 10, конверсия 25%, петли и события после /confirmation)
и исправлено ложное «разброс времени внутри click_id <= 1 мин».
- исправлен факт «1..7 событий» в KNOWN_ISSUES.md и ADR-0004; критерии
Validation спеки формы доработки приведены к фактам сида.
- Проверка:
- перекрёстные ссылки между спеками/ADR/CONTEXT.md открываются; цифры
профиля сида воспроизводятся скриптом подсчёта по полным data/*.jsonl.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
185 lines
16 KiB
Markdown
185 lines
16 KiB
Markdown
# Переработка генератора: иерархическая модель сущностей для 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`.
|