# ADR-0005: Модельные часы генератора, отвязанные от настенного времени Принято: 2026-06-11 Статус: accepted Связано: [ADR-0004](./0004-steady-stream-synthetic-generator.md) (расширяет — steady-stream как синтетический генератор), [`CONTEXT.md`](../../CONTEXT.md) (термины времени и «сида»), мат-спека [`docs/specs/2026-06-10-generator-math-model.md`](../specs/2026-06-10-generator-math-model.md) (разделы «Персистентность» и «Воспроизводимость» написаны от настенных часов — будут реконсилированы отдельным заходом), задача [`06-state-v2-and-restart`](../../.scratch/feature-data-generator/issues/06-state-v2-and-restart.md) (state v2 и правило рестарта построены на настенных часах). > Обновление (2026-06-14): направление «стартовая история стенда» запущено в > [ADR-0006](./0006-generation-as-sole-analytics-source.md); реконсиляция > разделов «Персистентность» и «Воспроизводимость» мат-спеки выполнена в > [спеке модельного времени](../specs/2026-06-14-generator-model-time-and-startup-history.md). ## Решение Модельное время генератора **расцеплено** от настенных часов (`now()`). Генератор потребляет метку такта **извне**; «драйвер часов» задаёт отображение модельного времени на реальное. Шов под это уже существует в коде (такт принимает метку времени параметром). Три режима — это **драйверы поверх одного шва**, а не отдельные архитектуры: - **живой ×1** — модельное время идёт со скоростью настенных часов (текущее поведение); - **живой ускоренный ×K** — модельное время идёт в `K` раз быстрее реального (`K` — конфигурация, дефолт ×1); - **заливка ×∞** — модельное время гонится без сна от стартовой точки до целевой; частный случай `K → ∞`. Живой стенд **масштабируется ×K, дефолт ×1**. Ускорение — учебная «ручка» урока 7: разогнать стенд и вживую увидеть медленные явления (возвраты, межсессионные паузы, сдвиг воронки после правки марковской таблицы переходов). Метки событий пишутся по **модельному** времени. При ускорении/заливке «сейчас» стенда расходится с настенными часами — для аналитического стенда (ключ — `event_timestamp`, внеочередная вставка в ClickHouse нормальна) это **безвредно**; фиксируем как явное свойство, чтобы «время стенда ≠ твои настенные часы» не читалось как баг. Имена параметров, формат конфигурации и алгоритм драйвера — за исполнителем; здесь не фиксируются. ## Контекст Текущая модель неявно держит инвариант **«время генератора ≡ реальное время ×1»**: главный цикл спит до настенного такта, метки событий = `now()` + смещения, а паузы внутри визита «проживаются» в реальном времени. Для учебного плана это неудобно по двум причинам: - **Медленные явления не успеть показать.** Возвраты (~2 ч межсессионной паузы), накопление воронки, удержание разворачиваются часами и днями — в рамках урока на ×1 их не увидеть. - **Историческую глубину живой генератор не создаёт вовсе.** За реальную минуту рождается ~λ событий реального времени; глубину сейчас даёт только статический сид. Дополнительно: воспроизводимость на ×1 завязана на настенный час (дневной коэффициент, метки) — запуск в другой час даёт другой поток. Расцепление с **фиксированной стартовой модельной точкой** эту зависимость снимает. Связь с задачей 06: персистентность (state v2 + рестарт) уже построена на настенных часах. Расцепление меняет смысл «правила 30 минут» — см. «Последствия». Эта ADR — **отдельный слой поверх 06**; она не правит то, что задачи 06/07 уже зафиксировали (их доводила автономная петля субагентов на реал-тайм-модели), реконсиляция идёт отдельным заходом. ## Рассмотренные варианты - **Оставить ×1 (статус-кво).** Отклонено: не решает учебное неудобство; историческую глубину даёт только сид, медленные явления вживую не показать. - **Живой всегда ×1, «быстро» только как разовая заливка прошлого.** Жизнеспособно, но слабее: урок 7 не может вживую ускорить наблюдение, и это два разных механизма (живой стенд и батч-заливка) вместо одного. - **Живой масштабируется ×K, дефолт ×1; заливка = `K → ∞` (принято).** Один механизм поглощает все режимы; ускорение прямо служит уроку 7; дефолт ×1 сохраняет «дыхание» настоящего сайта; пре-заливка прошлого тоже доступна (бакаешь на огромном `K`, живёшь на умеренном). Цена — метки уходят вперёд настенных часов при разгоне (безвредно для аналитики). ## Последствия - Режимы живой / ускоренный / заливка — конфигурация одного драйвера часов; реализация и имена параметров — за исполнителем. - **Правило 30 минут (задача 06) переопределяется в модельном времени.** Формула не меняется: разрыв между модельной меткой следующего события визита и **модельным временем возобновления** больше 30 минут → визит закрывается; меньше → продолжается. Меняется **origin возобновления** — он становится параметром: - **восстановление после краха** — origin = настенное `now()` (время реально прошло) → поведение как сегодня; - **продолжение из заморозки/сида** — origin = метка снимка → разрыв ≈ 0, мир продолжается непрерывно, активные визиты не закрываются. Текущий код 06 берёт `now()` жёстко: корректно для краха, требует параметризации для продолжения. - **Будущее направление (ещё не строим): «стартовая история стенда».** Заливка `K → ∞` + заморозка state v2 даёт сгенерированное прошлое, с которого живой стенд стартует **непрерывно** (те же люди возвращаются — `users < sessions` рождается вживую, без костыля статического сида `users == sessions`). При этом роль статического сида со временем смещается с «учебный датасет» на «dev-фикстура и калибровочный эталон» (термины — в `CONTEXT.md`). **На время разработки** сид и поток **сосуществуют** (как зафиксировано в ADR-0004); продолжение-из-сида — отдельная будущая задача после готовности генератора. - Воспроизводимость улучшается при фиксированной стартовой модельной точке; реконсиляция разделов «Персистентность» и «Воспроизводимость» мат-спеки — отдельный заход после ревью эксперимента с автономной петлёй.