aact 2.1.1 → 2.1.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 CHANGED
@@ -3,7 +3,7 @@
3
3
  # Architecture As Code Tools (aact)
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/aact)](https://www.npmjs.com/package/aact)
6
- [![test workflow](https://github.com/razonrus/ArchAsCode_Tests/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/razonrus/ArchAsCode_Tests/actions/workflows/test.yaml)
6
+ [![test workflow](https://github.com/Byndyusoft/aact/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/Byndyusoft/aact/actions/workflows/test.yaml)
7
7
 
8
8
  CLI и библиотека для валидации, анализа и генерации архитектуры микросервисных систем, описанной "as Code" (PlantUML C4, Structurizr).
9
9
 
@@ -21,29 +21,53 @@ CLI и библиотека для валидации, анализа и ген
21
21
 
22
22
  ## Quick Start (CLI)
23
23
 
24
+ В пустой папке:
25
+
24
26
  ```bash
25
- # Инициализация конфига
27
+ # Создаёт aact.config.ts и стартовый architecture.puml с одним
28
+ # умышленным нарушением, чтобы было что чинить
26
29
  npx aact init
27
30
 
28
- # Проверка правил архитектуры
31
+ # Покажет 1 нарушение CRUD-правила (orders → orders_db напрямую)
29
32
  npx aact check
30
33
 
31
- # Анализ метрик
32
- npx aact analyze
34
+ # Применит auto-fix: добавит orders_repo как посредника к БД
35
+ npx aact check --fix
36
+
37
+ # Снова чисто
38
+ npx aact check
39
+ ```
33
40
 
34
- # Генерация артефактов
35
- npx aact generate --format plantuml
41
+ После этого правь `architecture.puml` под свою систему — синтаксис
42
+ [C4-PlantUML](https://github.com/plantuml-stdlib/C4-PlantUML).
43
+
44
+ ### Остальные команды
45
+
46
+ ```bash
47
+ npx aact check --dry-run # preview auto-fix без записи
48
+ npx aact analyze # coupling/cohesion метрики
49
+ npx aact generate --format plantuml # сгенерировать .puml из источника
36
50
  npx aact generate --format kubernetes
37
51
  ```
38
52
 
39
- ### Конфигурация
53
+ > Для `structurizr` укажите `source.writePath` в `aact.config.ts` —
54
+ > путь к `workspace.dsl`, в который пишутся правки от `--fix`.
40
55
 
41
- `aact init` создаст файл `aact.config.ts`:
56
+ ### Что создаёт `aact init`
57
+
58
+ Два файла рядом:
59
+
60
+ - **`aact.config.ts`** — настройки источника и набор включённых правил.
61
+ Использует `import type { AactConfig }` — рантайм-резолва пакета не
62
+ происходит, поэтому `npx aact check` работает без `npm install aact`.
63
+ - **`architecture.puml`** — стартовая C4-схема с одним сервисом,
64
+ одной БД и умышленным нарушением CRUD-правила. Замени на свою.
42
65
 
43
66
  ```ts
44
- import { defineConfig } from "aact";
67
+ // aact.config.ts (фрагмент)
68
+ import type { AactConfig } from "aact";
45
69
 
46
- export default defineConfig({
70
+ const config: AactConfig = {
47
71
  source: {
48
72
  type: "plantuml", // "plantuml" | "structurizr"
49
73
  path: "./architecture.puml",
@@ -51,11 +75,16 @@ export default defineConfig({
51
75
  rules: {
52
76
  acl: true,
53
77
  acyclic: true,
78
+ apiGateway: true,
54
79
  crud: true,
55
80
  dbPerService: true,
56
81
  cohesion: true,
82
+ stableDependencies: true,
83
+ commonReuse: true,
57
84
  },
58
- });
85
+ };
86
+
87
+ export default config;
59
88
  ```
60
89
 
61
90
  ## Использование как библиотеки
@@ -84,8 +113,14 @@ console.log(`Elements: ${report.elementsCount}`);
84
113
 
85
114
  ## Примеры
86
115
 
87
- - [Banking (PlantUML)](examples/banking-plantuml/) проверка правил, CCR-анализ, генерация K8s-конфигов
88
- - [Microservices (Structurizr)](examples/microservices-structurizr/) — полный цикл: правила, анализ, генерация
116
+ Запускаемые из коробки (склонируй репо, `cd examples/<name>`, `npx aact check`):
117
+
118
+ - [`examples/ecommerce-structurizr/`](examples/ecommerce-structurizr/) — Structurizr-источник с `workspace.json` + `workspace.dsl`, полный цикл правил и auto-fix.
119
+ - [`examples/violations-demo/`](examples/violations-demo/) — мини-набор умышленных нарушений по каждому правилу — чтобы посмотреть, как выглядит вывод и какие правки предлагает `--fix`.
120
+
121
+ Тестовые сценарии (для разработчиков пакета, запускаются через `vitest`):
122
+
123
+ - [`examples/banking-plantuml/`](examples/banking-plantuml/) и [`examples/microservices-structurizr/`](examples/microservices-structurizr/) — интеграционные тесты архитектуры из `resources/`.
89
124
 
90
125
  ## Документация
91
126
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { defineCommand, runMain } from 'citty';
3
3
  import consola from 'consola';
4
- import { A as AactConfigSchema, H as loadStructurizrElements, G as loadPlantumlElements, J as mapContainersFromPlantumlElements, q as analyzeArchitecture, E as EXTERNAL_SYSTEM_TYPE, b as CONTAINER_DB_TYPE, r as checkAcl, s as checkAcyclic, t as checkApiGateway, w as checkCrud, x as checkDbPerService, u as checkCohesion, y as checkStableDependencies, v as checkCommonReuse, L as structurizrDslSyntax, D as generateKubernetes, F as generatePlantumlFromModel } from '../shared/aact.mCX-x14V.mjs';
4
+ import { A as AactConfigSchema, H as loadStructurizrElements, G as loadPlantumlElements, J as mapContainersFromPlantumlElements, q as analyzeArchitecture, E as EXTERNAL_SYSTEM_TYPE, b as CONTAINER_DB_TYPE, r as checkAcl, s as checkAcyclic, t as checkApiGateway, w as checkCrud, x as checkDbPerService, u as checkCohesion, y as checkStableDependencies, v as checkCommonReuse, L as structurizrDslSyntax, D as generateKubernetes, F as generatePlantumlFromModel } from '../shared/aact.BzhD7c9t.mjs';
5
5
  import { loadConfig } from 'c12';
6
6
  import * as v from 'valibot';
7
7
  import path from 'node:path';
@@ -10,6 +10,8 @@ import pc from 'picocolors';
10
10
  import 'yaml';
11
11
  import 'plantuml-parser';
12
12
 
13
+ const version = "2.1.3";
14
+
13
15
  const loadAndValidateConfig = async (configPath) => {
14
16
  const { config } = await loadConfig({
15
17
  name: "aact",
@@ -21,20 +23,48 @@ const loadAndValidateConfig = async (configPath) => {
21
23
  return v.parse(AactConfigSchema, config);
22
24
  };
23
25
 
26
+ const isFileNotFound = (err) => typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
27
+ const exitWithError = (message, hint) => {
28
+ consola.error(message);
29
+ if (hint) consola.info(hint);
30
+ return process.exit(1);
31
+ };
24
32
  const loadModel = async (config) => {
25
33
  const resolvedPath = path.resolve(config.source.path);
26
- switch (config.source.type) {
27
- case "plantuml": {
28
- const elements = await loadPlantumlElements(resolvedPath);
29
- return mapContainersFromPlantumlElements(elements);
34
+ try {
35
+ switch (config.source.type) {
36
+ case "plantuml": {
37
+ const elements = await loadPlantumlElements(resolvedPath);
38
+ return mapContainersFromPlantumlElements(elements);
39
+ }
40
+ case "structurizr": {
41
+ return await loadStructurizrElements(resolvedPath);
42
+ }
43
+ default: {
44
+ const sourceType = config.source.type;
45
+ throw new Error(`Unsupported source type: ${String(sourceType)}`);
46
+ }
47
+ }
48
+ } catch (error) {
49
+ if (isFileNotFound(error)) {
50
+ return exitWithError(
51
+ `Architecture file not found: ${config.source.path}`,
52
+ "Update source.path in aact.config.ts or create the file (`aact init` scaffolds a starter)."
53
+ );
30
54
  }
31
- case "structurizr": {
32
- return loadStructurizrElements(resolvedPath);
55
+ if (error instanceof SyntaxError && config.source.type === "structurizr") {
56
+ return exitWithError(
57
+ `Cannot parse Structurizr workspace: ${config.source.path}`,
58
+ `${error.message}. Check that the file is valid JSON.`
59
+ );
33
60
  }
34
- default: {
35
- const sourceType = config.source.type;
36
- throw new Error(`Unsupported source type: ${String(sourceType)}`);
61
+ if (error instanceof TypeError && config.source.type === "structurizr" && /softwareSystems|model|people/.test(error.message)) {
62
+ return exitWithError(
63
+ `Invalid Structurizr workspace: ${config.source.path}`,
64
+ 'Expected a top-level "model" object with "softwareSystems". See examples/ecommerce-structurizr/ for a working sample.'
65
+ );
37
66
  }
67
+ throw error;
38
68
  }
39
69
  };
40
70
 
@@ -795,61 +825,92 @@ const generate = defineCommand({
795
825
  }
796
826
  });
797
827
 
798
- const template = `import { defineConfig } from "aact";
828
+ const configTemplate = `import type { AactConfig } from "aact";
799
829
 
800
- export default defineConfig({
830
+ const config: AactConfig = {
801
831
  // Source of architecture description
802
832
  source: {
803
- type: "structurizr", // "plantuml" | "structurizr"
804
- path: "./workspace.json",
833
+ type: "plantuml", // "plantuml" | "structurizr"
834
+ path: "./architecture.puml",
805
835
  },
806
836
 
807
837
  // Validation rules (true = enabled with defaults, false = disabled)
808
838
  rules: {
809
839
  acl: true, // Anti-Corruption Layer: only tagged containers depend on externals
810
- // acl: { tag: "acl", externalType: "System_Ext" },
811
-
812
840
  acyclic: true, // No circular dependencies
813
-
841
+ apiGateway: true, // External calls go through an API gateway
814
842
  crud: true, // Only repo/relay containers access databases
815
- // crud: { repoTags: ["repo", "relay"], dbType: "ContainerDb" },
816
-
817
- dbPerService: true, // Each database accessed by single service
818
- // dbPerService: { dbType: "ContainerDb" },
819
-
843
+ dbPerService: true, // Each database accessed by a single service
820
844
  cohesion: true, // Boundary cohesion > coupling
821
- // cohesion: { externalType: "System_Ext", internalType: "Container" },
845
+ stableDependencies: true, // Depend on more stable components
846
+ commonReuse: true, // Reuse all of a context's public API or none
822
847
  },
823
848
 
824
849
  // PlantUML generation from Kubernetes configs (aact generate)
825
850
  // generate: {
826
- // kubernetes: {
827
- // path: "resources/kubernetes/microservices",
828
- // },
851
+ // kubernetes: { path: "./resources/kubernetes" },
829
852
  // boundaryLabel: "Our system",
830
853
  // },
831
- });
854
+ };
855
+
856
+ export default config;
857
+ `;
858
+ const architectureTemplate = `@startuml
859
+ !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
860
+
861
+ ' Starter architecture. Replace with your own.
862
+ ' One intentional CRUD violation: \`orders\` accesses \`orders_db\` directly.
863
+ ' Run \`aact check\` to see it, then \`aact check --fix\` to auto-add a repo.
864
+
865
+ System_Boundary(checkout, "Checkout") {
866
+ Container(orders, "Orders Service")
867
+ ContainerDb(orders_db, "Orders DB")
868
+ }
869
+
870
+ Rel(orders, orders_db, "PostgreSQL")
871
+ @enduml
832
872
  `;
833
873
  const configFileName = "aact.config.ts";
874
+ const architectureFileName = "architecture.puml";
875
+ const writeIfNew = async (filePath, content, label) => {
876
+ try {
877
+ await fs.access(filePath);
878
+ consola.warn(`${label} already exists. Skipping.`);
879
+ return false;
880
+ } catch {
881
+ await fs.writeFile(filePath, content);
882
+ consola.success(`Created ${label}`);
883
+ return true;
884
+ }
885
+ };
834
886
  const init = defineCommand({
835
- meta: { description: "Create aact.config.ts with default settings" },
887
+ meta: {
888
+ description: "Create aact.config.ts and a starter architecture file"
889
+ },
836
890
  async run() {
837
- const configPath = path.resolve(process.cwd(), configFileName);
838
- try {
839
- await fs.access(configPath);
840
- consola.warn(`${configFileName} already exists. Skipping.`);
841
- return;
842
- } catch {
891
+ const cwd = process.cwd();
892
+ const configCreated = await writeIfNew(
893
+ path.resolve(cwd, configFileName),
894
+ configTemplate,
895
+ configFileName
896
+ );
897
+ const archCreated = await writeIfNew(
898
+ path.resolve(cwd, architectureFileName),
899
+ architectureTemplate,
900
+ architectureFileName
901
+ );
902
+ if (configCreated || archCreated) {
903
+ consola.info(
904
+ "Next: run `aact check` to see violations, then `aact check --fix` to auto-fix."
905
+ );
843
906
  }
844
- await fs.writeFile(configPath, template);
845
- consola.success(`Created ${configFileName}`);
846
907
  }
847
908
  });
848
909
 
849
910
  const main = defineCommand({
850
911
  meta: {
851
912
  name: "aact",
852
- version: "2.0.2",
913
+ version,
853
914
  description: "Architecture analysis and compliance tool"
854
915
  },
855
916
  subCommands: { init, check, analyze, generate }
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { A as AactConfigSchema, B as BOUNDARY_TYPE, C as COMPONENT_TYPE, a as CONTAINER_BOUNDARY_TYPE, b as CONTAINER_DB_TYPE, c as CONTAINER_TYPE, E as EXTERNAL_SYSTEM_TYPE, P as PERSON_TYPE, d as PLANTUML_BOUNDARY, e as PLANTUML_COMPONENT, f as PLANTUML_CONTAINER, g as PLANTUML_CONTAINER_BOUNDARY, h as PLANTUML_CONTAINER_DB, i as PLANTUML_PERSON, j as PLANTUML_SYSTEM, k as PLANTUML_SYSTEM_BOUNDARY, l as PLANTUML_SYSTEM_EXT, S as STRUCTURIZR_INTERACTION_ASYNC, m as STRUCTURIZR_LOCATION_EXTERNAL, n as STRUCTURIZR_TAG_ASYNC, o as SYSTEM_BOUNDARY_TYPE, p as SYSTEM_TYPE, q as analyzeArchitecture, r as checkAcl, s as checkAcyclic, t as checkApiGateway, u as checkCohesion, v as checkCommonReuse, w as checkCrud, x as checkDbPerService, y as checkStableDependencies, z as defineConfig, D as generateKubernetes, F as generatePlantumlFromModel, G as loadPlantumlElements, H as loadStructurizrElements, I as loadStructurizrWorkspace, J as mapContainersFromPlantumlElements, K as mapContainersFromStructurizr, L as structurizrDslSyntax } from './shared/aact.mCX-x14V.mjs';
1
+ export { A as AactConfigSchema, B as BOUNDARY_TYPE, C as COMPONENT_TYPE, a as CONTAINER_BOUNDARY_TYPE, b as CONTAINER_DB_TYPE, c as CONTAINER_TYPE, E as EXTERNAL_SYSTEM_TYPE, P as PERSON_TYPE, d as PLANTUML_BOUNDARY, e as PLANTUML_COMPONENT, f as PLANTUML_CONTAINER, g as PLANTUML_CONTAINER_BOUNDARY, h as PLANTUML_CONTAINER_DB, i as PLANTUML_PERSON, j as PLANTUML_SYSTEM, k as PLANTUML_SYSTEM_BOUNDARY, l as PLANTUML_SYSTEM_EXT, S as STRUCTURIZR_INTERACTION_ASYNC, m as STRUCTURIZR_LOCATION_EXTERNAL, n as STRUCTURIZR_TAG_ASYNC, o as SYSTEM_BOUNDARY_TYPE, p as SYSTEM_TYPE, q as analyzeArchitecture, r as checkAcl, s as checkAcyclic, t as checkApiGateway, u as checkCohesion, v as checkCommonReuse, w as checkCrud, x as checkDbPerService, y as checkStableDependencies, z as defineConfig, D as generateKubernetes, F as generatePlantumlFromModel, G as loadPlantumlElements, H as loadStructurizrElements, I as loadStructurizrWorkspace, J as mapContainersFromPlantumlElements, K as mapContainersFromStructurizr, L as structurizrDslSyntax } from './shared/aact.BzhD7c9t.mjs';
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import YAML from 'yaml';
@@ -221,7 +221,7 @@ const generateKubernetes = (model, options) => {
221
221
  const dbConnectionTemplate = options?.dbConnectionTemplate ?? "postgresql://{name}:pass-{name}@postgresql:5432/{name}";
222
222
  const resolvedOptions = { defaultPort, dbConnectionTemplate };
223
223
  const containers = model.allContainers.filter(
224
- (c) => c.type !== CONTAINER_DB_TYPE && c.type !== EXTERNAL_SYSTEM_TYPE
224
+ (c) => c.type !== CONTAINER_DB_TYPE && c.type !== EXTERNAL_SYSTEM_TYPE && c.type !== PERSON_TYPE
225
225
  );
226
226
  return containers.map((container) => {
227
227
  const kebabName = toKebab(container.name);
@@ -734,13 +734,16 @@ const checkCohesion = (model, options) => {
734
734
  return violations;
735
735
  };
736
736
 
737
- const checkCommonReuse = (model) => {
738
- const boundaryOf = /* @__PURE__ */ new Map();
737
+ const buildBoundaryLookup = (model) => {
738
+ const map = /* @__PURE__ */ new Map();
739
739
  for (const boundary of model.boundaries) {
740
740
  for (const c of boundary.containers) {
741
- boundaryOf.set(c.name, boundary);
741
+ map.set(c.name, boundary);
742
742
  }
743
743
  }
744
+ return map;
745
+ };
746
+ const collectPublicAndUsage = (model, boundaryOf) => {
744
747
  const publicOf = /* @__PURE__ */ new Map();
745
748
  const used = /* @__PURE__ */ new Map();
746
749
  for (const source of model.allContainers) {
@@ -764,6 +767,11 @@ const checkCommonReuse = (model) => {
764
767
  u.add(rel.to.name);
765
768
  }
766
769
  }
770
+ return { publicOf, used };
771
+ };
772
+ const checkCommonReuse = (model) => {
773
+ const boundaryOf = buildBoundaryLookup(model);
774
+ const { publicOf, used } = collectPublicAndUsage(model, boundaryOf);
767
775
  const violations = [];
768
776
  for (const [provider, pubNames] of publicOf) {
769
777
  if (pubNames.size < 2) continue;
@@ -795,7 +803,7 @@ const checkCrud = (containers, options) => {
795
803
  message: `directly accesses database ${dbRelations.map((r) => r.to.name).join(", ")} \u2014 add a repo or relay`
796
804
  });
797
805
  }
798
- if (container.tags?.includes("repo") && container.relations.some((r) => r.to.type !== dbType)) {
806
+ if (isRepo && container.relations.some((r) => r.to.type !== dbType)) {
799
807
  violations.push({
800
808
  container: container.name,
801
809
  message: `repo has non-database dependencies: ${container.relations.filter((r) => r.to.type !== dbType).map((r) => r.to.name).join(", ")} \u2014 repos should only access databases`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aact",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "type": "module",
5
5
  "description": "Architecture analysis and compliance tool",
6
6
  "keywords": [
@@ -11,14 +11,22 @@
11
11
  "c4-model",
12
12
  "architecture-testing",
13
13
  "microservices",
14
- "validation"
14
+ "validation",
15
+ "linter",
16
+ "cli",
17
+ "auto-fix",
18
+ "kubernetes",
19
+ "coupling",
20
+ "cohesion"
15
21
  ],
16
22
  "bin": {
17
23
  "aact": "dist/cli/index.mjs"
18
24
  },
19
25
  "exports": {
20
26
  ".": {
21
- "import": "./dist/index.mjs"
27
+ "types": "./dist/index.d.mts",
28
+ "import": "./dist/index.mjs",
29
+ "default": "./dist/index.mjs"
22
30
  }
23
31
  },
24
32
  "homepage": "https://github.com/Byndyusoft/aact#readme",
@@ -30,6 +38,7 @@
30
38
  "url": "git+https://github.com/Byndyusoft/aact.git"
31
39
  },
32
40
  "packageManager": "pnpm@9.15.4",
41
+ "sideEffects": false,
33
42
  "files": [
34
43
  "dist"
35
44
  ],
@@ -38,11 +47,10 @@
38
47
  "scripts": {
39
48
  "build": "unbuild",
40
49
  "test": "vitest run",
41
- "lint": "npm run lint:eslint && npm run lint:markdown && npm run lint:prettier",
50
+ "lint": "npm run lint:eslint && npm run lint:prettier",
42
51
  "lint:eslint": "eslint .",
43
- "lint:markdown": "markdownlint --ignore-path ./.markdownlintignore \"./**/*.md\"",
44
52
  "lint:prettier": "prettier --ignore-path ./.gitignore --check \"./**/*.{ts,js,json,yaml,yml,md}\"",
45
- "prepare": "husky"
53
+ "prepare": "husky || true"
46
54
  },
47
55
  "lint-staged": {
48
56
  "*.{ts,js}": [
@@ -65,7 +73,6 @@
65
73
  "globals": "^17.3.0",
66
74
  "husky": "9.1.7",
67
75
  "lint-staged": "16.2.7",
68
- "markdownlint-cli": "0.31.1",
69
76
  "prettier": "3.8.1",
70
77
  "prettier-plugin-packagejson": "3.0.0",
71
78
  "typescript": "5.9.3",