docs(specs): собрана спека генератора из решений развилок карты #26

- Зачем:
  - этап 2 нельзя нарезать на тикеты без единой картины генератора,
    а решения четырёх развилок карты #26 жили только в тикетах трекера.
- Что:
  - новая спека docs/specs/2026-08-01-generator.md: функциональный мир,
    детерминизм до байта (SeedSequence сверен через Context7), контракт
    схемы, канонический сериализатор, числа и порог производительности;
    отклонённые варианты записаны с доводами.
  - мастер-спека согласована тем же коммитом: 1.4 — data contract вместо
    автогенерации DDL, 8 — в git только манифест, 11 — числа вместо
    «зафиксировать требования»; мелкие согласования в 7 и 9.
  - CONTEXT.md пополнен терминами модели мира и вывода генератора.
- Проверка:
  - вычитка; относительные ссылки спек указывают на существующие файлы
    в docs/specs/ и docs/research/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 19:47:29 +03:00
co-authored by Claude Opus 5
parent a9fade84f5
commit f85626508f
3 changed files with 403 additions and 20 deletions
+48
View File
@@ -0,0 +1,48 @@
# Кликстрим-платформа (стенд v2)
Словарь понятий проекта: одни и те же слова для одних и тех же вещей —
у владельца, кода, документов и агентов. Только язык, никаких решений.
## Язык
**Хранилище**:
Аналитическая база стенда — кластер ClickHouse со слоями STG/ODS/DDS/DM.
Принимающая сторона границы «трекер | хранилище»: нормализует имена и стили
источников, строится по их документации.
_Избегать_: склад, склад данных
**Состав мира**:
Постоянная часть мира генератора — популяция посетителей, их привычки,
календарь двухкуковых пар. Чистая функция зерна: вычисляется при старте
любого процесса, между прогонами не хранится.
_Избегать_: состояние мира
**Ось модельного времени**:
Собственный календарь мира генератора. Дни считаются от фиксированного
первого дня D0; реальный календарь в модели не участвует. Между прогонами
живут только зерно и позиция на оси.
**Пошаговый режим**:
Базовый способ движения по оси модельного времени: «прожить следующий
день» — явное действие.
**Живой день**:
Проигрывание текущего модельного дня в реальном времени с ускорением;
включается по требованию, не постоянный фон.
**Граница суток**:
Единственный структурный шов модели: сессии режутся по ней, слепок заказов
снимается на ней, день проживается только целиком.
**Контракт схемы**:
Python-модуль с описателями колонок события — собственность генератора.
Из него выводятся генератор, валидация и документация формата; хранилище
строится по документации, не по модулю.
**Канонический сериализатор**:
Единственное место, где событие превращается в байты. Фиксированный порядок
ключей и строк — основа побайтовой воспроизводимости.
**Проигрыватель**:
Компонент доставки готового потока дня в приёмник. Два режима: пакетный
(пачкой, без темпа) и живой день.
+33 -20
View File
@@ -163,12 +163,18 @@ Ecommerce (заполнены только у торговых событий):
### 1.4 Схема как контракт ### 1.4 Схема как контракт
Одно машинное описание схемы события (python-модуль или YAML) — источник Машинное описание схемы события python-модуль с чистыми данными,
истины: из него выводятся DDL и валидация генератора, а не наоборот. 47 собственность генератора (data contract; решение развилки «Архитектура»
колонок повторяются примерно в семи местах (генератор, DDL, SELECT матвью, карты #26, подробности — [спека генератора](2026-08-01-generator.md),
трансформации, витрины, манифест, доки) — без контракта они расходятся раздел 3). Из контракта выводятся сам генератор, его валидация и
молча. Заодно это учебный артефакт: менти видит на живом примере, что такое рендеренное «описание выгрузки» в доках — аналог документации Метрики.
«схема как контракт». Сторона хранилища (DDL, SELECT матвью, трансформации, витрины) пишется по
этой документации на своих этапах, как в бою хранилище адаптируется к
источнику; границу сторожат строгий приём (раздел 6) и contract-тест в
smoke — сравнение `system.columns` поднятого стенда со схемой генератора.
Без контракта 47 колонок, повторяясь примерно в семи местах, расходятся
молча. Заодно это учебный артефакт: менти видит на живом примере, что
такое data contract.
## 2. Заказы бэкенда ## 2. Заказы бэкенда
@@ -375,7 +381,8 @@ ReplacingMergeTree. Таблицы `stg.*_raw` хранят виртуальны
(`_topic`, `_partition`, `_offset`, `_timestamp`) — без них урок «какая нода (`_topic`, `_partition`, `_offset`, `_timestamp`) — без них урок «какая нода
читала топик» ненаблюдаем. `stg.hits_raw` дополнительно хранит извлечённый читала топик» ненаблюдаем. `stg.hits_raw` дополнительно хранит извлечённый
`event_date` — им кормится переобработка дня X (при исчерпании retention `event_date` — им кормится переобработка дня X (при исчерпании retention
Kafka переобработка возможна только из эталонного артефакта). Kafka день переигрывается генератором заново: снимок — кэш чистой функции,
см. [спеку генератора](2026-08-01-generator.md)).
`dds.v_event` — первый на стенде пример правила «слой — это контракт, а не `dds.v_event` — первый на стенде пример правила «слой — это контракт, а не
обязательно копия данных». обязательно копия данных».
@@ -425,9 +432,15 @@ Kafka переобработка возможна только из эталон
## 8. Эталонный мир и манифест ## 8. Эталонный мир и манифест
Пересборка артефакта `data/startup_history/` неизбежна и оплачена решением В git хранится только манифест эталонного мира; сам снимок (14 модельных
#18 один раз — все изменения генератора съезжаются в одну пересборку. дней) генерируется на месте — при `make up` и при проверках (решение
Манифест расширяется контрольными числами: развилки «Производительность» карты #26, подробности —
[спека генератора](2026-08-01-generator.md), раздел 5). Манифест несёт
паспорт мира (каноническое зерно, версия генератора), контрольные счётчики
и хеши по дням; проверки «пустой git diff» и «пересгенерируй день N —
сравни хеш» живут на нём. Политика версионирования артефакта (бывший туман
карты #10) закрыта этим же ходом: версионируется манифест.
Контрольные числа манифеста:
- заказная сторона: заказы и выручка по дням; манифест хранит точные - заказная сторона: заказы и выручка по дням; манифест хранит точные
счётчики по каждому классу расхождений (отмены, потери, дубли, дельты сумм) — счётчики по каждому классу расхождений (отмены, потери, дубли, дельты сумм) —
@@ -435,9 +448,8 @@ Kafka переобработка возможна только из эталон
- идентичность: uniq кук, uniq известных пользователей, число двухкуковых - идентичность: uniq кук, uniq известных пользователей, число двухкуковых
покупателей — лаба склейки получает самопроверку. покупателей — лаба склейки получает самопроверку.
Артефакт вырастет (ecommerce-массивы, заказы) — размер проверить при Снимок вырастет против v1 (ecommerce-массивы, заказы) — размер проверить
пересборке. Политика версионирования артефакта здесь не решается (туман при пересборке.
карты #10).
## 9. Оценка объёма исполнения ## 9. Оценка объёма исполнения
@@ -451,7 +463,7 @@ v2 стартует пустым, поэтому объём ниже — это
| SQL | 5 DDL-файлов (ON CLUSTER, Replicated*, Distributed) + трансформации событий, заказов, identity, сверки + словарь | L — ~12–15 файлов, главная сложность | | SQL | 5 DDL-файлов (ON CLUSTER, Replicated*, Distributed) + трансформации событий, заказов, identity, сверки + словарь | L — ~12–15 файлов, главная сложность |
| Airflow | DAG'и по образцу v1: etl_pipeline (партиционная переобработка, ожидание дневного батча заказов — сенсор/Datasets), world_init/next_day, helpers | M — ~56 файлов | | Airflow | DAG'и по образцу v1: etl_pipeline (партиционная переобработка, ожидание дневного батча заказов — сенсор/Datasets), world_init/next_day, helpers | M — ~56 файлов |
| Superset | датасеты + дашборд с тремя новыми сюжетами | M — 2 файла | | Superset | датасеты + дашборд с тремя новыми сюжетами | M — 2 файла |
| Эталонный мир | сборка артефакта v2, манифест-счётчики, чек-скрипты | M–L | | Эталонный мир | пересборка снимка на месте, манифест-счётчики, чек-скрипты | M–L |
| Мониторинг | дашборды Grafana «данные», «кластер», «запросы»; ClickHouse источником данных, панели на SQL; Prometheus тонким полом (ADR 0002) | M — конфиги и дашборды | | Мониторинг | дашборды Grafana «данные», «кластер», «запросы»; ClickHouse источником данных, панели на SQL; Prometheus тонким полом (ADR 0002) | M — конфиги и дашборды |
| Документация | доки v2 пишутся заново (см. раздел 12) | M, в тех же PR | | Документация | доки v2 пишутся заново (см. раздел 12) | M, в тех же PR |
@@ -496,7 +508,8 @@ v2 стартует пустым, поэтому объём ниже — это
5. Airflow: `etl_pipeline` (партиционная переобработка, ожидание дневного 5. Airflow: `etl_pipeline` (партиционная переобработка, ожидание дневного
батча заказов — сенсор/Datasets). батча заказов — сенсор/Datasets).
6. Расхождения B+D и опоздания; счётчики манифеста. 6. Расхождения B+D и опоздания; счётчики манифеста.
7. Эталонный мир: пересборка артефакта, чек-скрипты. 7. Эталонный мир: манифест и пересборка снимка, чек-скрипты; CI-генерация
на amd64 и arm64.
8. Superset-дашборд v2. 8. Superset-дашборд v2.
9. Мониторинг и runbook «keeper упал / DDL повис в очереди». Состав дашбордов 9. Мониторинг и runbook «keeper упал / DDL повис в очереди». Состав дашбордов
и границы — ADR 0002. и границы — ADR 0002.
@@ -510,7 +523,6 @@ smoke-проверки, а не «дашборд зелёный». Это мин
- «Грязь» в данных: боты, дубли на транспорте, опоздавшие мобильные батчи, - «Грязь» в данных: боты, дубли на транспорте, опоздавшие мобильные батчи,
расхождение часов клиент/коллектор — туман карты, вернётся своим тикетом. расхождение часов клиент/коллектор — туман карты, вернётся своим тикетом.
- Политика версионирования эталонного артефакта — туман карты.
- Лабы и курс: v2 — другой стенд, лабы для него пишутся с нуля отдельной - Лабы и курс: v2 — другой стенд, лабы для него пишутся с нуля отдельной
работой после этой спеки; редизайн лаб v1 (#7) остаётся в v1 и сюда не работой после этой спеки; редизайн лаб v1 (#7) остаётся в v1 и сюда не
переносится. Спека даёт будущим лабам только опорные точки — контрольные переносится. Спека даёт будущим лабам только опорные точки — контрольные
@@ -550,10 +562,11 @@ smoke-проверки, а не «дашборд зелёный». Это мин
(батчевая генерация вместо посточной, быстрая JSON-сериализация, (батчевая генерация вместо посточной, быстрая JSON-сериализация,
распараллеливание по модельным дням). Читаемость генератора для менти — распараллеливание по модельным дням). Читаемость генератора для менти —
не довод при выборе языка: он в любом случае сложнее уровня DE-джуна. не довод при выборе языка: он в любом случае сложнее уровня DE-джуна.
До этапа 3 зафиксировать требования производительности (пересборка Числовые требования производительности и способ замера зафиксированы
эталонного мира, живой поток ×60); переход на компилируемый язык [спекой генератора](2026-08-01-generator.md), раздел 5 (порядки величин —
(Rust/Go) — только если замеры покажут, что Python приемлемой скорости [исследование](../research/2026-08-01-python-batch-generation-speed.md));
не даёт. переход на компилируемый язык (Rust/Go) — только если живые замеры
этапа 2 выйдут за её порог.
## 12. Влияние на документацию ## 12. Влияние на документацию
+322
View File
@@ -0,0 +1,322 @@
# Генератор (этап 2): функциональный мир, детерминизм до байта, контракт схемы, числа скорости
Статус: Proposed — ждёт приёмки владельцем (тикет #33).
Дата: 2026-08-01. Мандат — тикет #14 (этап 2, родитель #4), карта #26.
Развилки: модель мира (#27), детерминизм от зерна (#28), архитектура вывода
(#29), производительность (#30); исследование скорости (#31).
Источники: мастер-спека
[«Боевой реализм стенда (v2)»](2026-07-30-stand-v2-realism.md) — разделы 1.4,
5, 911; заметка
[«Скорость батчевой генерации в Python»](../research/2026-08-01-python-batch-generation-speed.md).
## Зачем
Мастер-спека решила, **что** генерирует стенд: широкое событие в 47 колонок,
таксономию, анонимность, двухкуковых покупателей, заказы слепками. Четыре
развилки о том, **как** генератор устроен, она отложила: модель мира,
границы детерминизма, источник истины схемы с разделением потока и пакета,
числовые требования скорости. Карта #26 эти развилки прошла; спека собирает
решения в одну картину. По ней этап 2 режется на тикеты (#34).
Здесь не переоткрывается решённое мастер-спекой: модель данных события,
таксономия, анонимность, механика заказов (границы мандата #14). Генератор
слепков заказов — этап 3: эта спека лишь не должна ему мешать.
## Целевая картина одним взглядом
- **Мир — функция, не состояние.** Состав мира — чистая функция зерна;
модельный день 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, опоздания) выводятся из
плана состава, а не копятся в состоянии. Глобальные инварианты («каждый
двухкуковый покупатель заказал с обеих кук») гарантируются планом —
счётчики манифеста известны до генерации событий.
- **Своя ось модельного времени.** Мир рождается в фиксированный день D0
(понедельник — см. раздел 5); реальный календарь в модели не участвует.
Между прогонами живут только зерно и позиция на оси: выключенный ноутбук —
мир замер, потом продолжил.
- **Два режима движения по одной оси.** Пошаговый — базовый для лаб: старт
с эталонного снимка, дальше «прожить следующий день» — явное действие.
Живой день — текущий день проигрывается с ускорением, дашборд и мониторинг
«дышат»; включается по требованию, не постоянный фон.
- **Граница суток — единственный структурный шов.** Сессии режутся по ней,
дневная партиция самодостаточна; в конце модельного дня — слепок заказов.
День проживается целиком, полдня не бывает: недожитый из-за обрыва день
переигрывается (раздел 4).
- **Поток и пакет совместимы по построению.** День-функция выдаёт один
упорядоченный поток событий; режимы отличаются только способом
проигрывания — пачкой или с темпом.
Отклонено с доводами:
- *Мутирующее состояние мира* («мир стареет»): ломает параллельность по
дням, требует чекпоинтов, счётчики манифеста узнаваемы только постфактум;
ни один урок стенда на старении не стоит.
- *Чистая функция без слоя состава*: глобальные инварианты пришлось бы
выводить в каждом дне заново — тот же план мира, но неявный и размазанный.
- *Привязка модельного времени к реальному календарю* (T-1 с догоном):
конфликтует с ускорением ×60 — за вечер мир уезжает в будущее — и делает
даты эталонного мира зависимыми от даты запуска, манифест теряет
воспроизводимость.
## 2. Детерминизм от зерна
Резолюция развилки [«Детерминизм от зерна»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/28).
- **Обещание — содержимое до байта.** Два прогона с одним зерном дают тот же
набор событий: те же `WatchID`/`VisitID`, поля, метки модельного времени.
Артефакт снимка при пересборке побайтово совпадает: канонический порядок
ключей и строк, пустой diff означает «ничего не изменилось». Вне
обещания — транспорт: офсеты и партиции Kafka, какая нода прочитала,
`_ingested_at`, темп живого дня.
- **Условия обещания.** Детерминизм держится при зафиксированном `uv.lock`
и внутри канонического контейнера (везде Linux; переменная — только
архитектура CPU). Истина — CI на Linux; сходимость любой машины проверяет
скрипт «пересгенерируй день N — сравни хеш с манифестом». Расхождение на
любой платформе — баг генератора, а не допуск.
- **Раздача зерна — иерархией подпотоков.** Корневое зерно → состав мира;
(зерно, день) → подпоток дня → именованные подпотоки компонентов: трафик,
торговые события, расхождения, опоздания — в фиксированном порядке.
По построению: параллельный прогон равен последовательному; продление
истории днём N+1 не трогает дни 1…N; правка одного компонента меняет
только его часть снимка — diff читаем.
- **Механизм подпотоков — `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-имя
для DDS, группа полей, порядок), никакой логики. Читаемость для менти
несёт рендеренная таблица в доках, не модуль.
- **Сторона хранилища пишется по документации, не генерируется.** 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 в 514 раз,
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` — живая демонстрация детерминизма. Честная потеря —
страховка от платформенного бага; смягчение — 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 — только для справочников** (каталог, имена), не в горячем цикле:
он медленнее numpy на 2–3 порядка.
- **Сериализация — через единственный канонический сериализатор** (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)
Осталось из тумана карты — вопросы уровня тикетов, не развилок:
- интерфейс запуска генератора (CLI / цели make) и как он делит режимы
проигрывателя;
- формат описания мира и конфигурации (что константа кода, что параметр);
- как фиксируется «зерновой» мир конца этапа 2 (раздел 9 мастер-спеки):
с манифестным решением напрашивается мини-манифест зернового мира — форму
выбрать при нарезке.