docs(generator): уточнена мат-спека по итогам ревью, handoff для передачи

- Зачем:
  - повторное адверсариальное ревью нашло ошибку в формуле межсессионной
    паузы (двойной счёт кулдауна) и незакрытый контракт публикации
    device/geo; дизайн-этап завершён, работа передаётся исполнителю.
- Что:
  - исправлена формула паузы (баланс цикла, минус длительность визита);
    зафиксированы каденция device/geo «на каждое событие, как в сиде»,
    правило выбора возвращающегося, стартовое распределение страниц,
    калибровка по медиане и среднему, инвариант потолков конфигурации,
    компактное хранение профиля ссылкой на сид-сессию.
  - уточнены критерии приёмки (среднее паузы вместо медианы, счёт шага
    «товары», имена полей времени) в обеих спеках.
  - добавлен handoff .scratch/handoffs/2026-06-10-generator-spec-to-codex.md
    (передача на детальный план/реализацию), отработавший handoff от
    2026-06-09 удалён.
- Проверка:
  - цифры спеки сверены замерами по полным data/*.jsonl (стартовые страницы
    58/24/17, шаги воронки 98>=94>=56>=35>=25, device 1000 строк / 99
    уникальных); повторное ревью свежим агентом блокеров не оставило.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dmitry Dementiev
2026-06-10 18:02:57 +03:00
co-authored by Claude Fable 5
parent 0b5fc64f6f
commit bcca8f5127
4 changed files with 123 additions and 81 deletions
@@ -138,9 +138,10 @@
- `uniqExact(user_domain_id) < uniqExact(click_id) < count(event_id)`
пирамида не вырождена (есть возвраты и мультисобытийные сессии).
- У `click_id` с несколькими событиями `event_ts` строго **возрастает**;
мультисобытийные визиты составляют заметную долю потока (одностраничные
визиты-отказы допустимы и правдоподобны).
- У `click_id` с несколькими событиями время строго **возрастает**
(`event_timestamp` в источнике, `event_ts` в DDS — проверять можно в любой
точке); мультисобытийные визиты составляют заметную долю потока
(одностраничные визиты-отказы допустимы и правдоподобны).
- Один `click_id` принадлежит ровно одному `user_domain_id`; у пользователя
бывает несколько `click_id`.
- Воронка по `page_url_path` затухает от шага к шагу; доля визитов, доходящих
+54 -26
View File
@@ -44,7 +44,10 @@
3. **Популяция: постоянное ядро + медленная ротация.** Размер активной
популяции ограничен (память не растёт), но новые пользователи понемногу
приходят, давно неактивные — выбывают. Кумулятивное число уникальных
пользователей растёт со временем — стенд выглядит живым.
пользователей растёт со временем — стенд выглядит живым. «Медленно» — в
масштабе наблюдения: на ориентирах по умолчанию ядро полностью сменяется
примерно за 11 часов (это осознанный баланс: возвраты видны за вечер,
рост uniques — за сутки).
4. **Путь по страницам — марковская цепочка.** Для каждой страницы задана
вероятность перехода на каждую другую страницу или ухода с сайта. Один
механизм даёт сразу: длину визита, затухающую воронку, петли (возвраты на
@@ -73,12 +76,19 @@
device/geo-контекст одной случайной сид-сессии (включая `user_custom_id`;
повторы email между пользователями допустимы — демо-данные) с заменой
`user_domain_id` на свежий. Профиль используется во всех визитах пользователя
(KISS: смену устройства не моделируем).
(KISS: смену устройства не моделируем). В состоянии профиль хранится
**ссылкой на сид-сессию** (её `click_id`) плюс свежие идентификаторы, а не
копией всех полей — состояние остаётся компактным.
### Визиты (сессии)
- Визит = новый `click_id`, общий для всех его событий, с device/geo из профиля
пользователя.
- **Каденция публикации device/geo — как в сиде и у текущего генератора:**
записи `device_events` и `geo_events` публикуются **на каждое событие**
(в пределах визита дублируются с одинаковым содержимым; в сиде это
проверено: 1000 строк при 99 уникальных по содержимому). Контракт данных с
ETL/ODS не меняется — это требование, а не деталь реализации.
- **Связка с моделью интенсивности.** `_calculate_events_count()` сохраняется и
выдаёт на тик *бюджет событий*. Бюджет конвертируется в рождения визитов:
ожидаемое число новых визитов за тик = бюджет ÷ средняя длина визита
@@ -92,35 +102,47 @@
- **Кулдаун возврата:** после завершения визита пользователь недоступен для
нового минимум `GEN_MIN_RETURN_MINUTES` (ориентир 30 мин) — это гарантирует
нижнюю границу межсессионной паузы (инвариант пауз).
- **Выбор возвращающегося:** равновероятно среди **доступных** пользователей
(не в кулдауне и без активного визита) — это даёт естественный разброс пауз.
Если доступных нет (краевой случай при экстремальных параметрах) — визит
достаётся новому пользователю.
- **Межсессионные паузы — эмерджентная величина, не свободный параметр.**
Пауза складывается из кулдауна и времени ожидания «своей очереди» и в среднем
определяется балансом потока и популяции:
В стационарном режиме полный цикл пользователя (визит + пауза) определяется
балансом потока и популяции — кулдаун уже «сидит» внутри этого баланса и
отдельно не прибавляется (он задаёт только нижнюю границу каждой паузы):
```
средняя пауза ≈ кулдаун + GEN_POPULATION_MAX ÷ (λ ÷ L × (1 GEN_P_NEW_USER))
средняя пауза ≈ GEN_POPULATION_MAX ÷ (λ ÷ L × (1 GEN_P_NEW_USER)) − средняя длительность визита
где λ — событий/мин, L — средняя длина визита
```
На ориентирах по умолчанию (λ=30, L≈10, популяция 300, p_new=0.15):
возвратов ≈ 2.6/мин, пауза ≈ **~2 часа** — больше 30-минутного окна и при
этом возвраты видны уже за один вечер работы стенда. **Кто меняет λ или
размер популяции — обязан пересчитать паузу по формуле**: эти три величины
связаны, их нельзя крутить независимо.
возвратов ≈ 2.6/мин, цикл ≈ 118 мин, средняя пауза ≈ **~110 мин (~2 часа)**;
медиана ниже среднего (распределение скошено вправо), порядка 1.5 ч — всё
равно сильно больше 30-минутного окна, и возвраты видны уже за один вечер
работы стенда. **Кто меняет λ или размер популяции — обязан пересчитать
паузу по формуле**: эти величины связаны, их нельзя крутить независимо.
Ориентиры здесь посчитаны при L=10; после калибровки таблицы переходов
производные числа пересчитываются от измеренного L.
### События и путь по страницам
- **Марковская цепочка:** страницы — `/home`, `/product_a`, `/product_b`,
`/cart`, `/payment`, `/confirmation` плюс исход «ушёл». Визит начинается с
входной страницы (как правило `/home`), дальше каждый следующий шаг
разыгрывается по таблице переходов текущей страницы. Петли разрешены
(вернуться на главную, посмотреть оба товара, продолжить ходить после
`/confirmation` — как в сиде). Защита от зацикливания — потолок длины визита
`GEN_MAX_SESSION_EVENTS` (ориентир 30, как максимум в сиде).
- **Калибровка по сиду:** конкретные значения таблицы подбирает исполнитель под
два целевых показателя из профиля сида (см. `CONTEXT.md`): доля визитов,
достигших `/confirmation`, ≈ 25%, и длина визита с медианой ~10 событий.
Точного совпадения распределений не требуется — требуется сопоставимость,
чтобы дашборд на потоке показывал привычные по сиду цифры.
`/cart`, `/payment`, `/confirmation` плюс исход «ушёл». Визит начинается со
страницы, разыгранной по **стартовому распределению** (оно — часть модели:
в сиде `/home` — лишь ~59% входов, остальные входят на страницы товаров),
дальше каждый следующий шаг разыгрывается по таблице переходов текущей
страницы. Петли разрешены (вернуться на главную, посмотреть оба товара,
продолжить ходить после `/confirmation` — как в сиде). Защита от
зацикливания — потолок длины визита `GEN_MAX_SESSION_EVENTS` (ориентир 30,
как максимум в сиде).
- **Калибровка по сиду:** стартовое распределение и значения таблицы подбирает
исполнитель под целевые показатели из профиля сида (см. `CONTEXT.md`): доля
визитов, достигших `/confirmation`, ≈ 25%, длина визита с медианой ~10 и
**средним ~10** (в сиде среднее ≈ медиане; у цепочки с геометрическим
хвостом среднее легко уезжает выше — следить за обоими). Точного совпадения
распределений не требуется — требуется сопоставимость, чтобы дашборд на
потоке показывал привычные по сиду цифры.
- **Паузы внутри визита:** интервал между соседними событиями — величина
масштаба *секунд—минут* (например, лог-нормальное: большинство пауз короткие,
изредка длинные). Ориентир: медиана десятки секунд. Форма распределения и
@@ -152,7 +174,10 @@
Число одновременно активных визитов ограничено (`GEN_MAX_ACTIVE_SESSIONS`) —
защита памяти; при достижении потолка новые рождения в этот тик пропускаются
(бюджет не копится). На ориентирах по умолчанию стационарно активны ~15–30
визитов — потолок с большим запасом.
визитов — потолок с большим запасом. **Инвариант конфигурации:**
`GEN_MAX_ACTIVE_SESSIONS < GEN_POPULATION_MAX` — иначе правило вытеснения
(«без активного визита») может не найти кандидата; валидировать при старте,
как существующие проверки `Config`.
## Персистентность через рестарты
@@ -212,15 +237,16 @@
- Средняя интенсивность событий за длинное окно (час и больше) соответствует
целевой `λ × часовой коэффициент` с разумным отклонением.
- Паузы внутри `click_id`: 95-й перцентиль — единицы минут, максимум < 30 минут.
- Межсессионные паузы одного пользователя: минимум ≥ кулдауна, медиана
порядка расчётной по формуле (~2 ч на дефолтах).
- Межсессионные паузы одного пользователя: минимум ≥ кулдауна, **среднее**
порядка расчётного по формуле (~2 ч на дефолтах; медиана ниже, ~1.5 ч).
- Доля визитов новых пользователей за длинное окно ≈ `GEN_P_NEW_USER`;
кумулятивное число уникальных `user_domain_id` растёт со временем, размер
состояния генератора — нет.
- Длина визита (медиана ~10, максимум ≤ потолка) и доля дошедших до
`/confirmation` (~25%) сопоставимы с профилем сида из `CONTEXT.md`.
- Воронка затухает по шагам: число визитов, посетивших страницу, монотонно
убывает вдоль `/home → товары → /cart → /payment → /confirmation`.
- Воронка затухает по шагам: число визитов, посетивших шаг, монотонно убывает
вдоль `/home → товары → /cart → /payment → /confirmation` (шаг «товары» —
визит посетил хотя бы одну из страниц товаров; так же считает дашборд).
- После рестарта с простоем ≤ 30 минут активные визиты продолжаются (нет
скачка «все пользователи новые»); после долгого простоя закрываются только
просроченные визиты, популяция сохраняется.
@@ -236,6 +262,8 @@
на визит» (по полному замеру — 1..27, медиана 10).
- Урок 7 «Система живёт» (пишется на этапе реализации): марковская таблица
переходов — учебная «ручка» урока (поменяй вероятность → увидь сдвиг
конверсии на дашборде).
конверсии на дашборде). Оговорка скоупа: «увидеть на дашборде» предполагает,
что поток доезжает до витрин — это зависимость урока 7 от инкрементальной
загрузки ETL (план v2), а не требование к генератору.
- `generator/README.md` — обновляется при реализации; туда же — верхнеуровневый
обзор «как работает генератор» (два контура: интенсивность и сущности).