feat(generator): глагол next-day — следующий модельный день от слепка

- Зачем:
  - режим кормления стенда порциями: менти триггерит «следующий день»,
    видит полный цикл DWH за один шаг (задача 13, вариант 2 — генерация
    от слепка T_end).
- Что:
  - новый ограниченный режим next-day: восстановление мира из state,
    генерация ровно [T_end, T_end+24h), публикация данные -> state ->
    манифест (манифест — точка фиксации, автоотката нет).
  - операция next-day в DAG generator_control: своя предпроверка границы
    вместо clean-guard, идемпотентность через параметр expected_t_end.
  - цепочка границ — накопительное поле boundaries в манифесте, старый
    формат читается как [T0, T_end]; импорт не изменён.
  - новая проверка цепочки (make generated-history-chain-check): непарные
    счётчики и однородность по каждой границе, явный статус нулевого
    стыка, хвост за границей по всем четырём топикам, литералы в UTC
    с микросекундами.
  - документация OPERATIONS.md: глагол, предпроверка, восстановление
    после сбоя, ограничение retention; в задаче 13 — решения двух слепых
    ревью постановки и кода с аргументами отклонений.
- Проверка:
  - make test: 204 теста генератора + 31 контракт корня, зелёные.
  - make generated-history-chain-check: зелёный, 2 внутренние границы,
    непарные счётчики нулевые; учебный цикл: DM 322 -> 10026 -> 19196
    за два next-day подряд.
  - make generated-history-runtime-check (регрессия задачи 20): зелёный.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 23:53:48 +03:00
co-authored by Claude Fable 5
parent 540989d358
commit f5dee26eda
17 changed files with 1501 additions and 31 deletions
@@ -30,14 +30,15 @@ def test_trigger_form_has_expected_param_enums():
"""Форма запуска ограничивает операции и профили."""
text = DAG_PATH.read_text(encoding="utf-8")
assert 'enum=["backfill", "import", "check"]' in text
assert 'enum=["backfill", "import", "next-day", "check"]' in text
assert "enum=sorted(PROFILES)" in text
assert '"duration": Param(' in text
assert '"artifact_path": Param(' in text
assert '"expected_t_end": Param(' in text
def test_dag_branches_and_waits_for_etl_completion():
"""Backfill/import запускают ETL и ждут его завершения перед check."""
"""Backfill/import/next-day запускают ETL и ждут завершения перед check."""
text = DAG_PATH.read_text(encoding="utf-8")
assert "BranchPythonOperator" in text
@@ -48,6 +49,23 @@ def test_dag_branches_and_waits_for_etl_completion():
assert 'failed_states=["failed"]' in text
def test_next_day_has_own_boundary_precheck_and_serial_execution():
"""Next-day не использует clean-guard и не допускает параллельных запусков."""
text = DAG_PATH.read_text(encoding="utf-8")
assert "schedule=None" in text
assert "max_active_runs=1" in text
assert 'return "check_etl_not_paused_before_next_day"' in text
assert "assert_next_day_snapshot" in text
assert "assert_expected_t_end" in text
next_day_precheck = text.split("def precheck_next_day", maxsplit=1)[1].split(
"\ndef ", maxsplit=1
)[0]
assert "assert_stand_clean" not in next_day_precheck
assert "assert_live_generator_not_running" in next_day_precheck
assert "run_next_day" in text
def test_generator_control_prechecks_etl_dag_not_paused_before_waiting():
"""Пульт проверяет паузу etl_pipeline до долгого ожидания."""
text = DAG_PATH.read_text(encoding="utf-8")
@@ -62,8 +80,10 @@ def test_generator_control_prechecks_etl_dag_not_paused_before_waiting():
assert "fail_when_dag_is_paused" in text
assert 'return "check_etl_not_paused_before_backfill"' in text
assert 'return "check_etl_not_paused_before_import"' in text
assert 'return "check_etl_not_paused_before_next_day"' in text
assert "check_etl_not_paused_before_backfill >> precheck_backfill_task" in text
assert "check_etl_not_paused_before_import >> precheck_import_task" in text
assert "check_etl_not_paused_before_next_day >> precheck_next_day_task" in text
assert text.index("check_etl_not_paused_before_backfill >> precheck_backfill_task") < text.index(
"precheck_backfill_task >> backfill_task"
)