- Зачем: - тикет #38 требует решить и внести в спеку генератора числа притока, D0 и формат конфигурации мира до реализации плана состава. - Что: - раздел 1 спеки: ленивый план по когортам дня, предыстория с полкой от D0, гарантия двухкуковых пар назначенными заказами (единица — человек), пять отклонённых вариантов с доводами. - раздел 9: приток 3 800 кук/день, окно активности 90 дней (решение владельца), доля покупателей 5% людей, D0 = 2026-06-01, конфигурация мира — модуль чистых данных; шапка Proposed → Accepted. - CONTEXT.md: термины «план состава», «приток», «хвост возвратов», «предыстория»; «состав мира» уточнён, словарь очищен от решений. - Проверка: - двойное слепое ревью правок (Codex + Claude), все 20 находок закрыты; арифметика чисел пересчитана ревьюерами независимо. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
439 lines
41 KiB
Markdown
439 lines
41 KiB
Markdown
# Генератор (этап 2): функциональный мир, детерминизм до байта, контракт схемы, числа скорости
|
||
|
||
Статус: Accepted — принята владельцем 2026-08-01 (PR #35).
|
||
Дата: 2026-08-01. Мандат — тикет #14 (этап 2, родитель #4), карта #26.
|
||
Развилки: модель мира (#27), детерминизм от зерна (#28), архитектура вывода
|
||
(#29), производительность (#30); исследование скорости (#31).
|
||
Источники: мастер-спека
|
||
[«Боевой реализм стенда (v2)»](2026-07-30-stand-v2-realism.md) — разделы 1.4,
|
||
5, 9–11; заметка
|
||
[«Скорость батчевой генерации в Python»](../research/2026-08-01-python-batch-generation-speed.md).
|
||
|
||
## Зачем
|
||
|
||
Мастер-спека решила, **что** генерирует стенд: широкое событие в 47 колонок,
|
||
таксономию, анонимность, двухкуковых покупателей, заказы слепками. Четыре
|
||
развилки о том, **как** генератор устроен, она отложила: модель мира,
|
||
границы детерминизма, источник истины схемы с разделением потока и пакета,
|
||
числовые требования скорости. Карта #26 эти развилки прошла; спека собирает
|
||
решения в одну картину. По ней этап 2 режется на тикеты (#34).
|
||
|
||
Здесь не переоткрывается решённое мастер-спекой: модель данных события,
|
||
таксономия, анонимность, механика заказов (границы мандата #14). Генератор
|
||
слепков заказов — этап 3: эта спека лишь не должна ему мешать. Один
|
||
осознанный выход за границы: решение о хранении снимка (раздел 5) формально
|
||
касается этапа 7 — расширение подтверждено владельцем в резолюции
|
||
«Производительности».
|
||
|
||
## Целевая картина одним взглядом
|
||
|
||
- **Мир — функция, не состояние.** Состав мира — чистая функция зерна;
|
||
модельный день D — функция (зерно, D). Между прогонами живут только зерно
|
||
и позиция на оси времени.
|
||
- **Детерминизм до байта.** Одно зерно — побайтово тот же снимок; сверка —
|
||
хешами манифеста. Транспорт (офсеты Kafka, темп) — вне обещания.
|
||
- **Схема — контракт генератора.** Python-модуль с чистыми данными;
|
||
хранилище строится по рендеренной документации, границу сторожит
|
||
contract-тест.
|
||
- **Один сериализатор, глупые приёмники.** День-функция выдаёт канонические
|
||
байты; приёмники — файл, Kafka пачкой, Kafka с темпом.
|
||
- **Числа.** Средний день ~50 тыс. событий; эталонный снимок — 14 дней;
|
||
в git — только манифест; автоматический порог один — день ≤ 30 с.
|
||
|
||
## 1. Модель мира
|
||
|
||
Резолюция развилки [«Модель мира»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/27).
|
||
|
||
- **Мир функциональный, ничего не мутирует.** Постоянный состав мира —
|
||
популяция посетителей, их привычки, календарь двухкуковых пар — чистая
|
||
функция зерна; целиком не вычисляется и не хранится, спрашивается по
|
||
дням (форма плана — ниже). День D — функция (зерно, D); межднёвные
|
||
связи (окно заказов K, опоздания) выводятся из плана состава, а не
|
||
копятся в состоянии. Глобальные инварианты («каждый двухкуковый
|
||
покупатель заказал с обеих кук») гарантируются планом; счётчики
|
||
состава — посетители, пары, приток — известны до генерации событий,
|
||
торговые счётчики сложатся, когда торговое поведение определит #40.
|
||
- **Состав не замкнут: посетители появляются и затухают.** План состава
|
||
задаёт календарь появления — у каждого посетителя есть дата первого
|
||
визита и профиль возвратов, включая затухание: заметная доля кук
|
||
одноразовая, как в живом трафике. Новые посетители появляются на всём
|
||
протяжении оси: uniq(ClientID) растёт с горизонтом, дневная и накопленная
|
||
аудитории не сходятся в одно число. Приток — часть плана, а не мутация:
|
||
счётчики состава по-прежнему известны до генерации (уточнение по
|
||
вычитке владельца, 2026-08-01); его числа — раздел 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). Между прогонами живут только зерно и
|
||
позиция на оси: выключенный ноутбук — мир замер, потом продолжил.
|
||
- **Два режима движения по одной оси.** Пошаговый — базовый для лаб: старт
|
||
с эталонного снимка, дальше «прожить следующий день» — явное действие.
|
||
Живой день — текущий день проигрывается с ускорением, дашборд и мониторинг
|
||
«дышат»; включается по требованию, не постоянный фон.
|
||
- **Граница суток — единственный структурный шов.** Сессии режутся по ней,
|
||
дневная партиция самодостаточна; в конце модельного дня — слепок заказов.
|
||
День проживается целиком, полдня не бывает: недожитый из-за обрыва день
|
||
переигрывается (раздел 4).
|
||
- **Поток и пакет совместимы по построению.** День-функция выдаёт один
|
||
упорядоченный поток событий; режимы отличаются только способом
|
||
проигрывания — пачкой или с темпом.
|
||
|
||
Отклонено с доводами:
|
||
|
||
- *Мутирующее состояние мира* («мир стареет»): ломает параллельность по
|
||
дням, требует чекпоинтов, счётчики манифеста узнаваемы только постфактум;
|
||
ни один урок стенда на старении не стоит.
|
||
- *Чистая функция без слоя состава*: глобальные инварианты пришлось бы
|
||
выводить в каждом дне заново — тот же план мира, но неявный и размазанный.
|
||
- *Привязка модельного времени к реальному календарю* (T-1 с догоном):
|
||
конфликтует с ускорением ×60 — за вечер мир уезжает в будущее — и делает
|
||
даты эталонного мира зависимыми от даты запуска, манифест теряет
|
||
воспроизводимость.
|
||
|
||
Отклонено при исполнении #38 (2026-08-02):
|
||
|
||
- *Разгон вместо предыстории* («первые дни малы — магазин запустился»):
|
||
зерновой мир и половина снимка оказались бы на разгоне, недельная лаба
|
||
сравнивала бы несравнимые недели, «средний день ~50 тыс.» перестал бы
|
||
быть средним — пришлось бы двигать принятые числа раздела 5.
|
||
- *Материализованный план на горизонт*: горизонт становится обязательным
|
||
входом каждого запуска, стоимость старта растёт с прожитым; префиксную
|
||
устойчивость даёт и ленивая форма — даром, через подпотоки по номеру дня.
|
||
- *Замкнутый пул кук со сменой поколений*: постоянная память ценой фальшивой
|
||
константы — потолка одновременно живущих кук, которого в жизни нет и
|
||
который менти нечем объяснить.
|
||
- *Вероятностная гарантия пар* («почти наверняка купит с обеих кук»): не
|
||
гарантия — однажды манифест покраснеет, а чинить нечем, кроме смены
|
||
зерна; при этом несклеенная пара в данных неотличима от двух незнакомцев,
|
||
так что реализм этой лотереи невидим.
|
||
- *Покупка пары в первый визит куки*: гарантия железная и дешёвая, но узор
|
||
«все пары покупают в первый день» виден в данных ровно там, куда лаба
|
||
склейки смотрит пристальнее всего. Условность допустима, пока она не
|
||
видна в данных.
|
||
|
||
## 2. Детерминизм от зерна
|
||
|
||
Резолюция развилки [«Детерминизм от зерна»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/28).
|
||
|
||
- **Обещание — содержимое до байта.** Два прогона с одним зерном дают тот же
|
||
набор событий: те же `WatchID`/`VisitID`, поля, метки модельного времени.
|
||
Снимок при пересборке побайтово совпадает: канонический порядок ключей и
|
||
строк; сверка — по хешам манифеста, а два локально пересобранных снимка
|
||
сравнимы обычным diff — пустой означает «ничего не изменилось». Вне
|
||
обещания — транспорт: офсеты и партиции Kafka, какая нода прочитала,
|
||
`_ingested_at`, темп живого дня.
|
||
- **Условия обещания.** Детерминизм держится при зафиксированном `uv.lock`
|
||
и внутри канонического контейнера — то есть везде Linux, на маке и в WSL
|
||
тоже; единственная переменная — архитектура CPU. Истина — CI на Linux; сходимость любой машины проверяет
|
||
скрипт «пересгенерируй день N — сравни хеш с манифестом». Расхождение на
|
||
любой платформе — баг генератора, а не допуск.
|
||
- **Раздача зерна — иерархией подпотоков.** Корневое зерно → подпоток
|
||
состава мира, ветвящийся по номеру дня на когорты плана (состав
|
||
спрашивается по дням — раздел 1; уточнение при исполнении #38);
|
||
(зерно, день) → подпоток дня → именованные подпотоки компонентов: трафик,
|
||
торговые события, расхождения, опоздания — в фиксированном порядке.
|
||
По построению: параллельный прогон равен последовательному; продление
|
||
истории днём N+1 не трогает дни 1…N; правка одного компонента меняет
|
||
только его часть снимка — в манифесте меняются хеши только затронутых
|
||
дней, дифф двух локальных пересборок читаем.
|
||
- **Механизм подпотоков — `numpy.random.SeedSequence`.** Сверено через
|
||
Context7 по документации numpy (2026-08-01): `spawn(n)` порождает детей
|
||
расширением `spawn_key`, потомок полностью определяется парой
|
||
(entropy, spawn_key) — позицией в дереве, а не порядком вычислений. Это
|
||
ровно то свойство, на котором держатся три гарантии предыдущего пункта.
|
||
Каждый подпоток кормит `PCG64` — генератор, рекомендованный numpy.
|
||
- **Дисциплина целочисленной случайности.** Случайность тянется целыми
|
||
числами: диапазоны, выбор из таблиц. Плавающие распределения из системной
|
||
математики не используются — это снимает межархитектурные расхождения
|
||
amd64/arm64. Деньги считаются в целых копейках; Float64 — только
|
||
представление в клиентском `purchase` (урок мастер-спеки о расхождениях).
|
||
- **Канонический seed и паспорт мира.** Эталонный мир собирается одним
|
||
каноническим зерном — константой репозитория; свои зёрна менти крутит без
|
||
гарантий манифеста. Манифест хранит паспорт мира — зерно и версию
|
||
генератора; чек-скрипты сверяют паспорт раньше счётчиков.
|
||
- **Суточный профиль интенсивности задаёт день-функция.** Форма — волны:
|
||
ночной провал, обеденный и вечерний пики, различие будней и выходных;
|
||
пики — до ~2× среднего. С детерминизмом профиль совместим: это часть
|
||
функции дня, а не внешний шум.
|
||
|
||
Отклонено с доводами:
|
||
|
||
- *Воспроизводимы только состав мира и счётчики*: ломает доигрывание дня
|
||
через дедуп (другие `WatchID` — дубли вместо склейки) и воспроизводимую
|
||
отладку.
|
||
- *События те же, байты не обещаем*: экономия копеечная, а честный diff
|
||
снимка и тесты «хеш совпал» теряются.
|
||
- *Общий RNG-поток на все дни*: порядок исполнения менял бы результат —
|
||
«параллельно равно последовательно» недостижимо.
|
||
- *Обещание детерминизма поверх обновления зависимостей*: numpy сознательно
|
||
улучшает алгоритмы распределений между версиями (NEP 19), Faker меняет
|
||
словари. Фиксация — `uv.lock`; обновление зависимостей — осознанная
|
||
пересборка манифеста одним PR.
|
||
|
||
## 3. Контракт схемы
|
||
|
||
Резолюция развилки [«Архитектура»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/29), часть первая.
|
||
|
||
- **Граница вывода — по шву «трекер | хранилище», как data contract.**
|
||
Контракт схемы — собственность генератора, как формат выгрузки —
|
||
собственность Метрики. Из контракта выводятся: сам генератор, его
|
||
валидация и публичное «описание выгрузки» в доках — рендеренная таблица
|
||
колонок, аналог документации Метрики.
|
||
- **Форма контракта — импортируемый python-модуль с чистыми данными**:
|
||
описатели колонок (имя Метрики, тип ClickHouse, тип numpy, нормализованное
|
||
snake_case-имя, группа полей, порядок), никакой логики. Читаемость для
|
||
менти несёт рендеренная таблица в доках, не модуль. Уточнение при
|
||
исполнении (#36): нормализованное имя — имя источника, приведённое к
|
||
нашему стилю, а не имя атрибута в модели данных. Слой DDS складывает свою
|
||
модель и называет атрибуты по ней; `dds.v_event` эти имена берёт (раздел 7
|
||
мастер-спеки), но контракт их не диктует и тестами не сторожит.
|
||
- **Сторона хранилища пишется по документации, не генерируется.** DDL
|
||
`ods.event`, SELECT матвью, `dds.v_event`, трансформации — работа
|
||
следующих этапов по «описанию выгрузки», как в бою хранилище адаптируется
|
||
к источнику. Границу сторожат два боевых механизма: строгий приём
|
||
(`input_format_skip_unknown_fields = 0`, таблицы `*_errors` — раздел 6
|
||
мастер-спеки) и contract-тест в smoke — сравнение `system.columns`
|
||
поднятого стенда со схемой генератора.
|
||
|
||
Отклонено с доводами:
|
||
|
||
- *Автогенерация DDL хранилища из контракта* (буква раздела 1.4
|
||
мастер-спеки до правки): пересекает границу ответственности компонент —
|
||
в бою хранилище адаптируется к источнику руками, менти пришлось бы
|
||
объяснять приём, которого в жизни нет. Data contract даёт тот же щит от
|
||
дрейфа без этой условности.
|
||
- *YAML как форма контракта*: красота ценой загрузчика и «схемы для схемы»;
|
||
потребителя вне Python нет — хранилище читает рендеренную документацию,
|
||
не машинный файл.
|
||
|
||
## 4. Поток и пакет: сериализатор, проигрыватель, приёмники
|
||
|
||
Резолюция развилки [«Архитектура»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/29), часть вторая.
|
||
|
||
- **Один канонический сериализатор, глупые приёмники.** День-функция выдаёт
|
||
упорядоченный поток канонических байтов — единственное место, где событие
|
||
превращается в JSON. Приёмники не знают о содержимом: файл (локальный кэш
|
||
для пересборки и проверок манифеста), Kafka пачкой — пакетный режим,
|
||
Kafka с темпом ×60 — живой день. Новых топиков нет.
|
||
- **Рабочий выбор сериализатора — orjson**: быстрее stdlib json в 5–14 раз,
|
||
numpy-массивы и datetime сериализует нативно (заметка исследования #31).
|
||
Смена библиотеки меняет канонические байты, поэтому проходит как
|
||
обновление зависимости: осознанная пересборка манифеста одним PR.
|
||
- **Промежуточные файлы не хранятся.** Файл дня — кэш чистой функции:
|
||
потерял — пересчитал. В git снимок не попадает (раздел 5).
|
||
- **Обрыв любого режима — переигровка дня целиком**; дедуп склеивает
|
||
повторы: `WatchID` детерминированы, повтор — та же строка для
|
||
ReplacingMergeTree.
|
||
- **Эталонный снимок при старте стенда — через Kafka, пакетным режимом
|
||
проигрывателя.** Отдельный механизм заливки не строится: каждый `make up`
|
||
бесплатно прогоняет весь конвейер и contract-тест на настоящих данных.
|
||
Оговорка «если заливка уйдёт в десятки минут — вернуться к прямой
|
||
загрузке» проверена при фиксации чисел: 14 × 50 тыс. ≈ 700 тыс. событий —
|
||
расчётно минута-две, запас есть.
|
||
|
||
Отклонено с доводами:
|
||
|
||
- *Отдельный топик / Kafka как хранилище дней*: офсеты и партиции вне
|
||
обещания детерминизма, retention конечен, в git топик не положишь, хеш с
|
||
манифестом не сверишь; Kafka на стенде — труба, не хранилище (раздел 7
|
||
мастер-спеки).
|
||
- *Файл как обязательная станция доставки*: доигрывание обрыва уже решено
|
||
через дедуп, канон держит единственный сериализатор, а не диск; файл
|
||
остаётся только там, где нужен кэш.
|
||
- *Прямая загрузка снимка в ClickHouse* (и гибрид с ручным заполнением
|
||
сырого слоя): второй путь приёма, пустой либо поддельный `stg.hits_raw` —
|
||
теряются переобработка дня X по `event_date` и урок виртуальных колонок
|
||
«какая нода читала топик».
|
||
|
||
## 5. Числа: объёмы, режимы, бюджеты
|
||
|
||
Резолюция развилки [«Производительность»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/30);
|
||
порядки величин — [исследование #31](../research/2026-08-01-python-batch-generation-speed.md).
|
||
|
||
- **Средний модельный день — ~50 тыс. событий**; суточные волны с пиками до
|
||
~2× среднего, будни/выходные; ≈8–12 тыс. сессий, 6–8 тыс. посетителей —
|
||
правдоподобный средний магазин.
|
||
- **Эталонный снимок — 14 дней**: две полные календарные недели, D0 —
|
||
понедельник. Самая короткая длина, при которой есть замороженная зона за
|
||
окном K = 7, дышащая зона и две волны недельной сезонности. Удлинение до
|
||
месяца — дешёвый ход (пересборка манифеста), если понадобится.
|
||
- **В git — только манифест, снимок не хранится.** Снимок генерируется при
|
||
`make up` и при проверках: артефакт — кэш чистой функции, кэш в git не
|
||
хранят. Манифест несёт паспорт мира, счётчики и хеши по дням; проверки
|
||
«пустой git diff» и «пересгенерируй день N — сравни хеш» живут на нём.
|
||
Каждый `make up` — живая демонстрация детерминизма. Честная потеря —
|
||
страховка на случай платформенного бага: раньше менти с расходящимися
|
||
байтами мог взять готовый снимок из git, теперь он упрётся в красный чек
|
||
манифеста; смягчение — CI гоняет генерацию на amd64 и arm64.
|
||
- **Живой день — ×60 по умолчанию**: модельные сутки за 24 реальные минуты,
|
||
суточная волна разворачивается на глазах; темп в среднем ~35 событий/с,
|
||
в пиковые часы сильных дней — до ~100. Число —
|
||
значение по умолчанию, переопределяется флагом проигрывателя: ускорение —
|
||
свойство транспорта, вне обещания воспроизводимости, константой мира не
|
||
делается.
|
||
|
||
### Бюджеты и способ замера
|
||
|
||
Схема двухъярусная — урок ADR 0004: пороги впритык к расчёту на разном
|
||
железе кончаются ритуальным удалением проверки.
|
||
|
||
Ориентиры на референсной машине — в спеке, без автоматики:
|
||
|
||
| Операция | Ориентир |
|
||
|---|---|
|
||
| Генерация одного дня | секунды |
|
||
| Пересборка эталонного мира (14 дней + манифест, без транспорта) | до минуты |
|
||
| Заливка снимка при `make up` (Kafka → матвью → ODS) | минуты |
|
||
| Лаг живого дня | секунды |
|
||
|
||
**Автоматический порог один: полный день (50 тыс. событий) генерируется
|
||
≤ 30 с.** Расчёт по планке исследования (~2–5×10⁵ событий/с на ядро) — доли
|
||
секунды; порог держит машинный разброс ×2–5 и ловит деградацию на 1–2
|
||
порядка: Faker в горячем цикле, случайная квадратичность. Реализация —
|
||
pytest-тест с маркером `perf` и таймаутом-обрубанием: обязателен в CI,
|
||
исключён из быстрой локальной петли, зовётся отдельной целью при правках
|
||
горячего цикла.
|
||
|
||
Остальное — наблюдаемость без порогов: `make up` и smoke печатают тайминги
|
||
(генерация и доставка отдельно), проигрыватель логирует лаг. Прототип-замер
|
||
до этапа 2 не нужен: числа назначены с запасом порядок и больше от планки
|
||
исследования, планка подтверждена локальной проверкой на машине стенда;
|
||
первый замер настоящего кода — порог этапа 2.
|
||
|
||
Отклонено с доводами:
|
||
|
||
- *Четыре жёстких CI-ворот на все бюджеты*: машинный разброс против порогов
|
||
впритык — повторение истории с памятью (ADR 0004); порог оставлен один,
|
||
грубый, между «×5 шума» и «×100 беды».
|
||
- *Снимок в git* (статус-кво раздела 8 мастер-спеки): основание из v1 —
|
||
медленный генератор — съедено детерминизмом и скоростью; остаётся только
|
||
раздутый репозиторий. *Снимок вложением релиза Gitea*: страховка без
|
||
раздутия, но лишняя машинерия и вторая правда.
|
||
- *Снимок 7 дней*: ни одного замороженного дня, сезонность без сравнения.
|
||
*30 дней*: нового урока не даёт — отложено как дешёвое удлинение.
|
||
- *×120 / ×1440*: волна смазывается в перемотку либо превращается в пакет с
|
||
анимацией — живой режим теряет смысл.
|
||
|
||
## 6. Правила кода этапа 2
|
||
|
||
Хвосты резолюций, обязательные для реализации:
|
||
|
||
- **Целочисленная случайность** — правило кода, а не пожелание: диапазоны и
|
||
выбор из таблиц целыми, плавающие распределения системной математики не
|
||
звать; деньги — в целых копейках.
|
||
- **Случайные значения — векторно из numpy (PCG64)**; посточный цикл — лишь
|
||
там, где логика действительно посточная (цепочки сессий).
|
||
- **Посточные фейкеры (Faker, mimesis) — только для справочников** (каталог
|
||
и прочие справочные строки), не в горячем цикле: они медленнее numpy на
|
||
2–3 порядка. Выбор библиотеки — этапу 2: справочники генерируются один
|
||
раз, скорость безразлична; возможно, хватит таблиц-литералов в коде и
|
||
фейкер не понадобится вовсе.
|
||
- **Сериализация — через единственный канонический сериализатор** (orjson);
|
||
прямых `json.dumps` по коду нет.
|
||
- **Распараллеливание — multiprocessing по модельным дням**; внутри дня —
|
||
однопоточно, единица работы и так крупная.
|
||
|
||
## 7. Расхождения с мастер-спекой
|
||
|
||
Внесены в мастер-спеку тем же коммитом, что и эта спека:
|
||
|
||
- **Раздел 1.4**: «из контракта выводятся DDL и валидация» заменено на data
|
||
contract — хранилище пишется по документации, границу сторожит
|
||
contract-тест (раздел 3 здесь).
|
||
- **Раздел 8**: артефакт `data/startup_history/` в git заменён манифестом;
|
||
снимок генерируется на месте (раздел 5 здесь). Туман «политика
|
||
версионирования артефакта» закрыт этим же ходом: версионируется манифест.
|
||
- **Раздел 11**: пункт «до этапа 3 зафиксировать требования
|
||
производительности» закрыт числами раздела 5.
|
||
- Мелкие согласования там, где текст опирался на артефакт в git: источник
|
||
переобработки при исчерпании retention Kafka (раздел 7), формулировка
|
||
этапа 7 (раздел 9).
|
||
|
||
## 8. Хвосты следующим этапам
|
||
|
||
- **Этап 5 (Airflow)**: живой день со стороны хранилища — обычный ETL-даг
|
||
по расписанию (~раз в 24 минуты); генератор не дорабатывается.
|
||
- **Этап 7 (эталонный мир)**: пересборка — это манифест, не артефакт;
|
||
CI-генерация на amd64 и arm64.
|
||
- **Будущие лабы**: перезаливка дня X пакетным режимом проигрывателя —
|
||
готовая демонстрация идемпотентности конвейера.
|
||
|
||
## 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) — и в каком контейнере он живёт (#41);
|
||
- как фиксируется «зерновой» мир конца этапа 2 (раздел 9 мастер-спеки):
|
||
с манифестным решением напрашивается мини-манифест зернового мира —
|
||
форма за #42.
|