Зачем: путеводитель рассказывает путь данных через слои словами, а карта дает его увидеть и потрогать: у одного смысла в каждом слое свое имя и своя поверхность, а переносят данные три разных механизма — матвью непрерывно, даг пачкой, представление без копии. Постановка — тикет #222, форма — резолюция #208 (один самодостаточный HTML, исходники рядом). Что: docs/handbook/architecture-map.html — готовая карта; исходники в docs/handbook/architecture-map/ (map.yaml — слои, узлы, стрелки, карточки; template.html — разметка, стили, скрипт; render.py — сборка и --check; README.md). Щелчок по узлу открывает карточку и подсвечивает путь, щелчок по стрелке — кто и каким SQL переносит данные; переключатель потока приглушает чужую часть карты. Ссылки карточек ведут в справочники и DDL на Gitea. Путеводитель и глава о хранилище ведут на карту; make lint проверяет ruff для сборщика и свежесть рендера; testing.md и AGENTS.md обновлены. Проверка: uv run docs/handbook/architecture-map/render.py --check и make lint зеленые; внешних запросов в HTML нет; ссылки и якоря карточек сверены с Gitea; карта прощелкана в headless Chromium на ширинах 1280–1920: подсветка пути, фильтр, Esc, карточки узлов и стрелок. Ревью: постановка — Opus; реализация — Codex (Sol), два слепых ревью Opus (уместность и код), вычитка прозы Sol.
55 lines
1.8 KiB
Python
55 lines
1.8 KiB
Python
# /// 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 = (
|
|
"<!-- Файл собран автоматически. Правьте architecture-map/map.yaml и "
|
|
"template.html, затем выполните uv run "
|
|
"docs/handbook/architecture-map/render.py. -->\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())
|