diff --git a/CONTEXT.md b/CONTEXT.md index 081af02..cf7278a 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -12,11 +12,26 @@ _Избегать_: склад, склад данных **Состав мира**: -Постоянная часть мира генератора — популяция посетителей с календарём их -появления, привычки, календарь двухкуковых пар. Чистая функция зерна: -вычисляется при старте любого процесса, между прогонами не хранится. +Постоянная часть мира генератора — популяция посетителей, их привычки, +двухкуковые пары. По дням его выдаёт план состава. _Избегать_: состояние мира +**План состава**: +Способ спросить состав мира: функция зерна, выдающая его по дням — +когорту новых кук, их возвраты, назначенные заказы двухкуковых пар. + +**Приток**: +Появление новых кук на всём протяжении оси модельного времени; единица — +кука (`ClientID`). Из-за притока накопленная аудитория растёт с +горизонтом и не совпадает с дневной. + +**Хвост возвратов**: +Окно активности куки, отсчитанное от её первого визита; дольше окна +кука не возвращается. + +**Предыстория**: +Когорты плана с первым визитом до D0; событий не порождают. + **Ось модельного времени**: Собственный календарь мира генератора. Дни считаются от фиксированного первого дня D0; реальный календарь в модели не участвует. Между прогонами diff --git a/docs/specs/2026-08-01-generator.md b/docs/specs/2026-08-01-generator.md index 66367f6..18a2c74 100644 --- a/docs/specs/2026-08-01-generator.md +++ b/docs/specs/2026-08-01-generator.md @@ -1,6 +1,6 @@ # Генератор (этап 2): функциональный мир, детерминизм до байта, контракт схемы, числа скорости -Статус: Proposed — ждёт приёмки владельцем (тикет #33). +Статус: Accepted — принята владельцем 2026-08-01 (PR #35). Дата: 2026-08-01. Мандат — тикет #14 (этап 2, родитель #4), карта #26. Развилки: модель мира (#27), детерминизм от зерна (#28), архитектура вывода (#29), производительность (#30); исследование скорости (#31). @@ -46,25 +46,55 @@ - **Мир функциональный, ничего не мутирует.** Постоянный состав мира — популяция посетителей, их привычки, календарь двухкуковых пар — чистая - функция зерна, вычисляется при старте любого процесса. День D — функция - (зерно, D); межднёвные связи (окно заказов K, опоздания) выводятся из - плана состава, а не копятся в состоянии. Глобальные инварианты («каждый - двухкуковый покупатель заказал с обеих кук») гарантируются планом — - счётчики манифеста известны до генерации событий. + функция зерна; целиком не вычисляется и не хранится, спрашивается по + дням (форма плана — ниже). День D — функция (зерно, D); межднёвные + связи (окно заказов K, опоздания) выводятся из плана состава, а не + копятся в состоянии. Глобальные инварианты («каждый двухкуковый + покупатель заказал с обеих кук») гарантируются планом; счётчики + состава — посетители, пары, приток — известны до генерации событий, + торговые счётчики сложатся, когда торговое поведение определит #40. - **Состав не замкнут: посетители появляются и затухают.** План состава задаёт календарь появления — у каждого посетителя есть дата первого визита и профиль возвратов, включая затухание: заметная доля кук одноразовая, как в живом трафике. Новые посетители появляются на всём протяжении оси: uniq(ClientID) растёт с горизонтом, дневная и накопленная аудитории не сходятся в одно число. Приток — часть плана, а не мутация: - счётчики манифеста по-прежнему известны до генерации (уточнение по + счётчики состава по-прежнему известны до генерации (уточнение по вычитке владельца, 2026-08-01); его числа — раздел 9. -- **Своя ось модельного времени.** Мир рождается в фиксированный день D0 - (понедельник — см. раздел 5); реальный календарь в модели не участвует. - В `EventDate`/`UTCEventTime` дни оси ложатся конкретными датами, но это - константа мира, от даты запуска не зависящая (значение — раздел 9). - Между прогонами живут только зерно и позиция на оси: выключенный ноутбук — - мир замер, потом продолжил. +- **Форма плана — ленивая, по когортам дня** (уточнение при исполнении + #38, 2026-08-02). Глобальный список посетителей не строится: когорта + дня D — функция (зерно, D), её случайность — подпоток состава мира, + ветвящийся по номеру дня (позиция в дереве — раздел 2), отдельный от + подпотока дня-функции. Аудитория дня — когорта D плюс возвраты когорт + последних дней: окно активности куки — хвост возвратов (константа + мира — раздел 9), отсчитанный от её первого визита; за краем окна кука + не возвращается, а профиль возвратов затухает к краю, поэтому обрыв в + данных не виден. Стоимость дня не зависит от прожитого — день 500 + стоит как день 5, горизонт не является входом. Счётчики состава + считаются прогоном плана по дням, без генерации событий. +- **Предыстория: полка с первого дня.** Когорты существуют и до D0 — на + глубину хвоста возвратов. Событий они не порождают (ось событий + начинается в D0) — только дают, кому возвращаться в первые дни: дневная + аудитория на полке с самого D0, разгона «пустого магазина» нет. +- **Гарантия двухкуковых пар — назначенные заказы в плане.** Единица + здесь — человек, не кука: план помечает часть людей покупателями + (доля — раздел 9), и 15% покупателей (мастер-спека, раздел 5) получают + вторую куку. Обе куки пары принадлежат одному человеку одной когорты; + вторая рождается в пределах его окна активности, без фиксированного + зазора. Каждой паре план назначает дни гарантированных заказов: по + одному на куку, из дней визитов этой куки, на оси от D0 и позже; + человеку предыстории, чьё окно активности таких дней не оставляет, + пара не назначается. День-функция обязана назначенные заказы + реализовать; остальные покупки — вольные, их решает генератор торговых + событий (#40). Манифест считает пары, реализованные в горизонте + снимка. Условность в данных не видна: дни назначены той же + случайностью, просто брошенной планом один раз. +- **Своя ось модельного времени.** Ось событий начинается в + фиксированный день D0 (понедельник — см. раздел 5); реальный + календарь в модели не участвует. В `EventDate`/`UTCEventTime` дни оси + ложатся конкретными датами, но это константа мира, от даты запуска не + зависящая (значение — раздел 9). Между прогонами живут только зерно и + позиция на оси: выключенный ноутбук — мир замер, потом продолжил. - **Два режима движения по одной оси.** Пошаговый — базовый для лаб: старт с эталонного снимка, дальше «прожить следующий день» — явное действие. Живой день — текущий день проигрывается с ускорением, дашборд и мониторинг @@ -89,6 +119,27 @@ даты эталонного мира зависимыми от даты запуска, манифест теряет воспроизводимость. +Отклонено при исполнении #38 (2026-08-02): + +- *Разгон вместо предыстории* («первые дни малы — магазин запустился»): + зерновой мир и половина снимка оказались бы на разгоне, недельная лаба + сравнивала бы несравнимые недели, «средний день ~50 тыс.» перестал бы + быть средним — пришлось бы двигать принятые числа раздела 5. +- *Материализованный план на горизонт*: горизонт становится обязательным + входом каждого запуска, стоимость старта растёт с прожитым; префиксную + устойчивость даёт и ленивая форма — даром, через подпотоки по номеру дня. +- *Замкнутый пул кук со сменой поколений*: постоянная память ценой фальшивой + константы — потолка одновременно живущих кук, которого в жизни нет и + который менти нечем объяснить. +- *Вероятностная гарантия пар* («почти наверняка купит с обеих кук»): не + гарантия — однажды манифест покраснеет, а чинить нечем, кроме смены + зерна; при этом несклеенная пара в данных неотличима от двух незнакомцев, + так что реализм этой лотереи невидим. +- *Покупка пары в первый визит куки*: гарантия железная и дешёвая, но узор + «все пары покупают в первый день» виден в данных ровно там, куда лаба + склейки смотрит пристальнее всего. Условность допустима, пока она не + видна в данных. + ## 2. Детерминизм от зерна Резолюция развилки [«Детерминизм от зерна»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/28). @@ -105,7 +156,9 @@ тоже; единственная переменная — архитектура CPU. Истина — CI на Linux; сходимость любой машины проверяет скрипт «пересгенерируй день N — сравни хеш с манифестом». Расхождение на любой платформе — баг генератора, а не допуск. -- **Раздача зерна — иерархией подпотоков.** Корневое зерно → состав мира; +- **Раздача зерна — иерархией подпотоков.** Корневое зерно → подпоток + состава мира, ветвящийся по номеру дня на когорты плана (состав + спрашивается по дням — раздел 1; уточнение при исполнении #38); (зерно, день) → подпоток дня → именованные подпотоки компонентов: трафик, торговые события, расхождения, опоздания — в фиксированном порядке. По построению: параллельный прогон равен последовательному; продление @@ -334,18 +387,52 @@ pytest-тест с маркером `perf` и таймаутом-обрубан - **Будущие лабы**: перезаливка дня X пакетным режимом проигрывателя — готовая демонстрация идемпотентности конвейера. -## 9. Решается при нарезке этапа 2 (#34) +## 9. Вопросы нарезки этапа 2 (#34): решения и остатки -Осталось из тумана карты — вопросы уровня тикетов, не развилок: +Туман карты разложен нарезкой по тикетам; решения фиксируются здесь по +мере исполнения. + +Решено при исполнении #38 (2026-08-02): + +- **Числа притока и состава.** Приток — ~3 800 новых кук в средний день, + модулируется тем же недельным профилем, что трафик (иначе доля + новичков скакала бы по дням недели). Доля одноразовых кук — 75%; + возвращающиеся — в среднем 3–4 возврата, профиль убывающий: почти все + в первые 7–10 дней, тонкий хвост поздних возвратов и повторных + покупок — до края окна (цикл повторной покупки магазина — месяцы). + Хвост возвратов — окно активности куки от первого визита — и глубина + предыстории: 90 дней (решение владельца 2026-08-02: дольше квартала + стенд никто не гоняет, а заказы старых посетителей продолжаются весь + прогон; плата — чуть меньше пар, полностью реализованных внутри + 14-дневного снимка). Покупатели считаются людьми, + не куками; доля покупателей — 5% людей когорты, а людей в когорте + почти столько же, сколько кук: вторые куки пар добавляют меньше + процента. Доля — число плана, а не торгового тикета: без него не + отобрать двухкуковые пары; #40 наследует его, не переоткрывая. + Сходимость с разделом 5: средняя кука активна ≈1,9 дня → дневная + аудитория ≈7 100 — середина вилки 6–8 тыс.; уникумов за 14 дней + снимка ≈60 тыс. — ≈53 тыс. новыми куками плюс возвраты предыстории + (не путать с ~50 тыс. событий одного дня) — рост `uniq(ClientID)` с + горизонтом виден сразу; новых покупателей ~190 в день → конверсия + ~2% на сессию. +- **D0 = 2026-06-01, понедельник** — решение владельца при нарезке + (2026-08-01). Дата недавняя, чтобы данные первые месяцы выглядели + свежими; привязки к реальному календарю у констант мира всё равно нет. +- **Конфигурация мира — модуль чистых данных** рядом с контрактом схемы: + все числа мира в одном месте, написанном как приглашение любопытному + менти крутить. Правка модуля — смена мира: чек манифеста честно + краснеет, манифест сторожит только канон. Вне модуля — лишь то, что + мира не меняет: своё зерно и транспортные флаги проигрывателя. + Отклонено: внешний конфиг и env-переопределения — переменная мира, + которую паспорт манифеста не видит; файл-конфиг в репозитории — по + смыслу равен модулю, но платит загрузчиком и валидацией (довод + раздела 3 против YAML). + +Остаётся открытым, за тикетами: - интерфейс запуска генератора (CLI / цели make) и как он делит режимы проигрывателя; кто его зовёт в стенде — даги `world_init`/`next_day` из - оценки мастер-спеки (раздел 9) — и в каком контейнере он живёт; -- числа притока посетителей: доля одноразовых кук и темп появления новых - (раздел 1); -- календарная дата-константа D0: каким числом дни оси ложатся в - `EventDate`/`UTCEventTime`; -- формат описания мира и конфигурации (что константа кода, что параметр); + оценки мастер-спеки (раздел 9) — и в каком контейнере он живёт (#41); - как фиксируется «зерновой» мир конца этапа 2 (раздел 9 мастер-спеки): - с манифестным решением напрашивается мини-манифест зернового мира — форму - выбрать при нарезке. + с манифестным решением напрашивается мини-манифест зернового мира — + форма за #42.