aact 2.1.5 → 3.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,7 +19,7 @@ CLI и библиотека для валидации, анализа и ген
19
19
 
20
20
  <img src="https://github.com/Byndyusoft/aact/assets/1096954/a3c3b3b0-a09b-4da7-aca4-5538159b371c" width="15"/> Телеграм-канал: [Архитектура распределённых систем](https://t.me/rsa_enc)
21
21
 
22
- aact можно использовать двумя способами: как **CLI** (`npx aact check`, авто-фикс, генерация артефактов) или как **библиотеку** (импортировать `checkAcl`, `analyzeArchitecture` и пр. в свои тесты на vitest/jest). CLI — ниже, library-режим — в [соответствующем разделе](#использование-как-библиотеки).
22
+ aact можно использовать двумя способами: как **CLI** (`npx aact check`, авто-фикс, генерация артефактов) или как **библиотеку** (импортировать `aclRule`, `analyzeArchitecture` и пр. в свои тесты на vitest/jest). CLI — ниже, library-режим — в [соответствующем разделе](#использование-как-библиотеки).
23
23
 
24
24
  ## Quick Start (CLI)
25
25
 
@@ -93,26 +93,37 @@ export default config;
93
93
 
94
94
  ```ts
95
95
  import {
96
- loadPlantumlElements,
97
- mapContainersFromPlantumlElements,
98
- checkAcl,
99
- checkAcyclic,
100
- checkCrud,
96
+ plantumlFormat,
97
+ aclRule,
98
+ acyclicRule,
99
+ crudRule,
101
100
  analyzeArchitecture,
101
+ validateModel,
102
102
  } from "aact";
103
103
 
104
- const elements = await loadPlantumlElements("architecture.puml");
105
- const model = mapContainersFromPlantumlElements(elements);
104
+ // Загрузка через format — возвращает Model + diagnostic issues
105
+ const { model, issues } = await plantumlFormat.load("architecture.puml");
106
+ for (const issue of issues) console.warn(`model:`, issue);
106
107
 
107
- // Проверка правил
108
- const aclViolations = checkAcl(model.allContainers);
109
- const cyclicViolations = checkAcyclic(model.allContainers);
108
+ // Проверка правил — uniform signature (model, options?) => Violation[]
109
+ const aclViolations = aclRule.check(model);
110
+ const cyclicViolations = acyclicRule.check(model);
111
+ const crudViolations = crudRule.check(model, { repoTags: ["repo", "dao"] });
110
112
 
111
113
  // Анализ метрик
112
114
  const { report } = analyzeArchitecture(model);
113
115
  console.log(`Elements: ${report.elementsCount}`);
116
+
117
+ // Прямой доступ к containers / boundaries — Record<name, ...>
118
+ for (const container of Object.values(model.containers)) {
119
+ console.log(`${container.kind} ${container.name}`);
120
+ }
121
+ const ordersService = model.containers["orders"];
114
122
  ```
115
123
 
124
+ Полный API: [`Model`](./src/model/types.ts), [`Format`](./src/formats/types.ts),
125
+ [`RuleDefinition`](./src/rules/types.ts). См. `CHANGELOG.md` для v2 → v3 migration.
126
+
116
127
  ## Примеры
117
128
 
118
129
  Запускаемые из коробки (склонируй репо, `cd examples/<name>`, `npx aact check`):
@@ -122,7 +133,7 @@ console.log(`Elements: ${report.elementsCount}`);
122
133
 
123
134
  Тестовые сценарии (для разработчиков пакета, запускаются через `vitest`):
124
135
 
125
- - [`examples/banking-plantuml/`](examples/banking-plantuml/) и [`examples/microservices-structurizr/`](examples/microservices-structurizr/) — интеграционные тесты архитектуры из `resources/`.
136
+ - [`examples/banking-plantuml/`](examples/banking-plantuml/) и [`examples/microservices-structurizr/`](examples/microservices-structurizr/) — интеграционные тесты архитектуры из `fixtures/`.
126
137
 
127
138
  ## Документация
128
139
 
@@ -130,6 +141,27 @@ console.log(`Elements: ${report.elementsCount}`);
130
141
  - [ADR](ADRs/) — Architecture Decision Records
131
142
  - [Roadmap](roadmap.md) — планы развития
132
143
 
144
+ ## Testing
145
+
146
+ Тестовый стек разделён на четыре уровня:
147
+
148
+ ```bash
149
+ pnpm test # все unit + integration + e2e
150
+ pnpm test:unit # только unit
151
+ pnpm test:integration # интеграционные на реальных фикстурах
152
+ pnpm test:e2e # subprocess-тесты CLI через execa
153
+ pnpm test:coverage # с v8 coverage + порогами
154
+ pnpm test:mutation # Stryker mutation testing
155
+ ```
156
+
157
+ **Метрики качества тестов:**
158
+
159
+ - **Coverage** (v8): порог в CI — statements ≥95%, branches ≥85%, functions ≥95%, lines ≥95%
160
+ - **Mutation score** (Stryker) ≥95% — каждое смысловое изменение в исходнике должно ломать хотя бы один тест
161
+ - **Property-based** (`@fast-check/vitest`) на option-bearing правилах — защита от «hardcoded literal where option should be read» бага
162
+ - **Inline snapshots** на генераторах для regression-pin'а формата вывода
163
+ - **E2E** на цепочке `init → check → fix → recheck` через `npx aact` в subprocess
164
+
133
165
  ## Публичные материалы
134
166
 
135
167
  ### Раз архитектура — «as Code», почему бы её не покрыть тестами?!
@@ -172,13 +204,13 @@ https://www.youtube.com/watch?v=fb2UjqjHGUE
172
204
 
173
205
  ## Пример архитектуры, которую покроем тестами
174
206
 
175
- [![C4](resources/architecture/Demo%20Tests.svg)](resources/architecture/Demo%20Tests.svg)
207
+ [![C4](fixtures/architecture/Demo%20Tests.svg)](fixtures/architecture/Demo%20Tests.svg)
176
208
 
177
209
  ## Пример тестов
178
210
 
179
- 1. [find diff in configs and uml containers](examples/banking-plantuml/architecture.test.ts) — проверяет актуальность списка микросервисов на архитектуре и в [конфигурации инфраструктуры](resources/kubernetes/microservices)
180
- 2. [find diff in configs and uml dependencies](examples/banking-plantuml/architecture.test.ts) — проверяет актуальность зависимостей (связей) микросервисов на архитектуре и в [конфигурации инфраструктуры](resources/kubernetes/microservices)
181
- 3. [check that urls and topics from relations exist in config](examples/banking-plantuml/architecture.test.ts) — проверяет соответствие между параметрами связей микросервисов (REST-урлы, топики kafka) на архитектуре и в [конфигурации инфраструктуры](resources/kubernetes/microservices)
211
+ 1. [find diff in configs and uml containers](examples/banking-plantuml/architecture.test.ts) — проверяет актуальность списка микросервисов на архитектуре и в [конфигурации инфраструктуры](fixtures/kubernetes/microservices)
212
+ 2. [find diff in configs and uml dependencies](examples/banking-plantuml/architecture.test.ts) — проверяет актуальность зависимостей (связей) микросервисов на архитектуре и в [конфигурации инфраструктуры](fixtures/kubernetes/microservices)
213
+ 3. [check that urls and topics from relations exist in config](examples/banking-plantuml/architecture.test.ts) — проверяет соответствие между параметрами связей микросервисов (REST-урлы, топики kafka) на архитектуре и в [конфигурации инфраструктуры](fixtures/kubernetes/microservices)
182
214
  4. [only acl can depend on external systems](test/rules/acl.test.ts) — проверяет, что не нарушен выбранный принцип построения интеграций с внешними системами только через ACL (Anti Corruption Layer). Проверяет, что только acl-микросервисы имеют зависимости от внешних систем.
183
215
  5. [connect to external systems only by API Gateway or kafka](examples/banking-plantuml/architecture.test.ts) — проверяет, что все внешние интеграции идут через API Gateway или через kafka
184
216
 
@@ -190,11 +222,11 @@ https://www.youtube.com/watch?v=fb2UjqjHGUE
190
222
 
191
223
  ### Ручная:
192
224
 
193
- [![C4](resources/architecture/Demo%20Tests.svg)](resources/architecture/Demo%20Tests.svg)
225
+ [![C4](fixtures/architecture/Demo%20Tests.svg)](fixtures/architecture/Demo%20Tests.svg)
194
226
 
195
227
  ### Сгенерированная:
196
228
 
197
- [![C4](resources/architecture/Demo%20Generated.svg)](resources/architecture/Demo%20Generated.svg)
229
+ [![C4](fixtures/architecture/Demo%20Generated.svg)](fixtures/architecture/Demo%20Generated.svg)
198
230
 
199
231
  # Тестирование модульного монолита
200
232
 
@@ -0,0 +1,422 @@
1
+ import { p as getBoundary, q as getContainer } from '../shared/aact.CxegP3pU.mjs';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'pathe';
4
+ import { Comment, Stdlib_C4_Container_Component, Stdlib_C4_Context, Stdlib_C4_Dynamic_Rel, Relationship, Stdlib_C4_Boundary, parse } from 'plantuml-parser';
5
+ import { p as parseCsvTags } from '../shared/aact.BpV1UCZJ.mjs';
6
+ import { b as buildModel } from '../shared/aact.CfImn7en.mjs';
7
+ import 'valibot';
8
+ import 'consola';
9
+
10
+ const C4_KIND_MAP = Object.freeze({
11
+ // Person
12
+ Person: { kind: "Person", external: false },
13
+ Person_Ext: { kind: "Person", external: true },
14
+ // System / SystemDb / SystemQueue + _Ext variants
15
+ System: { kind: "System", external: false },
16
+ SystemDb: { kind: "System", external: false },
17
+ SystemQueue: { kind: "System", external: false },
18
+ System_Ext: { kind: "System", external: true },
19
+ SystemDb_Ext: { kind: "System", external: true },
20
+ SystemQueue_Ext: { kind: "System", external: true },
21
+ // Container variants
22
+ Container: { kind: "Container", external: false },
23
+ ContainerDb: { kind: "ContainerDb", external: false },
24
+ ContainerQueue: { kind: "ContainerQueue", external: false },
25
+ Container_Ext: { kind: "Container", external: true },
26
+ ContainerDb_Ext: { kind: "ContainerDb", external: true },
27
+ ContainerQueue_Ext: { kind: "ContainerQueue", external: true },
28
+ // Component variants
29
+ Component: { kind: "Component", external: false },
30
+ ComponentDb: { kind: "ComponentDb", external: false },
31
+ ComponentQueue: { kind: "ComponentQueue", external: false },
32
+ Component_Ext: { kind: "Component", external: true },
33
+ ComponentDb_Ext: { kind: "ComponentDb", external: true },
34
+ ComponentQueue_Ext: { kind: "ComponentQueue", external: true }
35
+ });
36
+ const parseC4MacroKind = (macroName) => C4_KIND_MAP[macroName];
37
+ const BOUNDARY_KIND_MAP = Object.freeze(
38
+ {
39
+ Boundary: "System",
40
+ // generic — sensible default
41
+ System_Boundary: "System",
42
+ Container_Boundary: "Container",
43
+ Component_Boundary: "Component",
44
+ Enterprise_Boundary: "Enterprise"
45
+ }
46
+ );
47
+ const parseBoundaryMacro = (macroName) => BOUNDARY_KIND_MAP[macroName] ?? "System";
48
+ const c4MacroName = (kind, external) => {
49
+ if (kind === "Person") return external ? "Person_Ext" : "Person";
50
+ if (kind === "System") return external ? "System_Ext" : "System";
51
+ return external ? `${kind}_Ext` : kind;
52
+ };
53
+ const boundaryMacroName = (kind) => {
54
+ if (kind === "System") return "System_Boundary";
55
+ if (kind === "Container") return "Container_Boundary";
56
+ if (kind === "Component") return "Component_Boundary";
57
+ return "Enterprise_Boundary";
58
+ };
59
+
60
+ const isContextKind = (kind) => kind === "Person" || kind === "System";
61
+ const renderContainer = (container) => {
62
+ const macro = c4MacroName(container.kind, container.external);
63
+ const parts = [container.name, `"${container.label}"`];
64
+ if (isContextKind(container.kind)) {
65
+ if (container.description) parts.push(`"${container.description}"`);
66
+ } else {
67
+ if (container.technology) parts.push(`"${container.technology}"`);
68
+ else if (container.description) parts.push('""');
69
+ if (container.description) parts.push(`"${container.description}"`);
70
+ }
71
+ const named = [];
72
+ if (container.sprite) named.push(`$sprite="${container.sprite}"`);
73
+ if (container.tags.length > 0)
74
+ named.push(`$tags="${container.tags.join("+")}"`);
75
+ if (container.link) named.push(`$link="${container.link}"`);
76
+ return `${macro}(${[...parts, ...named].join(", ")})`;
77
+ };
78
+ const renderBoundary = (model, boundary, indent) => {
79
+ const inner = indent + " ";
80
+ const macro = boundaryMacroName(boundary.kind);
81
+ const childBoundaries = boundary.boundaryNames.map((name) => getBoundary(model, name)).filter((b) => b !== void 0).map((b) => renderBoundary(model, b, inner));
82
+ const childContainers = boundary.containerNames.map((name) => getContainer(model, name)).filter((c) => c !== void 0).map((c) => `${inner}${renderContainer(c)}`);
83
+ const parts = [boundary.name, `"${boundary.label}"`];
84
+ const named = [];
85
+ if (boundary.tags.length > 0)
86
+ named.push(`$tags="${boundary.tags.join("+")}"`);
87
+ if (boundary.link) named.push(`$link="${boundary.link}"`);
88
+ return [
89
+ `${indent}${macro}(${[...parts, ...named].join(", ")}) {`,
90
+ ...childBoundaries,
91
+ ...childContainers,
92
+ `${indent}}`
93
+ ].join("\n");
94
+ };
95
+ const renderRelation = (from, relation) => {
96
+ const label = relation.description ?? "";
97
+ const parts = [from, relation.to, `"${label}"`];
98
+ if (relation.technology) parts.push(`"${relation.technology}"`);
99
+ const named = [];
100
+ if (relation.sprite) named.push(`$sprite="${relation.sprite}"`);
101
+ if (relation.tags.length > 0)
102
+ named.push(`$tags="${relation.tags.join("+")}"`);
103
+ if (relation.link) named.push(`$link="${relation.link}"`);
104
+ return `Rel(${[...parts, ...named].join(", ")})`;
105
+ };
106
+ const collectBoundedContainerNames = (model) => {
107
+ const names = /* @__PURE__ */ new Set();
108
+ const visit = (boundary) => {
109
+ for (const n of boundary.containerNames) names.add(n);
110
+ for (const child of boundary.boundaryNames) {
111
+ const b = getBoundary(model, child);
112
+ if (b) visit(b);
113
+ }
114
+ };
115
+ for (const root of model.rootBoundaryNames) {
116
+ const b = getBoundary(model, root);
117
+ if (b) visit(b);
118
+ }
119
+ return names;
120
+ };
121
+ const renderBody = (model, standaloneContainers, boundaryLabel) => {
122
+ const rootBoundaries = model.rootBoundaryNames.map((n) => getBoundary(model, n)).filter((b) => b !== void 0);
123
+ if (boundaryLabel) {
124
+ return [
125
+ `Boundary(project, "${boundaryLabel}") {`,
126
+ ...rootBoundaries.map((b) => renderBoundary(model, b, " ")),
127
+ ...standaloneContainers.map((c) => ` ${renderContainer(c)}`),
128
+ `}`
129
+ ];
130
+ }
131
+ return [
132
+ ...rootBoundaries.map((b) => renderBoundary(model, b, "")),
133
+ ...standaloneContainers.map((c) => renderContainer(c))
134
+ ];
135
+ };
136
+ const generate = (model, options) => {
137
+ const boundedNames = collectBoundedContainerNames(model);
138
+ const standalone = Object.values(model.containers).filter(
139
+ (c) => !boundedNames.has(c.name)
140
+ );
141
+ const relations = Object.values(model.containers).flatMap(
142
+ (container) => container.relations.map((rel) => renderRelation(container.name, rel))
143
+ );
144
+ const content = [
145
+ `@startuml`,
146
+ `!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml`,
147
+ `LAYOUT_WITH_LEGEND()`,
148
+ `AddRelTag("async", $lineStyle = DottedLine())`,
149
+ "",
150
+ ...renderBody(model, standalone, options?.boundaryLabel),
151
+ "",
152
+ ...relations,
153
+ "@enduml"
154
+ ].join("\n");
155
+ return {
156
+ files: [{ path: "architecture.puml", content }]
157
+ };
158
+ };
159
+
160
+ const CONTAINER_LIKE_NAMES = /* @__PURE__ */ new Set([
161
+ "Container",
162
+ "ContainerDb",
163
+ "ContainerQueue",
164
+ "Container_Ext",
165
+ "ContainerDb_Ext",
166
+ "ContainerQueue_Ext",
167
+ "Component",
168
+ "ComponentDb",
169
+ "ComponentQueue",
170
+ "Component_Ext",
171
+ "ComponentDb_Ext",
172
+ "ComponentQueue_Ext"
173
+ ]);
174
+ const CONTEXT_NAMES = /* @__PURE__ */ new Set([
175
+ "Person",
176
+ "Person_Ext",
177
+ "System",
178
+ "SystemDb",
179
+ "SystemQueue",
180
+ "System_Ext",
181
+ "SystemDb_Ext",
182
+ "SystemQueue_Ext"
183
+ ]);
184
+ const BOUNDARY_NAMES = /* @__PURE__ */ new Set([
185
+ "Boundary",
186
+ "System_Boundary",
187
+ "Container_Boundary",
188
+ "Component_Boundary",
189
+ "Enterprise_Boundary"
190
+ ]);
191
+ const filterElements = (elements) => {
192
+ const result = [];
193
+ for (const element of elements) {
194
+ if (element instanceof Comment) continue;
195
+ const typeName = element.type_?.name;
196
+ if (element instanceof Stdlib_C4_Container_Component && CONTAINER_LIKE_NAMES.has(typeName) || element instanceof Stdlib_C4_Context && CONTEXT_NAMES.has(typeName) || element instanceof Stdlib_C4_Dynamic_Rel || element instanceof Relationship) {
197
+ result.push(element);
198
+ }
199
+ if (element instanceof Stdlib_C4_Boundary && BOUNDARY_NAMES.has(typeName)) {
200
+ result.push(element, ...filterElements(element.elements));
201
+ }
202
+ if (Array.isArray(element)) {
203
+ result.push(...filterElements(element));
204
+ }
205
+ }
206
+ return result;
207
+ };
208
+
209
+ const TAGS_MARKER = "__aact_tags__:";
210
+ const LINK_MARKER = "__aact_link__:";
211
+ const SPRITE_MARKER = "__aact_sprite__:";
212
+ const preTransformNamedArgs = (raw) => raw.replaceAll(/, \$tags="(.+?)"/g, `, "${TAGS_MARKER}$1"`).replaceAll(/, \$link="(.+?)"/g, `, "${LINK_MARKER}$1"`).replaceAll(/, \$sprite="(.+?)"/g, `, "${SPRITE_MARKER}$1"`).replaceAll('""', '" "');
213
+ const stripMarker = (value, marker) => value && value.startsWith(marker) ? value.slice(marker.length) : void 0;
214
+ const extractMarked = (marker, ...slots) => {
215
+ for (const slot of slots) {
216
+ const v = stripMarker(slot, marker);
217
+ if (v !== void 0) return v;
218
+ }
219
+ return void 0;
220
+ };
221
+ const cleanSlot = (value, ...markers) => {
222
+ if (!value) return void 0;
223
+ if (markers.some((m) => value.startsWith(m))) return void 0;
224
+ return value;
225
+ };
226
+ const normalizeRelBack = (elements) => {
227
+ for (const element of elements) {
228
+ if (element instanceof Comment) continue;
229
+ if (!(element instanceof Stdlib_C4_Dynamic_Rel)) continue;
230
+ if (element.type_.name.startsWith("Rel_Back")) {
231
+ const from = element.from;
232
+ element.from = element.to;
233
+ element.to = from;
234
+ }
235
+ }
236
+ };
237
+ const ALL_MARKERS = [TAGS_MARKER, LINK_MARKER, SPRITE_MARKER];
238
+ const buildContainer = (el) => {
239
+ const macroKind = parseC4MacroKind(el.type_.name);
240
+ const kind = macroKind?.kind ?? "Container";
241
+ const external = macroKind?.external ?? false;
242
+ const rawTechn = "techn" in el && typeof el.techn === "string" ? el.techn : void 0;
243
+ const taggedValue = extractMarked(
244
+ TAGS_MARKER,
245
+ rawTechn,
246
+ el.descr,
247
+ el.sprite,
248
+ el.tags,
249
+ el.link
250
+ );
251
+ const linkValue = extractMarked(
252
+ LINK_MARKER,
253
+ rawTechn,
254
+ el.descr,
255
+ el.sprite,
256
+ el.tags,
257
+ el.link
258
+ );
259
+ const spriteNamedValue = extractMarked(
260
+ SPRITE_MARKER,
261
+ rawTechn,
262
+ el.descr,
263
+ el.sprite,
264
+ el.tags,
265
+ el.link
266
+ );
267
+ return {
268
+ name: el.alias,
269
+ label: el.label,
270
+ kind,
271
+ external,
272
+ description: cleanSlot(el.descr, ...ALL_MARKERS) ?? "",
273
+ technology: cleanSlot(rawTechn, ...ALL_MARKERS),
274
+ tags: taggedValue === void 0 ? parseCsvTags(cleanSlot(el.tags, ...ALL_MARKERS)) : parseCsvTags(taggedValue),
275
+ sprite: spriteNamedValue ?? cleanSlot(el.sprite, ...ALL_MARKERS),
276
+ relations: [],
277
+ link: linkValue ?? cleanSlot(el.link, ...ALL_MARKERS)
278
+ };
279
+ };
280
+ const buildRelation = (rel) => {
281
+ const taggedValue = extractMarked(
282
+ TAGS_MARKER,
283
+ rel.techn,
284
+ rel.descr,
285
+ rel.sprite,
286
+ rel.tags,
287
+ rel.link
288
+ );
289
+ const linkValue = extractMarked(
290
+ LINK_MARKER,
291
+ rel.techn,
292
+ rel.descr,
293
+ rel.sprite,
294
+ rel.tags,
295
+ rel.link
296
+ );
297
+ const spriteNamedValue = extractMarked(
298
+ SPRITE_MARKER,
299
+ rel.techn,
300
+ rel.descr,
301
+ rel.sprite,
302
+ rel.tags,
303
+ rel.link
304
+ );
305
+ return {
306
+ to: rel.to,
307
+ description: cleanSlot(rel.label, ...ALL_MARKERS) || void 0,
308
+ technology: cleanSlot(rel.techn, ...ALL_MARKERS),
309
+ tags: taggedValue === void 0 ? parseCsvTags(
310
+ cleanSlot(rel.descr, ...ALL_MARKERS) || cleanSlot(rel.tags, ...ALL_MARKERS)
311
+ ) : parseCsvTags(taggedValue),
312
+ sprite: spriteNamedValue ?? cleanSlot(rel.sprite, ...ALL_MARKERS),
313
+ link: linkValue ?? cleanSlot(rel.link, ...ALL_MARKERS)
314
+ };
315
+ };
316
+ const buildBoundary = (el, childContainers, childBoundaries) => {
317
+ const taggedValue = extractMarked(TAGS_MARKER, el.tags, el.link);
318
+ const linkValue = extractMarked(LINK_MARKER, el.tags, el.link);
319
+ return {
320
+ name: el.alias,
321
+ label: el.label,
322
+ kind: parseBoundaryMacro(el.type_.name),
323
+ tags: taggedValue === void 0 ? parseCsvTags(cleanSlot(el.tags, ...ALL_MARKERS)) : parseCsvTags(taggedValue),
324
+ containerNames: childContainers,
325
+ boundaryNames: childBoundaries,
326
+ link: linkValue ?? cleanSlot(el.link, ...ALL_MARKERS)
327
+ };
328
+ };
329
+ const isC4Element = (el) => el instanceof Stdlib_C4_Context || el instanceof Stdlib_C4_Container_Component;
330
+ const collectBoundaryChildren = (el) => {
331
+ const containers = [];
332
+ const boundaries = [];
333
+ for (const child of el.elements) {
334
+ if (isC4Element(child)) containers.push(child.alias);
335
+ else if (child instanceof Stdlib_C4_Boundary) boundaries.push(child.alias);
336
+ }
337
+ return { containers, boundaries };
338
+ };
339
+ const pushRelation = (acc, sourceName, relation) => {
340
+ const source = acc[sourceName];
341
+ if (!source) return;
342
+ acc[sourceName] = {
343
+ ...source,
344
+ relations: [...source.relations, relation]
345
+ };
346
+ };
347
+ const populateRelations = (elements, acc) => {
348
+ for (const el of elements) {
349
+ if (!(el instanceof Stdlib_C4_Dynamic_Rel)) continue;
350
+ pushRelation(acc, el.from, buildRelation(el));
351
+ if (el.type_.name.startsWith("BiRel")) {
352
+ pushRelation(
353
+ acc,
354
+ el.to,
355
+ buildRelation({ ...el, from: el.to, to: el.from })
356
+ );
357
+ }
358
+ }
359
+ };
360
+ const collectChildBoundaryNames = (boundaryElements) => {
361
+ const childOfBoundary = /* @__PURE__ */ new Set();
362
+ for (const b of boundaryElements) {
363
+ for (const child of b.elements) {
364
+ if (child instanceof Stdlib_C4_Boundary) {
365
+ childOfBoundary.add(child.alias);
366
+ }
367
+ }
368
+ }
369
+ return childOfBoundary;
370
+ };
371
+ const load = async (filePath) => {
372
+ const filepath = path.resolve(filePath);
373
+ const raw = await fs.readFile(filepath, "utf8");
374
+ const transformed = preTransformNamedArgs(raw);
375
+ const [{ elements: rawElements }] = parse(transformed);
376
+ const elements = filterElements(rawElements);
377
+ normalizeRelBack(elements);
378
+ const containerByAlias = /* @__PURE__ */ Object.create(
379
+ null
380
+ );
381
+ for (const el of elements) {
382
+ if (isC4Element(el)) containerByAlias[el.alias] = buildContainer(el);
383
+ }
384
+ populateRelations(elements, containerByAlias);
385
+ const boundaryElements = elements.filter(
386
+ (el) => el instanceof Stdlib_C4_Boundary
387
+ );
388
+ const childOfBoundary = collectChildBoundaryNames(boundaryElements);
389
+ const boundaries = boundaryElements.map((b) => {
390
+ const { containers, boundaries: childBoundaries } = collectBoundaryChildren(b);
391
+ return buildBoundary(b, containers, childBoundaries);
392
+ });
393
+ const rootBoundaryNames = boundaries.map((b) => b.name).filter((name) => !childOfBoundary.has(name));
394
+ return buildModel({
395
+ containers: Object.values(containerByAlias),
396
+ boundaries,
397
+ rootBoundaryNames
398
+ });
399
+ };
400
+
401
+ const plantumlSyntax = {
402
+ containerPattern: (name) => `(${name},`,
403
+ containerDecl: (name, label, tags) => {
404
+ const tagsPart = tags ? `, "", "", $tags="${tags}"` : "";
405
+ return `Container(${name}, "${label}"${tagsPart})`;
406
+ },
407
+ relationPattern: (from, to) => `Rel(${from}, ${to}`,
408
+ relationDecl: (from, to, tech, tags) => {
409
+ const tagsPart = tags ? `, $tags="${tags}"` : "";
410
+ return `Rel(${from}, ${to}, "${tech ?? ""}"${tagsPart})`;
411
+ }
412
+ };
413
+
414
+ const plantumlFormat = {
415
+ name: "plantuml",
416
+ defaultPattern: "*.puml",
417
+ load,
418
+ generate,
419
+ fix: { syntax: plantumlSyntax }
420
+ };
421
+
422
+ export { plantumlFormat };