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/12-...` (Airflow-DAG как пульт; дооформлен
дооформление — глаголы, профили, экспорт и импорт уже есть). 2026-07-04: без Docker-доступа из Airflow, конечные операции — Python-код
- Параллельно, независимы: `issues/09-...` (фикс браузерной фактуры на стыке) генератора в тасках, live и полный сброс остаются консолью).
и `issues/10-...` (читаемость гео-карты). - Параллельно, независимы: `issues/09-...` (фикс браузерной фактуры на
стыке), `issues/10-...` (читаемость гео-карты) и `issues/14-...` (быстрый
учебный профиль: суточная волна вживую за ~24 минуты; заведён 2026-07-04,
лучше до 08 — артефакт курса рождается из этого профиля).
- После фикса 09: `issues/13-...` (доливка истории от слепка; без приоритета, - После фикса 09: `issues/13-...` (доливка истории от слепка; без приоритета,
`needs-triage` — доливка тиражирует стыки восстановления, дооформлять после `needs-triage` — доливка тиражирует стыки восстановления, дооформлять после
фикса). фикса).
- Последней: `issues/08-...` (миграция курса на генерацию) — жёстко после 07 - Последней: `issues/08-...` (миграция курса на генерацию) — жёстко после 07
(уроки ссылаются на runbook), мягко после 12 (приёмку уроков человек ведёт (уроки ссылаются на runbook), мягко после 12 (приёмку уроков человек ведёт
уже через пульт). уже через пульт) и после 14 (артефакт курса — из быстрого профиля).
```mermaid ```mermaid
flowchart LR flowchart LR
@@ -105,6 +108,7 @@ flowchart LR
i11 --> i12["12 DAG-пульт"] i11 --> i12["12 DAG-пульт"]
i07 --> i08["08 миграция курса"] i07 --> i08["08 миграция курса"]
i12 -. "приёмка через пульт" .-> i08 i12 -. "приёмка через пульт" .-> i08
i14["14 быстрый профиль"] --> i08
i09["09 фикс стыка"] --> i13["13 доливка"] i09["09 фикс стыка"] --> i13["13 доливка"]
i10["10 гео-карта"] i10["10 гео-карта"]
``` ```
@@ -67,6 +67,10 @@ startup-history/backfill -> Kafka -> STG -> ODS -> DDS -> DM -> Superset
отдельный проект), шпаргалки удалены из репозитория 2026-07-04 (см. поправку в отдельный проект), шпаргалки удалены из репозитория 2026-07-04 (см. поправку в
шапке `docs/course/PRD.md`). шапке `docs/course/PRD.md`).
Рекомендуемый режим ревью по coordinator-loop: обычный (документы и уроки, кода
генератора и state задача не трогает); человеческая приёмка — проход по урокам
через пульт (HITL).
## Blocked by ## Blocked by
- Жёстких блокеров нет: штатный путь (`make generated-history-analytics`) уже - Жёстких блокеров нет: штатный путь (`make generated-history-analytics`) уже
@@ -79,3 +83,8 @@ startup-history/backfill -> Kafka -> STG -> ODS -> DDS -> DM -> Superset
человеческая приёмка миграции — это проход по урокам 00–05, и вести её удобнее человеческая приёмка миграции — это проход по урокам 00–05, и вести её удобнее
через DAG-пульт, а не через консоль. Поэтому эта задача идёт последней в через DAG-пульт, а не через консоль. Поэтому эта задача идёт последней в
очереди — после 12. Порядок всей очереди — в PRD, раздел «Задачи». очереди — после 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 — пульт управления генератором # Airflow-DAG — пульт управления генератором
@@ -11,45 +11,159 @@ Status: needs-triage
Даже с runbook и глаголами управление генератором остаётся консольным. Идея Даже с runbook и глаголами управление генератором остаётся консольным. Идея
(пользователь, 2026-06-14; приоритет поднят 2026-07-04): параметризованный DAG (пользователь, 2026-06-14; приоритет поднят 2026-07-04): параметризованный DAG
в Airflow как основной человеческий интерфейс стенда — форма в веб-UI, где в Airflow как основной человеческий интерфейс стенда — форма в веб-UI, где
выбираются режим, длительность/профиль, seed, скорость. Плюсы: валидация выбираются операция, профиль, длительность. Плюсы: валидация настроек против
настроек против манифеста ещё до запуска, повторные попытки, наглядный статус. манифеста ещё до запуска, наглядный статус, понятные ошибки.
Учебный бонус: такой DAG сам по себе учебный материал — живой пример Учебный бонус: такой DAG сам по себе учебный материал — живой пример
параметризованной оркестрации (тема урока 4), что ближе к цели курса, чем параметризованной оркестрации (тема урока 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 ## 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), ## Acceptance criteria
экспортировать артефакт, импортировать артефакт, сбросить мир.
- Валидация параметров против манифеста **до** запуска операции; при
несовместимости — понятная ошибка в UI, а не тихий новый мир.
- Конечные операции (backfill, экспорт, импорт, сброс) — таски DAG.
Непрерывный live — долгоживущий сервис compose: DAG его стартует и
останавливает, но не держит внутри таска.
- Под капотом DAG вызывает глаголы из задачи 11, а не собирает env-матрицу.
## Acceptance criteria (черновые, дооформить после 07 и 11) - [ ] После `make up` (генератор не автостартует) стартовую историю выбранного
профиля/длительности можно создать из веб-UI Airflow, не открывая консоль и
- [ ] Стартовую историю выбранной длительности можно создать из веб-UI Airflow, не выставляя переменных окружения; следом ETL запускается из той же цепочки
не выставляя переменных окружения вручную. и `check` зелёный.
- [ ] Несовместимые параметры отклоняются до запуска с указанием разошедшихся - [ ] Импорт артефакта из веб-UI: несовместимый артефакт отклоняется **до**
полей. записи в топики, в логе таска — перечень разошедшихся полей.
- [ ] Импорт/экспорт артефакта доступны как операции DAG. - [ ] Backfill/import на непустом стенде отклоняются предпроверкой с
- [ ] Runbook дополнен разделом «пульт в Airflow» как основным путём. подсказкой про `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 ## Notes
- Проверить API параметров DAG (params/Datasets) через MCP Context7 по правилу - Перед реализацией сверить API формы (`Param`, enum, описания полей) и
репозитория: спорные API Airflow сверять с актуальной документацией. `TriggerDagRunOperator` для Airflow 2.10 через MCP Context7 — правило
- DAG должен остаться понятным менти: это витрина оркестрации, не место для репозитория.
хитрой логики. Сложность — в глаголах генератора (задача 11), не здесь. - `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`операции - `07-startup-history-portable-artifact-and-usage-docs.md`сделана.
экспорта/импорта появляются там. - `11-generator-launch-verbs-and-profiles.md` — сделана.
- `11-generator-launch-verbs-and-profiles.md` — DAG оборачивает глаголы и - Мягкая связь с `14-fast-teaching-profile.md`: быстрый профиль появится в
профили; без них он превращается в переводчик формы в env-матрицу. выпадашке сам, если список профилей брать из `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 (артефакт курса рождается из этого
профиля — эту задачу лучше сделать до него).