Expand architecture documentation: enhance preview.html with an application layer component registry, update tooltips, refine SVG rendering, and adjust Archimate model structure.

This commit is contained in:
emelinda 2026-08-20 21:54:37 +03:00
parent 2647f2554b
commit 4c5f812003
6 changed files with 965 additions and 863 deletions

View File

@ -10,7 +10,7 @@
|---|---|---|
| `example-technology.archimate` | нативный формат [Archi](https://www.archimatetool.com/) | десктопный Archi: File → Open |
| `example-technology.xml` | The Open Group ArchiMate Model Exchange File Format 3.1 | любой инструмент с импортом Open Exchange, в том числе archi-online.com: **Import, save, and create models** → Open Exchange |
| `preview.html` | SVG-рендер всех представлений | любой браузер, без установки чего-либо |
| `preview.html` | SVG-рендер всех представлений плюс справочник компонентов прикладного слоя | любой браузер, без установки чего-либо |
| `matrix.example.json` | снимок матрицы «приложение → технологический сервис» | вход для сборки |
| `calls.example.json` | снимок графа межсервисных вызовов | вход для сборки |
| `app-summaries.md` | короткие описания компонентов прикладного слоя (13 предложения) | вход для сборки; попадает в поле `Documentation` модели |
@ -30,6 +30,8 @@
| A2A12 | По представлению на сервис с тремя и более потребителями: кто от него зависит |
| P1P13 | Паспорт сервиса — зеркало к A2A12: не «кто сломается без него», а «без чего не работает он сам» |
`preview.html` показывает не только схемы. Сверху — оглавление по всем представлениям, внизу — справочник прикладного слоя: описания всех компонентов, разложенные по тем же смысловым группам, что и на карте A0. Описания лежат в модели (поле `Documentation`), но на схемах их не видно — там помещается только имя, поэтому то же описание всплывает подсказкой при наведении на любой блок любой схемы. В Archi эти же тексты видны в панели свойств.
Направление связи — как принято в ArchiMate: стрелка идёт от вызываемого к вызывающему (serving). Читается как «кого лишишься — тот и сломается». Тот же вопрос с другой стороны — представления P: что нужно самому сервису, чтобы работать.
Разбиение по сервисам сделано осознанно: 152 связи «технологический сервис → приложение» и 86 межсервисных вызовов на одной схеме нечитаемы. В модели они лежат одним набором, представления — это срезы.

View File

@ -2,7 +2,7 @@
"_комментарий": "Профиль контура: всё, что зависит от конкретной инсталляции. Значения здесь — ПРИМЕР. Для реального контура скопируйте файл, подставьте настоящие имена и держите копию вне репозитория. Поля doc попадают в Documentation элемента и видны в Archi.",
"model": {
"name": "СДЕ Sarex — типовой контур (пример)",
"purpose": "Технологический слой типового контура. Имена кластера, узлов, доменов и реестра заменены на примеры. Прикладной слой присутствует как потребители технологических сервисов и как состав групп узлов; межсервисные связи приложений моделируются отдельно."
"purpose": "Архитектура типового контура платформы в двух слоях. Технологический: кластер, платформенные компоненты внутри него, внешние системы за периметром и технологические сервисы, которые всё это даёт приложениям. Прикладной: компоненты платформы по смысловым группам, вызовы между ними и зависимость каждого от технологических сервисов. Имена кластера, узлов, доменов и реестра заменены на примеры — конкретная инсталляция задаётся отдельным профилем контура.\n\nСвязи не нарисованы от руки, а выведены из манифестов развёртывания: переменных окружения, аннотаций и маршрутов. Исключения оговорены отдельно — внешние интеграции и смысловая группировка приложений заданы профилем, описания компонентов получены чтением исходного кода. Как читать нотацию — представление «L1»."
},
"cluster": {
"name": "Кластер k8s-prod",

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,9 @@
xsi:schemaLocation="http://www.opengroup.org/xsd/archimate/3.0/ https://www.opengroup.org/xsd/archimate/3.1/archimate3_Diagram.xsd"
identifier="model-contour">
<name xml:lang="ru">СДЕ Sarex — типовой контур (пример)</name>
<documentation xml:lang="ru">Технологический слой типового контура. Имена кластера, узлов, доменов и реестра заменены на примеры. Прикладной слой присутствует как потребители технологических сервисов и как состав групп узлов; межсервисные связи приложений моделируются отдельно.</documentation>
<documentation xml:lang="ru">Архитектура типового контура платформы в двух слоях. Технологический: кластер, платформенные компоненты внутри него, внешние системы за периметром и технологические сервисы, которые всё это даёт приложениям. Прикладной: компоненты платформы по смысловым группам, вызовы между ними и зависимость каждого от технологических сервисов. Имена кластера, узлов, доменов и реестра заменены на примеры — конкретная инсталляция задаётся отдельным профилем контура.
Связи не нарисованы от руки, а выведены из манифестов развёртывания: переменных окружения, аннотаций и маршрутов. Исключения оговорены отдельно — внешние интеграции и смысловая группировка приложений заданы профилем, описания компонентов получены чтением исходного кода. Как читать нотацию — представление «L1».</documentation>
<elements>
<element identifier="ac-ams-sync" xsi:type="ApplicationComponent">
<name xml:lang="ru">ams-sync</name>
@ -1489,34 +1491,34 @@
<node identifier="do-0184" elementRef="ac-flows" xsi:type="Element" x="830" y="320" w="170" h="55"/>
<node identifier="do-0185" elementRef="ac-processing" xsi:type="Element" x="711" y="532" w="170" h="55"/>
<node identifier="do-0186" elementRef="ac-eav" xsi:type="Element" x="425" y="620" w="170" h="55"/>
<node identifier="do-0187" elementRef="ac-issues" xsi:type="Element" x="139" y="532" w="170" h="55"/>
<node identifier="do-0188" elementRef="ac-resources" xsi:type="Element" x="20" y="320" w="170" h="55"/>
<node identifier="do-0187" elementRef="ac-resources" xsi:type="Element" x="139" y="532" w="170" h="55"/>
<node identifier="do-0188" elementRef="ac-issues" xsi:type="Element" x="20" y="320" w="170" h="55"/>
<node identifier="do-0189" elementRef="ac-pm" xsi:type="Element" x="139" y="108" w="170" h="55"/>
<connection identifier="cn-0190" relationshipRef="rel-295" xsi:type="Relationship" source="do-0182" target="do-0183"/>
<connection identifier="cn-0199" relationshipRef="rel-322" xsi:type="Relationship" source="do-0182" target="do-0184"/>
<connection identifier="cn-0205" relationshipRef="rel-331" xsi:type="Relationship" source="do-0182" target="do-0187"/>
<connection identifier="cn-0205" relationshipRef="rel-331" xsi:type="Relationship" source="do-0182" target="do-0188"/>
<connection identifier="cn-0214" relationshipRef="rel-351" xsi:type="Relationship" source="do-0182" target="do-0185"/>
<connection identifier="cn-0193" relationshipRef="rel-302" xsi:type="Relationship" source="do-0183" target="do-0182"/>
<connection identifier="cn-0198" relationshipRef="rel-321" xsi:type="Relationship" source="do-0183" target="do-0184"/>
<connection identifier="cn-0204" relationshipRef="rel-330" xsi:type="Relationship" source="do-0183" target="do-0187"/>
<connection identifier="cn-0204" relationshipRef="rel-330" xsi:type="Relationship" source="do-0183" target="do-0188"/>
<connection identifier="cn-0210" relationshipRef="rel-346" xsi:type="Relationship" source="do-0183" target="do-0189"/>
<connection identifier="cn-0213" relationshipRef="rel-350" xsi:type="Relationship" source="do-0183" target="do-0185"/>
<connection identifier="cn-0216" relationshipRef="rel-354" xsi:type="Relationship" source="do-0183" target="do-0188"/>
<connection identifier="cn-0216" relationshipRef="rel-354" xsi:type="Relationship" source="do-0183" target="do-0187"/>
<connection identifier="cn-0195" relationshipRef="rel-305" xsi:type="Relationship" source="do-0184" target="do-0182"/>
<connection identifier="cn-0207" relationshipRef="rel-333" xsi:type="Relationship" source="do-0184" target="do-0187"/>
<connection identifier="cn-0207" relationshipRef="rel-333" xsi:type="Relationship" source="do-0184" target="do-0188"/>
<connection identifier="cn-0192" relationshipRef="rel-298" xsi:type="Relationship" source="do-0185" target="do-0183"/>
<connection identifier="cn-0196" relationshipRef="rel-309" xsi:type="Relationship" source="do-0185" target="do-0182"/>
<connection identifier="cn-0202" relationshipRef="rel-326" xsi:type="Relationship" source="do-0185" target="do-0184"/>
<connection identifier="cn-0208" relationshipRef="rel-335" xsi:type="Relationship" source="do-0185" target="do-0187"/>
<connection identifier="cn-0208" relationshipRef="rel-335" xsi:type="Relationship" source="do-0185" target="do-0188"/>
<connection identifier="cn-0194" relationshipRef="rel-304" xsi:type="Relationship" source="do-0186" target="do-0182"/>
<connection identifier="cn-0200" relationshipRef="rel-323" xsi:type="Relationship" source="do-0186" target="do-0184"/>
<connection identifier="cn-0206" relationshipRef="rel-332" xsi:type="Relationship" source="do-0186" target="do-0187"/>
<connection identifier="cn-0206" relationshipRef="rel-332" xsi:type="Relationship" source="do-0186" target="do-0188"/>
<connection identifier="cn-0211" relationshipRef="rel-347" xsi:type="Relationship" source="do-0186" target="do-0189"/>
<connection identifier="cn-0197" relationshipRef="rel-311" xsi:type="Relationship" source="do-0188" target="do-0182"/>
<connection identifier="cn-0203" relationshipRef="rel-327" xsi:type="Relationship" source="do-0188" target="do-0184"/>
<connection identifier="cn-0209" relationshipRef="rel-336" xsi:type="Relationship" source="do-0188" target="do-0187"/>
<connection identifier="cn-0212" relationshipRef="rel-349" xsi:type="Relationship" source="do-0188" target="do-0189"/>
<connection identifier="cn-0215" relationshipRef="rel-353" xsi:type="Relationship" source="do-0188" target="do-0185"/>
<connection identifier="cn-0197" relationshipRef="rel-311" xsi:type="Relationship" source="do-0187" target="do-0182"/>
<connection identifier="cn-0203" relationshipRef="rel-327" xsi:type="Relationship" source="do-0187" target="do-0184"/>
<connection identifier="cn-0209" relationshipRef="rel-336" xsi:type="Relationship" source="do-0187" target="do-0188"/>
<connection identifier="cn-0212" relationshipRef="rel-349" xsi:type="Relationship" source="do-0187" target="do-0189"/>
<connection identifier="cn-0215" relationshipRef="rel-353" xsi:type="Relationship" source="do-0187" target="do-0185"/>
<connection identifier="cn-0191" relationshipRef="rel-297" xsi:type="Relationship" source="do-0189" target="do-0183"/>
<connection identifier="cn-0201" relationshipRef="rel-325" xsi:type="Relationship" source="do-0189" target="do-0184"/>
</view>

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
"""Рендерит представления из .archimate в один самодостаточный HTML со встроенным SVG.
Нужен, чтобы схемы можно было посмотреть без установки Archi."""
import sys, io, pathlib, html
import sys, io, pathlib, html, re
import xml.etree.ElementTree as ET
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
@ -23,7 +23,8 @@ for folder in root.findall("folder"):
if et.endswith("Relationship"):
rels[e.get("id")] = (et[:-len("Relationship")], e.get("source"), e.get("target"))
elif et != "ArchimateDiagramModel":
concepts[e.get("id")] = (et, e.get("name") or "")
concepts[e.get("id")] = (et, e.get("name") or "",
(e.findtext("documentation") or "").strip())
def collect(node, ox, oy, nodes, conns, notes):
b = node.find("bounds")
@ -38,6 +39,23 @@ def collect(node, ox, oy, nodes, conns, notes):
for ch in node.findall("child"):
collect(ch, x, y, nodes, conns, notes)
def tooltip(doc, limit=260):
"""Первый абзац описания, обрезанный по границе предложения: подсказка должна
давать понять, что это за элемент, а не пересказывать весь текст."""
s = doc.split("\n\n")[0].replace("\n", " ").replace("`", "").strip()
if len(s) <= limit:
return s
cut = s[:limit]
end = max(cut.rfind(". "), cut.rfind("; "))
return (cut[:end + 1] if end > limit // 2 else cut.rstrip() + "")
def paragraphs(doc):
return [p.replace("\n", " ").strip() for p in doc.split("\n\n") if p.strip()]
def rich(p):
"""Описания пришли из markdown-файлов: обратные кавычки там означают код."""
return re.sub(r"`([^`]+)`", r"<code>\1</code>", html.escape(p))
def wrap(s, width, limit=4):
out, line = [], ""
for word in s.split():
@ -117,11 +135,14 @@ for v in views:
# блоки: контейнеры раньше содержимого — сортируем по площади убыв.
for nid, (eref, x, y, w, h) in sorted(nodes.items(), key=lambda kv: -kv[1][3] * kv[1][4]):
etype, name = concepts.get(eref, ("", eref or "?"))
etype, name, doc = concepts.get(eref, ("", eref or "?", ""))
fill = LAYER.get(etype, "#EEEEEE")
stroke = (STROKE["Grouping"] if etype == "Grouping" else
STROKE["Application"] if etype.startswith("Application") else STROKE["Technology"])
big = w * h > 60000
# описание элемента — всплывающей подсказкой: на схеме ему места нет,
# а в модели оно есть, иначе его видно только в Archi
svg.append("<g>" + (f'<title>{html.escape(tooltip(doc))}</title>' if doc else ""))
svg.append(f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="4" fill="{fill}" '
f'fill-opacity="{0.35 if big else 1}" stroke="{stroke}" stroke-width="1.2"/>')
lines = wrap(name, max(12, w // 7))
@ -135,6 +156,7 @@ for v in views:
svg.append(f'<text x="{x + w/2}" y="{y0 + i*fs*1.25:.0f}" font-size="{fs}" '
f'text-anchor="middle" dominant-baseline="middle" fill="#111">'
f'{html.escape(ln)}</text>')
svg.append("</g>")
# надписи — поверх всего, без заливки слоя: это не элементы модели
for content, x, y, w, h in notes:
@ -146,29 +168,88 @@ for v in views:
svg.append("</svg>")
doc = v.find("documentation")
parts.append(f'<section><h2>{html.escape(v.get("name") or "")}</h2>'
parts.append(f'<section id="{v.get("id")}"><h2>{html.escape(v.get("name") or "")}</h2>'
+ (f'<p class="doc">{html.escape(doc.text)}</p>' if doc is not None and doc.text else "")
+ "".join(svg) + "</section>")
toc = "".join(f'<a href="#{v.get("id")}">{html.escape(v.get("name") or "")}</a>' for v in views)
purpose = (root.findtext("purpose") or "").strip()
# ---------------------------------------------------------------- прикладной слой
# Описания компонентов лежат в модели, но на схемах их не видно: там помещается
# только имя. Поэтому — отдельным справочником, в том же порядке, что и на карте A0.
APP = "ApplicationComponent"
groups, seen = [], set()
for gid, (etype, gname, gdoc) in concepts.items():
if etype != "Grouping":
continue
members = [t for rtype, s, t in rels.values()
if rtype == "Aggregation" and s == gid and concepts.get(t, ("",))[0] == APP]
if members:
seen.update(members)
groups.append((gname, gdoc, sorted(members, key=lambda i: concepts[i][1])))
rest = sorted((i for i, c in concepts.items() if c[0] == APP and i not in seen),
key=lambda i: concepts[i][1])
if rest:
groups.append(("Вне групп", "", rest))
app_html = ""
if groups:
total = sum(len(m) for _, _, m in groups)
app_html = ('<section id="app-layer"><h2>Прикладной слой: описания компонентов</h2>'
f'<p class="doc">{total} компонентов в том же порядке, что и на карте A0. '
'Описания выведены не из манифестов контура, а из исходного кода компонентных '
'репозиториев, и потому проверяются чтением кода, а не сверкой с конфигурацией. '
'То же описание всплывает подсказкой, если задержать курсор на блоке любой схемы.</p>')
for gname, gdoc, members in groups:
app_html += f'<h3>{html.escape(gname)}</h3>'
for p in paragraphs(gdoc):
app_html += f'<p class="doc">{rich(p)}</p>'
app_html += '<dl class="apps">'
for i in members:
_, name, doc = concepts[i]
# запасное значение сборщика — путь к манифестам, а не описание
body = ("" if doc.startswith("apps/") else
"".join(f"<p>{rich(p)}</p>" for p in paragraphs(doc))) or \
f'<p class="none">описание не заполнено — см. {html.escape(doc or "манифесты")}</p>'
app_html += f'<dt>{html.escape(name)}</dt><dd>{body}</dd>'
app_html += "</dl>"
app_html += "</section>"
page = f"""<!doctype html>
<html lang="ru"><meta charset="utf-8">
<title>{html.escape(root.get('name') or 'Модель')}</title>
<style>
body {{ font-family: system-ui, sans-serif; margin: 0 auto; padding: 24px; max-width: 1760px;
background:#fafafa; color:#111; }}
h1 {{ font-size: 22px; }} h2 {{ font-size: 16px; margin: 32px 0 4px; }}
h1 {{ font-size: 22px; }} h2 {{ font-size: 16px; margin: 0 0 4px; }}
h3 {{ font-size: 14px; margin: 22px 0 6px; }}
.doc {{ color:#555; font-size:13px; margin:0 0 10px; max-width:900px; }}
section {{ background:#fff; border:1px solid #ddd; border-radius:6px; padding:14px; margin-bottom:18px;
overflow-x:auto; }}
overflow-x:auto; scroll-margin-top:12px; }}
svg {{ width:100%; height:auto; min-width:700px; }}
.legend {{ font-size:13px; color:#444; }}
.legend {{ font-size:13px; color:#444; max-width:900px; }}
.legend code {{ background:#eee; padding:1px 5px; border-radius:3px; }}
nav {{ display:flex; flex-wrap:wrap; gap:6px; margin:14px 0 22px; }}
nav a {{ font-size:12px; color:#245; text-decoration:none; background:#fff; border:1px solid #ddd;
border-radius:4px; padding:3px 8px; }}
nav a:hover {{ background:#eef4f8; }}
dl.apps {{ margin:0; }}
dl.apps dt {{ font-weight:600; font-size:13px; margin-top:14px; }}
dl.apps dd {{ margin:2px 0 0; padding-left:0; color:#333; font-size:13px; max-width:900px; }}
dl.apps dd p {{ margin:0 0 6px; }}
dl.apps .none {{ color:#999; font-style:italic; }}
code {{ background:#f0f0f0; padding:1px 4px; border-radius:3px; font-size:12px; }}
</style>
<h1>{html.escape(root.get('name') or '')}</h1>
<p class="legend">Автогенерация из <code>{html.escape(src.name)}</code>. Зелёный технологический слой,
голубой прикладной. Пунктир с полым треугольником realization, стрелка serving,
ромб composition, линия с шариком assignment.</p>
ромб composition, линия с шариком assignment, полый ромб aggregation. Подробнее в представлении
«L1. Как читать схемы». Описание элемента всплывает подсказкой при наведении на блок.</p>
{''.join(f'<p class="legend">{html.escape(p)}</p>' for p in paragraphs(purpose))}
<nav>{toc}{'<a href="#app-layer">Прикладной слой: описания</a>' if app_html else ''}</nav>
{''.join(parts)}
{app_html}
</html>"""
dst.write_text(page, encoding="utf-8", newline="\n")