docs(issues): дооформлены задачи 12 и 14, обновлён граф бэклога генератора

- Зачем:
  - подготовить цепочку 12 -> 14 -> 09 -> 10 -> 08 к передаче в coordinator-loop:
    задаче 12 не хватало решений после 07 и 11, задача 14 родилась из обсуждения
    учебного UX (суточная волна вживую).
- Что:
  - issue 12 (DAG-пульт): решения 2026-07-04 — без Docker-доступа из Airflow,
    операции как Python-код генератора в тасках, генератор без автостарта,
    границы пульта, предпроверки чистоты (топики + STG), ожидание ETL перед
    check; учтены находки адверсарного ревью и ревью Codex.
  - issue 14 (новая): профиль daily-wave переводится на speed=60 при тике 1 с —
    модельный час за настенную минуту без изменения фактуры мира.
  - issue 08: мягкие зависимости от 14 и 09 (артефакт курса рождается после
    них), режим ревью; PRD: задача 14 в списке и графе, 12 помечена дооформленной.
- Проверка:
  - вычитка: статусы задач ready-for-agent, порядок в PRD и Blocked by
    согласованы; mermaid-граф рендерится.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 20:42:46 +03:00
co-authored by Claude Fable 5
parent 0a240e3077
commit c672ed0329
4 changed files with 255 additions and 34 deletions
@@ -88,16 +88,19 @@ Status: Draft
узкое место — время человека на ручную приёмку; тяжелее всего проверять руками
миграцию курса, поэтому она идёт последней, когда пульт уже есть):
- Основная ветка: `issues/12-...` (Airflow-DAG как пульт; после 11 вернуть на
дооформление — глаголы, профили, экспорт и импорт уже есть).
- Параллельно, независимы: `issues/09-...` (фикс браузерной фактуры на стыке)
и `issues/10-...` (читаемость гео-карты).
- Основная ветка: `issues/12-...` (Airflow-DAG как пульт; дооформлен
2026-07-04: без Docker-доступа из Airflow, конечные операции — Python-код
генератора в тасках, live и полный сброс остаются консолью).
- Параллельно, независимы: `issues/09-...` (фикс браузерной фактуры на
стыке), `issues/10-...` (читаемость гео-карты) и `issues/14-...` (быстрый
учебный профиль: суточная волна вживую за ~24 минуты; заведён 2026-07-04,
лучше до 08 — артефакт курса рождается из этого профиля).
- После фикса 09: `issues/13-...` (доливка истории от слепка; без приоритета,
`needs-triage` — доливка тиражирует стыки восстановления, дооформлять после
фикса).
- Последней: `issues/08-...` (миграция курса на генерацию) — жёстко после 07
(уроки ссылаются на runbook), мягко после 12 (приёмку уроков человек ведёт
уже через пульт).
уже через пульт) и после 14 (артефакт курса — из быстрого профиля).
```mermaid
flowchart LR
@@ -105,6 +108,7 @@ flowchart LR
i11 --> i12["12 DAG-пульт"]
i07 --> i08["08 миграция курса"]
i12 -. "приёмка через пульт" .-> i08
i14["14 быстрый профиль"] --> i08
i09["09 фикс стыка"] --> i13["13 доливка"]
i10["10 гео-карта"]
```
@@ -67,6 +67,10 @@ startup-history/backfill -> Kafka -> STG -> ODS -> DDS -> DM -> Superset
отдельный проект), шпаргалки удалены из репозитория 2026-07-04 (см. поправку в
шапке `docs/course/PRD.md`).
Рекомендуемый режим ревью по coordinator-loop: обычный (документы и уроки, кода
генератора и state задача не трогает); человеческая приёмка — проход по урокам
через пульт (HITL).
## Blocked by
- Жёстких блокеров нет: штатный путь (`make generated-history-analytics`) уже
@@ -79,3 +83,8 @@ startup-history/backfill -> Kafka -> STG -> ODS -> DDS -> DM -> Superset
человеческая приёмка миграции — это проход по урокам 00–05, и вести её удобнее
через DAG-пульт, а не через консоль. Поэтому эта задача идёт последней в
очереди — после 12. Порядок всей очереди — в PRD, раздел «Задачи».
- Мягкая зависимость от `14-fast-teaching-profile.md` и
`09-seam-browser-fixture-not-preserved.md` (дополнено 2026-07-04): обе меняют
мир — 14 переводит профиль `daily-wave` на быстрый темп, 09 меняет схему
state. Артефакт стартовой истории для уроков должен родиться **после** них,
иначе он протухнет по антисмешиванию и проход по урокам придётся повторять.
@@ -1,4 +1,4 @@
Status: needs-triage
Status: ready-for-agent
# Airflow-DAG — пульт управления генератором
@@ -11,45 +11,159 @@ Status: needs-triage
Даже с runbook и глаголами управление генератором остаётся консольным. Идея
(пользователь, 2026-06-14; приоритет поднят 2026-07-04): параметризованный DAG
в Airflow как основной человеческий интерфейс стенда — форма в веб-UI, где
выбираются режим, длительность/профиль, seed, скорость. Плюсы: валидация
настроек против манифеста ещё до запуска, повторные попытки, наглядный статус.
выбираются операция, профиль, длительность. Плюсы: валидация настроек против
манифеста ещё до запуска, наглядный статус, понятные ошибки.
Учебный бонус: такой DAG сам по себе учебный материал — живой пример
параметризованной оркестрации (тема урока 4), что ближе к цели курса, чем
устройство генератора.
## Решения (дооформление 2026-07-04)
- **Без Docker-доступа из Airflow.** Docker socket в контейнеры Airflow не
прокидываем. Конечные операции — это обычный Python-код генератора, которому
нужны только настройки и Kafka; таски DAG выполняют его сами, в worker'е
Airflow. Урок из проекта `airflow-greenplum-solution` (пользователь): DAG
говорит с управляемой системой по сетевому протоколу, жизненный цикл
контейнеров — только хост/Makefile.
- **Границы пульта.** `make up`, `make clean` (полный сброс) и старт/стоп
live-сервиса — консоль; операции `continue` в пульте нет намеренно.
Backfill и import работают только на чистом стенде (пустые топики данных);
грязный стенд лечится `make clean` с консоли — пульт при отказе прямо
подсказывает это.
- **Генератор не автостартует.** Сейчас `make up` поднимает live-генератор
вместе со стендом (`docker-compose.yml`, `restart: unless-stopped`) — с
пультом это неверно: любой backfill был бы сразу отбит предпроверкой, а
выключить live из UI нельзя. Live становится явным действием
(`make generator-continue`); механизм — например, compose-профиль для
сервиса `generator`, выбрать при реализации и поправить make-цели.
- **Три операции, а не четыре.** `backfill` | `import` | `check`. Отдельного
`export` нет: артефакт в коде пишется только по ходу backfill
(`StartupHistoryArtifactBuilder` внутри `_run_backfill`), «выгрузки задним
числом» не существует — поэтому у backfill есть поле `artifact_path`
(«сохранить артефакт», пусто — не сохранять).
- **Один DAG** с параметром «операция», а не несколько DAG по операциям.
- **После backfill/import DAG сам запускает ETL** (`TriggerDagRunOperator` на
существующий ETL-DAG) и **дожидается его завершения** перед check:
`wait_for_completion` в Airflow 2.10.5 по умолчанию `False` (сверено по
Context7, ревью Codex 2026-07-04) — простой trigger запустил бы check
раньше конца ETL. Менти видит цепочку «мир → пайплайн → витрины».
## What to build
Черновой контур (уточнить после задач 07 и 11):
- **Код генератора доступен таскам Airflow.** Варианты: смонтировать
`generator/src` томом (как уже смонтирован `./sql`) и добавить в
`PYTHONPATH`, либо ставить пакет в `Dockerfile.airflow`. Монтировать нужно
во все Airflow-сервисы (scheduler, webserver, worker): список профилей в
форме читается из `PROFILES` при разборе DAG. Критерий выбора — правка
генератора не должна требовать лишних пересборок.
- **Выравнивание зависимостей:** версии сейчас расходятся — kafka-python
2.0.5 у генератора против 2.0.6 в `airflow/requirements.txt`; привести к
одной. `prometheus-client` в образе Airflow нет — либо добавить, либо (лучше)
точка входа backfill не поднимает HTTP-сервер метрик (`service.py:65-66`
вызывается безусловно — понадобится небольшая склейка; это единственное
место, где честно появляется новый код, зафиксировать его в PR).
- **DAG `generator_control`** в `airflow/dags/`: `schedule=None`,
`max_active_runs=1`, форма запуска на `Param`:
- `operation`: `backfill` | `import` | `check` (выпадающий список);
- `profile`: из `PROFILES` (`launch.py`), список брать динамически;
- `duration`: строка вида `6h`/`2d`, пусто — из профиля;
- переопределения мира для backfill (минимум скорость и seed) — через
механизм `overrides` в `build_launch_env`;
- `artifact_path`: для backfill — куда сохранить артефакт (пусто — не
сохранять), для import — что импортировать; дефолт в общем томе `./data`.
- **Ветвление по операции** — `BranchPythonOperator`, в стиле существующих
DAG стенда.
- **Каждая операция начинается с валидации, падение — до любых записей:**
- backfill и import: топики данных пусты (переиспользовать
`KafkaTopicInspector.assert_data_topics_empty`) **и STG-таблицы
ClickHouse пусты** — батч-ETL пересобирает ODS из всего STG
(`sql/ods/20_stg_to_ods.sql`), поэтому пустых топиков мало: старый мир в
STG смешался бы с новым. При отказе в логе — подсказка про `make clean`;
- import дополнительно: артефакт читается, манифест совместим с выбранными
настройками (готовые проверки `startup_history_artifact.py`), в лог —
перечень разошедшихся полей;
- вспомогательно: проверка «live не работает» по метрикам
`generator:9109` внутри сети compose (см. Notes об ограничениях).
- **Тела операций — вызовы существующего кода:** `build_launch_env` + прогон
генерации до `T_end` (backfill), функции `startup_history_artifact`
(import), сверка контрольных чисел с манифестом (check). Источник манифеста
для check — compact-топик (он есть и после backfill, и после import);
артефакт — запасной вариант.
- `retries=0` у содержательных тасков: оператор должен сразу видеть ошибку
(проверенный приём из greenplum-проекта).
- **Документация:** в runbook `docs/runbooks/startup-history.md` — раздел
«Пульт в Airflow» как основной путь и явные границы пульта (что остаётся
консолью и почему, включая «continue в пульте нет намеренно»); тонкость
нестандартного мира: `make generator-continue` для мира с переопределёнными
настройками требует тех же настроек, громкий отказ подскажет разошедшиеся
поля. Ссылки из `docs/OPERATIONS.md` и `README.md`; правки compose и
Makefile описать в том же PR (правило репозитория).
- Параметризованный DAG с операциями: создать стартовую историю (backfill),
экспортировать артефакт, импортировать артефакт, сбросить мир.
- Валидация параметров против манифеста **до** запуска операции; при
несовместимости — понятная ошибка в UI, а не тихий новый мир.
- Конечные операции (backfill, экспорт, импорт, сброс) — таски DAG.
Непрерывный live — долгоживущий сервис compose: DAG его стартует и
останавливает, но не держит внутри таска.
- Под капотом DAG вызывает глаголы из задачи 11, а не собирает env-матрицу.
## Acceptance criteria
## Acceptance criteria (черновые, дооформить после 07 и 11)
- [ ] Стартовую историю выбранной длительности можно создать из веб-UI Airflow,
не выставляя переменных окружения вручную.
- [ ] Несовместимые параметры отклоняются до запуска с указанием разошедшихся
полей.
- [ ] Импорт/экспорт артефакта доступны как операции DAG.
- [ ] Runbook дополнен разделом «пульт в Airflow» как основным путём.
- [ ] После `make up` (генератор не автостартует) стартовую историю выбранного
профиля/длительности можно создать из веб-UI Airflow, не открывая консоль и
не выставляя переменных окружения; следом ETL запускается из той же цепочки
и `check` зелёный.
- [ ] Импорт артефакта из веб-UI: несовместимый артефакт отклоняется **до**
записи в топики, в логе таска — перечень разошедшихся полей.
- [ ] Backfill/import на непустом стенде отклоняются предпроверкой с
подсказкой про `make clean`; миры не смешиваются.
- [ ] Артефакт, сохранённый при backfill из веб-UI, пригоден для консольного
импорта (формат один и тот же), и наоборот.
- [ ] Операция check сверяет контрольные числа ClickHouse с манифестом и
падает при расхождении.
- [ ] Docker недоступен из контейнеров Airflow (socket не монтируется) — это
граница решения, а не упущение.
- [ ] Консольные глаголы работают как раньше; `scripts/*` и DAG сходятся в
одном `launch.py`, дублирования логики запуска нет.
- [ ] `make up` больше не запускает live; `make generator-continue` запускает
его явно; существующие сценарии (`generated-history-analytics`, CI) не
сломаны.
- [ ] Runbook и `docs/OPERATIONS.md` описывают пульт как основной путь и его
границы.
- [ ] DAG остаётся читаемым менти: витрина параметризованной оркестрации,
сложность живёт в коде генератора.
## Notes
- Проверить API параметров DAG (params/Datasets) через MCP Context7 по правилу
репозитория: спорные API Airflow сверять с актуальной документацией.
- DAG должен остаться понятным менти: это витрина оркестрации, не место для
хитрой логики. Сложность — в глаголах генератора (задача 11), не здесь.
- Перед реализацией сверить API формы (`Param`, enum, описания полей) и
`TriggerDagRunOperator` для Airflow 2.10 через MCP Context7 — правило
репозитория.
- `Config` генератора читает env процесса при создании: в таске собирать
окружение через `build_launch_env` и применять к процессу таска (executor —
LocalExecutor, таск живёт в своём процессе). Не забыть `GEN_DATA_DIR`:
`build_launch_env` его не задаёт, дефолт `/data`, а в контейнерах Airflow
данные смонтированы в `/opt/airflow/data` — без явной установки `Config()`
упадёт, словари `*.jsonl` не найдутся.
- Права на файлы: артефакт из worker'а пишется uid'ом Airflow (50000); в
контейнере генератора `./data` смонтирован **read-only**, консольные
скрипты монтируют каталог артефактов отдельно. Чтение работает везде,
перезапись чужого файла — не всегда; одну строку об этом — в runbook.
- Проверка `generator:9109` — вспомогательная эвристика, не защита: ошибка
DNS означает «сервис не поднят» (не ошибку таска); при крашлупе после
громкого отказа порт мигает (HTTP-сервер стартует до валидации state);
эфемерные контейнеры `docker compose run` под этим именем не видны. Основная
защита от смешивания — пустота топиков данных.
- Backfill — блокирующий таск на минуты; при `max_active_runs=1` для ручного
стенда это нормально.
- Грабли greenplum-проекта, применимые здесь: DAG создаются на паузе — в
инструкции приёмки не забыть unpause перед trigger; автоматические прогоны
гонять через REST API, а не CLI внутри контейнера.
- Возможное развитие (не в скоупе): операция «очистить данные» из пульта
(прецедент удаления/пересоздания топиков из DAG уже есть в
`airflow/dags/utils/kafka_helpers.py`) — сняла бы требование `make clean`
перед новым миром.
- Артефакты `daily-wave`, сделанные до задачи 14, протухнут после неё
(антисмешивание отработает громко) — при пересечении работ это ожидаемо.
- Рекомендуемый режим ревью по coordinator-loop: обычный — state и
сериализацию задача не трогает, код генератора переиспользуется как есть.
## Blocked by
## Зависимости (проверенные предпосылки)
- `07-startup-history-portable-artifact-and-usage-docs.md`операции
экспорта/импорта появляются там.
- `11-generator-launch-verbs-and-profiles.md` — DAG оборачивает глаголы и
профили; без них он превращается в переводчик формы в env-матрицу.
- `07-startup-history-portable-artifact-and-usage-docs.md`сделана.
- `11-generator-launch-verbs-and-profiles.md` — сделана.
- Мягкая связь с `14-fast-teaching-profile.md`: быстрый профиль появится в
выпадашке сам, если список профилей брать из `PROFILES` динамически;
блокером не является.
@@ -0,0 +1,94 @@
Status: ready-for-agent
# Быстрый учебный профиль: суточная волна за минуты занятия
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## Why
Мир по умолчанию живёт со скоростью настенных часов (`speed=1`): чтобы
наблюдать суточную волну вживую, стенд должен непрерывно работать сутки.
Столько стенды включёнными не живут, и люди не вытерпят (пользователь,
2026-07-04). Стартовая история закрывает только половину проблемы: она даёт
закономерности **в статике**, но правый край графика ползёт 1:1 — на занятии
live выглядит мёртвой картинкой.
Решение пользователя (2026-07-04): учебному миру нужна скорость 60 — модельный
час за настенную минуту, полная суточная волна проживается за ~24 минуты
занятия.
## What to build
- Перевести профиль `daily-wave` (`launch.py`) на `GEN_MODEL_TIME_SPEED=60`
при `GEN_TICK_SECONDS=1`. Модельный тик остаётся прежним: 1 настенная
секунда × 60 = 60 модельных секунд, поэтому фактура потока (крупность пачек,
бюджет тика до 72 событий в пике: 60/мин × 1.2) не отличается от
сегодняшней, и колпак `GEN_MAX_EVENTS_PER_TICK` трогать не нужно.
- Наивный вариант «speed=60 при тике 60 с» отвергнут при разборе
(2026-07-04): модельный тик стал бы часом; первые события всех визитов тика
получают один таймстемп — старт тика (`runtime.py:433`,
`generation.py:198`) — история слиплась бы в почасовые гребёнки, а пиковый
тик (4320 событий) упёрся бы в колпак 1000 и срезал бы верхушку волны.
- **Оценить цену тика в 1 Гц.** Сейчас каждый live-тик пишет state в
compact-топик, запись в историю пачек и несколько INFO-строк
(`service.py:566, 584-585, 591-600`): на тике 1 с это 86 400 сохранений и
~полмиллиона строк лога за настенные сутки против 1 440 сегодня. Пер-тиковые
INFO приглушить до DEBUG (или логировать раз в N тиков); частоту сохранения
state менять только с оглядкой на спеку — она входит в контракт
возобновления после сбоя.
- Профиль `ci` не трогать: это дефолт CI, его мир и контрольные суммы должны
остаться прежними.
- Обновить документы, где живут предположения о старом `daily-wave`:
runbook `docs/runbooks/startup-history.md` (таблица профилей; продолжение
мира — `PROFILE=daily-wave make generator-continue`; старые артефакты
профиля становятся несовместимыми — антисмешивание отработает громко, это
ожидаемо), `docs/OPERATIONS.md` (smoke-последовательность со `sleep 130`
ожидание двух минутных тиков — на тике 1 с теряет смысл), `README.md`,
`generator/README.md`.
- Спеку `docs/specs/2026-06-14-generator-model-time-and-startup-history.md`
дополнить: выбранная пара tick/speed и почему именно она.
## Acceptance criteria
- [ ] После backfill/импорта `daily-wave` и `PROFILE=daily-wave make
generator-continue` модельное время идёт ~в 60 раз быстрее настенного; на
дашборде суточная волна проживается за ~24 настенные минуты.
- [ ] Пиковые тики не упираются в `GEN_MAX_EVENTS_PER_TICK` — волна не
срезана.
- [ ] Стык «история → live» бесшовный: без дублей и дыр, антисмешивание
работает как раньше.
- [ ] Многочасовой live на тике 1 с не заливает журналы и compact-топики:
пер-тиковые INFO приглушены или их объём обоснован в PR; объём
state-записей (86 400/сутки) измерен и обоснован — либо частота сохранения
state осознанно изменена вместе с правкой спеки (контракт возобновления).
- [ ] Профиль `ci`, его контрольные суммы и поведение по умолчанию не
изменились; существующие тесты генератора проходят.
- [ ] Runbook, `docs/OPERATIONS.md`, README'и и спека обновлены.
## Notes
- Нагрузка по событиям: ~42–72 события в секунду (ночь–пик) — незаметно для
Kafka и ClickHouse; backfill не дорожает (число тиков на 2 суток то же, что
сейчас: модельный шаг тика не изменился).
- Если генерация тика займёт больше 1 настенной секунды, тики пойдут реже и
фактическая скорость окажется чуть ниже 60 (`service.py:623-627`) — это
допустимо, точная скорость не обещается.
- Несовместимость со старыми мирами ловится по-разному: импорт артефакта и
continue от стартовой истории сверяют полный набор настроек (включая
`tick_seconds` в манифесте), а обычный live-continue — только
seed/T0/timezone/speed (`service.py:213-230`); для этой правки хватает и
его — speed изменился.
- Альтернатива «третий профиль fast-live» отвергнута: у медленного
`daily-wave` не остаётся сценария использования, а лишний профиль усложняет
выпадашку пульта (задача 12).
- Рекомендуемый режим ревью по coordinator-loop: обычный (значения профиля и
логирование). Исключение: если по ходу решат менять частоту сохранения
state — это уже контракт возобновления, эскалировать в **гейт**.
## Blocked by
Нет: профили из задачи 11 сделаны. Мягкие связи: задача 12 (профиль появится
в форме пульта автоматически) и задача 08 (артефакт курса рождается из этого
профиля — эту задачу лучше сделать до него).