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
@@ -78,6 +78,11 @@ class Config:
os.getenv("GEN_MODEL_T0", "2026-01-01T00:00:00+00:00")
)
)
model_t_end: datetime | None = field(
default_factory=lambda: _parse_optional_model_timestamp(
os.getenv("GEN_MODEL_T_END")
)
)
model_timezone: str = field(
default_factory=lambda: os.getenv("GEN_MODEL_TIMEZONE", "UTC")
)
@@ -122,3 +127,21 @@ class Config:
raise ValueError("GEN_MODEL_TIME_SPEED must be > 0")
if self.run_mode not in {"live", "backfill"}:
raise ValueError("GEN_RUN_MODE must be live or backfill")
if self.model_t_end is not None:
object.__setattr__(
self,
"model_t_end",
self.model_t_end.astimezone(timezone.utc),
)
if self.run_mode == "backfill":
if self.model_t_end is None:
raise ValueError("GEN_MODEL_T_END is required for backfill")
if self.model_t_end <= self.model_t0:
raise ValueError("GEN_MODEL_T_END must be after GEN_MODEL_T0")
def _parse_optional_model_timestamp(value: str | None) -> datetime | None:
"""Разбирает необязательную ISO-метку модельного времени."""
if not value:
return None
return _parse_model_timestamp(value)