From bcca8f5127767cff8fa03220e94950702db674eb Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Wed, 10 Jun 2026 18:02:57 +0300 Subject: [PATCH] =?UTF-8?q?docs(generator):=20=D1=83=D1=82=D0=BE=D1=87?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B0=20=D0=BC=D0=B0=D1=82-=D1=81=D0=BF?= =?UTF-8?q?=D0=B5=D0=BA=D0=B0=20=D0=BF=D0=BE=20=D0=B8=D1=82=D0=BE=D0=B3?= =?UTF-8?q?=D0=B0=D0=BC=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E,=20handoff=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B0=D1=87?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - повторное адверсариальное ревью нашло ошибку в формуле межсессионной паузы (двойной счёт кулдауна) и незакрытый контракт публикации 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 --- .../handoffs/2026-06-09-generator-rework.md | 52 ------------ .../2026-06-10-generator-spec-to-codex.md | 65 +++++++++++++++ ...026-06-09-generator-rework-hierarchical.md | 7 +- docs/specs/2026-06-10-generator-math-model.md | 80 +++++++++++++------ 4 files changed, 123 insertions(+), 81 deletions(-) delete mode 100644 .scratch/handoffs/2026-06-09-generator-rework.md create mode 100644 .scratch/handoffs/2026-06-10-generator-spec-to-codex.md diff --git a/.scratch/handoffs/2026-06-09-generator-rework.md b/.scratch/handoffs/2026-06-09-generator-rework.md deleted file mode 100644 index 4df6ec5..0000000 --- a/.scratch/handoffs/2026-06-09-generator-rework.md +++ /dev/null @@ -1,52 +0,0 @@ -# Handoff: переработка генератора (steady-stream, иерархическая модель) - -Дата: 2026-06-09 -Ветка: `feature/data-generator` -Жанр: одноразовые леса́ (ADR-0003) — durable-рассуждение в ADR/спеке, не здесь. - -> Место: `.scratch/handoffs/` по [ADR-0003](../../docs/adr/0003-handoffs-in-scratch.md) -> и `AGENTS.md` (перекрывают generic-дефолт скилла «temp dir» — у владельца -> мультимашинность + worktrees, где `/tmp` не переживает). - -## Где остановились - -Брейнсторм-сессия (`brainstorm-with-docs`) завершена **на этапе документов** — -кода НЕ трогали. Зафиксировано направление переработки генератора и расчищена -рамка под реализацию. Дальше — отдельная follow-up-спека по математике, затем -реализация. - -## Контекст и решения (ссылки, не пересказ) - -- **Решение «почему» (генератор A, не реплей C):** - [`docs/adr/0004-steady-stream-synthetic-generator.md`](../../docs/adr/0004-steady-stream-synthetic-generator.md) -- **Форма доработки «что строим» (требования, без математики), статус Draft:** - [`docs/specs/2026-06-09-generator-rework-hierarchical.md`](../../docs/specs/2026-06-09-generator-rework-hierarchical.md) - — там же раздел **Open questions** (список вопросов для мат-спеки) и - **Validation** (критерии приёмки). Не дублирую сюда. -- **Глоссарий:** `CONTEXT.md` — добавлены «популяция пользователей» / - «возвращающийся пользователь». -- **Диагноз дефекта текущей реализации:** `generator/KNOWN_ISSUES.md`. - -## Следующий шаг - -**Написать follow-up-спеку по математике** генеративной модели. Перечень вопросов -— в разделе *Open questions* спеки доработки (распределения, session-timeout, -раскладка иерархии по тикам, персистентность популяции, `event_type` diversity). - -## Ловушки (одной строкой; детали — в спеке) - -- Воронка живёт в **`page_url_path`**, не в `event_type` (в сиде 100% `pageview`). -- device/geo — **сессионный** контекст уровня `click`, не «свойство пользователя». -- Модель интенсивности (`_calculate_events_count`, `_hour_factor`) — **сохраняем**; - переписываем только модель сущностей (`generate_batch`). Текущую реализацию - инкрементально не чиним. - -## Suggested skills (для следующей сессии) - -- **`conventional-commits`** — закоммитить готовые доки (см. git ниже). -- **`brainstorm-with-docs`** (или `grill-with-docs`) — для follow-up-спеки по - математике: продолжает тот же стиль (durable-доки + правка CONTEXT/ADR inline). -- **`tdd`** — на этапе реализации rewrite (у генератора уже есть pytest-набор - `generator/tests/`). -- **`adversarial-review`** / **`code-review`** — ревью переписанной модели - сущностей перед вливанием ветки. diff --git a/.scratch/handoffs/2026-06-10-generator-spec-to-codex.md b/.scratch/handoffs/2026-06-10-generator-spec-to-codex.md new file mode 100644 index 0000000..97b3bc5 --- /dev/null +++ b/.scratch/handoffs/2026-06-10-generator-spec-to-codex.md @@ -0,0 +1,65 @@ +# Handoff: спеки генератора готовы — передача на детальный план и реализацию + +Дата: 2026-06-10 +Ветка: `feature/data-generator` +Жанр: одноразовые леса́ (ADR-0003) — durable-рассуждение в спеках/ADR/CONTEXT, не здесь. + +> Место: `.scratch/handoffs/` по [ADR-0003](../../docs/adr/0003-handoffs-in-scratch.md) +> (перекрывает generic-дефолт скилла «temp dir»: мультимашинность + worktrees). +> Предыдущий handoff (2026-06-09-generator-rework.md) отработал и удалён. + +## Где остановились + +Дизайн-этап переработки генератора **завершён**. Спека математической модели +написана, прошла **два** адверсариальных ревью свежими агентами (второе ревью +ловило ошибки правок первого — практика себя оправдала), все находки закрыты, +ключевые цифры перепроверены замерами по полному сиду. Кода по-прежнему +не трогали. + +Разделение ролей (см. память проекта): дизайн/спеки — Fable, **детальный план +и реализация — Codex 5.5**. Следующая сессия — скорее всего, подготовка задачи +для Кодекса или сама реализация. + +## Документы (источники истины, не пересказываю) + +- **Мат-модель (главный документ для исполнителя):** + `docs/specs/2026-06-10-generator-math-model.md` — марковская цепочка по + страницам, формула «популяция ↔ интенсивность ↔ пауза», кулдаун, правило + 30 минут на рестарт, критерии приёмки. +- **Форма доработки:** `docs/specs/2026-06-09-generator-rework-hierarchical.md` + (Open questions закрыты ссылкой на мат-спеку). +- **Почему генератор, а не реплей:** `docs/adr/0004-steady-stream-synthetic-generator.md`. +- **Профиль сид-датасета (опора калибровки):** `CONTEXT.md`, раздел + «Профиль сид-датасета» — измерено по полным файлам 2026-06-10. +- **Диагноз дефекта старого кода:** `generator/KNOWN_ISSUES.md`. + +## Ловушки (одной строкой; детали — в спеках) + +- Ранний «факт» **«1..7 событий на визит» был неверен** (срез файла); реально + 1..27, медиана 10. Если встретишь «1..7» где-то ещё в доках/коде — это + остатки ошибки, чинить по профилю сида. +- **Формула паузы в мат-спеке обязательна** при смене λ/популяции; кулдаун в + неё не прибавляется (уже учтён балансом). Вторая ревизия исправляла именно + двойной счёт — не откатить случайно. +- **device/geo публикуются на каждое событие** (как в сиде) — это контракт с + ETL, не деталь. +- `event_timestamp` событий — **запланированное** время, не момент отправки + тика (иначе метки прилипают к сетке тиков). +- Модель интенсивности (`_calculate_events_count`) сохраняем; дефолт + `GEN_LAMBDA_BASE_PER_MIN` меняется 200 → 30. + +## Следующий шаг + +Передать пару спек Кодексу: детальный план реализации → код. Уровень спек +сознательно «решения и инварианты, без алгоритмов» — конкретику исполнитель +достраивает сам. Возможная подготовка: оформить задачу в `.scratch//` +по `docs/agents/issue-tracker.md` (скилл `to-issues`, если план дробить). + +## Suggested skills (для следующей сессии) + +- **`to-issues`** — если решим дробить реализацию на задачи в локальном трекере. +- **`tdd`** — для этапа реализации (pytest-набор `generator/tests/` существует, + но писался под старую модель — пересмотр под новую неизбежен). +- **`adversarial-review`** / **`code-review`** — ревью реализации против + мат-спеки перед вливанием. +- **`conventional-commits`** — коммиты по правилам репозитория. diff --git a/docs/specs/2026-06-09-generator-rework-hierarchical.md b/docs/specs/2026-06-09-generator-rework-hierarchical.md index a248df2..629325b 100644 --- a/docs/specs/2026-06-09-generator-rework-hierarchical.md +++ b/docs/specs/2026-06-09-generator-rework-hierarchical.md @@ -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` затухает от шага к шагу; доля визитов, доходящих diff --git a/docs/specs/2026-06-10-generator-math-model.md b/docs/specs/2026-06-10-generator-math-model.md index a0a7091..c4d5468 100644 --- a/docs/specs/2026-06-10-generator-math-model.md +++ b/docs/specs/2026-06-10-generator-math-model.md @@ -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` — обновляется при реализации; туда же — верхнеуровневый обзор «как работает генератор» (два контура: интенсивность и сущности).