aact 2.0.2 → 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.
@@ -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 apiTechnologies = ["http", "grpc", "tcp"];
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 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)) {
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 = parentBoundary?.boundaries.some(
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 analyzeModel = (model) => {
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((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
- });
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 = model.boundaries.find(
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
- boundary,
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 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
- )
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: dbContainers.length,
91
- consumes: dbRelations.length
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 === "ContainerDb") {
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 === "System_Ext") {
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 === "Container") {
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 !== "ContainerDb" && c.type !== "System_Ext"
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
- Container: "Container",
210
- ContainerDb: "ContainerDb",
211
- System_Ext: "System_Ext",
212
- Person: "Person"
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 ?? "Container"] ?? "Container";
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 === "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) {
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 (["System_Boundary", "Container_Boundary", "Boundary"].includes(
292
- elementAsBoundary.type_.name
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: "System_Ext",
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) ? "ContainerDb" : "Container",
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: "Boundary",
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 === "Asynchronous") {
472
- tags = [...tags ?? [], "async"];
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 === "External") {
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: "Person",
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 ?? "System_Ext";
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: `depends on external systems: ${externalRelations.map((r) => r.to.name).join(", ")}`
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 ?? "System_Ext";
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 relation to ${rel.to.name} does not go through API Gateway`
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 = "System_Ext", internalType = "Container") => {
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 && !boundary.containers.some((c) => c.name === r.to.name)
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 ?? "System_Ext";
626
- const internalType = options?.internalType ?? "Container";
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}) is not greater than coupling (${coupling})`
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}) is not less than sum of inner boundary cohesions (${innerCohesionSum})`
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 ?? "ContainerDb";
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 without repo/relay tag: ${dbRelations.map((r) => r.to.name).join(", ")}`
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 ?? "ContainerDb";
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: `accessed by multiple services: ${accessors.join(", ")}`
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 ?? "System_Ext";
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=${iSource.toFixed(2)} \u2192 I=${iTarget.toFixed(2)})`
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, 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 };
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.2",
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"