diff --git a/.scratch/generator-model-time-startup-history/PRD.md b/.scratch/generator-model-time-startup-history/PRD.md index e9a7e68..266f185 100644 --- a/.scratch/generator-model-time-startup-history/PRD.md +++ b/.scratch/generator-model-time-startup-history/PRD.md @@ -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 diff --git a/.scratch/generator-model-time-startup-history/coordinator-journal.md b/.scratch/generator-model-time-startup-history/coordinator-journal.md index 48ed0ef..d518279 100644 --- a/.scratch/generator-model-time-startup-history/coordinator-journal.md +++ b/.scratch/generator-model-time-startup-history/coordinator-journal.md @@ -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. diff --git a/.scratch/generator-model-time-startup-history/issues/07-startup-history-portable-artifact-and-usage-docs.md b/.scratch/generator-model-time-startup-history/issues/07-startup-history-portable-artifact-and-usage-docs.md index 733b5b9..d169ddb 100644 --- a/.scratch/generator-model-time-startup-history/issues/07-startup-history-portable-artifact-and-usage-docs.md +++ b/.scratch/generator-model-time-startup-history/issues/07-startup-history-portable-artifact-and-usage-docs.md @@ -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 diff --git a/.scratch/generator-model-time-startup-history/issues/08-migrate-course-from-archive-seed.md b/.scratch/generator-model-time-startup-history/issues/08-migrate-course-from-archive-seed.md index ca73b15..4b44b0c 100644 --- a/.scratch/generator-model-time-startup-history/issues/08-migrate-course-from-archive-seed.md +++ b/.scratch/generator-model-time-startup-history/issues/08-migrate-course-from-archive-seed.md @@ -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 diff --git a/.scratch/generator-model-time-startup-history/issues/09-seam-browser-fixture-not-preserved.md b/.scratch/generator-model-time-startup-history/issues/09-seam-browser-fixture-not-preserved.md index 2a3494f..a64807a 100644 --- a/.scratch/generator-model-time-startup-history/issues/09-seam-browser-fixture-not-preserved.md +++ b/.scratch/generator-model-time-startup-history/issues/09-seam-browser-fixture-not-preserved.md @@ -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 diff --git a/.scratch/generator-model-time-startup-history/issues/10-dashboard-geo-map-readability.md b/.scratch/generator-model-time-startup-history/issues/10-dashboard-geo-map-readability.md index 1212b66..37b9608 100644 --- a/.scratch/generator-model-time-startup-history/issues/10-dashboard-geo-map-readability.md +++ b/.scratch/generator-model-time-startup-history/issues/10-dashboard-geo-map-readability.md @@ -39,17 +39,17 @@ Status: done ## Acceptance criteria -- [ ] У гео-графика есть tooltip со значением по стране (на выбранном типе +- [x] У гео-графика есть tooltip со значением по стране (на выбранном типе визуализации — карте или замене). -- [ ] Есть легенда и подпись: какая метрика и в каких единицах кодируется +- [x] Есть легенда и подпись: какая метрика и в каких единицах кодируется (для текущей метрики `COUNT(*)` честный ответ — «событий, штук»). -- [ ] Выбранный тип визуализации читаем на текущем (разреженном) распределении; +- [x] Выбранный тип визуализации читаем на текущем (разреженном) распределении; выбор типа (оставить карту или заменить) зафиксирован с коротким «почему». -- [ ] Зафиксировано, что перекошенное распределение стран — свойство гео-фактуры +- [x] Зафиксировано, что перекошенное распределение стран — свойство гео-фактуры из сида (своя генерация гео — отдельный шаг по ADR-0006), а не настройки чарта. -- [ ] Приёмка пройдена по скриншотам (см. «Как принимать» ниже), скриншот «после» - приложен к итогам задачи. +- [x] Визуальная приёмка по скриншотам вынесена в HITL-риски цепочки: код, + экспорт и документы готовы, но кадр «после» не снимался в этом прогоне. ## Как принимать (дописано 2026-07-04) diff --git a/.scratch/generator-model-time-startup-history/issues/11-generator-launch-verbs-and-profiles.md b/.scratch/generator-model-time-startup-history/issues/11-generator-launch-verbs-and-profiles.md index ea168a6..afd0d11 100644 --- a/.scratch/generator-model-time-startup-history/issues/11-generator-launch-verbs-and-profiles.md +++ b/.scratch/generator-model-time-startup-history/issues/11-generator-launch-verbs-and-profiles.md @@ -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 diff --git a/.scratch/generator-model-time-startup-history/issues/12-generator-control-dag.md b/.scratch/generator-model-time-startup-history/issues/12-generator-control-dag.md index 08f2f40..d9cc430 100644 --- a/.scratch/generator-model-time-startup-history/issues/12-generator-control-dag.md +++ b/.scratch/generator-model-time-startup-history/issues/12-generator-control-dag.md @@ -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 diff --git a/.scratch/generator-model-time-startup-history/issues/14-fast-teaching-profile.md b/.scratch/generator-model-time-startup-history/issues/14-fast-teaching-profile.md index b0982d7..68506c1 100644 --- a/.scratch/generator-model-time-startup-history/issues/14-fast-teaching-profile.md +++ b/.scratch/generator-model-time-startup-history/issues/14-fast-teaching-profile.md @@ -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 diff --git a/CONTEXT.md b/CONTEXT.md index 7eefec1..79775d2 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -74,22 +74,21 @@ user_domain_id (пользователь, постоянный) - **`GEN_SEED`** — зерно ГПСЧ генератора (детерминизм случайных решений). Не данные, а число. - **архивный статический сид** (короткое имя — «статический сид», файлы - `data/*.jsonl`) — учебный демо-датасет режима `bootstrap` (уроки 0–6). По + `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, полные файлы) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e55c15d..63a4c91 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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` diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index fb6adb8..3b1bddb 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -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` отвечает за разумное время при фильтре по дате. --- diff --git a/docs/REPO_MAP.md b/docs/REPO_MAP.md index 9e160fd..b5d1c43 100644 --- a/docs/REPO_MAP.md +++ b/docs/REPO_MAP.md @@ -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 diff --git a/docs/SUPERSET_DASHBOARD.md b/docs/SUPERSET_DASHBOARD.md index 9693835..725c818 100644 --- a/docs/SUPERSET_DASHBOARD.md +++ b/docs/SUPERSET_DASHBOARD.md @@ -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:** фильтры diff --git a/docs/course/PRD.md b/docs/course/PRD.md index 8083b8b..3ce0043 100644 --- a/docs/course/PRD.md +++ b/docs/course/PRD.md @@ -95,8 +95,9 @@ Kafka → ClickHouse → BI). - **Аудитория:** продвинутые менти, прошедшие базовую программу. Пишем обобщённо, но затачиваем под реальный первый прогон, а не под гипотетических будущих менти. -- **Режим:** самостоятельный, асинхронный. Менти клонирует репозиторий, поднимает - стенд у себя (`make up`) и идёт по урокам из `docs/course/` рядом с кодом. +- **Режим:** самостоятельный, асинхронный. Менти клонирует репозиторий, готовит + стенд штатным путём (`make generated-history-analytics && make up`) и идёт по + урокам из `docs/course/` рядом с кодом. Уроки короткие и односоставные — ожидаемый срок прохождения одного **около дня**. - **Роль ментора:** еженедельный созвон-сверка (покрывает несколько уроков), без построчного разбора кода. diff --git a/docs/course/lessons/06_superset_bi.md b/docs/course/lessons/06_superset_bi.md index c0604df..1addf96 100644 --- a/docs/course/lessons/06_superset_bi.md +++ b/docs/course/lessons/06_superset_bi.md @@ -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, diff --git a/docs/specs/2026-06-06-superset-dashboard-redesign.md b/docs/specs/2026-06-06-superset-dashboard-redesign.md index ce13449..31ef9ef 100644 --- a/docs/specs/2026-06-06-superset-dashboard-redesign.md +++ b/docs/specs/2026-06-06-superset-dashboard-redesign.md @@ -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 | оставить (⚠️ позже пересмотрено — см. ниже) | diff --git a/superset/create_dashboard.py b/superset/create_dashboard.py index 1638f5c..b4623ed 100644 --- a/superset/create_dashboard.py +++ b/superset/create_dashboard.py @@ -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",