Зачем Стенд поднимался пустым, и всякая приёмка следующих этапов начиналась с ручной заливки данных. Теперь `make up` сам приводит стенд к одному и тому же состоянию, а в git лежит то, чем это состояние проверяется. Что - Опись мира `data/world-inventory.json`: паспорт (зерно, версия генератора, хеш каталога) и по строке на каждый из восьми дней — дата, число событий, хеш байтов. Собирается `make inventory`, свежесть сторожит `test_inventory.py` — тем же способом, что свежесть описания выгрузки. - Разовая служба `world-init` вышла из-под профиля и играет в топик восемь дней при каждом подъёме; зависимый у неё — `airflow-init`, иначе `--wait` считает успешно отработавшую службу упавшей. - `scripts/wait-for-world.sh` — вторая половина `make up`: приём асинхронный, поэтому ждать надо доезда до `ods.event`, а не завершения заливки. Ограниченный цикл опроса, не пауза наугад. - Девятая проверка `make check-clickhouse`: подневный счёт событий против описи, рамка по датам стартового мира, счёт через `FINAL`. При расхождении называет, где искать, — в событиях или в браке. - Порог «день ≤ 30 с» снят из спеки генератора в обоих местах: замер дал 1,7 с, порог был выше факта в восемнадцать раз. На его месте — замеры с датой. Раздел 9 спеки закрыт: открытых вопросов не осталось. - Слова: «манифест» стал описью мира, «зерновой мир» — стартовым миром (решение владельца). Оба заведены в словарь CONTEXT.md. Проверка `make clean && make up` с нуля — 2 м 50 с, доехало ровно 401 185 событий. `make check-clickhouse` зелёный (8 с), `make smoke` зелёный (9 с), `make test` — 407 тестов за 71 с, `make lint`, `make typecheck`, `make config-test` зелёные. Что проверка умеет краснеть, снято двумя поломками: снос партиции 2026-06-03 дал диагноз «не доехали до ODS», негодная строка в сырье — «сломан разбор». Строки опыта убраны, день переигран, счёт вернулся. Тест свежести проверен молчаливой правкой цены в каталоге: покраснел. Ссылка: #42
73 lines
3.4 KiB
Python
73 lines
3.4 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
|
|
DISCREPANCIES = 2
|
|
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))
|