@lamentis/naome 1.3.11 → 1.3.12

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 (30) 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 +185 -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/imports/extractors.rs +4 -7
  18. package/crates/naome-core/src/architecture/scan/imports/resolver.rs +3 -21
  19. package/crates/naome-core/src/architecture/scan/imports.rs +16 -0
  20. package/crates/naome-core/src/architecture.rs +1 -1
  21. package/crates/naome-core/src/lib.rs +1 -0
  22. package/crates/naome-core/tests/architecture.rs +53 -85
  23. package/crates/naome-core/tests/architecture_rules.rs +498 -0
  24. package/crates/naome-core/tests/architecture_support/mod.rs +78 -0
  25. package/installer/harness-files.js +3 -3
  26. package/native/darwin-arm64/naome +0 -0
  27. package/native/linux-x64/naome +0 -0
  28. package/package.json +1 -1
  29. package/templates/naome-root/.naome/manifest.json +1 -1
  30. package/templates/naome-root/docs/naome/architecture-fitness.md +49 -6
@@ -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,498 @@
1
+ use std::fs;
2
+ use std::path::{Path, PathBuf};
3
+ use std::sync::atomic::{AtomicU64, Ordering};
4
+ use std::time::{SystemTime, UNIX_EPOCH};
5
+
6
+ use naome_core::{validate_architecture, ArchitectureScanOptions};
7
+
8
+ static FIXTURE_COUNTER: AtomicU64 = AtomicU64::new(0);
9
+ const BILLING_INTERNAL_DB_IMPORT: &str = "import { db } from '../src/billing/internal/db';\n";
10
+ const BILLING_INTERNAL_DB_EXPORT: &str = "export const db = 1;\n";
11
+ const RUST_PARENT_MODULE_ENTRY: &str = "mod parent;\nuse parent::run;\npub fn main() { run(); }\n";
12
+ const RUST_PARENT_MODULE_SOURCE: &str =
13
+ "mod child;\nuse self::child::call;\npub fn helper() {}\npub fn run() { call(); }\n";
14
+ const RUST_CHILD_MODULE_SOURCE: &str = "use super::helper;\npub fn call() { helper(); }\n";
15
+ const RUST_CRATE_PARENT_MODULE_SOURCE: &str =
16
+ "mod child;\nuse crate::parent::child::call;\npub fn helper() {}\npub fn run() { call(); }\n";
17
+ const RUST_CRATE_CHILD_MODULE_SOURCE: &str =
18
+ "use crate::parent::helper;\npub fn call() { helper(); }\n";
19
+ const TRANSITIVE_LAYER_POLICY_CONFIG: &str = "layers:\n domain:\n paths:\n - \"src/domain/**\"\n shared:\n paths:\n - \"src/shared/**\"\n infrastructure:\n paths:\n - \"src/infrastructure/**\"\nallowed_dependencies:\n domain:\n - shared\n shared:\n infrastructure:\n - domain\n - shared\nexternal_dependencies:\n domain:\n allow: []\n infrastructure:\n allow:\n - \"stripe\"\n - \"@supabase/*\"\nrules:\n no_transitive_forbidden_layer_dependencies:\n enabled: true\n severity: error\n external_dependency_policy:\n enabled: true\n severity: error\n";
20
+ const GO_EXTERNAL_POLICY_CONFIG: &str = "layers:\n domain:\n paths:\n - \"domain/**\"\nexternal_dependencies:\n domain:\n allow:\n - \"github.com/acme/payments\"\nrules:\n external_dependency_policy:\n enabled: true\n severity: error\n";
21
+ const PYTHON_RUST_EXTERNAL_POLICY_CONFIG: &str = "layers:\n domain:\n paths:\n - \"src/domain/**\"\nexternal_dependencies:\n domain:\n allow:\n - \"requests\"\n - \"serde\"\nrules:\n external_dependency_policy:\n enabled: true\n severity: error\n";
22
+ const STDLIB_EXTERNAL_POLICY_CONFIG: &str = "layers:\n domain:\n paths:\n - \"src/domain/**\"\n - \"domain/**\"\nexternal_dependencies:\n domain:\n allow: []\nrules:\n external_dependency_policy:\n enabled: true\n severity: error\n";
23
+
24
+ #[test]
25
+ fn context_public_api_cycles_and_budgets_report_deterministic_violations() {
26
+ let repo = fixture_root();
27
+ write_context_boundary_config(
28
+ &repo,
29
+ false,
30
+ " no_cycles:\n enabled: true\n severity: error\n max_imports_per_file:\n enabled: true\n value: 1\n severity: warning\n max_fan_out:\n enabled: true\n value: 1\n severity: warning\n",
31
+ );
32
+ fixture_write(
33
+ &repo,
34
+ "src/billing/use_ticket.ts",
35
+ "import { internal } from '../ticketing/internal/service';\nimport { helper } from './helper';\n",
36
+ );
37
+ fixture_write(&repo, "src/billing/helper.ts", "export const helper = 1;\n");
38
+ fixture_write(
39
+ &repo,
40
+ "src/ticketing/internal/service.ts",
41
+ "export const internal = 1;\n",
42
+ );
43
+ fixture_write(
44
+ &repo,
45
+ "src/billing/a.ts",
46
+ "import { b } from './b';\nexport const a = b;\n",
47
+ );
48
+ fixture_write(
49
+ &repo,
50
+ "src/billing/b.ts",
51
+ "import { a } from './a';\nexport const b = a;\n",
52
+ );
53
+
54
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
55
+ let ids = report
56
+ .violations
57
+ .iter()
58
+ .map(|violation| violation.id.as_str())
59
+ .collect::<Vec<_>>();
60
+
61
+ assert!(ids.contains(&"arch.no_cross_context_internal_imports"));
62
+ assert!(ids.contains(&"arch.public_api_boundary"));
63
+ assert!(ids.contains(&"arch.no_cycles"));
64
+ assert!(ids.contains(&"arch.max_imports_per_file"));
65
+ assert!(ids.contains(&"arch.max_fan_out"));
66
+ assert!(report
67
+ .violations
68
+ .iter()
69
+ .any(|violation| violation.agent_instruction.contains("public API")));
70
+ }
71
+
72
+ #[test]
73
+ fn catch_all_contexts_do_not_hide_specific_context_boundaries() {
74
+ let repo = fixture_root();
75
+ write_context_boundary_config(&repo, true, "");
76
+ fixture_write(
77
+ &repo,
78
+ "src/billing/use_ticket.ts",
79
+ "import { internal } from '../ticketing/internal/service';\n",
80
+ );
81
+ fixture_write(
82
+ &repo,
83
+ "src/ticketing/internal/service.ts",
84
+ "export const internal = 1;\n",
85
+ );
86
+
87
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
88
+
89
+ assert!(report
90
+ .violations
91
+ .iter()
92
+ .any(|violation| violation.id == "arch.no_cross_context_internal_imports"));
93
+ assert!(report
94
+ .violations
95
+ .iter()
96
+ .any(|violation| violation.id == "arch.public_api_boundary"));
97
+ }
98
+
99
+ #[test]
100
+ fn catch_all_contexts_do_not_turn_shared_helpers_into_context_targets() {
101
+ let repo = fixture_root();
102
+ write_context_boundary_config(&repo, true, "");
103
+ fixture_write(
104
+ &repo,
105
+ "src/billing/use_logger.ts",
106
+ "import { logger } from '../shared/logger';\n",
107
+ );
108
+ fixture_write(
109
+ &repo,
110
+ "src/shared/logger.ts",
111
+ "export const logger = console;\n",
112
+ );
113
+
114
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
115
+
116
+ assert!(
117
+ report.violations.iter().all(|violation| !matches!(
118
+ violation.id.as_str(),
119
+ "arch.no_cross_context_internal_imports" | "arch.public_api_boundary"
120
+ )),
121
+ "{:?}",
122
+ report.violations
123
+ );
124
+ }
125
+
126
+ fn write_context_boundary_config(repo: &Path, include_default: bool, extra_rules: &str) {
127
+ let default_context = if include_default {
128
+ " default:\n paths:\n - \"src/**\"\n public_api:\n - \"src/index.ts\"\n"
129
+ } else {
130
+ ""
131
+ };
132
+ fixture_write(
133
+ repo,
134
+ "naome.arch.yaml",
135
+ &format!(
136
+ "contexts:\n{default_context} billing:\n paths:\n - \"src/billing/**\"\n public_api:\n - \"src/billing/index.ts\"\n ticketing:\n paths:\n - \"src/ticketing/**\"\n public_api:\n - \"src/ticketing/index.ts\"\nrules:\n no_cross_context_internal_imports:\n enabled: true\n severity: error\n public_api_boundary:\n enabled: true\n severity: error\n{extra_rules}"
137
+ ),
138
+ );
139
+ }
140
+
141
+ #[test]
142
+ fn transitive_layer_and_external_dependency_policies_are_enforced() {
143
+ let repo = fixture_root();
144
+ fixture_write(&repo, "naome.arch.yaml", TRANSITIVE_LAYER_POLICY_CONFIG);
145
+ fixture_write(
146
+ &repo,
147
+ "src/domain/event.ts",
148
+ "import { shared } from '../shared/shared';\nimport Stripe from 'stripe';\n",
149
+ );
150
+ fixture_write(
151
+ &repo,
152
+ "src/shared/shared.ts",
153
+ "import { db } from '../infrastructure/db';\nexport const shared = db;\n",
154
+ );
155
+ fixture_write(&repo, "src/infrastructure/db.ts", "export const db = 1;\n");
156
+ fixture_write(
157
+ &repo,
158
+ "src/infrastructure/payments.ts",
159
+ "import Stripe from 'stripe';\nexport const payments = Stripe;\n",
160
+ );
161
+
162
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
163
+
164
+ assert!(report.violations.iter().any(|violation| {
165
+ violation.id == "arch.no_transitive_forbidden_layer_dependencies"
166
+ && violation.path.as_deref() == Some("src/domain/event.ts")
167
+ }));
168
+ assert!(report.violations.iter().any(|violation| {
169
+ violation.id == "arch.external_dependency_policy"
170
+ && violation.path.as_deref() == Some("src/domain/event.ts")
171
+ && violation.to.as_deref() == Some("external:stripe")
172
+ }));
173
+ assert!(!report.violations.iter().any(|violation| {
174
+ violation.id == "arch.external_dependency_policy"
175
+ && violation.path.as_deref() == Some("src/infrastructure/payments.ts")
176
+ }));
177
+ }
178
+
179
+ #[test]
180
+ fn go_external_dependency_policy_matches_full_module_paths() {
181
+ let repo = fixture_root();
182
+ fixture_write(&repo, "naome.arch.yaml", GO_EXTERNAL_POLICY_CONFIG);
183
+ fixture_write(
184
+ &repo,
185
+ "domain/payments.go",
186
+ "package domain\nimport \"github.com/acme/payments\"\n",
187
+ );
188
+ fixture_write(
189
+ &repo,
190
+ "domain/storage.go",
191
+ "package domain\nimport \"github.com/acme/storage\"\n",
192
+ );
193
+
194
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
195
+
196
+ assert!(!report.violations.iter().any(|violation| {
197
+ violation.id == "arch.external_dependency_policy"
198
+ && violation.path.as_deref() == Some("domain/payments.go")
199
+ }));
200
+ assert!(report.violations.iter().any(|violation| {
201
+ violation.id == "arch.external_dependency_policy"
202
+ && violation.path.as_deref() == Some("domain/storage.go")
203
+ && violation.to.as_deref() == Some("external:github.com/acme/storage")
204
+ }));
205
+ }
206
+
207
+ #[test]
208
+ fn external_dependency_policy_normalizes_python_members_and_rust_aliases() {
209
+ let repo = fixture_root();
210
+ fixture_write(&repo, "naome.arch.yaml", PYTHON_RUST_EXTERNAL_POLICY_CONFIG);
211
+ fixture_write(
212
+ &repo,
213
+ "src/domain/http.py",
214
+ "from requests import Session\n",
215
+ );
216
+ fixture_write(
217
+ &repo,
218
+ "src/domain/lib.rs",
219
+ "extern crate serde as serde_json;\n",
220
+ );
221
+
222
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
223
+
224
+ assert!(
225
+ report
226
+ .violations
227
+ .iter()
228
+ .all(|violation| violation.id != "arch.external_dependency_policy"),
229
+ "{:?}",
230
+ report.violations
231
+ );
232
+ }
233
+
234
+ #[test]
235
+ fn external_dependency_policy_ignores_language_standard_libraries() {
236
+ let repo = fixture_root();
237
+ fixture_write(&repo, "naome.arch.yaml", STDLIB_EXTERNAL_POLICY_CONFIG);
238
+ fixture_write(
239
+ &repo,
240
+ "src/domain/node.js",
241
+ "import fs from 'node:fs';\nimport express from 'express';\n",
242
+ );
243
+ fixture_write(
244
+ &repo,
245
+ "src/domain/service.py",
246
+ "from os import path\nimport requests\n",
247
+ );
248
+ fixture_write(
249
+ &repo,
250
+ "src/domain/lib.rs",
251
+ "use std::fs;\nextern crate serde;\n",
252
+ );
253
+ fixture_write(
254
+ &repo,
255
+ "domain/main.go",
256
+ "package domain\nimport (\n \"fmt\"\n \"github.com/acme/storage\"\n)\n",
257
+ );
258
+
259
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
260
+ let external_targets = report
261
+ .violations
262
+ .iter()
263
+ .filter(|violation| violation.id == "arch.external_dependency_policy")
264
+ .filter_map(|violation| violation.to.as_deref())
265
+ .collect::<Vec<_>>();
266
+
267
+ assert_eq!(
268
+ external_targets,
269
+ vec![
270
+ "external:github.com/acme/storage",
271
+ "external:serde",
272
+ "external:express",
273
+ "external:requests"
274
+ ]
275
+ );
276
+ }
277
+
278
+ #[test]
279
+ fn uncontexted_callers_must_use_context_public_apis() {
280
+ let repo = fixture_root();
281
+ fixture_write(
282
+ &repo,
283
+ "naome.arch.yaml",
284
+ r#"
285
+ contexts:
286
+ billing:
287
+ paths:
288
+ - "src/billing/**"
289
+ public_api:
290
+ - "src/billing/index.ts"
291
+ rules:
292
+ no_cross_context_internal_imports:
293
+ enabled: true
294
+ severity: error
295
+ public_api_boundary:
296
+ enabled: true
297
+ severity: error
298
+ "#,
299
+ );
300
+ fixture_write(&repo, "tests/billing_test.ts", BILLING_INTERNAL_DB_IMPORT);
301
+ fixture_write(
302
+ &repo,
303
+ "src/billing/internal/db.ts",
304
+ BILLING_INTERNAL_DB_EXPORT,
305
+ );
306
+
307
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
308
+
309
+ assert!(report
310
+ .violations
311
+ .iter()
312
+ .any(|violation| violation.id == "arch.no_cross_context_internal_imports"));
313
+ assert!(report
314
+ .violations
315
+ .iter()
316
+ .any(|violation| violation.id == "arch.public_api_boundary"));
317
+ }
318
+
319
+ #[test]
320
+ fn non_public_cross_context_imports_do_not_count_as_internal_imports() {
321
+ let repo = fixture_root();
322
+ write_context_boundary_config(&repo, false, "");
323
+ fixture_write(
324
+ &repo,
325
+ "src/billing/use_ticket.ts",
326
+ "import { helper } from '../ticketing/helpers';\n",
327
+ );
328
+ fixture_write(
329
+ &repo,
330
+ "src/ticketing/helpers.ts",
331
+ "export const helper = 1;\n",
332
+ );
333
+
334
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
335
+
336
+ assert!(!report
337
+ .violations
338
+ .iter()
339
+ .any(|violation| violation.id == "arch.no_cross_context_internal_imports"));
340
+ assert!(report
341
+ .violations
342
+ .iter()
343
+ .any(|violation| violation.id == "arch.public_api_boundary"));
344
+ }
345
+
346
+ #[test]
347
+ fn rust_module_declarations_do_not_create_import_cycles() {
348
+ let repo = fixture_root();
349
+ write_cycle_config(&repo);
350
+ fixture_write(&repo, "src/lib.rs", RUST_PARENT_MODULE_ENTRY);
351
+ fixture_write(&repo, "src/parent.rs", RUST_PARENT_MODULE_SOURCE);
352
+ fixture_write(&repo, "src/parent/child.rs", RUST_CHILD_MODULE_SOURCE);
353
+
354
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
355
+
356
+ assert!(
357
+ report
358
+ .violations
359
+ .iter()
360
+ .all(|violation| violation.id != "arch.no_cycles"),
361
+ "{:?}",
362
+ report.violations
363
+ );
364
+ }
365
+
366
+ #[test]
367
+ fn rust_crate_qualified_parent_child_modules_do_not_create_import_cycles() {
368
+ let repo = fixture_root();
369
+ write_cycle_config(&repo);
370
+ fixture_write(&repo, "src/lib.rs", "mod parent;\n");
371
+ fixture_write(&repo, "src/parent.rs", RUST_CRATE_PARENT_MODULE_SOURCE);
372
+ fixture_write(&repo, "src/parent/child.rs", RUST_CRATE_CHILD_MODULE_SOURCE);
373
+
374
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
375
+
376
+ assert!(
377
+ report
378
+ .violations
379
+ .iter()
380
+ .all(|violation| violation.id != "arch.no_cycles"),
381
+ "{:?}",
382
+ report.violations
383
+ );
384
+ }
385
+
386
+ #[test]
387
+ fn self_imports_are_reported_as_cycles() {
388
+ let repo = fixture_root();
389
+ write_cycle_config(&repo);
390
+ fixture_write(
391
+ &repo,
392
+ "src/a.ts",
393
+ "import { a } from './a';\nexport const a = 1;\n",
394
+ );
395
+
396
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
397
+
398
+ assert!(
399
+ report
400
+ .violations
401
+ .iter()
402
+ .any(|violation| violation.id == "arch.no_cycles"),
403
+ "{:?}",
404
+ report.violations
405
+ );
406
+ }
407
+
408
+ #[test]
409
+ fn rust_sibling_module_import_cycles_are_still_reported() {
410
+ let repo = rust_sibling_cycle_repo(
411
+ "use crate::parent::right::right;\npub fn left() { right(); }\n",
412
+ "use crate::parent::left::left;\npub fn right() { left(); }\n",
413
+ );
414
+
415
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
416
+
417
+ assert!(
418
+ report
419
+ .violations
420
+ .iter()
421
+ .any(|violation| violation.id == "arch.no_cycles"),
422
+ "{:?}",
423
+ report.violations
424
+ );
425
+ }
426
+
427
+ #[test]
428
+ fn rust_relative_sibling_module_references_are_module_internal() {
429
+ let repo = rust_sibling_cycle_repo(
430
+ "use super::right::right;\npub fn left() { right(); }\n",
431
+ "use super::left::left;\npub fn right() { left(); }\n",
432
+ );
433
+
434
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
435
+
436
+ assert!(
437
+ report
438
+ .violations
439
+ .iter()
440
+ .any(|violation| violation.id == "arch.no_cycles"),
441
+ "{:?}",
442
+ report.violations
443
+ );
444
+ }
445
+
446
+ fn rust_sibling_cycle_repo(left_source: &str, right_source: &str) -> PathBuf {
447
+ let repo = fixture_root();
448
+ write_cycle_config(&repo);
449
+ fixture_write(&repo, "src/lib.rs", "mod parent;\n");
450
+ fixture_write(&repo, "src/parent.rs", "pub mod left;\npub mod right;\n");
451
+ fixture_write(&repo, "src/parent/left.rs", left_source);
452
+ fixture_write(&repo, "src/parent/right.rs", right_source);
453
+ repo
454
+ }
455
+
456
+ fn write_cycle_config(repo: &Path) {
457
+ fixture_write(
458
+ repo,
459
+ "naome.arch.yaml",
460
+ r#"
461
+ rules:
462
+ no_cycles:
463
+ enabled: true
464
+ severity: error
465
+ "#,
466
+ );
467
+ }
468
+
469
+ fn fixture_root() -> PathBuf {
470
+ let nonce = SystemTime::now()
471
+ .duration_since(UNIX_EPOCH)
472
+ .unwrap()
473
+ .as_nanos();
474
+ let counter = FIXTURE_COUNTER.fetch_add(1, Ordering::Relaxed);
475
+ let mut root = std::env::temp_dir();
476
+ root.push(format!(
477
+ "naome-arch-rules-fixture-{}-{nonce}-{counter}",
478
+ std::process::id()
479
+ ));
480
+ fs::create_dir_all(&root).unwrap();
481
+ fs::write(
482
+ root.join(".naomeignore"),
483
+ [".naome/archive/", ".naome/tasks/", ""].join("\n"),
484
+ )
485
+ .unwrap();
486
+ root
487
+ }
488
+
489
+ fn fixture_write(root: &Path, relative_path: &str, content: &str) {
490
+ let path = relative_path
491
+ .split('/')
492
+ .fold(root.to_path_buf(), |mut path, segment| {
493
+ path.push(segment);
494
+ path
495
+ });
496
+ fs::create_dir_all(path.parent().unwrap()).unwrap();
497
+ fs::write(path, content).unwrap();
498
+ }