Интерактивная карта архитектуры: слои хранилища и путь данных #222

Closed
opened 2026-09-03 09:18:42 +03:00 by ddmitry · 1 comment
Owner

Part of #206

Цель

Интерактивная карта архитектуры хранилища — один самодостаточный HTML-файл
docs/handbook/architecture-map.html. Читатель открывает его двойным щелчком
из любой копии репозитория, без сети и сервера, и видит путь данных через
слои: топики Kafka → STG → ODS → DDS → DM, сбоку зона dic. Щелчок по узлу
показывает карточку объекта: что это, какой поверхностью его читать, из чего
он физически состоит, какими ключами соединяется с соседями, кто и в каком
ритме его наполняет, ссылки в справочник и DDL. Щелчок по стрелке показывает,
кто переносит данные между узлами и каким SQL.

Охват первой версии — только слои хранилища и путь данных через них
(резолюция #208). Потоки Airflow как второй слой карты, читатели DM
(Superset, Grafana) и генератор в эту версию не входят.

Урок карты: у одного смысла в каждом слое свое имя и своя поверхность, а
данные между слоями переносят три разных механизма — матвью непрерывно, даг
пачкой, представление без копии. Глава «Хранилище и его слои» это
рассказывает, карта дает это увидеть и потрогать.

Файлы

Файл Что это
docs/handbook/architecture-map.html Готовая карта. Рендер, не правится руками; строка-комментарий в начале файла говорит об этом и называет команду сборки
docs/handbook/architecture-map/map.yaml Исходник содержания: слои, узлы, стрелки, карточки
docs/handbook/architecture-map/template.html Исходник формы: разметка, стили, скрипт; данные подставляются в отмеченное место
docs/handbook/architecture-map/render.py Сборка: читает map.yaml, вкладывает его в шаблон как JSON, пишет HTML. Флаг --check сравнивает свежий рендер с файлом в репозитории и падает при расхождении
docs/handbook/architecture-map/README.md Пять-десять строк: что где лежит, как пересобрать, правило «правится исходник, файл пересобирается»

render.py — скрипт uv с inline-метаданными (# /// script), единственная
зависимость — pyyaml. Запуск: uv run docs/handbook/architecture-map/render.py.
Рендер детерминирован: тот же map.yaml и template.html дают байт в байт тот
же HTML (без времени сборки, без случайных id; ключи JSON — в порядке
map.yaml, не сортируются; ensure_ascii=False). Порядок полей карточки
задает шаблон, а не порядок ключей в данных.

Объем: render.py — не длиннее 80 строк; template.html — не длиннее 450
строк; готовый HTML — не тяжелее 80 КБ. Если форма и бюджет спорят,
побеждает форма: превышение назвать в отчете с причиной.

Содержание: слои, узлы, стрелки

Слои — колонки слева направо. У каждой колонки заголовок и подпись-вопрос из
главы о хранилище:

Слой Подпись
Kafka откуда приходит
STG как поступило
ODS язык источника
DDS язык бизнеса
DM данные для дашбордов

Зона dic — не колонка, а отдельный узел под колонками DDS и DM с подписью
«сбоку цепочки: справочники».

Узел — один логический объект; его физические формы (_kafka, _rep,
_dist, _mv, _stage_rep, _stage_dist, _v; таблицы ошибок *_errors_*
— отдельные узлы, у них свои формы) живут внутри карточки, а не отдельными
узлами. Имя узла на карте — имя, которым читатель его читает: поверхность
_v там, где представление есть; где его нет (STG, таблицы ошибок) — _dist;
для словаря — dic.products, для Kafka — имя топика.

Состав узлов (проверить по DDL в sql/ddl/ и карте таблиц
docs/architecture/storage.md#карта-таблиц; расхождение — исправить у себя и
назвать в отчете):

Слой Узел Потоки
Kafka топик hits hits
Kafka топик orders orders
STG stg.hits_raw_dist hits
STG stg.orders_raw_dist orders
ODS ods.event_v hits
ODS ods.event_errors_dist hits
ODS ods.order_v orders
ODS ods.order_errors_dist orders
DDS dds.event_v hits
DDS dds.session_v hits
DDS dds.order_v orders
DDS dds.identity_map_v hits, orders
DM dm.daily_traffic_v hits
DM dm.revenue_daily_v orders
DM dm.purchase_vs_orders_v hits, orders
DM dm.dq_summary_v hits, orders
dic dic.products orders

Стрелка — перенос данных из узла в узел. У каждой стрелки есть механизм, и
механизмов ровно три; они же — легенда карты:

Механизм Линия Ритм
матвью сплошная непрерывно, по мере прихода сообщений
даг Airflow штриховая пачкой: раз в модельные сутки (orders_ingest) или на каждом прогоне конвейера (dds_transform, dm_transform, dq_check)
представление пунктир без копии: данные читаются на месте

Состав стрелок — гипотеза постановки; источники каждой проверить по SQL в
sql/stg/, sql/ods/, sql/dds/, sql/dm/ и по дагам в dags/ (sql/dq/
стрелок не дает, см. ниже).
Стрелка, которой SQL не подтверждает, убирается; стрелка, которую SQL
показывает, а таблица нет, добавляется; и то и другое — в отчет.

Откуда Куда Механизм Кто именно
hits stg.hits_raw_dist матвью чтец stg.hits_raw_kafka, матвью stg.hits_raw_mv
stg.hits_raw_dist ods.event_v матвью ods.event_mv
stg.hits_raw_dist ods.event_errors_dist матвью ods.event_errors_mv
orders stg.orders_raw_dist даг orders_ingest, задача pull_batch, чтец stg.orders_raw_kafka, sql/stg/orders_raw_load.sql
stg.orders_raw_dist ods.order_v даг orders_ingest, задача parse_batch, sql/ods/order_parse.sql
stg.orders_raw_dist ods.order_errors_dist даг то же
ods.event_v dds.event_v представление sql/ddl/50-dds-views.sql
dds.event_v dds.session_v даг dds_transform, sql/dds/session_*.sql
ods.order_v dds.order_v даг dds_transform, sql/dds/order_*.sql
dds.event_v dds.identity_map_v даг dds_transform, sql/dds/identity_map_rebuild.sql
dds.order_v dds.identity_map_v даг то же
dds.session_v dm.daily_traffic_v даг dm_transform, sql/dm/daily_traffic_rebuild.sql
dds.identity_map_v dm.daily_traffic_v даг то же: известные пользователи по client_id
dds.order_v dm.revenue_daily_v даг dm_transform, sql/dm/revenue_daily_*.sql
dic.products dm.revenue_daily_v даг то же: категория товара — dictGet по item_sku заказа, ключ словаря sku
dds.event_v dm.purchase_vs_orders_v даг dm_transform, sql/dm/purchase_vs_orders_*.sql
dds.order_v dm.purchase_vs_orders_v даг то же

У dm.dq_summary_v стрелок нет. Ее наполняет даг dq_check, но он не
переносит данные, а сравнивает: витрины DM (в том числе те, что строятся на
уроках и в DDL стенда отсутствуют) с DDS, ODS, словарем и паспортом мира.
Рисовать это стрелками — значит утопить карту в линиях ради одного узла.
Узел остается в колонке DM, а его карточка в поле «Кто наполняет» объясняет
это своими словами и перечисляет пять проверок, которые запускает
dags/dq_check.py, с именами их SQL из sql/dq/.

У dic.products входящих стрелок тоже нет: словарь читает CSV-файл
(dic.products_file), это не путь данных через слои. Карточка называет
источник в поле «Из чего состоит».

Карточка узла

Поля, в этом порядке; пустое поле не показывается:

  1. Имя — заголовок карточки; под ним слой и потоки.
  2. Что это — одна-две фразы. Основа — колонка «Что это» карты таблиц и
    главы путеводителя; не пересказ справочника.
  3. Как читать — какой поверхностью читать и почему (_v — публичный
    договор; _dist у STG и ошибок — потому что представления там нет).
  4. Из чего состоит — список физических объектов с одной фразой на каждый:
    stg.hits_raw_kafka — чтец топика; stg.hits_raw_rep — реплицируемая
    таблица на шарде; и так далее. Имена — точно как в DDL.
  5. Ключи — колонки, которыми узел соединяется с соседями, с указанием, как
    тот же смысл называется у соседа. Основа — таблица «Сквозные ключи» главы о
    хранилище; имена колонок сверить с DDL.
  6. Кто наполняет и как часто — механизм и ритм из таблицы стрелок; для
    DDS и DM — правило обновления: замена партиции дня, полный пересчет новой
    версией, FINAL в представлении.
  7. Ссылки — «Справочник» (раздел docs/architecture/, для DDS и DM —
    файл объекта в docs/architecture/dds/ или раздел dm.md) и «DDL»
    (файл в sql/ddl/). Ссылки абсолютные, на ветку main Gitea:
    https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/<путь>#<якорь>.
    Карта открывается из любого места, относительные ссылки там не живут.
    Якорь — фактический id заголовка на странице Gitea, а не слаг по
    догадке: у заголовков с обратными кавычками он другой (dm.md
    дает dmrevenue_daily_v, не dm-revenue_daily_v). Снимать так:
    curl -sS <url файла> | grep -oE 'id="user-content-[^"]*"'.

Обязательные поля: у каждого узла — «Что это», «Как читать», «Кто наполняет
и как часто», «Ссылки»; «Из чего состоит» — у всех узлов, кроме топиков
Kafka (у dds.event_v поле состоит из одной фразы: только представление над
ods.event_v, своей таблицы нет); «Ключи» — у всех узлов ODS,
DDS, DM и у словаря, кроме таблиц ошибок и dm.dq_summary_v. В «Ссылках» у
узлов хранилища обе ссылки — справочник и DDL; у топиков Kafka вместо DDL —
описание формата (docs/formats/clickstream-event.md для hits,
docs/architecture/orders/snapshot.md для orders).

Карточка стрелки: заголовок «откуда → куда», механизм, ритм, кто именно
(даг и задача или матвью), ссылка на SQL и на раздел справочника
(docs/architecture/etl.md, docs/architecture/orders/ingestion.md,
docs/architecture/dds/*.md, docs/architecture/dm.md).

Каждый факт карточек берется из DDL, SQL, справочников docs/architecture/,
CONTEXT.md или глав путеводителя. Ничего не додумывается. Нашли расхождение
справочника с кодом — карточка следует коду, расхождение идет в отчет.

Взаимодействие

  • Щелчок по узлу — карточка в боковой панели справа; на карте узел
    выделен, его входящий путь (все узлы и стрелки, из которых он собран, до
    Kafka) и исходящий путь (все, что из него получается, до DM) подсвечены,
    остальное приглушено. Повторный щелчок или Esc снимает выделение. У узлов
    без стрелок (dic.products на входе, dm.dq_summary_v целиком) путь
    пуст — это не ошибка, подсвечивается только сам узел.
  • Щелчок по стрелке — карточка стрелки в той же панели; стрелка и ее два
    узла выделены.
  • Наведение — узел и его прямые соседи подсвечиваются слабее, чем при
    щелчке; уходит вместе с курсором.
  • Переключатель потока над картой: «Все», «События», «Заказы». Узлы и
    стрелки не выбранного потока приглушаются, но остаются на месте: карта не
    перестраивается.
  • Легенда трех механизмов — всегда видна, под переключателем.
  • Пока ничего не выбрано, панель показывает вводку в пять-семь строк: что
    это за карта, как ею пользоваться, ссылка на главу «Хранилище и его слои»
    (абсолютная, на Gitea).

Раскладку считает скрипт в браузере из данных: колонка — по слою, строка —
по полю order узла внутри слоя, зона dic — под колонками DDS и DM.
Стрелки между колонками — кривые Безье от правого края узла к левому краю
следующего. Стрелки внутри одной колонки (три в DDS) выходят из правого
края источника, изгибаются в промежуток между колонками и входят в правый
край цели; порядок узлов в DDS — dds.event_v, dds.session_v,
dds.order_v, dds.identity_map_v, чтобы источник всегда стоял выше цели.
Стрелка из dic выходит из верхнего края словаря. Карта — один SVG с viewBox, тянется
по ширине окна, при узком окне появляется горизонтальная прокрутка. Ни
масштабирования колесом, ни перетаскивания: карта помещается на экран.

Цвета: серые узлы и линии, один акцент для выделения, вторым тоном —
приглушение; потоки различаются не цветом, а фильтром. Шрифт системный.
Все тексты интерфейса — по-русски, обращение на «вы», тире «—» и
кавычки-елочки, без буквы «ё». Никаких внешних запросов: ни шрифтов, ни
скриптов, ни картинок по сети; файл работает по file://.

Где карта упоминается

  • docs/handbook/README.md: после раздела «Как читать» — раздел «Карта
    хранилища» в четыре-пять строк: что это, ссылка на architecture-map.html,
    совет открыть рядом с главой о хранилище. Gitea показывает файл как текст,
    а не как страницу, поэтому раздел говорит прямо: откройте файл из своей
    копии репозитория или скачайте его кнопкой «Raw» и откройте в браузере.
    Остальной текст не меняется.
  • docs/handbook/storage-layers.md, раздел «Куда дальше»: одна фраза со
    ссылкой на карту и той же оговоркой в двух словах — открывать из своей
    копии репозитория. Остальной текст главы не меняется.
  • Makefile, цель lint: третья строка
    uv run docs/handbook/architecture-map/render.py --check, а в две строки
    ruff добавляется путь docs/handbook/architecture-map. Комментарий над
    целью дополняется одной фразой: почему проверка рендера живет здесь
    (быстрая, стенд не нужен, ловит протухший рендер).
  • docs/architecture/testing.md: там, где описано, что утверждает
    make lint, — одна фраза про проверку рендера карты; длительность цели
    и дата замера, которые там записаны, обновляются по фактическому прогону.
    Больше ничего.
  • AGENTS.md, раздел «Структура», абзац про docs/handbook/: после фразы
    про индекс папки вставить ровно это предложение: «Исходники интерактивной
    карты лежат подпапкой architecture-map/, а ее рендер — файлом
    architecture-map.html рядом с главами.» Больше ничего.
  • render.py объявляет requires-python = ">=3.13": так target-version
    корневого ruff.toml для него верен, и сам ruff.toml не трогается.

Критерии приемки

  • uv run docs/handbook/architecture-map/render.py собирает
    docs/handbook/architecture-map.html; повторный запуск не меняет файл;
    --check зеленый на свежем рендере и красный, если в HTML изменить
    один символ.
  • HTML открывается из file:// без сети: ни одного внешнего src,
    href на стили и скрипты, @import, url() на сетевые адреса.
    Ссылки на Gitea в карточках — обычные <a target="_blank">.
  • Все узлы и стрелки из таблиц (с поправками, названными в отчете) на
    месте; каждый узел открывает карточку со всеми непустыми полями; каждая
    стрелка открывает свою карточку.
  • Выделение пути, наведение, переключатель потока, Esc, легенда и вводка
    работают так, как описано в «Взаимодействии». Браузера у исполнителя нет:
    он проверяет скрипт чтением и, где возможно, прогоном чистых функций
    раскладки и достижимости через node -e; проверка в браузере — за
    оркестратором на приемке, исполнитель называет в отчете, что проверить
    щелчками.
  • Каждое имя объекта, колонки, дага, задачи и SQL-файла в map.yaml
    существует в коде под этим именем.
  • Каждая ссылка карточек ведет на существующий файл в main
    (curl -sI отвечает 200) и на фактический id заголовка (снят командой
    из раздела «Карточка узла», не угадан).
  • Объемы в пределах раздела «Файлы».
  • make lint зеленый; ruff в него теперь проверяет и render.py.
  • docs/handbook/README.md, storage-layers.md, Makefile,
    testing.md изменены только в названных местах.
  • В прозе карточек, интерфейса и README.md карты нет «ё», слов «менти»
    и «оператор», олицетворений («дашборд спрашивает», «заказ ездит»);
    обращение на «вы».
  • Никаких других файлов, кроме перечисленных в разделах «Файлы» и «Где
    карта упоминается», не создано и не изменено.

Границы

  • Справочники docs/architecture/ (кроме названных правок в testing.md),
    CONTEXT.md, корневой README.md, ruff.toml, AGENTS.md (кроме одного
    названного предложения), DDL, SQL, даги и код генератора не правятся. Расхождение справочника с кодом — в отчет, не в правку.
  • Никаких JS-библиотек и сборщиков: ни mermaid, ни d3, ни npm. Скрипт — на
    чистом JS, стили — на чистом CSS, оба внутри шаблона.
  • Никаких коммитов и веток: правки остаются в рабочем дереве.
  • Никаких изменений вне репозитория.

Сначала прочитать

  1. docs/handbook/storage-layers.md — глава, которую карта дополняет;
    таблица «Сквозные ключи» и подписи слоев берутся отсюда.
  2. docs/architecture/storage.md — разделы «Имена объектов», «Служебные
    колонки», «Актуальные события», «Версии заказов», «Справочники», «Карта
    таблиц».
  3. sql/ddl/*.sql — имена и устройство всех объектов.
  4. sql/stg/, sql/ods/, sql/dds/, sql/dm/, sql/dq/ — источники
    каждой стрелки.
  5. dags/ — какая задача какого дага исполняет какой SQL; имена дагов и
    задач — отсюда.
  6. docs/architecture/etl.md, docs/architecture/orders/ingestion.md,
    docs/architecture/dds/README.md, docs/architecture/dm.md — ритмы и
    правила обновления для карточек.
  7. docs/handbook/README.md — куда встает раздел о карте.
  8. AGENTS.md, раздел «Язык».

Команды проверки

uv run docs/handbook/architecture-map/render.py
uv run docs/handbook/architecture-map/render.py --check
make lint
grep -nE 'https?://' docs/handbook/architecture-map.html | grep -v -e git.dementev.space -e www.w3.org
grep -n 'ё' docs/handbook/architecture-map/map.yaml docs/handbook/architecture-map/template.html docs/handbook/architecture-map/README.md

Последние две команды должны молчать. Стенд для задачи не нужен.

Part of #206 ## Цель Интерактивная карта архитектуры хранилища — один самодостаточный HTML-файл `docs/handbook/architecture-map.html`. Читатель открывает его двойным щелчком из любой копии репозитория, без сети и сервера, и видит путь данных через слои: топики Kafka → STG → ODS → DDS → DM, сбоку зона `dic`. Щелчок по узлу показывает карточку объекта: что это, какой поверхностью его читать, из чего он физически состоит, какими ключами соединяется с соседями, кто и в каком ритме его наполняет, ссылки в справочник и DDL. Щелчок по стрелке показывает, кто переносит данные между узлами и каким SQL. Охват первой версии — только слои хранилища и путь данных через них (резолюция #208). Потоки Airflow как второй слой карты, читатели DM (Superset, Grafana) и генератор в эту версию не входят. Урок карты: у одного смысла в каждом слое свое имя и своя поверхность, а данные между слоями переносят три разных механизма — матвью непрерывно, даг пачкой, представление без копии. Глава «Хранилище и его слои» это рассказывает, карта дает это увидеть и потрогать. ## Файлы | Файл | Что это | |---|---| | `docs/handbook/architecture-map.html` | Готовая карта. Рендер, не правится руками; строка-комментарий в начале файла говорит об этом и называет команду сборки | | `docs/handbook/architecture-map/map.yaml` | Исходник содержания: слои, узлы, стрелки, карточки | | `docs/handbook/architecture-map/template.html` | Исходник формы: разметка, стили, скрипт; данные подставляются в отмеченное место | | `docs/handbook/architecture-map/render.py` | Сборка: читает `map.yaml`, вкладывает его в шаблон как JSON, пишет HTML. Флаг `--check` сравнивает свежий рендер с файлом в репозитории и падает при расхождении | | `docs/handbook/architecture-map/README.md` | Пять-десять строк: что где лежит, как пересобрать, правило «правится исходник, файл пересобирается» | `render.py` — скрипт `uv` с inline-метаданными (`# /// script`), единственная зависимость — `pyyaml`. Запуск: `uv run docs/handbook/architecture-map/render.py`. Рендер детерминирован: тот же `map.yaml` и `template.html` дают байт в байт тот же HTML (без времени сборки, без случайных id; ключи JSON — в порядке `map.yaml`, не сортируются; `ensure_ascii=False`). Порядок полей карточки задает шаблон, а не порядок ключей в данных. Объем: `render.py` — не длиннее 80 строк; `template.html` — не длиннее 450 строк; готовый HTML — не тяжелее 80 КБ. Если форма и бюджет спорят, побеждает форма: превышение назвать в отчете с причиной. ## Содержание: слои, узлы, стрелки Слои — колонки слева направо. У каждой колонки заголовок и подпись-вопрос из главы о хранилище: | Слой | Подпись | |---|---| | Kafka | откуда приходит | | STG | как поступило | | ODS | язык источника | | DDS | язык бизнеса | | DM | данные для дашбордов | Зона `dic` — не колонка, а отдельный узел под колонками DDS и DM с подписью «сбоку цепочки: справочники». Узел — один логический объект; его физические формы (`_kafka`, `_rep`, `_dist`, `_mv`, `_stage_rep`, `_stage_dist`, `_v`; таблицы ошибок `*_errors_*` — отдельные узлы, у них свои формы) живут внутри карточки, а не отдельными узлами. Имя узла на карте — имя, которым читатель его читает: поверхность `_v` там, где представление есть; где его нет (STG, таблицы ошибок) — `_dist`; для словаря — `dic.products`, для Kafka — имя топика. Состав узлов (проверить по DDL в `sql/ddl/` и карте таблиц `docs/architecture/storage.md#карта-таблиц`; расхождение — исправить у себя и назвать в отчете): | Слой | Узел | Потоки | |---|---|---| | Kafka | топик `hits` | hits | | Kafka | топик `orders` | orders | | STG | `stg.hits_raw_dist` | hits | | STG | `stg.orders_raw_dist` | orders | | ODS | `ods.event_v` | hits | | ODS | `ods.event_errors_dist` | hits | | ODS | `ods.order_v` | orders | | ODS | `ods.order_errors_dist` | orders | | DDS | `dds.event_v` | hits | | DDS | `dds.session_v` | hits | | DDS | `dds.order_v` | orders | | DDS | `dds.identity_map_v` | hits, orders | | DM | `dm.daily_traffic_v` | hits | | DM | `dm.revenue_daily_v` | orders | | DM | `dm.purchase_vs_orders_v` | hits, orders | | DM | `dm.dq_summary_v` | hits, orders | | dic | `dic.products` | orders | Стрелка — перенос данных из узла в узел. У каждой стрелки есть механизм, и механизмов ровно три; они же — легенда карты: | Механизм | Линия | Ритм | |---|---|---| | матвью | сплошная | непрерывно, по мере прихода сообщений | | даг Airflow | штриховая | пачкой: раз в модельные сутки (`orders_ingest`) или на каждом прогоне конвейера (`dds_transform`, `dm_transform`, `dq_check`) | | представление | пунктир | без копии: данные читаются на месте | Состав стрелок — гипотеза постановки; источники каждой проверить по SQL в `sql/stg/`, `sql/ods/`, `sql/dds/`, `sql/dm/` и по дагам в `dags/` (`sql/dq/` стрелок не дает, см. ниже). Стрелка, которой SQL не подтверждает, убирается; стрелка, которую SQL показывает, а таблица нет, добавляется; и то и другое — в отчет. | Откуда | Куда | Механизм | Кто именно | |---|---|---|---| | `hits` | `stg.hits_raw_dist` | матвью | чтец `stg.hits_raw_kafka`, матвью `stg.hits_raw_mv` | | `stg.hits_raw_dist` | `ods.event_v` | матвью | `ods.event_mv` | | `stg.hits_raw_dist` | `ods.event_errors_dist` | матвью | `ods.event_errors_mv` | | `orders` | `stg.orders_raw_dist` | даг | `orders_ingest`, задача `pull_batch`, чтец `stg.orders_raw_kafka`, `sql/stg/orders_raw_load.sql` | | `stg.orders_raw_dist` | `ods.order_v` | даг | `orders_ingest`, задача `parse_batch`, `sql/ods/order_parse.sql` | | `stg.orders_raw_dist` | `ods.order_errors_dist` | даг | то же | | `ods.event_v` | `dds.event_v` | представление | `sql/ddl/50-dds-views.sql` | | `dds.event_v` | `dds.session_v` | даг | `dds_transform`, `sql/dds/session_*.sql` | | `ods.order_v` | `dds.order_v` | даг | `dds_transform`, `sql/dds/order_*.sql` | | `dds.event_v` | `dds.identity_map_v` | даг | `dds_transform`, `sql/dds/identity_map_rebuild.sql` | | `dds.order_v` | `dds.identity_map_v` | даг | то же | | `dds.session_v` | `dm.daily_traffic_v` | даг | `dm_transform`, `sql/dm/daily_traffic_rebuild.sql` | | `dds.identity_map_v` | `dm.daily_traffic_v` | даг | то же: известные пользователи по `client_id` | | `dds.order_v` | `dm.revenue_daily_v` | даг | `dm_transform`, `sql/dm/revenue_daily_*.sql` | | `dic.products` | `dm.revenue_daily_v` | даг | то же: категория товара — `dictGet` по `item_sku` заказа, ключ словаря `sku` | | `dds.event_v` | `dm.purchase_vs_orders_v` | даг | `dm_transform`, `sql/dm/purchase_vs_orders_*.sql` | | `dds.order_v` | `dm.purchase_vs_orders_v` | даг | то же | У `dm.dq_summary_v` стрелок нет. Ее наполняет даг `dq_check`, но он не переносит данные, а сравнивает: витрины DM (в том числе те, что строятся на уроках и в DDL стенда отсутствуют) с DDS, ODS, словарем и паспортом мира. Рисовать это стрелками — значит утопить карту в линиях ради одного узла. Узел остается в колонке DM, а его карточка в поле «Кто наполняет» объясняет это своими словами и перечисляет пять проверок, которые запускает `dags/dq_check.py`, с именами их SQL из `sql/dq/`. У `dic.products` входящих стрелок тоже нет: словарь читает CSV-файл (`dic.products_file`), это не путь данных через слои. Карточка называет источник в поле «Из чего состоит». ## Карточка узла Поля, в этом порядке; пустое поле не показывается: 1. **Имя** — заголовок карточки; под ним слой и потоки. 2. **Что это** — одна-две фразы. Основа — колонка «Что это» карты таблиц и главы путеводителя; не пересказ справочника. 3. **Как читать** — какой поверхностью читать и почему (`_v` — публичный договор; `_dist` у STG и ошибок — потому что представления там нет). 4. **Из чего состоит** — список физических объектов с одной фразой на каждый: `stg.hits_raw_kafka` — чтец топика; `stg.hits_raw_rep` — реплицируемая таблица на шарде; и так далее. Имена — точно как в DDL. 5. **Ключи** — колонки, которыми узел соединяется с соседями, с указанием, как тот же смысл называется у соседа. Основа — таблица «Сквозные ключи» главы о хранилище; имена колонок сверить с DDL. 6. **Кто наполняет и как часто** — механизм и ритм из таблицы стрелок; для DDS и DM — правило обновления: замена партиции дня, полный пересчет новой версией, `FINAL` в представлении. 7. **Ссылки** — «Справочник» (раздел `docs/architecture/`, для DDS и DM — файл объекта в `docs/architecture/dds/` или раздел `dm.md`) и «DDL» (файл в `sql/ddl/`). Ссылки абсолютные, на ветку `main` Gitea: `https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/<путь>#<якорь>`. Карта открывается из любого места, относительные ссылки там не живут. Якорь — фактический id заголовка на странице Gitea, а не слаг по догадке: у заголовков с обратными кавычками он другой (`dm.md` дает `dmrevenue_daily_v`, не `dm-revenue_daily_v`). Снимать так: `curl -sS <url файла> | grep -oE 'id="user-content-[^"]*"'`. Обязательные поля: у каждого узла — «Что это», «Как читать», «Кто наполняет и как часто», «Ссылки»; «Из чего состоит» — у всех узлов, кроме топиков Kafka (у `dds.event_v` поле состоит из одной фразы: только представление над `ods.event_v`, своей таблицы нет); «Ключи» — у всех узлов ODS, DDS, DM и у словаря, кроме таблиц ошибок и `dm.dq_summary_v`. В «Ссылках» у узлов хранилища обе ссылки — справочник и DDL; у топиков Kafka вместо DDL — описание формата (`docs/formats/clickstream-event.md` для `hits`, `docs/architecture/orders/snapshot.md` для `orders`). Карточка стрелки: заголовок «откуда → куда», механизм, ритм, кто именно (даг и задача или матвью), ссылка на SQL и на раздел справочника (`docs/architecture/etl.md`, `docs/architecture/orders/ingestion.md`, `docs/architecture/dds/*.md`, `docs/architecture/dm.md`). Каждый факт карточек берется из DDL, SQL, справочников `docs/architecture/`, `CONTEXT.md` или глав путеводителя. Ничего не додумывается. Нашли расхождение справочника с кодом — карточка следует коду, расхождение идет в отчет. ## Взаимодействие - **Щелчок по узлу** — карточка в боковой панели справа; на карте узел выделен, его входящий путь (все узлы и стрелки, из которых он собран, до Kafka) и исходящий путь (все, что из него получается, до DM) подсвечены, остальное приглушено. Повторный щелчок или Esc снимает выделение. У узлов без стрелок (`dic.products` на входе, `dm.dq_summary_v` целиком) путь пуст — это не ошибка, подсвечивается только сам узел. - **Щелчок по стрелке** — карточка стрелки в той же панели; стрелка и ее два узла выделены. - **Наведение** — узел и его прямые соседи подсвечиваются слабее, чем при щелчке; уходит вместе с курсором. - **Переключатель потока** над картой: «Все», «События», «Заказы». Узлы и стрелки не выбранного потока приглушаются, но остаются на месте: карта не перестраивается. - **Легенда** трех механизмов — всегда видна, под переключателем. - Пока ничего не выбрано, панель показывает вводку в пять-семь строк: что это за карта, как ею пользоваться, ссылка на главу «Хранилище и его слои» (абсолютная, на Gitea). Раскладку считает скрипт в браузере из данных: колонка — по слою, строка — по полю `order` узла внутри слоя, зона `dic` — под колонками DDS и DM. Стрелки между колонками — кривые Безье от правого края узла к левому краю следующего. Стрелки внутри одной колонки (три в DDS) выходят из правого края источника, изгибаются в промежуток между колонками и входят в правый край цели; порядок узлов в DDS — `dds.event_v`, `dds.session_v`, `dds.order_v`, `dds.identity_map_v`, чтобы источник всегда стоял выше цели. Стрелка из `dic` выходит из верхнего края словаря. Карта — один SVG с `viewBox`, тянется по ширине окна, при узком окне появляется горизонтальная прокрутка. Ни масштабирования колесом, ни перетаскивания: карта помещается на экран. Цвета: серые узлы и линии, один акцент для выделения, вторым тоном — приглушение; потоки различаются не цветом, а фильтром. Шрифт системный. Все тексты интерфейса — по-русски, обращение на «вы», тире «—» и кавычки-елочки, без буквы «ё». Никаких внешних запросов: ни шрифтов, ни скриптов, ни картинок по сети; файл работает по `file://`. ## Где карта упоминается - `docs/handbook/README.md`: после раздела «Как читать» — раздел «Карта хранилища» в четыре-пять строк: что это, ссылка на `architecture-map.html`, совет открыть рядом с главой о хранилище. Gitea показывает файл как текст, а не как страницу, поэтому раздел говорит прямо: откройте файл из своей копии репозитория или скачайте его кнопкой «Raw» и откройте в браузере. Остальной текст не меняется. - `docs/handbook/storage-layers.md`, раздел «Куда дальше»: одна фраза со ссылкой на карту и той же оговоркой в двух словах — открывать из своей копии репозитория. Остальной текст главы не меняется. - `Makefile`, цель `lint`: третья строка `uv run docs/handbook/architecture-map/render.py --check`, а в две строки `ruff` добавляется путь `docs/handbook/architecture-map`. Комментарий над целью дополняется одной фразой: почему проверка рендера живет здесь (быстрая, стенд не нужен, ловит протухший рендер). - `docs/architecture/testing.md`: там, где описано, что утверждает `make lint`, — одна фраза про проверку рендера карты; длительность цели и дата замера, которые там записаны, обновляются по фактическому прогону. Больше ничего. - `AGENTS.md`, раздел «Структура», абзац про `docs/handbook/`: после фразы про индекс папки вставить ровно это предложение: «Исходники интерактивной карты лежат подпапкой `architecture-map/`, а ее рендер — файлом `architecture-map.html` рядом с главами.» Больше ничего. - `render.py` объявляет `requires-python = ">=3.13"`: так `target-version` корневого `ruff.toml` для него верен, и сам `ruff.toml` не трогается. ## Критерии приемки - [ ] `uv run docs/handbook/architecture-map/render.py` собирает `docs/handbook/architecture-map.html`; повторный запуск не меняет файл; `--check` зеленый на свежем рендере и красный, если в HTML изменить один символ. - [ ] HTML открывается из `file://` без сети: ни одного внешнего `src`, `href` на стили и скрипты, `@import`, `url()` на сетевые адреса. Ссылки на Gitea в карточках — обычные `<a target="_blank">`. - [ ] Все узлы и стрелки из таблиц (с поправками, названными в отчете) на месте; каждый узел открывает карточку со всеми непустыми полями; каждая стрелка открывает свою карточку. - [ ] Выделение пути, наведение, переключатель потока, Esc, легенда и вводка работают так, как описано в «Взаимодействии». Браузера у исполнителя нет: он проверяет скрипт чтением и, где возможно, прогоном чистых функций раскладки и достижимости через `node -e`; проверка в браузере — за оркестратором на приемке, исполнитель называет в отчете, что проверить щелчками. - [ ] Каждое имя объекта, колонки, дага, задачи и SQL-файла в `map.yaml` существует в коде под этим именем. - [ ] Каждая ссылка карточек ведет на существующий файл в `main` (`curl -sI` отвечает 200) и на фактический id заголовка (снят командой из раздела «Карточка узла», не угадан). - [ ] Объемы в пределах раздела «Файлы». - [ ] `make lint` зеленый; `ruff` в него теперь проверяет и `render.py`. - [ ] `docs/handbook/README.md`, `storage-layers.md`, `Makefile`, `testing.md` изменены только в названных местах. - [ ] В прозе карточек, интерфейса и `README.md` карты нет «ё», слов «менти» и «оператор», олицетворений («дашборд спрашивает», «заказ ездит»); обращение на «вы». - [ ] Никаких других файлов, кроме перечисленных в разделах «Файлы» и «Где карта упоминается», не создано и не изменено. ## Границы - Справочники `docs/architecture/` (кроме названных правок в `testing.md`), `CONTEXT.md`, корневой `README.md`, `ruff.toml`, `AGENTS.md` (кроме одного названного предложения), DDL, SQL, даги и код генератора не правятся. Расхождение справочника с кодом — в отчет, не в правку. - Никаких JS-библиотек и сборщиков: ни mermaid, ни d3, ни npm. Скрипт — на чистом JS, стили — на чистом CSS, оба внутри шаблона. - Никаких коммитов и веток: правки остаются в рабочем дереве. - Никаких изменений вне репозитория. ## Сначала прочитать 1. `docs/handbook/storage-layers.md` — глава, которую карта дополняет; таблица «Сквозные ключи» и подписи слоев берутся отсюда. 2. `docs/architecture/storage.md` — разделы «Имена объектов», «Служебные колонки», «Актуальные события», «Версии заказов», «Справочники», «Карта таблиц». 3. `sql/ddl/*.sql` — имена и устройство всех объектов. 4. `sql/stg/`, `sql/ods/`, `sql/dds/`, `sql/dm/`, `sql/dq/` — источники каждой стрелки. 5. `dags/` — какая задача какого дага исполняет какой SQL; имена дагов и задач — отсюда. 6. `docs/architecture/etl.md`, `docs/architecture/orders/ingestion.md`, `docs/architecture/dds/README.md`, `docs/architecture/dm.md` — ритмы и правила обновления для карточек. 7. `docs/handbook/README.md` — куда встает раздел о карте. 8. `AGENTS.md`, раздел «Язык». ## Команды проверки ```bash uv run docs/handbook/architecture-map/render.py uv run docs/handbook/architecture-map/render.py --check make lint grep -nE 'https?://' docs/handbook/architecture-map.html | grep -v -e git.dementev.space -e www.w3.org grep -n 'ё' docs/handbook/architecture-map/map.yaml docs/handbook/architecture-map/template.html docs/handbook/architecture-map/README.md ``` Последние две команды должны молчать. Стенд для задачи не нужен.
ddmitry added the wayfinder:task label 2026-09-03 09:18:42 +03:00
ddmitry self-assigned this 2026-09-03 09:18:43 +03:00
Author
Owner

Сделано, PR #223 (сливается с Closes #222).

Что получилось. docs/handbook/architecture-map.html — один самодостаточный файл: пять колонок Kafka → STG → ODS → DDS → DM, зона dic сбоку, 17 узлов и 17 стрелок. Щелчок по узлу открывает карточку (что это, как читать, из чего состоит, ключи, кто наполняет, ссылки в справочник и DDL на Gitea) и подсвечивает входящий и исходящий путь; щелчок по стрелке — механизм, ритм, даг и задача, SQL. Переключатель потока приглушает чужую часть карты, легенда трех механизмов всегда на виду. Исходники — docs/handbook/architecture-map/ (map.yaml, template.html, render.py, README.md); make lint проверяет ruff для сборщика и свежесть рендера.

Решения по ходу.

  • У dm.dq_summary_v стрелок нет: dq_check сравнивает, а не переносит; карточка объясняет это и называет пять проверок. У dic.products входа нет — словарь читает CSV.
  • Стрелки внутри колонки DDS идут петлями через промежуток между колонками; кривая из словаря проведена так, чтобы не проходить под узлами.
  • Якоря ссылок — фактические id заголовков Gitea (у заголовков с обратными кавычками они не совпадают со слагом); ё в якорях закодирована.
  • Карта тянется по ширине окна; уже 1530 px панель карточки уходит под карту, чтобы колонка DM не обрезалась.
  • Gitea показывает HTML как текст: README путеводителя и глава о хранилище говорят открывать файл из своей копии репозитория.
  • Шаблон 453 строки при бюджете 450 — форма важнее бюджета.

Расхождения справочника с кодом (не правились): storage.md во вводке называет две физические модели DDS этапа 4 и не упоминает карту идентичностей; описание 55-dm-tables.sql там же не упоминает dm.dq_summary (карта таблиц ниже его содержит).

Конвейер. Постановка — Fable, ревью постановки — Opus (18 находок, все внесены). Реализация — Codex (Sol), саморевью 7/7. Два слепых ревью Opus: уместность и вкус (11 находок), код (10); два круга починки, все закрыты; от оркестратора — проверка в headless Chromium (карта не помещалась в окно, совет читать Kafka-таблицу напрямую). Вычитка прозы — Sol: 36 находок, 34 внесены.

Дальше по карте: ТЗ ступеней 2–7 в docs/labs/ — следующий тикет открывается при старте работы.

Сделано, PR #223 (сливается с `Closes #222`). **Что получилось.** `docs/handbook/architecture-map.html` — один самодостаточный файл: пять колонок Kafka → STG → ODS → DDS → DM, зона `dic` сбоку, 17 узлов и 17 стрелок. Щелчок по узлу открывает карточку (что это, как читать, из чего состоит, ключи, кто наполняет, ссылки в справочник и DDL на Gitea) и подсвечивает входящий и исходящий путь; щелчок по стрелке — механизм, ритм, даг и задача, SQL. Переключатель потока приглушает чужую часть карты, легенда трех механизмов всегда на виду. Исходники — `docs/handbook/architecture-map/` (`map.yaml`, `template.html`, `render.py`, `README.md`); `make lint` проверяет ruff для сборщика и свежесть рендера. **Решения по ходу.** - У `dm.dq_summary_v` стрелок нет: `dq_check` сравнивает, а не переносит; карточка объясняет это и называет пять проверок. У `dic.products` входа нет — словарь читает CSV. - Стрелки внутри колонки DDS идут петлями через промежуток между колонками; кривая из словаря проведена так, чтобы не проходить под узлами. - Якоря ссылок — фактические id заголовков Gitea (у заголовков с обратными кавычками они не совпадают со слагом); `ё` в якорях закодирована. - Карта тянется по ширине окна; уже 1530 px панель карточки уходит под карту, чтобы колонка DM не обрезалась. - Gitea показывает HTML как текст: README путеводителя и глава о хранилище говорят открывать файл из своей копии репозитория. - Шаблон 453 строки при бюджете 450 — форма важнее бюджета. **Расхождения справочника с кодом (не правились):** `storage.md` во вводке называет две физические модели DDS этапа 4 и не упоминает карту идентичностей; описание `55-dm-tables.sql` там же не упоминает `dm.dq_summary` (карта таблиц ниже его содержит). **Конвейер.** Постановка — Fable, ревью постановки — Opus (18 находок, все внесены). Реализация — Codex (Sol), саморевью 7/7. Два слепых ревью Opus: уместность и вкус (11 находок), код (10); два круга починки, все закрыты; от оркестратора — проверка в headless Chromium (карта не помещалась в окно, совет читать Kafka-таблицу напрямую). Вычитка прозы — Sol: 36 находок, 34 внесены. **Дальше по карте:** ТЗ ступеней 2–7 в `docs/labs/` — следующий тикет открывается при старте работы.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: ddmitry/clickstream-data-platform#222