Files
clickstream-data-platform/generator/src/clickstream_generator/seeds.py
T
ddadminandClaude Opus 5 b2fcc7e593 feat(generator): заказы дня — проекция покупок с деньгами магазина
- Зачем:
  - тикет #90: второй источник должен смотреть на торговую половину дня
    глазами бэкенда — заказ есть проекция покупки, а не второе порождение;
    учебный результат — два источника согласованы по построению, а не
    сверкой.
- Что:
  - `commerce` отдаёт вторым выходом покупки дня: номер заказа, корзину,
    выручку клиента, промокод и человека за кукой; новых бросков в
    подпоток `COMMERCE` не добавилось, события дня не сдвинулись.
  - заведена заказная сторона: подпоток `ORDERS` на позиции 2 дерева зерна
    (прежнее мёртвое имя `DISCREPANCIES`), ветвление — по дню рождения
    заказа; `LATECOMERS` не тронут, его наполнит этап 6.
  - новый модуль `orders.py`: деньги заказа целыми копейками —
    `items_total` из корзины, `discount` по таблице «код → скидка»
    (округление вниз), `delivery` броском по таблице весов на полную длину
    дня, `total` = `items_total` − `discount` + `delivery`.
  - план состава завёл `person_id` — последним броском когорты, после
    паспортов: вторая кука пары повторяет ID своего человека, заказ
    показывает его как `user_id`, в событие Метрики он не попадает.
  - `world`: окно изменяемости `ORDER_WINDOW_DAYS` = 7 рядом с D0 и поясом,
    черновая таблица стоимости доставки.
  - слепок дня в тестах сторожит обе половины: заказы сравниваются наравне
    с потоком событий.
  - доки: имя подпотока заказной стороны и состав денег заказа записаны в
    `docs/architecture/orders`, README генератора знает про новый модуль.
- Проверка:
  - из `generator/`: make lint, make typecheck, make test (417 тестов);
    в корне — make lint.
  - опись мира не покраснела: байты восьми дней те же, заказная сторона
    события не сдвинула.

Closes #90

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:30:13 +03:00

80 lines
4.1 KiB
Python

"""Иерархия зёрен: откуда любая часть мира берёт свою случайность.
Дерево подпотоков (спека генератора, раздел 2):
корневое зерно
├── состав мира
│ ├── ось → номер дня: когорта этого дня
│ └── предыстория → глубина: когорта дня до D0
└── дни
└── номер дня → трафик, торговля, заказная сторона, опоздания
Механизм — `numpy.random.SeedSequence`: потомок полностью определяется парой
(зерно, позиция в дереве), а не порядком вычислений. Сверено через Context7
по документации numpy (2026-08-01) и проверено тестом: `spawn_key`, выписанный
руками, даёт тот же подпоток, что цепочка `spawn`. На этом держатся три
обещания: параллельный прогон равен последовательному, день N+1 не трогает
дни 1…N, правка одного компонента меняет только его часть снимка.
Позиция в дереве — неотрицательные целые, а дни предыстории отрицательны;
поэтому у предыстории своя ветвь, а не общий ряд с осью.
"""
from collections.abc import Sequence
from enum import IntEnum
import numpy as np
# Каноническое зерно эталонного мира — константа репозитория; опись хранит
# его в паспорте мира. Свои зёрна менти крутит без гарантий описи.
CANONICAL_SEED = 20260601
class Component(IntEnum):
"""Подпотоки внутри дня; порядок объявления — позиция в дереве."""
TRAFFIC = 0
COMMERCE = 1
# Заказная сторона: деньги магазина и судьба заказа. Ветвится по дню
# рождения заказа — слепок несёт семь дней рождения сразу, и судьбу
# каждого заказа читает из его собственного дня
# (docs/architecture/orders/fate.md).
ORDERS = 2
# Событийную сторону — потерю и дубль события — заведёт этап 6; до тех
# пор позиция занята мёртвым именем прежней нарезки, и из него никто не
# бросает.
LATECOMERS = 3
class _Branch(IntEnum):
"""Две ветви корня: постоянный состав мира и проживание дней."""
COMPOSITION = 0
DAYS = 1
class _Era(IntEnum):
"""Две ветви состава: ось событий и предыстория до D0."""
AXIS = 0
PREHISTORY = 1
def _stream(seed: int, position: Sequence[int]) -> np.random.Generator:
"""Подпоток на позиции `position` дерева зерна `seed`."""
sequence = np.random.SeedSequence(entropy=seed, spawn_key=position)
return np.random.Generator(np.random.PCG64(sequence))
def cohort_stream(seed: int, day: int) -> np.random.Generator:
"""Случайность когорты дня `day`; отрицательный день — предыстория."""
era, index = (_Era.AXIS, day) if day >= 0 else (_Era.PREHISTORY, -day - 1)
return _stream(seed, (_Branch.COMPOSITION, era, index))
def day_stream(seed: int, day: int, component: Component) -> np.random.Generator:
"""Случайность одного компонента дня `day` — дня оси, не предыстории."""
if day < 0:
raise ValueError(f"события начинаются в D0: дня {day} на оси нет")
return _stream(seed, (_Branch.DAYS, day, component))