docs(handoff): кросс-линейное ревью цепочки генератора

- Зачем:
  - зафиксировать результат кросс-прохода другой родословной по
    c672ed0..ec815ce для передачи в багфикс-цепочку.
- Что:
  - отчёт: 1 critical, 5 high, 10 medium, 4 low; четыре сквозных семейства
    (смешение миров, стенд без live, ложнозелёные проверки, чистый клон).
  - маршрутизация находок и предложение порядка багфикс-цепочки.
- Проверка:
  - находки доказаны исполнением: мутационные тесты, compose-эксперимент,
    зонды на сиде, Context7 по Airflow 2.10.5.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 00:03:44 +03:00
co-authored by Claude Fable 5
parent 549ce0c00d
commit 749ae91a07
@@ -0,0 +1,160 @@
# Кросс-линейное ревью цепочки генератора 12 -> 14 -> 09 -> 10 -> 08
Дата: 2026-07-05 00:02.
Жанр: одноразовый handoff по ADR-0003; отчёт кросс-прохода другой родословной
(Claude) по диапазону `c672ed0..ec815ce`. Бриф — из
`.scratch/handoffs/2026-07-04-generator-chain-review.md`.
Метод: четыре параллельных адверсарных ревьюера с исполняемыми доказательствами
(мутационные тесты на копии, эксперимент с compose-профилем на реальной версии
Compose, зонды на реальном сиде, сверка Airflow API через Context7). Репозиторий
не менялся.
## Вердикт
**CHANGES_REQUESTED.** Счёт: 1 critical, 5 high, 10 medium, 4 low — после
`APPROVED` финального ревью той же линии. Третье подтверждение находки о
родословной: внутренние ревью закрыли локальные дефекты честно, кросс-линия
нашла сквозные сценарии.
## Сквозные семейства (главная ценность прохода)
1. **«Смешение миров» возвращается тремя дорогами.** Инвариант задачи 07
(«чистый стенд, громкий отказ») пробит: (а) state v2 после обновления кода
классифицируется как «повреждённый» → тихий чистый старт нового мира поверх
истории; (б) live-генератор ушёл в compose-профиль, и `make clean`/`down`
его больше **не убивают** — осиротевший генератор старого мира пишет в чистые
топики и переживает перезагрузку (`restart: unless-stopped`); (в) консольные
пути (`make generator-backfill`, `startup-history-import`) остались без
предпроверок, которые пульту сочли обязательными.
2. **Последствия «make up больше не стартует live» не выметены.** Алерт
`Kafka No Messages Produced` горит весь курс на здоровом стенде; уроки 0/5
ожидают поток; `sleep 5` в документированной проверке шва меньше времени
старта генератора — проверка стала пустышкой.
3. **Ложнозелёные проверки.** Superset-тесты пропускают дрейф параметров
(top-15 → 500 строк — 5 тестов зелёные); стыковая проверка сверяет шов там,
куда указал env, а не где он по манифесту; ODS-блок проходит на пустом ODS.
4. **Чистый клон/свежий стенд никто не прошёл.** `uv` не заявлен в
пререквизитах; README-быстрый старт падает без `ddl_init`; пульт вечно висит
на запаused `etl_pipeline`; после `git pull` образ Airflow не пересобирается
(DAG красный); центральное упражнение урока 1 физически невоспроизводимо.
Всё это лежало ровно в вынесенной в риски HITL-приёмке.
## Индекс находок по issue
### Issue 08 (курс) — CHANGES_REQUESTED
- **CRITICAL** `docs/course/lessons/01_kafka_to_clickhouse.md:192-239`
упражнение с `kafka_msg_ts` невоспроизводимо: «перезаливка среза» заменена на
`make generated-history-analytics && make up`, который делает `down -v` и
накатывает эталонный DDL — добавленная менти колонка уничтожается, финальный
SELECT падает; в тексте остался висящий `TRUNCATE` (след нестыковки).
Нужен путь перезаливки без сброса схемы (смена `kafka_group_name` /
`CLEAN_START=0` + повторный backfill) и проверка на стенде.
- **HIGH** `docs/course/README.md:19-34`, `README.md:43-47` — первая команда
курса требует `uv` на хосте; ни пререквизиты, ни quick start его не называют.
- **HIGH** `docs/course/lessons/05_monitoring.md:254,259-262,339` — на штатном
пути (без live) `Kafka No Messages Produced` горит постоянно; урок обещает
«алерты не шумят»; панели описаны для потока, которого нет.
- **MEDIUM** `00_kafka_intro.md:56-65` — служебные топики `generator_state` и
`generator_startup_history_manifest` не объяснены (урок построен на
разглядывании списка топиков).
- **MEDIUM** `00_kafka_intro.md:117-122` vs `05_monitoring.md:156-161`
взаимоисключающие утверждения о видимости consumer lag; проверить на стенде.
- **MEDIUM** `README.md:64-68`, `docs/OPERATIONS.md:175-199`
`make generated-history-check` после `PROFILE=daily-wave` падает: в check
зашит диапазон ci, PROFILE не передаётся.
- **MEDIUM** `docs/OPERATIONS.md:95` — источником STG первым назван архивный
`make data`.
### Issue 12/14 (пульт, профиль) — CHANGES_REQUESTED
- **HIGH** `airflow/dags/generator_control_dag.py:227-235` +
`etl_pipeline_dag.py:263``etl_pipeline` создаётся на паузе; `trigger_etl`
(`wait_for_completion`, без `execution_timeout`) виснет навсегда,
`max_active_runs=1` блокирует повторные запуски. В Airflow 2.10 нет
`fail_when_dag_is_paused` (Context7). Нужна предпроверка «etl_pipeline не на
паузе» + шаг unpause в README/runbook.
- **HIGH** `docker-compose.yml:293-294,334`, Makefile (`down`, `clean`) —
профильный `live-generator` не удаляется `down -v` (доказано экспериментом,
Compose v5.1.3); `restart: unless-stopped` воскрешает его после перезагрузки.
В `down`/`clean`/CLEAN_START нужен `--profile live-generator`.
- **MEDIUM** `README.md:32-40` — быстрый старт без `ddl_init`: precheck падает
сырым `UNKNOWN_TABLE`; добавить шаг и человекочитаемую подсказку.
- **MEDIUM** `airflow/requirements.txt:9-10`, Makefile `up` — после `git pull`
образ Airflow не пересобирается → `ModuleNotFoundError: prometheus_client`,
DAG красный. Нужен `--build` или инструкция.
- **MEDIUM** `docs/OPERATIONS.md:237-241` vs `kafka_io.py:161,253``sleep 5`
меньше времени старта continue (≥10 с): live-строк ноль, `CHECK_LIVE_SEAM=auto`
молча пропускает шов. Было `sleep 130` при тике 60 с.
- **MEDIUM** `airflow_control.py:102-141` vs `run_generator.sh:56-77`,
`import_startup_history_artifact.sh` — предпроверки только в DAG; консольные
пути тихо смешивают миры. Вынести `assert_stand_clean` в общую точку входа.
- **LOW** `service.py:203-215` — простой стенда ×60: ночь = +30 модельных суток
дыры; предупредить в runbook, советовать reset/повторный импорт.
- **LOW** — фиксация Context7-проверки Airflow API только в `.scratch/`, не в
коде/доках (AGENTS.md требует); сами вызовы для 2.10.5 корректны.
### Issue 09 (state v3, фактура) — CHANGES_REQUESTED
- **HIGH** `state.py:236-241``kafka_io.py:181``service.py:100-102`
поднятие STATE_VERSION молча превращает живой стенд во «второй мир»: v2 —
читаемый, несовместимый state, но version-гейт убивает его до семантики,
`from_dict_safe` даёт `None`, сервис тихо стартует новый мир с `model_t0`
поверх истории. Сверка `manifest.state_version != state.version`
(`service.py:246`) недостижима. Спека (строки 198-204, 245-247) требует
громкий отказ; `IncompatibleStateError` уже существует — использовать его при
известной старой версии и `GEN_STATE_RESET=false`; миграционной заметки в
доках нет.
- **MEDIUM** `check_generated_analytics.sh:13,273-289,342,387-393` — проверка
стыка сверяет шов по env-дефолту (`2026-01-01T06:00`), не по манифесту: после
`daily-wave` голый `make generated-history-check` «доказывает» однородность
фальшивого шва посреди backfill. LEFT JOIN `dds.click` структурно пуст; ODS-блок
проходит на пустом ODS. Брать `model_t_end` из манифеста, требовать ненулевые
ODS-строки.
- **MEDIUM** `generation.py:196-209` — предпосылка issue «fallback не
срабатывает» неверна: срабатывает у ~8% визитов (зонд, seed=4242, N=20000);
выпуск не изменился только потому, что у всех доноров сида строки внутри
click_id идентичны — свойство нигде не зафиксировано; при регенерации сида
с вариативностью ~8% визитов поедет. Зафиксировать свойство сида или
ограничить `GEN_MAX_SESSION_EVENTS` длиной максимального донора.
- Ядро фикса крепкое (проверено исполнением): инвариант «рождение ==
восстановление» держится, RNG не потребляется при restore, микросекунды
чинятся честно, форма распределений не сместилась (сверка с `e2d0684^`).
### Issue 10 (Superset) — CHANGES_REQUESTED
- **MEDIUM** `tests/test_superset_dashboard_config.py:64-76` — тесты сверяют
подмножество параметров: мутация «top-15 по убыванию» в экспорте (row_limit
15→500, order_desc→false и др.) проходит все 5 тестов. Нужно полное сравнение
`params` config-vs-export. Классы «возврат world_map» и «рассинхрон layout»
закрыты честно (мутации падают).
- **MEDIUM** issue `10-*:51-68`, `create_dashboard.py:158-185` — приёмка «по
кадру» выполнена декларативно, чарт никем не отрисован; порядок столбцов после
pivot не гарантирован — задать `x_axis_sort: "Events, pcs"`,
`x_axis_sort_asc: false`; снять кадр при HITL.
- **MEDIUM** `create_dashboard.py:505-520` — удаление дублей по глобальному
совпадению имени: повторный прогон молча удалит одноимённый чарт менти;
сузить по `datasource_id`, задокументировать деструктивность.
- **LOW** `create_dashboard.py:176``tooltipTimeFormat` бессмыслен для
категориальной оси; легенда дублирует ось при единственной серии.
- **LOW** `docs/SUPERSET_DASHBOARD.md:205-211` — путь ручного импорта экспорта
(legacy v0) ничем не проверяется.
## Маршрутизация (предложение)
- **Багфикс-цепочка Codex** (кандидат-порядок по риску): (1) семейство
«смешение миров» — 09-HIGH + 12-HIGH-clean + разъезд предпроверок одним
issue; (2) 12-HIGH-паузы + README/ddl_init/build — «свежий стенд» одним
issue; (3) 08-CRITICAL урок 1 + uv + алерт урока 5 — «курс на чистом клоне»;
(4) ложнозелёные проверки (09-MEDIUM-шов, 10-MEDIUM-тесты); (5) остальное.
- **HITL остаётся HITL** (кадры Superset, живой проход Airflow UI, проход по
урокам) — но пункты 08-CRITICAL и 12-HIGH показывают, что часть HITL дешевле
заменить проверками на чистом клоне в CI-манере.
- Отдельно: третий случай подтверждения находки о родословной — кандидат на
датированную поправку в `references/rationale.md` скилла coordinator-loop.
## Что подтверждено как корректное
Стыковка env DAG→генератор; ветвление DAG; отсутствие потребности в Docker
изнутри Airflow; make-цели/имена DAG/DDL-колонки/числа профилей в уроках; ядро
state v3; замена карты в обоих источниках; 172 unit-теста зелёные.