aact 2.1.2 → 2.1.4

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
 
@@ -19,31 +19,57 @@ CLI и библиотека для валидации, анализа и ген
19
19
 
20
20
  <img src="https://github.com/Byndyusoft/aact/assets/1096954/a3c3b3b0-a09b-4da7-aca4-5538159b371c" width="15"/> Телеграм-канал: [Архитектура распределённых систем](https://t.me/rsa_enc)
21
21
 
22
+ aact можно использовать двумя способами: как **CLI** (`npx aact check`, авто-фикс, генерация артефактов) или как **библиотеку** (импортировать `checkAcl`, `analyzeArchitecture` и пр. в свои тесты на vitest/jest). CLI — ниже, library-режим — в [соответствующем разделе](#использование-как-библиотеки).
23
+
22
24
  ## Quick Start (CLI)
23
25
 
26
+ В пустой папке:
27
+
24
28
  ```bash
25
- # Инициализация конфига
29
+ # Создаёт aact.config.ts и стартовый architecture.puml с одним
30
+ # умышленным нарушением, чтобы было что чинить
26
31
  npx aact init
27
32
 
28
- # Проверка правил архитектуры
33
+ # Покажет 1 нарушение CRUD-правила (orders → orders_db напрямую)
29
34
  npx aact check
30
35
 
31
- # Анализ метрик
32
- npx aact analyze
36
+ # Применит auto-fix: добавит orders_repo как посредника к БД
37
+ npx aact check --fix
38
+
39
+ # Снова чисто
40
+ npx aact check
41
+ ```
42
+
43
+ После этого правь `architecture.puml` под свою систему — синтаксис
44
+ [C4-PlantUML](https://github.com/plantuml-stdlib/C4-PlantUML).
45
+
46
+ ### Остальные команды
33
47
 
34
- # Генерация артефактов
35
- npx aact generate --format plantuml
48
+ ```bash
49
+ npx aact check --dry-run # preview auto-fix без записи
50
+ npx aact analyze # coupling/cohesion метрики
51
+ npx aact generate --format plantuml # сгенерировать .puml из источника
36
52
  npx aact generate --format kubernetes
37
53
  ```
38
54
 
39
- ### Конфигурация
55
+ > Для `structurizr` укажите `source.writePath` в `aact.config.ts` —
56
+ > путь к `workspace.dsl`, в который пишутся правки от `--fix`.
57
+
58
+ ### Что создаёт `aact init`
40
59
 
41
- `aact init` создаст файл `aact.config.ts`:
60
+ Два файла рядом:
61
+
62
+ - **`aact.config.ts`** — настройки источника и набор включённых правил.
63
+ Использует `import type { AactConfig }` — рантайм-резолва пакета не
64
+ происходит, поэтому `npx aact check` работает без `npm install aact`.
65
+ - **`architecture.puml`** — стартовая C4-схема с одним сервисом,
66
+ одной БД и умышленным нарушением CRUD-правила. Замени на свою.
42
67
 
43
68
  ```ts
44
- import { defineConfig } from "aact";
69
+ // aact.config.ts (фрагмент)
70
+ import type { AactConfig } from "aact";
45
71
 
46
- export default defineConfig({
72
+ const config: AactConfig = {
47
73
  source: {
48
74
  type: "plantuml", // "plantuml" | "structurizr"
49
75
  path: "./architecture.puml",
@@ -51,11 +77,16 @@ export default defineConfig({
51
77
  rules: {
52
78
  acl: true,
53
79
  acyclic: true,
80
+ apiGateway: true,
54
81
  crud: true,
55
82
  dbPerService: true,
56
83
  cohesion: true,
84
+ stableDependencies: true,
85
+ commonReuse: true,
57
86
  },
58
- });
87
+ };
88
+
89
+ export default config;
59
90
  ```
60
91
 
61
92
  ## Использование как библиотеки
@@ -84,8 +115,14 @@ console.log(`Elements: ${report.elementsCount}`);
84
115
 
85
116
  ## Примеры
86
117
 
87
- - [Banking (PlantUML)](examples/banking-plantuml/) проверка правил, CCR-анализ, генерация K8s-конфигов
88
- - [Microservices (Structurizr)](examples/microservices-structurizr/) — полный цикл: правила, анализ, генерация
118
+ Запускаемые из коробки (склонируй репо, `cd examples/<name>`, `npx aact check`):
119
+
120
+ - [`examples/ecommerce-structurizr/`](examples/ecommerce-structurizr/) — Structurizr-источник с `workspace.json` + `workspace.dsl`, полный цикл правил и auto-fix.
121
+ - [`examples/violations-demo/`](examples/violations-demo/) — мини-набор умышленных нарушений по каждому правилу — чтобы посмотреть, как выглядит вывод и какие правки предлагает `--fix`.
122
+
123
+ Тестовые сценарии (для разработчиков пакета, запускаются через `vitest`):
124
+
125
+ - [`examples/banking-plantuml/`](examples/banking-plantuml/) и [`examples/microservices-structurizr/`](examples/microservices-structurizr/) — интеграционные тесты архитектуры из `resources/`.
89
126
 
90
127
  ## Документация
91
128
 
@@ -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.Cpfuzz1A.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.4";
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
+ }
30
47
  }
31
- case "structurizr": {
32
- return loadStructurizrElements(resolvedPath);
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
+ );
33
54
  }
34
- default: {
35
- const sourceType = config.source.type;
36
- throw new Error(`Unsupported source type: ${String(sourceType)}`);
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
+ );
60
+ }
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
 
@@ -685,7 +715,12 @@ const handleFixMode = async (model, results, config, dryRun) => {
685
715
  consola.info("No auto-fixes available for these violations");
686
716
  exitWithViolations();
687
717
  }
718
+ console.log(
719
+ pc.bold(dryRun ? "Suggested fixes (dry run):" : "Applying fixes:")
720
+ );
721
+ console.log();
688
722
  formatFixes(fixes);
723
+ console.log();
689
724
  if (!dryRun) {
690
725
  await writeFixes(config, fixes);
691
726
  }
@@ -795,61 +830,92 @@ const generate = defineCommand({
795
830
  }
796
831
  });
797
832
 
798
- const template = `import { defineConfig } from "aact";
833
+ const configTemplate = `import type { AactConfig } from "aact";
799
834
 
800
- export default defineConfig({
835
+ const config: AactConfig = {
801
836
  // Source of architecture description
802
837
  source: {
803
- type: "structurizr", // "plantuml" | "structurizr"
804
- path: "./workspace.json",
838
+ type: "plantuml", // "plantuml" | "structurizr"
839
+ path: "./architecture.puml",
805
840
  },
806
841
 
807
842
  // Validation rules (true = enabled with defaults, false = disabled)
808
843
  rules: {
809
844
  acl: true, // Anti-Corruption Layer: only tagged containers depend on externals
810
- // acl: { tag: "acl", externalType: "System_Ext" },
811
-
812
845
  acyclic: true, // No circular dependencies
813
-
846
+ apiGateway: true, // External calls go through an API gateway
814
847
  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
-
848
+ dbPerService: true, // Each database accessed by a single service
820
849
  cohesion: true, // Boundary cohesion > coupling
821
- // cohesion: { externalType: "System_Ext", internalType: "Container" },
850
+ stableDependencies: true, // Depend on more stable components
851
+ commonReuse: true, // Reuse all of a context's public API or none
822
852
  },
823
853
 
824
854
  // PlantUML generation from Kubernetes configs (aact generate)
825
855
  // generate: {
826
- // kubernetes: {
827
- // path: "resources/kubernetes/microservices",
828
- // },
856
+ // kubernetes: { path: "./resources/kubernetes" },
829
857
  // boundaryLabel: "Our system",
830
858
  // },
831
- });
859
+ };
860
+
861
+ export default config;
862
+ `;
863
+ const architectureTemplate = `@startuml
864
+ !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
865
+
866
+ ' Starter architecture. Replace with your own.
867
+ ' One intentional CRUD violation: \`orders\` accesses \`orders_db\` directly.
868
+ ' Run \`aact check\` to see it, then \`aact check --fix\` to auto-add a repo.
869
+
870
+ System_Boundary(checkout, "Checkout") {
871
+ Container(orders, "Orders Service")
872
+ ContainerDb(orders_db, "Orders DB")
873
+ }
874
+
875
+ Rel(orders, orders_db, "PostgreSQL")
876
+ @enduml
832
877
  `;
833
878
  const configFileName = "aact.config.ts";
879
+ const architectureFileName = "architecture.puml";
880
+ const writeIfNew = async (filePath, content, label) => {
881
+ try {
882
+ await fs.access(filePath);
883
+ consola.warn(`${label} already exists. Skipping.`);
884
+ return false;
885
+ } catch {
886
+ await fs.writeFile(filePath, content);
887
+ consola.success(`Created ${label}`);
888
+ return true;
889
+ }
890
+ };
834
891
  const init = defineCommand({
835
- meta: { description: "Create aact.config.ts with default settings" },
892
+ meta: {
893
+ description: "Create aact.config.ts and a starter architecture file"
894
+ },
836
895
  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 {
896
+ const cwd = process.cwd();
897
+ const configCreated = await writeIfNew(
898
+ path.resolve(cwd, configFileName),
899
+ configTemplate,
900
+ configFileName
901
+ );
902
+ const archCreated = await writeIfNew(
903
+ path.resolve(cwd, architectureFileName),
904
+ architectureTemplate,
905
+ architectureFileName
906
+ );
907
+ if (configCreated || archCreated) {
908
+ consola.info(
909
+ "Next: run `aact check` to see violations, then `aact check --fix` to auto-fix."
910
+ );
843
911
  }
844
- await fs.writeFile(configPath, template);
845
- consola.success(`Created ${configFileName}`);
846
912
  }
847
913
  });
848
914
 
849
915
  const main = defineCommand({
850
916
  meta: {
851
917
  name: "aact",
852
- version: "2.0.2",
918
+ version,
853
919
  description: "Architecture analysis and compliance tool"
854
920
  },
855
921
  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.Cpfuzz1A.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);
@@ -803,7 +803,7 @@ const checkCrud = (containers, options) => {
803
803
  message: `directly accesses database ${dbRelations.map((r) => r.to.name).join(", ")} \u2014 add a repo or relay`
804
804
  });
805
805
  }
806
- if (container.tags?.includes("repo") && container.relations.some((r) => r.to.type !== dbType)) {
806
+ if (isRepo && container.relations.some((r) => r.to.type !== dbType)) {
807
807
  violations.push({
808
808
  container: container.name,
809
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.2",
3
+ "version": "2.1.4",
4
4
  "type": "module",
5
5
  "description": "Architecture analysis and compliance tool",
6
6
  "keywords": [
@@ -47,11 +47,10 @@
47
47
  "scripts": {
48
48
  "build": "unbuild",
49
49
  "test": "vitest run",
50
- "lint": "npm run lint:eslint && npm run lint:markdown && npm run lint:prettier",
50
+ "lint": "npm run lint:eslint && npm run lint:prettier",
51
51
  "lint:eslint": "eslint .",
52
- "lint:markdown": "markdownlint --ignore-path ./.markdownlintignore \"./**/*.md\"",
53
52
  "lint:prettier": "prettier --ignore-path ./.gitignore --check \"./**/*.{ts,js,json,yaml,yml,md}\"",
54
- "prepare": "husky"
53
+ "prepare": "husky || true"
55
54
  },
56
55
  "lint-staged": {
57
56
  "*.{ts,js}": [
@@ -74,7 +73,6 @@
74
73
  "globals": "^17.3.0",
75
74
  "husky": "9.1.7",
76
75
  "lint-staged": "16.2.7",
77
- "markdownlint-cli": "0.31.1",
78
76
  "prettier": "3.8.1",
79
77
  "prettier-plugin-packagejson": "3.0.0",
80
78
  "typescript": "5.9.3",