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:
parent
2647f2554b
commit
4c5f812003
@ -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` | короткие описания компонентов прикладного слоя (1–3 предложения) | вход для сборки; попадает в поле `Documentation` модели |
|
||||
@ -30,6 +30,8 @@
|
||||
| A2–A12 | По представлению на сервис с тремя и более потребителями: кто от него зависит |
|
||||
| P1–P13 | Паспорт сервиса — зеркало к A2–A12: не «кто сломается без него», а «без чего не работает он сам» |
|
||||
|
||||
`preview.html` показывает не только схемы. Сверху — оглавление по всем представлениям, внизу — справочник прикладного слоя: описания всех компонентов, разложенные по тем же смысловым группам, что и на карте A0. Описания лежат в модели (поле `Documentation`), но на схемах их не видно — там помещается только имя, поэтому то же описание всплывает подсказкой при наведении на любой блок любой схемы. В Archi эти же тексты видны в панели свойств.
|
||||
|
||||
Направление связи — как принято в ArchiMate: стрелка идёт от вызываемого к вызывающему (serving). Читается как «кого лишишься — тот и сломается». Тот же вопрос с другой стороны — представления P: что нужно самому сервису, чтобы работать.
|
||||
|
||||
Разбиение по сервисам сделано осознанно: 152 связи «технологический сервис → приложение» и 86 межсервисных вызовов на одной схеме нечитаемы. В модели они лежат одним набором, представления — это срезы.
|
||||
|
||||
@ -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
@ -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
@ -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")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user