From 8357ccee8a7921836fb56c43b3e608a2a19b6ce1 Mon Sep 17 00:00:00 2001 From: emelinda Date: Thu, 20 Aug 2026 19:23:16 +0300 Subject: [PATCH] Expand architecture documentation: add support for labels (archimate:Note), update SVG rendering, extend node group definitions, and refine inter-service relationship mappings. --- docs/architecture/build_archimate.py | 168 +- .../architecture/contour-profile.example.json | 82 + .../architecture/example-technology.archimate | 2641 +++++++++++------ docs/architecture/example-technology.xml | 1845 ++++++++---- docs/architecture/preview.html | 288 +- docs/architecture/render_preview.py | 38 +- docs/architecture/to_open_exchange.py | 16 +- legend.png | Bin 0 -> 53020 bytes 8 files changed, 3535 insertions(+), 1543 deletions(-) create mode 100644 legend.png diff --git a/docs/architecture/build_archimate.py b/docs/architecture/build_archimate.py index 5ca654f..89a08ea 100644 --- a/docs/architecture/build_archimate.py +++ b/docs/architecture/build_archimate.py @@ -130,6 +130,13 @@ def obj(v, eid, x, y, w, h, parent=None): (parent["children"] if parent else v["objects"]).append(o) return o +def note(v, text, x, y, w, h, parent=None): + """Пояснительная надпись на схеме. Элементом модели не является: нужна легенде, + где подписать надо не архитектуру, а саму нотацию.""" + o = obj(v, None, x, y, w, h, parent) + o["note"] = text + return o + def conn(rtype, so, to): key = (rtype, so["el"], to["el"]) if key not in REL_INDEX: @@ -141,6 +148,12 @@ def conn(rtype, so, to): SERVICES = PROFILE["services"] SW = {s["id"]: s for s in PROFILE["software"]} +MIN_LINKS = 3 # порог, с которого сервису отводится отдельное представление +_tno = [2] # T1 и T2 названы явно, дальше нумерация сквозная по семейству + +def tnum(): + _tno[0] += 1 + return f"T{_tno[0]}" v1 = view("T1. Технологический слой", "Что развёрнуто в кластере и какие технологические сервисы это даёт. " @@ -242,7 +255,7 @@ if GROUPS: # --- представление «Внешние интеграции» IV = PROFILE.get("integration_view") if IV: - vi = view(f"T{len(views)+1}. {IV['name']}", IV.get("doc", "")) + vi = view(f"{tnum()}. {IV['name']}", IV.get("doc", "")) CW, CH = 320, 110 shown = {} for ci, col in enumerate(IV["columns"]): @@ -254,6 +267,44 @@ if IV: if src in shown and tgt in shown: conn(rtype, shown[src], shown[tgt]) +# --- прикладной слой: смысловые домены +# Строится последним из прикладного, чтобы захватить и компоненты, заданные +# в node_groups вручную: в матрице контура их нет. +DOMAINS = PROFILE.get("domains", []) +if DOMAINS: + app_ids = {e[1] for e in elements if e[0] == "application"} + claimed = {app_id.get(a, a) for g in DOMAINS + if isinstance(g.get("apps"), list) for a in g["apps"]} + for g in DOMAINS: + want = ([e for e in app_ids if e not in claimed] if g.get("apps") == "*" + else [app_id.get(a, a) for a in g.get("apps") or []]) + missing = [e for e in want if e not in app_ids] + if missing: + sys.exit(f"домен {g['id']}: нет таких компонентов — {', '.join(missing)}") + g["_apps"] = sorted(want) + # пустая группа — не элемент модели, а незаполненная строка профиля + DOMAINS = [g for g in DOMAINS if g["_apps"]] + for g in DOMAINS: + el("other", g["id"], "Grouping", g["name"], g.get("doc", "")) + for eid in g["_apps"]: + R("Aggregation", g["id"], eid) + + va = view("A0. Карта прикладного слоя", + f"Из чего состоит прикладной слой: {len(app_ids)} " + f"{plural(len(app_ids), 'компонент', 'компонента', 'компонентов')} по смысловым " + f"группам. Связи здесь намеренно не показаны — на таком числе компонентов они " + f"делают схему нечитаемой; для них есть представления A1 и далее. Вложенность " + f"означает aggregation: группа объединяет компоненты, но не владеет ими.") + PER = 7 + y = 20 + for g in DOMAINS: + rows = (len(g["_apps"]) + PER - 1) // PER + h = 34 + rows * 62 + 8 + og = obj(va, g["id"], 20, y, PER * 180 + 28, h) + for i, eid in enumerate(g["_apps"]): + obj(va, eid, 14 + (i % PER) * 180, 34 + (i // PER) * 62, 165, 50, og) + y += h + 20 + # --- прикладной слой: межсервисные вызовы if CALLS: import math, collections @@ -286,19 +337,48 @@ if CALLS: conn("Serving", core_obj[b], core_obj[a]) # A2…: по представлению на каждый сервис с тремя и более потребителями - a_no = 2 + a_no, hub_view = 2, {} for hub, n in inbound.most_common(): - if n < 3: + if n < MIN_LINKS: break callers = sorted(a for a, b, _ in calls if b == hub) v = view(f"A{a_no}. Зависят от: {hub}", f"К {hub} обращаются {n} {plural(n, 'сервис', 'сервиса', 'сервисов')}.") + hub_view[hub] = f"A{a_no}" a_no += 1 oh = obj(v, app_id[hub], 20, 20, 260, 60) for i, a in enumerate(callers): conn("Serving", oh, obj(v, app_id[a], 20 + (i % 6) * 190, 150 + (i // 6) * 80, 170, 55)) + # P1…: зеркало к A-представлениям. Те отвечают «кто сломается без сервиса», + # эти — «без чего не работает он сам»: технологические сервисы сверху, + # вызываемые им приложения снизу. + p_no = 1 + for hub, n in outbound.most_common(): + if n < MIN_LINKS: + break + callees = sorted({b for a, b, _ in calls if a == hub}) + uses = [ts for ts in SERVICES if hub in consumers[ts["id"]]] + back = (f"Кто зависит от самого {hub} — представление {hub_view[hub]}." + if hub in hub_view else + f"Входящих вызовов у {hub} меньше {MIN_LINKS}, отдельного представления для них нет.") + m = len(callees) + v = view(f"P{p_no}. Паспорт: {hub}", + f"Что нужно, чтобы {hub} работал: {len(uses)} " + f"{plural(len(uses), 'технологический сервис', 'технологических сервиса', 'технологических сервисов')} " + f"и {m} {plural(m, 'прикладной сервис', 'прикладных сервиса', 'прикладных сервисов')}. " + f"Стрелки, как и везде, идут от обслуживающего к обслуживаемому. {back}") + p_no += 1 + wide = min(6, max(len(uses), m)) + oh = obj(v, app_id[hub], 20 + max(0, (wide * 190 - 260) // 2), + 20 + 80 * ((len(uses) + 5) // 6), 260, 60) + for i, ts in enumerate(uses): + conn("Serving", obj(v, ts["id"], 20 + (i % 6) * 190, 20 + (i // 6) * 80, 170, 55), oh) + base = oh["b"][1] + 130 + for i, b in enumerate(callees): + conn("Serving", obj(v, app_id[b], 20 + (i % 6) * 190, base + (i // 6) * 80, 170, 55), oh) + PER_ROW = 6 for ts in SERVICES: lst = consumers[ts["id"]] @@ -306,7 +386,7 @@ for ts in SERVICES: if not lst and not extra: continue n = len(lst) - v = view(f"T{len(views)+1}. Потребители: {ts['name']}", + v = view(f"{tnum()}. Потребители: {ts['name']}", f"Сервисом {plural(n, 'пользуется', 'пользуются', 'пользуются')} " f"{n} из {len(DATA['apps'])} приложений контура.") ots = obj(v, ts["id"], 20, 20, 260, 60) @@ -316,8 +396,79 @@ for ts in SERVICES: conn("Serving", ots, obj(v, eid, 20 + (i % PER_ROW) * 190, 150 + (i // PER_ROW) * 80, 170, 55)) +# --- легенда нотации +# Собрана из настоящих элементов модели, а не из выдуманных ради неё: так легенда +# заодно оказывается разобранным примером и не может разойтись с остальными схемами. +LEG_TS = max((t for t in SERVICES if consumers[t["id"]] and t["realizers"][0] in SW), + key=lambda t: len(consumers[t["id"]]), default=None) +if LEG_TS and GROUPS and DOMAINS: + l_ss = LEG_TS["realizers"][0] + l_app = app_id[consumers[LEG_TS["id"]][0]] + l_gsw = next((g for g in GROUPS if l_ss in (g.get("runs_software") or [])), None) + l_gap = next((g for g in GROUPS if any(app_id[a] == l_app for a in g["_apps"])), None) + l_dm = next((g for g in DOMAINS if l_app in g["_apps"]), None) + l_art = next((e[1] for e in elements if e[2] == "Artifact"), None) + + vl = view("L1. Как читать схемы", + "Легенда нотации. Показан настоящий фрагмент модели, подписанный по типам: " + "каждый элемент и каждая линия здесь встречаются на остальных представлениях " + "в том же начертании.") + W, COL = 260, 320 + note(vl, "Стрелка serving читается не как «кто кого вызывает», а как «кто без кого не " + "работает»: она идёт от обслуживающего к обслуживаемому. Направление выбрано так " + "потому, что от схемы обычно нужен ответ на вопрос «что откажет, если убрать этот " + "элемент», — и он читается по стрелкам вперёд.", 20, 20, COL * 4 - 60, 66) + + row_a = [ + (nd_cluster, "Узел (Node) — вычислительная среда: кластер, группа серверов или " + "отдельный хост. Ромб на линии — composition: узел состоит из этого, " + "и отдельно от узла содержимое не существует."), + (l_ss, "Системное ПО (System Software) — платформенный компонент внутри узла. " + "Пунктир с полым треугольником — realization: компонент реализует " + "технологический сервис."), + (LEG_TS["id"], "Технологический сервис (Technology Service) — что платформа даёт " + "приложениям, без указания, чем именно это сделано. Приложение зависит " + "от сервиса, а не от конкретного продукта под ним."), + (l_app, "Прикладной компонент (Application Component) — сервис прикладного слоя. " + "Цветом различаются слои: голубой — прикладной, зелёный — технологический."), + ] + o_leg = {} + for i, (eid, txt) in enumerate(row_a): + o_leg[eid] = obj(vl, eid, 20 + i * COL, 120, W, 60) + note(vl, txt, 20 + i * COL, 190, W, 96) + conn("Composition", o_leg[nd_cluster], o_leg[l_ss]) + conn("Realization", o_leg[l_ss], o_leg[LEG_TS["id"]]) + conn("Serving", o_leg[LEG_TS["id"]], o_leg[l_app]) + + row_b = [ + (1, l_gsw and l_gsw["id"], l_ss, "Aggregation", + "Линия с полым ромбом — aggregation: группа объединяет элемент, но тот может " + "существовать и без неё. Так показано, на каких узлах работает системное ПО."), + (2, l_gap and l_gap["id"], l_app, "Assignment", + "Линия с шариком и закрашенной стрелкой — assignment: узел выделен под компонент, " + "и компонент на нём исполняется."), + (3, l_dm and l_dm["id"], l_app, "Aggregation", + "Смысловая группа (Grouping) — домен прикладного слоя. Тоже aggregation: " + "компонент принадлежит домену по смыслу, а не по месту размещения."), + ] + for i, src, tgt, rtype, txt in row_b: + if not src: + continue + os_ = obj(vl, src, 20 + i * COL, 320, W, 60) + note(vl, txt, 20 + i * COL, 390, W, 96) + conn(rtype, os_, o_leg[tgt]) + if l_art: + obj(vl, l_art, 20, 320, W, 60) + note(vl, "Артефакт (Artifact) — то, что физически лежит в хранилище: файл, " + "образ контейнера, дистрибутив.", 20, 390, W, 96) + +# --- порядок представлений в файле: легенда, технологический слой, прикладной, паспорта +ORDER = {"L": 0, "T": 1, "A": 2, "P": 3} +views.sort(key=lambda v: (ORDER.get(v["name"][0], 9), + int(re.match(r"[A-Z](\d+)", v["name"]).group(1)))) + # ---------------------------------------------------------------- сериализация -FOLDERS = [("Strategy", "strategy"), ("Business", "business"), ("Application", "application"), +FOLDERS =[("Strategy", "strategy"), ("Business", "business"), ("Application", "application"), ("Technology & Physical", "technology"), ("Motivation", "motivation"), ("Implementation & Migration", "implementation_migration"), ("Other", "other"), ("Relations", "relations"), ("Views", "diagrams")] @@ -329,6 +480,11 @@ def emit_obj(o, ind): p = " " * ind tgt = f' targetConnections={quoteattr(" ".join(o["tgt"]))}' if o["tgt"] else "" x, y, w, h = o["b"] + if "note" in o: + return (f'{p}\n' + f'{p} \n' + f'{p} {escape(o["note"])}\n' + f'{p}\n') s = f'{p}\n' s += f'{p} \n' for c in o["src"]: @@ -346,7 +502,7 @@ out = ['', for fname, ftype in FOLDERS: inner = "" - if ftype in ("technology", "application"): + if ftype in ("technology", "application", "other"): for folder, eid, etype, name, doc in elements: if folder != ftype: continue diff --git a/docs/architecture/contour-profile.example.json b/docs/architecture/contour-profile.example.json index f758523..8c10691 100644 --- a/docs/architecture/contour-profile.example.json +++ b/docs/architecture/contour-profile.example.json @@ -265,6 +265,88 @@ ] ] }, + "_домены": "Смысловая группировка прикладного слоя. Описывает продукт, а не инсталляцию, и от контура к контуру обычно не меняется. apps — имена приложений либо идентификаторы компонентов, заданных в node_groups; \"*\" в последней группе собирает всё, что не попало в предыдущие, — так новое приложение не исчезает с карты молча. Пустые группы в представление не выводятся.", + "domains": [ + { + "id": "dm-core", + "name": "Платформа", + "doc": "Общие механизмы, на которые опираются все прикладные домены: единая точка входа в интерфейс, вход и права, справочник атрибутов, рабочие пространства и проекты как контейнеры данных, журнал действий, уведомления, оркестрация процессов.\n\nПочти все связи прикладного слоя ведут сюда, поэтому изменения в этой группе стоят дороже всего: отказ любого её компонента виден сразу во всех доменах, тогда как отказ прикладного сервиса ограничен своей областью.", + "apps": [ + "control-interface", + "auth-flow", + "iam", + "ams-sync", + "django", + "eav", + "workspaces", + "projects", + "flows", + "faas", + "message-hub", + "subscriptions", + "system-log" + ] + }, + { + "id": "dm-pm", + "name": "Управление проектом", + "doc": "Планирование и учёт: календарно-сетевые планы, договоры и обязательства по ним, справочник ресурсов, рабочие заметки.\n\nГруппа потребляет данные остальных доменов чаще, чем отдаёт: план и договор ссылаются на документы, замечания и полевые измерения, обратные ссылки редки.", + "apps": [ + "pm", + "contracts", + "resources", + "notes" + ] + }, + { + "id": "dm-cde", + "name": "Документы и общая среда данных", + "doc": "Ядро CDE: хранение документов и их версий, связи между документами, вложения, комплекты передачи, чертежи и трёхмерные модели, подготовка САПР-моделей к просмотру, проверка штампов.\n\nСамая нагруженная группа по числу входящих ссылок — на документ ссылается почти каждый прикладной сценарий. Хранилище документов здесь же является и самым связанным компонентом всей платформы.", + "apps": [ + "cde", + "documentations", + "document-link", + "attachments", + "transmittal", + "drawings", + "bim", + "ac-bim-converter", + "stamp-verification" + ] + }, + { + "id": "dm-qc", + "name": "Контроль качества", + "doc": "Работа с несоответствиями: инспекции и чек-листы, замечания, проблемы, запросы информации, ревью, предписания, сравнение редакций чертежей.\n\nГруппа однородна по устройству — почти каждый её сервис читает документ, пишет запись о несоответствии и поднимает процесс в оркестраторе, — поэтому её компоненты сильнее прочих зависят от домена документов и от платформенных процессов.", + "apps": [ + "inspections", + "checklists", + "remarks", + "issues", + "rfi", + "reviews", + "prescriptions", + "comparisons" + ] + }, + { + "id": "dm-field", + "name": "Полевые данные", + "doc": "Данные, приходящие со стройплощадки: замеры, привязка к местности, сечения, а также обработка тяжёлых входных файлов.\n\nЕдинственная группа с существенной вычислительной нагрузкой: обработка вынесена на отдельные узлы, потому что её всплески не должны мешать интерактивной работе остальных сервисов.", + "apps": [ + "measurements", + "mapper", + "cross-section", + "processing" + ] + }, + { + "id": "dm-other", + "name": "Не отнесено", + "doc": "Приложения, не попавшие ни в одну группу выше. В норме группа пуста; появление компонента здесь означает, что группировку пора дополнить.", + "apps": "*" + } + ], "services": [ { "id": "ts-ingress", diff --git a/docs/architecture/example-technology.archimate b/docs/architecture/example-technology.archimate index 876edfe..8769fcc 100644 --- a/docs/architecture/example-technology.archimate +++ b/docs/architecture/example-technology.archimate @@ -373,7 +373,33 @@ - + + + Общие механизмы, на которые опираются все прикладные домены: единая точка входа в интерфейс, вход и права, справочник атрибутов, рабочие пространства и проекты как контейнеры данных, журнал действий, уведомления, оркестрация процессов. + +Почти все связи прикладного слоя ведут сюда, поэтому изменения в этой группе стоят дороже всего: отказ любого её компонента виден сразу во всех доменах, тогда как отказ прикладного сервиса ограничен своей областью. + + + Планирование и учёт: календарно-сетевые планы, договоры и обязательства по ним, справочник ресурсов, рабочие заметки. + +Группа потребляет данные остальных доменов чаще, чем отдаёт: план и договор ссылаются на документы, замечания и полевые измерения, обратные ссылки редки. + + + Ядро CDE: хранение документов и их версий, связи между документами, вложения, комплекты передачи, чертежи и трёхмерные модели, подготовка САПР-моделей к просмотру, проверка штампов. + +Самая нагруженная группа по числу входящих ссылок — на документ ссылается почти каждый прикладной сценарий. Хранилище документов здесь же является и самым связанным компонентом всей платформы. + + + Работа с несоответствиями: инспекции и чек-листы, замечания, проблемы, запросы информации, ревью, предписания, сравнение редакций чертежей. + +Группа однородна по устройству — почти каждый её сервис читает документ, пишет запись о несоответствии и поднимает процесс в оркестраторе, — поэтому её компоненты сильнее прочих зависят от домена документов и от платформенных процессов. + + + Данные, приходящие со стройплощадки: замеры, привязка к местности, сечения, а также обработка тяжёлых входных файлов. + +Единственная группа с существенной вычислительной нагрузкой: обработка вынесена на отдельные узлы, потому что её всплески не должны мешать интерактивной работе остальных сервисов. + + @@ -620,94 +646,201 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Легенда нотации. Показан настоящий фрагмент модели, подписанный по типам: каждый элемент и каждая линия здесь встречаются на остальных представлениях в том же начертании. + + + Стрелка serving читается не как «кто кого вызывает», а как «кто без кого не работает»: она идёт от обслуживающего к обслуживаемому. Направление выбрано так потому, что от схемы обычно нужен ответ на вопрос «что откажет, если убрать этот элемент», — и он читается по стрелкам вперёд. + + + + + + + + Узел (Node) — вычислительная среда: кластер, группа серверов или отдельный хост. Ромб на линии — composition: узел состоит из этого, и отдельно от узла содержимое не существует. + + + + + + + + Системное ПО (System Software) — платформенный компонент внутри узла. Пунктир с полым треугольником — realization: компонент реализует технологический сервис. + + + + + + + + Технологический сервис (Technology Service) — что платформа даёт приложениям, без указания, чем именно это сделано. Приложение зависит от сервиса, а не от конкретного продукта под ним. + + + + + + + Прикладной компонент (Application Component) — сервис прикладного слоя. Цветом различаются слои: голубой — прикладной, зелёный — технологический. + + + + + + + + Линия с полым ромбом — aggregation: группа объединяет элемент, но тот может существовать и без неё. Так показано, на каких узлах работает системное ПО. + + + + + + + + Линия с шариком и закрашенной стрелкой — assignment: узел выделен под компонент, и компонент на нём исполняется. + + + + + + + + Смысловая группа (Grouping) — домен прикладного слоя. Тоже aggregation: компонент принадлежит домену по смыслу, а не по месту размещения. + + + + + + + Артефакт (Artifact) — то, что физически лежит в хранилище: файл, образ контейнера, дистрибутив. + + Что развёрнуто в кластере и какие технологические сервисы это даёт. Приложения-потребители вынесены в отдельные представления T2 и далее. @@ -1071,1133 +1204,1943 @@ - - Восемь сервисов с наибольшим числом связей и вызовы между ними. Стрелка идёт от вызываемого к вызывающему (serving): кого лишишься — тот и сломается. Всего в контуре 86 межсервисных связей. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - К django обращаются 18 сервисов. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - К documentations обращаются 11 сервисов. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - К eav обращаются 9 сервисов. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - К resources обращаются 7 сервисов. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - К processing обращаются 6 сервисов. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - К iam обращаются 6 сервисов. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - К flows обращаются 4 сервиса. - - - - - - - - - - - - - - - - - - - - - - К workspaces обращаются 4 сервиса. - - - - - - - - - - - - - - - - - - - - - - К system-log обращаются 3 сервиса. - - - - - - - - - - - - - - - - - - К bim обращаются 3 сервиса. - - - - - - - - - - - - - - - - - - К pm обращаются 3 сервиса. - - - - - - - - - - - - - - - - - + Сервисом пользуется 21 из 37 приложений контура. - + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + Сервисом пользуются 25 из 37 приложений контура. - + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + Сервисом пользуются 26 из 37 приложений контура. - + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + Сервисом пользуются 24 из 37 приложений контура. - + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + Сервисом пользуются 5 из 37 приложений контура. - + - - - - - + + + + + - + - + - + - + - + - + - + - + Сервисом пользуются 14 из 37 приложений контура. - + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + Сервисом пользуются 12 из 37 приложений контура. - + - - - - - - - - - - - - - + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + Сервисом пользуется 1 из 37 приложений контура. - + - + - + - + - + - + Сервисом пользуются 16 из 37 приложений контура. - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + Сервисом пользуются 6 из 37 приложений контура. - + - - - - - - + + + + + + - + - + - + - + - + - + - + - + + + Из чего состоит прикладной слой: 38 компонентов по смысловым группам. Связи здесь намеренно не показаны — на таком числе компонентов они делают схему нечитаемой; для них есть представления A1 и далее. Вложенность означает aggregation: группа объединяет компоненты, но не владеет ими. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Восемь сервисов с наибольшим числом связей и вызовы между ними. Стрелка идёт от вызываемого к вызывающему (serving): кого лишишься — тот и сломается. Всего в контуре 86 межсервисных связей. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + К django обращаются 18 сервисов. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + К documentations обращаются 11 сервисов. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + К eav обращаются 9 сервисов. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + К resources обращаются 7 сервисов. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + К processing обращаются 6 сервисов. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + К iam обращаются 6 сервисов. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + К flows обращаются 4 сервиса. + + + + + + + + + + + + + + + + + + + + + + К workspaces обращаются 4 сервиса. + + + + + + + + + + + + + + + + + + + + + + К system-log обращаются 3 сервиса. + + + + + + + + + + + + + + + + + + К bim обращаются 3 сервиса. + + + + + + + + + + + + + + + + + + К pm обращаются 3 сервиса. + + + + + + + + + + + + + + + + + + Что нужно, чтобы documentations работал: 7 технологических сервисов и 15 прикладных сервисов. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Кто зависит от самого documentations — представление A3. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Что нужно, чтобы flows работал: 7 технологических сервисов и 8 прикладных сервисов. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Кто зависит от самого flows — представление A8. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Что нужно, чтобы issues работал: 9 технологических сервисов и 7 прикладных сервисов. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у issues меньше 3, отдельного представления для них нет. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Что нужно, чтобы comparisons работал: 3 технологических сервиса и 6 прикладных сервисов. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у comparisons меньше 3, отдельного представления для них нет. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Что нужно, чтобы django работал: 8 технологических сервисов и 6 прикладных сервисов. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Кто зависит от самого django — представление A2. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Что нужно, чтобы faas работал: 0 технологических сервисов и 5 прикладных сервисов. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у faas меньше 3, отдельного представления для них нет. + + + + + + + + + + + + + + + + + + + + + + + + + + Что нужно, чтобы transmittal работал: 6 технологических сервисов и 5 прикладных сервисов. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у transmittal меньше 3, отдельного представления для них нет. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Что нужно, чтобы message-hub работал: 5 технологических сервисов и 4 прикладных сервиса. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у message-hub меньше 3, отдельного представления для них нет. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Что нужно, чтобы pm работал: 8 технологических сервисов и 4 прикладных сервиса. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Кто зависит от самого pm — представление A12. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Что нужно, чтобы processing работал: 5 технологических сервисов и 4 прикладных сервиса. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Кто зависит от самого processing — представление A6. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Что нужно, чтобы rfi работал: 7 технологических сервисов и 4 прикладных сервиса. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у rfi меньше 3, отдельного представления для них нет. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Что нужно, чтобы cde работал: 5 технологических сервисов и 3 прикладных сервиса. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у cde меньше 3, отдельного представления для них нет. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Что нужно, чтобы notes работал: 6 технологических сервисов и 3 прикладных сервиса. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у notes меньше 3, отдельного представления для них нет. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/architecture/example-technology.xml b/docs/architecture/example-technology.xml index d5921a1..eeff0ce 100644 --- a/docs/architecture/example-technology.xml +++ b/docs/architecture/example-technology.xml @@ -457,6 +457,36 @@ CAD ферма Узлы обработки исходных файлов BIM-моделей в проприетарных форматах NWD и RVT. Работают асинхронно: воркер разбирает очередь заданий, забирает исходный файл из объектного хранилища, конвертирует его в пригодный для просмотра и анализа вид и складывает результат обратно. Вынесены в отдельную группу, потому что требуют специфического окружения и дают тяжёлую неравномерную нагрузку — обработка одной модели занимает несоизмеримо больше времени, чем любой интерактивный запрос. В манифестах кластера этих компонентов нет: группа обслуживается вне GitOps-контура платформы. + + Платформа + Общие механизмы, на которые опираются все прикладные домены: единая точка входа в интерфейс, вход и права, справочник атрибутов, рабочие пространства и проекты как контейнеры данных, журнал действий, уведомления, оркестрация процессов. + +Почти все связи прикладного слоя ведут сюда, поэтому изменения в этой группе стоят дороже всего: отказ любого её компонента виден сразу во всех доменах, тогда как отказ прикладного сервиса ограничен своей областью. + + + Управление проектом + Планирование и учёт: календарно-сетевые планы, договоры и обязательства по ним, справочник ресурсов, рабочие заметки. + +Группа потребляет данные остальных доменов чаще, чем отдаёт: план и договор ссылаются на документы, замечания и полевые измерения, обратные ссылки редки. + + + Документы и общая среда данных + Ядро CDE: хранение документов и их версий, связи между документами, вложения, комплекты передачи, чертежи и трёхмерные модели, подготовка САПР-моделей к просмотру, проверка штампов. + +Самая нагруженная группа по числу входящих ссылок — на документ ссылается почти каждый прикладной сценарий. Хранилище документов здесь же является и самым связанным компонентом всей платформы. + + + Контроль качества + Работа с несоответствиями: инспекции и чек-листы, замечания, проблемы, запросы информации, ревью, предписания, сравнение редакций чертежей. + +Группа однородна по устройству — почти каждый её сервис читает документ, пишет запись о несоответствии и поднимает процесс в оркестраторе, — поэтому её компоненты сильнее прочих зависят от домена документов и от платформенных процессов. + + + Полевые данные + Данные, приходящие со стройплощадки: замеры, привязка к местности, сечения, а также обработка тяжёлых входных файлов. + +Единственная группа с существенной вычислительной нагрузкой: обработка вынесена на отдельные узлы, потому что её всплески не должны мешать интерактивной работе остальных сервисов. + @@ -704,95 +734,178 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + L1. Как читать схемы + Легенда нотации. Показан настоящий фрагмент модели, подписанный по типам: каждый элемент и каждая линия здесь встречаются на остальных представлениях в том же начертании. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + T1. Технологический слой Что развёрнуто в кластере и какие технологические сервисы это даёт. Приложения-потребители вынесены в отдельные представления T2 и далее. @@ -945,623 +1058,1035 @@ + + T4. Потребители: Маршрутизация HTTPS-трафика + Сервисом пользуется 21 из 37 приложений контура. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + T5. Потребители: Аутентификация OIDC + Сервисом пользуются 25 из 37 приложений контура. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + T6. Потребители: Управление секретами + Сервисом пользуются 26 из 37 приложений контура. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + T7. Потребители: Реляционное хранилище + Сервисом пользуются 24 из 37 приложений контура. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + T8. Потребители: Кэш + Сервисом пользуются 5 из 37 приложений контура. + + + + + + + + + + + + + + + + T9. Потребители: Событийная шина + Сервисом пользуются 14 из 37 приложений контура. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + T10. Потребители: Очередь сообщений AMQP + Сервисом пользуются 12 из 37 приложений контура. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + T11. Потребители: Оркестрация процессов BPMN + Сервисом пользуется 1 из 37 приложений контура. + + + + + + + + T12. Потребители: Объектное хранилище + Сервисом пользуются 16 из 37 приложений контура. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + T13. Потребители: Отправка почтовых уведомлений + Сервисом пользуются 6 из 37 приложений контура. + + + + + + + + + + + + + + + + - A1. Ядро прикладного слоя - Восемь сервисов с наибольшим числом связей и вызовы между ними. Стрелка идёт от вызываемого к вызывающему (serving): кого лишишься — тот и сломается. Всего в контуре 86 межсервисных связей. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + A0. Карта прикладного слоя + Из чего состоит прикладной слой: 38 компонентов по смысловым группам. Связи здесь намеренно не показаны — на таком числе компонентов они делают схему нечитаемой; для них есть представления A1 и далее. Вложенность означает aggregation: группа объединяет компоненты, но не владеет ими. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - A2. Зависят от: django - К django обращаются 18 сервисов. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + A1. Ядро прикладного слоя + Восемь сервисов с наибольшим числом связей и вызовы между ними. Стрелка идёт от вызываемого к вызывающему (serving): кого лишишься — тот и сломается. Всего в контуре 86 межсервисных связей. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - A3. Зависят от: documentations - К documentations обращаются 11 сервисов. - - - - - - - - - - - - - - - - - - - - - - - + A2. Зависят от: django + К django обращаются 18 сервисов. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - A4. Зависят от: eav - К eav обращаются 9 сервисов. - - - - - - - - - - - - - - - - - - - + A3. Зависят от: documentations + К documentations обращаются 11 сервисов. + + + + + + + + + + + + + + + + + + + + + + + - A5. Зависят от: resources - К resources обращаются 7 сервисов. - - - - - - - - - - - - - - - + A4. Зависят от: eav + К eav обращаются 9 сервисов. + + + + + + + + + + + + + + + + + + + - A6. Зависят от: processing - К processing обращаются 6 сервисов. - - - - - - - - - - - - - + A5. Зависят от: resources + К resources обращаются 7 сервисов. + + + + + + + + + + + + + + + - A7. Зависят от: iam - К iam обращаются 6 сервисов. - - - - - - - - - - - - - + A6. Зависят от: processing + К processing обращаются 6 сервисов. + + + + + + + + + + + + + - A8. Зависят от: flows - К flows обращаются 4 сервиса. - - - - - - - - - + A7. Зависят от: iam + К iam обращаются 6 сервисов. + + + + + + + + + + + + + - A9. Зависят от: workspaces - К workspaces обращаются 4 сервиса. - - - - - - - - - + A8. Зависят от: flows + К flows обращаются 4 сервиса. + + + + + + + + + - A10. Зависят от: system-log - К system-log обращаются 3 сервиса. - - - - - - - + A9. Зависят от: workspaces + К workspaces обращаются 4 сервиса. + + + + + + + + + - A11. Зависят от: bim - К bim обращаются 3 сервиса. - - - - - - - + A10. Зависят от: system-log + К system-log обращаются 3 сервиса. + + + + + + + - A12. Зависят от: pm - К pm обращаются 3 сервиса. - - - - - - - + A11. Зависят от: bim + К bim обращаются 3 сервиса. + + + + + + + - T16. Потребители: Маршрутизация HTTPS-трафика - Сервисом пользуется 21 из 37 приложений контура. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + A12. Зависят от: pm + К pm обращаются 3 сервиса. + + + + + + + - T17. Потребители: Аутентификация OIDC - Сервисом пользуются 25 из 37 приложений контура. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + P1. Паспорт: documentations + Что нужно, чтобы documentations работал: 7 технологических сервисов и 15 прикладных сервисов. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Кто зависит от самого documentations — представление A3. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - T18. Потребители: Управление секретами - Сервисом пользуются 26 из 37 приложений контура. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + P2. Паспорт: flows + Что нужно, чтобы flows работал: 7 технологических сервисов и 8 прикладных сервисов. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Кто зависит от самого flows — представление A8. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - T19. Потребители: Реляционное хранилище - Сервисом пользуются 24 из 37 приложений контура. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + P3. Паспорт: issues + Что нужно, чтобы issues работал: 9 технологических сервисов и 7 прикладных сервисов. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у issues меньше 3, отдельного представления для них нет. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - T20. Потребители: Кэш - Сервисом пользуются 5 из 37 приложений контура. - - - - - - - - - - - - - + P4. Паспорт: comparisons + Что нужно, чтобы comparisons работал: 3 технологических сервиса и 6 прикладных сервисов. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у comparisons меньше 3, отдельного представления для них нет. + + + + + + + + + + + + + + + + + + + - T21. Потребители: Событийная шина - Сервисом пользуются 14 из 37 приложений контура. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + P5. Паспорт: django + Что нужно, чтобы django работал: 8 технологических сервисов и 6 прикладных сервисов. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Кто зависит от самого django — представление A2. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - T22. Потребители: Очередь сообщений AMQP - Сервисом пользуются 12 из 37 приложений контура. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + P6. Паспорт: faas + Что нужно, чтобы faas работал: 0 технологических сервисов и 5 прикладных сервисов. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у faas меньше 3, отдельного представления для них нет. + + + + + + + + + + + - T23. Потребители: Оркестрация процессов BPMN - Сервисом пользуется 1 из 37 приложений контура. - - - - - + P7. Паспорт: transmittal + Что нужно, чтобы transmittal работал: 6 технологических сервисов и 5 прикладных сервисов. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у transmittal меньше 3, отдельного представления для них нет. + + + + + + + + + + + + + + + + + + + + + + + - T24. Потребители: Объектное хранилище - Сервисом пользуются 16 из 37 приложений контура. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + P8. Паспорт: message-hub + Что нужно, чтобы message-hub работал: 5 технологических сервисов и 4 прикладных сервиса. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у message-hub меньше 3, отдельного представления для них нет. + + + + + + + + + + + + + + + + + + + - T25. Потребители: Отправка почтовых уведомлений - Сервисом пользуются 6 из 37 приложений контура. - - - - - - - - - - - - - - - + P9. Паспорт: pm + Что нужно, чтобы pm работал: 8 технологических сервисов и 4 прикладных сервиса. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Кто зависит от самого pm — представление A12. + + + + + + + + + + + + + + + + + + + + + + + + + + + + P10. Паспорт: processing + Что нужно, чтобы processing работал: 5 технологических сервисов и 4 прикладных сервиса. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Кто зависит от самого processing — представление A6. + + + + + + + + + + + + + + + + + + + + + + P11. Паспорт: rfi + Что нужно, чтобы rfi работал: 7 технологических сервисов и 4 прикладных сервиса. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у rfi меньше 3, отдельного представления для них нет. + + + + + + + + + + + + + + + + + + + + + + + + + + P12. Паспорт: cde + Что нужно, чтобы cde работал: 5 технологических сервисов и 3 прикладных сервиса. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у cde меньше 3, отдельного представления для них нет. + + + + + + + + + + + + + + + + + + + + P13. Паспорт: notes + Что нужно, чтобы notes работал: 6 технологических сервисов и 3 прикладных сервиса. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у notes меньше 3, отдельного представления для них нет. + + + + + + + + + + + + + + + + + + + diff --git a/docs/architecture/preview.html b/docs/architecture/preview.html index 12cebfd..8905c87 100644 --- a/docs/architecture/preview.html +++ b/docs/architecture/preview.html @@ -16,7 +16,7 @@

Автогенерация из example-technology.archimate. Зелёный — технологический слой, голубой — прикладной. Пунктир с полым треугольником — realization, стрелка — serving, ромб — composition, линия с шариком — assignment.

-

T1. Технологический слой

Что развёрнуто в кластере и какие технологические сервисы это даёт. Приложения-потребители вынесены в отдельные представления T2 и далее.

+

L1. Как читать схемы

Легенда нотации. Показан настоящий фрагмент модели, подписанный по типам: каждый элемент и каждая линия здесь встречаются на остальных представлениях в том же начертании.

@@ -28,6 +28,22 @@ + + +Кластер k8s-prodVault + Agent InjectorУправление секретамиattachmentsГруппа узлов K8s (Поддерживающаяинфраструктура)Группа узлов generic (Сервераприложений)Документы и общая среда данныхHelm-чарты (OCI)Стрелка serving читается не как «кто кого вызывает», а как «кто без кого не работает»: она идёт от обслуживающего к обслуживаемому. Направление выбрано так потому, что от схемы обычно нужен ответна вопрос «что откажет, если убрать этот элемент», — и он читается по стрелкам вперёд.Узел (Node) — вычислительная среда:кластер, группа серверов или отдельныйхост. Ромб на линии — composition: узелсостоит из этого, и отдельно от узласодержимое не существует.Системное ПО (System Software) —платформенный компонент внутри узла.Пунктир с полым треугольником —realization: компонент реализуеттехнологический сервис.Технологический сервис (TechnologyService) — что платформа даётприложениям, без указания, чем именно этосделано. Приложение зависит от сервиса, ане от конкретного продукта под ним.Прикладной компонент (ApplicationComponent) — сервис прикладного слоя.Цветом различаются слои: голубой —прикладной, зелёный — технологический.Линия с полым ромбом — aggregation:группа объединяет элемент, но тот можетсуществовать и без неё. Так показано, накаких узлах работает системное ПО.Линия с шариком и закрашенной стрелкой —assignment: узел выделен под компонент, икомпонент на нём исполняется.Смысловая группа (Grouping) — доменприкладного слоя. Тоже aggregation:компонент принадлежит домену по смыслу, ане по месту размещения.Артефакт (Artifact) — то, что физическилежит в хранилище: файл, образконтейнера, дистрибутив.

T1. Технологический слой

Что развёрнуто в кластере и какие технологические сервисы это даёт. Приложения-потребители вынесены в отдельные представления T2 и далее.

+ + + + + + + + + + + + + Кластер k8s-prodCamunda Platformregistry.example.coms3.example.com — объектное хранилищеСетевое хранилище (StorageClass)Git-сервер контураsmtp.example.com — почтовый релейKeycloak — брокер идентификацииAD FS — служба федерацииActive Directory — каталог предприятияKerberos KDC — центр выдачи билетовfrontend-node-01Istio Ingress GatewayIstio Service Meshcert-managerZitadelVault + Agent InjectorPostgreSQLRedisKafkaRabbitMQZeebeZeebe GatewayOperateTasklistOptimizeIdentityConnectorsМаршрутизацияHTTPS-трафикаВыпускTLS-сертификатовАутентификация OIDCУправление секретамиРеляционное хранилищеКэшСобытийная шинаОчередь сообщенийAMQPОркестрация процессовBPMNОбъектное хранилищеОтправка почтовыхуведомленийHelm-чарты (OCI)Образы контейнеров

T2. Развёртывание по узлам

Какие группы узлов какие компоненты несут. Вложенность заменяет стрелки: хост внутри группы — composition, приложение — assignment, системное ПО — aggregation. Имена хостов заданы в профиле контура.

@@ -40,6 +56,8 @@ + + Группа узлов generic (Сервера приложений)Группа узлов K8s (Поддерживающая инфраструктура)Группа узлов processing (Сервера приложений)Объектное хранилищеГруппа узлов DB (Базы данных)Репозиторий компонентовCAD фермаprocessings3.example.com —объектное хранилищеPostgreSQLIstio Ingress GatewayIstio Service Meshcert-managerZitadelVault + Agent InjectorRedisKafkaRabbitMQCamunda Platformregistry.example.comGit-сервер контураКонвертер BIM-моделейams-syncattachmentsauth-flowbimcdechecklistscomparisonscontractscontrol-interfacecross-sectiondjangodocument-linkdocumentationsdrawingseavfaasflowsiaminspectionsissuesmappermeasurementsmessage-hubnotespmprescriptionsprojectsremarksresourcesreviewsrfistamp-verificationsubscriptionssystem-logtransmittalworkspaces

T3. Внешние интеграции

Системы за периметром контура, с которыми платформа обменивается данными, и направление зависимости. Слева — технологические сервисы, которые видят приложения; справа — то, чем эти сервисы обеспечены на стороне заказчика. Схем подключения каталога предприятия четыре, и в конкретном контуре работает одна: каталог напрямую к провайдеру идентификации платформы; каталог через брокер Keycloak; то же с доменным входом по Kerberos, который умеет проверять только брокер; либо служба федерации AD FS поверх того же каталога. Здесь показаны все четыре сразу — при развёртывании лишние узлы из профиля убираются.

@@ -54,7 +72,9 @@ -Аутентификация OIDCОтправка почтовых уведомленийZitadelsmtp.example.com — почтовый релейKeycloak — брокер идентификацииAD FS — служба федерацииActive Directory — каталог предприятияKerberos KDC — центр выдачи билетов

A1. Ядро прикладного слоя

Восемь сервисов с наибольшим числом связей и вызовы между ними. Стрелка идёт от вызываемого к вызывающему (serving): кого лишишься — тот и сломается. Всего в контуре 86 межсервисных связей.

+ + +Аутентификация OIDCОтправка почтовых уведомленийZitadelsmtp.example.com — почтовый релейKeycloak — брокер идентификацииAD FS — служба федерацииActive Directory — каталог предприятияKerberos KDC — центр выдачи билетов

T4. Потребители: Маршрутизация HTTPS-трафика

Сервисом пользуется 21 из 37 приложений контура.

@@ -66,7 +86,9 @@ -documentationsdjangoflowsprocessingeavresourcesissuespm

A2. Зависят от: django

К django обращаются 18 сервисов.

+ + +Маршрутизация HTTPS-трафикаIstio Ingress GatewayIstio Service Meshauth-flowbimcdechecklistscontrol-interfacedjangodocument-linkdocumentationseavflowsiaminspectionsissuespmprocessingremarksreviewsrfistamp-verificationtransmittalworkspaces

T5. Потребители: Аутентификация OIDC

Сервисом пользуются 25 из 37 приложений контура.

@@ -78,6 +100,162 @@ + + +Аутентификация OIDCZitadelams-syncauth-flowchecklistscomparisonscontractscontrol-interfacecross-sectiondjangodocumentationseavflowsiaminspectionsissuesmappernotespmprescriptionsprocessingprojectsremarksreviewsrfitransmittalworkspaces

T6. Потребители: Управление секретами

Сервисом пользуются 26 из 37 приложений контура.

+ + + + + + + + + + + + + +Управление секретамиVault + Agent Injectorattachmentsbimcdechecklistscomparisonscontractsdjangodocumentationsdrawingseavflowsiaminspectionsissuesmappermeasurementsmessage-hubnotespmprocessingresourcesrfisubscriptionssystem-logtransmittalworkspaces

T7. Потребители: Реляционное хранилище

Сервисом пользуются 24 из 37 приложений контура.

+ + + + + + + + + + + + + +Реляционное хранилищеPostgreSQLattachmentsbimchecklistscomparisonscontractsdjangodocumentationsdrawingseavflowsiaminspectionsissuesmappermessage-hubnotespmprocessingresourcesrfisubscriptionssystem-logtransmittalworkspaces

T8. Потребители: Кэш

Сервисом пользуются 5 из 37 приложений контура.

+ + + + + + + + + + + + + +КэшRedisdjangoissuesmappermessage-hubpm

T9. Потребители: Событийная шина

Сервисом пользуются 14 из 37 приложений контура.

+ + + + + + + + + + + + + +Событийная шинаKafkaams-synccontractsdjangodocumentationseavflowsiaminspectionsissuesmappermessage-hubnotespmsystem-log

T10. Потребители: Очередь сообщений AMQP

Сервисом пользуются 12 из 37 приложений контура.

+ + + + + + + + + + + + + +Очередь сообщений AMQPRabbitMQcdecontractsdjangodocumentationsflowsissuesmappernotespmprocessingrfitransmittalКонвертер BIM-моделей

T11. Потребители: Оркестрация процессов BPMN

Сервисом пользуется 1 из 37 приложений контура.

+ + + + + + + + + + + + + +Оркестрация процессов BPMNCamunda Platformcde

T12. Потребители: Объектное хранилище

Сервисом пользуются 16 из 37 приложений контура.

+ + + + + + + + + + + + + +Объектное хранилищеs3.example.com — объектное хранилищеattachmentscdecontractsdjangodocumentationseaviamissuesmappermeasurementsmessage-hubnotespmresourcesrfisubscriptionsКонвертер BIM-моделей

T13. Потребители: Отправка почтовых уведомлений

Сервисом пользуются 6 из 37 приложений контура.

+ + + + + + + + + + + + + +Отправка почтовых уведомленийsmtp.example.com — почтовый релейflowsinspectionsissuesrfisubscriptionstransmittal

A0. Карта прикладного слоя

Из чего состоит прикладной слой: 38 компонентов по смысловым группам. Связи здесь намеренно не показаны — на таком числе компонентов они делают схему нечитаемой; для них есть представления A1 и далее. Вложенность означает aggregation: группа объединяет компоненты, но не владеет ими.

+ + + + + + + + + + + + + +ПлатформаДокументы и общая среда данныхКонтроль качестваУправление проектомПолевые данныеams-syncauth-flowcontrol-interfacedjangoeavfaasflowsiammessage-hubprojectssubscriptionssystem-logworkspacescontractsnotespmresourcesattachmentsbimКонвертер BIM-моделейcdedocument-linkdocumentationsdrawingsstamp-verificationtransmittalchecklistscomparisonsinspectionsissuesprescriptionsremarksreviewsrficross-sectionmappermeasurementsprocessing

A1. Ядро прикладного слоя

Восемь сервисов с наибольшим числом связей и вызовы между ними. Стрелка идёт от вызываемого к вызывающему (serving): кого лишишься — тот и сломается. Всего в контуре 86 межсервисных связей.

+ + + + + + + + + + + + + +documentationsdjangoflowsprocessingeavissuesresourcespm

A2. Зависят от: django

К django обращаются 18 сервисов.

+ + + + + + + + + + + + + djangobimcomparisonsdocumentationsfaasflowsissuesmappermeasurementsmessage-hubnotespmprocessingresourcesrfisubscriptionssystem-logtransmittalworkspaces

A3. Зависят от: documentations

К documentations обращаются 11 сервисов.

@@ -90,6 +268,8 @@ + + documentationscomparisonsdjangofaasflowsissuesnotesprocessingrfisystem-logtransmittalworkspaces

A4. Зависят от: eav

К eav обращаются 9 сервисов.

@@ -102,6 +282,8 @@ + + eavcomparisonsdocumentationsfaasflowsinspectionsissuesmessage-hubpmrfi

A5. Зависят от: resources

К resources обращаются 7 сервисов.

@@ -114,6 +296,8 @@ + + resourcesdocumentationsflowsissuespmprocessingrfitransmittal

A6. Зависят от: processing

К processing обращаются 6 сервисов.

@@ -126,6 +310,8 @@ + + processingcomparisonsdjangodocumentationsflowsinspectionsissues

A7. Зависят от: iam

К iam обращаются 6 сервисов.

@@ -138,6 +324,8 @@ + + iamdocumentationsflowsissuespmprocessingtransmittal

A8. Зависят от: flows

К flows обращаются 4 сервиса.

@@ -150,6 +338,8 @@ + + flowscdedocumentationsissuestransmittal

A9. Зависят от: workspaces

К workspaces обращаются 4 сервиса.

@@ -162,6 +352,8 @@ + + workspacescdecomparisonsdjangodocumentations

A10. Зависят от: system-log

К system-log обращаются 3 сервиса.

@@ -174,6 +366,8 @@ + + system-logcdedocumentationssubscriptions

A11. Зависят от: bim

К bim обращаются 3 сервиса.

@@ -186,6 +380,8 @@ + + bimcomparisonsdjangodocumentations

A12. Зависят от: pm

К pm обращаются 3 сервиса.

@@ -198,7 +394,9 @@ -pmdjangoflowsmessage-hub

T16. Потребители: Маршрутизация HTTPS-трафика

Сервисом пользуется 21 из 37 приложений контура.

+ + +pmdjangoflowsmessage-hub

P1. Паспорт: documentations

Что нужно, чтобы documentations работал: 7 технологических сервисов и 15 прикладных сервисов. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Кто зависит от самого documentations — представление A3.

@@ -210,7 +408,9 @@ -Маршрутизация HTTPS-трафикаIstio Ingress GatewayIstio Service Meshauth-flowbimcdechecklistscontrol-interfacedjangodocument-linkdocumentationseavflowsiaminspectionsissuespmprocessingremarksreviewsrfistamp-verificationtransmittalworkspaces

T17. Потребители: Аутентификация OIDC

Сервисом пользуются 25 из 37 приложений контура.

+ + +documentationsМаршрутизацияHTTPS-трафикаАутентификация OIDCУправление секретамиРеляционное хранилищеСобытийная шинаОчередь сообщений AMQPОбъектное хранилищеattachmentsbimdjangodrawingseavflowsiaminspectionsnotesprocessingremarksresourcessubscriptionssystem-logworkspaces

P2. Паспорт: flows

Что нужно, чтобы flows работал: 7 технологических сервисов и 8 прикладных сервисов. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Кто зависит от самого flows — представление A8.

@@ -222,7 +422,9 @@ -Аутентификация OIDCZitadelams-syncauth-flowchecklistscomparisonscontractscontrol-interfacecross-sectiondjangodocumentationseavflowsiaminspectionsissuesmappernotespmprescriptionsprocessingprojectsremarksreviewsrfitransmittalworkspaces

T18. Потребители: Управление секретами

Сервисом пользуются 26 из 37 приложений контура.

+ + +flowsМаршрутизацияHTTPS-трафикаАутентификация OIDCУправление секретамиРеляционное хранилищеСобытийная шинаОчередь сообщений AMQPОтправка почтовыхуведомленийchecklistsdjangodocumentationseaviampmprocessingresources

P3. Паспорт: issues

Что нужно, чтобы issues работал: 9 технологических сервисов и 7 прикладных сервисов. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у issues меньше 3, отдельного представления для них нет.

@@ -234,7 +436,9 @@ -Управление секретамиVault + Agent Injectorattachmentsbimcdechecklistscomparisonscontractsdjangodocumentationsdrawingseavflowsiaminspectionsissuesmappermeasurementsmessage-hubnotespmprocessingresourcesrfisubscriptionssystem-logtransmittalworkspaces

T19. Потребители: Реляционное хранилище

Сервисом пользуются 24 из 37 приложений контура.

+ + +issuesМаршрутизацияHTTPS-трафикаАутентификация OIDCУправление секретамиРеляционное хранилищеКэшСобытийная шинаОчередь сообщений AMQPОбъектное хранилищеОтправка почтовыхуведомленийdjangodocumentationseavflowsiamprocessingresources

P4. Паспорт: comparisons

Что нужно, чтобы comparisons работал: 3 технологических сервиса и 6 прикладных сервисов. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у comparisons меньше 3, отдельного представления для них нет.

@@ -246,7 +450,9 @@ -Реляционное хранилищеPostgreSQLattachmentsbimchecklistscomparisonscontractsdjangodocumentationsdrawingseavflowsiaminspectionsissuesmappermessage-hubnotespmprocessingresourcesrfisubscriptionssystem-logtransmittalworkspaces

T20. Потребители: Кэш

Сервисом пользуются 5 из 37 приложений контура.

+ + +comparisonsАутентификация OIDCУправление секретамиРеляционное хранилищеbimdjangodocumentationseavprocessingworkspaces

P5. Паспорт: django

Что нужно, чтобы django работал: 8 технологических сервисов и 6 прикладных сервисов. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Кто зависит от самого django — представление A2.

@@ -258,7 +464,9 @@ -КэшRedisdjangoissuesmappermessage-hubpm

T21. Потребители: Событийная шина

Сервисом пользуются 14 из 37 приложений контура.

+ + +djangoМаршрутизацияHTTPS-трафикаАутентификация OIDCУправление секретамиРеляционное хранилищеКэшСобытийная шинаОчередь сообщений AMQPОбъектное хранилищеbimdocumentationsmeasurementspmprocessingworkspaces

P6. Паспорт: faas

Что нужно, чтобы faas работал: 0 технологических сервисов и 5 прикладных сервисов. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у faas меньше 3, отдельного представления для них нет.

@@ -270,7 +478,9 @@ -Событийная шинаKafkaams-synccontractsdjangodocumentationseavflowsiaminspectionsissuesmappermessage-hubnotespmsystem-log

T22. Потребители: Очередь сообщений AMQP

Сервисом пользуются 12 из 37 приложений контура.

+ + +faasdjangodocumentationseavreviewstransmittal

P7. Паспорт: transmittal

Что нужно, чтобы transmittal работал: 6 технологических сервисов и 5 прикладных сервисов. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у transmittal меньше 3, отдельного представления для них нет.

@@ -282,7 +492,9 @@ -Очередь сообщений AMQPRabbitMQcdecontractsdjangodocumentationsflowsissuesmappernotespmprocessingrfitransmittalКонвертер BIM-моделей

T23. Потребители: Оркестрация процессов BPMN

Сервисом пользуется 1 из 37 приложений контура.

+ + +transmittalМаршрутизацияHTTPS-трафикаАутентификация OIDCУправление секретамиРеляционное хранилищеОчередь сообщений AMQPОтправка почтовыхуведомленийdjangodocumentationsflowsiamresources

P8. Паспорт: message-hub

Что нужно, чтобы message-hub работал: 5 технологических сервисов и 4 прикладных сервиса. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у message-hub меньше 3, отдельного представления для них нет.

@@ -294,7 +506,9 @@ -Оркестрация процессов BPMNCamunda Platformcde

T24. Потребители: Объектное хранилище

Сервисом пользуются 16 из 37 приложений контура.

+ + +message-hubУправление секретамиРеляционное хранилищеКэшСобытийная шинаОбъектное хранилищеdjangoeavissuespm

P9. Паспорт: pm

Что нужно, чтобы pm работал: 8 технологических сервисов и 4 прикладных сервиса. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Кто зависит от самого pm — представление A12.

@@ -306,7 +520,9 @@ -Объектное хранилищеs3.example.com — объектное хранилищеattachmentscdecontractsdjangodocumentationseaviamissuesmappermeasurementsmessage-hubnotespmresourcesrfisubscriptionsКонвертер BIM-моделей

T25. Потребители: Отправка почтовых уведомлений

Сервисом пользуются 6 из 37 приложений контура.

+ + +pmМаршрутизацияHTTPS-трафикаАутентификация OIDCУправление секретамиРеляционное хранилищеКэшСобытийная шинаОчередь сообщений AMQPОбъектное хранилищеdjangoeaviamresources

P10. Паспорт: processing

Что нужно, чтобы processing работал: 5 технологических сервисов и 4 прикладных сервиса. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Кто зависит от самого processing — представление A6.

@@ -318,5 +534,49 @@ -Отправка почтовых уведомленийsmtp.example.com — почтовый релейflowsinspectionsissuesrfisubscriptionstransmittal
+ + +processingМаршрутизацияHTTPS-трафикаАутентификация OIDCУправление секретамиРеляционное хранилищеОчередь сообщений AMQPdjangodocumentationsiamresources

P11. Паспорт: rfi

Что нужно, чтобы rfi работал: 7 технологических сервисов и 4 прикладных сервиса. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у rfi меньше 3, отдельного представления для них нет.

+ + + + + + + + + + + + + +rfiМаршрутизацияHTTPS-трафикаАутентификация OIDCУправление секретамиРеляционное хранилищеОчередь сообщений AMQPОбъектное хранилищеОтправка почтовыхуведомленийdjangodocumentationseavresources

P12. Паспорт: cde

Что нужно, чтобы cde работал: 5 технологических сервисов и 3 прикладных сервиса. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у cde меньше 3, отдельного представления для них нет.

+ + + + + + + + + + + + + +cdeМаршрутизацияHTTPS-трафикаУправление секретамиОчередь сообщений AMQPОркестрация процессовBPMNОбъектное хранилищеflowssystem-logworkspaces

P13. Паспорт: notes

Что нужно, чтобы notes работал: 6 технологических сервисов и 3 прикладных сервиса. Стрелки, как и везде, идут от обслуживающего к обслуживаемому. Входящих вызовов у notes меньше 3, отдельного представления для них нет.

+ + + + + + + + + + + + + +notesАутентификация OIDCУправление секретамиРеляционное хранилищеСобытийная шинаОчередь сообщений AMQPОбъектное хранилищеattachmentsdjangodocumentations
\ No newline at end of file diff --git a/docs/architecture/render_preview.py b/docs/architecture/render_preview.py index 3808afb..60dd43e 100644 --- a/docs/architecture/render_preview.py +++ b/docs/architecture/render_preview.py @@ -12,8 +12,9 @@ LAYER = { # цвета Archi по слоям "ApplicationComponent": "#B5FFFF", "ApplicationService": "#B5FFFF", "Node": "#C9E7B7", "SystemSoftware": "#C9E7B7", "TechnologyService": "#C9E7B7", "Artifact": "#C9E7B7", + "Grouping": "#F4F4F4", } -STROKE = {"Application": "#2a7f8f", "Technology": "#4a7f3a"} +STROKE = {"Application": "#2a7f8f", "Technology": "#4a7f3a", "Grouping": "#888"} concepts, rels = {}, {} for folder in root.findall("folder"): @@ -24,17 +25,20 @@ for folder in root.findall("folder"): elif et != "ArchimateDiagramModel": concepts[e.get("id")] = (et, e.get("name") or "") -def collect(node, ox, oy, nodes, conns): +def collect(node, ox, oy, nodes, conns, notes): b = node.find("bounds") x, y = ox + int(b.get("x", 0)), oy + int(b.get("y", 0)) w, h = int(b.get("width", 120)), int(b.get("height", 55)) + if (node.get(XSI) or "") == "archimate:Note": + notes.append((node.findtext("content") or "", x, y, w, h)) + return nodes[node.get("id")] = (node.get("archimateElement"), x, y, w, h) for c in node.findall("sourceConnection"): conns.append((c.get("relationship"), c.get("source"), c.get("target"))) for ch in node.findall("child"): - collect(ch, x, y, nodes, conns) + collect(ch, x, y, nodes, conns, notes) -def wrap(s, width): +def wrap(s, width, limit=4): out, line = [], "" for word in s.split(): if len(line) + len(word) + 1 > width and line: @@ -43,7 +47,7 @@ def wrap(s, width): line = f"{line} {word}".strip() if line: out.append(line) - return out[:4] + return out[:limit] def edge_point(x, y, w, h, tx, ty): """Точка пересечения луча к (tx,ty) с границей прямоугольника.""" @@ -68,6 +72,8 @@ MARKERS = """ + + """ STYLE = { @@ -75,6 +81,7 @@ STYLE = { "Realization": ('stroke-dasharray="6,4"', 'marker-end="url(#hollow)"', ""), "Composition": ('stroke-dasharray="none"', "", 'marker-start="url(#diamond)"'), "Assignment": ('stroke-dasharray="none"', 'marker-end="url(#filled)"', 'marker-start="url(#ball)"'), + "Aggregation": ('stroke-dasharray="none"', "", 'marker-start="url(#odiamond)"'), } parts = [] @@ -82,13 +89,15 @@ views = [e for f in root.findall("folder") for e in f.findall("element") if (e.get(XSI) or "").endswith("ArchimateDiagramModel")] for v in views: - nodes, conns = {}, [] + nodes, conns, notes = {}, [], [] for ch in v.findall("child"): - collect(ch, 0, 0, nodes, conns) + collect(ch, 0, 0, nodes, conns, notes) if not nodes: continue - maxx = max(x + w for _, x, _, w, _ in nodes.values()) + 40 - maxy = max(y + h for _, _, y, _, h in nodes.values()) + 40 + boxes = [(x, y, w, h) for _, x, y, w, h in nodes.values()] + \ + [(x, y, w, h) for _, x, y, w, h in notes] + maxx = max(x + w for x, _, w, _ in boxes) + 40 + maxy = max(y + h for _, y, _, h in boxes) + 40 svg = [f'', MARKERS] @@ -110,7 +119,8 @@ 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 "?")) fill = LAYER.get(etype, "#EEEEEE") - stroke = STROKE["Application"] if etype.startswith("Application") else STROKE["Technology"] + stroke = (STROKE["Grouping"] if etype == "Grouping" else + STROKE["Application"] if etype.startswith("Application") else STROKE["Technology"]) big = w * h > 60000 svg.append(f'') @@ -125,6 +135,14 @@ for v in views: svg.append(f'' f'{html.escape(ln)}') + + # надписи — поверх всего, без заливки слоя: это не элементы модели + for content, x, y, w, h in notes: + svg.append(f'') + for i, ln in enumerate(wrap(content, max(16, int(w / 6.2)), limit=int(h // 15))): + svg.append(f'' + f'{html.escape(ln)}') svg.append("") doc = v.find("documentation") diff --git a/docs/architecture/to_open_exchange.py b/docs/architecture/to_open_exchange.py index aac4e59..5dd416e 100644 --- a/docs/architecture/to_open_exchange.py +++ b/docs/architecture/to_open_exchange.py @@ -45,8 +45,10 @@ def flatten(node, ox=0, oy=0, out=None, conns=None): conns = [] if conns is None else conns b = node.find("bounds") x = ox + int(b.get("x", 0)); y = oy + int(b.get("y", 0)) + # у надписей (Note) нет archimateElement — в обменном формате это узел типа Label out.append((node.get("id"), node.get("archimateElement"), - x, y, int(b.get("width", 120)), int(b.get("height", 55)))) + x, y, int(b.get("width", 120)), int(b.get("height", 55)), + text(node, "content"))) for c in node.findall("sourceConnection"): conns.append((c.get("id"), c.get("relationship"), c.get("source"), c.get("target"))) @@ -88,9 +90,15 @@ for v in views: nodes, conns = [], [] for child in v.findall("child"): flatten(child, 0, 0, nodes, conns) - for nid, eref, x, y, w, h in nodes: - out.append(f' ') + for nid, eref, x, y, w, h, content in nodes: + if eref: + out.append(f' ') + else: + out.append(f' ') + out.append(f' ') + out.append(' ') for cid, rref, s, tg in conns: out.append(f' ') diff --git a/legend.png b/legend.png new file mode 100644 index 0000000000000000000000000000000000000000..fdb516b378f498015c13b7577c08cf62396fafa7 GIT binary patch literal 53020 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