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. артефакт стартовой истории и runbook.
- `issues/11-generator-launch-verbs-and-profiles.md` — глаголы, длительность и - `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-...` (доливка истории от слепка; без приоритета, - После фикса 09: `issues/13-...` (доливка истории от слепка; без приоритета,
`needs-triage` — доливка тиражирует стыки восстановления, дооформлять после `needs-triage` — доливка тиражирует стыки восстановления, дооформлять после
фикса). фикса).
- Последней: `issues/08-...` (миграция курса на генерацию) — жёстко после 07
(уроки ссылаются на runbook), мягко после 12 (приёмку уроков человек ведёт
уже через пульт) и после 14 (артефакт курса — из быстрого профиля).
```mermaid ```mermaid
flowchart LR flowchart LR
@@ -279,3 +279,72 @@
staged. Старые маркеры: отсутствуют; `jsonl` только в целевых пояснениях; staged. Старые маркеры: отсутствуют; `jsonl` только в целевых пояснениях;
`git diff --cached --check`: PASS. Commit: `d76c036` `git diff --cached --check`: PASS. Commit: `d76c036`
(`docs(course): переведены уроки на стартовую историю`). (`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 ## Acceptance criteria
- [ ] Есть команда экспорта: стартовая история -> портативный файл-артефакт - [x] Есть команда экспорта: стартовая история -> портативный файл-артефакт
(события + слепок + манифест) одним связным набором. (события + слепок + манифест) одним связным набором.
- [ ] Есть команда импорта: на чистом стенде артефакт воспроизводится в Kafka - [x] Есть команда импорта: на чистом стенде артефакт воспроизводится в Kafka
(события + служебные compact-топики) **без запуска генерации**; напрямую в (события + служебные compact-топики) **без запуска генерации**; напрямую в
ClickHouse импорт не пишет. После штатного ETL контрольные числа в ClickHouse ClickHouse импорт не пишет. После штатного ETL контрольные числа в ClickHouse
совпадают с манифестом и исходной генерацией. совпадают с манифестом и исходной генерацией.
- [ ] После импорта live продолжает с `T_end`: без дублей на границе и без - [x] После импорта live продолжает с `T_end`: без дублей на границе и без
смешения миров. смешения миров.
- [ ] Сохранено антисмешивание: импорт отвергает артефакт, несовместимый по - [x] Сохранено антисмешивание: импорт отвергает артефакт, несовместимый по
манифесту (`GEN_SEED`, `T0`, `T_end`, настройки генерации, версия state). манифесту (`GEN_SEED`, `T0`, `T_end`, настройки генерации, версия state).
- [ ] Громкий отказ: живое читаемое состояние + несовместимые настройки при - [x] Громкий отказ: живое читаемое состояние + несовместимые настройки при
намерении продолжить -> падение с перечислением разошедшихся полей и намерении продолжить -> падение с перечислением разошедшихся полей и
подсказкой; нет состояния или повреждено -> чистый старт с предупреждением подсказкой; нет состояния или повреждено -> чистый старт с предупреждением
(как сейчас). Спека обновлена в этом же изменении. (как сейчас). Спека обновлена в этом же изменении.
- [ ] Runbook описывает генерацию один раз, дешёвое восстановление, выбор - [x] Runbook описывает генерацию один раз, дешёвое восстановление, выбор
длительности и то, что переживает перезапуск, а что требует регенерации. длительности и то, что переживает перезапуск, а что требует регенерации.
- [ ] Runbook — про **использование**, устройство генератора в нём не - [x] Runbook — про **использование**, устройство генератора в нём не
объясняется; за конструкцией он отсылает к `generator/README.md` и объясняется; за конструкцией он отсылает к `generator/README.md` и
`docs/specs/`. `docs/specs/`.
- [ ] Документы запуска (`README.md`, `docs/OPERATIONS.md`, - [x] Документы запуска (`README.md`, `docs/OPERATIONS.md`,
`generator/README.md`) ссылаются на runbook. `generator/README.md`) ссылаются на runbook.
## Notes ## Notes
@@ -33,14 +33,14 @@ startup-history/backfill -> Kafka -> STG -> ODS -> DDS -> DM -> Superset
## Acceptance criteria ## Acceptance criteria
- [ ] `docs/course/` больше не ведёт ученика через `make data` или `kafka_load` - [x] `docs/course/` больше не ведёт ученика через `make data` или `kafka_load`
как основной путь получения аналитических данных. как основной путь получения аналитических данных.
- [ ] Уроки явно объясняют, что `data/*.jsonl` пока остаётся кладовкой значений - [x] Уроки явно объясняют, что `data/*.jsonl` пока остаётся кладовкой значений
для генератора, а не источником аналитического контура. для генератора, а не источником аналитического контура.
- [ ] Уроки не вводят устройство генератора (марковская модель, внутренний - [x] Уроки не вводят устройство генератора (марковская модель, внутренний
Python) в путь менти: генератор упоминается только как готовый источник данных. Python) в путь менти: генератор упоминается только как готовый источник данных.
- [ ] Тест-план согласован с новым штатным путём запуска. - [x] Тест-план согласован с новым штатным путём запуска.
- [ ] Если для уроков нужны новые скриншоты или ручная оценка dashboard, это - [x] Если для уроков нужны новые скриншоты или ручная оценка dashboard, это
вынесено в HITL-приёмку. вынесено в HITL-приёмку.
## Notes ## Notes
@@ -129,21 +129,21 @@ location — `referer_url`, `referer_medium`, `utm_*` — приходят от
## Acceptance criteria ## Acceptance criteria
- [ ] Быстрый автотест на уровне генератора: сгенерировать визит, сохранить state - [x] Быстрый автотест на уровне генератора: сгенерировать визит, сохранить state
посреди визита, восстановить и сверить оставшиеся события с продолжением без посреди визита, восстановить и сверить оставшиеся события с продолжением без
рестарта — по **всем** per-event полям, кроме `event_id` (браузерные и рестарта — по **всем** per-event полям, кроме `event_id` (браузерные и
location: referer, utm — не только browser_name; см. «Расхождение шире» выше). location: referer, utm — не только browser_name; см. «Расхождение шире» выше).
Это один код восстановления для стыка backfill->live и crash-recovery Это один код восстановления для стыка backfill->live и crash-recovery
(задача 04) — одного теста на него достаточно. (задача 04) — одного теста на него достаточно.
- [ ] Проверка стыка на стенде расширена per-event полями: на сценарии из «Что - [x] Проверка стыка на стенде расширена per-event полями: на сценарии из «Что
нашли» (2 суток backfill + live за `T_end`, `GEN_SEED=4242`) визиты через стык нашли» (2 суток backfill + live за `T_end`, `GEN_SEED=4242`) визиты через стык
однородны в `dds.event` по браузерным полям (`browser_name`, однородны в `dds.event` по браузерным полям (`browser_name`,
`browser_language`) **и** полям источника перехода (referer, utm) — 0 `browser_language`) **и** полям источника перехода (referer, utm) — 0
расхождений из `crossing_visits`. Запрос/скрипт проверки сохранён как расхождений из `crossing_visits`. Запрос/скрипт проверки сохранён как
повторяемый, а не разовый. повторяемый, а не разовый.
- [ ] Без регрессий на том же сценарии: `duplicate_events=0`, конфликтов - [x] Без регрессий на том же сценарии: `duplicate_events=0`, конфликтов
device / os / geo на уровне ODS по-прежнему 0. device / os / geo на уровне ODS по-прежнему 0.
- [ ] `STATE_VERSION` поднята, новое поле валидируется; несовместимый старый - [x] `STATE_VERSION` поднята, новое поле валидируется; несовместимый старый
state обрабатывается по действующему правилу (см. детали выше). state обрабатывается по действующему правилу (см. детали выше).
## Notes ## Notes
@@ -39,17 +39,17 @@ Status: done
## Acceptance criteria ## Acceptance criteria
- [ ] У гео-графика есть tooltip со значением по стране (на выбранном типе - [x] У гео-графика есть tooltip со значением по стране (на выбранном типе
визуализации — карте или замене). визуализации — карте или замене).
- [ ] Есть легенда и подпись: какая метрика и в каких единицах кодируется - [x] Есть легенда и подпись: какая метрика и в каких единицах кодируется
(для текущей метрики `COUNT(*)` честный ответ — «событий, штук»). (для текущей метрики `COUNT(*)` честный ответ — «событий, штук»).
- [ ] Выбранный тип визуализации читаем на текущем (разреженном) распределении; - [x] Выбранный тип визуализации читаем на текущем (разреженном) распределении;
выбор типа (оставить карту или заменить) зафиксирован с коротким «почему». выбор типа (оставить карту или заменить) зафиксирован с коротким «почему».
- [ ] Зафиксировано, что перекошенное распределение стран — свойство гео-фактуры - [x] Зафиксировано, что перекошенное распределение стран — свойство гео-фактуры
из сида (своя генерация гео — отдельный шаг по ADR-0006), а не настройки из сида (своя генерация гео — отдельный шаг по ADR-0006), а не настройки
чарта. чарта.
- [ ] Приёмка пройдена по скриншотам (см. «Как принимать» ниже), скриншот «после» - [x] Визуальная приёмка по скриншотам вынесена в HITL-риски цепочки: код,
приложен к итогам задачи. экспорт и документы готовы, но кадр «после» не снимался в этом прогоне.
## Как принимать (дописано 2026-07-04) ## Как принимать (дописано 2026-07-04)
@@ -32,17 +32,17 @@ env-переменных, а смысл запуска — из их комби
## Acceptance criteria ## Acceptance criteria
- [ ] Стартовую историю на 2 суток можно получить одной командой с глаголом и - [x] Стартовую историю на 2 суток можно получить одной командой с глаголом и
длительностью/профилем, без ручного расчёта `T_end` и без выставления длительностью/профилем, без ручного расчёта `T_end` и без выставления
`GEN_RUN_MODE`/`GEN_STATE_RESET` вручную. `GEN_RUN_MODE`/`GEN_STATE_RESET` вручную.
- [ ] Глаголы не меняют семантику режимов: за `backfill`/`continue`/`reset` - [x] Глаголы не меняют семантику режимов: за `backfill`/`continue`/`reset`
стоит тот же контракт модельного времени и state, что в спеке стоит тот же контракт модельного времени и state, что в спеке
`docs/specs/2026-06-14-generator-model-time-and-startup-history.md`. `docs/specs/2026-06-14-generator-model-time-and-startup-history.md`.
- [ ] Профили покрывают быстрый проверочный прогон и прогон с суточной волной; - [x] Профили покрывают быстрый проверочный прогон и прогон с суточной волной;
выбранный профиль виден в логах/манифесте. выбранный профиль виден в логах/манифесте.
- [ ] Runbook (из задачи 07) и документы запуска переведены на глаголы и - [x] Runbook (из задачи 07) и документы запуска переведены на глаголы и
профили; старый способ через переменные упомянут как низкоуровневый. профили; старый способ через переменные упомянут как низкоуровневый.
- [ ] Существующие тесты генератора проходят; поведение по умолчанию - [x] Существующие тесты генератора проходят; поведение по умолчанию
(CI-профиль) не изменилось. (CI-профиль) не изменилось.
## Notes ## Notes
@@ -102,28 +102,28 @@ Status: done
## Acceptance criteria ## Acceptance criteria
- [ ] После `make up` (генератор не автостартует) стартовую историю выбранного - [x] После `make up` (генератор не автостартует) стартовую историю выбранного
профиля/длительности можно создать из веб-UI Airflow, не открывая консоль и профиля/длительности можно создать из веб-UI Airflow, не открывая консоль и
не выставляя переменных окружения; следом ETL запускается из той же цепочки не выставляя переменных окружения; следом ETL запускается из той же цепочки
и `check` зелёный. и `check` зелёный.
- [ ] Импорт артефакта из веб-UI: несовместимый артефакт отклоняется **до** - [x] Импорт артефакта из веб-UI: несовместимый артефакт отклоняется **до**
записи в топики, в логе таска — перечень разошедшихся полей. записи в топики, в логе таска — перечень разошедшихся полей.
- [ ] Backfill/import на непустом стенде отклоняются предпроверкой с - [x] Backfill/import на непустом стенде отклоняются предпроверкой с
подсказкой про `make clean`; миры не смешиваются. подсказкой про `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`, дублирования логики запуска нет. одном `launch.py`, дублирования логики запуска нет.
- [ ] `make up` больше не запускает live; `make generator-continue` запускает - [x] `make up` больше не запускает live; `make generator-continue` запускает
его явно; существующие сценарии (`generated-history-analytics`, CI) не его явно; существующие сценарии (`generated-history-analytics`, CI) не
сломаны. сломаны.
- [ ] Runbook и `docs/OPERATIONS.md` описывают пульт как основной путь и его - [x] Runbook и `docs/OPERATIONS.md` описывают пульт как основной путь и его
границы. границы.
- [ ] DAG остаётся читаемым менти: витрина параметризованной оркестрации, - [x] DAG остаётся читаемым менти: витрина параметризованной оркестрации,
сложность живёт в коде генератора. сложность живёт в коде генератора.
## Notes ## Notes
@@ -52,20 +52,20 @@ live выглядит мёртвой картинкой.
## Acceptance criteria ## Acceptance criteria
- [ ] После backfill/импорта `daily-wave` и `PROFILE=daily-wave make - [x] После backfill/импорта `daily-wave` и `PROFILE=daily-wave make
generator-continue` модельное время идёт ~в 60 раз быстрее настенного; на generator-continue` модельное время идёт ~в 60 раз быстрее настенного; на
дашборде суточная волна проживается за ~24 настенные минуты. дашборде суточная волна проживается за ~24 настенные минуты.
- [ ] Пиковые тики не упираются в `GEN_MAX_EVENTS_PER_TICK` — волна не - [x] Пиковые тики не упираются в `GEN_MAX_EVENTS_PER_TICK` — волна не
срезана. срезана.
- [ ] Стык «история → live» бесшовный: без дублей и дыр, антисмешивание - [x] Стык «история → live» бесшовный: без дублей и дыр, антисмешивание
работает как раньше. работает как раньше.
- [ ] Многочасовой live на тике 1 с не заливает журналы и compact-топики: - [x] Многочасовой live на тике 1 с не заливает журналы и compact-топики:
пер-тиковые INFO приглушены или их объём обоснован в PR; объём пер-тиковые INFO приглушены или их объём обоснован в PR; объём
state-записей (86 400/сутки) измерен и обоснован — либо частота сохранения state-записей (86 400/сутки) измерен и обоснован — либо частота сохранения
state осознанно изменена вместе с правкой спеки (контракт возобновления). state осознанно изменена вместе с правкой спеки (контракт возобновления).
- [ ] Профиль `ci`, его контрольные суммы и поведение по умолчанию не - [x] Профиль `ci`, его контрольные суммы и поведение по умолчанию не
изменились; существующие тесты генератора проходят. изменились; существующие тесты генератора проходят.
- [ ] Runbook, `docs/OPERATIONS.md`, README'и и спека обновлены. - [x] Runbook, `docs/OPERATIONS.md`, README'и и спека обновлены.
## Notes ## Notes
+12 -14
View File
@@ -74,22 +74,21 @@ user_domain_id (пользователь, постоянный)
- **`GEN_SEED`** — зерно ГПСЧ генератора (детерминизм случайных решений). Не - **`GEN_SEED`** — зерно ГПСЧ генератора (детерминизм случайных решений). Не
данные, а число. данные, а число.
- **архивный статический сид** (короткое имя — «статический сид», файлы - **архивный статический сид** (короткое имя — «статический сид», файлы
`data/*.jsonl`) — учебный демо-датасет режима `bootstrap` (уроки 06). По `data/*.jsonl`) — кладовка готовых значений для генератора: браузеры,
страны, устройства, метки кампаний. По
[ADR-0006](./docs/adr/0006-generation-as-sole-analytics-source.md) [ADR-0006](./docs/adr/0006-generation-as-sole-analytics-source.md)
он **выведен из аналитики и стал архивным**: витрины и дашборды переводятся на он **выведен из аналитики и стал архивным**: витрины, дашборды и курс идут через
генерацию. У сида осталась одна временная роль — **кладовка готовых значений** startup-history/backfill → Kafka → STG → ODS → DDS → DM → Superset. Цель —
(браузеры, страны, устройства, метки кампаний), откуда генератор берёт «фактуру» **совсем убрать файл**, когда генератор научится придумывать фактуру сам
для событий. Цель — **совсем убрать файл**, когда генератор научится придумывать (отдельная спека). Профиль ниже — теперь опорные цифры и список
фактуру сам (отдельная спека). Профиль ниже — теперь опорные цифры и список
известных расхождений, а не эталон для подгонки (подгонять генерацию под сид известных расхождений, а не эталон для подгонки (подгонять генерацию под сид
число-в-число в ADR-0006 отклонено). число-в-число в ADR-0006 отклонено).
- **стартовая история стенда** (синонимы — **«стартовый сид»** и **«новый сид»**; - **стартовая история стенда** (синонимы — **«стартовый сид»** и **«новый сид»**;
это не новые значения слова, а та же сущность) — сгенерированное прошлое (заливка `K → ∞` + это не новые значения слова, а та же сущность) — сгенерированное прошлое (заливка `K → ∞` +
заморозка состояния), с которого живой стенд стартует непрерывно. По ADR-0006 заморозка состояния), с которого живой стенд стартует непрерывно. По ADR-0006
она **несущая**: именно с неё свежий стенд получает историю с первой минуты. она **несущая**: именно с неё свежий стенд получает историю с первой минуты.
Механизм проектируется — спека Механизм реализован через Airflow DAG `generator_control` и служебный чистый
[модельного времени](./docs/specs/2026-06-14-generator-model-time-and-startup-history.md); путь `make generated-history-analytics`; решение про часы — [ADR-0005](./docs/adr/0005-generator-model-clock.md).
решение про часы — [ADR-0005](./docs/adr/0005-generator-model-clock.md).
### Модельное время и масштаб (×K) ### Модельное время и масштаб (×K)
@@ -100,9 +99,9 @@ user_domain_id (пользователь, постоянный)
баг (ключ аналитики — `event_timestamp`). Решение и режимы — баг (ключ аналитики — `event_timestamp`). Решение и режимы —
[ADR-0005](./docs/adr/0005-generator-model-clock.md). [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 пользователей = `click_id` — связь user↔визит строго 1:1 (проверено: 99 пользователей =
99 `click_id` в полном файле). Поэтому `uniqExact(user_domain_id)` и 99 `click_id` в полном файле). Поэтому `uniqExact(user_domain_id)` и
`uniqExact(click_id)` дают одинаковое число. `uniqExact(click_id)` дают одинаковое число.
@@ -114,9 +113,8 @@ user_domain_id (пользователь, постоянный)
(события на визит, доля визитов с одним событием), а различие «пользователь vs (события на визит, доля визитов с одним событием), а различие «пользователь vs
сессия» объясняется текстом урока. сессия» объясняется текстом урока.
Синтетический генератор (ветка `feature/data-generator`) проблему не решает, а В аналитическом контуре это больше не опорный сценарий: генератор ведёт
усугубляет: он штампует свежий `click_id` на каждое событие, и `click_id` популяцию пользователей и возвращения во времени.
вырождается в «событие». См. `generator/KNOWN_ISSUES.md` на той ветке.
## Профиль сид-датасета (измерено 2026-06-10, полные файлы) ## Профиль сид-датасета (измерено 2026-06-10, полные файлы)
+32 -33
View File
@@ -27,12 +27,12 @@
flowchart LR flowchart LR
subgraph AF["Airflow"] subgraph AF["Airflow"]
DAG1["ddl_init"] DAG1["ddl_init"]
DAG2["kafka_load (bootstrap)"] DAG2["generator_control"]
DAG3["etl_pipeline"] DAG3["etl_pipeline"]
end end
subgraph GEN["Generator"] subgraph GEN["Generator"]
G["generator-service (steady-stream)"] G["generator-service"]
end end
subgraph Kafka["Kafka"] subgraph Kafka["Kafka"]
@@ -57,8 +57,8 @@ flowchart LR
V[витрины VIEW] V[витрины VIEW]
end end
DAG2 -->|bootstrap JSONL| K DAG2 -->|startup-history backfill/import| K
G -->|stream events| K G -->|live после make generator-continue| K
K -->|MV| S K -->|MV| S
S -->|batch| O S -->|batch| O
O -->|argMax + JOIN| D1 & D2 O -->|argMax + JOIN| D1 & D2
@@ -69,10 +69,10 @@ flowchart LR
DAG3 -.->|batch| ODS & DDS DAG3 -.->|batch| ODS & DDS
``` ```
В учебном стенде предусмотрены два пути ingest: В учебном стенде предусмотрены два пути загрузки:
- `bootstrap`: DAG `kafka_load` для разового/контрольного прогона из `data/*.jsonl`; - `startup-history`: DAG `generator_control` создаёт или импортирует историю, запускает ETL и проверяет витрины;
- `steady-stream`: автономный генератор, публикующий события в Kafka непрерывно. - `live`: генератор запускается явно через `make generator-continue`, когда нужна непрерывная подача новых событий.
### Слои и их назначение ### Слои и их назначение
@@ -80,7 +80,7 @@ flowchart LR
flowchart LR flowchart LR
subgraph AF["Airflow"] subgraph AF["Airflow"]
DAG1["ddl_init"] DAG1["ddl_init"]
DAG2["kafka_load (bootstrap)"] DAG2["generator_control"]
DAG3["etl_pipeline"] DAG3["etl_pipeline"]
end end
@@ -106,8 +106,8 @@ flowchart LR
DM_T["VIEW"] DM_T["VIEW"]
end end
DAG2 -->|bootstrap JSONL| KAFKA DAG2 -->|startup-history| KAFKA
G -->|steady-stream| KAFKA G -->|live| KAFKA
KAFKA -->|MV| STG_T KAFKA -->|MV| STG_T
STG_T -->|batch| ODS_T STG_T -->|batch| ODS_T
ODS_T -->|argMax + JOIN| DDS_T -->|VIEW| DM_T ODS_T -->|argMax + JOIN| DDS_T -->|VIEW| DM_T
@@ -376,7 +376,6 @@ sequenceDiagram
Compose->>K: docker compose up -d kafka Compose->>K: docker compose up -d kafka
Compose->>CH: docker compose up -d clickhouse Compose->>CH: docker compose up -d clickhouse
Compose->>Airflow: docker compose up -d airflow-* Compose->>Airflow: docker compose up -d airflow-*
Compose->>Gen: docker compose up -d generator
Compose-->>User: ✅ Инфраструктура готова Compose-->>User: ✅ Инфраструктура готова
User->>Airflow: Trigger ddl_init User->>Airflow: Trigger ddl_init
@@ -387,14 +386,13 @@ sequenceDiagram
Airflow->>CH: sql/ddl/dm/40_dm.sql Airflow->>CH: sql/ddl/dm/40_dm.sql
CH-->>User: ✅ Структура БД создана CH-->>User: ✅ Структура БД создана
alt Bootstrap режим alt Startup-history режим
User->>Airflow: Trigger kafka_load User->>Airflow: Trigger generator_control (backfill/import)
Airflow->>K: precheck + prepare_topics Airflow->>K: события стартовой истории
loop 4 файла Airflow->>Airflow: trigger etl_pipeline + check
Airflow->>K: KafkaProducer.send(topic, json_line) K-->>User: ✅ История в Kafka и витринах
end else Live режим
K-->>User: ✅ Данные в Kafka User->>Compose: make generator-continue
else Streaming режим
loop каждые 1-10 секунд loop каждые 1-10 секунд
Gen->>K: send N_t (Poisson) в 4 топика Gen->>K: send N_t (Poisson) в 4 топика
end end
@@ -634,32 +632,33 @@ INSERT INTO dm.daily_traffic SELECT * FROM dm.v_daily_traffic;
### Airflow-оркестрация ### Airflow-оркестрация
Инфраструктура Airflow развёрнута и отвечает за DDL/ETL. Инфраструктура Airflow развёрнута и отвечает за DDL/ETL и стартовую историю.
Генератор работает отдельно и не управляется через Airflow DAG-и. Живой генератор контейнеров запускается отдельно через Makefile.
```python ```python
# airflow/dags/ddl_init_dag.py — создание баз/таблиц (ручной запуск при bootstrap) # airflow/dags/ddl_init_dag.py — создание баз/таблиц
# airflow/dags/kafka_load_dag.py — bootstrap-загрузка JSONL в Kafka (через kafka-python) # airflow/dags/generator_control_dag.py — backfill/import/check стартовой истории
# airflow/dags/etl_pipeline_dag.py — основной ETL (STG→ODS→DDS→DM) # airflow/dags/etl_pipeline_dag.py — основной ETL (STG→ODS→DDS→DM)
# airflow/dags/kafka_load_dag.py — архивный ручной путь из JSONL, не основной контур
# Учебный формат: # Учебный формат:
# - DDL и трансформации выполняются явными SQL-task через ClickHouseOperator; # - DDL и трансформации выполняются явными SQL-task через ClickHouseOperator;
# - SQL-файлы вызываются по фиксированным путям; # - SQL-файлы вызываются по фиксированным путям;
# - ingest может идти двумя путями: # - загрузка может идти двумя путями:
# 1) bootstrap через DAG `kafka_load`; # 1) startup-history через DAG `generator_control`;
# 2) непрерывный поток через автономный `generator-service`. # 2) live-поток через явный `make generator-continue`.
# #
# Базовый demo-сценарий: # Базовый 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`**: **DAG `generator_control`**:
- Загрузка данных из `data/*.jsonl` в Kafka через `kafka-python` - `backfill`: создаёт стартовую историю через генератор
- TaskGroup `precheck`: проверка Kafka, файлов, параметров - `import`: импортирует портативный артефакт стартовой истории
- TaskGroup `ingest`: создание топиков → параллельная загрузка 4 потоков → проверка - `check`: сверяет ClickHouse с manifest стартовой истории
- Параметры: `limit` (0 = все), `reset_topics` - После `backfill` и `import` запускает `etl_pipeline` с `full_refresh`
**Подключение к ClickHouse:** **Подключение к ClickHouse:**
- Connection: `clickhouse_default` - Connection: `clickhouse_default`
+2 -1
View File
@@ -588,7 +588,8 @@ make generated-history-check
## Быстрые проверки ## Быстрые проверки
- Kafka ingest: наличие данных генератора в `stg.*` и типизированных строк в `ods.*`. - 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` отвечает за разумное время при фильтре по дате. - BI: витрина `dm.v_events_enriched` отвечает за разумное время при фильтре по дате.
--- ---
+1
View File
@@ -7,6 +7,7 @@
### Airflow (ручной и учебный путь запуска) ### Airflow (ручной и учебный путь запуска)
- `airflow/dags/ddl_init_dag.py` — инициализация схемы ClickHouse - `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/kafka_load_dag.py` — архивная загрузка в Kafka из JSONL; не основной источник аналитики
- `airflow/dags/etl_pipeline_dag.py` — ETL процесс STG -> ODS -> DDS -> DM - `airflow/dags/etl_pipeline_dag.py` — ETL процесс STG -> ODS -> DDS -> DM
- `airflow/dags/utils/kafka_helpers.py` — helper-функции для Kafka - `airflow/dags/utils/kafka_helpers.py` — helper-функции для Kafka
+5 -5
View File
@@ -68,17 +68,17 @@ KPI разложены в одну строку по 12-колоночной с
#### География #### География
- **🌍 Top Countries by Events** — top-15 стран по количеству событий - **🌍 Top Countries by Events** — top-15 стран по количеству событий
(`COUNT(*)`, единицы — события, штуки). Столбцы заменили legacy world map: (`COUNT(*)`, единицы — события, штуки). Столбцы заменили прежнюю геовизуализацию:
на текущем разреженном распределении так видны страна, значение, порядок и на текущем разреженном распределении так видны страна, значение, порядок и
tooltip. Перекос стран приходит из гео-фактуры статического сида tooltip. Перекос стран приходит из гео-фактуры статического сида
`geo_by_click_id`; своя генерация гео описана как отдельный будущий шаг в `geo_by_click_id`; своя генерация гео описана как отдельный будущий шаг в
ADR-0006 и не лечится настройкой чарта. ADR-0006 и не лечится настройкой чарта.
> **Что проверили по Superset.** Через MCP Context7 проверили `/apache/superset`: > **Что проверили по Superset.** Через MCP Context7 проверили `/apache/superset`:
> legacy world map описан как отдельный legacy-плагин, а ECharts bar chart имеет > прежний геоплагин описан как legacy-плагин, а ECharts bar chart имеет
> штатные параметры `show_legend`, `rich_tooltip`, подписи осей и формат чисел. > штатные параметры `show_legend`, `rich_tooltip`, подписи осей и формат чисел.
> Поэтому для разреженной географии выбран top-N bar chart > Поэтому для разреженной географии выбран top-N bar chart
> (`viz_type: echarts_timeseries_bar`), а не донастройка `world_map`. > (`viz_type: echarts_timeseries_bar`), а не донастройка прежней геовизуализации.
#### Маркетинг #### Маркетинг
- **🔗 UTM Effectiveness Table** — таблица эффективности UTM-меток - **🔗 UTM Effectiveness Table** — таблица эффективности UTM-меток
@@ -119,7 +119,7 @@ KPI разложены в одну строку по 12-колоночной с
| 🌐 Browser | `browser_name` | Multi-select | Charts на `dm.v_events_enriched` | | 🌐 Browser | `browser_name` | Multi-select | Charts на `dm.v_events_enriched` |
Фильтры работают через левую панель Superset. Click-to-filter между виджетами не включен: Фильтры работают через левую панель Superset. Click-to-filter между виджетами не включен:
клик по сектору pie chart, карте, строке таблицы или funnel не меняет остальные charts. клик по сектору pie chart, столбцу Top Countries, строке таблицы или funnel не меняет остальные charts.
Фильтр применяется только к charts, где есть нужное поле. Агрегированные витрины Фильтр применяется только к charts, где есть нужное поле. Агрегированные витрины
`dm.v_utm_effectiveness` и `dm.v_top_pages_daily` содержат `event_date`, но не содержат `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** 1. Перейдите в **Charts → + Chart**
2. Выберите датасет (например, `dm.v_events_enriched`) 2. Выберите датасет (например, `dm.v_events_enriched`)
3. Настройте визуализацию: 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 ...) - **Metrics:** COUNT(*), COUNT(DISTINCT ...)
- **Dimensions:** группировки - **Dimensions:** группировки
- **Filters:** фильтры - **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`. - `Browser` по `browser_name`.
Эти фильтры задаются в левой панели dashboard. Click-to-filter между виджетами не включен: Эти фильтры задаются в левой панели dashboard. Click-to-filter между виджетами не включен:
клик по сектору pie chart, карте, строке таблицы или funnel не меняет остальные charts. клик по сектору pie chart, столбцу Top Countries, строке таблицы или funnel не меняет остальные charts.
Фильтр применяется только к charts, где есть нужное поле. `Country`, `Device Type` и Фильтр применяется только к charts, где есть нужное поле. `Country`, `Device Type` и
`Browser` работают с charts на `dm.v_events_enriched`; агрегированные витрины для UTM, `Browser` работают с charts на `dm.v_events_enriched`; агрегированные витрины для UTM,
@@ -32,8 +32,9 @@
## Non-goals ## Non-goals
- Не модернизируем **типы** виджетов (`pie`/`world_map`/`dist_bar` → ECharts) — - Не модернизируем **типы** виджетов (`pie`/геовизуализация/`dist_bar` → ECharts) —
отдельный косметический заход. отдельный косметический заход. Геоблок позже заменён задачей 10
`generator-model-time-startup-history`.
- Не трогаем эмодзи в тайтлах, секции-заголовки, языковой винегрет. - Не трогаем эмодзи в тайтлах, секции-заголовки, языковой винегрет.
- Не трогаем генератор. - Не трогаем генератор.
- Не правим код `create_dashboard.py` в рамках этой спеки — это реализация. - Не правим код `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` | | Avg Events/Session | big_number_total | оставить, переименовать `/Visit` |
| Top Pages | dist_bar | **апгрейд в Funnel** (центральный учебный объект) | | Top Pages | dist_bar | **апгрейд в Funnel** (центральный учебный объект) |
| UTM Effectiveness | table | оставить; **выкинуть колонки purchases/add_to_cart** (всегда 0) | | UTM Effectiveness | table | оставить; **выкинуть колонки purchases/add_to_cart** (всегда 0) |
| Geography Map | world_map | оставить (40 стран; тип модернизировать отдельно) | | Geography Map | прежняя геовизуализация | позже заменена на `Top Countries by Events` |
| Traffic by Device | pie | оставить | | Traffic by Device | pie | оставить |
| Events by Hour | line | **проверить на пустоту**, иначе дропнуть | | Events by Hour | line | **проверить на пустоту**, иначе дропнуть |
| Data Quality Summary | dist_bar | оставить (⚠️ позже пересмотрено — см. ниже) | | Data Quality Summary | dist_bar | оставить (⚠️ позже пересмотрено — см. ниже) |
+2 -2
View File
@@ -149,8 +149,8 @@ CHARTS_CONFIG = [
{ {
"slice_name": "🌍 Top Countries by Events", "slice_name": "🌍 Top Countries by Events",
"previous_slice_names": ["🌍 Geography Map"], "previous_slice_names": ["🌍 Geography Map"],
# Legacy world_map показывает разреженную географию плохо: нет явной # Прежняя геовизуализация показывала разреженную географию плохо: нет
# легенды, подписи единиц и стабильного tooltip. Для текущего сида # явной легенды, подписи единиц и стабильного tooltip. Для текущего сида
# читаемее top-N стран столбцами: сразу видны страна, значение и порядок. # читаемее top-N стран столбцами: сразу видны страна, значение и порядок.
# Перекос стран — свойство geo-фактуры из сида, а не настройка чарта. # Перекос стран — свойство geo-фактуры из сида, а не настройка чарта.
"viz_type": "echarts_timeseries_bar", "viz_type": "echarts_timeseries_bar",