aact 2.1.5 → 3.0.0-beta.3
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 +50 -18
- package/dist/chunks/index.mjs +422 -0
- package/dist/chunks/index2.mjs +248 -0
- package/dist/chunks/index3.mjs +72 -0
- package/dist/cli/index.mjs +338 -542
- package/dist/index.d.mts +520 -229
- package/dist/index.d.ts +520 -229
- package/dist/index.mjs +5 -89
- package/dist/shared/aact.BpV1UCZJ.mjs +3 -0
- package/dist/shared/aact.CfImn7en.mjs +138 -0
- package/dist/shared/aact.CxegP3pU.mjs +900 -0
- package/package.json +44 -14
- package/dist/shared/aact.CJGFUdeF.mjs +0 -886
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
import * as v from 'valibot';
|
|
2
|
+
import consola from 'consola';
|
|
3
|
+
|
|
4
|
+
const getContainer = (m, name) => m.containers[name];
|
|
5
|
+
const getBoundary = (m, name) => m.boundaries[name];
|
|
6
|
+
const targetOf = (m, rel) => m.containers[rel.to];
|
|
7
|
+
const allContainers = (m) => Object.values(m.containers);
|
|
8
|
+
const allBoundaries = (m) => Object.values(m.boundaries);
|
|
9
|
+
const walkBoundaries = function* (m) {
|
|
10
|
+
const visited = /* @__PURE__ */ new Set();
|
|
11
|
+
const visit = function* (name) {
|
|
12
|
+
if (visited.has(name)) return;
|
|
13
|
+
visited.add(name);
|
|
14
|
+
const b = m.boundaries[name];
|
|
15
|
+
if (!b) return;
|
|
16
|
+
yield b;
|
|
17
|
+
for (const child of b.boundaryNames) yield* visit(child);
|
|
18
|
+
};
|
|
19
|
+
for (const root of m.rootBoundaryNames) yield* visit(root);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const DEFAULT_API_TECHNOLOGIES = ["http", "grpc", "tcp"];
|
|
23
|
+
const allRelations = (model) => allContainers(model).flatMap(
|
|
24
|
+
(container) => container.relations.map((relation) => ({ from: container, relation }))
|
|
25
|
+
);
|
|
26
|
+
const classifyRelation = (names, childNames, parentBoundary, from, relation, result, parentResult) => {
|
|
27
|
+
if (!names.has(from.name)) return;
|
|
28
|
+
if (names.has(relation.to)) {
|
|
29
|
+
result.cohesion++;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const isInParentSibling = childNames?.has(relation.to) ?? false;
|
|
33
|
+
if (!parentBoundary || isInParentSibling) {
|
|
34
|
+
result.coupling++;
|
|
35
|
+
result.couplingRelations.push({ from: from.name, to: relation.to });
|
|
36
|
+
if (parentResult) parentResult.cohesion++;
|
|
37
|
+
} else if (parentResult) {
|
|
38
|
+
parentResult.coupling++;
|
|
39
|
+
parentResult.couplingRelations.push({
|
|
40
|
+
from: from.name,
|
|
41
|
+
to: relation.to
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const buildBoundaryLookups = (model) => {
|
|
46
|
+
const boundaries = Object.values(model.boundaries);
|
|
47
|
+
const nameSets = new Map(
|
|
48
|
+
boundaries.map((b) => [b.name, new Set(b.containerNames)])
|
|
49
|
+
);
|
|
50
|
+
const parentMap = /* @__PURE__ */ new Map();
|
|
51
|
+
for (const b of boundaries) {
|
|
52
|
+
for (const childName of b.boundaryNames) {
|
|
53
|
+
const child = getBoundary(model, childName);
|
|
54
|
+
if (child) parentMap.set(child.name, b);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const result = /* @__PURE__ */ new Map();
|
|
58
|
+
for (const b of boundaries) {
|
|
59
|
+
const parentBoundary = parentMap.get(b.name);
|
|
60
|
+
let childNames;
|
|
61
|
+
if (parentBoundary) {
|
|
62
|
+
childNames = /* @__PURE__ */ new Set();
|
|
63
|
+
for (const siblingName of parentBoundary.boundaryNames) {
|
|
64
|
+
const sibling = getBoundary(model, siblingName);
|
|
65
|
+
if (sibling) {
|
|
66
|
+
for (const cName of sibling.containerNames) childNames.add(cName);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
result.set(b.name, {
|
|
71
|
+
nameSet: nameSets.get(b.name),
|
|
72
|
+
childNames,
|
|
73
|
+
parentBoundary
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
};
|
|
78
|
+
const isSyncApiCall = (model, it, apiTechnologies) => {
|
|
79
|
+
if (it.relation.tags.includes("async")) return false;
|
|
80
|
+
const target = getContainer(model, it.relation.to);
|
|
81
|
+
if (target?.external === true && target.kind === "System") return true;
|
|
82
|
+
return apiTechnologies.some(
|
|
83
|
+
(t) => (it.relation.technology ?? "").toLowerCase().includes(t)
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
const analyzeModel = (model, options) => {
|
|
87
|
+
const apiTechnologies = options?.apiTechnologies ?? DEFAULT_API_TECHNOLOGIES;
|
|
88
|
+
const relations = allRelations(model);
|
|
89
|
+
const asyncApiCalls = relations.filter(
|
|
90
|
+
(it) => it.relation.tags.includes("async")
|
|
91
|
+
);
|
|
92
|
+
const syncApiCalls = relations.filter(
|
|
93
|
+
(it) => isSyncApiCall(model, it, apiTechnologies)
|
|
94
|
+
);
|
|
95
|
+
const lookups = buildBoundaryLookups(model);
|
|
96
|
+
const boundaryResults = /* @__PURE__ */ new Map();
|
|
97
|
+
for (const boundary of Object.values(model.boundaries)) {
|
|
98
|
+
boundaryResults.set(boundary.name, {
|
|
99
|
+
name: boundary.name,
|
|
100
|
+
label: boundary.label,
|
|
101
|
+
cohesion: 0,
|
|
102
|
+
coupling: 0,
|
|
103
|
+
couplingRelations: []
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
for (const boundary of Object.values(model.boundaries)) {
|
|
107
|
+
const { nameSet, childNames, parentBoundary } = lookups.get(boundary.name);
|
|
108
|
+
const result = boundaryResults.get(boundary.name);
|
|
109
|
+
const parentResult = parentBoundary ? boundaryResults.get(parentBoundary.name) : void 0;
|
|
110
|
+
for (const { from, relation } of relations) {
|
|
111
|
+
classifyRelation(
|
|
112
|
+
nameSet,
|
|
113
|
+
childNames,
|
|
114
|
+
parentBoundary,
|
|
115
|
+
from,
|
|
116
|
+
relation,
|
|
117
|
+
result,
|
|
118
|
+
parentResult
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
elementsCount: allContainers(model).length,
|
|
124
|
+
syncApiCalls: syncApiCalls.length,
|
|
125
|
+
asyncApiCalls: asyncApiCalls.length,
|
|
126
|
+
databases: analyzeDatabases(model),
|
|
127
|
+
boundaries: [...boundaryResults.values()]
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
const analyzeDatabases = (model) => {
|
|
131
|
+
const dbNames = new Set(
|
|
132
|
+
allContainers(model).filter((it) => it.kind === "ContainerDb").map((it) => it.name)
|
|
133
|
+
);
|
|
134
|
+
let consumes = 0;
|
|
135
|
+
for (const container of allContainers(model)) {
|
|
136
|
+
for (const r of container.relations) {
|
|
137
|
+
if (dbNames.has(r.to)) consumes++;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
count: dbNames.size,
|
|
142
|
+
consumes
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
const analyzeArchitecture = (model, options) => ({
|
|
146
|
+
model,
|
|
147
|
+
report: analyzeModel(model, options)
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const ruleOption = (entries) => v.optional(v.union([v.boolean(), v.strictObject(entries)]));
|
|
151
|
+
const AactConfigSchema = v.strictObject({
|
|
152
|
+
source: v.union([
|
|
153
|
+
v.string(),
|
|
154
|
+
v.strictObject({
|
|
155
|
+
path: v.string(),
|
|
156
|
+
/** Optional — infer'ится из `path` через format registry `defaultPattern` если опущен. */
|
|
157
|
+
type: v.optional(v.string()),
|
|
158
|
+
/** Structurizr only: куда писать fix'ы (workspace.dsl). */
|
|
159
|
+
writePath: v.optional(v.string())
|
|
160
|
+
})
|
|
161
|
+
]),
|
|
162
|
+
rules: v.optional(
|
|
163
|
+
v.looseObject({
|
|
164
|
+
acl: ruleOption({
|
|
165
|
+
tag: v.optional(v.string())
|
|
166
|
+
}),
|
|
167
|
+
acyclic: v.optional(v.boolean()),
|
|
168
|
+
apiGateway: ruleOption({
|
|
169
|
+
aclTag: v.optional(v.string()),
|
|
170
|
+
gatewayPattern: v.optional(v.instance(RegExp))
|
|
171
|
+
}),
|
|
172
|
+
crud: ruleOption({
|
|
173
|
+
repoTags: v.optional(v.array(v.string()))
|
|
174
|
+
}),
|
|
175
|
+
dbPerService: ruleOption({
|
|
176
|
+
ownerTags: v.optional(v.array(v.string()))
|
|
177
|
+
}),
|
|
178
|
+
cohesion: v.optional(v.boolean()),
|
|
179
|
+
stableDependencies: v.optional(v.boolean()),
|
|
180
|
+
commonReuse: v.optional(v.boolean())
|
|
181
|
+
})
|
|
182
|
+
),
|
|
183
|
+
// RuleDefinition содержит function fields (check/fix) — valibot не валидирует
|
|
184
|
+
// shape глубже массива. Структурная проверка делается в check.ts на activation
|
|
185
|
+
// time (name/check required, conflict detection vs built-ins).
|
|
186
|
+
customRules: v.optional(v.array(v.any())),
|
|
187
|
+
generate: v.optional(
|
|
188
|
+
v.strictObject({
|
|
189
|
+
kubernetes: v.optional(
|
|
190
|
+
v.strictObject({
|
|
191
|
+
path: v.optional(v.string())
|
|
192
|
+
})
|
|
193
|
+
),
|
|
194
|
+
boundaryLabel: v.optional(v.string())
|
|
195
|
+
})
|
|
196
|
+
)
|
|
197
|
+
});
|
|
198
|
+
const defineConfig = (config) => config;
|
|
199
|
+
|
|
200
|
+
const formatLoaders = Object.freeze({
|
|
201
|
+
plantuml: () => import('../chunks/index.mjs').then((m) => m.plantumlFormat),
|
|
202
|
+
structurizr: () => import('../chunks/index2.mjs').then((m) => m.structurizrFormat),
|
|
203
|
+
kubernetes: () => import('../chunks/index3.mjs').then((m) => m.kubernetesFormat)
|
|
204
|
+
});
|
|
205
|
+
const loadFormat = async (name) => {
|
|
206
|
+
const loader = formatLoaders[name];
|
|
207
|
+
if (!loader) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Unknown format "${name}". Known formats: ${Object.keys(formatLoaders).join(", ")}.`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return loader();
|
|
213
|
+
};
|
|
214
|
+
const knownFormatNames = () => Object.keys(formatLoaders);
|
|
215
|
+
|
|
216
|
+
const canLoad = (f) => f.load !== void 0;
|
|
217
|
+
const canGenerate = (f) => f.generate !== void 0;
|
|
218
|
+
const canFix = (f) => f.fix !== void 0;
|
|
219
|
+
|
|
220
|
+
const detectNamingConvention = (model) => {
|
|
221
|
+
const names = allContainers(model).map((c) => c.name);
|
|
222
|
+
if (names.length === 0) return "snake";
|
|
223
|
+
const withUnderscore = names.filter((n) => n.includes("_")).length;
|
|
224
|
+
const withHyphen = names.filter((n) => n.includes("-")).length;
|
|
225
|
+
const withCamel = names.filter((n) => /[a-z][A-Z]/.test(n)).length;
|
|
226
|
+
if (withHyphen > withUnderscore && withHyphen > withCamel) return "kebab";
|
|
227
|
+
if (withCamel > withUnderscore) return "camel";
|
|
228
|
+
return "snake";
|
|
229
|
+
};
|
|
230
|
+
const joinName = (base, word, convention) => {
|
|
231
|
+
switch (convention) {
|
|
232
|
+
case "camel": {
|
|
233
|
+
return base + word.charAt(0).toUpperCase() + word.slice(1);
|
|
234
|
+
}
|
|
235
|
+
case "kebab": {
|
|
236
|
+
return `${base}-${word}`;
|
|
237
|
+
}
|
|
238
|
+
default: {
|
|
239
|
+
return `${base}_${word}`;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const aclRule = {
|
|
245
|
+
name: "acl",
|
|
246
|
+
description: "Containers calling external systems must be tagged as ACL (Anti-corruption Layer)",
|
|
247
|
+
check(model, options) {
|
|
248
|
+
const tag = options?.tag ?? "acl";
|
|
249
|
+
const violations = [];
|
|
250
|
+
for (const container of allContainers(model)) {
|
|
251
|
+
const externalRelations = container.relations.filter(
|
|
252
|
+
(r) => targetOf(model, r)?.external === true
|
|
253
|
+
);
|
|
254
|
+
if (!container.tags.includes(tag) && externalRelations.length > 0) {
|
|
255
|
+
const names = externalRelations.map((r) => r.to).join(", ");
|
|
256
|
+
const label = externalRelations.length === 1 ? "system" : "systems";
|
|
257
|
+
violations.push({
|
|
258
|
+
container: container.name,
|
|
259
|
+
message: `calls external ${label} ${names} without an ACL layer`
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return violations;
|
|
264
|
+
},
|
|
265
|
+
fix(model, violations, syntax, options) {
|
|
266
|
+
const tag = options?.tag ?? "acl";
|
|
267
|
+
const convention = detectNamingConvention(model);
|
|
268
|
+
const results = [];
|
|
269
|
+
for (const violation of violations) {
|
|
270
|
+
const container = model.containers[violation.container];
|
|
271
|
+
if (!container) continue;
|
|
272
|
+
const externalRels = container.relations.filter(
|
|
273
|
+
(r) => targetOf(model, r)?.external === true
|
|
274
|
+
);
|
|
275
|
+
if (externalRels.length === 0) continue;
|
|
276
|
+
const aclName = joinName(container.name, "acl", convention);
|
|
277
|
+
if (aclName in model.containers) {
|
|
278
|
+
consola.warn(
|
|
279
|
+
`fix acl: skipping ${container.name} \u2014 ${aclName} already exists`
|
|
280
|
+
);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
results.push({
|
|
284
|
+
rule: "acl",
|
|
285
|
+
description: `Add ACL layer for ${container.name}`,
|
|
286
|
+
edits: [
|
|
287
|
+
{
|
|
288
|
+
type: "add",
|
|
289
|
+
search: syntax.containerPattern(container.name),
|
|
290
|
+
content: syntax.containerDecl(
|
|
291
|
+
aclName,
|
|
292
|
+
`${container.label} ACL`,
|
|
293
|
+
tag
|
|
294
|
+
)
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
type: "add",
|
|
298
|
+
search: syntax.containerPattern(aclName),
|
|
299
|
+
content: syntax.relationDecl(container.name, aclName)
|
|
300
|
+
},
|
|
301
|
+
...externalRels.map((rel) => ({
|
|
302
|
+
type: "replace",
|
|
303
|
+
search: syntax.relationPattern(container.name, rel.to),
|
|
304
|
+
content: syntax.relationDecl(aclName, rel.to, rel.technology)
|
|
305
|
+
}))
|
|
306
|
+
]
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return results;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const acyclicRule = {
|
|
314
|
+
name: "acyclic",
|
|
315
|
+
description: "Dependency graph between containers must be acyclic (no cycles)",
|
|
316
|
+
check(model) {
|
|
317
|
+
const violations = [];
|
|
318
|
+
const findCycle = (fromName, target, visited) => {
|
|
319
|
+
const container = getContainer(model, fromName);
|
|
320
|
+
if (!container) return false;
|
|
321
|
+
for (const rel of container.relations) {
|
|
322
|
+
if (rel.to === target) return true;
|
|
323
|
+
if (visited.has(rel.to)) continue;
|
|
324
|
+
visited.add(rel.to);
|
|
325
|
+
if (findCycle(rel.to, target, visited)) return true;
|
|
326
|
+
}
|
|
327
|
+
return false;
|
|
328
|
+
};
|
|
329
|
+
for (const container of allContainers(model)) {
|
|
330
|
+
if (findCycle(container.name, container.name, /* @__PURE__ */ new Set())) {
|
|
331
|
+
violations.push({
|
|
332
|
+
container: container.name,
|
|
333
|
+
message: "participates in a dependency cycle"
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return violations;
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const apiGatewayRule = {
|
|
342
|
+
name: "apiGateway",
|
|
343
|
+
description: "ACL containers calling external systems must route through an API Gateway",
|
|
344
|
+
check(model, options) {
|
|
345
|
+
const aclTag = options?.aclTag ?? "acl";
|
|
346
|
+
const gatewayPattern = options?.gatewayPattern ?? /gateway/i;
|
|
347
|
+
const violations = [];
|
|
348
|
+
for (const container of allContainers(model)) {
|
|
349
|
+
if (!container.tags.includes(aclTag)) continue;
|
|
350
|
+
for (const rel of container.relations) {
|
|
351
|
+
if (targetOf(model, rel)?.external !== true) continue;
|
|
352
|
+
const techs = rel.technology?.split(", ") ?? [];
|
|
353
|
+
if (!techs.some((t) => gatewayPattern.test(t))) {
|
|
354
|
+
violations.push({
|
|
355
|
+
container: container.name,
|
|
356
|
+
message: `calls external "${rel.to}" without going through an API Gateway`
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return violations;
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const getBoundaryCohesion = (model, boundary) => {
|
|
366
|
+
const names = new Set(boundary.containerNames);
|
|
367
|
+
let result = 0;
|
|
368
|
+
for (const containerName of boundary.containerNames) {
|
|
369
|
+
const container = getContainer(model, containerName);
|
|
370
|
+
if (!container) continue;
|
|
371
|
+
result += container.relations.filter((r) => names.has(r.to)).length;
|
|
372
|
+
}
|
|
373
|
+
for (const innerName of boundary.boundaryNames) {
|
|
374
|
+
const inner = getBoundary(model, innerName);
|
|
375
|
+
if (inner) result += getBoundaryCoupling(model, inner);
|
|
376
|
+
}
|
|
377
|
+
return result;
|
|
378
|
+
};
|
|
379
|
+
const getBoundaryCoupling = (model, boundary) => {
|
|
380
|
+
const names = new Set(boundary.containerNames);
|
|
381
|
+
let result = 0;
|
|
382
|
+
for (const containerName of boundary.containerNames) {
|
|
383
|
+
const container = getContainer(model, containerName);
|
|
384
|
+
if (!container) continue;
|
|
385
|
+
result += container.relations.filter((r) => {
|
|
386
|
+
const target = getContainer(model, r.to);
|
|
387
|
+
return target && !target.external && !names.has(r.to);
|
|
388
|
+
}).length;
|
|
389
|
+
}
|
|
390
|
+
for (const innerName of boundary.boundaryNames) {
|
|
391
|
+
const inner = getBoundary(model, innerName);
|
|
392
|
+
if (!inner) continue;
|
|
393
|
+
for (const containerName of inner.containerNames) {
|
|
394
|
+
const container = getContainer(model, containerName);
|
|
395
|
+
if (!container) continue;
|
|
396
|
+
result += container.relations.filter(
|
|
397
|
+
(r) => getContainer(model, r.to)?.external === true
|
|
398
|
+
).length;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return result;
|
|
402
|
+
};
|
|
403
|
+
const cohesionRule = {
|
|
404
|
+
name: "cohesion",
|
|
405
|
+
description: "Each boundary should be more cohesive than coupled; parent boundaries less cohesive than inner ones",
|
|
406
|
+
check(model) {
|
|
407
|
+
const violations = [];
|
|
408
|
+
for (const boundary of Object.values(model.boundaries)) {
|
|
409
|
+
const cohesion = getBoundaryCohesion(model, boundary);
|
|
410
|
+
const coupling = getBoundaryCoupling(model, boundary);
|
|
411
|
+
if (cohesion <= coupling) {
|
|
412
|
+
violations.push({
|
|
413
|
+
container: boundary.name,
|
|
414
|
+
message: `coupling (${coupling}) \u2265 cohesion (${cohesion}) \u2014 more cross-boundary dependencies than internal connections`
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
if (boundary.boundaryNames.length > 0) {
|
|
418
|
+
const innerCohesionSum = boundary.boundaryNames.reduce(
|
|
419
|
+
(sum, innerName) => {
|
|
420
|
+
const inner = getBoundary(model, innerName);
|
|
421
|
+
return sum + (inner ? getBoundaryCohesion(model, inner) : 0);
|
|
422
|
+
},
|
|
423
|
+
0
|
|
424
|
+
);
|
|
425
|
+
if (cohesion >= innerCohesionSum) {
|
|
426
|
+
violations.push({
|
|
427
|
+
container: boundary.name,
|
|
428
|
+
message: `parent cohesion (${cohesion}) \u2265 sum of inner cohesions (${innerCohesionSum}) \u2014 parent boundary should be less cohesive than its sub-boundaries`
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return violations;
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const buildBoundaryLookup = (model) => {
|
|
438
|
+
const map = /* @__PURE__ */ new Map();
|
|
439
|
+
for (const boundary of Object.values(model.boundaries)) {
|
|
440
|
+
for (const containerName of boundary.containerNames) {
|
|
441
|
+
map.set(containerName, boundary);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return map;
|
|
445
|
+
};
|
|
446
|
+
const collectPublicAndUsage = (model, boundaryOf) => {
|
|
447
|
+
const publicOf = /* @__PURE__ */ new Map();
|
|
448
|
+
const used = /* @__PURE__ */ new Map();
|
|
449
|
+
for (const source of allContainers(model)) {
|
|
450
|
+
const srcBoundary = boundaryOf.get(source.name);
|
|
451
|
+
if (!srcBoundary) continue;
|
|
452
|
+
for (const rel of source.relations) {
|
|
453
|
+
const tgtBoundary = boundaryOf.get(rel.to);
|
|
454
|
+
if (!tgtBoundary || tgtBoundary === srcBoundary) continue;
|
|
455
|
+
let pub = publicOf.get(tgtBoundary);
|
|
456
|
+
if (!pub) {
|
|
457
|
+
pub = /* @__PURE__ */ new Set();
|
|
458
|
+
publicOf.set(tgtBoundary, pub);
|
|
459
|
+
}
|
|
460
|
+
pub.add(rel.to);
|
|
461
|
+
const key = `${srcBoundary.name}\0${tgtBoundary.name}`;
|
|
462
|
+
let u = used.get(key);
|
|
463
|
+
if (!u) {
|
|
464
|
+
u = /* @__PURE__ */ new Set();
|
|
465
|
+
used.set(key, u);
|
|
466
|
+
}
|
|
467
|
+
u.add(rel.to);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return { publicOf, used };
|
|
471
|
+
};
|
|
472
|
+
const commonReuseRule = {
|
|
473
|
+
name: "commonReuse",
|
|
474
|
+
description: "Consumers using part of a boundary's public surface should use all of it",
|
|
475
|
+
check(model) {
|
|
476
|
+
const boundaryOf = buildBoundaryLookup(model);
|
|
477
|
+
const { publicOf, used } = collectPublicAndUsage(model, boundaryOf);
|
|
478
|
+
const violations = [];
|
|
479
|
+
for (const [provider, pubNames] of publicOf) {
|
|
480
|
+
if (pubNames.size < 2) continue;
|
|
481
|
+
for (const consumer of Object.values(model.boundaries)) {
|
|
482
|
+
if (consumer === provider) continue;
|
|
483
|
+
const key = `${consumer.name}\0${provider.name}`;
|
|
484
|
+
const usedNames = used.get(key);
|
|
485
|
+
if (!usedNames || usedNames.size >= pubNames.size) continue;
|
|
486
|
+
const missing = [...pubNames].filter((n) => !usedNames.has(n));
|
|
487
|
+
violations.push({
|
|
488
|
+
container: consumer.name,
|
|
489
|
+
message: `uses ${[...usedNames].join(", ")} of "${provider.name}" but not ${missing.join(", ")} \u2014 all public services of a context should be used together`
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return violations;
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const buildContainerBoundaryMap = (model) => {
|
|
498
|
+
const map = /* @__PURE__ */ new Map();
|
|
499
|
+
for (const boundary of Object.values(model.boundaries)) {
|
|
500
|
+
for (const containerName of boundary.containerNames) {
|
|
501
|
+
map.set(containerName, boundary);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return map;
|
|
505
|
+
};
|
|
506
|
+
const findPublicApiCandidate = (targetBoundary, ownerTags, model, containerBoundaryMap) => {
|
|
507
|
+
const candidates = targetBoundary.containerNames.map((name) => getContainer(model, name)).filter((c) => c !== void 0).filter(
|
|
508
|
+
(c) => c.kind !== "ContainerDb" && !ownerTags.some((t) => c.tags.includes(t))
|
|
509
|
+
);
|
|
510
|
+
if (candidates.length === 0) return void 0;
|
|
511
|
+
if (candidates.length === 1) return candidates[0];
|
|
512
|
+
const candidateNames = new Set(candidates.map((c) => c.name));
|
|
513
|
+
const inDegree = new Map(candidates.map((c) => [c.name, 0]));
|
|
514
|
+
for (const container of allContainers(model)) {
|
|
515
|
+
if (containerBoundaryMap.get(container.name) === targetBoundary) continue;
|
|
516
|
+
for (const rel of container.relations) {
|
|
517
|
+
if (candidateNames.has(rel.to)) {
|
|
518
|
+
inDegree.set(rel.to, (inDegree.get(rel.to) ?? 0) + 1);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return candidates.toSorted(
|
|
523
|
+
(a, b) => (inDegree.get(b.name) ?? 0) - (inDegree.get(a.name) ?? 0)
|
|
524
|
+
)[0];
|
|
525
|
+
};
|
|
526
|
+
const resolveRedirectTarget = (accessor, db, owner, ownerTags, model, containerBoundaryMap, ruleName) => {
|
|
527
|
+
const accessorBoundary = containerBoundaryMap.get(accessor.name);
|
|
528
|
+
const dbBoundary = containerBoundaryMap.get(db.name);
|
|
529
|
+
const isCrossBoundary = accessorBoundary !== void 0 && dbBoundary !== void 0 && accessorBoundary !== dbBoundary;
|
|
530
|
+
if (!isCrossBoundary) return owner;
|
|
531
|
+
const publicApi = findPublicApiCandidate(
|
|
532
|
+
dbBoundary,
|
|
533
|
+
ownerTags,
|
|
534
|
+
model,
|
|
535
|
+
containerBoundaryMap
|
|
536
|
+
);
|
|
537
|
+
if (!publicApi) {
|
|
538
|
+
consola.warn(
|
|
539
|
+
`fix ${ruleName}: boundary "${dbBoundary.name}" has no public API \u2014 cannot auto-redirect "${accessor.name}" away from "${db.name}", fix manually`
|
|
540
|
+
);
|
|
541
|
+
return void 0;
|
|
542
|
+
}
|
|
543
|
+
if (publicApi === owner) {
|
|
544
|
+
consola.warn(
|
|
545
|
+
`fix ${ruleName}: the only public API candidate in "${dbBoundary.name}" is the repo owner \u2014 cross-boundary access from "${accessor.name}" requires manual review`
|
|
546
|
+
);
|
|
547
|
+
return void 0;
|
|
548
|
+
}
|
|
549
|
+
return publicApi;
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const DEFAULT_REPO_TAGS = ["repo", "relay"];
|
|
553
|
+
const stripDbWord = (name) => {
|
|
554
|
+
const lower = name.toLowerCase();
|
|
555
|
+
for (const suffix of [
|
|
556
|
+
"_database",
|
|
557
|
+
"-database",
|
|
558
|
+
"database",
|
|
559
|
+
"_db",
|
|
560
|
+
"-db",
|
|
561
|
+
"db"
|
|
562
|
+
]) {
|
|
563
|
+
if (lower.endsWith(suffix)) return name.slice(0, -suffix.length);
|
|
564
|
+
}
|
|
565
|
+
return name;
|
|
566
|
+
};
|
|
567
|
+
const deriveRepoName = (dbName, convention) => {
|
|
568
|
+
const base = stripDbWord(dbName);
|
|
569
|
+
return joinName(base || dbName, "repo", convention);
|
|
570
|
+
};
|
|
571
|
+
const deriveRepoLabel = (dbName) => {
|
|
572
|
+
const base = stripDbWord(dbName);
|
|
573
|
+
const word = base || dbName;
|
|
574
|
+
return word.charAt(0).toUpperCase() + word.slice(1).replaceAll("_", " ") + " Repo";
|
|
575
|
+
};
|
|
576
|
+
const fixNonRepoAccessesDb = (accessor, model, syntax, ownerTags, convention) => {
|
|
577
|
+
const dbRels = accessor.relations.filter(
|
|
578
|
+
(r) => targetOf(model, r)?.kind === "ContainerDb"
|
|
579
|
+
);
|
|
580
|
+
const containerBoundaryMap = buildContainerBoundaryMap(model);
|
|
581
|
+
const edits = dbRels.flatMap((rel) => {
|
|
582
|
+
const db = targetOf(model, rel);
|
|
583
|
+
if (!db) return [];
|
|
584
|
+
const existingRepo = allContainers(model).find(
|
|
585
|
+
(c) => c !== accessor && c.relations.some((r) => r.to === db.name) && ownerTags.some((t) => c.tags.includes(t))
|
|
586
|
+
);
|
|
587
|
+
if (existingRepo) {
|
|
588
|
+
const redirectTarget = resolveRedirectTarget(
|
|
589
|
+
accessor,
|
|
590
|
+
db,
|
|
591
|
+
existingRepo,
|
|
592
|
+
ownerTags,
|
|
593
|
+
model,
|
|
594
|
+
containerBoundaryMap,
|
|
595
|
+
"crud"
|
|
596
|
+
);
|
|
597
|
+
if (!redirectTarget) return [];
|
|
598
|
+
return [
|
|
599
|
+
{
|
|
600
|
+
type: "replace",
|
|
601
|
+
search: syntax.relationPattern(accessor.name, db.name),
|
|
602
|
+
content: syntax.relationDecl(
|
|
603
|
+
accessor.name,
|
|
604
|
+
redirectTarget.name,
|
|
605
|
+
rel.technology
|
|
606
|
+
)
|
|
607
|
+
}
|
|
608
|
+
];
|
|
609
|
+
}
|
|
610
|
+
const accessorBoundary = containerBoundaryMap.get(accessor.name);
|
|
611
|
+
const dbBoundary = containerBoundaryMap.get(db.name);
|
|
612
|
+
if (accessorBoundary !== void 0 && dbBoundary !== void 0 && accessorBoundary !== dbBoundary) {
|
|
613
|
+
consola.warn(
|
|
614
|
+
`fix crud: "${accessor.name}" accesses "${db.name}" cross-boundary with no existing repo \u2014 fix manually`
|
|
615
|
+
);
|
|
616
|
+
return [];
|
|
617
|
+
}
|
|
618
|
+
const repoName = deriveRepoName(db.name, convention);
|
|
619
|
+
if (repoName in model.containers) {
|
|
620
|
+
consola.warn(
|
|
621
|
+
`fix crud: cannot create repo for "${db.name}" \u2014 "${repoName}" already exists`
|
|
622
|
+
);
|
|
623
|
+
return [];
|
|
624
|
+
}
|
|
625
|
+
return [
|
|
626
|
+
{
|
|
627
|
+
type: "add",
|
|
628
|
+
search: syntax.containerPattern(db.name),
|
|
629
|
+
content: syntax.containerDecl(
|
|
630
|
+
repoName,
|
|
631
|
+
deriveRepoLabel(db.name),
|
|
632
|
+
ownerTags[0] ?? "repo"
|
|
633
|
+
)
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
type: "add",
|
|
637
|
+
search: syntax.containerPattern(repoName),
|
|
638
|
+
content: syntax.relationDecl(repoName, db.name, rel.technology)
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
type: "replace",
|
|
642
|
+
search: syntax.relationPattern(accessor.name, db.name),
|
|
643
|
+
content: syntax.relationDecl(accessor.name, repoName, rel.technology)
|
|
644
|
+
}
|
|
645
|
+
];
|
|
646
|
+
});
|
|
647
|
+
if (edits.length === 0) return void 0;
|
|
648
|
+
return {
|
|
649
|
+
rule: "crud",
|
|
650
|
+
description: `Add repo intermediary for ${accessor.name} \u2192 ${dbRels.map((r) => r.to).join(", ")}`,
|
|
651
|
+
edits
|
|
652
|
+
};
|
|
653
|
+
};
|
|
654
|
+
const fixRepoWithNonDbDeps = (repo, model, syntax) => {
|
|
655
|
+
const nonDbRels = repo.relations.filter(
|
|
656
|
+
(r) => targetOf(model, r)?.kind !== "ContainerDb"
|
|
657
|
+
);
|
|
658
|
+
if (nonDbRels.length === 0) return void 0;
|
|
659
|
+
return {
|
|
660
|
+
rule: "crud",
|
|
661
|
+
description: `Remove non-database dependencies from repo ${repo.name}`,
|
|
662
|
+
edits: nonDbRels.map((rel) => ({
|
|
663
|
+
type: "remove",
|
|
664
|
+
search: syntax.relationPattern(repo.name, rel.to)
|
|
665
|
+
}))
|
|
666
|
+
};
|
|
667
|
+
};
|
|
668
|
+
const crudRule = {
|
|
669
|
+
name: "crud",
|
|
670
|
+
description: "Direct database access only through repo/relay containers; repos must access databases only",
|
|
671
|
+
check(model, options) {
|
|
672
|
+
const repoTags = options?.repoTags ?? DEFAULT_REPO_TAGS;
|
|
673
|
+
const violations = [];
|
|
674
|
+
for (const container of allContainers(model)) {
|
|
675
|
+
const dbRelations = container.relations.filter(
|
|
676
|
+
(r) => targetOf(model, r)?.kind === "ContainerDb"
|
|
677
|
+
);
|
|
678
|
+
const isRepo = repoTags.some((tag) => container.tags.includes(tag));
|
|
679
|
+
if (!isRepo && dbRelations.length > 0) {
|
|
680
|
+
violations.push({
|
|
681
|
+
container: container.name,
|
|
682
|
+
message: `directly accesses database ${dbRelations.map((r) => r.to).join(", ")} \u2014 add a repo or relay`
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
if (isRepo && container.relations.some(
|
|
686
|
+
(r) => targetOf(model, r)?.kind !== "ContainerDb"
|
|
687
|
+
)) {
|
|
688
|
+
const nonDbTargets = container.relations.filter((r) => targetOf(model, r)?.kind !== "ContainerDb").map((r) => r.to).join(", ");
|
|
689
|
+
violations.push({
|
|
690
|
+
container: container.name,
|
|
691
|
+
message: `repo has non-database dependencies: ${nonDbTargets} \u2014 repos should only access databases`
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return violations;
|
|
696
|
+
},
|
|
697
|
+
fix(model, violations, syntax, options) {
|
|
698
|
+
const ownerTags = options?.repoTags ?? DEFAULT_REPO_TAGS;
|
|
699
|
+
const convention = detectNamingConvention(model);
|
|
700
|
+
const results = [];
|
|
701
|
+
for (const violation of violations) {
|
|
702
|
+
const container = model.containers[violation.container];
|
|
703
|
+
if (!container) continue;
|
|
704
|
+
const isRepo = ownerTags.some((t) => container.tags.includes(t));
|
|
705
|
+
const fix = isRepo ? fixRepoWithNonDbDeps(container, model, syntax) : fixNonRepoAccessesDb(container, model, syntax, ownerTags, convention);
|
|
706
|
+
if (fix) results.push(fix);
|
|
707
|
+
}
|
|
708
|
+
return results;
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
const DEFAULT_OWNER_TAGS = ["repo", "relay"];
|
|
713
|
+
const resolveOwner = (dbName, accessors, ownerTags) => {
|
|
714
|
+
const tagged = accessors.filter(
|
|
715
|
+
(c) => c.tags.some((t) => ownerTags.includes(t))
|
|
716
|
+
);
|
|
717
|
+
if (tagged.length === 0) {
|
|
718
|
+
consola.warn(
|
|
719
|
+
`Cannot determine owner of ${dbName}: no ${ownerTags.join("/")} tagged accessor found, using ${accessors[0].name}`
|
|
720
|
+
);
|
|
721
|
+
return accessors[0];
|
|
722
|
+
}
|
|
723
|
+
if (tagged.length > 1) {
|
|
724
|
+
consola.warn(
|
|
725
|
+
`Cannot determine owner of ${dbName}: multiple tagged accessors (${tagged.map((c) => c.name).join(", ")}), using ${tagged[0].name}`
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
return tagged[0];
|
|
729
|
+
};
|
|
730
|
+
const dbPerServiceRule = {
|
|
731
|
+
name: "dbPerService",
|
|
732
|
+
description: "Each database container must have a single owner (one repo/relay per DB)",
|
|
733
|
+
check(model) {
|
|
734
|
+
const violations = [];
|
|
735
|
+
const dbAccessMap = /* @__PURE__ */ new Map();
|
|
736
|
+
for (const container of allContainers(model)) {
|
|
737
|
+
for (const rel of container.relations) {
|
|
738
|
+
if (targetOf(model, rel)?.kind === "ContainerDb") {
|
|
739
|
+
const accessors = dbAccessMap.get(rel.to) ?? [];
|
|
740
|
+
accessors.push(container.name);
|
|
741
|
+
dbAccessMap.set(rel.to, accessors);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
for (const [db, accessors] of dbAccessMap) {
|
|
746
|
+
if (accessors.length > 1) {
|
|
747
|
+
violations.push({
|
|
748
|
+
container: db,
|
|
749
|
+
message: `shared between ${accessors.join(", ")} \u2014 each database should have a single owner`
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return violations;
|
|
754
|
+
},
|
|
755
|
+
fix(model, violations, syntax, options) {
|
|
756
|
+
const ownerTags = options?.ownerTags ?? DEFAULT_OWNER_TAGS;
|
|
757
|
+
const containerBoundaryMap = buildContainerBoundaryMap(model);
|
|
758
|
+
const results = [];
|
|
759
|
+
for (const violation of violations) {
|
|
760
|
+
const db = allContainers(model).find(
|
|
761
|
+
(c) => c.name === violation.container && c.kind === "ContainerDb"
|
|
762
|
+
);
|
|
763
|
+
if (!db) continue;
|
|
764
|
+
const accessors = allContainers(model).filter(
|
|
765
|
+
(c) => c.relations.some((r) => r.to === db.name)
|
|
766
|
+
);
|
|
767
|
+
if (accessors.length <= 1) continue;
|
|
768
|
+
const owner = resolveOwner(db.name, accessors, ownerTags);
|
|
769
|
+
const edits = accessors.filter((c) => c !== owner).flatMap((accessor) => {
|
|
770
|
+
const rel = accessor.relations.find((r) => r.to === db.name);
|
|
771
|
+
const redirectTarget = resolveRedirectTarget(
|
|
772
|
+
accessor,
|
|
773
|
+
db,
|
|
774
|
+
owner,
|
|
775
|
+
ownerTags,
|
|
776
|
+
model,
|
|
777
|
+
containerBoundaryMap,
|
|
778
|
+
"dbPerService"
|
|
779
|
+
);
|
|
780
|
+
if (!redirectTarget) return [];
|
|
781
|
+
const tags = rel.tags.length > 0 ? rel.tags.join("+") : void 0;
|
|
782
|
+
return [
|
|
783
|
+
{
|
|
784
|
+
type: "replace",
|
|
785
|
+
search: syntax.relationPattern(accessor.name, db.name),
|
|
786
|
+
content: syntax.relationDecl(
|
|
787
|
+
accessor.name,
|
|
788
|
+
redirectTarget.name,
|
|
789
|
+
rel.technology ?? "",
|
|
790
|
+
tags
|
|
791
|
+
)
|
|
792
|
+
}
|
|
793
|
+
];
|
|
794
|
+
});
|
|
795
|
+
if (edits.length === 0) continue;
|
|
796
|
+
results.push({
|
|
797
|
+
rule: "dbPerService",
|
|
798
|
+
description: `Redirect access to ${db.name} through ${owner.name}`,
|
|
799
|
+
edits
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
return results;
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
const applyIndent = (content, indent) => content.split("\n").map((line) => line.trim() ? indent + line : line).join("\n");
|
|
807
|
+
const applyEdits = (source, edits) => {
|
|
808
|
+
const lines = source.split("\n");
|
|
809
|
+
for (const edit of edits) {
|
|
810
|
+
const idx = lines.findIndex((line) => line.includes(edit.search));
|
|
811
|
+
if (idx === -1) {
|
|
812
|
+
consola.warn(`fix: pattern not found in source \u2014 "${edit.search}"`);
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
const matchCount = lines.filter(
|
|
816
|
+
(line) => line.includes(edit.search)
|
|
817
|
+
).length;
|
|
818
|
+
if (matchCount > 1) {
|
|
819
|
+
consola.warn(
|
|
820
|
+
`fix: ambiguous pattern "${edit.search}" matches ${matchCount} lines, using first`
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
const indent = /^(\s*)/.exec(lines[idx])[1];
|
|
824
|
+
switch (edit.type) {
|
|
825
|
+
case "remove": {
|
|
826
|
+
lines.splice(idx, 1);
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
case "replace": {
|
|
830
|
+
lines[idx] = applyIndent(edit.content ?? "", indent);
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
case "add": {
|
|
834
|
+
lines.splice(idx + 1, 0, applyIndent(edit.content ?? "", indent));
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return lines.join("\n");
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
const computeCoupling = (internal, internalNames) => {
|
|
843
|
+
const ca = /* @__PURE__ */ new Map();
|
|
844
|
+
const ce = /* @__PURE__ */ new Map();
|
|
845
|
+
for (const c of internal) {
|
|
846
|
+
ca.set(c.name, 0);
|
|
847
|
+
ce.set(c.name, 0);
|
|
848
|
+
}
|
|
849
|
+
for (const c of internal) {
|
|
850
|
+
for (const rel of c.relations) {
|
|
851
|
+
if (!internalNames.has(rel.to)) continue;
|
|
852
|
+
ce.set(c.name, ce.get(c.name) + 1);
|
|
853
|
+
ca.set(rel.to, ca.get(rel.to) + 1);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return { ca, ce };
|
|
857
|
+
};
|
|
858
|
+
const stableDependenciesRule = {
|
|
859
|
+
name: "stableDependencies",
|
|
860
|
+
description: "Dependencies should point toward more stable containers (instability calculation)",
|
|
861
|
+
check(model) {
|
|
862
|
+
const violations = [];
|
|
863
|
+
const internal = allContainers(model).filter((c) => !c.external);
|
|
864
|
+
const internalNames = new Set(internal.map((c) => c.name));
|
|
865
|
+
const { ca, ce } = computeCoupling(internal, internalNames);
|
|
866
|
+
const instability = (name) => {
|
|
867
|
+
const afferent = ca.get(name);
|
|
868
|
+
const efferent = ce.get(name);
|
|
869
|
+
if (afferent + efferent === 0) return 1;
|
|
870
|
+
return efferent / (afferent + efferent);
|
|
871
|
+
};
|
|
872
|
+
for (const c of internal) {
|
|
873
|
+
for (const rel of c.relations) {
|
|
874
|
+
if (!internalNames.has(rel.to)) continue;
|
|
875
|
+
const iSource = instability(c.name);
|
|
876
|
+
const iTarget = instability(rel.to);
|
|
877
|
+
if (iSource < iTarget) {
|
|
878
|
+
violations.push({
|
|
879
|
+
container: c.name,
|
|
880
|
+
message: `stable module (I=${iSource.toFixed(2)}) depends on less stable "${rel.to}" (I=${iTarget.toFixed(2)}) \u2014 dependencies should point toward stability`
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return violations;
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
const ruleRegistry = [
|
|
890
|
+
aclRule,
|
|
891
|
+
acyclicRule,
|
|
892
|
+
apiGatewayRule,
|
|
893
|
+
crudRule,
|
|
894
|
+
dbPerServiceRule,
|
|
895
|
+
cohesionRule,
|
|
896
|
+
stableDependenciesRule,
|
|
897
|
+
commonReuseRule
|
|
898
|
+
];
|
|
899
|
+
|
|
900
|
+
export { AactConfigSchema as A, aclRule as a, acyclicRule as b, allBoundaries as c, allContainers as d, analyzeArchitecture as e, apiGatewayRule as f, applyEdits as g, canFix as h, canGenerate as i, canLoad as j, cohesionRule as k, commonReuseRule as l, crudRule as m, dbPerServiceRule as n, defineConfig as o, getBoundary as p, getContainer as q, knownFormatNames as r, loadFormat as s, ruleRegistry as t, stableDependenciesRule as u, targetOf as v, walkBoundaries as w };
|