feat(generator): добавлена стартовая история через backfill

- Зачем:
  - стенду нужна повторяемая история с живым продолжением от модельной границы без дублей и разрыва визитов.
- Что:
  - добавлен backfill-режим с `GEN_MODEL_T_END`, manifest и state на `T_end`.
  - live-запуск восстанавливается из manifest без настенной дельты и проверяет совместимость state.
  - добавлены SQL-проверки формы данных, повторяемости и стыка backfill с live.
- Проверка:
  - make generator-test.
  - два чистых ClickHouse-прогона backfill дали одинаковые manifest checksums и digest.
  - reviewer gate issue 05 пройден после исправлений state/manifest.
This commit is contained in:
2026-06-14 19:45:58 +03:00
parent fcb06da16e
commit d5408f28e9
11 changed files with 1509 additions and 21 deletions
+27 -1
View File
@@ -394,6 +394,24 @@ class TickStreamGenerator:
return tick_batch
def drain_until(
self,
cutoff_at: datetime,
include_boundary: bool = False,
) -> dict[str, list[dict]]:
"""Выпускает созревшие события без рождения новых визитов."""
cutoff_time = _normalize_tick_time(cutoff_at)
tick_batch = _empty_batch()
self._release_due_events(
cutoff_time,
tick_batch,
include_boundary=include_boundary,
)
self._drop_finished_visits()
return tick_batch
def _birth_visits(self, event_budget: int, tick_time: datetime) -> None:
if len(self.active_visits) >= self.generator.config.max_active_sessions:
self._pending_visit_births = 0.0
@@ -436,9 +454,17 @@ class TickStreamGenerator:
self,
tick_time: datetime,
tick_batch: dict[str, list[dict]],
include_boundary: bool = True,
) -> None:
for visit in self.active_visits:
while not visit.is_finished and visit.timestamps[visit.next_index] <= tick_time:
while not visit.is_finished:
next_timestamp = visit.timestamps[visit.next_index]
if include_boundary:
is_due = next_timestamp <= tick_time
else:
is_due = next_timestamp < tick_time
if not is_due:
break
event_index = visit.next_index
for topic in TOPICS:
tick_batch[topic].append(visit.batch[topic][event_index])