@alveolus/arch 0.3.0 → 0.5.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.
@@ -166,22 +166,26 @@ var Member = class {
166
166
  kind;
167
167
  visibility;
168
168
  isStatic;
169
+ isReadonly;
169
170
  isParameterProperty;
170
171
  isCallable;
171
172
  valueTypes;
172
173
  parameterTypes;
173
174
  returns;
175
+ holdsCollection;
174
176
  constructor(props) {
175
177
  this.name = props.name;
176
178
  this.line = props.line;
177
179
  this.kind = props.kind;
178
180
  this.visibility = props.visibility;
179
181
  this.isStatic = props.isStatic;
182
+ this.isReadonly = props.isReadonly;
180
183
  this.isParameterProperty = props.isParameterProperty;
181
184
  this.isCallable = props.isCallable;
182
185
  this.valueTypes = props.valueTypes;
183
186
  this.parameterTypes = props.parameterTypes;
184
187
  this.returns = props.returns;
188
+ this.holdsCollection = props.holdsCollection;
185
189
  }
186
190
  get isPublicInstance() {
187
191
  return this.visibility === "public" && !this.isStatic;
@@ -227,6 +231,18 @@ var GlobalUse = class {
227
231
  }
228
232
  };
229
233
  //#endregion
234
+ //#region src/model/dependencies/wiring.ts
235
+ var Wiring = class {
236
+ line;
237
+ receiver;
238
+ links;
239
+ constructor(line, receiver, links) {
240
+ this.line = line;
241
+ this.receiver = receiver;
242
+ this.links = links;
243
+ }
244
+ };
245
+ //#endregion
230
246
  //#region src/model/project.ts
231
247
  var Project$1 = class {
232
248
  directory;
@@ -257,6 +273,7 @@ var SourceFile = class {
257
273
  statements;
258
274
  throws;
259
275
  globals;
276
+ wirings;
260
277
  disables;
261
278
  lines;
262
279
  constructor(props) {
@@ -266,6 +283,7 @@ var SourceFile = class {
266
283
  this.statements = props.statements;
267
284
  this.throws = props.throws;
268
285
  this.globals = props.globals;
286
+ this.wirings = props.wirings;
269
287
  this.disables = props.disables;
270
288
  this.lines = props.text.split(/\r?\n/);
271
289
  }
@@ -586,6 +604,60 @@ var CoreApi = class {
586
604
  }
587
605
  };
588
606
  //#endregion
607
+ //#region src/architecture/crossing.ts
608
+ var Crossings = class {
609
+ architecture;
610
+ constructor(architecture) {
611
+ this.architecture = architecture;
612
+ }
613
+ in(file) {
614
+ const crossings = /* @__PURE__ */ new Map();
615
+ for (const wiring of file.wirings) {
616
+ const crossing = this.of(wiring);
617
+ if (crossing !== void 0) crossings.set(`${crossing.line}:${crossing.expression}:${crossing.from}:${crossing.to}`, crossing);
618
+ }
619
+ return [...crossings.values()];
620
+ }
621
+ of(wiring) {
622
+ const receiver = this.architecture.locationOfPath(wiring.receiver);
623
+ if (!receiver.isInBoundedContext || receiver.context === void 0) return;
624
+ for (const link of wiring.links) {
625
+ const source = this.sourceOf(link, receiver);
626
+ if (source !== void 0) return {
627
+ expression: link.text,
628
+ from: receiver.context,
629
+ isOpenHostService: this.isOpenHostService(link, receiver),
630
+ line: wiring.line,
631
+ to: source
632
+ };
633
+ }
634
+ }
635
+ sourceOf(link, receiver) {
636
+ if (link.declaredIn !== void 0) {
637
+ const declared = this.architecture.locationOfPath(link.declaredIn);
638
+ if (declared.isOtherBoundedContextThan(receiver)) return declared.context;
639
+ }
640
+ return this.foreignTypesOf(link, receiver)[0]?.context;
641
+ }
642
+ isOpenHostService(link, receiver) {
643
+ const foreign = link.types.filter((type) => this.isForeignModel(type.declaredIn, receiver));
644
+ return foreign.length > 0 && foreign.every((type) => {
645
+ const declaration = this.architecture.classOf(type);
646
+ return declaration !== void 0 && this.architecture.implementsMarker(declaration, "OpenHostService");
647
+ });
648
+ }
649
+ foreignTypesOf(link, receiver) {
650
+ const locations = [];
651
+ for (const type of link.types) if (this.isForeignModel(type.declaredIn, receiver)) locations.push(this.architecture.locationOfPath(type.declaredIn ?? ""));
652
+ return locations;
653
+ }
654
+ isForeignModel(path, receiver) {
655
+ if (path === void 0) return false;
656
+ const location = this.architecture.locationOfPath(path);
657
+ return location.isOtherBoundedContextThan(receiver) && !location.isCompositionRoot;
658
+ }
659
+ };
660
+ //#endregion
589
661
  //#region src/architecture/layers.ts
590
662
  var Layers = class {
591
663
  isLayer(name) {
@@ -769,6 +841,7 @@ var Architecture = class {
769
841
  shape;
770
842
  layout;
771
843
  locations = /* @__PURE__ */ new Map();
844
+ crossings = new Crossings(this);
772
845
  constructor(project, settings) {
773
846
  this.project = project;
774
847
  this.settings = settings;
@@ -790,6 +863,12 @@ var Architecture = class {
790
863
  locationOf(file) {
791
864
  return this.locate(file.path);
792
865
  }
866
+ locationOfPath(path) {
867
+ return this.locate(path);
868
+ }
869
+ crossingsIn(file) {
870
+ return this.crossings.in(file);
871
+ }
793
872
  locationOfTarget(target) {
794
873
  if (target.visibility !== "analysed") return new Location({
795
874
  area: "outside",
@@ -839,24 +918,22 @@ var Architecture = class {
839
918
  };
840
919
  //#endregion
841
920
  //#region src/architecture/context-map.ts
842
- var ContextMap = class ContextMap {
921
+ var ContextMap = class {
843
922
  upstreams;
844
923
  constructor(upstreams) {
845
924
  this.upstreams = upstreams;
846
925
  }
847
- static ofEdges(edges) {
848
- const upstreams = {};
849
- for (const { from, to } of edges) {
850
- const known = upstreams[from] ?? [];
851
- if (!known.includes(to)) upstreams[from] = [...known, to];
852
- }
853
- return new ContextMap(upstreams);
854
- }
855
926
  get contexts() {
856
- const names = new Set(Object.keys(this.upstreams));
927
+ return Object.keys(this.upstreams);
928
+ }
929
+ get consumed() {
930
+ const names = /* @__PURE__ */ new Set();
857
931
  for (const targets of Object.values(this.upstreams)) for (const target of targets) names.add(target);
858
932
  return [...names];
859
933
  }
934
+ selfConsumers() {
935
+ return this.contexts.filter((context) => this.allows(context, context));
936
+ }
860
937
  allows(from, to) {
861
938
  return (this.upstreams[from] ?? []).includes(to);
862
939
  }
@@ -868,9 +945,6 @@ var ContextMap = class ContextMap {
868
945
  if (found !== void 0) return found;
869
946
  }
870
947
  }
871
- cycleThrough(from, to) {
872
- return this.reaches(to, from, /* @__PURE__ */ new Set());
873
- }
874
948
  cycleFrom(context, path, visiting, done) {
875
949
  if (done.has(context)) return;
876
950
  if (visiting.has(context)) return [...path.slice(path.indexOf(context)), context];
@@ -882,21 +956,17 @@ var ContextMap = class ContextMap {
882
956
  visiting.delete(context);
883
957
  done.add(context);
884
958
  }
885
- reaches(from, target, seen) {
886
- if (from === target) return true;
887
- if (seen.has(from)) return false;
888
- seen.add(from);
889
- return (this.upstreams[from] ?? []).some((upstream) => this.reaches(upstream, target, seen));
890
- }
891
959
  };
892
960
  //#endregion
893
961
  //#region src/rules/framework/wording.ts
894
962
  var Wording = class {
895
963
  target(target, architecture) {
964
+ if (target.visibility === "dynamic") return `code loaded at runtime with ${target.path}`;
896
965
  return `${architecture.relativePath(target.path)} (${this.location(architecture.locationOfTarget(target))})`;
897
966
  }
898
967
  location(location) {
899
968
  if (location.unseen === "ignored") return "ignored by the analysis";
969
+ if (location.unseen === "dynamic") return "loaded at runtime";
900
970
  if (location.unseen === "unresolved") return "not resolved by the analysis";
901
971
  if (location.isOutside) return "outside the declared bounded contexts and shared kernel";
902
972
  if (location.isAtRoot) return "at the root";
@@ -1100,6 +1170,7 @@ var NoOutwardImportRule = class extends ImportRule {
1100
1170
  description: "A dependency pointing away from the domain, a file outside the layers or in the wrong folder of its layer.",
1101
1171
  id: "layers/no-outward-import",
1102
1172
  messages: {
1173
+ compositionRootImport: "The composition root imports {target}: it wires its own context and the shared kernel only.",
1103
1174
  compositionRoots: "{context} has {count} composition roots ({names}): keep one, and move the rest into the layers.",
1104
1175
  directlyInLayer: "The file sits directly in {layer}/: put it in the folder of its kind, such as {example}.",
1105
1176
  drivenTooDeep: "The file is nested too deep: driven/ holds driven/<technology>/<folder>/, such as driven/pg/adapters/.",
@@ -1194,11 +1265,15 @@ var NoOutwardImportRule = class extends ImportRule {
1194
1265
  return findings;
1195
1266
  }
1196
1267
  layerProblem(from, to, architecture) {
1197
- if (from.isAtRoot) return to.isInBoundedContext && !to.isCompositionRoot ? {
1268
+ if (from.isAtRoot) return to.isOutside || to.isInBoundedContext && !to.isCompositionRoot ? {
1198
1269
  data: {},
1199
1270
  messageId: "rootImport"
1200
1271
  } : void 0;
1201
- if (from.isCompositionRoot || to.isOtherBoundedContextThan(from)) return;
1272
+ if (from.isCompositionRoot) return to.isOutside ? {
1273
+ data: {},
1274
+ messageId: "compositionRootImport"
1275
+ } : void 0;
1276
+ if (to.isOtherBoundedContextThan(from)) return;
1202
1277
  if (to.isCompositionRoot) return {
1203
1278
  data: {},
1204
1279
  messageId: "wiring"
@@ -1260,16 +1335,28 @@ var NoPortlessAdapterRule = class extends ClassRule {
1260
1335
  var NoCrossContextImportRule = class extends ImportRule {
1261
1336
  meta = {
1262
1337
  contexts: "every",
1263
- description: "An import from another bounded context that is not its open host service, a composition root that re-exports.",
1338
+ description: "An import from another bounded context that is not its open host service, a file the analysis does not see, a composition root that re-exports, a value of another context given in the wiring that is not its open host service.",
1264
1339
  id: "strategic/no-cross-context-import",
1265
1340
  messages: {
1341
+ dynamicLoad: "Loads code at runtime with {loader}: the analysis cannot tell which bounded context it reaches; use a static import.",
1266
1342
  notOpenHostService: "Imports {target}: only an OpenHostService of another bounded context may be imported.",
1267
1343
  outsideAntiCorruptionLayer: "Uses the open host service of {context} outside an AntiCorruptionLayer: a core context translates what it consumes in an anti-corruption layer.",
1268
1344
  publishedLanguage: "Imports the published language of {context}: redeclare the fields you read in your own published-language/.",
1269
1345
  reexport: "The composition root re-exports {names}: it exports its own module only, so that no other context reaches through it.",
1270
- sharedKernel: "The shared kernel imports no bounded context, but imports {target}."
1346
+ sharedKernel: "The shared kernel imports no bounded context, but imports {target}.",
1347
+ unseen: "Imports {target}: the analysis cannot tell which bounded context it reaches; move the file into a bounded context or the shared kernel.",
1348
+ wiredModel: "Gives {expression}, from {to}, to {from}: only an OpenHostService of another bounded context may cross, in an import or in the wiring."
1271
1349
  }
1272
1350
  };
1351
+ check(architecture) {
1352
+ const findings = super.check(architecture);
1353
+ for (const file of this.filesOf(architecture)) for (const crossing of architecture.crossingsIn(file)) if (!crossing.isOpenHostService) findings.push(this.finding(file, crossing.line, crossing.expression, "wiredModel", {
1354
+ expression: crossing.expression,
1355
+ from: crossing.from,
1356
+ to: crossing.to
1357
+ }));
1358
+ return findings;
1359
+ }
1273
1360
  appliesTo(file, architecture) {
1274
1361
  const location = architecture.locationOf(file);
1275
1362
  return location.isInBoundedContext || location.isInSharedKernel;
@@ -1280,6 +1367,8 @@ var NoCrossContextImportRule = class extends ImportRule {
1280
1367
  const target = dependency.target;
1281
1368
  if (target.kind !== "file") return;
1282
1369
  const to = architecture.locationOfTarget(target);
1370
+ if (target.visibility === "dynamic") return this.finding(file, dependency.line, dependency.label, "dynamicLoad", { loader: target.path });
1371
+ if (to.isOutside) return this.finding(file, dependency.line, dependency.label, "unseen", { target: this.wording.target(target, architecture) });
1283
1372
  if (!to.isOtherBoundedContextThan(from)) return;
1284
1373
  if (from.isInSharedKernel) return this.finding(file, dependency.line, dependency.label, "sharedKernel", { target: this.wording.target(target, architecture) });
1285
1374
  if (from.isCompositionRoot && to.isCompositionRoot) return;
@@ -1370,27 +1459,60 @@ var NoLeakyHostServiceRule = class extends ClassRule {
1370
1459
  }
1371
1460
  };
1372
1461
  //#endregion
1462
+ //#region src/rules/strategic/no-shared-state.rule.ts
1463
+ var NoSharedStateRule = class extends ClassRule {
1464
+ meta = {
1465
+ contexts: "every",
1466
+ description: "A static field of the shared kernel that holds state: one without readonly, or one holding a collection.",
1467
+ id: "strategic/no-shared-state",
1468
+ messages: {
1469
+ collection: "{class}.{field} holds a collection in a static field: every context reaches the same one, a channel the context map does not show. The shared kernel shares a model, not state: integrate through an open host service.",
1470
+ mutable: "{class}.{field} is a static field without readonly: every context reaches the same one, a channel the context map does not show. The shared kernel shares a model, not state: integrate through an open host service."
1471
+ }
1472
+ };
1473
+ findingsFor(codeClass, file, architecture) {
1474
+ if (!architecture.locationOf(file).isInSharedKernel) return [];
1475
+ const findings = [];
1476
+ for (const member of codeClass.members) {
1477
+ const messageId = this.stateOf(member);
1478
+ if (messageId !== void 0) findings.push(this.finding(file, member.line, member.name, messageId, {
1479
+ class: codeClass.name,
1480
+ field: member.name
1481
+ }));
1482
+ }
1483
+ return findings;
1484
+ }
1485
+ stateOf(member) {
1486
+ if (!member.isStatic || member.kind !== "field") return;
1487
+ if (!member.isReadonly) return "mutable";
1488
+ return member.holdsCollection ? "collection" : void 0;
1489
+ }
1490
+ };
1491
+ //#endregion
1373
1492
  //#region src/rules/strategic/no-unmapped-context.rule.ts
1374
1493
  var NoUnmappedContextRule = class extends Rule {
1375
1494
  meta = {
1376
1495
  contexts: "every",
1377
- description: "A bounded context consuming one the context map does not allow, or two contexts that depend on each other.",
1496
+ description: "A bounded context consuming one the context map does not allow, through an import or through the wiring.",
1378
1497
  id: "strategic/no-unmapped-context",
1379
1498
  messages: {
1380
- cycle: "{from} consumes {to}, which consumes {from} back: two contexts that depend on each other can no longer change alone; declare a contextMap and reverse one dependency.",
1381
- unmapped: "{from} consumes {to}, which the context map does not allow: add {to} to contextMap.{from}, or reverse the dependency."
1499
+ unmapped: "{from} consumes {to}, which the context map does not allow: reverse the dependency, or if {from} really is downstream of {to}, add {to} to contextMap.{from}.consumes.",
1500
+ unmappedWiring: "{from} receives {expression} from {to} here, which the context map does not allow: reverse the dependency, or if {from} really is downstream of {to}, add {to} to contextMap.{from}.consumes."
1382
1501
  }
1383
1502
  };
1384
1503
  check(architecture) {
1385
- const consumptions = this.consumptionsIn(architecture);
1386
- const declared = architecture.contextMap;
1387
1504
  const findings = [];
1388
- if (declared !== void 0) {
1389
- for (const consumption of consumptions) if (!declared.allows(consumption.from, consumption.to)) findings.push(this.consumptionFinding(consumption, "unmapped"));
1390
- return findings;
1391
- }
1392
- const observed = ContextMap.ofEdges(consumptions);
1393
- for (const consumption of consumptions) if (observed.cycleThrough(consumption.from, consumption.to)) findings.push(this.consumptionFinding(consumption, "cycle"));
1505
+ for (const consumption of this.consumptionsIn(architecture)) if (!architecture.contextMap.allows(consumption.from, consumption.to)) findings.push(this.consumptionFinding(consumption));
1506
+ findings.push(...this.unmappedWirings(architecture));
1507
+ return findings;
1508
+ }
1509
+ unmappedWirings(architecture) {
1510
+ const findings = [];
1511
+ for (const file of this.filesOf(architecture)) for (const crossing of architecture.crossingsIn(file)) if (!architecture.contextMap.allows(crossing.from, crossing.to)) findings.push(this.finding(file, crossing.line, crossing.expression, "unmappedWiring", {
1512
+ expression: crossing.expression,
1513
+ from: crossing.from,
1514
+ to: crossing.to
1515
+ }));
1394
1516
  return findings;
1395
1517
  }
1396
1518
  consumptionsIn(architecture) {
@@ -1411,9 +1533,9 @@ var NoUnmappedContextRule = class extends Rule {
1411
1533
  }
1412
1534
  return consumptions;
1413
1535
  }
1414
- consumptionFinding(consumption, messageId) {
1536
+ consumptionFinding(consumption) {
1415
1537
  const { dependency, file, from, to } = consumption;
1416
- return this.finding(file, dependency.line, dependency.label, messageId, {
1538
+ return this.finding(file, dependency.line, dependency.label, "unmapped", {
1417
1539
  from,
1418
1540
  to
1419
1541
  });
@@ -1851,6 +1973,7 @@ const ruleIds = [
1851
1973
  "strategic/no-leaky-host-service",
1852
1974
  "strategic/no-unmapped-context",
1853
1975
  "strategic/no-fat-shared-kernel",
1976
+ "strategic/no-shared-state",
1854
1977
  "layers/no-impure-domain",
1855
1978
  "layers/no-outward-import",
1856
1979
  "layers/no-portless-adapter",
@@ -1871,6 +1994,7 @@ var RuleRegistry = class {
1871
1994
  new NoLeakyHostServiceRule(),
1872
1995
  new NoUnmappedContextRule(),
1873
1996
  new NoFatSharedKernelRule(),
1997
+ new NoSharedStateRule(),
1874
1998
  new NoImpureDomainRule(),
1875
1999
  new NoOutwardImportRule(),
1876
2000
  new NoPortlessAdapterRule(),
@@ -2109,6 +2233,7 @@ var Config = class {
2109
2233
  contextMap;
2110
2234
  ignored;
2111
2235
  rules;
2236
+ layout;
2112
2237
  constructor(config, projectDir) {
2113
2238
  this.projectDir = resolve(projectDir);
2114
2239
  this.rootDir = resolve(projectDir, config.root);
@@ -2118,7 +2243,7 @@ var Config = class {
2118
2243
  this.applicationDependencies = this.domainDependencies.with(new AllowedPackages(config.applicationDependencies));
2119
2244
  this.ignored = [...testFiles, ...config.ignore ?? []];
2120
2245
  this.extraFolders = config.layout?.extraFolders ?? {};
2121
- this.contextMap = config.contextMap === void 0 ? void 0 : this.validContextMap(config.contextMap, Object.keys(config.boundedContexts));
2246
+ this.contextMap = this.validContextMap(config.contextMap, Object.keys(config.boundedContexts));
2122
2247
  this.rules = config.rules;
2123
2248
  const sharedKernel = config.sharedKernel ?? "shared-kernel";
2124
2249
  this.contextFolders = [...this.classifiedContexts(config.subdomains ?? {}, config.boundedContexts), {
@@ -2126,6 +2251,7 @@ var Config = class {
2126
2251
  isSharedKernel: true,
2127
2252
  name: "shared kernel"
2128
2253
  }];
2254
+ this.layout = new Layout(this);
2129
2255
  }
2130
2256
  classifiedContexts(subdomains, boundedContexts) {
2131
2257
  const types = [
@@ -2152,10 +2278,16 @@ var Config = class {
2152
2278
  if (unclassified.length > 0) throw new Error(`boundedContexts declares ${unclassified.join(", ")}, which subdomains does not classify: list each context under subdomains.core, subdomains.supporting or subdomains.generic.`);
2153
2279
  return folders;
2154
2280
  }
2155
- validContextMap(upstreams, contexts) {
2281
+ validContextMap(relations, contexts) {
2282
+ const upstreams = {};
2283
+ for (const [name, { consumes }] of Object.entries(relations)) upstreams[name] = consumes;
2156
2284
  const map = new ContextMap(upstreams);
2157
- const unknown = map.contexts.filter((name) => !contexts.includes(name));
2285
+ const unknown = [.../* @__PURE__ */ new Set([...map.contexts, ...map.consumed])].filter((name) => !contexts.includes(name));
2158
2286
  if (unknown.length > 0) throw new Error(`contextMap names ${unknown.join(", ")}, which boundedContexts does not declare.`);
2287
+ const unlisted = contexts.filter((name) => !map.contexts.includes(name));
2288
+ if (unlisted.length > 0) throw new Error(`contextMap does not list ${unlisted.join(", ")}: every bounded context lists the contexts it consumes, consumes: [] when none.`);
2289
+ const selfConsumers = map.selfConsumers();
2290
+ if (selfConsumers.length > 0) throw new Error(`contextMap lists ${selfConsumers.join(", ")} as consuming itself: a context consumes other contexts only.`);
2159
2291
  const cycle = map.cycle();
2160
2292
  if (cycle !== void 0) throw new Error(`contextMap has a cycle: ${cycle.join(" → ")}. Two contexts that depend on each other can no longer change alone: reverse one dependency.`);
2161
2293
  return map;
@@ -2164,6 +2296,10 @@ var Config = class {
2164
2296
  const projectPath = relative(this.projectDir, path).split(sep).join("/");
2165
2297
  return this.ignored.some((glob) => matchesGlob(projectPath, glob));
2166
2298
  }
2299
+ readsWiring(path) {
2300
+ const location = this.layout.locate(path);
2301
+ return location.isAtRoot || location.isCompositionRoot;
2302
+ }
2167
2303
  severityOf(rule) {
2168
2304
  return this.rules?.[rule] ?? "error";
2169
2305
  }
@@ -2182,7 +2318,7 @@ const schema = z.strictObject({
2182
2318
  applicationDependencies: packageDependencies.exactOptional(),
2183
2319
  boundedContexts: z.record(z.string(), z.string()),
2184
2320
  compositionRoot: z.string().exactOptional(),
2185
- contextMap: z.record(z.string(), z.array(z.string())).exactOptional(),
2321
+ contextMap: z.record(z.string(), z.strictObject({ consumes: z.array(z.string()) })),
2186
2322
  domainDependencies: packageDependencies.exactOptional(),
2187
2323
  ignore: z.array(z.string()).exactOptional(),
2188
2324
  layout: z.strictObject({ extraFolders: z.strictObject({
@@ -2268,6 +2404,7 @@ var ClassReader = class {
2268
2404
  const type = member.getType();
2269
2405
  const [signature] = type.getCallSignatures();
2270
2406
  return [this.member(member, "field", {
2407
+ holdsCollection: this.types.holdsCollection(type),
2271
2408
  isCallable: signature !== void 0,
2272
2409
  returns: signature === void 0 ? void 0 : this.types.returnShapeOf(signature.getReturnType()),
2273
2410
  valueTypes: this.types.classTypesIn(type)
@@ -2297,8 +2434,10 @@ var ClassReader = class {
2297
2434
  const isStatic = Node.isStaticable(member) && member.isStatic();
2298
2435
  const scope = Node.isScoped(member) ? member.getScope() : Scope.Public;
2299
2436
  return new Member({
2437
+ holdsCollection: types.holdsCollection ?? false,
2300
2438
  isCallable: types.isCallable ?? false,
2301
2439
  isParameterProperty: false,
2440
+ isReadonly: Node.isReadonlyable(member) && member.isReadonly(),
2302
2441
  isStatic,
2303
2442
  kind,
2304
2443
  line: member.getStartLineNumber(),
@@ -2314,8 +2453,10 @@ var ClassReader = class {
2314
2453
  }
2315
2454
  constructorParameter(parameter) {
2316
2455
  return new Member({
2456
+ holdsCollection: this.types.holdsCollection(parameter.getType()),
2317
2457
  isCallable: false,
2318
2458
  isParameterProperty: parameter.isParameterProperty(),
2459
+ isReadonly: parameter.isReadonly(),
2319
2460
  isStatic: false,
2320
2461
  kind: "constructor parameter",
2321
2462
  line: parameter.getStartLineNumber(),
@@ -2355,6 +2496,19 @@ var ClassReader = class {
2355
2496
  //#endregion
2356
2497
  //#region src/importer/ts-morph/readers/dependency-reader.ts
2357
2498
  const everything = ["*"];
2499
+ const loaderPackages = [
2500
+ "module",
2501
+ "node:module",
2502
+ "vm",
2503
+ "node:vm"
2504
+ ];
2505
+ const globalLoaders = ["eval", "Function"];
2506
+ const loaderMembers = [
2507
+ "eval",
2508
+ "Function",
2509
+ "require",
2510
+ "getBuiltinModule"
2511
+ ];
2358
2512
  var DependencyReader = class {
2359
2513
  project;
2360
2514
  scope;
@@ -2374,7 +2528,17 @@ var DependencyReader = class {
2374
2528
  for (const declaration of file.getExportDeclarations()) if (declaration.hasModuleSpecifier()) references.push(this.fromDeclaration(declaration, "re-export"));
2375
2529
  for (const declaration of file.getDescendantsOfKind(SyntaxKind.ImportEqualsDeclaration)) references.push(...this.fromImportEquals(declaration));
2376
2530
  for (const node of file.getDescendantsOfKind(SyntaxKind.ImportType)) references.push(this.fromImportType(node));
2377
- for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) references.push(...this.fromCall(call));
2531
+ for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
2532
+ references.push(...this.fromCall(call));
2533
+ references.push(...this.fromLoader(call));
2534
+ }
2535
+ for (const construction of file.getDescendantsOfKind(SyntaxKind.NewExpression)) references.push(...this.fromLoader(construction));
2536
+ for (const reference of file.getPathReferenceDirectives()) references.push({
2537
+ ...this.fromDirective(reference, file),
2538
+ isPath: true
2539
+ });
2540
+ for (const reference of file.getTypeReferenceDirectives()) references.push(this.fromDirective(reference, file));
2541
+ for (const declaration of file.getDescendantsOfKind(SyntaxKind.ModuleDeclaration)) references.push(...this.fromAugmentation(declaration));
2378
2542
  return references;
2379
2543
  }
2380
2544
  fromDeclaration(declaration, form) {
@@ -2414,6 +2578,47 @@ var DependencyReader = class {
2414
2578
  if (callee.isKind(SyntaxKind.Identifier) && callee.getText() === "require") return [this.reference(call, "require", argument, everything)];
2415
2579
  return [];
2416
2580
  }
2581
+ fromDirective(reference, file) {
2582
+ const line = file.getLineAndColumnAtPos(reference.getPos()).line;
2583
+ const specifier = reference.getFileName();
2584
+ return {
2585
+ form: "reference",
2586
+ line,
2587
+ names: everything,
2588
+ specifier,
2589
+ text: specifier
2590
+ };
2591
+ }
2592
+ fromAugmentation(declaration) {
2593
+ const name = declaration.getNameNode();
2594
+ if (!name.isKind(SyntaxKind.StringLiteral) || name.getLiteralValue().includes("*")) return [];
2595
+ return [this.reference(declaration, "augmentation", name, everything)];
2596
+ }
2597
+ fromLoader(call) {
2598
+ const loader = this.loaderOf(call.getExpression());
2599
+ if (loader === void 0) return [];
2600
+ return [{
2601
+ form: "dynamic load",
2602
+ line: call.getStartLineNumber(),
2603
+ names: [],
2604
+ specifier: loader,
2605
+ text: loader
2606
+ }];
2607
+ }
2608
+ loaderOf(callee) {
2609
+ const expression = this.unwrapped(callee);
2610
+ if (expression.isKind(SyntaxKind.Identifier)) return globalLoaders.includes(expression.getText()) && !this.isDeclaredInProject(expression) ? expression.getText() : void 0;
2611
+ if (expression.isKind(SyntaxKind.PropertyAccessExpression) && loaderMembers.includes(expression.getName())) return expression.getText();
2612
+ }
2613
+ unwrapped(expression) {
2614
+ if (expression.isKind(SyntaxKind.ParenthesizedExpression)) return this.unwrapped(expression.getExpression());
2615
+ if (expression.isKind(SyntaxKind.BinaryExpression) && expression.getOperatorToken().isKind(SyntaxKind.CommaToken)) return this.unwrapped(expression.getRight());
2616
+ return expression;
2617
+ }
2618
+ isDeclaredInProject(expression) {
2619
+ const declaration = expression.getSymbol()?.getDeclarations()[0];
2620
+ return declaration !== void 0 && this.isInsideProject(normalize(declaration.getSourceFile().getFilePath()));
2621
+ }
2417
2622
  reference(node, form, specifierNode, names) {
2418
2623
  const isLiteral = specifierNode.isKind(SyntaxKind.StringLiteral) || specifierNode.isKind(SyntaxKind.NoSubstitutionTemplateLiteral);
2419
2624
  return {
@@ -2425,15 +2630,20 @@ var DependencyReader = class {
2425
2630
  };
2426
2631
  }
2427
2632
  targetOf(reference, file) {
2633
+ if (reference.form === "dynamic load" || loaderPackages.includes(reference.specifier ?? "")) return {
2634
+ kind: "file",
2635
+ path: reference.text,
2636
+ visibility: "dynamic"
2637
+ };
2428
2638
  const directory = file.getDirectoryPath();
2429
2639
  if (reference.specifier === void 0) return {
2430
2640
  kind: "file",
2431
2641
  path: normalize(join(directory, reference.text)),
2432
2642
  visibility: "unresolved"
2433
2643
  };
2434
- const resolved = this.resolve(reference.specifier, file);
2644
+ const resolved = reference.isPath === true ? this.referencedPath(reference.specifier, directory) : this.resolve(reference.specifier, file);
2435
2645
  if (resolved !== void 0 && this.isInsideProject(resolved)) return this.fileTarget(resolved);
2436
- if (reference.specifier.startsWith(".")) return {
2646
+ if (reference.isPath === true || reference.specifier.startsWith(".")) return {
2437
2647
  kind: "file",
2438
2648
  path: normalize(join(directory, reference.specifier)),
2439
2649
  visibility: "unresolved"
@@ -2454,6 +2664,10 @@ var DependencyReader = class {
2454
2664
  const resolved = ts.resolveModuleName(specifier, file.getFilePath(), this.project.getCompilerOptions(), this.project.getModuleResolutionHost()).resolvedModule?.resolvedFileName;
2455
2665
  return resolved === void 0 ? void 0 : normalize(resolved);
2456
2666
  }
2667
+ referencedPath(specifier, directory) {
2668
+ const path = normalize(join(directory, specifier));
2669
+ return this.project.getModuleResolutionHost().fileExists(path) ? path : void 0;
2670
+ }
2457
2671
  packageNameOf(specifier) {
2458
2672
  const segments = specifier.split("/");
2459
2673
  const length = specifier.startsWith("@") ? 2 : 1;
@@ -2639,6 +2853,12 @@ var ThrowReader = class {
2639
2853
  };
2640
2854
  //#endregion
2641
2855
  //#region src/importer/ts-morph/readers/type-reader.ts
2856
+ const collections = [
2857
+ "Map",
2858
+ "Set",
2859
+ "WeakMap",
2860
+ "WeakSet"
2861
+ ];
2642
2862
  var TypeReader = class {
2643
2863
  packageNames;
2644
2864
  constructor(packageNames) {
@@ -2661,6 +2881,12 @@ var TypeReader = class {
2661
2881
  packageName: void 0
2662
2882
  } : this.namedSymbol(symbol);
2663
2883
  }
2884
+ holdsCollection(type) {
2885
+ if (type.isArray() || type.isTuple()) return !type.isReadonlyArray();
2886
+ if (collections.includes(type.getSymbol()?.getName() ?? "")) return true;
2887
+ if (type.getStringIndexType() !== void 0 || type.getNumberIndexType() !== void 0) return true;
2888
+ return type.isAnonymous() && type.getCallSignatures().length === 0 && type.getConstructSignatures().length === 0;
2889
+ }
2664
2890
  returnShapeOf(type) {
2665
2891
  if (type.getSymbol()?.getName() === "Promise") {
2666
2892
  const [inner] = type.getTypeArguments();
@@ -2725,17 +2951,98 @@ var TypeReader = class {
2725
2951
  }
2726
2952
  };
2727
2953
  //#endregion
2954
+ //#region src/importer/ts-morph/readers/wiring-reader.ts
2955
+ const references = [SyntaxKind.Identifier, SyntaxKind.PropertyAccessExpression];
2956
+ var WiringReader = class {
2957
+ types;
2958
+ scope;
2959
+ constructor(types, scope) {
2960
+ this.types = types;
2961
+ this.scope = scope;
2962
+ }
2963
+ read(file) {
2964
+ if (!this.scope.readsWiring(file.getFilePath())) return [];
2965
+ const wirings = [];
2966
+ for (const site of this.sitesOf(file)) {
2967
+ const receiver = this.declaringFileOf(site.receiver);
2968
+ if (receiver === void 0) continue;
2969
+ for (const value of site.values) for (const reference of this.referencesIn(value)) wirings.push(new Wiring(reference.getStartLineNumber(), receiver, this.linksOf(reference)));
2970
+ }
2971
+ return wirings;
2972
+ }
2973
+ sitesOf(file) {
2974
+ const sites = [];
2975
+ file.forEachDescendant((node) => {
2976
+ if (Node.isCallExpression(node) || Node.isNewExpression(node)) sites.push({
2977
+ receiver: node.getExpression(),
2978
+ values: node.getArguments()
2979
+ });
2980
+ else if (Node.isBinaryExpression(node) && node.getOperatorToken().isKind(SyntaxKind.EqualsToken)) sites.push({
2981
+ receiver: node.getLeft(),
2982
+ values: [node.getRight()]
2983
+ });
2984
+ else if ((Node.isVariableDeclaration(node) || Node.isPropertyDeclaration(node)) && node.getTypeNode() !== void 0) {
2985
+ const initializer = node.getInitializer();
2986
+ sites.push({
2987
+ receiver: node.getTypeNode(),
2988
+ values: initializer === void 0 ? [] : [initializer]
2989
+ });
2990
+ }
2991
+ });
2992
+ return sites;
2993
+ }
2994
+ referencesIn(value) {
2995
+ const found = [];
2996
+ const visit = (node) => {
2997
+ if (this.isOutermostReference(node)) found.push(node);
2998
+ node.forEachChild(visit);
2999
+ };
3000
+ visit(value);
3001
+ return found;
3002
+ }
3003
+ isOutermostReference(node) {
3004
+ if (!references.includes(node.getKind())) return false;
3005
+ const parent = node.getParent();
3006
+ if (parent === void 0 || Node.isPropertyAccessExpression(parent)) return false;
3007
+ if (Node.isTypeNode(parent) || Node.isQualifiedName(parent) || Node.isExpressionWithTypeArguments(parent)) return false;
3008
+ return Node.isShorthandPropertyAssignment(parent) || !(Node.hasName(parent) && parent.getNameNode() === node);
3009
+ }
3010
+ linksOf(reference) {
3011
+ if (Node.isPropertyAccessExpression(reference)) return [...this.linksOf(reference.getExpression()), this.link(reference, reference.getNameNode())];
3012
+ if (Node.isIdentifier(reference)) return [this.link(reference, reference)];
3013
+ return [];
3014
+ }
3015
+ link(expression, name) {
3016
+ return {
3017
+ declaredIn: this.declaringFileOf(name),
3018
+ text: expression.getText(),
3019
+ types: this.types.classTypesIn(expression.getType())
3020
+ };
3021
+ }
3022
+ declaringFileOf(node) {
3023
+ if (node === void 0) return;
3024
+ const named = Node.isTypeNode(node) ? node.getType().getAliasSymbol() ?? node.getType().getSymbol() : this.nameOf(node).getSymbol();
3025
+ const declaration = (named?.isAlias() === true ? named.getAliasedSymbol() : named)?.getDeclarations()[0];
3026
+ return declaration === void 0 ? void 0 : normalize(declaration.getSourceFile().getFilePath());
3027
+ }
3028
+ nameOf(node) {
3029
+ return Node.isPropertyAccessExpression(node) ? node.getNameNode() : node;
3030
+ }
3031
+ };
3032
+ //#endregion
2728
3033
  //#region src/importer/ts-morph/ts-morph-importer.ts
2729
3034
  var TsMorphImporter = class TsMorphImporter extends Importer {
2730
3035
  sources;
2731
3036
  classes;
3037
+ types;
2732
3038
  statements = new StatementReader();
2733
3039
  disables = new DisableReader();
2734
3040
  throws = new ThrowReader();
2735
3041
  constructor(sources) {
2736
3042
  super();
2737
3043
  this.sources = sources;
2738
- this.classes = new ClassReader(new TypeReader(new PackageNames(sources.getFileSystem())));
3044
+ this.types = new TypeReader(new PackageNames(sources.getFileSystem()));
3045
+ this.classes = new ClassReader(this.types);
2739
3046
  }
2740
3047
  static fromTsConfig(tsConfigFilePath) {
2741
3048
  if (!existsSync(tsConfigFilePath)) throw new Error(`No TypeScript configuration found at ${tsConfigFilePath}: pass --tsconfig, or set tsconfig in alveolus.config.ts.`);
@@ -2747,12 +3054,13 @@ var TsMorphImporter = class TsMorphImporter extends Importer {
2747
3054
  }
2748
3055
  read(scope) {
2749
3056
  const dependencies = new DependencyReader(this.sources, scope);
3057
+ const wirings = new WiringReader(this.types, scope);
2750
3058
  const globals = new GlobalReader(scope.projectDir);
2751
3059
  const files = [];
2752
- for (const file of this.sources.getSourceFiles()) if (this.isInside(scope.rootDir, file.getFilePath()) && !scope.isIgnored(file.getFilePath())) files.push(this.readFile(file, dependencies, globals));
3060
+ for (const file of this.sources.getSourceFiles()) if (this.isInside(scope.rootDir, file.getFilePath()) && !scope.isIgnored(file.getFilePath())) files.push(this.readFile(file, dependencies, globals, wirings));
2753
3061
  return new Project$1(scope.projectDir, files);
2754
3062
  }
2755
- readFile(file, dependencies, globals) {
3063
+ readFile(file, dependencies, globals, wirings) {
2756
3064
  const globalReferences = globals.read(file);
2757
3065
  return new SourceFile({
2758
3066
  classes: file.getClasses().map((declaration) => this.classes.read(declaration)),
@@ -2762,7 +3070,8 @@ var TsMorphImporter = class TsMorphImporter extends Importer {
2762
3070
  path: normalize(file.getFilePath()),
2763
3071
  statements: this.statements.read(file),
2764
3072
  text: file.getFullText(),
2765
- throws: this.throws.read(file)
3073
+ throws: this.throws.read(file),
3074
+ wirings: wirings.read(file)
2766
3075
  });
2767
3076
  }
2768
3077
  globalUsesOf(references) {
@@ -2781,6 +3090,7 @@ const scaffolds = [
2781
3090
 
2782
3091
  export default defineConfig({
2783
3092
  boundedContexts: {},
3093
+ contextMap: {},
2784
3094
  root: "src",
2785
3095
  });
2786
3096
  `,
@@ -2799,7 +3109,7 @@ The documentation is installed with the package: read it with \`npx alveolus exp
2799
3109
  1. Before writing a class, read its building block: \`npx alveolus explain aggregates\`, \`entities\`, \`value-objects\`, \`domain-events\`, \`domain-errors\`, \`ports\`, \`repositories\`, \`command-handlers\`, \`query-handlers\`, \`result\`.
2800
3110
  2. Where a file goes and what each layer may import: \`npx alveolus explain project-layout\`.
2801
3111
  3. After each change, run \`npx alveolus arch check\`.
2802
- 4. On a violation, read the rule before changing the code: \`npx alveolus explain <rule>\`, with the rule id of the report, such as \`layers/no-impure-domain\`. Fix the cause: never turn a rule off, add a disable comment or edit \`alveolus.baseline.json\` by hand without asking.
3112
+ 4. On a violation, read the rule before changing the code: \`npx alveolus explain <rule>\`, with the rule id of the report, such as \`layers/no-impure-domain\`. Fix the cause: never turn a rule off, add a disable comment, add a context to \`contextMap\` or edit \`alveolus.baseline.json\` by hand without asking.
2803
3113
  `,
2804
3114
  path: ".claude/skills/alveolus/SKILL.md"
2805
3115
  },
@@ -3056,4 +3366,4 @@ var Docs = class Docs {
3056
3366
  //#endregion
3057
3367
  export { TsMorphImporter as a, Config as c, RuleRegistry as d, Rule as f, Baseline as h, Init as i, Report as l, AllowedPackages as m, Page as n, Importer as o, Architecture as p, Cli as r, ConfigLoader as s, Docs as t, Checker as u };
3058
3368
 
3059
- //# sourceMappingURL=docs-DsQHpTtV.mjs.map
3369
+ //# sourceMappingURL=docs-B1Tzkjrn.mjs.map