diff --git a/AGENTS.md b/AGENTS.md
index 0b73c53..e6b700f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -164,7 +164,9 @@ Airflow) и названия из кода. Если для понятия ес
- Имена файлов в `docs/handbook/` — слаг строчными латинскими буквами через
дефис, индекс папки — `README.md`. Здесь живет путеводитель по стенду:
учебные главы для менти. Ни дат, ни номеров: глава — живой документ и
- правится вместе с тем, что описывает.
+ правится вместе с тем, что описывает. Исходники интерактивной карты лежат
+ подпапкой `architecture-map/`, а ее рендер — файлом `architecture-map.html`
+ рядом с главами.
- Имена файлов в `docs/adr/` — `NNNN-краткое-имя.md`: сквозной номер из четырёх
цифр и слаг (`0001-stand-services.md`). Решения нумеруются подряд, дата
в имени не нужна.
diff --git a/Makefile b/Makefile
index 6fe145f..eb50d65 100644
--- a/Makefile
+++ b/Makefile
@@ -51,9 +51,11 @@ config-test:
# того своя дверь и свой конфиг. Версия закреплена, потому что лока в корне
# нет, а форматтер между версиями меняет вывод — иначе проверка однажды
# покраснела бы сама, без единой правки в репозитории.
+# Проверка рендера быстрая, не требует стенда и ловит устаревший HTML.
lint:
- uvx ruff@0.16.1 check dags infra/superset
- uvx ruff@0.16.1 format --check dags infra/superset
+ uvx ruff@0.16.1 check dags infra/superset docs/handbook/architecture-map
+ uvx ruff@0.16.1 format --check dags infra/superset docs/handbook/architecture-map
+ uv run docs/handbook/architecture-map/render.py --check
# --- Наполнение миром ---
diff --git a/docs/architecture/testing.md b/docs/architecture/testing.md
index 3364601..7b0f457 100644
--- a/docs/architecture/testing.md
+++ b/docs/architecture/testing.md
@@ -36,13 +36,13 @@
Стенд нужен трём целям из восьми. Цена — замер, см. «Что проверено».
`make smoke` и `make check-services` перемерены 28 августа 2026 года,
`make config-test` — 21 августа, `make check-clickhouse` — 19 августа,
-корневой `make lint` — 9 августа, `make test` — 30 августа; остальные стоят с
+корневой `make lint` — 3 сентября, `make test` — 30 августа; остальные стоят с
замера 6 августа.
| Цель | Откуда | Что утверждает | Стенд | Цена |
|---|---|---|---|---|
| `make config-test` | корень | Compose разбирается, файлы DAG и дашборды Grafana синтаксически целы, в diff нет пробельных ошибок. О работоспособности не говорит ничего | не нужен | 1 с |
-| `make lint` | корень | Код стенда — `dags/` и `infra/superset/` — отформатирован и проходит ruff | не нужен | 0,4 с |
+| `make lint` | корень | Код стенда и сборщик карты отформатированы и проходят ruff; готовый HTML карты соответствует исходникам | не нужен | 0,8 с |
| `make smoke` | корень | Стенд **собран**: службы живы, порты отвечают, подключения настроены друг на друга. Вширь и по касательной к каждой службе. Единственная цель, которая здесь правда смоук | нужен | 25 с |
| `make check-clickhouse` | корень | Всё, что спрашивают **у ClickHouse** и он отвечает сам: макросы, шарды, реплики, путь в keeper, ключ шардирования, очередь распределённых DDL, ответ словаря товаров, счёт событий стартового мира против паспорта | нужен | 24 с |
| `make check-services` | корень | **Службы работают**: DAG запускается и доходит, топик создаётся и удаляется, Superset логинится и ходит в базу | нужен | 71 с |
@@ -128,7 +128,7 @@ ClickHouse отвечает сразу.
Правки генератора добавляют к этому `make lint`, `make typecheck` и `make test`
из каталога `generator/`: стенд им не нужен, а `make test` из них самая
-дорогая. Правки дагов и Superset — корневой `make lint`.
+дорогая. Правки дагов, Superset и карты — корневой `make lint`.
Двадцать секунд из ее ста семидесяти шести — пересборка восьми модельных дней
для сверки с паспортом мира (20,5 с из 176,5). Дешевле хеши не сравнить: чтобы
@@ -286,6 +286,10 @@ Kafka по той же причине спрашивают снаружи. Её
## Что проверено
+**Перезамер 3 сентября 2026 года при исполнении #222.** `make lint` прошел за
+0,768 секунды; в таблице 0,8 с. `ruff format --check` проверил 13 файлов стенда,
+включая исходники карты, а затем проверка рендера подтвердила свежесть HTML.
+
**Калибровка заказной стороны 30 августа 2026 года при исполнении #194.**
Пересборки эталонного мира не будет (#192), поэтому доли заказной стороны
сверены замером внутренностей генератора — окно 28 модельных дней,
@@ -474,11 +478,11 @@ shell-версия узкой пересборки за 2 м 25 с пересо
24 секунды. Это цена всей цели, а не одной новой строки.
**Замер 9 августа 2026 года при появлении корневого `lint` (#65).** Три прогона
-подряд — 0,39, 0,36 и 0,39 секунды на трёх файлах стенда; в таблице 0,4 с. На
+подряд — 0,39, 0,36 и 0,39 секунды на трех файлах стенда, которые тогда входили в проверку; в таблице 0,4 с. На
машине с непрогретым кешем первый прогон дороже: `uvx` сначала скачивает ruff.
Тогда же снято, что двери не перекрываются: `ruff check --show-files` из корня
-перечисляет ровно три файла стенда, из `generator/` — только файлы генератора.
+перечислял ровно три файла стенда, из `generator/` — только файлы генератора.
Корневой конфиг правила генератору не подменяет: конфиг ищется вверх по дереву
от файла, и ближний выигрывает.
diff --git a/docs/handbook/README.md b/docs/handbook/README.md
index ab95d89..76a16a6 100644
--- a/docs/handbook/README.md
+++ b/docs/handbook/README.md
@@ -14,6 +14,13 @@
Первая статья словаря объясняет хранилище. Следом идет несколько статей об
устройстве генератора; они понадобятся нескоро.
+## Карта хранилища
+
+[Интерактивная карта](architecture-map.html) показывает путь данных через слои.
+Gitea показывает HTML как текст. Откройте файл из своей копии репозитория
+или скачайте его кнопкой «Raw» и откройте в браузере.
+Держите карту рядом с главой «Хранилище и его слои».
+
## Главы
### [Хранилище и его слои](storage-layers.md)
diff --git a/docs/handbook/architecture-map.html b/docs/handbook/architecture-map.html
new file mode 100644
index 0000000..c493bc9
--- /dev/null
+++ b/docs/handbook/architecture-map.html
@@ -0,0 +1,1501 @@
+
+
+
+
+
+
+ Карта слоев хранилища
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/handbook/architecture-map/README.md b/docs/handbook/architecture-map/README.md
new file mode 100644
index 0000000..a94a896
--- /dev/null
+++ b/docs/handbook/architecture-map/README.md
@@ -0,0 +1,9 @@
+# Интерактивная карта хранилища
+
+`map.yaml` хранит слои, узлы, стрелки и тексты карточек.
+`template.html` задает внешний вид и поведение карты.
+`render.py` вкладывает данные в шаблон и пишет файл рядом с главами.
+Правьте исходники, а не `architecture-map.html`.
+Пересоберите карту командой:
+`uv run docs/handbook/architecture-map/render.py`.
+Проверьте свежесть готового файла с флагом `--check`.
diff --git a/docs/handbook/architecture-map/map.yaml b/docs/handbook/architecture-map/map.yaml
new file mode 100644
index 0000000..aa1ad18
--- /dev/null
+++ b/docs/handbook/architecture-map/map.yaml
@@ -0,0 +1,678 @@
+title: Карта слоев хранилища
+lead: Проследите, как один смысл меняет имя и поверхность между слоями.
+chapter_url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/handbook/storage-layers.md#user-content-хранилище-и-его-слои
+intro:
+ - Эта карта показывает путь событий и заказов от Kafka до витрин DM.
+ - Щелкните по узлу, чтобы увидеть его назначение, устройство и ключи.
+ - Щелкните по стрелке, чтобы узнать, кто и каким запросом переносит данные.
+ - Когда вы выбираете узел, карта подсвечивает все пути к нему и от него.
+ - Переключатель потока приглушает другую часть карты, но не меняет раскладку.
+ - Сначала прочитайте главу «Хранилище и его слои», затем вернитесь к карте и рассмотрите объекты.
+
+streams:
+ all: Все
+ events: События
+ orders: Заказы
+
+dictionary:
+ title: Словарь
+ caption: 'сбоку цепочки: справочники'
+
+layers:
+ - id: kafka
+ title: Kafka
+ caption: откуда приходит
+ - id: stg
+ title: STG
+ caption: как поступило
+ - id: ods
+ title: ODS
+ caption: язык источника
+ - id: dds
+ title: DDS
+ caption: язык бизнеса
+ - id: dm
+ title: DM
+ caption: данные для дашбордов
+
+mechanisms:
+ - id: materialized_view
+ label: матвью
+ line: solid
+ rhythm: непрерывно, по мере прихода сообщений
+ - id: airflow_dag
+ label: даг Airflow
+ line: dashed
+ rhythm: 'пачкой: раз в модельные сутки (`orders_ingest`) или на каждом прогоне конвейера'
+ - id: view
+ label: представление
+ line: dotted
+ rhythm: без копии, данные читаются на месте
+
+nodes:
+ - id: kafka_hits
+ name: hits
+ layer: kafka
+ order: 0
+ streams: [events]
+ what: Топик Kafka с событиями кликстрима. Каждое сообщение содержит одно событие.
+ read: 'Топик напрямую не читают: таблица `stg.hits_raw_kafka` забирает сообщения из топика. Для данных событий используйте `stg.hits_raw_dist`.'
+ fill:
+ - Источник событий пишет сообщения в топик в темпе выбранного режима мира.
+ links:
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-при%D1%91м-поток-и-его-свойства
+ - label: Формат
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/formats/clickstream-event.md#user-content-описание-выгрузки-событие-кликстрима
+
+ - id: kafka_orders
+ name: orders
+ layer: kafka
+ order: 1
+ streams: [orders]
+ what: Топик Kafka со слепками заказов магазина. После модельного дня приходит снимок изменяемого окна.
+ read: 'Топик напрямую не читают: таблица `stg.orders_raw_kafka` забирает сообщения из топика. Для заказов используйте `stg.orders_raw_dist`.'
+ fill:
+ - Источник заказов публикует слепок пачкой после каждого модельного дня.
+ links:
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/orders/ingestion.md#user-content-поток-данных
+ - label: Устройство слепка
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/orders/snapshot.md#user-content-слепок-и-его-доставка
+
+ - id: stg_hits
+ name: stg.hits_raw_dist
+ layer: stg
+ order: 0
+ streams: [events]
+ what: Сырая строка события и метаданные доставки. STG сохраняет сообщение без разбора.
+ read: 'Читайте `_dist`: у STG нет представления `_v`, а распределенная таблица показывает оба шарда.'
+ parts:
+ - '`stg.hits_raw_kafka` — чтец топика `hits`.'
+ - '`stg.hits_raw_rep` — реплицируемая таблица на шарде.'
+ - '`stg.hits_raw_dist` — распределенная поверхность записи и чтения.'
+ - '`stg.hits_raw_mv` — матвью приема сообщений из чтеца.'
+ fill:
+ - '`stg.hits_raw_mv` пишет непрерывно, по мере прихода сообщений.'
+ - Сырые строки живут трое суток реального времени плюс хвост до суток.
+ links:
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-при%D1%91м-поток-и-его-свойства
+ - label: DDL таблиц
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/10-stg-tables.sql
+ - label: DDL матвью
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/40-stg-views.sql
+
+ - id: stg_orders
+ name: stg.orders_raw_dist
+ layer: stg
+ order: 1
+ streams: [orders]
+ what: Сырые строки слепка заказов, метаданные доставки и идентификатор пачки `_load_id`.
+ read: 'Читайте `_dist`: у STG нет представления `_v`, а распределенная таблица показывает оба шарда.'
+ parts:
+ - '`stg.orders_raw_kafka` — чтец топика `orders` на первой ноде.'
+ - '`stg.orders_raw_rep` — реплицируемая таблица на шарде.'
+ - '`stg.orders_raw_dist` — распределенная поверхность записи и чтения.'
+ fill:
+ - Задача `pull_batch` дага `orders_ingest` забирает слепок пачкой раз в модельные сутки.
+ - Сырые строки живут трое суток реального времени плюс хвост до суток.
+ links:
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/orders/ingestion.md#user-content-поток-данных
+ - label: DDL
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/10-stg-tables.sql
+
+ - id: ods_event
+ name: ods.event_v
+ layer: ods
+ order: 0
+ streams: [events]
+ what: Типизированное широкое событие с именами полей источника. Запрос представления с `FINAL` возвращает актуальную версию события.
+ read: 'Читайте `ods.event_v`: запрос представления с `FINAL` возвращает актуальные строки вместо физических версий.'
+ parts:
+ - '`ods.event_rep` — реплицируемая таблица версий события на шарде.'
+ - '`ods.event_dist` — распределенная поверхность записи и диагностики.'
+ - '`ods.event_v` — представление актуального состояния с `FINAL`.'
+ - '`ods.event_mv` — матвью разбора годных строк STG.'
+ keys:
+ - '`WatchID` — тот же идентификатор, что `event_id` в DDS.'
+ - '`ClientID` — тот же посетитель, что `client_id` в DDS.'
+ - '`EventDate` — тот же день, что `event_date` в DDS.'
+ fill:
+ - '`ods.event_mv` разбирает строки непрерывно, по мере записи в STG.'
+ - '`ReplacingMergeTree(_load_ts)` хранит версии, а `FINAL` выбирает актуальную.'
+ links:
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-актуальные-события
+ - label: DDL таблиц
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/20-ods-tables.sql
+ - label: DDL представлений
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/30-ods-views.sql
+
+ - id: ods_event_errors
+ name: ods.event_errors_dist
+ layer: ods
+ order: 1
+ streams: [events]
+ what: Сырые события, которые не прошли строгий прием, с классом ошибки и метаданными доставки.
+ read: 'Читайте `_dist`: представления `_v` нет, а распределенная таблица показывает ошибки с обоих шардов.'
+ parts:
+ - '`ods.event_errors_rep` — реплицируемая таблица ошибок на шарде.'
+ - '`ods.event_errors_dist` — распределенная поверхность чтения и записи.'
+ - '`ods.event_errors_mv` — матвью, которое отбирает негодные строки STG.'
+ fill:
+ - '`ods.event_errors_mv` пишет непрерывно, по мере записи в STG.'
+ - Ошибки живут один месяц и не останавливают прием годных событий.
+ links:
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-таблица-ошибок
+ - label: DDL таблиц
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/20-ods-tables.sql
+ - label: DDL матвью
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/30-ods-views.sql
+
+ - id: ods_order
+ name: ods.order_v
+ layer: ods
+ order: 2
+ streams: [orders]
+ what: Текущая версия заказа на языке источника. Позиции пока остаются сырым фрагментом JSON.
+ read: 'Читайте `ods.order_v`: запрос представления с `FINAL` возвращает последнюю версию по `snapshot_date`.'
+ parts:
+ - '`ods.order_rep` — реплицируемая таблица версий заказа на шарде.'
+ - '`ods.order_dist` — распределенная поверхность записи и диагностики.'
+ - '`ods.order_v` — представление текущего состояния с `FINAL`.'
+ keys:
+ - '`order_id` — тот же заказ в `dds.order_v` и `dm.purchase_vs_orders_v`.'
+ - '`user_id` — пользователь магазина, который переходит в заказ DDS.'
+ fill:
+ - Задача `parse_batch` дага `orders_ingest` разбирает пачку раз в модельные сутки.
+ - '`ReplacingMergeTree(snapshot_date)` оставляет версию из последнего принятого слепка.'
+ links:
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-версии-заказов
+ - label: DDL таблиц
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/20-ods-tables.sql
+ - label: DDL представления
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/30-ods-views.sql
+
+ - id: ods_order_errors
+ name: ods.order_errors_dist
+ layer: ods
+ order: 3
+ streams: [orders]
+ what: Сырые строки слепка, которые не прошли строгий прием, с классом ошибки и `_load_id`.
+ read: 'Читайте `_dist`: представления `_v` нет, а распределенная таблица показывает ошибки с обоих шардов.'
+ parts:
+ - '`ods.order_errors_rep` — реплицируемая таблица ошибок на шарде.'
+ - '`ods.order_errors_dist` — распределенная поверхность чтения и записи.'
+ fill:
+ - Задача `parse_batch` дага `orders_ingest` пишет ошибки из той же пачки, что и годные заказы.
+ - Ошибки живут один месяц и не останавливают прием годных заказов.
+ links:
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-таблица-ошибок
+ - label: DDL
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/20-ods-tables.sql
+
+ - id: dds_event
+ name: dds.event_v
+ layer: dds
+ order: 0
+ streams: [events]
+ what: Публичное представление события на языке бизнеса. Его запрос переименовывает поля и расшифровывает категорию устройства.
+ read: 'Читайте `dds.event_v`: запрос представления возвращает явный список колонок DDS.'
+ parts:
+ - '`dds.event_v` — только представление над `ods.event_v`; своей таблицы в DDS нет.'
+ keys:
+ - '`event_id` — тот же идентификатор, что `WatchID` в ODS.'
+ - '`client_id` — тот же посетитель, что `ClientID` в ODS; соединяет событие с сессией и картой идентичностей.'
+ - '`event_date` — тот же день, что `EventDate` в ODS.'
+ - '`purchase_id[1]` — тот же заказ, что `order_id` в DDS и DM.'
+ fill:
+ - Данные для `dds.event_v` читаются из актуальных событий ODS без копии и отдельного расписания.
+ links:
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dds/event.md#user-content-форма
+ - label: DDL
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/50-dds-views.sql
+
+ - id: dds_session
+ name: dds.session_v
+ layer: dds
+ order: 1
+ streams: [events]
+ what: Сессия, которую запрос дага `dds_transform` собирает из последовательных событий одной куки. В источнике готовой сессии нет.
+ read: 'Читайте `dds.session_v`: запрос представления возвращает деловые и служебные колонки.'
+ parts:
+ - '`dds.session_rep` — реплицируемая таблица сессий на шарде.'
+ - '`dds.session_dist` — распределенная поверхность загрузки и диагностики.'
+ - '`dds.session_stage_rep` — донор дневных партиций на шарде.'
+ - '`dds.session_stage_dist` — распределенная поверхность донора.'
+ - '`dds.session_v` — публичное представление.'
+ keys:
+ - '`client_id` — тот же посетитель, что в событии DDS и карте идентичностей.'
+ - '`session_date` — день, который в `dm.daily_traffic_v` называется `report_date`.'
+ fill:
+ - Даг `dds_transform` пересобирает данные прожитых дней при каждом прогоне конвейера.
+ - Задачи `session.rebuild` и `session.replace` собирают день в доноре и заменяют партицию.
+ links:
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dds/session.md#user-content-движок-и-наполнение
+ - label: DDL таблиц
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/45-dds-tables.sql
+ - label: DDL представления
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/50-dds-views.sql
+
+ - id: dds_order
+ name: dds.order_v
+ layer: dds
+ order: 2
+ streams: [orders]
+ what: Заказ в текущем состоянии на языке бизнеса. Артикулы, количества и цены позиций хранятся в согласованных массивах.
+ read: 'Читайте `dds.order_v`: запрос представления возвращает данные без таблиц и донора партиций.'
+ parts:
+ - '`dds.order_rep` — реплицируемая таблица заказов на шарде.'
+ - '`dds.order_dist` — распределенная поверхность загрузки и диагностики.'
+ - '`dds.order_stage_rep` — донор дневных партиций на шарде.'
+ - '`dds.order_stage_dist` — распределенная поверхность донора.'
+ - '`dds.order_v` — публичное представление.'
+ keys:
+ - '`order_id` — тот же заказ, что в ODS, событии покупки и витрине сверки.'
+ - '`user_id` — пользователь магазина, который входит в карту идентичностей.'
+ - '`item_sku` — тот же артикул, что `sku` в `dic.products`.'
+ - '`order_date` — день, который в витринах называется `report_date` или `order_day`.'
+ fill:
+ - Даг `dds_transform` пересобирает изменяемое окно при каждом прогоне конвейера.
+ - Задачи `order.rebuild` и `order.replace` собирают дни в доноре и заменяют партиции.
+ links:
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dds/order.md#user-content-движок-и-наполнение
+ - label: DDL таблиц
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/45-dds-tables.sql
+ - label: DDL представления
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/50-dds-views.sql
+
+ - id: dds_identity
+ name: dds.identity_map_v
+ layer: dds
+ order: 3
+ streams: [events, orders]
+ what: Известные пары куки трекера и пользователя магазина. Связь появляется после сопоставленной покупки.
+ read: 'Читайте `dds.identity_map_v`: запрос представления с `FINAL` возвращает актуальные строки полного пересчета.'
+ parts:
+ - '`dds.identity_map_rep` — реплицируемая таблица версий пар на шарде.'
+ - '`dds.identity_map_dist` — распределенная поверхность записи.'
+ - '`dds.identity_map_v` — точное публичное представление с `FINAL`.'
+ keys:
+ - '`client_id` — кука из события и сессии DDS.'
+ - '`user_id` — пользователь магазина из заказа DDS.'
+ fill:
+ - Задача `identity_map` дага `dds_transform` пересчитывает все известные пары после обновления заказов.
+ - Новая версия полного набора пишется при каждом прогоне; `FINAL` скрывает физические повторы.
+ links:
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dds/identity.md#user-content-сборка
+ - label: DDL таблиц
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/45-dds-tables.sql
+ - label: DDL представления
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/50-dds-views.sql
+
+ - id: dm_daily_traffic
+ name: dm.daily_traffic_v
+ layer: dm
+ order: 0
+ streams: [events]
+ what: Дневные счетчики посетителей и известных пользователей после склейки идентичностей.
+ read: 'Читайте `dm.daily_traffic_v`: запрос представления с `FINAL` возвращает актуальные строки полного пересчета.'
+ parts:
+ - '`dm.daily_traffic_rep` — реплицируемая таблица версий счетчиков.'
+ - '`dm.daily_traffic_dist` — распределенная поверхность записи.'
+ - '`dm.daily_traffic_v` — точное публичное представление с `FINAL`.'
+ keys:
+ - '`report_date` — тот же день, что `session_date` в сессии DDS.'
+ fill:
+ - Задача `daily_traffic` дага `dm_transform` при каждом прогоне пересчитывает всю историю и записывает новую версию.
+ - '`FINAL` оставляет текущую строку каждого дня.'
+ links:
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dm.md#user-content-dmdaily_traffic_v
+ - label: DDL таблиц
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/55-dm-tables.sql
+ - label: DDL представления
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/60-dm-views.sql
+
+ - id: dm_revenue
+ name: dm.revenue_daily_v
+ layer: dm
+ order: 1
+ streams: [orders]
+ what: Дневная выручка, число заказов и единиц товара по категориям.
+ read: 'Читайте `dm.revenue_daily_v`: это публичное представление готовой дневной витрины.'
+ parts:
+ - '`dm.revenue_daily_rep` — реплицируемая таблица витрины на шарде.'
+ - '`dm.revenue_daily_dist` — распределенная поверхность загрузки.'
+ - '`dm.revenue_daily_stage_rep` — донор дневных партиций на шарде.'
+ - '`dm.revenue_daily_stage_dist` — распределенная поверхность донора.'
+ - '`dm.revenue_daily_v` — публичное представление.'
+ keys:
+ - '`report_date` — тот же день, что `order_date` в заказе DDS.'
+ - '`product_category` — категория, найденная по `item_sku` заказа и ключу `sku` словаря.'
+ fill:
+ - Даг `dm_transform` пересобирает изменяемое окно заказов при каждом прогоне конвейера.
+ - Задачи `revenue_daily.rebuild` и `revenue_daily.replace` собирают дни в доноре и заменяют партиции.
+ links:
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dm.md#user-content-dmrevenue_daily_v
+ - label: DDL таблиц
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/55-dm-tables.sql
+ - label: DDL представления
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/60-dm-views.sql
+
+ - id: dm_purchase
+ name: dm.purchase_vs_orders_v
+ layer: dm
+ order: 2
+ streams: [events, orders]
+ what: Сверка события покупки с заказом магазина. Одна строка показывает совпадение или класс расхождения.
+ read: 'Читайте `dm.purchase_vs_orders_v`: в результате запроса отсутствующей стороне соответствует `NULL`.'
+ parts:
+ - '`dm.purchase_vs_orders_rep` — реплицируемая таблица сверки на шарде.'
+ - '`dm.purchase_vs_orders_dist` — распределенная поверхность загрузки.'
+ - '`dm.purchase_vs_orders_stage_rep` — донор дневных партиций на шарде.'
+ - '`dm.purchase_vs_orders_stage_dist` — распределенная поверхность донора.'
+ - '`dm.purchase_vs_orders_v` — публичное представление.'
+ keys:
+ - '`order_id` — тот же заказ, что `purchase_id[1]` в событии и `order_id` в заказе DDS.'
+ - '`order_day` — общий день события покупки и заказа.'
+ fill:
+ - Даг `dm_transform` пересобирает изменяемый хвост обоих источников при каждом прогоне конвейера.
+ - Задачи `purchase_vs_orders.rebuild` и `purchase_vs_orders.replace` собирают дни в доноре и заменяют партиции.
+ links:
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dm.md#user-content-dmpurchase_vs_orders_v
+ - label: DDL таблиц
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/55-dm-tables.sql
+ - label: DDL представления
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/60-dm-views.sql
+
+ - id: dm_dq
+ name: dm.dq_summary_v
+ layer: dm
+ order: 3
+ streams: [events, orders]
+ what: Текущее состояние пяти атомарных проверок качества по дням. История запусков остается в Airflow.
+ read: 'Читайте `dm.dq_summary_v`: запрос к публичному представлению возвращает последнюю опубликованную сводку дня.'
+ parts:
+ - '`dm.dq_summary_rep` — реплицируемая таблица опубликованной сводки.'
+ - '`dm.dq_summary_dist` — распределенная поверхность чтения и записи.'
+ - '`dm.dq_summary_stage_rep` — общий донор дневных партиций.'
+ - '`dm.dq_summary_stage_dist` — распределенная поверхность донора.'
+ - '`dm.dq_summary_v` — публичное представление.'
+ fill:
+ - Даг `dq_check` при каждом прогоне сначала выполняет все пять проверок, затем заменяет готовые дневные партиции.
+ - '`sessions_vs_reference` — `sql/dq/sessions_vs_reference.sql`.'
+ - '`classes_vs_inventory` — `sql/dq/classes_vs_inventory.sql` и паспорт мира.'
+ - '`revenue_daily_vs_dds` — `sql/dq/revenue_daily_vs_dds.sql`.'
+ - '`purchase_vs_orders_vs_dds` — `sql/dq/purchase_vs_orders_vs_dds.sql`.'
+ - '`daily_traffic_vs_dds` — `sql/dq/daily_traffic_vs_dds.sql`.'
+ links:
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dm.md#user-content-dmdq_summary
+ - label: DDL таблиц
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/55-dm-tables.sql
+ - label: DDL представления
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/60-dm-views.sql
+
+ - id: dic_products
+ name: dic.products
+ layer: dic
+ order: 0
+ streams: [orders]
+ what: Словарь товаров вне цепочки слоев. По артикулу доступны название, бренд, категория и цена.
+ read: 'Читайте `dic.products`: у словаря одна поверхность без суффикса.'
+ parts:
+ - '`dic.products_file` — чтец CSV-каталога из файла репозитория.'
+ - '`dic.products` — словарь в памяти поверх подложки.'
+ keys:
+ - '`sku` — тот же артикул, что `item_sku` в позиции заказа DDS.'
+ fill:
+ - ClickHouse перечитывает подложку каждые 60–90 секунд по настройке `LIFETIME`.
+ links:
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-справочники
+ - label: DDL
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/05-dic-catalog.sql
+
+edges:
+ - id: hits_to_stg
+ from: kafka_hits
+ to: stg_hits
+ mechanism: materialized_view
+ streams: [events]
+ rhythm: непрерывно, по мере прихода сообщений
+ actor: Чтец `stg.hits_raw_kafka` и матвью `stg.hits_raw_mv`.
+ links:
+ - label: SQL
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/40-stg-views.sql
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-при%D1%91м-поток-и-его-свойства
+
+ - id: stg_to_event
+ from: stg_hits
+ to: ods_event
+ mechanism: materialized_view
+ streams: [events]
+ rhythm: непрерывно, по мере прихода сообщений
+ actor: Матвью `ods.event_mv` разбирает годные строки.
+ links:
+ - label: SQL
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/30-ods-views.sql
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-при%D1%91м-поток-и-его-свойства
+
+ - id: stg_to_event_errors
+ from: stg_hits
+ to: ods_event_errors
+ mechanism: materialized_view
+ streams: [events]
+ rhythm: непрерывно, по мере прихода сообщений
+ actor: Матвью `ods.event_errors_mv` отбирает строки, которые не прошли строгий прием.
+ links:
+ - label: SQL
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/30-ods-views.sql
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-таблица-ошибок
+
+ - id: orders_to_stg
+ from: kafka_orders
+ to: stg_orders
+ mechanism: airflow_dag
+ streams: [orders]
+ rhythm: пачкой, раз в модельные сутки
+ actor: Задача `pull_batch` дага `orders_ingest` читает `stg.orders_raw_kafka`.
+ links:
+ - label: SQL
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/stg/orders_raw_load.sql
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/orders/ingestion.md#user-content-одно-чтение-kafka
+
+ - id: stg_to_order
+ from: stg_orders
+ to: ods_order
+ mechanism: airflow_dag
+ streams: [orders]
+ rhythm: пачкой, раз в модельные сутки
+ actor: Задача `parse_batch` дага `orders_ingest` пишет годные версии заказа.
+ links:
+ - label: SQL
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ods/order_parse.sql
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/orders/ingestion.md#user-content-граница-строгого-при%D1%91ма
+
+ - id: stg_to_order_errors
+ from: stg_orders
+ to: ods_order_errors
+ mechanism: airflow_dag
+ streams: [orders]
+ rhythm: пачкой, раз в модельные сутки
+ actor: Задача `parse_batch` дага `orders_ingest` пишет негодные строки той же пачки.
+ links:
+ - label: SQL
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ods/order_parse.sql
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/orders/ingestion.md#user-content-граница-строгого-при%D1%91ма
+
+ - id: ods_to_dds_event
+ from: ods_event
+ to: dds_event
+ mechanism: view
+ streams: [events]
+ rhythm: без копии, данные читаются на месте
+ actor: Запрос `dds.event_v` читает данные из `ods.event_v` и переименовывает поля на языке бизнеса.
+ links:
+ - label: SQL
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/50-dds-views.sql
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dds/event.md#user-content-форма
+
+ - id: event_to_session
+ from: dds_event
+ to: dds_session
+ mechanism: airflow_dag
+ streams: [events]
+ rhythm: пачкой, при каждом прогоне `dds_transform`
+ actor: Задачи `session.rebuild` и `session.replace` дага `dds_transform`.
+ links:
+ - label: SQL сборки
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dds/session_rebuild.sql
+ - label: SQL замены
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dds/session_replace.sql
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dds/session.md#user-content-движок-и-наполнение
+
+ - id: order_to_dds
+ from: ods_order
+ to: dds_order
+ mechanism: airflow_dag
+ streams: [orders]
+ rhythm: пачкой, при каждом прогоне `dds_transform`
+ actor: Задачи `order.rebuild` и `order.replace` дага `dds_transform`.
+ links:
+ - label: SQL сборки
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dds/order_rebuild.sql
+ - label: SQL замены
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dds/order_replace.sql
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dds/order.md#user-content-движок-и-наполнение
+
+ - id: event_to_identity
+ from: dds_event
+ to: dds_identity
+ mechanism: airflow_dag
+ streams: [events]
+ rhythm: пачкой, при каждом прогоне `dds_transform`
+ actor: Задача `identity_map` дага `dds_transform` соединяет покупки с заказами.
+ links:
+ - label: SQL
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dds/identity_map_rebuild.sql
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dds/identity.md#user-content-сборка
+
+ - id: order_to_identity
+ from: dds_order
+ to: dds_identity
+ mechanism: airflow_dag
+ streams: [orders]
+ rhythm: пачкой, при каждом прогоне `dds_transform`
+ actor: Задача `identity_map` дага `dds_transform` соединяет заказы с покупками.
+ links:
+ - label: SQL
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dds/identity_map_rebuild.sql
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dds/identity.md#user-content-сборка
+
+ - id: session_to_traffic
+ from: dds_session
+ to: dm_daily_traffic
+ mechanism: airflow_dag
+ streams: [events]
+ rhythm: пачкой, при каждом прогоне `dm_transform`
+ actor: Задача `daily_traffic` дага `dm_transform` считает посетителей по сессиям.
+ links:
+ - label: SQL
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dm/daily_traffic_rebuild.sql
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dm.md#user-content-dmdaily_traffic_v
+
+ - id: identity_to_traffic
+ from: dds_identity
+ to: dm_daily_traffic
+ mechanism: airflow_dag
+ streams: [events]
+ rhythm: пачкой, при каждом прогоне `dm_transform`
+ actor: Задача `daily_traffic` дага `dm_transform` считает известных пользователей по `client_id`.
+ links:
+ - label: SQL
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dm/daily_traffic_rebuild.sql
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dm.md#user-content-dmdaily_traffic_v
+
+ - id: order_to_revenue
+ from: dds_order
+ to: dm_revenue
+ mechanism: airflow_dag
+ streams: [orders]
+ rhythm: пачкой, при каждом прогоне `dm_transform`
+ actor: Задачи `revenue_daily.rebuild` и `revenue_daily.replace` дага `dm_transform`.
+ links:
+ - label: SQL сборки
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dm/revenue_daily_rebuild.sql
+ - label: SQL замены
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dm/revenue_daily_replace.sql
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dm.md#user-content-dmrevenue_daily_v
+
+ - id: products_to_revenue
+ from: dic_products
+ to: dm_revenue
+ mechanism: airflow_dag
+ streams: [orders]
+ rhythm: пачкой, при каждом прогоне `dm_transform`
+ actor: Даг `dm_transform` получает категорию через `dictGet` по `item_sku`; ключ словаря — `sku`.
+ links:
+ - label: SQL
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dm/revenue_daily_rebuild.sql
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dm.md#user-content-dmrevenue_daily_v
+
+ - id: event_to_purchase
+ from: dds_event
+ to: dm_purchase
+ mechanism: airflow_dag
+ streams: [events]
+ rhythm: пачкой, при каждом прогоне `dm_transform`
+ actor: Задачи `purchase_vs_orders.rebuild` и `purchase_vs_orders.replace` дага `dm_transform` читают покупки.
+ links:
+ - label: SQL сборки
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dm/purchase_vs_orders_rebuild.sql
+ - label: SQL замены
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dm/purchase_vs_orders_replace.sql
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dm.md#user-content-dmpurchase_vs_orders_v
+
+ - id: order_to_purchase
+ from: dds_order
+ to: dm_purchase
+ mechanism: airflow_dag
+ streams: [orders]
+ rhythm: пачкой, при каждом прогоне `dm_transform`
+ actor: Задачи `purchase_vs_orders.rebuild` и `purchase_vs_orders.replace` дага `dm_transform` читают заказы.
+ links:
+ - label: SQL сборки
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dm/purchase_vs_orders_rebuild.sql
+ - label: SQL замены
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dm/purchase_vs_orders_replace.sql
+ - label: Справочник
+ url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dm.md#user-content-dmpurchase_vs_orders_v
diff --git a/docs/handbook/architecture-map/render.py b/docs/handbook/architecture-map/render.py
new file mode 100644
index 0000000..2398ccb
--- /dev/null
+++ b/docs/handbook/architecture-map/render.py
@@ -0,0 +1,54 @@
+# /// script
+# requires-python = ">=3.13"
+# dependencies = ["pyyaml"]
+# ///
+
+"""Собрать интерактивную карту хранилища из YAML и HTML-шаблона."""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+import yaml
+
+ROOT = Path(__file__).parent
+SOURCE = ROOT / "map.yaml"
+TEMPLATE = ROOT / "template.html"
+OUTPUT = ROOT.parent / "architecture-map.html"
+MARKER = "/*__MAP_DATA__*/"
+NOTICE = (
+ "\n"
+)
+
+
+def render() -> str:
+ """Подставить данные карты в шаблон без изменения порядка ключей."""
+ data = yaml.safe_load(SOURCE.read_text(encoding="utf-8"))
+ payload = json.dumps(data, ensure_ascii=False, indent=2)
+ template = TEMPLATE.read_text(encoding="utf-8")
+ return NOTICE + template.replace(MARKER, payload)
+
+
+def main() -> int:
+ """Записать карту или проверить, что готовый файл не устарел."""
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--check", action="store_true", help="проверить готовый файл")
+ args = parser.parse_args()
+ rendered = render()
+ if args.check:
+ if not OUTPUT.exists() or OUTPUT.read_text(encoding="utf-8") != rendered:
+ print(f"{OUTPUT} устарел; пересоберите карту")
+ return 1
+ print(f"{OUTPUT} соответствует исходникам")
+ return 0
+ OUTPUT.write_text(rendered, encoding="utf-8")
+ print(f"собрана карта: {OUTPUT}")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/docs/handbook/architecture-map/template.html b/docs/handbook/architecture-map/template.html
new file mode 100644
index 0000000..0a018ce
--- /dev/null
+++ b/docs/handbook/architecture-map/template.html
@@ -0,0 +1,453 @@
+
+
+
+
+
+ Карта слоев хранилища
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/handbook/storage-layers.md b/docs/handbook/storage-layers.md
index 26624b1..3494bfe 100644
--- a/docs/handbook/storage-layers.md
+++ b/docs/handbook/storage-layers.md
@@ -132,6 +132,8 @@ ORDER BY revenue DESC;
## Куда дальше
+Откройте [интерактивную карту](architecture-map.html) из своей копии
+репозитория и проследите путь данных.
Список объектов, сроки хранения и устройство таблиц — в [справочнике
хранилища](../architecture/storage.md). Как данные переносят между слоями — в
следующей главе: [потоки Airflow и даги модельного