@alveolus/arch 0.1.0 → 0.3.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.
Files changed (60) hide show
  1. package/README.md +8 -1
  2. package/dist/bin.mjs +4 -2
  3. package/dist/bin.mjs.map +1 -1
  4. package/dist/{cli-CwPCGjDg.mjs → docs-DsQHpTtV.mjs} +287 -38
  5. package/dist/docs-DsQHpTtV.mjs.map +1 -0
  6. package/dist/index.d.mts +90 -36
  7. package/dist/index.d.mts.map +1 -1
  8. package/dist/index.mjs +2 -2
  9. package/docs/core/application/command-handlers.md +617 -0
  10. package/docs/core/application/event-publishers.md +234 -0
  11. package/docs/core/application/event-translators.md +329 -0
  12. package/docs/core/application/index.md +99 -0
  13. package/docs/core/application/integration-events.md +277 -0
  14. package/docs/core/application/outbox.md +416 -0
  15. package/docs/core/application/query-handlers.md +292 -0
  16. package/docs/core/application/unit-of-work.md +352 -0
  17. package/docs/core/domain/aggregates.md +822 -0
  18. package/docs/core/domain/domain-errors.md +251 -0
  19. package/docs/core/domain/domain-events.md +292 -0
  20. package/docs/core/domain/domain-services.md +249 -0
  21. package/docs/core/domain/entities.md +431 -0
  22. package/docs/core/domain/index.md +93 -0
  23. package/docs/core/domain/ports.md +284 -0
  24. package/docs/core/domain/repositories.md +335 -0
  25. package/docs/core/domain/value-objects.md +425 -0
  26. package/docs/core/domain/views.md +265 -0
  27. package/docs/core/index.md +108 -0
  28. package/docs/core/strategic/anti-corruption-layers.md +349 -0
  29. package/docs/core/strategic/index.md +83 -0
  30. package/docs/core/strategic/open-host-services.md +287 -0
  31. package/docs/core/strategic/published-language.md +265 -0
  32. package/docs/core/utilities/result.md +413 -0
  33. package/docs/guide/agents.md +68 -0
  34. package/docs/guide/existing-project.md +105 -0
  35. package/docs/guide/getting-started.md +275 -0
  36. package/docs/guide/learning-path.md +123 -0
  37. package/docs/guide/project-layout.md +324 -0
  38. package/docs/guide/versioning.md +42 -0
  39. package/docs/integrations/index.md +112 -0
  40. package/docs/integrations/nestjs.md +169 -0
  41. package/docs/rules/index.md +183 -0
  42. package/docs/rules/layers/no-driving-shortcut.md +119 -0
  43. package/docs/rules/layers/no-impure-domain.md +189 -0
  44. package/docs/rules/layers/no-outward-import.md +184 -0
  45. package/docs/rules/layers/no-portless-adapter.md +123 -0
  46. package/docs/rules/strategic/no-cross-context-import.md +140 -0
  47. package/docs/rules/strategic/no-fat-shared-kernel.md +81 -0
  48. package/docs/rules/strategic/no-leaky-host-service.md +107 -0
  49. package/docs/rules/strategic/no-unmapped-context.md +111 -0
  50. package/docs/rules/tactical/no-aggregate-reference.md +139 -0
  51. package/docs/rules/tactical/no-foreign-command-dependency.md +119 -0
  52. package/docs/rules/tactical/no-foreign-query-dependency.md +106 -0
  53. package/docs/rules/tactical/no-loose-code.md +171 -0
  54. package/docs/rules/tactical/no-misplaced-class.md +146 -0
  55. package/docs/rules/tactical/no-public-field.md +113 -0
  56. package/docs/rules/tactical/no-stateful-service.md +102 -0
  57. package/docs/rules/tactical/no-thrown-failure.md +162 -0
  58. package/docs/rules/tooling/no-loose-disable.md +98 -0
  59. package/package.json +4 -3
  60. package/dist/cli-CwPCGjDg.mjs.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { Command, CommanderError, Option } from "commander";
2
- import { existsSync } from "node:fs";
2
+ import { existsSync, globSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { basename, dirname, isAbsolute, join, matchesGlob, normalize, relative, resolve, sep } from "node:path";
4
4
  import { readFile, writeFile } from "node:fs/promises";
5
5
  import { createHash } from "node:crypto";
@@ -640,6 +640,7 @@ var LayerShape = class {
640
640
  var Location = class {
641
641
  area;
642
642
  context;
643
+ subdomain;
643
644
  layer;
644
645
  folder;
645
646
  foldersInLayer;
@@ -649,6 +650,7 @@ var Location = class {
649
650
  constructor(props) {
650
651
  this.area = props.area;
651
652
  this.context = props.context;
653
+ this.subdomain = props.subdomain;
652
654
  this.layer = props.layer;
653
655
  this.folder = props.folder;
654
656
  this.foldersInLayer = props.foldersInLayer ?? [];
@@ -659,6 +661,9 @@ var Location = class {
659
661
  get isInBoundedContext() {
660
662
  return this.area === "context";
661
663
  }
664
+ get isInCoreDomain() {
665
+ return this.isInBoundedContext && this.subdomain === "core";
666
+ }
662
667
  get isInSharedKernel() {
663
668
  return this.area === "shared-kernel";
664
669
  }
@@ -693,39 +698,44 @@ var Layout = class {
693
698
  area: dirname(file) === this.settings.rootDir ? "root" : "outside",
694
699
  fileName
695
700
  });
696
- const area = folder.isSharedKernel ? "shared-kernel" : "context";
697
- const context = folder.name;
701
+ const inside = this.insideOf(folder);
698
702
  const directories = this.directoriesBetween(folder.dir, file);
699
703
  if (directories.length === 0) return new Location({
700
- area,
701
- context,
704
+ ...inside,
702
705
  fileName,
703
706
  isCompositionRoot: this.isCompositionRoot(fileName)
704
707
  });
705
708
  const layerIndex = this.layerIndexOf(directories, folder.isSharedKernel);
706
- if (layerIndex === void 0) return new Location({
707
- area,
708
- context,
709
- fileName,
710
- isCompositionRoot: folder.isSharedKernel && directories.length === 1 && this.isCompositionRoot(fileName)
711
- });
709
+ if (layerIndex === void 0) {
710
+ const isFeatureRoot = folder.isSharedKernel && directories.length === 1;
711
+ return new Location({
712
+ ...inside,
713
+ fileName,
714
+ isCompositionRoot: isFeatureRoot && this.isCompositionRoot(fileName)
715
+ });
716
+ }
712
717
  const layer = directories[layerIndex];
713
718
  if (layer === void 0 || !this.isLayer(layer)) return new Location({
714
- area,
715
- context,
719
+ ...inside,
716
720
  fileName
717
721
  });
718
722
  const foldersInLayer = directories.slice(layerIndex + 1);
719
723
  const subfolder = foldersInLayer.at(-1);
720
724
  return new Location({
721
- area,
722
- context,
725
+ ...inside,
723
726
  fileName,
724
727
  foldersInLayer,
725
728
  layer,
726
729
  ...subfolder === void 0 ? {} : { folder: subfolder }
727
730
  });
728
731
  }
732
+ insideOf(folder) {
733
+ return {
734
+ area: folder.isSharedKernel ? "shared-kernel" : "context",
735
+ context: folder.name,
736
+ ...folder.subdomain === void 0 ? {} : { subdomain: folder.subdomain }
737
+ };
738
+ }
729
739
  layerIndexOf(directories, isSharedKernel) {
730
740
  if (this.isLayer(directories[0] ?? "")) return 0;
731
741
  if (isSharedKernel && this.isLayer(directories[1] ?? "")) return 1;
@@ -901,6 +911,16 @@ var Wording = class {
901
911
  //#region src/rules/framework/rule.ts
902
912
  var Rule = class {
903
913
  wording = new Wording();
914
+ filesOf(architecture) {
915
+ const files = [];
916
+ for (const file of architecture.files) if (this.checks(file, architecture)) files.push(file);
917
+ return files;
918
+ }
919
+ checks(file, architecture) {
920
+ if (this.meta.contexts === "every") return true;
921
+ const location = architecture.locationOf(file);
922
+ return !location.isInBoundedContext || location.isInCoreDomain;
923
+ }
904
924
  finding(file, line, symbol, messageId, data = {}) {
905
925
  return {
906
926
  data,
@@ -916,7 +936,7 @@ var Rule = class {
916
936
  var ClassRule = class extends Rule {
917
937
  check(architecture) {
918
938
  const findings = [];
919
- for (const file of architecture.files) for (const codeClass of file.classes) findings.push(...this.findingsFor(codeClass, file, architecture));
939
+ for (const file of this.filesOf(architecture)) for (const codeClass of file.classes) findings.push(...this.findingsFor(codeClass, file, architecture));
920
940
  return findings;
921
941
  }
922
942
  };
@@ -925,7 +945,7 @@ var ClassRule = class extends Rule {
925
945
  var ImportRule = class extends Rule {
926
946
  check(architecture) {
927
947
  const findings = [];
928
- for (const file of architecture.files) {
948
+ for (const file of this.filesOf(architecture)) {
929
949
  if (!this.appliesTo(file, architecture)) continue;
930
950
  for (const dependency of file.dependencies) {
931
951
  const finding = this.findingFor(dependency, file, architecture);
@@ -974,6 +994,7 @@ const shortcuts = [
974
994
  ];
975
995
  var NoDrivingShortcutRule = class extends ImportRule {
976
996
  meta = {
997
+ contexts: "core",
977
998
  description: "A driving adapter importing a port, a repository, an aggregate, an entity or a domain service instead of calling a handler.",
978
999
  id: "layers/no-driving-shortcut",
979
1000
  messages: { shortcut: "Imports {name}, {kind}: a driving adapter calls the command and query handlers, never the ports, repositories or aggregates of the domain." }
@@ -1013,6 +1034,7 @@ var NoDrivingShortcutRule = class extends ImportRule {
1013
1034
  //#region src/rules/layers/no-impure-domain.rule.ts
1014
1035
  var NoImpureDomainRule = class extends ImportRule {
1015
1036
  meta = {
1037
+ contexts: "core",
1016
1038
  description: "The domain importing a framework, a database, another layer or a package not allowed, or using the host, the clock or randomness.",
1017
1039
  id: "layers/no-impure-domain",
1018
1040
  messages: {
@@ -1027,7 +1049,7 @@ var NoImpureDomainRule = class extends ImportRule {
1027
1049
  };
1028
1050
  check(architecture) {
1029
1051
  const findings = super.check(architecture);
1030
- for (const file of architecture.files) if (this.appliesTo(file, architecture)) findings.push(...this.impureGlobals(file));
1052
+ for (const file of this.filesOf(architecture)) if (this.appliesTo(file, architecture)) findings.push(...this.impureGlobals(file));
1031
1053
  return findings;
1032
1054
  }
1033
1055
  appliesTo(file, architecture) {
@@ -1074,6 +1096,7 @@ var NoImpureDomainRule = class extends ImportRule {
1074
1096
  //#region src/rules/layers/no-outward-import.rule.ts
1075
1097
  var NoOutwardImportRule = class extends ImportRule {
1076
1098
  meta = {
1099
+ contexts: "core",
1077
1100
  description: "A dependency pointing away from the domain, a file outside the layers or in the wrong folder of its layer.",
1078
1101
  id: "layers/no-outward-import",
1079
1102
  messages: {
@@ -1125,7 +1148,7 @@ var NoOutwardImportRule = class extends ImportRule {
1125
1148
  }
1126
1149
  misplacedFiles(architecture) {
1127
1150
  const findings = [];
1128
- for (const file of architecture.files) {
1151
+ for (const file of this.filesOf(architecture)) {
1129
1152
  const location = architecture.locationOf(file);
1130
1153
  if (location.isOutside) findings.push(this.finding(file, 1, location.fileName, "outsideContexts"));
1131
1154
  else if (!location.isAtRoot && !location.isInLayer && !location.isCompositionRoot) findings.push(this.finding(file, 1, location.fileName, "outsideLayers"));
@@ -1150,7 +1173,7 @@ var NoOutwardImportRule = class extends ImportRule {
1150
1173
  }
1151
1174
  extraCompositionRoots(architecture) {
1152
1175
  const byFolder = /* @__PURE__ */ new Map();
1153
- for (const file of architecture.files) if (architecture.locationOf(file).isCompositionRoot) {
1176
+ for (const file of this.filesOf(architecture)) if (architecture.locationOf(file).isCompositionRoot) {
1154
1177
  const folder = dirname(file.path);
1155
1178
  byFolder.set(folder, [...byFolder.get(folder) ?? [], file]);
1156
1179
  }
@@ -1210,6 +1233,7 @@ var NoOutwardImportRule = class extends ImportRule {
1210
1233
  //#region src/rules/layers/no-portless-adapter.rule.ts
1211
1234
  var NoPortlessAdapterRule = class extends ClassRule {
1212
1235
  meta = {
1236
+ contexts: "core",
1213
1237
  description: "A driven adapter that extends no port, a port declared outside the domain.",
1214
1238
  id: "layers/no-portless-adapter",
1215
1239
  messages: {
@@ -1235,11 +1259,12 @@ var NoPortlessAdapterRule = class extends ClassRule {
1235
1259
  //#region src/rules/strategic/no-cross-context-import.rule.ts
1236
1260
  var NoCrossContextImportRule = class extends ImportRule {
1237
1261
  meta = {
1262
+ contexts: "every",
1238
1263
  description: "An import from another bounded context that is not its open host service, a composition root that re-exports.",
1239
1264
  id: "strategic/no-cross-context-import",
1240
1265
  messages: {
1241
1266
  notOpenHostService: "Imports {target}: only an OpenHostService of another bounded context may be imported.",
1242
- outsideAntiCorruptionLayer: "Uses the open host service of {context} outside an AntiCorruptionLayer: translate it in an anti-corruption layer.",
1267
+ outsideAntiCorruptionLayer: "Uses the open host service of {context} outside an AntiCorruptionLayer: a core context translates what it consumes in an anti-corruption layer.",
1243
1268
  publishedLanguage: "Imports the published language of {context}: redeclare the fields you read in your own published-language/.",
1244
1269
  reexport: "The composition root re-exports {names}: it exports its own module only, so that no other context reaches through it.",
1245
1270
  sharedKernel: "The shared kernel imports no bounded context, but imports {target}."
@@ -1260,7 +1285,7 @@ var NoCrossContextImportRule = class extends ImportRule {
1260
1285
  if (from.isCompositionRoot && to.isCompositionRoot) return;
1261
1286
  if (to.layer === "published-language") return this.finding(file, dependency.line, dependency.label, "publishedLanguage", { context: to.context ?? "" });
1262
1287
  if (!this.importsOnlyOpenHostServices(dependency, target.path, architecture)) return this.finding(file, dependency.line, dependency.label, "notOpenHostService", { target: this.wording.target(target, architecture) });
1263
- if (!from.isCompositionRoot && !this.declaresAntiCorruptionLayer(file, architecture)) return this.finding(file, dependency.line, dependency.label, "outsideAntiCorruptionLayer", { context: to.context ?? "" });
1288
+ if (from.isInCoreDomain && !from.isCompositionRoot && !this.declaresAntiCorruptionLayer(file, architecture)) return this.finding(file, dependency.line, dependency.label, "outsideAntiCorruptionLayer", { context: to.context ?? "" });
1264
1289
  }
1265
1290
  importsOnlyOpenHostServices(dependency, path, architecture) {
1266
1291
  const target = architecture.project.file(path);
@@ -1289,6 +1314,7 @@ const owned = [
1289
1314
  ];
1290
1315
  var NoFatSharedKernelRule = class extends ClassRule {
1291
1316
  meta = {
1317
+ contexts: "every",
1292
1318
  description: "An aggregate, an entity, an event, a domain service, a repository or a handler in the shared kernel.",
1293
1319
  id: "strategic/no-fat-shared-kernel",
1294
1320
  messages: { owned: "{class} is {kind} in the shared kernel: it belongs to one bounded context; the shared kernel holds value objects, ports and their adapters." }
@@ -1308,6 +1334,7 @@ var NoFatSharedKernelRule = class extends ClassRule {
1308
1334
  //#region src/rules/strategic/no-leaky-host-service.rule.ts
1309
1335
  var NoLeakyHostServiceRule = class extends ClassRule {
1310
1336
  meta = {
1337
+ contexts: "every",
1311
1338
  description: "An open host service that exposes a class of its context instead of the published language.",
1312
1339
  id: "strategic/no-leaky-host-service",
1313
1340
  messages: { exposes: "{member} exposes {type}, {kind}{owner}: an open host service speaks the published language." }
@@ -1346,6 +1373,7 @@ var NoLeakyHostServiceRule = class extends ClassRule {
1346
1373
  //#region src/rules/strategic/no-unmapped-context.rule.ts
1347
1374
  var NoUnmappedContextRule = class extends Rule {
1348
1375
  meta = {
1376
+ contexts: "every",
1349
1377
  description: "A bounded context consuming one the context map does not allow, or two contexts that depend on each other.",
1350
1378
  id: "strategic/no-unmapped-context",
1351
1379
  messages: {
@@ -1367,7 +1395,7 @@ var NoUnmappedContextRule = class extends Rule {
1367
1395
  }
1368
1396
  consumptionsIn(architecture) {
1369
1397
  const consumptions = [];
1370
- for (const file of architecture.files) {
1398
+ for (const file of this.filesOf(architecture)) {
1371
1399
  const from = architecture.locationOf(file);
1372
1400
  if (!from.isInBoundedContext || from.context === void 0) continue;
1373
1401
  for (const dependency of file.dependencies) {
@@ -1400,6 +1428,7 @@ const holders = [
1400
1428
  ];
1401
1429
  var NoAggregateReferenceRule = class extends Rule {
1402
1430
  meta = {
1431
+ contexts: "core",
1403
1432
  description: "An aggregate holding another aggregate instead of its identifier, an entity held by two aggregates.",
1404
1433
  id: "tactical/no-aggregate-reference",
1405
1434
  messages: {
@@ -1410,7 +1439,7 @@ var NoAggregateReferenceRule = class extends Rule {
1410
1439
  };
1411
1440
  check(architecture) {
1412
1441
  const findings = [];
1413
- for (const file of architecture.files) for (const codeClass of file.classes) findings.push(...this.heldAggregates(codeClass, file, architecture));
1442
+ for (const file of this.filesOf(architecture)) for (const codeClass of file.classes) findings.push(...this.heldAggregates(codeClass, file, architecture));
1414
1443
  findings.push(...this.sharedEntities(architecture));
1415
1444
  return findings;
1416
1445
  }
@@ -1459,7 +1488,7 @@ var NoAggregateReferenceRule = class extends Rule {
1459
1488
  }
1460
1489
  holdingsIn(architecture) {
1461
1490
  const holdings = [];
1462
- for (const file of architecture.files) for (const aggregate of file.classes) {
1491
+ for (const file of this.filesOf(architecture)) for (const aggregate of file.classes) {
1463
1492
  if (!architecture.is(aggregate, "AggregateRoot")) continue;
1464
1493
  const visited = /* @__PURE__ */ new Set();
1465
1494
  for (const through of this.heldBy(aggregate)) for (const type of through.types) for (const entity of this.entitiesReachedFrom(type, architecture, visited)) holdings.push({
@@ -1514,6 +1543,7 @@ const statementMessages = {
1514
1543
  };
1515
1544
  var NoLooseCodeRule = class extends Rule {
1516
1545
  meta = {
1546
+ contexts: "core",
1517
1547
  description: "Code outside a building block in the domain or the application, outside a class in an adapter layer, anything but the module class in a composition root.",
1518
1548
  id: "tactical/no-loose-code",
1519
1549
  messages: {
@@ -1535,7 +1565,7 @@ var NoLooseCodeRule = class extends Rule {
1535
1565
  };
1536
1566
  check(architecture) {
1537
1567
  const findings = [];
1538
- for (const file of architecture.files) {
1568
+ for (const file of this.filesOf(architecture)) {
1539
1569
  const location = architecture.locationOf(file);
1540
1570
  if (guarded$2.has(location.layer)) {
1541
1571
  findings.push(...this.looseClasses(file, location.layer ?? "domain", architecture));
@@ -1585,6 +1615,7 @@ const markerWording = {
1585
1615
  };
1586
1616
  var NoMisplacedClassRule = class extends Rule {
1587
1617
  meta = {
1618
+ contexts: "core",
1588
1619
  description: "A class in the wrong folder or file, two classes in one file.",
1589
1620
  id: "tactical/no-misplaced-class",
1590
1621
  messages: {
@@ -1595,7 +1626,7 @@ var NoMisplacedClassRule = class extends Rule {
1595
1626
  };
1596
1627
  check(architecture) {
1597
1628
  const findings = [];
1598
- for (const file of architecture.files) {
1629
+ for (const file of this.filesOf(architecture)) {
1599
1630
  findings.push(...this.extraClasses(file));
1600
1631
  const [first] = file.classes;
1601
1632
  const finding = first === void 0 ? void 0 : this.misplacement(first, file, architecture.locationOf(file), architecture);
@@ -1630,6 +1661,7 @@ var NoMisplacedClassRule = class extends Rule {
1630
1661
  //#region src/rules/tactical/command-handlers/no-foreign-command-dependency.rule.ts
1631
1662
  var NoForeignCommandDependencyRule = class extends InjectionRule {
1632
1663
  meta = {
1664
+ contexts: "core",
1633
1665
  description: "A command handler receiving a query repository, an event publisher, another handler or a plain class.",
1634
1666
  id: "tactical/no-foreign-command-dependency",
1635
1667
  messages: { foreign: "The CommandHandler {class} receives {type}, {kind}: a command handler receives command repositories, ports, event translators, domain services and value objects; events leave through the outbox, never a publisher." }
@@ -1657,6 +1689,7 @@ const exempt = /* @__PURE__ */ new Set([
1657
1689
  const guarded$1 = /* @__PURE__ */ new Set(["domain", "application"]);
1658
1690
  var NoThrownFailureRule = class extends Rule {
1659
1691
  meta = {
1692
+ contexts: "core",
1660
1693
  description: "A business failure thrown instead of returned, an entity method without a Result, a setter.",
1661
1694
  id: "tactical/no-thrown-failure",
1662
1695
  messages: {
@@ -1668,7 +1701,7 @@ var NoThrownFailureRule = class extends Rule {
1668
1701
  };
1669
1702
  check(architecture) {
1670
1703
  const findings = [];
1671
- for (const file of architecture.files) {
1704
+ for (const file of this.filesOf(architecture)) {
1672
1705
  findings.push(...this.operationsWithoutResult(file, architecture));
1673
1706
  if (guarded$1.has(architecture.locationOf(file).layer)) findings.push(...this.raisedFailures(file));
1674
1707
  }
@@ -1703,6 +1736,7 @@ var NoThrownFailureRule = class extends Rule {
1703
1736
  //#region src/rules/tactical/domain-services/no-stateful-service.rule.ts
1704
1737
  var NoStatefulServiceRule = class extends InjectionRule {
1705
1738
  meta = {
1739
+ contexts: "core",
1706
1740
  description: "A domain service holding a port, a repository or another service.",
1707
1741
  id: "tactical/no-stateful-service",
1708
1742
  messages: { foreign: "The DomainService {class} holds {type}, {kind}: a domain service holds configuration only; the command handler passes it what it needs." }
@@ -1720,6 +1754,7 @@ const guarded = [
1720
1754
  ];
1721
1755
  var NoPublicFieldRule = class extends ClassRule {
1722
1756
  meta = {
1757
+ contexts: "core",
1723
1758
  description: "A public field on an aggregate, an entity, a value object or an identifier.",
1724
1759
  id: "tactical/no-public-field",
1725
1760
  messages: { publicField: "{member} is a public field: keep the state private, and expose what callers need through a getter." }
@@ -1738,6 +1773,7 @@ var NoPublicFieldRule = class extends ClassRule {
1738
1773
  //#region src/rules/tactical/query-handlers/no-foreign-query-dependency.rule.ts
1739
1774
  var NoForeignQueryDependencyRule = class extends InjectionRule {
1740
1775
  meta = {
1776
+ contexts: "core",
1741
1777
  description: "A query handler receiving what writes or changes state.",
1742
1778
  id: "tactical/no-foreign-query-dependency",
1743
1779
  messages: { foreign: "The QueryHandler {class} receives {type}, {kind}: a query handler receives query repositories, ports that do not write, and value objects." }
@@ -1775,6 +1811,7 @@ var DisableDirective = class {
1775
1811
  var NoLooseDisableRule = class extends Rule {
1776
1812
  knownRules;
1777
1813
  meta = {
1814
+ contexts: "every",
1778
1815
  description: "A disable comment that names no known rule, gives no reason, or disables nothing.",
1779
1816
  id: "tooling/no-loose-disable",
1780
1817
  messages: {
@@ -1790,7 +1827,7 @@ var NoLooseDisableRule = class extends Rule {
1790
1827
  }
1791
1828
  check(architecture) {
1792
1829
  const findings = [];
1793
- for (const file of architecture.files) for (const comment of file.disables) {
1830
+ for (const file of this.filesOf(architecture)) for (const comment of file.disables) {
1794
1831
  const finding = this.malformed(file, comment);
1795
1832
  if (finding !== void 0) findings.push(finding);
1796
1833
  }
@@ -1960,7 +1997,9 @@ var Report = class {
1960
1997
  text() {
1961
1998
  const blocks = [];
1962
1999
  for (const [file, violations] of this.byFile()) blocks.push(this.block(file, violations));
1963
- return `${[...blocks, this.summary()].join("\n\n")}\n`;
2000
+ const parts = [...blocks, this.summary()];
2001
+ if (blocks.length > 0) parts.push(this.colors.dim("Why, and how to fix it: npx alveolus explain <rule>"));
2002
+ return `${parts.join("\n\n")}\n`;
1964
2003
  }
1965
2004
  json() {
1966
2005
  const { baselined, files, stale, suppressed, violations } = this.input;
@@ -2082,16 +2121,37 @@ var Config = class {
2082
2121
  this.contextMap = config.contextMap === void 0 ? void 0 : this.validContextMap(config.contextMap, Object.keys(config.boundedContexts));
2083
2122
  this.rules = config.rules;
2084
2123
  const sharedKernel = config.sharedKernel ?? "shared-kernel";
2085
- this.contextFolders = [...Object.entries(config.boundedContexts).map(([name, folder]) => ({
2086
- dir: resolve(this.rootDir, folder),
2087
- isSharedKernel: false,
2088
- name
2089
- })), {
2124
+ this.contextFolders = [...this.classifiedContexts(config.subdomains ?? {}, config.boundedContexts), {
2090
2125
  dir: resolve(this.rootDir, sharedKernel),
2091
2126
  isSharedKernel: true,
2092
2127
  name: "shared kernel"
2093
2128
  }];
2094
2129
  }
2130
+ classifiedContexts(subdomains, boundedContexts) {
2131
+ const types = [
2132
+ "core",
2133
+ "supporting",
2134
+ "generic"
2135
+ ];
2136
+ const classified = /* @__PURE__ */ new Map();
2137
+ const folders = [];
2138
+ for (const type of types) for (const name of subdomains[type] ?? []) {
2139
+ const folder = boundedContexts[name];
2140
+ if (folder === void 0) throw new Error(`subdomains names ${name}, which boundedContexts does not declare.`);
2141
+ const already = classified.get(name);
2142
+ if (already !== void 0) throw new Error(`subdomains lists ${name} as ${already} and as ${type}: a bounded context implements one subdomain.`);
2143
+ classified.set(name, type);
2144
+ folders.push({
2145
+ dir: resolve(this.rootDir, folder),
2146
+ isSharedKernel: false,
2147
+ name,
2148
+ subdomain: type
2149
+ });
2150
+ }
2151
+ const unclassified = Object.keys(boundedContexts).filter((name) => !classified.has(name));
2152
+ 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
+ return folders;
2154
+ }
2095
2155
  validContextMap(upstreams, contexts) {
2096
2156
  const map = new ContextMap(upstreams);
2097
2157
  const unknown = map.contexts.filter((name) => !contexts.includes(name));
@@ -2117,6 +2177,7 @@ const ruleSetting = z.enum([
2117
2177
  "info",
2118
2178
  "off"
2119
2179
  ]);
2180
+ const contextNames = z.array(z.string()).exactOptional();
2120
2181
  const schema = z.strictObject({
2121
2182
  applicationDependencies: packageDependencies.exactOptional(),
2122
2183
  boundedContexts: z.record(z.string(), z.string()),
@@ -2131,6 +2192,11 @@ const schema = z.strictObject({
2131
2192
  root: z.string(),
2132
2193
  rules: z.strictObject(Object.fromEntries(new RuleRegistry().ids.map((id) => [id, ruleSetting.exactOptional()]))).exactOptional(),
2133
2194
  sharedKernel: z.string().exactOptional(),
2195
+ subdomains: z.strictObject({
2196
+ core: contextNames,
2197
+ generic: contextNames,
2198
+ supporting: contextNames
2199
+ }).exactOptional(),
2134
2200
  tsconfig: z.string().exactOptional()
2135
2201
  });
2136
2202
  var ConfigLoader = class ConfigLoader {
@@ -2709,18 +2775,91 @@ var TsMorphImporter = class TsMorphImporter extends Importer {
2709
2775
  return offset !== "" && !offset.startsWith("..") && !isAbsolute(offset) && !offset.split(/[\\/]/).includes("node_modules");
2710
2776
  }
2711
2777
  };
2778
+ const scaffolds = [
2779
+ {
2780
+ content: `import { defineConfig } from "@alveolus/arch";
2781
+
2782
+ export default defineConfig({
2783
+ boundedContexts: {},
2784
+ root: "src",
2785
+ });
2786
+ `,
2787
+ path: "alveolus.config.ts"
2788
+ },
2789
+ {
2790
+ content: `---
2791
+ name: alveolus
2792
+ description: Domain-Driven Design with @alveolus/core and @alveolus/arch. Use when writing or changing a class of the domain, the application or an adapter, when a class extends an Alveolus building block, or when alveolus arch check reports a violation.
2793
+ ---
2794
+
2795
+ # Alveolus
2796
+
2797
+ The documentation is installed with the package: read it with \`npx alveolus explain <topic>\`, not on the web. \`npx alveolus explain\` lists every topic.
2798
+
2799
+ 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
+ 2. Where a file goes and what each layer may import: \`npx alveolus explain project-layout\`.
2801
+ 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.
2803
+ `,
2804
+ path: ".claude/skills/alveolus/SKILL.md"
2805
+ },
2806
+ {
2807
+ content: `## Alveolus
2808
+
2809
+ This project uses Alveolus for Domain-Driven Design: the building blocks of \`@alveolus/core\` and the architecture checks of \`@alveolus/arch\`. The documentation is installed with the package: \`npx alveolus explain\` lists the topics and \`npx alveolus explain <topic>\` prints one, so do not look for it on the web. Run \`npx alveolus arch check\` after each change, and read the rule reported with \`npx alveolus explain <rule>\` before fixing. See \`.claude/skills/alveolus/SKILL.md\`.
2810
+ `,
2811
+ marker: "npx alveolus explain",
2812
+ path: "AGENTS.md"
2813
+ }
2814
+ ];
2815
+ //#endregion
2816
+ //#region src/init/init.ts
2817
+ var Init = class {
2818
+ projectDir;
2819
+ constructor(projectDir) {
2820
+ this.projectDir = projectDir;
2821
+ }
2822
+ run() {
2823
+ const written = [];
2824
+ for (const scaffold of scaffolds) written.push({
2825
+ outcome: this.write(scaffold),
2826
+ path: scaffold.path
2827
+ });
2828
+ return written;
2829
+ }
2830
+ get hint() {
2831
+ const claude = join(this.projectDir, "CLAUDE.md");
2832
+ if (!existsSync(claude)) return;
2833
+ return "CLAUDE.md exists: Claude Code reads it instead of AGENTS.md, so add a line with @AGENTS.md to it.";
2834
+ }
2835
+ write(scaffold) {
2836
+ const path = join(this.projectDir, scaffold.path);
2837
+ if (!existsSync(path)) {
2838
+ mkdirSync(dirname(path), { recursive: true });
2839
+ writeFileSync(path, scaffold.content);
2840
+ return "created";
2841
+ }
2842
+ if (scaffold.marker === void 0) return "kept";
2843
+ const existing = readFileSync(path, "utf8");
2844
+ if (existing.includes(scaffold.marker)) return "kept";
2845
+ writeFileSync(path, `${existing.trimEnd()}\n\n${scaffold.content}`);
2846
+ return "appended";
2847
+ }
2848
+ };
2712
2849
  //#endregion
2713
2850
  //#region src/cli/cli.ts
2714
2851
  var Cli = class {
2715
2852
  stdout;
2716
2853
  stderr;
2717
2854
  cwd;
2855
+ docs;
2718
2856
  colored;
2719
2857
  exitCode = 0;
2720
- constructor(stdout, stderr, cwd, colored = false) {
2858
+ constructor(stdout, stderr, cwd, docs, colored = false) {
2721
2859
  this.stdout = stdout;
2722
2860
  this.stderr = stderr;
2723
2861
  this.cwd = cwd;
2862
+ this.docs = docs;
2724
2863
  this.colored = colored;
2725
2864
  }
2726
2865
  async run(args) {
@@ -2740,8 +2879,30 @@ var Cli = class {
2740
2879
  const arch = program.command("arch").description("Check the architecture of a Domain-Driven Design project");
2741
2880
  this.withOptions(arch.command("check").description("Report the violations that are not in the baseline")).action((options) => this.check(options));
2742
2881
  this.withOptions(arch.command("baseline").description(`Write the current violations to ${Baseline.fileName}`)).option("--allow-growth", "write the baseline even when it holds more entries than before").action((options) => this.baseline(options));
2882
+ program.command("explain").description("Print a page of the documentation: a rule, a building block or a guide").argument("[topic]", "a rule id, a building block or a guide; without it, the list of topics").action((topic) => this.explain(topic));
2883
+ program.command("init").description(`Write ${ConfigLoader.fileName}, and the instructions that tell a coding agent to read the documentation installed with the package`).option("--project <dir>", "project directory", ".").action((options) => this.init(options));
2743
2884
  return program;
2744
2885
  }
2886
+ explain(topic) {
2887
+ if (topic === void 0) {
2888
+ this.stdout.write(`${this.docs.topics().join("\n")}\n`);
2889
+ return;
2890
+ }
2891
+ const match = this.docs.find(topic);
2892
+ if ("page" in match) {
2893
+ this.stdout.write(match.page.text());
2894
+ return;
2895
+ }
2896
+ const list = match.candidates.length === 0 ? "alveolus explain lists the topics." : `Did you mean ${match.candidates.join(", ")}?`;
2897
+ this.stderr.write(`No page for ${topic}: ${list}\n`);
2898
+ this.exitCode = 1;
2899
+ }
2900
+ init(options) {
2901
+ const init = new Init(resolve(this.cwd, options.project));
2902
+ for (const { outcome, path } of init.run()) this.stdout.write(`${outcome} ${path}\n`);
2903
+ this.stdout.write(`Name your bounded contexts in ${ConfigLoader.fileName}, then run alveolus arch check.\n`);
2904
+ if (init.hint !== void 0) this.stderr.write(`${init.hint}\n`);
2905
+ }
2745
2906
  withOptions(command) {
2746
2907
  return command.option("--project <dir>", "project directory", ".").option("--config <file>", "configuration file", ConfigLoader.fileName).option("--tsconfig <file>", "TypeScript configuration, tsconfig.json or the one set in the configuration file").addOption(new Option("--format <format>", "how violations are printed").choices([
2747
2908
  "text",
@@ -2805,6 +2966,94 @@ var Cli = class {
2805
2966
  }
2806
2967
  };
2807
2968
  //#endregion
2808
- export { Config as a, RuleRegistry as c, AllowedPackages as d, Baseline as f, ConfigLoader as i, Rule as l, TsMorphImporter as n, Report as o, Importer as r, Checker as s, Cli as t, Architecture as u };
2969
+ //#region src/docs/page.ts
2970
+ const html = [
2971
+ {
2972
+ pattern: /<dt>/g,
2973
+ text: "- "
2974
+ },
2975
+ {
2976
+ pattern: /<\/dt>\s*<dd>/g,
2977
+ text: ": "
2978
+ },
2979
+ {
2980
+ pattern: /<[^>\n]+>/g,
2981
+ text: ""
2982
+ },
2983
+ {
2984
+ pattern: /^\t+- /gm,
2985
+ text: "- "
2986
+ },
2987
+ {
2988
+ pattern: /&lt;/g,
2989
+ text: "<"
2990
+ },
2991
+ {
2992
+ pattern: /&gt;/g,
2993
+ text: ">"
2994
+ },
2995
+ {
2996
+ pattern: /&amp;/g,
2997
+ text: "&"
2998
+ }
2999
+ ];
3000
+ const frontmatter = /^---\n([\s\S]*?)\n---\n/;
3001
+ var Page = class {
3002
+ topic;
3003
+ source;
3004
+ constructor(topic, source) {
3005
+ this.topic = topic;
3006
+ this.source = source;
3007
+ }
3008
+ get description() {
3009
+ const header = frontmatter.exec(this.source)?.[1] ?? "";
3010
+ return /^description: "?(.*?)"?$/m.exec(header)?.[1] ?? "";
3011
+ }
3012
+ text() {
3013
+ let text = this.source.replace(frontmatter, "");
3014
+ for (const { pattern, text: replacement } of html) text = text.replace(pattern, replacement);
3015
+ return `${text.replace(/\n{3,}/g, "\n\n").trim()}\n`;
3016
+ }
3017
+ };
3018
+ //#endregion
3019
+ //#region src/docs/docs.ts
3020
+ var Docs = class Docs {
3021
+ dir;
3022
+ static sections = [
3023
+ "guide",
3024
+ "integrations",
3025
+ "core",
3026
+ "rules"
3027
+ ];
3028
+ constructor(dir) {
3029
+ this.dir = dir;
3030
+ }
3031
+ topics() {
3032
+ const topics = [];
3033
+ for (const section of Docs.sections) for (const file of globSync("**/*.md", { cwd: join(this.dir, section) }).sort()) topics.push(this.topicOf(join(section, file)));
3034
+ return topics.sort();
3035
+ }
3036
+ find(name) {
3037
+ const wanted = name.replace(/\.md$/, "").replace(/\/$/, "").replace(/\\/g, "/");
3038
+ const candidates = this.topics().filter((topic) => topic === wanted || topic.endsWith(`/${wanted}`));
3039
+ const exact = candidates.find((topic) => topic === wanted);
3040
+ if (exact !== void 0) return { page: this.read(exact) };
3041
+ if (candidates.length === 1 && candidates[0] !== void 0) return { page: this.read(candidates[0]) };
3042
+ return { candidates };
3043
+ }
3044
+ read(topic) {
3045
+ const path = join(this.dir, ...topic.split("/"));
3046
+ try {
3047
+ return new Page(topic, readFileSync(`${path}.md`, "utf8"));
3048
+ } catch {
3049
+ return new Page(topic, readFileSync(join(path, "index.md"), "utf8"));
3050
+ }
3051
+ }
3052
+ topicOf(file) {
3053
+ return file.split(sep).join("/").replace(/\.md$/, "").replace(/\/index$/, "");
3054
+ }
3055
+ };
3056
+ //#endregion
3057
+ 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 };
2809
3058
 
2810
- //# sourceMappingURL=cli-CwPCGjDg.mjs.map
3059
+ //# sourceMappingURL=docs-DsQHpTtV.mjs.map