aact 1.0.0 → 2.0.1

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.
@@ -0,0 +1,744 @@
1
+ import * as v from 'valibot';
2
+ import YAML from 'yaml';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { Comment, Stdlib_C4_Dynamic_Rel, Relationship, parse, Stdlib_C4_Container_Component, Stdlib_C4_Context, Stdlib_C4_Boundary } from 'plantuml-parser';
6
+
7
+ const apiTechnologies = ["http", "grpc", "tcp"];
8
+ const allRelations = (model) => model.allContainers.flatMap(
9
+ (container) => container.relations.map((relation) => ({ from: container, relation }))
10
+ );
11
+ const boundaryContainsName = (boundary, name) => boundary.containers.some((c) => c.name === name);
12
+ const classifyRelation = (boundary, parentBoundary, from, relation, result, parentResult) => {
13
+ if (!boundaryContainsName(boundary, from.name)) return;
14
+ if (boundaryContainsName(boundary, relation.to.name)) {
15
+ result.cohesion++;
16
+ return;
17
+ }
18
+ const isInParentSibling = parentBoundary?.boundaries.some(
19
+ (b) => b.containers.some((c) => c.name === relation.to.name)
20
+ ) ?? false;
21
+ if (!parentBoundary || isInParentSibling) {
22
+ result.coupling++;
23
+ result.couplingRelations.push({ from: from.name, to: relation.to.name });
24
+ if (parentResult) parentResult.cohesion++;
25
+ } else if (parentResult) {
26
+ parentResult.coupling++;
27
+ parentResult.couplingRelations.push({
28
+ from: from.name,
29
+ to: relation.to.name
30
+ });
31
+ }
32
+ };
33
+ const analyzeModel = (model) => {
34
+ const relations = allRelations(model);
35
+ const asyncApiCalls = relations.filter(
36
+ (it) => it.relation.tags?.includes("async")
37
+ );
38
+ const syncApiCalls = relations.filter((it) => {
39
+ const isExternalApi = it.relation.to.type === "System_Ext";
40
+ const isApiTechnology = apiTechnologies.some(
41
+ (apiTechn) => (it.relation.technology ?? "").toLowerCase().includes(apiTechn)
42
+ );
43
+ return !it.relation.tags?.includes("async") && (isExternalApi || isApiTechnology);
44
+ });
45
+ const boundaryResults = /* @__PURE__ */ new Map();
46
+ for (const boundary of model.boundaries) {
47
+ boundaryResults.set(boundary.name, {
48
+ name: boundary.name,
49
+ label: boundary.label,
50
+ cohesion: 0,
51
+ coupling: 0,
52
+ couplingRelations: []
53
+ });
54
+ }
55
+ for (const boundary of model.boundaries) {
56
+ const parentBoundary = model.boundaries.find(
57
+ (b) => b.boundaries.some((child) => child.name === boundary.name)
58
+ );
59
+ const result = boundaryResults.get(boundary.name);
60
+ const parentResult = parentBoundary ? boundaryResults.get(parentBoundary.name) : void 0;
61
+ for (const { from, relation } of relations) {
62
+ classifyRelation(
63
+ boundary,
64
+ parentBoundary,
65
+ from,
66
+ relation,
67
+ result,
68
+ parentResult
69
+ );
70
+ }
71
+ }
72
+ return {
73
+ elementsCount: model.allContainers.length,
74
+ syncApiCalls: syncApiCalls.length,
75
+ asyncApiCalls: asyncApiCalls.length,
76
+ databases: analyzeDatabases(model),
77
+ boundaries: [...boundaryResults.values()]
78
+ };
79
+ };
80
+ const analyzeDatabases = (model) => {
81
+ const dbContainers = model.allContainers.filter(
82
+ (it) => it.type === "ContainerDb"
83
+ );
84
+ const dbRelations = model.allContainers.flatMap(
85
+ (container) => container.relations.filter(
86
+ (r) => dbContainers.some((db) => db.name === r.to.name)
87
+ )
88
+ );
89
+ return {
90
+ count: dbContainers.length,
91
+ consumes: dbRelations.length
92
+ };
93
+ };
94
+ const analyzeArchitecture = (model) => {
95
+ return {
96
+ model,
97
+ report: analyzeModel(model)
98
+ };
99
+ };
100
+
101
+ const ruleOption = (entries) => v.optional(v.union([v.boolean(), v.strictObject(entries)]));
102
+ const AactConfigSchema = v.strictObject({
103
+ source: v.strictObject({
104
+ type: v.picklist(["plantuml", "structurizr"]),
105
+ path: v.string()
106
+ }),
107
+ rules: v.optional(
108
+ v.strictObject({
109
+ acl: ruleOption({
110
+ tag: v.optional(v.string()),
111
+ externalType: v.optional(v.string())
112
+ }),
113
+ acyclic: v.optional(v.boolean()),
114
+ apiGateway: ruleOption({
115
+ aclTag: v.optional(v.string()),
116
+ externalType: v.optional(v.string()),
117
+ gatewayPattern: v.optional(v.instance(RegExp))
118
+ }),
119
+ crud: ruleOption({
120
+ repoTags: v.optional(v.array(v.string())),
121
+ dbType: v.optional(v.string())
122
+ }),
123
+ dbPerService: ruleOption({
124
+ dbType: v.optional(v.string())
125
+ }),
126
+ cohesion: ruleOption({
127
+ externalType: v.optional(v.string()),
128
+ internalType: v.optional(v.string())
129
+ }),
130
+ stableDependencies: ruleOption({
131
+ externalType: v.optional(v.string())
132
+ })
133
+ })
134
+ ),
135
+ generate: v.optional(
136
+ v.strictObject({
137
+ kubernetes: v.optional(
138
+ v.strictObject({
139
+ path: v.optional(v.string()),
140
+ exclude: v.optional(v.array(v.string()))
141
+ })
142
+ ),
143
+ boundaryLabel: v.optional(v.string())
144
+ })
145
+ )
146
+ });
147
+ const defineConfig = (config) => config;
148
+
149
+ const toKebab = (name) => name.replaceAll("_", "-");
150
+ const toEnvKey = (name) => name.replaceAll("-", "_").toUpperCase();
151
+ const buildEnvVar = (relation, sourceKebab, options) => {
152
+ const targetType = relation.to.type;
153
+ const targetKebab = toKebab(relation.to.name);
154
+ const targetUpper = toEnvKey(targetKebab);
155
+ if (targetType === "ContainerDb") {
156
+ const value = options.dbConnectionTemplate.replaceAll(
157
+ "{name}",
158
+ sourceKebab
159
+ );
160
+ return { key: "PG_CONNECTION_STRING", value };
161
+ }
162
+ if (relation.tags?.includes("async")) {
163
+ const value = relation.technology ?? targetKebab;
164
+ return { key: `KAFKA_${targetUpper}_TOPIC`, value };
165
+ }
166
+ if (targetType === "System_Ext") {
167
+ const value = relation.technology ?? `https://${targetKebab}`;
168
+ return { key: `${targetUpper}_BASE_URL`, value };
169
+ }
170
+ if (targetType === "Container") {
171
+ const value = relation.technology ?? `http://${targetKebab}:${options.defaultPort}`;
172
+ return { key: `${targetUpper}_BASE_URL`, value };
173
+ }
174
+ return void 0;
175
+ };
176
+ const generateKubernetes = (model, options) => {
177
+ const defaultPort = options?.defaultPort ?? 8080;
178
+ const dbConnectionTemplate = options?.dbConnectionTemplate ?? "postgresql://{name}:pass-{name}@postgresql:5432/{name}";
179
+ const resolvedOptions = { defaultPort, dbConnectionTemplate };
180
+ const containers = model.allContainers.filter(
181
+ (c) => c.type !== "ContainerDb" && c.type !== "System_Ext"
182
+ );
183
+ return containers.map((container) => {
184
+ const kebabName = toKebab(container.name);
185
+ const envEntries = [];
186
+ for (const relation of container.relations) {
187
+ const entry = buildEnvVar(relation, kebabName, resolvedOptions);
188
+ if (entry) {
189
+ envEntries.push(entry);
190
+ }
191
+ }
192
+ envEntries.sort((a, b) => a.key.localeCompare(b.key));
193
+ const doc = { name: kebabName };
194
+ if (envEntries.length > 0) {
195
+ const environment = {};
196
+ for (const { key, value } of envEntries) {
197
+ environment[key] = { default: value };
198
+ }
199
+ doc.environment = environment;
200
+ }
201
+ return {
202
+ fileName: `${kebabName}.yml`,
203
+ content: YAML.stringify(doc)
204
+ };
205
+ });
206
+ };
207
+
208
+ const containerTypeMap = {
209
+ Container: "Container",
210
+ ContainerDb: "ContainerDb",
211
+ System_Ext: "System_Ext",
212
+ Person: "Person"
213
+ };
214
+ const renderContainer = (container) => {
215
+ const type = containerTypeMap[container.type ?? "Container"] ?? "Container";
216
+ const tags = container.tags && container.tags.length > 0 ? `, $tags="${container.tags.join("+")}"` : "";
217
+ const desc = container.description ? `, "${container.description}"` : "";
218
+ return `${type}(${container.name}, "${container.label}"${desc}${tags})`;
219
+ };
220
+ const renderBoundary = (boundary, indent) => {
221
+ const inner = indent + " ";
222
+ const children = [
223
+ ...boundary.boundaries.map((child) => renderBoundary(child, inner)),
224
+ ...boundary.containers.map(
225
+ (container) => `${inner}${renderContainer(container)}`
226
+ )
227
+ ];
228
+ return [
229
+ `${indent}Boundary(${boundary.name}, "${boundary.label}") {`,
230
+ ...children,
231
+ `${indent}}`
232
+ ].join("\n");
233
+ };
234
+ const renderRelation = (container, relation) => {
235
+ const tech = relation.technology ? `, "${relation.technology}"` : "";
236
+ const tags = relation.tags && relation.tags.length > 0 ? `, $tags="${relation.tags.join("+")}"` : "";
237
+ return `Rel(${container.name}, ${relation.to.name}, ""${tech}${tags})`;
238
+ };
239
+ const collectBoundaryContainerNames = (boundaries) => {
240
+ const names = /* @__PURE__ */ new Set();
241
+ const collect = (boundary) => {
242
+ for (const c of boundary.containers) names.add(c.name);
243
+ for (const b of boundary.boundaries) collect(b);
244
+ };
245
+ for (const boundary of boundaries) collect(boundary);
246
+ return names;
247
+ };
248
+ const renderBody = (model, standaloneContainers, boundaryLabel) => {
249
+ if (boundaryLabel) {
250
+ return [
251
+ `Boundary(project, "${boundaryLabel}") {`,
252
+ ...model.boundaries.map((b) => renderBoundary(b, " ")),
253
+ ...standaloneContainers.map((c) => ` ${renderContainer(c)}`),
254
+ `}`
255
+ ];
256
+ }
257
+ return [
258
+ ...model.boundaries.map((b) => renderBoundary(b, "")),
259
+ ...standaloneContainers.map((c) => renderContainer(c))
260
+ ];
261
+ };
262
+ const generatePlantumlFromModel = (model, options) => {
263
+ const boundaryNames = collectBoundaryContainerNames(model.boundaries);
264
+ const standaloneContainers = model.allContainers.filter(
265
+ (c) => !boundaryNames.has(c.name)
266
+ );
267
+ const relations = model.allContainers.flatMap(
268
+ (container) => container.relations.map((rel) => renderRelation(container, rel))
269
+ );
270
+ return [
271
+ `@startuml`,
272
+ `!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml`,
273
+ `LAYOUT_WITH_LEGEND()`,
274
+ `AddRelTag("async", $lineStyle = DottedLine())`,
275
+ "",
276
+ ...renderBody(model, standaloneContainers, options?.boundaryLabel),
277
+ "",
278
+ ...relations,
279
+ "@enduml"
280
+ ].join("\n");
281
+ };
282
+
283
+ const filterElements = (elements) => {
284
+ const result = [];
285
+ for (const element of elements) {
286
+ if (element instanceof Comment) continue;
287
+ if (element.type_.name === "Container" || element.type_.name === "ContainerDb" || element.type_.name === "Component" || element.type_.name === "System_Ext" || element.type_.name === "System" || element.type_.name === "Person" || element instanceof Stdlib_C4_Dynamic_Rel || element instanceof Relationship) {
288
+ result.push(element);
289
+ }
290
+ const elementAsBoundary = element;
291
+ if (["System_Boundary", "Container_Boundary", "Boundary"].includes(
292
+ elementAsBoundary.type_.name
293
+ )) {
294
+ result.push(elementAsBoundary);
295
+ const resultFromBoundary = filterElements(elementAsBoundary.elements);
296
+ result.push(...resultFromBoundary);
297
+ }
298
+ if (Array.isArray(element)) {
299
+ const resultFromArray = filterElements(element);
300
+ result.push(...resultFromArray);
301
+ }
302
+ }
303
+ return result;
304
+ };
305
+
306
+ const loadPlantumlElements = async (filePath) => {
307
+ const filepath = path.resolve(filePath);
308
+ let data = await fs.readFile(filepath, "utf8");
309
+ data = data.replaceAll(/, \$tags=(".+?")/g, ", $1").replaceAll('""', '" "');
310
+ const [{ elements }] = parse(data);
311
+ for (const element of elements) {
312
+ if (element instanceof Comment) continue;
313
+ const relation = element;
314
+ if (relation.type_.name.startsWith("Rel_Back")) {
315
+ const from = relation.from;
316
+ relation.from = relation.to;
317
+ relation.to = from;
318
+ }
319
+ }
320
+ return filterElements(elements);
321
+ };
322
+
323
+ const addDependency = (containers, relation) => {
324
+ const containerFrom = containers.find((x) => x.name === relation.from);
325
+ if (!containerFrom) return;
326
+ const containerTo = containers.find((x) => x.name === relation.to);
327
+ if (!containerTo) return;
328
+ containerFrom.relations.push({
329
+ to: containerTo,
330
+ technology: relation.techn,
331
+ tags: relation.descr?.split(",").map((t) => t.trim())
332
+ });
333
+ };
334
+ const mapContainersFromPlantumlElements = (elements) => {
335
+ const containers = elements.filter(
336
+ (element) => element instanceof Stdlib_C4_Container_Component || element instanceof Stdlib_C4_Context
337
+ ).map((element) => {
338
+ const component = element;
339
+ return {
340
+ name: component.alias,
341
+ label: component.label,
342
+ type: component.type_.name,
343
+ relations: [],
344
+ tags: component.sprite ? [component.sprite] : void 0,
345
+ description: component.descr
346
+ };
347
+ });
348
+ for (const element of elements) {
349
+ if (element instanceof Stdlib_C4_Container_Component) {
350
+ continue;
351
+ }
352
+ if (element instanceof Stdlib_C4_Dynamic_Rel) {
353
+ addDependency(containers, element);
354
+ }
355
+ }
356
+ const boundaries = elements.filter((element) => element instanceof Stdlib_C4_Boundary).map((element) => {
357
+ const component = element;
358
+ return {
359
+ name: component.alias,
360
+ label: component.label,
361
+ type: component.type_.name,
362
+ boundaries: [],
363
+ containers: containers.filter(
364
+ (container) => component.elements.filter(
365
+ (element2) => element2 instanceof Stdlib_C4_Container_Component
366
+ ).some((e) => e.alias == container.name)
367
+ )
368
+ };
369
+ });
370
+ for (const boundary of boundaries) {
371
+ const component = elements.find(
372
+ (element) => element instanceof Stdlib_C4_Boundary && element.alias == boundary.name
373
+ );
374
+ boundary.boundaries = boundaries.filter(
375
+ (b) => component.elements.filter((element) => element instanceof Stdlib_C4_Boundary).some((e) => e.alias == b.name)
376
+ );
377
+ }
378
+ return {
379
+ allContainers: containers.toSorted((a, b) => a.name.localeCompare(b.name)),
380
+ boundaries
381
+ };
382
+ };
383
+
384
+ const DATABASE_TECHNOLOGIES = [
385
+ "postgresql",
386
+ "postgres",
387
+ "mysql",
388
+ "mariadb",
389
+ "mongodb",
390
+ "mongo",
391
+ "redis",
392
+ "elasticsearch",
393
+ "dynamodb",
394
+ "cassandra",
395
+ "sqlite",
396
+ "oracle",
397
+ "sqlserver",
398
+ "mssql",
399
+ "database",
400
+ "db"
401
+ ];
402
+ const isDatabase = (technology, name) => {
403
+ const techLower = technology?.toLowerCase() ?? "";
404
+ const nameLower = name?.toLowerCase() ?? "";
405
+ if (DATABASE_TECHNOLOGIES.some((db) => techLower.includes(db))) {
406
+ return true;
407
+ }
408
+ if (nameLower.endsWith(" db") || nameLower.endsWith("_db") || nameLower.endsWith("database")) {
409
+ return true;
410
+ }
411
+ return false;
412
+ };
413
+ const enrichTags = (existingTags, name) => {
414
+ const tags = existingTags?.split(",").map((t) => t.trim()).filter(Boolean) ?? [];
415
+ const nameLower = name?.toLowerCase() ?? "";
416
+ if (nameLower.includes("crud") && !tags.includes("repo")) {
417
+ tags.push("repo");
418
+ }
419
+ if (nameLower.includes("acl") && !tags.includes("acl")) {
420
+ tags.push("acl");
421
+ }
422
+ return tags;
423
+ };
424
+ const loadStructurizrWorkspace = async (filePath) => {
425
+ const filepath = path.resolve(filePath);
426
+ const data = await fs.readFile(filepath, "utf8");
427
+ return JSON.parse(data);
428
+ };
429
+ const processExternalSystem = (system, registry) => {
430
+ const container = {
431
+ name: system.id,
432
+ label: system.name,
433
+ type: "System_Ext",
434
+ tags: system.tags?.split(",").map((t) => t.trim()).filter(Boolean),
435
+ description: system.description ?? "",
436
+ relations: []
437
+ };
438
+ registry.containers.push(container);
439
+ registry.allElements.set(system.id, container);
440
+ };
441
+ const processInternalSystem = (system, registry) => {
442
+ const systemContainers = [];
443
+ for (const cont of system.containers ?? []) {
444
+ const container = {
445
+ name: cont.id,
446
+ label: cont.name,
447
+ type: isDatabase(cont.technology, cont.name) ? "ContainerDb" : "Container",
448
+ tags: enrichTags(cont.tags, cont.name),
449
+ description: cont.description ?? "",
450
+ relations: []
451
+ };
452
+ systemContainers.push(container);
453
+ registry.containers.push(container);
454
+ registry.allElements.set(cont.id, container);
455
+ }
456
+ registry.boundaries.push({
457
+ name: system.id,
458
+ label: system.name,
459
+ type: "Boundary",
460
+ boundaries: [],
461
+ containers: systemContainers
462
+ });
463
+ };
464
+ const addRelations = (allElements, sourceId, relationships) => {
465
+ const sourceContainer = allElements.get(sourceId);
466
+ if (!sourceContainer || !relationships) return;
467
+ for (const rel of relationships) {
468
+ const targetContainer = allElements.get(rel.destinationId);
469
+ if (!targetContainer) continue;
470
+ let tags = rel.tags?.split(",").map((t) => t.trim());
471
+ if (rel.interactionStyle === "Asynchronous") {
472
+ tags = [...tags ?? [], "async"];
473
+ }
474
+ const relation = {
475
+ to: targetContainer,
476
+ technology: rel.technology,
477
+ tags
478
+ };
479
+ sourceContainer.relations.push(relation);
480
+ }
481
+ };
482
+ const mapContainersFromStructurizr = (workspace) => {
483
+ const registry = {
484
+ allElements: /* @__PURE__ */ new Map(),
485
+ containers: [],
486
+ boundaries: []
487
+ };
488
+ for (const system of workspace.model.softwareSystems ?? []) {
489
+ if (system.location === "External") {
490
+ processExternalSystem(system, registry);
491
+ } else {
492
+ processInternalSystem(system, registry);
493
+ }
494
+ }
495
+ for (const person of workspace.model.people ?? []) {
496
+ const container = {
497
+ name: person.id,
498
+ label: person.name,
499
+ type: "Person",
500
+ tags: person.tags?.split(",").map((t) => t.trim()).filter(Boolean),
501
+ description: person.description ?? "",
502
+ relations: []
503
+ };
504
+ registry.containers.push(container);
505
+ registry.allElements.set(person.id, container);
506
+ }
507
+ for (const system of workspace.model.softwareSystems ?? []) {
508
+ addRelations(registry.allElements, system.id, system.relationships);
509
+ for (const cont of system.containers ?? []) {
510
+ addRelations(registry.allElements, cont.id, cont.relationships);
511
+ for (const comp of cont.components ?? []) {
512
+ addRelations(registry.allElements, comp.id, comp.relationships);
513
+ }
514
+ }
515
+ }
516
+ for (const person of workspace.model.people ?? []) {
517
+ addRelations(registry.allElements, person.id, person.relationships);
518
+ }
519
+ return {
520
+ allContainers: registry.containers.toSorted(
521
+ (a, b) => a.name.localeCompare(b.name)
522
+ ),
523
+ boundaries: registry.boundaries
524
+ };
525
+ };
526
+ const loadStructurizrElements = async (filePath) => {
527
+ const workspace = await loadStructurizrWorkspace(filePath);
528
+ return mapContainersFromStructurizr(workspace);
529
+ };
530
+
531
+ const checkAcl = (containers, options) => {
532
+ const tag = options?.tag ?? "acl";
533
+ const externalType = options?.externalType ?? "System_Ext";
534
+ const violations = [];
535
+ for (const container of containers) {
536
+ const externalRelations = container.relations.filter(
537
+ (r) => r.to.type === externalType
538
+ );
539
+ if (!container.tags?.includes(tag) && externalRelations.length > 0) {
540
+ violations.push({
541
+ container: container.name,
542
+ message: `depends on external systems: ${externalRelations.map((r) => r.to.name).join(", ")}`
543
+ });
544
+ }
545
+ }
546
+ return violations;
547
+ };
548
+
549
+ const checkAcyclic = (containers) => {
550
+ const violations = [];
551
+ const findCycle = (relations, sourceContainerName, visited = /* @__PURE__ */ new Set()) => {
552
+ for (const rel of relations) {
553
+ if (rel.to.name === sourceContainerName) {
554
+ return true;
555
+ }
556
+ if (visited.has(rel.to.name)) continue;
557
+ visited.add(rel.to.name);
558
+ if (findCycle(rel.to.relations, sourceContainerName, visited)) {
559
+ return true;
560
+ }
561
+ }
562
+ return false;
563
+ };
564
+ for (const container of containers) {
565
+ if (findCycle(container.relations, container.name)) {
566
+ violations.push({
567
+ container: container.name,
568
+ message: "participates in a dependency cycle"
569
+ });
570
+ }
571
+ }
572
+ return violations;
573
+ };
574
+
575
+ const checkApiGateway = (containers, options) => {
576
+ const aclTag = options?.aclTag ?? "acl";
577
+ const externalType = options?.externalType ?? "System_Ext";
578
+ const gatewayPattern = options?.gatewayPattern ?? /gateway/i;
579
+ const violations = [];
580
+ for (const container of containers) {
581
+ if (!container.tags?.includes(aclTag)) continue;
582
+ for (const rel of container.relations) {
583
+ if (rel.to.type !== externalType) continue;
584
+ const techs = rel.technology?.split(", ") ?? [];
585
+ if (!techs.some((t) => gatewayPattern.test(t))) {
586
+ violations.push({
587
+ container: container.name,
588
+ message: `external relation to ${rel.to.name} does not go through API Gateway`
589
+ });
590
+ }
591
+ }
592
+ }
593
+ return violations;
594
+ };
595
+
596
+ const getBoundaryCohesion = (boundary) => {
597
+ let result = 0;
598
+ for (const container of boundary.containers) {
599
+ result += container.relations.filter(
600
+ (r) => boundary.containers.some((c) => c.name === r.to.name)
601
+ ).length;
602
+ }
603
+ for (const innerBoundary of boundary.boundaries) {
604
+ result += getBoundaryCoupling(innerBoundary);
605
+ }
606
+ return result;
607
+ };
608
+ const getBoundaryCoupling = (boundary, externalType = "System_Ext", internalType = "Container") => {
609
+ let result = 0;
610
+ for (const container of boundary.containers) {
611
+ result += container.relations.filter(
612
+ (r) => r.to.type === internalType && !boundary.containers.some((c) => c.name === r.to.name)
613
+ ).length;
614
+ }
615
+ for (const innerBoundary of boundary.boundaries) {
616
+ for (const container of innerBoundary.containers) {
617
+ result += container.relations.filter(
618
+ (r) => r.to.type === externalType
619
+ ).length;
620
+ }
621
+ }
622
+ return result;
623
+ };
624
+ const checkCohesion = (model, options) => {
625
+ const externalType = options?.externalType ?? "System_Ext";
626
+ const internalType = options?.internalType ?? "Container";
627
+ const violations = [];
628
+ for (const boundary of model.boundaries) {
629
+ const cohesion = getBoundaryCohesion(boundary);
630
+ const coupling = getBoundaryCoupling(boundary, externalType, internalType);
631
+ if (cohesion <= coupling) {
632
+ violations.push({
633
+ container: boundary.name,
634
+ message: `cohesion (${cohesion}) is not greater than coupling (${coupling})`
635
+ });
636
+ }
637
+ if (boundary.boundaries.length > 0) {
638
+ const innerCohesionSum = boundary.boundaries.reduce(
639
+ (sum, current) => sum + getBoundaryCohesion(current),
640
+ 0
641
+ );
642
+ if (cohesion >= innerCohesionSum) {
643
+ violations.push({
644
+ container: boundary.name,
645
+ message: `cohesion (${cohesion}) is not less than sum of inner boundary cohesions (${innerCohesionSum})`
646
+ });
647
+ }
648
+ }
649
+ }
650
+ return violations;
651
+ };
652
+
653
+ const checkCrud = (containers, options) => {
654
+ const repoTags = options?.repoTags ?? ["repo", "relay"];
655
+ const dbType = options?.dbType ?? "ContainerDb";
656
+ const violations = [];
657
+ for (const container of containers) {
658
+ const dbRelations = container.relations.filter((r) => r.to.type === dbType);
659
+ const isRepo = repoTags.some((tag) => container.tags?.includes(tag));
660
+ if (!isRepo && dbRelations.length > 0) {
661
+ violations.push({
662
+ container: container.name,
663
+ message: `accesses database without repo/relay tag: ${dbRelations.map((r) => r.to.name).join(", ")}`
664
+ });
665
+ }
666
+ if (container.tags?.includes("repo") && container.relations.some((r) => r.to.type !== dbType)) {
667
+ violations.push({
668
+ container: container.name,
669
+ message: `repo has non-database dependencies: ${container.relations.filter((r) => r.to.type !== dbType).map((r) => r.to.name).join(", ")}`
670
+ });
671
+ }
672
+ }
673
+ return violations;
674
+ };
675
+
676
+ const checkDbPerService = (containers, options) => {
677
+ const dbType = options?.dbType ?? "ContainerDb";
678
+ const violations = [];
679
+ const dbAccessMap = /* @__PURE__ */ new Map();
680
+ for (const container of containers) {
681
+ for (const rel of container.relations) {
682
+ if (rel.to.type === dbType) {
683
+ const accessors = dbAccessMap.get(rel.to.name) ?? [];
684
+ accessors.push(container.name);
685
+ dbAccessMap.set(rel.to.name, accessors);
686
+ }
687
+ }
688
+ }
689
+ for (const [db, accessors] of dbAccessMap) {
690
+ if (accessors.length > 1) {
691
+ violations.push({
692
+ container: db,
693
+ message: `accessed by multiple services: ${accessors.join(", ")}`
694
+ });
695
+ }
696
+ }
697
+ return violations;
698
+ };
699
+
700
+ const computeCoupling = (internal, internalNames) => {
701
+ const ca = /* @__PURE__ */ new Map();
702
+ const ce = /* @__PURE__ */ new Map();
703
+ for (const c of internal) {
704
+ ca.set(c.name, 0);
705
+ ce.set(c.name, 0);
706
+ }
707
+ for (const c of internal) {
708
+ for (const rel of c.relations) {
709
+ if (!internalNames.has(rel.to.name)) continue;
710
+ ce.set(c.name, ce.get(c.name) + 1);
711
+ ca.set(rel.to.name, ca.get(rel.to.name) + 1);
712
+ }
713
+ }
714
+ return { ca, ce };
715
+ };
716
+ const checkStableDependencies = (containers, options) => {
717
+ const externalType = options?.externalType ?? "System_Ext";
718
+ const violations = [];
719
+ const internal = containers.filter((c) => c.type !== externalType);
720
+ const internalNames = new Set(internal.map((c) => c.name));
721
+ const { ca, ce } = computeCoupling(internal, internalNames);
722
+ const instability = (name) => {
723
+ const afferent = ca.get(name);
724
+ const efferent = ce.get(name);
725
+ if (afferent + efferent === 0) return 1;
726
+ return efferent / (afferent + efferent);
727
+ };
728
+ for (const c of internal) {
729
+ for (const rel of c.relations) {
730
+ if (!internalNames.has(rel.to.name)) continue;
731
+ const iSource = instability(c.name);
732
+ const iTarget = instability(rel.to.name);
733
+ if (iSource < iTarget) {
734
+ violations.push({
735
+ container: c.name,
736
+ message: `depends on less stable ${rel.to.name} (I=${iSource.toFixed(2)} \u2192 I=${iTarget.toFixed(2)})`
737
+ });
738
+ }
739
+ }
740
+ }
741
+ return violations;
742
+ };
743
+
744
+ export { AactConfigSchema as A, analyzeArchitecture as a, checkAcyclic as b, checkAcl as c, checkApiGateway as d, checkCohesion as e, checkCrud as f, checkDbPerService as g, checkStableDependencies as h, defineConfig as i, generateKubernetes as j, generatePlantumlFromModel as k, loadPlantumlElements as l, loadStructurizrElements as m, loadStructurizrWorkspace as n, mapContainersFromPlantumlElements as o, mapContainersFromStructurizr as p };