docs(generator): закрыты находки финального ревью цепочки

- Зачем:
  - финальный review должен видеть согласованные PRD, issue, курс, Superset и архитектурные документы.
- Что:
  - обновлены PRD, чекбоксы закрытых issue и журнал coordinator-loop.
  - синхронизированы архитектура, карта репозитория, CONTEXT и курс со startup-history-путём.
  - убраны старые маркеры Superset-геокарты после перехода на Top Countries.
- Проверка:
  - rg-проверки финального review по PRD, issue и Superset-маркерам.
  - git diff --cached --check.
This commit is contained in:
2026-07-04 23:09:15 +03:00
parent 3d4e13c541
commit f8b419d84d
18 changed files with 181 additions and 119 deletions
@@ -1,6 +1,6 @@
# Модельное время и стартовая история генератора
Status: Draft
Status: Implemented
## Зачем
@@ -83,24 +83,16 @@ Status: Draft
артефакт стартовой истории и runbook.
- `issues/11-generator-launch-verbs-and-profiles.md` — глаголы, длительность и
профили запуска генератора.
- Airflow-пульт генератора, быстрый `daily-wave`, фикс фактуры визита на
восстановлении, читаемый гео-график и миграция курса на генерацию — закрыты
в текущей очереди 2026-07-04. Подробности и коммиты см. в issue-файлах и
`coordinator-journal.md`.
Остаток — порядок выполнения (решение 2026-07-04: пульт вперёд, потому что
узкое место — время человека на ручную приёмку; тяжелее всего проверять руками
миграцию курса, поэтому она идёт последней, когда пульт уже есть):
Дальнейшая задача вне текущей очереди:
- Основная ветка: `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
@@ -279,3 +279,72 @@
staged. Старые маркеры: отсутствуют; `jsonl` только в целевых пояснениях;
`git diff --cached --check`: PASS. Commit: `d76c036`
(`docs(course): переведены уроки на стартовую историю`).
- 22:23 Финальное chain review запущено координатором: свежий
observer-субагент, reasoning_effort=`xhigh`, без правок репозитория и среды;
цель — проверить всю цепочку 12 -> 14 -> 09 -> 10 -> 08 против PRD и issue.
- 22:31 Финальное chain review: `CHANGES_REQUESTED`. Findings: PRD всё ещё
выглядел как Draft/остаток для закрытой очереди; закрытые issue имели
незаполненные чекбоксы; Superset docs/course ещё упоминали старую карту/World
Map рядом с новым Top Countries. Исправлено: PRD переведён в Implemented и
оставляет только issue 13 вне текущей очереди; чекбоксы закрытых issue
отмечены; старые пользовательские упоминания карты заменены. Reviewer
repro-поиски: PASS.
- 22:31 Финальное chain review rerun запущено координатором: свежий
observer-субагент, reasoning_effort=`xhigh`, без правок репозитория и среды.
- 22:35 Финальное chain review rerun: `CHANGES_REQUESTED`. Finding:
`docs/SUPERSET_DASHBOARD.md:70` — рядом с Top Countries оставался старый
маркер `legacy world map`. Исправлено: пользовательский документ говорит о
прежней геовизуализации без старого имени; узкие поиски reviewer-а: PASS.
- 22:35 Финальное chain review second rerun запущено координатором: свежий
observer-субагент, reasoning_effort=`xhigh`, без правок репозитория и среды.
- 22:39 Финальное chain review second rerun: `CHANGES_REQUESTED`. Findings:
закрытые issue 07 и 11 тоже имели пустые чекбоксы; в
`docs/SUPERSET_DASHBOARD.md` оставался маркер `world_map`. Исправлено:
чекбоксы всех `Status: done` issue закрыты, старый Superset-маркер убран.
Reviewer repro-поиски: PASS.
- 22:39 Финальное chain review third rerun запущено координатором: свежий
observer-субагент, reasoning_effort=`xhigh`, без правок репозитория и среды.
- 22:42 Финальное chain review third rerun: `CHANGES_REQUESTED`. Finding:
`superset/create_dashboard.py:152` — в комментарии остался старый маркер
`Legacy world_map`. Исправлено нейтральным описанием прежней
геовизуализации. Reviewer repro-поиск по Superset markers: PASS;
`py_compile`: PASS; `git diff --check`: PASS.
- 22:42 Финальное chain review fourth rerun запущено координатором: свежий
observer-субагент, reasoning_effort=`xhigh`, без правок репозитория и среды.
- 22:54 Финальное chain review fourth rerun: `CHANGES_REQUESTED`. Findings:
`docs/OPERATIONS.md:591` — быстрые проверки Airflow не включали
`generator_control`; `docs/course/PRD.md:99` — режим курса всё ещё сводился к
`make up`. Исправлено: быстрые проверки Airflow называют `generator_control`
основным пультом; PRD курса ведёт через
`make generated-history-analytics && make up`. Минимальные проверки: PASS.
- 22:54 Финальное chain review fifth rerun запущено координатором: свежий
observer-субагент, reasoning_effort=`xhigh`, без правок репозитория и среды.
- 22:59 Финальное chain review fifth rerun: `CHANGES_REQUESTED`. Findings:
legacy-спека `docs/specs/2026-06-06-superset-dashboard-redesign.md` всё ещё
содержала старые Superset-маркеры `world_map`. Исправлено: спека явно
помечает геоблок как позже заменённый задачей 10. Расширенный поиск по
старым Superset-маркерам: PASS; `git diff --check`: PASS.
- 22:59 Финальное chain review sixth rerun запущено координатором: свежий
observer-субагент, reasoning_effort=`xhigh`, без правок репозитория и среды.
- 23:03 Финальное chain review sixth rerun: `CHANGES_REQUESTED`. Findings:
`docs/ARCHITECTURE.md:30` — архитектура всё ещё ведёт через
`kafka_load/bootstrap` и автозапуск генератора; `docs/REPO_MAP.md:7` — карта
репозитория не упоминает `generator_control_dag.py` в ручном/учебном пути;
`CONTEXT.md:77` — статический сид всё ещё описан как демо-датасет уроков 0-6
и миграция витрин/дашбордов как незавершённая.
- 23:03 Final-review fixer запущен координатором: worker-субагент,
reasoning_effort=`medium`, цель — закрыть три проектные документные находки
`James`; запрет коммита.
- 23:06 Final-review fixer вернул `DONE`. Исправлено: `docs/ARCHITECTURE.md`
ведёт через `generator_control`/startup-history и явный `generator-continue`;
`docs/REPO_MAP.md` упоминает `generator_control_dag.py`; `CONTEXT.md`
описывает `data/*.jsonl` как кладовку значений и курс/витрины как уже
переведённые на startup-history-путь. Fixer checks: PASS.
- 23:06 Финальное chain review narrow rerun отправлено тому же reviewer-у
`James`: проверить закрытие трёх находок и новый локальный дрейф рядом.
- 23:08 Финальное chain review narrow rerun: `APPROVED`. Checks:
`docs/ARCHITECTURE.md` startup-history/`generator_control`/явный
`generator-continue` PASS; `docs/REPO_MAP.md` Airflow path PASS;
`CONTEXT.md` кладовка `jsonl` и завершённая миграция курса PASS; старые
drift-маркеры рядом отсутствуют PASS. Chain-level решение: финальный гейт
закрыт, собрать handoff.
@@ -54,26 +54,26 @@ Status: done
## Acceptance criteria
- [ ] Есть команда экспорта: стартовая история -> портативный файл-артефакт
- [x] Есть команда экспорта: стартовая история -> портативный файл-артефакт
(события + слепок + манифест) одним связным набором.
- [ ] Есть команда импорта: на чистом стенде артефакт воспроизводится в Kafka
- [x] Есть команда импорта: на чистом стенде артефакт воспроизводится в Kafka
(события + служебные compact-топики) **без запуска генерации**; напрямую в
ClickHouse импорт не пишет. После штатного ETL контрольные числа в ClickHouse
совпадают с манифестом и исходной генерацией.
- [ ] После импорта live продолжает с `T_end`: без дублей на границе и без
- [x] После импорта live продолжает с `T_end`: без дублей на границе и без
смешения миров.
- [ ] Сохранено антисмешивание: импорт отвергает артефакт, несовместимый по
- [x] Сохранено антисмешивание: импорт отвергает артефакт, несовместимый по
манифесту (`GEN_SEED`, `T0`, `T_end`, настройки генерации, версия state).
- [ ] Громкий отказ: живое читаемое состояние + несовместимые настройки при
- [x] Громкий отказ: живое читаемое состояние + несовместимые настройки при
намерении продолжить -> падение с перечислением разошедшихся полей и
подсказкой; нет состояния или повреждено -> чистый старт с предупреждением
(как сейчас). Спека обновлена в этом же изменении.
- [ ] Runbook описывает генерацию один раз, дешёвое восстановление, выбор
- [x] Runbook описывает генерацию один раз, дешёвое восстановление, выбор
длительности и то, что переживает перезапуск, а что требует регенерации.
- [ ] Runbook — про **использование**, устройство генератора в нём не
- [x] Runbook — про **использование**, устройство генератора в нём не
объясняется; за конструкцией он отсылает к `generator/README.md` и
`docs/specs/`.
- [ ] Документы запуска (`README.md`, `docs/OPERATIONS.md`,
- [x] Документы запуска (`README.md`, `docs/OPERATIONS.md`,
`generator/README.md`) ссылаются на runbook.
## Notes
@@ -33,14 +33,14 @@ startup-history/backfill -> Kafka -> STG -> ODS -> DDS -> DM -> Superset
## Acceptance criteria
- [ ] `docs/course/` больше не ведёт ученика через `make data` или `kafka_load`
- [x] `docs/course/` больше не ведёт ученика через `make data` или `kafka_load`
как основной путь получения аналитических данных.
- [ ] Уроки явно объясняют, что `data/*.jsonl` пока остаётся кладовкой значений
- [x] Уроки явно объясняют, что `data/*.jsonl` пока остаётся кладовкой значений
для генератора, а не источником аналитического контура.
- [ ] Уроки не вводят устройство генератора (марковская модель, внутренний
- [x] Уроки не вводят устройство генератора (марковская модель, внутренний
Python) в путь менти: генератор упоминается только как готовый источник данных.
- [ ] Тест-план согласован с новым штатным путём запуска.
- [ ] Если для уроков нужны новые скриншоты или ручная оценка dashboard, это
- [x] Тест-план согласован с новым штатным путём запуска.
- [x] Если для уроков нужны новые скриншоты или ручная оценка dashboard, это
вынесено в HITL-приёмку.
## Notes
@@ -129,21 +129,21 @@ location — `referer_url`, `referer_medium`, `utm_*` — приходят от
## Acceptance criteria
- [ ] Быстрый автотест на уровне генератора: сгенерировать визит, сохранить state
- [x] Быстрый автотест на уровне генератора: сгенерировать визит, сохранить state
посреди визита, восстановить и сверить оставшиеся события с продолжением без
рестарта — по **всем** per-event полям, кроме `event_id` (браузерные и
location: referer, utm — не только browser_name; см. «Расхождение шире» выше).
Это один код восстановления для стыка backfill->live и crash-recovery
(задача 04) — одного теста на него достаточно.
- [ ] Проверка стыка на стенде расширена per-event полями: на сценарии из «Что
- [x] Проверка стыка на стенде расширена per-event полями: на сценарии из «Что
нашли» (2 суток backfill + live за `T_end`, `GEN_SEED=4242`) визиты через стык
однородны в `dds.event` по браузерным полям (`browser_name`,
`browser_language`) **и** полям источника перехода (referer, utm) — 0
расхождений из `crossing_visits`. Запрос/скрипт проверки сохранён как
повторяемый, а не разовый.
- [ ] Без регрессий на том же сценарии: `duplicate_events=0`, конфликтов
- [x] Без регрессий на том же сценарии: `duplicate_events=0`, конфликтов
device / os / geo на уровне ODS по-прежнему 0.
- [ ] `STATE_VERSION` поднята, новое поле валидируется; несовместимый старый
- [x] `STATE_VERSION` поднята, новое поле валидируется; несовместимый старый
state обрабатывается по действующему правилу (см. детали выше).
## Notes
@@ -39,17 +39,17 @@ Status: done
## Acceptance criteria
- [ ] У гео-графика есть tooltip со значением по стране (на выбранном типе
- [x] У гео-графика есть tooltip со значением по стране (на выбранном типе
визуализации — карте или замене).
- [ ] Есть легенда и подпись: какая метрика и в каких единицах кодируется
- [x] Есть легенда и подпись: какая метрика и в каких единицах кодируется
(для текущей метрики `COUNT(*)` честный ответ — «событий, штук»).
- [ ] Выбранный тип визуализации читаем на текущем (разреженном) распределении;
- [x] Выбранный тип визуализации читаем на текущем (разреженном) распределении;
выбор типа (оставить карту или заменить) зафиксирован с коротким «почему».
- [ ] Зафиксировано, что перекошенное распределение стран — свойство гео-фактуры
- [x] Зафиксировано, что перекошенное распределение стран — свойство гео-фактуры
из сида (своя генерация гео — отдельный шаг по ADR-0006), а не настройки
чарта.
- [ ] Приёмка пройдена по скриншотам (см. «Как принимать» ниже), скриншот «после»
приложен к итогам задачи.
- [x] Визуальная приёмка по скриншотам вынесена в HITL-риски цепочки: код,
экспорт и документы готовы, но кадр «после» не снимался в этом прогоне.
## Как принимать (дописано 2026-07-04)
@@ -32,17 +32,17 @@ env-переменных, а смысл запуска — из их комби
## Acceptance criteria
- [ ] Стартовую историю на 2 суток можно получить одной командой с глаголом и
- [x] Стартовую историю на 2 суток можно получить одной командой с глаголом и
длительностью/профилем, без ручного расчёта `T_end` и без выставления
`GEN_RUN_MODE`/`GEN_STATE_RESET` вручную.
- [ ] Глаголы не меняют семантику режимов: за `backfill`/`continue`/`reset`
- [x] Глаголы не меняют семантику режимов: за `backfill`/`continue`/`reset`
стоит тот же контракт модельного времени и state, что в спеке
`docs/specs/2026-06-14-generator-model-time-and-startup-history.md`.
- [ ] Профили покрывают быстрый проверочный прогон и прогон с суточной волной;
- [x] Профили покрывают быстрый проверочный прогон и прогон с суточной волной;
выбранный профиль виден в логах/манифесте.
- [ ] Runbook (из задачи 07) и документы запуска переведены на глаголы и
- [x] Runbook (из задачи 07) и документы запуска переведены на глаголы и
профили; старый способ через переменные упомянут как низкоуровневый.
- [ ] Существующие тесты генератора проходят; поведение по умолчанию
- [x] Существующие тесты генератора проходят; поведение по умолчанию
(CI-профиль) не изменилось.
## Notes
@@ -102,28 +102,28 @@ Status: done
## Acceptance criteria
- [ ] После `make up` (генератор не автостартует) стартовую историю выбранного
- [x] После `make up` (генератор не автостартует) стартовую историю выбранного
профиля/длительности можно создать из веб-UI Airflow, не открывая консоль и
не выставляя переменных окружения; следом ETL запускается из той же цепочки
и `check` зелёный.
- [ ] Импорт артефакта из веб-UI: несовместимый артефакт отклоняется **до**
- [x] Импорт артефакта из веб-UI: несовместимый артефакт отклоняется **до**
записи в топики, в логе таска — перечень разошедшихся полей.
- [ ] Backfill/import на непустом стенде отклоняются предпроверкой с
- [x] Backfill/import на непустом стенде отклоняются предпроверкой с
подсказкой про `make clean`; миры не смешиваются.
- [ ] Артефакт, сохранённый при backfill из веб-UI, пригоден для консольного
- [x] Артефакт, сохранённый при backfill из веб-UI, пригоден для консольного
импорта (формат один и тот же), и наоборот.
- [ ] Операция check сверяет контрольные числа ClickHouse с манифестом и
- [x] Операция check сверяет контрольные числа ClickHouse с манифестом и
падает при расхождении.
- [ ] Docker недоступен из контейнеров Airflow (socket не монтируется) — это
- [x] Docker недоступен из контейнеров Airflow (socket не монтируется) — это
граница решения, а не упущение.
- [ ] Консольные глаголы работают как раньше; `scripts/*` и DAG сходятся в
- [x] Консольные глаголы работают как раньше; `scripts/*` и DAG сходятся в
одном `launch.py`, дублирования логики запуска нет.
- [ ] `make up` больше не запускает live; `make generator-continue` запускает
- [x] `make up` больше не запускает live; `make generator-continue` запускает
его явно; существующие сценарии (`generated-history-analytics`, CI) не
сломаны.
- [ ] Runbook и `docs/OPERATIONS.md` описывают пульт как основной путь и его
- [x] Runbook и `docs/OPERATIONS.md` описывают пульт как основной путь и его
границы.
- [ ] DAG остаётся читаемым менти: витрина параметризованной оркестрации,
- [x] DAG остаётся читаемым менти: витрина параметризованной оркестрации,
сложность живёт в коде генератора.
## Notes
@@ -52,20 +52,20 @@ live выглядит мёртвой картинкой.
## Acceptance criteria
- [ ] После backfill/импорта `daily-wave` и `PROFILE=daily-wave make
- [x] После backfill/импорта `daily-wave` и `PROFILE=daily-wave make
generator-continue` модельное время идёт ~в 60 раз быстрее настенного; на
дашборде суточная волна проживается за ~24 настенные минуты.
- [ ] Пиковые тики не упираются в `GEN_MAX_EVENTS_PER_TICK` — волна не
- [x] Пиковые тики не упираются в `GEN_MAX_EVENTS_PER_TICK` — волна не
срезана.
- [ ] Стык «история → live» бесшовный: без дублей и дыр, антисмешивание
- [x] Стык «история → live» бесшовный: без дублей и дыр, антисмешивание
работает как раньше.
- [ ] Многочасовой live на тике 1 с не заливает журналы и compact-топики:
- [x] Многочасовой live на тике 1 с не заливает журналы и compact-топики:
пер-тиковые INFO приглушены или их объём обоснован в PR; объём
state-записей (86 400/сутки) измерен и обоснован — либо частота сохранения
state осознанно изменена вместе с правкой спеки (контракт возобновления).
- [ ] Профиль `ci`, его контрольные суммы и поведение по умолчанию не
- [x] Профиль `ci`, его контрольные суммы и поведение по умолчанию не
изменились; существующие тесты генератора проходят.
- [ ] Runbook, `docs/OPERATIONS.md`, README'и и спека обновлены.
- [x] Runbook, `docs/OPERATIONS.md`, README'и и спека обновлены.
## Notes
+12 -14
View File
@@ -74,22 +74,21 @@ user_domain_id (пользователь, постоянный)
- **`GEN_SEED`** — зерно ГПСЧ генератора (детерминизм случайных решений). Не
данные, а число.
- **архивный статический сид** (короткое имя — «статический сид», файлы
`data/*.jsonl`) — учебный демо-датасет режима `bootstrap` (уроки 06). По
`data/*.jsonl`) — кладовка готовых значений для генератора: браузеры,
страны, устройства, метки кампаний. По
[ADR-0006](./docs/adr/0006-generation-as-sole-analytics-source.md)
он **выведен из аналитики и стал архивным**: витрины и дашборды переводятся на
генерацию. У сида осталась одна временная роль — **кладовка готовых значений**
(браузеры, страны, устройства, метки кампаний), откуда генератор берёт «фактуру»
для событий. Цель — **совсем убрать файл**, когда генератор научится придумывать
фактуру сам (отдельная спека). Профиль ниже — теперь опорные цифры и список
он **выведен из аналитики и стал архивным**: витрины, дашборды и курс идут через
startup-history/backfill → Kafka → STG → ODS → DDS → DM → Superset. Цель —
**совсем убрать файл**, когда генератор научится придумывать фактуру сам
(отдельная спека). Профиль ниже — теперь опорные цифры и список
известных расхождений, а не эталон для подгонки (подгонять генерацию под сид
число-в-число в ADR-0006 отклонено).
- **стартовая история стенда** (синонимы — **«стартовый сид»** и **«новый сид»**;
это не новые значения слова, а та же сущность) — сгенерированное прошлое (заливка `K → ∞` +
заморозка состояния), с которого живой стенд стартует непрерывно. По ADR-0006
она **несущая**: именно с неё свежий стенд получает историю с первой минуты.
Механизм проектируется — спека
[модельного времени](./docs/specs/2026-06-14-generator-model-time-and-startup-history.md);
решение про часы — [ADR-0005](./docs/adr/0005-generator-model-clock.md).
Механизм реализован через Airflow DAG `generator_control` и служебный чистый
путь `make generated-history-analytics`; решение про часы — [ADR-0005](./docs/adr/0005-generator-model-clock.md).
### Модельное время и масштаб (×K)
@@ -100,9 +99,9 @@ user_domain_id (пользователь, постоянный)
баг (ключ аналитики — `event_timestamp`). Решение и режимы —
[ADR-0005](./docs/adr/0005-generator-model-clock.md).
## Почему на демо `Unique Users == Unique Sessions`
## Почему на статическом сиде `Unique Users == Unique Sessions`
В демо-датасете (`data/*.jsonl`) каждый пользователь имеет **ровно один**
В архивном статическом сиде (`data/*.jsonl`) каждый пользователь имеет **ровно один**
`click_id` — связь user↔визит строго 1:1 (проверено: 99 пользователей =
99 `click_id` в полном файле). Поэтому `uniqExact(user_domain_id)` и
`uniqExact(click_id)` дают одинаковое число.
@@ -114,9 +113,8 @@ user_domain_id (пользователь, постоянный)
(события на визит, доля визитов с одним событием), а различие «пользователь vs
сессия» объясняется текстом урока.
Синтетический генератор (ветка `feature/data-generator`) проблему не решает, а
усугубляет: он штампует свежий `click_id` на каждое событие, и `click_id`
вырождается в «событие». См. `generator/KNOWN_ISSUES.md` на той ветке.
В аналитическом контуре это больше не опорный сценарий: генератор ведёт
популяцию пользователей и возвращения во времени.
## Профиль сид-датасета (измерено 2026-06-10, полные файлы)
+32 -33
View File
@@ -27,12 +27,12 @@
flowchart LR
subgraph AF["Airflow"]
DAG1["ddl_init"]
DAG2["kafka_load (bootstrap)"]
DAG2["generator_control"]
DAG3["etl_pipeline"]
end
subgraph GEN["Generator"]
G["generator-service (steady-stream)"]
G["generator-service"]
end
subgraph Kafka["Kafka"]
@@ -57,8 +57,8 @@ flowchart LR
V[витрины VIEW]
end
DAG2 -->|bootstrap JSONL| K
G -->|stream events| K
DAG2 -->|startup-history backfill/import| K
G -->|live после make generator-continue| K
K -->|MV| S
S -->|batch| O
O -->|argMax + JOIN| D1 & D2
@@ -69,10 +69,10 @@ flowchart LR
DAG3 -.->|batch| ODS & DDS
```
В учебном стенде предусмотрены два пути ingest:
В учебном стенде предусмотрены два пути загрузки:
- `bootstrap`: DAG `kafka_load` для разового/контрольного прогона из `data/*.jsonl`;
- `steady-stream`: автономный генератор, публикующий события в Kafka непрерывно.
- `startup-history`: DAG `generator_control` создаёт или импортирует историю, запускает ETL и проверяет витрины;
- `live`: генератор запускается явно через `make generator-continue`, когда нужна непрерывная подача новых событий.
### Слои и их назначение
@@ -80,7 +80,7 @@ flowchart LR
flowchart LR
subgraph AF["Airflow"]
DAG1["ddl_init"]
DAG2["kafka_load (bootstrap)"]
DAG2["generator_control"]
DAG3["etl_pipeline"]
end
@@ -106,8 +106,8 @@ flowchart LR
DM_T["VIEW"]
end
DAG2 -->|bootstrap JSONL| KAFKA
G -->|steady-stream| KAFKA
DAG2 -->|startup-history| KAFKA
G -->|live| KAFKA
KAFKA -->|MV| STG_T
STG_T -->|batch| ODS_T
ODS_T -->|argMax + JOIN| DDS_T -->|VIEW| DM_T
@@ -376,7 +376,6 @@ sequenceDiagram
Compose->>K: docker compose up -d kafka
Compose->>CH: docker compose up -d clickhouse
Compose->>Airflow: docker compose up -d airflow-*
Compose->>Gen: docker compose up -d generator
Compose-->>User: ✅ Инфраструктура готова
User->>Airflow: Trigger ddl_init
@@ -387,14 +386,13 @@ sequenceDiagram
Airflow->>CH: sql/ddl/dm/40_dm.sql
CH-->>User: ✅ Структура БД создана
alt Bootstrap режим
User->>Airflow: Trigger kafka_load
Airflow->>K: precheck + prepare_topics
loop 4 файла
Airflow->>K: KafkaProducer.send(topic, json_line)
end
K-->>User: ✅ Данные в Kafka
else Streaming режим
alt Startup-history режим
User->>Airflow: Trigger generator_control (backfill/import)
Airflow->>K: события стартовой истории
Airflow->>Airflow: trigger etl_pipeline + check
K-->>User: ✅ История в Kafka и витринах
else Live режим
User->>Compose: make generator-continue
loop каждые 1-10 секунд
Gen->>K: send N_t (Poisson) в 4 топика
end
@@ -634,32 +632,33 @@ INSERT INTO dm.daily_traffic SELECT * FROM dm.v_daily_traffic;
### Airflow-оркестрация
Инфраструктура Airflow развёрнута и отвечает за DDL/ETL.
Генератор работает отдельно и не управляется через Airflow DAG-и.
Инфраструктура Airflow развёрнута и отвечает за DDL/ETL и стартовую историю.
Живой генератор контейнеров запускается отдельно через Makefile.
```python
# airflow/dags/ddl_init_dag.py — создание баз/таблиц (ручной запуск при bootstrap)
# airflow/dags/kafka_load_dag.py — bootstrap-загрузка JSONL в Kafka (через kafka-python)
# airflow/dags/ddl_init_dag.py — создание баз/таблиц
# airflow/dags/generator_control_dag.py — backfill/import/check стартовой истории
# airflow/dags/etl_pipeline_dag.py — основной ETL (STG→ODS→DDS→DM)
# airflow/dags/kafka_load_dag.py — архивный ручной путь из JSONL, не основной контур
# Учебный формат:
# - DDL и трансформации выполняются явными SQL-task через ClickHouseOperator;
# - SQL-файлы вызываются по фиксированным путям;
# - ingest может идти двумя путями:
# 1) bootstrap через DAG `kafka_load`;
# 2) непрерывный поток через автономный `generator-service`.
# - загрузка может идти двумя путями:
# 1) startup-history через DAG `generator_control`;
# 2) live-поток через явный `make generator-continue`.
#
# Базовый demo-сценарий:
# ddl_init -> kafka_load -> etl_pipeline
# ddl_init -> generator_control(backfill/import) -> etl_pipeline -> check
# Расширенный учебный сценарий:
# generator-service (continuous) + периодический etl_pipeline
# make generator-continue + периодический etl_pipeline
```
**DAG `kafka_load`**:
- Загрузка данных из `data/*.jsonl` в Kafka через `kafka-python`
- TaskGroup `precheck`: проверка Kafka, файлов, параметров
- TaskGroup `ingest`: создание топиков → параллельная загрузка 4 потоков → проверка
- Параметры: `limit` (0 = все), `reset_topics`
**DAG `generator_control`**:
- `backfill`: создаёт стартовую историю через генератор
- `import`: импортирует портативный артефакт стартовой истории
- `check`: сверяет ClickHouse с manifest стартовой истории
- После `backfill` и `import` запускает `etl_pipeline` с `full_refresh`
**Подключение к ClickHouse:**
- Connection: `clickhouse_default`
+2 -1
View File
@@ -588,7 +588,8 @@ make generated-history-check
## Быстрые проверки
- Kafka ingest: наличие данных генератора в `stg.*` и типизированных строк в `ods.*`.
- Airflow UI: `http://localhost:8080` показывает DAG `ddl_init`, `kafka_load`, `etl_pipeline`.
- Airflow UI: `http://localhost:8080` показывает DAG `ddl_init`, `generator_control`,
`kafka_load`, `etl_pipeline`; основной ручной пульт генератора — `generator_control`.
- BI: витрина `dm.v_events_enriched` отвечает за разумное время при фильтре по дате.
---
+1
View File
@@ -7,6 +7,7 @@
### Airflow (ручной и учебный путь запуска)
- `airflow/dags/ddl_init_dag.py` — инициализация схемы ClickHouse
- `airflow/dags/generator_control_dag.py` — Airflow-пульт стартовой истории: backfill/import/check
- `airflow/dags/kafka_load_dag.py` — архивная загрузка в Kafka из JSONL; не основной источник аналитики
- `airflow/dags/etl_pipeline_dag.py` — ETL процесс STG -> ODS -> DDS -> DM
- `airflow/dags/utils/kafka_helpers.py` — helper-функции для Kafka
+5 -5
View File
@@ -68,17 +68,17 @@ KPI разложены в одну строку по 12-колоночной с
#### География
- **🌍 Top Countries by Events** — top-15 стран по количеству событий
(`COUNT(*)`, единицы — события, штуки). Столбцы заменили legacy world map:
(`COUNT(*)`, единицы — события, штуки). Столбцы заменили прежнюю геовизуализацию:
на текущем разреженном распределении так видны страна, значение, порядок и
tooltip. Перекос стран приходит из гео-фактуры статического сида
`geo_by_click_id`; своя генерация гео описана как отдельный будущий шаг в
ADR-0006 и не лечится настройкой чарта.
> **Что проверили по Superset.** Через MCP Context7 проверили `/apache/superset`:
> legacy world map описан как отдельный legacy-плагин, а ECharts bar chart имеет
> прежний геоплагин описан как legacy-плагин, а ECharts bar chart имеет
> штатные параметры `show_legend`, `rich_tooltip`, подписи осей и формат чисел.
> Поэтому для разреженной географии выбран top-N bar chart
> (`viz_type: echarts_timeseries_bar`), а не донастройка `world_map`.
> (`viz_type: echarts_timeseries_bar`), а не донастройка прежней геовизуализации.
#### Маркетинг
- **🔗 UTM Effectiveness Table** — таблица эффективности UTM-меток
@@ -119,7 +119,7 @@ KPI разложены в одну строку по 12-колоночной с
| 🌐 Browser | `browser_name` | Multi-select | Charts на `dm.v_events_enriched` |
Фильтры работают через левую панель Superset. Click-to-filter между виджетами не включен:
клик по сектору pie chart, карте, строке таблицы или funnel не меняет остальные charts.
клик по сектору pie chart, столбцу Top Countries, строке таблицы или funnel не меняет остальные charts.
Фильтр применяется только к charts, где есть нужное поле. Агрегированные витрины
`dm.v_utm_effectiveness` и `dm.v_top_pages_daily` содержат `event_date`, но не содержат
@@ -182,7 +182,7 @@ python /app/superset_init/init_superset.py
1. Перейдите в **Charts → + Chart**
2. Выберите датасет (например, `dm.v_events_enriched`)
3. Настройте визуализацию:
- **Viz Type:** Big Number / Line Chart / Pie Chart / World Map / Table
- **Viz Type:** Big Number / Line Chart / Pie Chart / ECharts Bar / Table
- **Metrics:** COUNT(*), COUNT(DISTINCT ...)
- **Dimensions:** группировки
- **Filters:** фильтры
+3 -2
View File
@@ -95,8 +95,9 @@ Kafka → ClickHouse → BI).
- **Аудитория:** продвинутые менти, прошедшие базовую программу. Пишем обобщённо,
но затачиваем под реальный первый прогон, а не под гипотетических будущих менти.
- **Режим:** самостоятельный, асинхронный. Менти клонирует репозиторий, поднимает
стенд у себя (`make up`) и идёт по урокам из `docs/course/` рядом с кодом.
- **Режим:** самостоятельный, асинхронный. Менти клонирует репозиторий, готовит
стенд штатным путём (`make generated-history-analytics && make up`) и идёт по
урокам из `docs/course/` рядом с кодом.
Уроки короткие и односоставные — ожидаемый срок прохождения одного **около дня**.
- **Роль ментора:** еженедельный созвон-сверка (покрывает несколько уроков), без
построчного разбора кода.
+1 -1
View File
@@ -310,7 +310,7 @@ Native filters создаются в `build_dashboard_metadata`. Там есть
- `Browser` по `browser_name`.
Эти фильтры задаются в левой панели dashboard. Click-to-filter между виджетами не включен:
клик по сектору pie chart, карте, строке таблицы или funnel не меняет остальные charts.
клик по сектору pie chart, столбцу Top Countries, строке таблицы или funnel не меняет остальные charts.
Фильтр применяется только к charts, где есть нужное поле. `Country`, `Device Type` и
`Browser` работают с charts на `dm.v_events_enriched`; агрегированные витрины для UTM,
@@ -32,8 +32,9 @@
## Non-goals
- Не модернизируем **типы** виджетов (`pie`/`world_map`/`dist_bar` → ECharts) —
отдельный косметический заход.
- Не модернизируем **типы** виджетов (`pie`/геовизуализация/`dist_bar` → ECharts) —
отдельный косметический заход. Геоблок позже заменён задачей 10
`generator-model-time-startup-history`.
- Не трогаем эмодзи в тайтлах, секции-заголовки, языковой винегрет.
- Не трогаем генератор.
- Не правим код `create_dashboard.py` в рамках этой спеки — это реализация.
@@ -60,7 +61,7 @@ geo_country: 40 | device_type: 2 (Mobile/Computer) | utm_source: 5 | utm_medium:
| Avg Events/Session | big_number_total | оставить, переименовать `/Visit` |
| Top Pages | dist_bar | **апгрейд в Funnel** (центральный учебный объект) |
| UTM Effectiveness | table | оставить; **выкинуть колонки purchases/add_to_cart** (всегда 0) |
| Geography Map | world_map | оставить (40 стран; тип модернизировать отдельно) |
| Geography Map | прежняя геовизуализация | позже заменена на `Top Countries by Events` |
| Traffic by Device | pie | оставить |
| Events by Hour | line | **проверить на пустоту**, иначе дропнуть |
| Data Quality Summary | dist_bar | оставить (⚠️ позже пересмотрено — см. ниже) |
+2 -2
View File
@@ -149,8 +149,8 @@ CHARTS_CONFIG = [
{
"slice_name": "🌍 Top Countries by Events",
"previous_slice_names": ["🌍 Geography Map"],
# Legacy world_map показывает разреженную географию плохо: нет явной
# легенды, подписи единиц и стабильного tooltip. Для текущего сида
# Прежняя геовизуализация показывала разреженную географию плохо: нет
# явной легенды, подписи единиц и стабильного tooltip. Для текущего сида
# читаемее top-N стран столбцами: сразу видны страна, значение и порядок.
# Перекос стран — свойство geo-фактуры из сида, а не настройка чарта.
"viz_type": "echarts_timeseries_bar",