@lamentis/naome 1.3.11 → 1.3.13

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 (41) hide show
  1. package/Cargo.lock +2 -2
  2. package/crates/naome-cli/Cargo.toml +1 -1
  3. package/crates/naome-cli/src/architecture_commands.rs +2 -6
  4. package/crates/naome-cli/tests/architecture_cli.rs +60 -0
  5. package/crates/naome-core/Cargo.toml +1 -1
  6. package/crates/naome-core/src/architecture/config/parser/sections.rs +44 -1
  7. package/crates/naome-core/src/architecture/config/parser.rs +1 -0
  8. package/crates/naome-core/src/architecture/config.rs +35 -0
  9. package/crates/naome-core/src/architecture/output.rs +15 -1
  10. package/crates/naome-core/src/architecture/rules/budgets.rs +179 -0
  11. package/crates/naome-core/src/architecture/rules/context.rs +138 -0
  12. package/crates/naome-core/src/architecture/rules/cycles.rs +39 -0
  13. package/crates/naome-core/src/architecture/rules/external.rs +244 -0
  14. package/crates/naome-core/src/architecture/rules/graph.rs +177 -0
  15. package/crates/naome-core/src/architecture/rules/transitive.rs +89 -0
  16. package/crates/naome-core/src/architecture/rules.rs +13 -39
  17. package/crates/naome-core/src/architecture/scan/graph_builder.rs +130 -30
  18. package/crates/naome-core/src/architecture/scan/imports/extractors/swift.rs +48 -0
  19. package/crates/naome-core/src/architecture/scan/imports/extractors.rs +7 -7
  20. package/crates/naome-core/src/architecture/scan/imports/resolver.rs +44 -22
  21. package/crates/naome-core/src/architecture/scan/imports.rs +17 -0
  22. package/crates/naome-core/src/architecture/scan/manifest/common.rs +102 -0
  23. package/crates/naome-core/src/architecture/scan/manifest/parsers/json.rs +46 -0
  24. package/crates/naome-core/src/architecture/scan/manifest/parsers/other.rs +280 -0
  25. package/crates/naome-core/src/architecture/scan/manifest/parsers/toml.rs +184 -0
  26. package/crates/naome-core/src/architecture/scan/manifest/parsers.rs +3 -0
  27. package/crates/naome-core/src/architecture/scan/manifest.rs +33 -0
  28. package/crates/naome-core/src/architecture/scan.rs +27 -1
  29. package/crates/naome-core/src/architecture.rs +1 -1
  30. package/crates/naome-core/src/lib.rs +1 -0
  31. package/crates/naome-core/tests/architecture.rs +53 -85
  32. package/crates/naome-core/tests/architecture_manifests.rs +289 -0
  33. package/crates/naome-core/tests/architecture_rules.rs +498 -0
  34. package/crates/naome-core/tests/architecture_support/mod.rs +80 -0
  35. package/crates/naome-core/tests/architecture_swift.rs +111 -0
  36. package/installer/harness-files.js +3 -3
  37. package/native/darwin-arm64/naome +0 -0
  38. package/native/linux-x64/naome +0 -0
  39. package/package.json +1 -1
  40. package/templates/naome-root/.naome/manifest.json +2 -2
  41. package/templates/naome-root/docs/naome/architecture-fitness.md +61 -8
@@ -1,15 +1,12 @@
1
- use std::fs;
2
- use std::path::{Path, PathBuf};
3
- use std::process::Command;
4
- use std::sync::atomic::{AtomicU64, Ordering};
5
- use std::time::{SystemTime, UNIX_EPOCH};
6
-
7
1
  use naome_core::{
8
2
  default_architecture_config_text, scan_architecture, validate_architecture, ArchitectureConfig,
9
3
  ArchitectureEdgeKind, ArchitectureScanOptions,
10
4
  };
11
5
 
12
- static FIXTURE_COUNTER: AtomicU64 = AtomicU64::new(0);
6
+ mod architecture_support;
7
+
8
+ use architecture_support::{has_import_edge, FixtureRepo};
9
+
13
10
  const FORBIDDEN_DOMAIN_TO_INFRASTRUCTURE_CONFIG: &str = "layers:\n domain:\n paths:\n - \"src/domain/**\"\n infrastructure:\n paths:\n - \"src/infrastructure/**\"\nallowed_dependencies:\n domain:\n infrastructure:\n - domain\nrules:\n no_forbidden_layer_dependencies:\n enabled: true\n severity: error\n";
14
11
 
15
12
  #[test]
@@ -180,7 +177,7 @@ fn import_resolution_covers_common_language_forms_without_dropping_layer_edges()
180
177
  #[test]
181
178
  fn relative_import_resolution_prefers_source_language_extensions() {
182
179
  let repo = FixtureRepo::new();
183
- repo.write("src/domain/mod.rs", "mod db;\n");
180
+ repo.write("src/domain/mod.rs", "use self::db::right;\n");
184
181
  repo.write("src/domain/db.ts", "export const wrong = true;\n");
185
182
  repo.write("src/domain/db.rs", "pub fn right() {}\n");
186
183
 
@@ -235,12 +232,24 @@ fn import_resolver_handles_language_specific_module_roots() {
235
232
  "file:packages/app/src/domain/event.rs",
236
233
  "file:packages/app/src/infrastructure/db.rs"
237
234
  ));
238
- assert!(has("file:src/domain/service.rs", "file:src/domain/service/helper.rs"));
239
- assert!(!has("file:src/domain/service.rs", "file:src/domain/helper.rs"));
240
- assert!(has("file:src/domain/service.rs", "file:src/domain/infra.rs"));
235
+ assert!(has(
236
+ "file:src/domain/service.rs",
237
+ "file:src/domain/service/helper.rs"
238
+ ));
239
+ assert!(!has(
240
+ "file:src/domain/service.rs",
241
+ "file:src/domain/helper.rs"
242
+ ));
243
+ assert!(has(
244
+ "file:src/domain/service.rs",
245
+ "file:src/domain/infra.rs"
246
+ ));
241
247
  assert!(!has("file:src/domain/service.rs", "file:src/infra.rs"));
242
248
  assert!(has("file:domain/event.go", "file:infrastructure/db/db.go"));
243
- assert!(!has("file:domain/external.go", "file:infrastructure/db/db.go"));
249
+ assert!(!has(
250
+ "file:domain/external.go",
251
+ "file:infrastructure/db/db.go"
252
+ ));
244
253
  }
245
254
 
246
255
  #[test]
@@ -291,20 +300,44 @@ fn overlapping_target_layers_do_not_create_false_forbidden_dependency() {
291
300
  fn late_review_import_forms_keep_forbidden_layer_edges_visible() {
292
301
  let repo = FixtureRepo::new();
293
302
  repo.write("naome.arch.yaml", FORBIDDEN_DOMAIN_TO_INFRASTRUCTURE_CONFIG);
294
- repo.write("src/domain/grouped.rs", "use crate::{domain::types, infrastructure::db};\n");
303
+ repo.write(
304
+ "src/domain/grouped.rs",
305
+ "use crate::{domain::types, infrastructure::db};\n",
306
+ );
295
307
  repo.write("src/domain/types.rs", "pub struct Event;\n");
296
308
  repo.write("src/infrastructure/db.rs", "pub fn connect() {}\n");
297
- repo.write("src/domain/visible.rs", "pub(crate) use crate::infrastructure::db as database;\n");
298
- repo.write("src/domain/nested.rs", "use crate::{domain::types, infrastructure::{db, cache}};\n");
309
+ repo.write(
310
+ "src/domain/visible.rs",
311
+ "pub(crate) use crate::infrastructure::db as database;\n",
312
+ );
313
+ repo.write(
314
+ "src/domain/nested.rs",
315
+ "use crate::{domain::types, infrastructure::{db, cache}};\n",
316
+ );
299
317
  repo.write("src/infrastructure/cache.rs", "pub fn cache() {}\n");
300
- repo.write("src/domain/parenthesized.py", "from src.infrastructure import (\n client,\n)\n");
318
+ repo.write(
319
+ "src/domain/parenthesized.py",
320
+ "from src.infrastructure import (\n client,\n)\n",
321
+ );
301
322
  repo.write("src/infrastructure/client.py", "db = object()\n");
302
- repo.write("src/domain/dynamic.ts", "const db = await import(\n '../infrastructure/db_client'\n);\n");
323
+ repo.write(
324
+ "src/domain/dynamic.ts",
325
+ "const db = await import(\n '../infrastructure/db_client'\n);\n",
326
+ );
303
327
  repo.write("src/infrastructure/db_client.ts", "export const db = 1;\n");
304
- repo.write("src/domain/export_literal.ts", "export const fixture = '../infrastructure/db_client';\n");
305
- repo.write("src/domain/commented.ts", "// await import('../infrastructure/db_client')\n");
328
+ repo.write(
329
+ "src/domain/export_literal.ts",
330
+ "export const fixture = '../infrastructure/db_client';\n",
331
+ );
332
+ repo.write(
333
+ "src/domain/commented.ts",
334
+ "// await import('../infrastructure/db_client')\n",
335
+ );
306
336
  repo.write("go.mod", "module github.com/acme/app\n");
307
- repo.write("src/domain/commented.go", "package domain\nimport (\n // \"github.com/acme/app/src/infrastructure/dbgo\"\n)\n");
337
+ repo.write(
338
+ "src/domain/commented.go",
339
+ "package domain\nimport (\n // \"github.com/acme/app/src/infrastructure/dbgo\"\n)\n",
340
+ );
308
341
  repo.write("src/infrastructure/dbgo/dbgo.go", "package dbgo\n");
309
342
 
310
343
  let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
@@ -481,68 +514,3 @@ ignore:
481
514
  Some("generated/client.ts")
482
515
  );
483
516
  }
484
-
485
- struct FixtureRepo {
486
- root: PathBuf,
487
- }
488
-
489
- impl FixtureRepo {
490
- fn new() -> Self {
491
- let nonce = SystemTime::now()
492
- .duration_since(UNIX_EPOCH)
493
- .unwrap()
494
- .as_nanos();
495
- let counter = FIXTURE_COUNTER.fetch_add(1, Ordering::Relaxed);
496
- let root = std::env::temp_dir().join(format!(
497
- "naome-arch-fixture-{}-{nonce}-{counter}",
498
- std::process::id()
499
- ));
500
- fs::create_dir_all(&root).unwrap();
501
- fs::write(
502
- root.join(".naomeignore"),
503
- ".naome/archive/\n.naome/tasks/\n",
504
- )
505
- .unwrap();
506
- Self { root }
507
- }
508
-
509
- fn path(&self) -> &Path {
510
- &self.root
511
- }
512
-
513
- fn write(&self, relative_path: &str, content: &str) {
514
- let path = self.root.join(relative_path);
515
- fs::create_dir_all(path.parent().unwrap()).unwrap();
516
- fs::write(path, content).unwrap();
517
- }
518
-
519
- fn init_git(&self) {
520
- run_git(&self.root, &["init"]);
521
- run_git(&self.root, &["config", "user.email", "naome@example.com"]);
522
- run_git(&self.root, &["config", "user.name", "NAOME Test"]);
523
- self.write("README.md", "# Fixture\n");
524
- run_git(&self.root, &["add", "."]);
525
- run_git(&self.root, &["commit", "-m", "baseline"]);
526
- }
527
- }
528
-
529
- fn run_git(root: &Path, args: &[&str]) {
530
- let result = Command::new("git")
531
- .args(args)
532
- .current_dir(root)
533
- .output()
534
- .unwrap();
535
- assert!(
536
- result.status.success(),
537
- "git {:?} failed: {}{}",
538
- args,
539
- String::from_utf8_lossy(&result.stdout),
540
- String::from_utf8_lossy(&result.stderr)
541
- );
542
- }
543
-
544
- fn has_import_edge(scan: &naome_core::ArchitectureScanReport, from: &str, to: &str) -> bool {
545
- scan.graph.edges.iter().any(|edge| {
546
- edge.kind == ArchitectureEdgeKind::Imports && edge.from == from && edge.to == to
547
- })
548
- }
@@ -0,0 +1,289 @@
1
+ use naome_core::{scan_architecture, ArchitectureEdgeKind, ArchitectureScanOptions};
2
+
3
+ mod architecture_support;
4
+
5
+ use architecture_support::FixtureRepo;
6
+
7
+ #[test]
8
+ fn manifest_extractors_add_package_dependency_graph_nodes() {
9
+ let repo = FixtureRepo::new();
10
+ repo.write(
11
+ "package.json",
12
+ r#"{
13
+ "name": "@acme/web",
14
+ "dependencies": { "react": "^18.0.0" },
15
+ "devDependencies": { "vite": "^5.0.0" }
16
+ }
17
+ "#,
18
+ );
19
+ repo.write(
20
+ "crates/app/Cargo.toml",
21
+ "[package]\nname = \"app\"\nversion = \"0.1.0\"\n[dependencies]\nserde = \"1\"\n",
22
+ );
23
+ repo.write(
24
+ "pyproject.toml",
25
+ "[project]\nname = \"acme-python\"\ndependencies = [\"requests>=2\", \"fastapi\"]\n",
26
+ );
27
+ repo.write(
28
+ "go.mod",
29
+ "module github.com/acme/app\nrequire github.com/gin-gonic/gin v1.9.0\n",
30
+ );
31
+ repo.write(
32
+ "pom.xml",
33
+ "<project><groupId>com.acme</groupId><artifactId>service</artifactId><dependencies><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId></dependency></dependencies></project>",
34
+ );
35
+ repo.write(
36
+ "build.gradle",
37
+ "plugins { id 'java' }\ndependencies { implementation 'com.google.guava:guava:33.0.0-jre' }\n",
38
+ );
39
+
40
+ let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
41
+
42
+ for node_id in [
43
+ "package:npm:package.json",
44
+ "package:cargo:crates/app/Cargo.toml",
45
+ "package:python:pyproject.toml",
46
+ "package:go:go.mod",
47
+ "package:maven:pom.xml",
48
+ "package:gradle:build.gradle",
49
+ "external:react",
50
+ "external:vite",
51
+ "external:serde",
52
+ "external:requests",
53
+ "external:fastapi",
54
+ "external:github.com/gin-gonic/gin",
55
+ "external:org.slf4j:slf4j-api",
56
+ "external:com.google.guava:guava",
57
+ ] {
58
+ assert!(
59
+ scan.graph.nodes.iter().any(|node| node.id == node_id),
60
+ "missing {node_id}"
61
+ );
62
+ }
63
+ assert!(scan.graph.edges.iter().any(|edge| {
64
+ edge.kind == ArchitectureEdgeKind::DependsOn
65
+ && edge.from == "package:npm:package.json"
66
+ && edge.to == "external:react"
67
+ && edge.metadata.extractor == "manifest:package.json"
68
+ }));
69
+ assert!(scan.graph.edges.iter().any(|edge| {
70
+ edge.kind == ArchitectureEdgeKind::DependsOn
71
+ && edge.from == "package:go:go.mod"
72
+ && edge.to == "external:github.com/gin-gonic/gin"
73
+ }));
74
+ assert!(scan
75
+ .graph
76
+ .nodes
77
+ .iter()
78
+ .any(|node| { node.id == "package:npm:package.json" && node.label == "@acme/web" }));
79
+ }
80
+
81
+ #[test]
82
+ fn manifest_package_ids_are_namespaced_and_parse_common_multiline_forms() {
83
+ let repo = FixtureRepo::new();
84
+ repo.write(
85
+ "package.json",
86
+ r#"{
87
+ "name": "core",
88
+ "dependencies": { "react": "^18.0.0" }
89
+ }
90
+ "#,
91
+ );
92
+ repo.write(
93
+ "crates/core/Cargo.toml",
94
+ "[package]\nname = \"core\"\nversion = \"0.1.0\"\n[dependencies.serde]\nversion = \"1\"\n",
95
+ );
96
+ repo.write(
97
+ "pyproject.toml",
98
+ "[project]\nname = \"core\"\ndependencies = [\n \"requests>=2\",\n \"fastapi\",\n]\n",
99
+ );
100
+ repo.write(
101
+ "build.gradle.kts",
102
+ "dependencies {\n // implementation(\"com.acme:old-lib:1.0\")\n implementation(\"com.google.guava:guava:33.0.0-jre\")\n implementation group: 'com.squareup.okio', name: 'okio', version: '3.9.0'\n}\n",
103
+ );
104
+ repo.write(
105
+ "child/pom.xml",
106
+ r#"<project>
107
+ <parent>
108
+ <groupId>com.acme.parent</groupId>
109
+ <artifactId>parent</artifactId>
110
+ </parent>
111
+ <groupId>com.acme.child</groupId>
112
+ <artifactId>service</artifactId>
113
+ <dependencyManagement>
114
+ <dependencies>
115
+ <dependency>
116
+ <groupId>com.acme</groupId>
117
+ <artifactId>managed-only</artifactId>
118
+ </dependency>
119
+ </dependencies>
120
+ </dependencyManagement>
121
+ <dependencies>
122
+ <dependency>
123
+ <groupId>org.slf4j</groupId>
124
+ <artifactId>slf4j-api</artifactId>
125
+ </dependency>
126
+ </dependencies>
127
+ <build>
128
+ <plugins>
129
+ <plugin>
130
+ <dependencies>
131
+ <dependency>
132
+ <groupId>com.acme</groupId>
133
+ <artifactId>plugin-only</artifactId>
134
+ </dependency>
135
+ </dependencies>
136
+ </plugin>
137
+ </plugins>
138
+ </build>
139
+ </project>"#,
140
+ );
141
+ let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
142
+
143
+ for (node_id, label) in [
144
+ ("package:npm:package.json", "core"),
145
+ ("package:cargo:crates/core/Cargo.toml", "core"),
146
+ ("package:python:pyproject.toml", "core"),
147
+ ("package:gradle:build.gradle.kts", "gradle:build.gradle.kts"),
148
+ ("package:maven:child/pom.xml", "com.acme.child:service"),
149
+ ] {
150
+ assert!(
151
+ scan.graph
152
+ .nodes
153
+ .iter()
154
+ .any(|node| node.id == node_id && node.label == label),
155
+ "missing {node_id} with label {label}"
156
+ );
157
+ }
158
+ for (package_id, dependency_id) in [
159
+ ("package:npm:package.json", "external:react"),
160
+ ("package:cargo:crates/core/Cargo.toml", "external:serde"),
161
+ ("package:python:pyproject.toml", "external:requests"),
162
+ ("package:python:pyproject.toml", "external:fastapi"),
163
+ (
164
+ "package:gradle:build.gradle.kts",
165
+ "external:com.google.guava:guava",
166
+ ),
167
+ (
168
+ "package:gradle:build.gradle.kts",
169
+ "external:com.squareup.okio:okio",
170
+ ),
171
+ (
172
+ "package:maven:child/pom.xml",
173
+ "external:org.slf4j:slf4j-api",
174
+ ),
175
+ ] {
176
+ assert!(
177
+ scan.graph.edges.iter().any(|edge| {
178
+ edge.kind == ArchitectureEdgeKind::DependsOn
179
+ && edge.from == package_id
180
+ && edge.to == dependency_id
181
+ }),
182
+ "missing edge {package_id} -> {dependency_id}"
183
+ );
184
+ }
185
+ assert!(!scan
186
+ .graph
187
+ .nodes
188
+ .iter()
189
+ .any(|node| node.id == "external:com.acme:old-lib"));
190
+ assert!(!scan
191
+ .graph
192
+ .nodes
193
+ .iter()
194
+ .any(|node| node.id == "external:com.acme:managed-only"));
195
+ assert!(!scan
196
+ .graph
197
+ .nodes
198
+ .iter()
199
+ .any(|node| node.id == "external:com.acme:plugin-only"));
200
+ }
201
+
202
+ #[test]
203
+ fn swift_manifests_emit_package_and_dependency_nodes() {
204
+ let repo = FixtureRepo::new();
205
+ repo.write(
206
+ "Package.swift",
207
+ r#"// swift-tools-version: 5.9
208
+ import PackageDescription
209
+
210
+ let package = Package(
211
+ name: "Tickets",
212
+ dependencies: [
213
+ .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.9.0"),
214
+ .package(url: "https://github.com/pointfreeco/swift-composable-architecture", exact: "1.8.0")
215
+ ]
216
+ )
217
+ "#,
218
+ );
219
+ repo.write(
220
+ "Tickets.xcodeproj/project.pbxproj",
221
+ r#"
222
+ /* Begin XCRemoteSwiftPackageReference section */
223
+ repositoryURL = "https://github.com/onevcat/Kingfisher.git";
224
+ /* End XCRemoteSwiftPackageReference section */
225
+ "#,
226
+ );
227
+
228
+ let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
229
+
230
+ for node_id in [
231
+ "package:swift:Package.swift",
232
+ "package:xcode:Tickets.xcodeproj/project.pbxproj",
233
+ "external:Alamofire",
234
+ "external:swift-composable-architecture",
235
+ "external:Kingfisher",
236
+ ] {
237
+ assert!(
238
+ scan.graph.nodes.iter().any(|node| node.id == node_id),
239
+ "missing {node_id}"
240
+ );
241
+ }
242
+ assert!(scan.graph.edges.iter().any(|edge| {
243
+ edge.kind == ArchitectureEdgeKind::DependsOn
244
+ && edge.from == "package:swift:Package.swift"
245
+ && edge.to == "external:Alamofire"
246
+ }));
247
+ assert!(scan.graph.edges.iter().any(|edge| {
248
+ edge.kind == ArchitectureEdgeKind::DependsOn
249
+ && edge.from == "package:xcode:Tickets.xcodeproj/project.pbxproj"
250
+ && edge.to == "external:Kingfisher"
251
+ }));
252
+ }
253
+
254
+ #[test]
255
+ fn swift_manifests_ignore_commented_package_declarations() {
256
+ let repo = FixtureRepo::new();
257
+ repo.write(
258
+ "Package.swift",
259
+ r#"// swift-tools-version: 5.9
260
+ import PackageDescription
261
+
262
+ let package = Package(
263
+ name: "CoreSwift",
264
+ dependencies: [
265
+ // .package(url: "https://github.com/Old/Old.git", from: "1.0.0"),
266
+ .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.9.0")
267
+ ]
268
+ )
269
+ "#,
270
+ );
271
+
272
+ let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
273
+
274
+ assert!(scan
275
+ .graph
276
+ .nodes
277
+ .iter()
278
+ .any(|node| node.id == "package:swift:Package.swift" && node.label == "CoreSwift"));
279
+ assert!(scan.graph.edges.iter().any(|edge| {
280
+ edge.kind == ArchitectureEdgeKind::DependsOn
281
+ && edge.from == "package:swift:Package.swift"
282
+ && edge.to == "external:Alamofire"
283
+ }));
284
+ assert!(!scan
285
+ .graph
286
+ .nodes
287
+ .iter()
288
+ .any(|node| node.id == "external:Old"));
289
+ }