@lamentis/naome 1.3.10 → 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 (35) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +5 -0
  3. package/crates/naome-cli/Cargo.toml +1 -1
  4. package/crates/naome-cli/src/architecture_commands.rs +3 -3
  5. package/crates/naome-cli/tests/architecture_cli.rs +60 -0
  6. package/crates/naome-core/Cargo.toml +1 -1
  7. package/crates/naome-core/src/architecture/config/parser/sections.rs +61 -1
  8. package/crates/naome-core/src/architecture/config/parser.rs +2 -0
  9. package/crates/naome-core/src/architecture/config.rs +47 -0
  10. package/crates/naome-core/src/architecture/output.rs +15 -1
  11. package/crates/naome-core/src/architecture/rules/budgets.rs +179 -0
  12. package/crates/naome-core/src/architecture/rules/context.rs +138 -0
  13. package/crates/naome-core/src/architecture/rules/cycles.rs +39 -0
  14. package/crates/naome-core/src/architecture/rules/external.rs +185 -0
  15. package/crates/naome-core/src/architecture/rules/graph.rs +177 -0
  16. package/crates/naome-core/src/architecture/rules/transitive.rs +89 -0
  17. package/crates/naome-core/src/architecture/rules.rs +73 -27
  18. package/crates/naome-core/src/architecture/scan/graph_builder/emit.rs +63 -1
  19. package/crates/naome-core/src/architecture/scan/graph_builder/facts.rs +2 -3
  20. package/crates/naome-core/src/architecture/scan/graph_builder.rs +78 -1
  21. package/crates/naome-core/src/architecture/scan/imports/extractors.rs +404 -0
  22. package/crates/naome-core/src/architecture/scan/imports/resolver.rs +316 -0
  23. package/crates/naome-core/src/architecture/scan/imports.rs +75 -0
  24. package/crates/naome-core/src/architecture/scan.rs +20 -0
  25. package/crates/naome-core/src/architecture.rs +1 -1
  26. package/crates/naome-core/src/lib.rs +1 -0
  27. package/crates/naome-core/tests/architecture.rs +380 -73
  28. package/crates/naome-core/tests/architecture_rules.rs +498 -0
  29. package/crates/naome-core/tests/architecture_support/mod.rs +78 -0
  30. package/installer/harness-files.js +3 -3
  31. package/native/darwin-arm64/naome +0 -0
  32. package/native/linux-x64/naome +0 -0
  33. package/package.json +1 -1
  34. package/templates/naome-root/.naome/manifest.json +2 -2
  35. package/templates/naome-root/docs/naome/architecture-fitness.md +62 -7
@@ -1,26 +1,20 @@
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
- ArchitectureScanOptions,
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
+
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";
13
11
 
14
12
  #[test]
15
13
  fn parses_starter_architecture_config() {
16
14
  let config = ArchitectureConfig::parse(default_architecture_config_text(), "test").unwrap();
17
15
 
18
16
  assert!(config.layers.contains_key("application"));
19
- assert_eq!(
20
- config.rule("max_file_lines").value,
21
- Some(400),
22
- "starter config should seed a production file-size budget"
23
- );
17
+ assert_eq!(config.rule("max_file_lines").value, Some(400));
24
18
  assert_eq!(
25
19
  config.ignore[0].reason,
26
20
  "Generated code is not architecture-owned."
@@ -58,7 +52,6 @@ rules:
58
52
  );
59
53
 
60
54
  let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
61
-
62
55
  assert!(scan
63
56
  .graph
64
57
  .nodes
@@ -84,6 +77,379 @@ rules:
84
77
  );
85
78
  }
86
79
 
80
+ #[test]
81
+ fn import_extractors_emit_file_and_external_dependency_edges() {
82
+ let repo = FixtureRepo::new();
83
+ repo.write(
84
+ "src/domain/event.ts",
85
+ "import { db } from '../infrastructure/db';\nimport React from 'react';\n",
86
+ );
87
+ repo.write("src/infrastructure/db.ts", "export const db = 1;\n");
88
+ repo.write(
89
+ "src/domain/service.py",
90
+ "from ..infrastructure.client import db\nimport requests\n",
91
+ );
92
+ repo.write("src/infrastructure/client.py", "db = object()\n");
93
+ repo.write(
94
+ "src/domain/lib.rs",
95
+ "use crate::infrastructure::db;\nextern crate serde;\n",
96
+ );
97
+ repo.write("src/infrastructure/db.rs", "pub fn connect() {}\n");
98
+ repo.write("cmd/server/main.go", "package main\nimport \"fmt\"\n");
99
+
100
+ let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
101
+
102
+ assert!(scan.graph.edges.iter().any(|edge| {
103
+ edge.kind == ArchitectureEdgeKind::Imports
104
+ && edge.from == "file:src/domain/event.ts"
105
+ && edge.to == "file:src/infrastructure/db.ts"
106
+ }));
107
+ assert!(scan
108
+ .graph
109
+ .nodes
110
+ .iter()
111
+ .any(|node| node.id == "external:react"));
112
+ assert!(scan
113
+ .graph
114
+ .nodes
115
+ .iter()
116
+ .any(|node| node.id == "external:requests"));
117
+ assert!(scan
118
+ .graph
119
+ .nodes
120
+ .iter()
121
+ .any(|node| node.id == "external:fmt"));
122
+ assert!(scan.graph.edges.iter().any(|edge| {
123
+ edge.kind == ArchitectureEdgeKind::Imports
124
+ && edge.from == "file:src/domain/service.py"
125
+ && edge.to == "file:src/infrastructure/client.py"
126
+ }));
127
+ assert!(scan.graph.edges.iter().any(|edge| {
128
+ edge.kind == ArchitectureEdgeKind::Imports
129
+ && edge.from == "file:src/domain/lib.rs"
130
+ && edge.to == "file:src/infrastructure/db.rs"
131
+ }));
132
+ }
133
+
134
+ #[test]
135
+ fn import_resolution_covers_common_language_forms_without_dropping_layer_edges() {
136
+ let repo = FixtureRepo::new();
137
+ repo.write("naome.arch.yaml", FORBIDDEN_DOMAIN_TO_INFRASTRUCTURE_CONFIG);
138
+ repo.write(
139
+ "src/domain/event.ts",
140
+ "import {\n db\n} from '../infrastructure/db';\n",
141
+ );
142
+ repo.write("src/infrastructure/db.ts", "export const db = 1;\n");
143
+ repo.write(
144
+ "src/domain/rust_item.rs",
145
+ "use crate::infrastructure::pool::Pool;\n",
146
+ );
147
+ repo.write("src/infrastructure/pool.rs", "pub struct Pool;\n");
148
+ repo.write(
149
+ "src/domain/python_from_relative.py",
150
+ "from ..infrastructure import client\n",
151
+ );
152
+ repo.write(
153
+ "src/domain/python_from_absolute.py",
154
+ "from src.infrastructure.client import db\n",
155
+ );
156
+ repo.write(
157
+ "src/domain/python_import_list.py",
158
+ "import requests, src.infrastructure.client\n",
159
+ );
160
+ repo.write("src/infrastructure/client.py", "db = object()\n");
161
+
162
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
163
+
164
+ assert_eq!(report.summary.errors, 5);
165
+ let violation_paths = report
166
+ .violations
167
+ .iter()
168
+ .map(|violation| violation.path.as_deref().unwrap_or(""))
169
+ .collect::<Vec<_>>();
170
+ assert!(violation_paths.contains(&"src/domain/event.ts"));
171
+ assert!(violation_paths.contains(&"src/domain/rust_item.rs"));
172
+ assert!(violation_paths.contains(&"src/domain/python_from_relative.py"));
173
+ assert!(violation_paths.contains(&"src/domain/python_from_absolute.py"));
174
+ assert!(violation_paths.contains(&"src/domain/python_import_list.py"));
175
+ }
176
+
177
+ #[test]
178
+ fn relative_import_resolution_prefers_source_language_extensions() {
179
+ let repo = FixtureRepo::new();
180
+ repo.write("src/domain/mod.rs", "use self::db::right;\n");
181
+ repo.write("src/domain/db.ts", "export const wrong = true;\n");
182
+ repo.write("src/domain/db.rs", "pub fn right() {}\n");
183
+
184
+ let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
185
+
186
+ assert!(scan.graph.edges.iter().any(|edge| {
187
+ edge.kind == ArchitectureEdgeKind::Imports
188
+ && edge.from == "file:src/domain/mod.rs"
189
+ && edge.to == "file:src/domain/db.rs"
190
+ }));
191
+ assert!(!scan.graph.edges.iter().any(|edge| {
192
+ edge.kind == ArchitectureEdgeKind::Imports
193
+ && edge.from == "file:src/domain/mod.rs"
194
+ && edge.to == "file:src/domain/db.ts"
195
+ }));
196
+ }
197
+
198
+ #[test]
199
+ fn import_resolver_handles_language_specific_module_roots() {
200
+ let repo = FixtureRepo::new();
201
+ repo.write(
202
+ "packages/app/src/domain/event.rs",
203
+ "use crate::infrastructure::db::Pool;\n",
204
+ );
205
+ repo.write(
206
+ "packages/app/src/infrastructure/db.rs",
207
+ "pub struct Pool;\n",
208
+ );
209
+ repo.write(
210
+ "src/domain/service.rs",
211
+ "mod helper;\nuse self::helper::Thing;\nuse super::infra;\n",
212
+ );
213
+ repo.write("src/domain/helper.rs", "pub fn wrong() {}\n");
214
+ repo.write("src/domain/service/helper.rs", "pub fn right() {}\n");
215
+ repo.write("src/infra.rs", "pub fn wrong() {}\n");
216
+ repo.write("src/domain/infra.rs", "pub fn right() {}\n");
217
+ repo.write("go.mod", "module github.com/acme/app\n");
218
+ repo.write(
219
+ "domain/event.go",
220
+ "package domain\nimport \"github.com/acme/app/infrastructure/db\"\n",
221
+ );
222
+ repo.write(
223
+ "domain/external.go",
224
+ "package domain\nimport \"github.com/other/db\"\n",
225
+ );
226
+ repo.write("infrastructure/db/db.go", "package db\n");
227
+
228
+ let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
229
+
230
+ let has = |from, to| has_import_edge(&scan, from, to);
231
+ assert!(has(
232
+ "file:packages/app/src/domain/event.rs",
233
+ "file:packages/app/src/infrastructure/db.rs"
234
+ ));
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
+ ));
247
+ assert!(!has("file:src/domain/service.rs", "file:src/infra.rs"));
248
+ assert!(has("file:domain/event.go", "file:infrastructure/db/db.go"));
249
+ assert!(!has(
250
+ "file:domain/external.go",
251
+ "file:infrastructure/db/db.go"
252
+ ));
253
+ }
254
+
255
+ #[test]
256
+ fn missing_layer_allowlist_is_treated_as_no_allowed_dependencies() {
257
+ let repo = FixtureRepo::new();
258
+ repo.write(
259
+ "naome.arch.yaml",
260
+ r#"
261
+ layers:
262
+ domain:
263
+ paths:
264
+ - "src/domain/**"
265
+ infrastructure:
266
+ paths:
267
+ - "src/infrastructure/**"
268
+ rules:
269
+ no_forbidden_layer_dependencies:
270
+ enabled: true
271
+ severity: error
272
+ "#,
273
+ );
274
+ repo.write(
275
+ "src/domain/event.ts",
276
+ "import { db } from '../infrastructure/db';\n",
277
+ );
278
+ repo.write("src/infrastructure/db.ts", "export const db = 1;\n");
279
+
280
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
281
+
282
+ assert_eq!(report.status, "fail");
283
+ assert!(report.violations.iter().any(|violation| {
284
+ violation.id == "arch.no_forbidden_layer_dependencies"
285
+ && violation.path.as_deref() == Some("src/domain/event.ts")
286
+ }));
287
+ }
288
+
289
+ #[test]
290
+ fn overlapping_target_layers_do_not_create_false_forbidden_dependency() {
291
+ let repo = FixtureRepo::new();
292
+ repo.write("naome.arch.yaml", "layers:\n application:\n paths:\n - \"src/**\"\n domain:\n paths:\n - \"src/domain/**\"\nallowed_dependencies:\n application:\n - domain\n domain:\nrules:\n no_forbidden_layer_dependencies:\n enabled: true\n severity: error\n");
293
+ repo.write("src/domain/event.ts", "import { type } from './types';\n");
294
+ repo.write("src/domain/types.ts", "export const type = 1;\n");
295
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
296
+ assert_eq!(report.status, "pass");
297
+ }
298
+
299
+ #[test]
300
+ fn late_review_import_forms_keep_forbidden_layer_edges_visible() {
301
+ let repo = FixtureRepo::new();
302
+ repo.write("naome.arch.yaml", FORBIDDEN_DOMAIN_TO_INFRASTRUCTURE_CONFIG);
303
+ repo.write(
304
+ "src/domain/grouped.rs",
305
+ "use crate::{domain::types, infrastructure::db};\n",
306
+ );
307
+ repo.write("src/domain/types.rs", "pub struct Event;\n");
308
+ repo.write("src/infrastructure/db.rs", "pub fn connect() {}\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
+ );
317
+ repo.write("src/infrastructure/cache.rs", "pub fn cache() {}\n");
318
+ repo.write(
319
+ "src/domain/parenthesized.py",
320
+ "from src.infrastructure import (\n client,\n)\n",
321
+ );
322
+ repo.write("src/infrastructure/client.py", "db = object()\n");
323
+ repo.write(
324
+ "src/domain/dynamic.ts",
325
+ "const db = await import(\n '../infrastructure/db_client'\n);\n",
326
+ );
327
+ repo.write("src/infrastructure/db_client.ts", "export const db = 1;\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
+ );
336
+ repo.write("go.mod", "module github.com/acme/app\n");
337
+ repo.write(
338
+ "src/domain/commented.go",
339
+ "package domain\nimport (\n // \"github.com/acme/app/src/infrastructure/dbgo\"\n)\n",
340
+ );
341
+ repo.write("src/infrastructure/dbgo/dbgo.go", "package dbgo\n");
342
+
343
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
344
+ let violation_paths = report
345
+ .violations
346
+ .iter()
347
+ .map(|violation| violation.path.as_deref().unwrap_or(""))
348
+ .collect::<Vec<_>>();
349
+
350
+ assert!(violation_paths.contains(&"src/domain/grouped.rs"));
351
+ assert!(violation_paths.contains(&"src/domain/visible.rs"));
352
+ assert!(violation_paths.contains(&"src/domain/nested.rs"));
353
+ assert!(violation_paths.contains(&"src/domain/parenthesized.py"));
354
+ assert!(violation_paths.contains(&"src/domain/dynamic.ts"));
355
+ assert!(!violation_paths.contains(&"src/domain/export_literal.ts"));
356
+ assert!(!violation_paths.contains(&"src/domain/commented.ts"));
357
+ assert!(!violation_paths.contains(&"src/domain/commented.go"));
358
+ }
359
+
360
+ #[test]
361
+ fn unresolved_relative_imports_are_represented_explicitly() {
362
+ let repo = FixtureRepo::new();
363
+ repo.write("src/app.ts", "import missing from './missing';\n");
364
+
365
+ let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
366
+
367
+ assert!(scan
368
+ .graph
369
+ .nodes
370
+ .iter()
371
+ .any(|node| node.id == "unknown-import:./missing"));
372
+ assert!(scan.graph.edges.iter().any(|edge| {
373
+ edge.kind == ArchitectureEdgeKind::Imports
374
+ && edge.from == "file:src/app.ts"
375
+ && edge.to == "unknown-import:./missing"
376
+ }));
377
+ }
378
+
379
+ #[test]
380
+ fn validates_forbidden_layer_dependency_from_import_edges() {
381
+ let repo = FixtureRepo::new();
382
+ repo.write("naome.arch.yaml", FORBIDDEN_DOMAIN_TO_INFRASTRUCTURE_CONFIG);
383
+ repo.write(
384
+ "src/domain/event.ts",
385
+ "import { db } from '../infrastructure/db';\n",
386
+ );
387
+ repo.write("src/infrastructure/db.ts", "export const db = 1;\n");
388
+
389
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
390
+
391
+ assert_eq!(report.status, "fail");
392
+ let violation = report
393
+ .violations
394
+ .iter()
395
+ .find(|violation| violation.id == "arch.no_forbidden_layer_dependencies")
396
+ .expect("expected forbidden layer dependency violation");
397
+ assert_eq!(violation.path.as_deref(), Some("src/domain/event.ts"));
398
+ assert_eq!(violation.source_range.as_ref().unwrap().start_line, 1);
399
+ assert!(violation
400
+ .agent_instruction
401
+ .contains("Do not import infrastructure from domain"));
402
+ }
403
+
404
+ #[test]
405
+ fn exact_entrypoint_layer_paths_keep_bootstrap_out_of_cli_layer() {
406
+ let repo = FixtureRepo::new();
407
+ repo.write(
408
+ "naome.arch.yaml",
409
+ r#"
410
+ layers:
411
+ cli:
412
+ paths:
413
+ - "packages/naome/bin/naome.js"
414
+ installer:
415
+ paths:
416
+ - "packages/naome/bin/naome-node.js"
417
+ - "packages/naome/installer/**"
418
+ allowed_dependencies:
419
+ cli:
420
+ installer:
421
+ rules:
422
+ no_forbidden_layer_dependencies:
423
+ enabled: true
424
+ severity: error
425
+ "#,
426
+ );
427
+ repo.write(
428
+ "packages/naome/bin/naome.js",
429
+ "console.log('native wrapper');\n",
430
+ );
431
+ repo.write(
432
+ "packages/naome/bin/naome-node.js",
433
+ "import { runNaomeNodeCli } from '../installer/main.js';\nawait runNaomeNodeCli();\n",
434
+ );
435
+ repo.write(
436
+ "packages/naome/installer/main.js",
437
+ "export async function runNaomeNodeCli() {}\n",
438
+ );
439
+
440
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
441
+ let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
442
+
443
+ assert_eq!(report.status, "pass");
444
+ assert_eq!(
445
+ scan.file_facts
446
+ .get("packages/naome/bin/naome-node.js")
447
+ .unwrap()
448
+ .layers,
449
+ vec!["installer"]
450
+ );
451
+ }
452
+
87
453
  #[test]
88
454
  fn validates_file_size_budget_with_stable_json_shape() {
89
455
  let repo = FixtureRepo::new();
@@ -148,62 +514,3 @@ ignore:
148
514
  Some("generated/client.ts")
149
515
  );
150
516
  }
151
-
152
- struct FixtureRepo {
153
- root: PathBuf,
154
- }
155
-
156
- impl FixtureRepo {
157
- fn new() -> Self {
158
- let nonce = SystemTime::now()
159
- .duration_since(UNIX_EPOCH)
160
- .unwrap()
161
- .as_nanos();
162
- let counter = FIXTURE_COUNTER.fetch_add(1, Ordering::Relaxed);
163
- let root = std::env::temp_dir().join(format!(
164
- "naome-arch-fixture-{}-{nonce}-{counter}",
165
- std::process::id()
166
- ));
167
- fs::create_dir_all(&root).unwrap();
168
- fs::write(
169
- root.join(".naomeignore"),
170
- ".naome/archive/\n.naome/tasks/\n",
171
- )
172
- .unwrap();
173
- Self { root }
174
- }
175
-
176
- fn path(&self) -> &Path {
177
- &self.root
178
- }
179
-
180
- fn write(&self, relative_path: &str, content: &str) {
181
- let path = self.root.join(relative_path);
182
- fs::create_dir_all(path.parent().unwrap()).unwrap();
183
- fs::write(path, content).unwrap();
184
- }
185
-
186
- fn init_git(&self) {
187
- run_git(&self.root, &["init"]);
188
- run_git(&self.root, &["config", "user.email", "naome@example.com"]);
189
- run_git(&self.root, &["config", "user.name", "NAOME Test"]);
190
- self.write("README.md", "# Fixture\n");
191
- run_git(&self.root, &["add", "."]);
192
- run_git(&self.root, &["commit", "-m", "baseline"]);
193
- }
194
- }
195
-
196
- fn run_git(root: &Path, args: &[&str]) {
197
- let result = Command::new("git")
198
- .args(args)
199
- .current_dir(root)
200
- .output()
201
- .unwrap();
202
- assert!(
203
- result.status.success(),
204
- "git {:?} failed: {}{}",
205
- args,
206
- String::from_utf8_lossy(&result.stdout),
207
- String::from_utf8_lossy(&result.stderr)
208
- );
209
- }