aact 2.0.1 → 2.1.0
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 +1 -1
- package/dist/cli/index.mjs +489 -138
- package/dist/index.d.mts +60 -4
- package/dist/index.d.ts +60 -4
- package/dist/index.mjs +22 -21
- package/dist/shared/{aact.Dqryafrg.mjs → aact.mCX-x14V.mjs} +214 -82
- package/package.json +2 -1
|
@@ -4,20 +4,27 @@ import fs from 'node:fs/promises';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { Comment, Stdlib_C4_Dynamic_Rel, Relationship, parse, Stdlib_C4_Container_Component, Stdlib_C4_Context, Stdlib_C4_Boundary } from 'plantuml-parser';
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const EXTERNAL_SYSTEM_TYPE = "System_Ext";
|
|
8
|
+
const SYSTEM_TYPE = "System";
|
|
9
|
+
const CONTAINER_TYPE = "Container";
|
|
10
|
+
const CONTAINER_DB_TYPE = "ContainerDb";
|
|
11
|
+
const COMPONENT_TYPE = "Component";
|
|
12
|
+
const BOUNDARY_TYPE = "Boundary";
|
|
13
|
+
const SYSTEM_BOUNDARY_TYPE = "System_Boundary";
|
|
14
|
+
const CONTAINER_BOUNDARY_TYPE = "Container_Boundary";
|
|
15
|
+
const PERSON_TYPE = "Person";
|
|
16
|
+
|
|
17
|
+
const DEFAULT_API_TECHNOLOGIES = ["http", "grpc", "tcp"];
|
|
8
18
|
const allRelations = (model) => model.allContainers.flatMap(
|
|
9
19
|
(container) => container.relations.map((relation) => ({ from: container, relation }))
|
|
10
20
|
);
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
if (
|
|
14
|
-
if (boundaryContainsName(boundary, relation.to.name)) {
|
|
21
|
+
const classifyRelation = (names, childNames, parentBoundary, from, relation, result, parentResult) => {
|
|
22
|
+
if (!names.has(from.name)) return;
|
|
23
|
+
if (names.has(relation.to.name)) {
|
|
15
24
|
result.cohesion++;
|
|
16
25
|
return;
|
|
17
26
|
}
|
|
18
|
-
const isInParentSibling =
|
|
19
|
-
(b) => b.containers.some((c) => c.name === relation.to.name)
|
|
20
|
-
) ?? false;
|
|
27
|
+
const isInParentSibling = childNames?.has(relation.to.name) ?? false;
|
|
21
28
|
if (!parentBoundary || isInParentSibling) {
|
|
22
29
|
result.coupling++;
|
|
23
30
|
result.couplingRelations.push({ from: from.name, to: relation.to.name });
|
|
@@ -30,18 +37,51 @@ const classifyRelation = (boundary, parentBoundary, from, relation, result, pare
|
|
|
30
37
|
});
|
|
31
38
|
}
|
|
32
39
|
};
|
|
33
|
-
const
|
|
40
|
+
const buildBoundaryLookups = (boundaries) => {
|
|
41
|
+
const nameSets = new Map(
|
|
42
|
+
boundaries.map((b) => [b.name, new Set(b.containers.map((c) => c.name))])
|
|
43
|
+
);
|
|
44
|
+
const parentMap = /* @__PURE__ */ new Map();
|
|
45
|
+
for (const b of boundaries) {
|
|
46
|
+
for (const child of b.boundaries) parentMap.set(child.name, b);
|
|
47
|
+
}
|
|
48
|
+
const result = /* @__PURE__ */ new Map();
|
|
49
|
+
for (const b of boundaries) {
|
|
50
|
+
const parentBoundary = parentMap.get(b.name);
|
|
51
|
+
let childNames;
|
|
52
|
+
if (parentBoundary) {
|
|
53
|
+
childNames = /* @__PURE__ */ new Set();
|
|
54
|
+
for (const sibling of parentBoundary.boundaries) {
|
|
55
|
+
for (const c of sibling.containers) childNames.add(c.name);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
result.set(b.name, {
|
|
59
|
+
nameSet: nameSets.get(b.name),
|
|
60
|
+
childNames,
|
|
61
|
+
parentBoundary
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
};
|
|
66
|
+
const isSyncApiCall = (it, externalType, apiTechnologies) => {
|
|
67
|
+
if (it.relation.tags?.includes("async")) return false;
|
|
68
|
+
if (it.relation.to.type === externalType) return true;
|
|
69
|
+
return apiTechnologies.some(
|
|
70
|
+
(t) => (it.relation.technology ?? "").toLowerCase().includes(t)
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
const analyzeModel = (model, options) => {
|
|
74
|
+
const apiTechnologies = options?.apiTechnologies ?? DEFAULT_API_TECHNOLOGIES;
|
|
75
|
+
const externalType = options?.externalType ?? EXTERNAL_SYSTEM_TYPE;
|
|
76
|
+
const dbType = options?.dbType ?? CONTAINER_DB_TYPE;
|
|
34
77
|
const relations = allRelations(model);
|
|
35
78
|
const asyncApiCalls = relations.filter(
|
|
36
79
|
(it) => it.relation.tags?.includes("async")
|
|
37
80
|
);
|
|
38
|
-
const syncApiCalls = relations.filter(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
);
|
|
43
|
-
return !it.relation.tags?.includes("async") && (isExternalApi || isApiTechnology);
|
|
44
|
-
});
|
|
81
|
+
const syncApiCalls = relations.filter(
|
|
82
|
+
(it) => isSyncApiCall(it, externalType, apiTechnologies)
|
|
83
|
+
);
|
|
84
|
+
const lookups = buildBoundaryLookups(model.boundaries);
|
|
45
85
|
const boundaryResults = /* @__PURE__ */ new Map();
|
|
46
86
|
for (const boundary of model.boundaries) {
|
|
47
87
|
boundaryResults.set(boundary.name, {
|
|
@@ -53,14 +93,13 @@ const analyzeModel = (model) => {
|
|
|
53
93
|
});
|
|
54
94
|
}
|
|
55
95
|
for (const boundary of model.boundaries) {
|
|
56
|
-
const parentBoundary =
|
|
57
|
-
(b) => b.boundaries.some((child) => child.name === boundary.name)
|
|
58
|
-
);
|
|
96
|
+
const { nameSet, childNames, parentBoundary } = lookups.get(boundary.name);
|
|
59
97
|
const result = boundaryResults.get(boundary.name);
|
|
60
98
|
const parentResult = parentBoundary ? boundaryResults.get(parentBoundary.name) : void 0;
|
|
61
99
|
for (const { from, relation } of relations) {
|
|
62
100
|
classifyRelation(
|
|
63
|
-
|
|
101
|
+
nameSet,
|
|
102
|
+
childNames,
|
|
64
103
|
parentBoundary,
|
|
65
104
|
from,
|
|
66
105
|
relation,
|
|
@@ -73,28 +112,29 @@ const analyzeModel = (model) => {
|
|
|
73
112
|
elementsCount: model.allContainers.length,
|
|
74
113
|
syncApiCalls: syncApiCalls.length,
|
|
75
114
|
asyncApiCalls: asyncApiCalls.length,
|
|
76
|
-
databases: analyzeDatabases(model),
|
|
115
|
+
databases: analyzeDatabases(model, dbType),
|
|
77
116
|
boundaries: [...boundaryResults.values()]
|
|
78
117
|
};
|
|
79
118
|
};
|
|
80
|
-
const analyzeDatabases = (model) => {
|
|
81
|
-
const
|
|
82
|
-
(it) => it.type ===
|
|
83
|
-
);
|
|
84
|
-
const dbRelations = model.allContainers.flatMap(
|
|
85
|
-
(container) => container.relations.filter(
|
|
86
|
-
(r) => dbContainers.some((db) => db.name === r.to.name)
|
|
87
|
-
)
|
|
119
|
+
const analyzeDatabases = (model, dbType) => {
|
|
120
|
+
const dbNames = new Set(
|
|
121
|
+
model.allContainers.filter((it) => it.type === dbType).map((it) => it.name)
|
|
88
122
|
);
|
|
123
|
+
let consumes = 0;
|
|
124
|
+
for (const container of model.allContainers) {
|
|
125
|
+
for (const r of container.relations) {
|
|
126
|
+
if (dbNames.has(r.to.name)) consumes++;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
89
129
|
return {
|
|
90
|
-
count:
|
|
91
|
-
consumes
|
|
130
|
+
count: dbNames.size,
|
|
131
|
+
consumes
|
|
92
132
|
};
|
|
93
133
|
};
|
|
94
|
-
const analyzeArchitecture = (model) => {
|
|
134
|
+
const analyzeArchitecture = (model, options) => {
|
|
95
135
|
return {
|
|
96
136
|
model,
|
|
97
|
-
report: analyzeModel(model)
|
|
137
|
+
report: analyzeModel(model, options)
|
|
98
138
|
};
|
|
99
139
|
};
|
|
100
140
|
|
|
@@ -102,7 +142,8 @@ const ruleOption = (entries) => v.optional(v.union([v.boolean(), v.strictObject(
|
|
|
102
142
|
const AactConfigSchema = v.strictObject({
|
|
103
143
|
source: v.strictObject({
|
|
104
144
|
type: v.picklist(["plantuml", "structurizr"]),
|
|
105
|
-
path: v.string()
|
|
145
|
+
path: v.string(),
|
|
146
|
+
writePath: v.optional(v.string())
|
|
106
147
|
}),
|
|
107
148
|
rules: v.optional(
|
|
108
149
|
v.strictObject({
|
|
@@ -121,7 +162,8 @@ const AactConfigSchema = v.strictObject({
|
|
|
121
162
|
dbType: v.optional(v.string())
|
|
122
163
|
}),
|
|
123
164
|
dbPerService: ruleOption({
|
|
124
|
-
dbType: v.optional(v.string())
|
|
165
|
+
dbType: v.optional(v.string()),
|
|
166
|
+
ownerTags: v.optional(v.array(v.string()))
|
|
125
167
|
}),
|
|
126
168
|
cohesion: ruleOption({
|
|
127
169
|
externalType: v.optional(v.string()),
|
|
@@ -129,7 +171,8 @@ const AactConfigSchema = v.strictObject({
|
|
|
129
171
|
}),
|
|
130
172
|
stableDependencies: ruleOption({
|
|
131
173
|
externalType: v.optional(v.string())
|
|
132
|
-
})
|
|
174
|
+
}),
|
|
175
|
+
commonReuse: v.optional(v.boolean())
|
|
133
176
|
})
|
|
134
177
|
),
|
|
135
178
|
generate: v.optional(
|
|
@@ -152,7 +195,7 @@ const buildEnvVar = (relation, sourceKebab, options) => {
|
|
|
152
195
|
const targetType = relation.to.type;
|
|
153
196
|
const targetKebab = toKebab(relation.to.name);
|
|
154
197
|
const targetUpper = toEnvKey(targetKebab);
|
|
155
|
-
if (targetType ===
|
|
198
|
+
if (targetType === CONTAINER_DB_TYPE) {
|
|
156
199
|
const value = options.dbConnectionTemplate.replaceAll(
|
|
157
200
|
"{name}",
|
|
158
201
|
sourceKebab
|
|
@@ -163,11 +206,11 @@ const buildEnvVar = (relation, sourceKebab, options) => {
|
|
|
163
206
|
const value = relation.technology ?? targetKebab;
|
|
164
207
|
return { key: `KAFKA_${targetUpper}_TOPIC`, value };
|
|
165
208
|
}
|
|
166
|
-
if (targetType ===
|
|
209
|
+
if (targetType === EXTERNAL_SYSTEM_TYPE) {
|
|
167
210
|
const value = relation.technology ?? `https://${targetKebab}`;
|
|
168
211
|
return { key: `${targetUpper}_BASE_URL`, value };
|
|
169
212
|
}
|
|
170
|
-
if (targetType ===
|
|
213
|
+
if (targetType === CONTAINER_TYPE) {
|
|
171
214
|
const value = relation.technology ?? `http://${targetKebab}:${options.defaultPort}`;
|
|
172
215
|
return { key: `${targetUpper}_BASE_URL`, value };
|
|
173
216
|
}
|
|
@@ -178,7 +221,7 @@ const generateKubernetes = (model, options) => {
|
|
|
178
221
|
const dbConnectionTemplate = options?.dbConnectionTemplate ?? "postgresql://{name}:pass-{name}@postgresql:5432/{name}";
|
|
179
222
|
const resolvedOptions = { defaultPort, dbConnectionTemplate };
|
|
180
223
|
const containers = model.allContainers.filter(
|
|
181
|
-
(c) => c.type !==
|
|
224
|
+
(c) => c.type !== CONTAINER_DB_TYPE && c.type !== EXTERNAL_SYSTEM_TYPE
|
|
182
225
|
);
|
|
183
226
|
return containers.map((container) => {
|
|
184
227
|
const kebabName = toKebab(container.name);
|
|
@@ -205,14 +248,24 @@ const generateKubernetes = (model, options) => {
|
|
|
205
248
|
});
|
|
206
249
|
};
|
|
207
250
|
|
|
251
|
+
const PLANTUML_CONTAINER = "Container";
|
|
252
|
+
const PLANTUML_CONTAINER_DB = "ContainerDb";
|
|
253
|
+
const PLANTUML_SYSTEM_EXT = "System_Ext";
|
|
254
|
+
const PLANTUML_SYSTEM = "System";
|
|
255
|
+
const PLANTUML_PERSON = "Person";
|
|
256
|
+
const PLANTUML_COMPONENT = "Component";
|
|
257
|
+
const PLANTUML_SYSTEM_BOUNDARY = "System_Boundary";
|
|
258
|
+
const PLANTUML_CONTAINER_BOUNDARY = "Container_Boundary";
|
|
259
|
+
const PLANTUML_BOUNDARY = "Boundary";
|
|
260
|
+
|
|
208
261
|
const containerTypeMap = {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
262
|
+
[CONTAINER_TYPE]: PLANTUML_CONTAINER,
|
|
263
|
+
[CONTAINER_DB_TYPE]: PLANTUML_CONTAINER_DB,
|
|
264
|
+
[EXTERNAL_SYSTEM_TYPE]: PLANTUML_SYSTEM_EXT,
|
|
265
|
+
[PERSON_TYPE]: PLANTUML_PERSON
|
|
213
266
|
};
|
|
214
267
|
const renderContainer = (container) => {
|
|
215
|
-
const type = containerTypeMap[container.type ??
|
|
268
|
+
const type = containerTypeMap[container.type ?? CONTAINER_TYPE] ?? PLANTUML_CONTAINER;
|
|
216
269
|
const tags = container.tags && container.tags.length > 0 ? `, $tags="${container.tags.join("+")}"` : "";
|
|
217
270
|
const desc = container.description ? `, "${container.description}"` : "";
|
|
218
271
|
return `${type}(${container.name}, "${container.label}"${desc}${tags})`;
|
|
@@ -284,13 +337,15 @@ const filterElements = (elements) => {
|
|
|
284
337
|
const result = [];
|
|
285
338
|
for (const element of elements) {
|
|
286
339
|
if (element instanceof Comment) continue;
|
|
287
|
-
if (element.type_.name ===
|
|
340
|
+
if (element.type_.name === PLANTUML_CONTAINER || element.type_.name === PLANTUML_CONTAINER_DB || element.type_.name === PLANTUML_COMPONENT || element.type_.name === PLANTUML_SYSTEM_EXT || element.type_.name === PLANTUML_SYSTEM || element.type_.name === PLANTUML_PERSON || element instanceof Stdlib_C4_Dynamic_Rel || element instanceof Relationship) {
|
|
288
341
|
result.push(element);
|
|
289
342
|
}
|
|
290
343
|
const elementAsBoundary = element;
|
|
291
|
-
if ([
|
|
292
|
-
|
|
293
|
-
|
|
344
|
+
if ([
|
|
345
|
+
PLANTUML_SYSTEM_BOUNDARY,
|
|
346
|
+
PLANTUML_CONTAINER_BOUNDARY,
|
|
347
|
+
PLANTUML_BOUNDARY
|
|
348
|
+
].includes(elementAsBoundary.type_.name)) {
|
|
294
349
|
result.push(elementAsBoundary);
|
|
295
350
|
const resultFromBoundary = filterElements(elementAsBoundary.elements);
|
|
296
351
|
result.push(...resultFromBoundary);
|
|
@@ -381,6 +436,11 @@ const mapContainersFromPlantumlElements = (elements) => {
|
|
|
381
436
|
};
|
|
382
437
|
};
|
|
383
438
|
|
|
439
|
+
const STRUCTURIZR_LOCATION_EXTERNAL = "External";
|
|
440
|
+
const STRUCTURIZR_INTERACTION_ASYNC = "Asynchronous";
|
|
441
|
+
const STRUCTURIZR_TAG_ASYNC = "async";
|
|
442
|
+
|
|
443
|
+
const dslId = (id, properties) => properties?.["structurizr.dsl.identifier"] ?? id;
|
|
384
444
|
const DATABASE_TECHNOLOGIES = [
|
|
385
445
|
"postgresql",
|
|
386
446
|
"postgres",
|
|
@@ -428,9 +488,9 @@ const loadStructurizrWorkspace = async (filePath) => {
|
|
|
428
488
|
};
|
|
429
489
|
const processExternalSystem = (system, registry) => {
|
|
430
490
|
const container = {
|
|
431
|
-
name: system.id,
|
|
491
|
+
name: dslId(system.id, system.properties),
|
|
432
492
|
label: system.name,
|
|
433
|
-
type:
|
|
493
|
+
type: EXTERNAL_SYSTEM_TYPE,
|
|
434
494
|
tags: system.tags?.split(",").map((t) => t.trim()).filter(Boolean),
|
|
435
495
|
description: system.description ?? "",
|
|
436
496
|
relations: []
|
|
@@ -442,9 +502,9 @@ const processInternalSystem = (system, registry) => {
|
|
|
442
502
|
const systemContainers = [];
|
|
443
503
|
for (const cont of system.containers ?? []) {
|
|
444
504
|
const container = {
|
|
445
|
-
name: cont.id,
|
|
505
|
+
name: dslId(cont.id, cont.properties),
|
|
446
506
|
label: cont.name,
|
|
447
|
-
type: isDatabase(cont.technology, cont.name) ?
|
|
507
|
+
type: isDatabase(cont.technology, cont.name) ? CONTAINER_DB_TYPE : CONTAINER_TYPE,
|
|
448
508
|
tags: enrichTags(cont.tags, cont.name),
|
|
449
509
|
description: cont.description ?? "",
|
|
450
510
|
relations: []
|
|
@@ -454,9 +514,9 @@ const processInternalSystem = (system, registry) => {
|
|
|
454
514
|
registry.allElements.set(cont.id, container);
|
|
455
515
|
}
|
|
456
516
|
registry.boundaries.push({
|
|
457
|
-
name: system.id,
|
|
517
|
+
name: dslId(system.id, system.properties),
|
|
458
518
|
label: system.name,
|
|
459
|
-
type:
|
|
519
|
+
type: BOUNDARY_TYPE,
|
|
460
520
|
boundaries: [],
|
|
461
521
|
containers: systemContainers
|
|
462
522
|
});
|
|
@@ -468,12 +528,12 @@ const addRelations = (allElements, sourceId, relationships) => {
|
|
|
468
528
|
const targetContainer = allElements.get(rel.destinationId);
|
|
469
529
|
if (!targetContainer) continue;
|
|
470
530
|
let tags = rel.tags?.split(",").map((t) => t.trim());
|
|
471
|
-
if (rel.interactionStyle ===
|
|
472
|
-
tags = [...tags ?? [],
|
|
531
|
+
if (rel.interactionStyle === STRUCTURIZR_INTERACTION_ASYNC) {
|
|
532
|
+
tags = [...tags ?? [], STRUCTURIZR_TAG_ASYNC];
|
|
473
533
|
}
|
|
474
534
|
const relation = {
|
|
475
535
|
to: targetContainer,
|
|
476
|
-
technology: rel.technology,
|
|
536
|
+
technology: rel.technology ?? (rel.description?.includes(" ") ? void 0 : rel.description),
|
|
477
537
|
tags
|
|
478
538
|
};
|
|
479
539
|
sourceContainer.relations.push(relation);
|
|
@@ -486,7 +546,7 @@ const mapContainersFromStructurizr = (workspace) => {
|
|
|
486
546
|
boundaries: []
|
|
487
547
|
};
|
|
488
548
|
for (const system of workspace.model.softwareSystems ?? []) {
|
|
489
|
-
if (system.location ===
|
|
549
|
+
if (system.location === STRUCTURIZR_LOCATION_EXTERNAL || system.tags?.includes(STRUCTURIZR_LOCATION_EXTERNAL)) {
|
|
490
550
|
processExternalSystem(system, registry);
|
|
491
551
|
} else {
|
|
492
552
|
processInternalSystem(system, registry);
|
|
@@ -494,9 +554,9 @@ const mapContainersFromStructurizr = (workspace) => {
|
|
|
494
554
|
}
|
|
495
555
|
for (const person of workspace.model.people ?? []) {
|
|
496
556
|
const container = {
|
|
497
|
-
name: person.id,
|
|
557
|
+
name: dslId(person.id, person.properties),
|
|
498
558
|
label: person.name,
|
|
499
|
-
type:
|
|
559
|
+
type: PERSON_TYPE,
|
|
500
560
|
tags: person.tags?.split(",").map((t) => t.trim()).filter(Boolean),
|
|
501
561
|
description: person.description ?? "",
|
|
502
562
|
relations: []
|
|
@@ -528,18 +588,42 @@ const loadStructurizrElements = async (filePath) => {
|
|
|
528
588
|
return mapContainersFromStructurizr(workspace);
|
|
529
589
|
};
|
|
530
590
|
|
|
591
|
+
const structurizrDslSyntax = {
|
|
592
|
+
containerPattern: (name) => `${name} = container`,
|
|
593
|
+
containerDecl: (name, label, tags) => {
|
|
594
|
+
if (tags) {
|
|
595
|
+
return `${name} = container "${label}" {
|
|
596
|
+
tags "${tags}"
|
|
597
|
+
}`;
|
|
598
|
+
}
|
|
599
|
+
return `${name} = container "${label}"`;
|
|
600
|
+
},
|
|
601
|
+
relationPattern: (from, to) => `${from} -> ${to}`,
|
|
602
|
+
relationDecl: (from, to, tech, tags) => {
|
|
603
|
+
const techPart = tech ? ` "${tech}"` : "";
|
|
604
|
+
if (tags) {
|
|
605
|
+
return `${from} -> ${to}${techPart} {
|
|
606
|
+
tags "${tags}"
|
|
607
|
+
}`;
|
|
608
|
+
}
|
|
609
|
+
return `${from} -> ${to}${techPart}`;
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
531
613
|
const checkAcl = (containers, options) => {
|
|
532
614
|
const tag = options?.tag ?? "acl";
|
|
533
|
-
const externalType = options?.externalType ??
|
|
615
|
+
const externalType = options?.externalType ?? EXTERNAL_SYSTEM_TYPE;
|
|
534
616
|
const violations = [];
|
|
535
617
|
for (const container of containers) {
|
|
536
618
|
const externalRelations = container.relations.filter(
|
|
537
619
|
(r) => r.to.type === externalType
|
|
538
620
|
);
|
|
539
621
|
if (!container.tags?.includes(tag) && externalRelations.length > 0) {
|
|
622
|
+
const names = externalRelations.map((r) => r.to.name).join(", ");
|
|
623
|
+
const label = externalRelations.length === 1 ? "system" : "systems";
|
|
540
624
|
violations.push({
|
|
541
625
|
container: container.name,
|
|
542
|
-
message: `
|
|
626
|
+
message: `calls external ${label} ${names} without an ACL layer`
|
|
543
627
|
});
|
|
544
628
|
}
|
|
545
629
|
}
|
|
@@ -574,7 +658,7 @@ const checkAcyclic = (containers) => {
|
|
|
574
658
|
|
|
575
659
|
const checkApiGateway = (containers, options) => {
|
|
576
660
|
const aclTag = options?.aclTag ?? "acl";
|
|
577
|
-
const externalType = options?.externalType ??
|
|
661
|
+
const externalType = options?.externalType ?? EXTERNAL_SYSTEM_TYPE;
|
|
578
662
|
const gatewayPattern = options?.gatewayPattern ?? /gateway/i;
|
|
579
663
|
const violations = [];
|
|
580
664
|
for (const container of containers) {
|
|
@@ -585,7 +669,7 @@ const checkApiGateway = (containers, options) => {
|
|
|
585
669
|
if (!techs.some((t) => gatewayPattern.test(t))) {
|
|
586
670
|
violations.push({
|
|
587
671
|
container: container.name,
|
|
588
|
-
message: `external
|
|
672
|
+
message: `calls external "${rel.to.name}" without going through an API Gateway`
|
|
589
673
|
});
|
|
590
674
|
}
|
|
591
675
|
}
|
|
@@ -594,22 +678,22 @@ const checkApiGateway = (containers, options) => {
|
|
|
594
678
|
};
|
|
595
679
|
|
|
596
680
|
const getBoundaryCohesion = (boundary) => {
|
|
681
|
+
const names = new Set(boundary.containers.map((c) => c.name));
|
|
597
682
|
let result = 0;
|
|
598
683
|
for (const container of boundary.containers) {
|
|
599
|
-
result += container.relations.filter(
|
|
600
|
-
(r) => boundary.containers.some((c) => c.name === r.to.name)
|
|
601
|
-
).length;
|
|
684
|
+
result += container.relations.filter((r) => names.has(r.to.name)).length;
|
|
602
685
|
}
|
|
603
686
|
for (const innerBoundary of boundary.boundaries) {
|
|
604
687
|
result += getBoundaryCoupling(innerBoundary);
|
|
605
688
|
}
|
|
606
689
|
return result;
|
|
607
690
|
};
|
|
608
|
-
const getBoundaryCoupling = (boundary, externalType =
|
|
691
|
+
const getBoundaryCoupling = (boundary, externalType = EXTERNAL_SYSTEM_TYPE, internalType = CONTAINER_TYPE) => {
|
|
692
|
+
const names = new Set(boundary.containers.map((c) => c.name));
|
|
609
693
|
let result = 0;
|
|
610
694
|
for (const container of boundary.containers) {
|
|
611
695
|
result += container.relations.filter(
|
|
612
|
-
(r) => r.to.type === internalType && !
|
|
696
|
+
(r) => r.to.type === internalType && !names.has(r.to.name)
|
|
613
697
|
).length;
|
|
614
698
|
}
|
|
615
699
|
for (const innerBoundary of boundary.boundaries) {
|
|
@@ -622,8 +706,8 @@ const getBoundaryCoupling = (boundary, externalType = "System_Ext", internalType
|
|
|
622
706
|
return result;
|
|
623
707
|
};
|
|
624
708
|
const checkCohesion = (model, options) => {
|
|
625
|
-
const externalType = options?.externalType ??
|
|
626
|
-
const internalType = options?.internalType ??
|
|
709
|
+
const externalType = options?.externalType ?? EXTERNAL_SYSTEM_TYPE;
|
|
710
|
+
const internalType = options?.internalType ?? CONTAINER_TYPE;
|
|
627
711
|
const violations = [];
|
|
628
712
|
for (const boundary of model.boundaries) {
|
|
629
713
|
const cohesion = getBoundaryCohesion(boundary);
|
|
@@ -631,7 +715,7 @@ const checkCohesion = (model, options) => {
|
|
|
631
715
|
if (cohesion <= coupling) {
|
|
632
716
|
violations.push({
|
|
633
717
|
container: boundary.name,
|
|
634
|
-
message: `cohesion (${cohesion})
|
|
718
|
+
message: `coupling (${coupling}) \u2265 cohesion (${cohesion}) \u2014 more cross-boundary dependencies than internal connections`
|
|
635
719
|
});
|
|
636
720
|
}
|
|
637
721
|
if (boundary.boundaries.length > 0) {
|
|
@@ -642,7 +726,7 @@ const checkCohesion = (model, options) => {
|
|
|
642
726
|
if (cohesion >= innerCohesionSum) {
|
|
643
727
|
violations.push({
|
|
644
728
|
container: boundary.name,
|
|
645
|
-
message: `cohesion (${cohesion})
|
|
729
|
+
message: `parent cohesion (${cohesion}) \u2265 sum of inner cohesions (${innerCohesionSum}) \u2014 parent boundary should be less cohesive than its sub-boundaries`
|
|
646
730
|
});
|
|
647
731
|
}
|
|
648
732
|
}
|
|
@@ -650,9 +734,57 @@ const checkCohesion = (model, options) => {
|
|
|
650
734
|
return violations;
|
|
651
735
|
};
|
|
652
736
|
|
|
737
|
+
const checkCommonReuse = (model) => {
|
|
738
|
+
const boundaryOf = /* @__PURE__ */ new Map();
|
|
739
|
+
for (const boundary of model.boundaries) {
|
|
740
|
+
for (const c of boundary.containers) {
|
|
741
|
+
boundaryOf.set(c.name, boundary);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const publicOf = /* @__PURE__ */ new Map();
|
|
745
|
+
const used = /* @__PURE__ */ new Map();
|
|
746
|
+
for (const source of model.allContainers) {
|
|
747
|
+
const srcBoundary = boundaryOf.get(source.name);
|
|
748
|
+
if (!srcBoundary) continue;
|
|
749
|
+
for (const rel of source.relations) {
|
|
750
|
+
const tgtBoundary = boundaryOf.get(rel.to.name);
|
|
751
|
+
if (!tgtBoundary || tgtBoundary === srcBoundary) continue;
|
|
752
|
+
let pub = publicOf.get(tgtBoundary);
|
|
753
|
+
if (!pub) {
|
|
754
|
+
pub = /* @__PURE__ */ new Set();
|
|
755
|
+
publicOf.set(tgtBoundary, pub);
|
|
756
|
+
}
|
|
757
|
+
pub.add(rel.to.name);
|
|
758
|
+
const key = `${srcBoundary.name}\0${tgtBoundary.name}`;
|
|
759
|
+
let u = used.get(key);
|
|
760
|
+
if (!u) {
|
|
761
|
+
u = /* @__PURE__ */ new Set();
|
|
762
|
+
used.set(key, u);
|
|
763
|
+
}
|
|
764
|
+
u.add(rel.to.name);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
const violations = [];
|
|
768
|
+
for (const [provider, pubNames] of publicOf) {
|
|
769
|
+
if (pubNames.size < 2) continue;
|
|
770
|
+
for (const consumer of model.boundaries) {
|
|
771
|
+
if (consumer === provider) continue;
|
|
772
|
+
const key = `${consumer.name}\0${provider.name}`;
|
|
773
|
+
const usedNames = used.get(key);
|
|
774
|
+
if (!usedNames || usedNames.size >= pubNames.size) continue;
|
|
775
|
+
const missing = [...pubNames].filter((n) => !usedNames.has(n));
|
|
776
|
+
violations.push({
|
|
777
|
+
container: consumer.name,
|
|
778
|
+
message: `uses ${[...usedNames].join(", ")} of "${provider.name}" but not ${missing.join(", ")} \u2014 all public services of a context should be used together`
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return violations;
|
|
783
|
+
};
|
|
784
|
+
|
|
653
785
|
const checkCrud = (containers, options) => {
|
|
654
786
|
const repoTags = options?.repoTags ?? ["repo", "relay"];
|
|
655
|
-
const dbType = options?.dbType ??
|
|
787
|
+
const dbType = options?.dbType ?? CONTAINER_DB_TYPE;
|
|
656
788
|
const violations = [];
|
|
657
789
|
for (const container of containers) {
|
|
658
790
|
const dbRelations = container.relations.filter((r) => r.to.type === dbType);
|
|
@@ -660,13 +792,13 @@ const checkCrud = (containers, options) => {
|
|
|
660
792
|
if (!isRepo && dbRelations.length > 0) {
|
|
661
793
|
violations.push({
|
|
662
794
|
container: container.name,
|
|
663
|
-
message: `accesses database
|
|
795
|
+
message: `directly accesses database ${dbRelations.map((r) => r.to.name).join(", ")} \u2014 add a repo or relay`
|
|
664
796
|
});
|
|
665
797
|
}
|
|
666
798
|
if (container.tags?.includes("repo") && container.relations.some((r) => r.to.type !== dbType)) {
|
|
667
799
|
violations.push({
|
|
668
800
|
container: container.name,
|
|
669
|
-
message: `repo has non-database dependencies: ${container.relations.filter((r) => r.to.type !== dbType).map((r) => r.to.name).join(", ")}`
|
|
801
|
+
message: `repo has non-database dependencies: ${container.relations.filter((r) => r.to.type !== dbType).map((r) => r.to.name).join(", ")} \u2014 repos should only access databases`
|
|
670
802
|
});
|
|
671
803
|
}
|
|
672
804
|
}
|
|
@@ -674,7 +806,7 @@ const checkCrud = (containers, options) => {
|
|
|
674
806
|
};
|
|
675
807
|
|
|
676
808
|
const checkDbPerService = (containers, options) => {
|
|
677
|
-
const dbType = options?.dbType ??
|
|
809
|
+
const dbType = options?.dbType ?? CONTAINER_DB_TYPE;
|
|
678
810
|
const violations = [];
|
|
679
811
|
const dbAccessMap = /* @__PURE__ */ new Map();
|
|
680
812
|
for (const container of containers) {
|
|
@@ -690,7 +822,7 @@ const checkDbPerService = (containers, options) => {
|
|
|
690
822
|
if (accessors.length > 1) {
|
|
691
823
|
violations.push({
|
|
692
824
|
container: db,
|
|
693
|
-
message: `
|
|
825
|
+
message: `shared between ${accessors.join(", ")} \u2014 each database should have a single owner`
|
|
694
826
|
});
|
|
695
827
|
}
|
|
696
828
|
}
|
|
@@ -714,7 +846,7 @@ const computeCoupling = (internal, internalNames) => {
|
|
|
714
846
|
return { ca, ce };
|
|
715
847
|
};
|
|
716
848
|
const checkStableDependencies = (containers, options) => {
|
|
717
|
-
const externalType = options?.externalType ??
|
|
849
|
+
const externalType = options?.externalType ?? EXTERNAL_SYSTEM_TYPE;
|
|
718
850
|
const violations = [];
|
|
719
851
|
const internal = containers.filter((c) => c.type !== externalType);
|
|
720
852
|
const internalNames = new Set(internal.map((c) => c.name));
|
|
@@ -733,7 +865,7 @@ const checkStableDependencies = (containers, options) => {
|
|
|
733
865
|
if (iSource < iTarget) {
|
|
734
866
|
violations.push({
|
|
735
867
|
container: c.name,
|
|
736
|
-
message: `depends on less stable ${rel.to.name} (I=${
|
|
868
|
+
message: `stable module (I=${iSource.toFixed(2)}) depends on less stable "${rel.to.name}" (I=${iTarget.toFixed(2)}) \u2014 dependencies should point toward stability`
|
|
737
869
|
});
|
|
738
870
|
}
|
|
739
871
|
}
|
|
@@ -741,4 +873,4 @@ const checkStableDependencies = (containers, options) => {
|
|
|
741
873
|
return violations;
|
|
742
874
|
};
|
|
743
875
|
|
|
744
|
-
export { AactConfigSchema as A,
|
|
876
|
+
export { AactConfigSchema as A, BOUNDARY_TYPE as B, COMPONENT_TYPE as C, generateKubernetes as D, EXTERNAL_SYSTEM_TYPE as E, generatePlantumlFromModel as F, loadPlantumlElements as G, loadStructurizrElements as H, loadStructurizrWorkspace as I, mapContainersFromPlantumlElements as J, mapContainersFromStructurizr as K, structurizrDslSyntax as L, PERSON_TYPE as P, STRUCTURIZR_INTERACTION_ASYNC as S, CONTAINER_BOUNDARY_TYPE as a, CONTAINER_DB_TYPE as b, CONTAINER_TYPE as c, PLANTUML_BOUNDARY as d, PLANTUML_COMPONENT as e, PLANTUML_CONTAINER as f, PLANTUML_CONTAINER_BOUNDARY as g, PLANTUML_CONTAINER_DB as h, PLANTUML_PERSON as i, PLANTUML_SYSTEM as j, PLANTUML_SYSTEM_BOUNDARY as k, PLANTUML_SYSTEM_EXT as l, STRUCTURIZR_LOCATION_EXTERNAL as m, STRUCTURIZR_TAG_ASYNC as n, SYSTEM_BOUNDARY_TYPE as o, SYSTEM_TYPE as p, analyzeArchitecture as q, checkAcl as r, checkAcyclic as s, checkApiGateway as t, checkCohesion as u, checkCommonReuse as v, checkCrud as w, checkDbPerService as x, checkStableDependencies as y, defineConfig as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aact",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Architecture analysis and compliance tool",
|
|
6
6
|
"keywords": [
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
"c12": "4.0.0-beta.2",
|
|
81
81
|
"citty": "^0.2.0",
|
|
82
82
|
"consola": "^3.4.2",
|
|
83
|
+
"picocolors": "^1.1.1",
|
|
83
84
|
"plantuml-parser": "0.4.0",
|
|
84
85
|
"valibot": "^1.2.0",
|
|
85
86
|
"yaml": "2.8.2"
|