- Зачем: - тикет #38: состав мира должен быть чистой функцией зерна, а счётчики будущего манифеста — известны до генерации хоть одного события. - Что: - `world.py` — конфигурация мира одним модулем чистых данных: приток, недельная волна, профиль возвратов, доли покупателей и пар, D0. - `seeds.py` — иерархия подпотоков на `SeedSequence`: состав мира (ось и предыстория) отдельно от дней и их компонентов. - `plan.py` — ленивый план состава: когорта дня, аудитория дня из окна возвратов, гарантированные заказы пар, счётчики горизонта. Случайность — только целыми числами. - спека, раздел 1: вторая кука пары рождается по затухающему профилю возвратов; раздел 9: измеренные числа канонического мира, оценка накопленной аудитории поправлена с ≈60 до 68 тыс. - CONTEXT.md: термин «когорта дня»; README генератора — новые модули. - Проверка: - make test (295 тестов), make lint, make typecheck; - тесты проверены мутациями: 12 подмен в плане и конфигурации, каждая роняет ровно свой тест. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
67 lines
2.5 KiB
Python
67 lines
2.5 KiB
Python
"""Свойства иерархии зёрен, на которых держится детерминизм.
|
|
|
|
Проверяется не «числа такие-то» (они зависят от numpy и закреплены
|
|
`uv.lock`), а три обещания спеки, раздел 2: подпоток определяется позицией
|
|
в дереве, а не порядком вычислений; продление истории днём N+1 не трогает
|
|
дни 1…N; правка одного компонента не задевает соседние.
|
|
"""
|
|
|
|
import numpy as np
|
|
|
|
from clickstream_generator.seeds import (
|
|
CANONICAL_SEED,
|
|
Component,
|
|
cohort_stream,
|
|
day_stream,
|
|
)
|
|
|
|
|
|
def first_draws(stream: np.random.Generator) -> list[int]:
|
|
"""Отпечаток подпотока: первые броски целыми."""
|
|
return stream.integers(0, 2**32, 8).tolist()
|
|
|
|
|
|
def test_canonical_seed_is_a_repository_constant():
|
|
assert isinstance(CANONICAL_SEED, int)
|
|
|
|
|
|
def test_stream_is_a_position_in_the_tree_not_an_order_of_calls():
|
|
straight = first_draws(cohort_stream(CANONICAL_SEED, 5))
|
|
cohort_stream(CANONICAL_SEED, 0)
|
|
day_stream(CANONICAL_SEED, 3, Component.TRAFFIC)
|
|
detoured = first_draws(cohort_stream(CANONICAL_SEED, 5))
|
|
assert straight == detoured
|
|
|
|
|
|
def test_cohorts_of_different_days_are_independent():
|
|
draws = [first_draws(cohort_stream(CANONICAL_SEED, day)) for day in range(5)]
|
|
assert len({tuple(draw) for draw in draws}) == len(draws)
|
|
|
|
|
|
def test_prehistory_does_not_collide_with_the_axis():
|
|
"""Дни до D0 отрицательны, позиция в дереве — нет: своя ветвь."""
|
|
for depth in range(1, 5):
|
|
assert first_draws(cohort_stream(CANONICAL_SEED, -depth)) != first_draws(
|
|
cohort_stream(CANONICAL_SEED, depth)
|
|
)
|
|
|
|
|
|
def test_day_components_do_not_share_randomness():
|
|
draws = [
|
|
first_draws(day_stream(CANONICAL_SEED, 3, component)) for component in Component
|
|
]
|
|
assert len({tuple(draw) for draw in draws}) == len(Component)
|
|
|
|
|
|
def test_composition_and_day_are_separate_streams():
|
|
"""Состав мира ветвится сам по себе — день-функция его не сдвигает."""
|
|
assert first_draws(cohort_stream(CANONICAL_SEED, 3)) != first_draws(
|
|
day_stream(CANONICAL_SEED, 3, Component.TRAFFIC)
|
|
)
|
|
|
|
|
|
def test_another_seed_is_another_world():
|
|
assert first_draws(cohort_stream(CANONICAL_SEED, 3)) != first_draws(
|
|
cohort_stream(CANONICAL_SEED + 1, 3)
|
|
)
|