@lamentis/naome 1.3.15 → 1.3.17

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 (40) 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 +5 -3
  4. package/crates/naome-cli/src/architecture_init/infer.rs +131 -0
  5. package/crates/naome-cli/src/architecture_init/render.rs +56 -0
  6. package/crates/naome-cli/src/architecture_init/repository.rs +59 -0
  7. package/crates/naome-cli/src/architecture_init.rs +17 -0
  8. package/crates/naome-cli/src/main.rs +1 -0
  9. package/crates/naome-cli/tests/architecture_cli.rs +75 -0
  10. package/crates/naome-core/Cargo.toml +1 -1
  11. package/crates/naome-core/src/architecture/config_findings/configuration/coverage.rs +81 -0
  12. package/crates/naome-core/src/architecture/config_findings/configuration/overlap.rs +117 -0
  13. package/crates/naome-core/src/architecture/config_findings/configuration.rs +12 -0
  14. package/crates/naome-core/src/architecture/config_findings/imports.rs +30 -0
  15. package/crates/naome-core/src/architecture/config_findings.rs +50 -0
  16. package/crates/naome-core/src/architecture/explain.rs +45 -0
  17. package/crates/naome-core/src/architecture/output.rs +59 -36
  18. package/crates/naome-core/src/architecture/rules.rs +5 -1
  19. package/crates/naome-core/src/architecture/scan/cache.rs +1 -1
  20. package/crates/naome-core/src/architecture/scan/imports/resolver/candidates.rs +71 -0
  21. package/crates/naome-core/src/architecture/scan/imports/resolver/js_ts_alias.rs +241 -0
  22. package/crates/naome-core/src/architecture/scan/imports/resolver.rs +162 -91
  23. package/crates/naome-core/src/architecture/scan.rs +20 -6
  24. package/crates/naome-core/src/architecture.rs +6 -2
  25. package/crates/naome-core/src/lib.rs +8 -7
  26. package/crates/naome-core/tests/architecture.rs +30 -0
  27. package/crates/naome-core/tests/architecture_acceptance.rs +304 -0
  28. package/crates/naome-core/tests/architecture_aliases.rs +101 -0
  29. package/crates/naome-core/tests/architecture_cache.rs +57 -0
  30. package/crates/naome-core/tests/architecture_config.rs +154 -0
  31. package/crates/naome-core/tests/architecture_rules.rs +32 -0
  32. package/crates/naome-core/tests/architecture_unresolved.rs +36 -0
  33. package/native/darwin-arm64/naome +0 -0
  34. package/native/linux-x64/naome +0 -0
  35. package/package.json +1 -1
  36. package/templates/naome-root/.naome/bin/check-harness-health.js +1 -0
  37. package/templates/naome-root/.naome/bin/check-task-state.js +1 -0
  38. package/templates/naome-root/.naome/manifest.json +1 -1
  39. package/templates/naome-root/.naome/verification.json +6 -1
  40. package/templates/naome-root/docs/naome/architecture-fitness.md +68 -51
@@ -5,6 +5,11 @@ use std::path::{Path, PathBuf};
5
5
  use super::{external_target, language_for_path};
6
6
  use crate::architecture::scan::ImportTarget;
7
7
 
8
+ mod candidates;
9
+ mod js_ts_alias;
10
+
11
+ use candidates::{resolve_candidates, resolve_progressively};
12
+
8
13
  pub(super) fn resolve_import(
9
14
  root: &Path,
10
15
  from_path: &str,
@@ -27,9 +32,30 @@ pub(super) fn resolve_import(
27
32
  .map(ImportTarget::File)
28
33
  .unwrap_or_else(|| ImportTarget::Unknown(specifier.to_string()));
29
34
  }
35
+ if matches!(language, Some("typescript") | Some("javascript")) {
36
+ match js_ts_alias::resolve(root, from_path, specifier, repository_files, language) {
37
+ Some(js_ts_alias::AliasResolution::Resolved(path)) => return ImportTarget::File(path),
38
+ Some(js_ts_alias::AliasResolution::Unresolved) => {
39
+ return ImportTarget::Unknown(specifier.to_string());
40
+ }
41
+ None => {}
42
+ }
43
+ }
30
44
  if let Some(path) = resolve_repo_absolute(specifier, repository_files, language) {
31
45
  return ImportTarget::File(path);
32
46
  }
47
+ if is_repo_absolute_specifier(specifier) {
48
+ return ImportTarget::Unknown(specifier.to_string());
49
+ }
50
+ if from_path.ends_with(".py") {
51
+ if let Some(path) = resolve_python_absolute_package(from_path, specifier, repository_files)
52
+ {
53
+ return ImportTarget::File(path);
54
+ }
55
+ if is_python_local_package_specifier(from_path, specifier, repository_files) {
56
+ return ImportTarget::Unknown(specifier.to_string());
57
+ }
58
+ }
33
59
  if from_path.ends_with(".go") {
34
60
  if let Some(path) = resolve_go_module_path(root, from_path, specifier, repository_files) {
35
61
  return ImportTarget::File(path);
@@ -72,12 +98,7 @@ fn resolve_python_relative(
72
98
  for _ in 1..dots {
73
99
  base = base.parent().unwrap_or_else(|| Path::new(""));
74
100
  }
75
- resolve_progressively(
76
- &normalize(base.join(module)),
77
- repository_files,
78
- Some("python"),
79
- '/',
80
- )
101
+ resolve_python_package_candidate(&normalize(base.join(module)), repository_files)
81
102
  }
82
103
 
83
104
  fn resolve_repo_absolute(
@@ -102,6 +123,86 @@ fn resolve_repo_absolute(
102
123
  None
103
124
  }
104
125
 
126
+ fn resolve_python_absolute_package(
127
+ from_path: &str,
128
+ specifier: &str,
129
+ repository_files: &BTreeSet<String>,
130
+ ) -> Option<String> {
131
+ if specifier.contains('/') {
132
+ return None;
133
+ }
134
+ let dotted_candidate = specifier.replace('.', "/");
135
+ let package_name = dotted_candidate.split('/').next()?;
136
+ python_package_roots(package_name, from_path, repository_files)
137
+ .into_iter()
138
+ .find_map(|root| {
139
+ resolve_python_package_candidate(&format!("{root}{dotted_candidate}"), repository_files)
140
+ })
141
+ }
142
+
143
+ fn resolve_python_package_candidate(
144
+ candidate: &str,
145
+ repository_files: &BTreeSet<String>,
146
+ ) -> Option<String> {
147
+ resolve_candidates(candidate, repository_files, Some("python")).or_else(|| {
148
+ let (module_candidate, _) = candidate.rsplit_once('/')?;
149
+ resolve_candidates(module_candidate, repository_files, Some("python"))
150
+ .filter(|path| !path.ends_with("/__init__.py"))
151
+ })
152
+ }
153
+
154
+ fn is_python_local_package_specifier(
155
+ from_path: &str,
156
+ specifier: &str,
157
+ repository_files: &BTreeSet<String>,
158
+ ) -> bool {
159
+ if specifier.contains('/') || !specifier.contains('.') {
160
+ return false;
161
+ }
162
+ let Some(package_name) = specifier.split('.').next() else {
163
+ return false;
164
+ };
165
+ !python_package_roots(package_name, from_path, repository_files).is_empty()
166
+ }
167
+
168
+ fn python_package_roots(
169
+ package_name: &str,
170
+ from_path: &str,
171
+ repository_files: &BTreeSet<String>,
172
+ ) -> Vec<String> {
173
+ let package_marker = format!("{package_name}/__init__.py");
174
+ let module_marker = format!("{package_name}.py");
175
+ let mut roots = repository_files
176
+ .iter()
177
+ .filter_map(|path| {
178
+ if path == &package_marker || path == &module_marker {
179
+ Some(String::new())
180
+ } else if let Some(prefix) = path.strip_suffix(&package_marker) {
181
+ Some(prefix.to_string())
182
+ } else {
183
+ path.strip_suffix(&module_marker).map(ToString::to_string)
184
+ }
185
+ })
186
+ .collect::<Vec<_>>();
187
+ roots.sort_by(|left, right| {
188
+ let left_local = from_path.starts_with(left.as_str());
189
+ let right_local = from_path.starts_with(right.as_str());
190
+ right_local
191
+ .cmp(&left_local)
192
+ .then_with(|| right.len().cmp(&left.len()))
193
+ .then_with(|| left.cmp(right))
194
+ });
195
+ roots.dedup();
196
+ roots
197
+ }
198
+
199
+ fn is_repo_absolute_specifier(specifier: &str) -> bool {
200
+ specifier.starts_with("@/")
201
+ || specifier.starts_with("src/")
202
+ || specifier.starts_with("packages/")
203
+ || specifier.starts_with("apps/")
204
+ }
205
+
105
206
  fn resolve_rust_local(
106
207
  from_path: &str,
107
208
  specifier: &str,
@@ -119,22 +220,27 @@ fn resolve_rust_local(
119
220
  "crate" => {
120
221
  parts.remove(0);
121
222
  let crate_root = rust_crate_src_root(from_path);
122
- resolve_progressively(
223
+ let allow_root_fallback = parts.len() == 1;
224
+ resolve_rust_path_candidate(
123
225
  &normalize(Path::new(&crate_root).join(parts.join("/"))),
124
226
  repository_files,
125
- Some("rust"),
126
- '/',
127
227
  )
228
+ .or_else(|| {
229
+ allow_root_fallback
230
+ .then(|| rust_crate_root_file(&crate_root, repository_files))
231
+ .flatten()
232
+ })
128
233
  }
129
234
  "self" => {
130
235
  parts.remove(0);
131
236
  let base = PathBuf::from(rust_module_directory(from_path));
132
- resolve_progressively(
133
- &normalize(base.join(parts.join("/"))),
134
- repository_files,
135
- Some("rust"),
136
- '/',
137
- )
237
+ let allow_module_fallback = parts.len() == 1;
238
+ resolve_rust_path_candidate(&normalize(base.join(parts.join("/"))), repository_files)
239
+ .or_else(|| {
240
+ allow_module_fallback
241
+ .then(|| rust_module_file_for_directory(&base, repository_files))
242
+ .flatten()
243
+ })
138
244
  }
139
245
  "super" => {
140
246
  let mut base = PathBuf::from(rust_module_directory(from_path));
@@ -142,17 +248,52 @@ fn resolve_rust_local(
142
248
  parts.remove(0);
143
249
  base.pop();
144
250
  }
145
- resolve_progressively(
146
- &normalize(base.join(parts.join("/"))),
147
- repository_files,
148
- Some("rust"),
149
- '/',
150
- )
251
+ let allow_module_fallback = parts.len() == 1;
252
+ resolve_rust_path_candidate(&normalize(base.join(parts.join("/"))), repository_files)
253
+ .or_else(|| {
254
+ allow_module_fallback
255
+ .then(|| rust_module_file_for_directory(&base, repository_files))
256
+ .flatten()
257
+ })
151
258
  }
152
259
  _ => None,
153
260
  }
154
261
  }
155
262
 
263
+ fn resolve_rust_path_candidate(
264
+ candidate: &str,
265
+ repository_files: &BTreeSet<String>,
266
+ ) -> Option<String> {
267
+ resolve_candidates(candidate, repository_files, Some("rust")).or_else(|| {
268
+ let (module_candidate, _) = candidate.rsplit_once('/')?;
269
+ resolve_candidates(module_candidate, repository_files, Some("rust"))
270
+ })
271
+ }
272
+
273
+ fn rust_crate_root_file(crate_root: &str, repository_files: &BTreeSet<String>) -> Option<String> {
274
+ ["lib.rs", "main.rs"]
275
+ .iter()
276
+ .map(|file_name| normalize(Path::new(crate_root).join(file_name)))
277
+ .find(|path| repository_files.contains(path))
278
+ }
279
+
280
+ fn rust_module_file_for_directory(
281
+ directory: &Path,
282
+ repository_files: &BTreeSet<String>,
283
+ ) -> Option<String> {
284
+ let normalized = normalize(directory);
285
+ let mut candidates = Vec::new();
286
+ if !normalized.is_empty() {
287
+ candidates.push(format!("{normalized}.rs"));
288
+ candidates.push(format!("{normalized}/mod.rs"));
289
+ candidates.push(format!("{normalized}/lib.rs"));
290
+ candidates.push(format!("{normalized}/main.rs"));
291
+ }
292
+ candidates
293
+ .into_iter()
294
+ .find(|path| repository_files.contains(path))
295
+ }
296
+
156
297
  fn rust_module_directory(from_path: &str) -> String {
157
298
  let source = Path::new(from_path);
158
299
  let parent = source.parent().unwrap_or_else(|| Path::new(""));
@@ -264,76 +405,6 @@ fn swift_sources_root(from_path: &str) -> Option<String> {
264
405
  Some(format!("{prefix}/Sources/"))
265
406
  }
266
407
 
267
- fn resolve_candidates(
268
- candidate: &str,
269
- repository_files: &BTreeSet<String>,
270
- language: Option<&str>,
271
- ) -> Option<String> {
272
- candidate_paths(candidate, language)
273
- .into_iter()
274
- .find(|path| repository_files.contains(path))
275
- }
276
-
277
- fn resolve_progressively(
278
- candidate: &str,
279
- repository_files: &BTreeSet<String>,
280
- language: Option<&str>,
281
- separator: char,
282
- ) -> Option<String> {
283
- let mut current = candidate.to_string();
284
- loop {
285
- if let Some(path) = resolve_candidates(&current, repository_files, language) {
286
- return Some(path);
287
- }
288
- let Some((parent, _)) = current.rsplit_once(separator) else {
289
- return None;
290
- };
291
- current = parent.to_string();
292
- }
293
- }
294
-
295
- fn candidate_paths(candidate: &str, language: Option<&str>) -> Vec<String> {
296
- let extensions = extensions_for_language(language);
297
- let mut candidates = extensions
298
- .iter()
299
- .map(|extension| format!("{candidate}{extension}"))
300
- .collect::<Vec<_>>();
301
- match language {
302
- Some("rust") => candidates.push(format!("{candidate}/mod.rs")),
303
- Some("python") => candidates.push(format!("{candidate}/__init__.py")),
304
- Some("go") => {}
305
- Some("typescript") | Some("javascript") => candidates.extend([
306
- format!("{candidate}/index.ts"),
307
- format!("{candidate}/index.tsx"),
308
- format!("{candidate}/index.js"),
309
- format!("{candidate}/index.jsx"),
310
- ]),
311
- _ => candidates.extend([
312
- format!("{candidate}/index.ts"),
313
- format!("{candidate}/index.tsx"),
314
- format!("{candidate}/index.js"),
315
- format!("{candidate}/index.jsx"),
316
- format!("{candidate}/mod.rs"),
317
- format!("{candidate}/__init__.py"),
318
- ]),
319
- }
320
- candidates
321
- }
322
-
323
- fn extensions_for_language(language: Option<&str>) -> &'static [&'static str] {
324
- match language {
325
- Some("rust") => &["", ".rs"],
326
- Some("python") => &["", ".py"],
327
- Some("go") => &["", ".go"],
328
- Some("swift") => &["", ".swift"],
329
- Some("typescript") => &["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
330
- Some("javascript") => &["", ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"],
331
- _ => &[
332
- "", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".rs", ".py", ".go", ".swift",
333
- ],
334
- }
335
- }
336
-
337
408
  fn is_rust_local(specifier: &str) -> bool {
338
409
  specifier.starts_with("crate::")
339
410
  || specifier.starts_with("self::")
@@ -19,6 +19,22 @@ mod imports;
19
19
  mod manifest;
20
20
  mod path_scan;
21
21
 
22
+ const IMPORT_RESOLVER_CONTEXT_FILENAMES: &[&str] = &[
23
+ "Cargo.toml",
24
+ "Cartfile",
25
+ "Package.swift",
26
+ "Podfile",
27
+ "build.gradle",
28
+ "build.gradle.kts",
29
+ "go.mod",
30
+ "jsconfig.json",
31
+ "package.json",
32
+ "pom.xml",
33
+ "project.pbxproj",
34
+ "pyproject.toml",
35
+ "tsconfig.json",
36
+ ];
37
+
22
38
  #[derive(Debug, Clone, Default)]
23
39
  pub struct ArchitectureScanOptions {
24
40
  pub config_path: Option<PathBuf>,
@@ -262,12 +278,10 @@ fn cached_clean_content_changed(
262
278
  }
263
279
 
264
280
  fn import_resolver_context_changed(changed_paths: &[String]) -> bool {
265
- changed_paths.iter().any(|path| {
266
- Path::new(path)
267
- .file_name()
268
- .and_then(|value| value.to_str())
269
- .is_some_and(|name| matches!(name, "go.mod" | "tsconfig.json" | "jsconfig.json"))
270
- })
281
+ changed_paths
282
+ .iter()
283
+ .filter_map(|path| Path::new(path).file_name()?.to_str())
284
+ .any(|name| IMPORT_RESOLVER_CONTEXT_FILENAMES.contains(&name))
271
285
  }
272
286
 
273
287
  fn degraded_full_scan(
@@ -1,4 +1,6 @@
1
1
  mod config;
2
+ mod config_findings;
3
+ mod explain;
2
4
  mod model;
3
5
  mod output;
4
6
  mod rules;
@@ -11,13 +13,15 @@ use crate::models::NaomeError;
11
13
  pub use config::{
12
14
  default_architecture_config_text, ArchitectureConfig, ContextConfig, LayerConfig, RuleConfig,
13
15
  };
16
+ pub use config_findings::config_findings_for;
17
+ pub use explain::format_architecture_explain;
14
18
  pub use model::{
15
19
  ArchitectureEdge, ArchitectureEdgeKind, ArchitectureGraph, ArchitectureMetadata,
16
20
  ArchitectureNode, ArchitectureNodeKind, Severity, SourceRange,
17
21
  };
18
22
  pub use output::{
19
- format_architecture_explain, format_architecture_scan, format_architecture_validation,
20
- ArchitectureAgentFeedback, ArchitectureValidation, ArchitectureViolation, ViolationSummary,
23
+ format_architecture_scan, format_architecture_validation, ArchitectureAgentFeedback,
24
+ ArchitectureConfigFinding, ArchitectureValidation, ArchitectureViolation, ViolationSummary,
21
25
  ARCHITECTURE_RULE_IDS,
22
26
  };
23
27
  pub use scan::{scan_architecture, ArchitectureScanOptions, ArchitectureScanReport};
@@ -21,13 +21,13 @@ mod verification_contract_policy;
21
21
  mod workflow;
22
22
 
23
23
  pub use architecture::{
24
- default_architecture_config_text, format_architecture_explain, format_architecture_scan,
25
- format_architecture_validation, scan_architecture, validate_architecture,
26
- ArchitectureAgentFeedback, ArchitectureConfig, ArchitectureEdge, ArchitectureEdgeKind,
27
- ArchitectureGraph, ArchitectureMetadata, ArchitectureNode, ArchitectureNodeKind,
28
- ArchitectureScanOptions, ArchitectureScanReport, ArchitectureValidation, ArchitectureViolation,
29
- ContextConfig, LayerConfig, RuleConfig, Severity, SourceRange, ViolationSummary,
30
- ARCHITECTURE_RULE_IDS,
24
+ config_findings_for, default_architecture_config_text, format_architecture_explain,
25
+ format_architecture_scan, format_architecture_validation, scan_architecture,
26
+ validate_architecture, ArchitectureAgentFeedback, ArchitectureConfig,
27
+ ArchitectureConfigFinding, ArchitectureEdge, ArchitectureEdgeKind, ArchitectureGraph,
28
+ ArchitectureMetadata, ArchitectureNode, ArchitectureNodeKind, ArchitectureScanOptions,
29
+ ArchitectureScanReport, ArchitectureValidation, ArchitectureViolation, ContextConfig,
30
+ LayerConfig, RuleConfig, Severity, SourceRange, ViolationSummary, ARCHITECTURE_RULE_IDS,
31
31
  };
32
32
  pub use context::{
33
33
  select_context_for_changed_paths, select_context_for_prompt, ContextBudgetLedger,
@@ -44,6 +44,7 @@ pub use install_plan::{MACHINE_OWNED_PATHS, PROJECT_OWNED_PATHS};
44
44
  pub use intent::{evaluate_intent, format_intent, IntentDecision, PromptEvidence};
45
45
  pub use journal::{append_task_journal, TaskJournalEntry};
46
46
  pub use models::{Decision, NaomeError};
47
+ pub use paths::{matches_any as path_matches_any, naomeignore_patterns};
47
48
  pub use quality::{
48
49
  check_repository_quality, check_repository_quality_paths, check_semantic_legacy,
49
50
  check_semantic_legacy_paths, clear_quality_cache, explain_repository_structure,
@@ -376,6 +376,36 @@ fn unresolved_relative_imports_are_represented_explicitly() {
376
376
  }));
377
377
  }
378
378
 
379
+ #[test]
380
+ fn rust_root_module_item_imports_resolve_to_crate_root_files() {
381
+ let repo = FixtureRepo::new();
382
+ repo.write(
383
+ "src/lib.rs",
384
+ "pub fn repository_model_drift() {}\npub fn matches_any() {}\n",
385
+ );
386
+ repo.write(
387
+ "src/quality/mod.rs",
388
+ "use crate::repository_model_drift;\npub fn check() { repository_model_drift(); }\n",
389
+ );
390
+ repo.write(
391
+ "src/paths.rs",
392
+ "use super::matches_any;\npub fn check() { matches_any(); }\n",
393
+ );
394
+
395
+ let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
396
+
397
+ assert!(scan.graph.edges.iter().any(|edge| {
398
+ edge.kind == ArchitectureEdgeKind::Imports
399
+ && edge.from == "file:src/quality/mod.rs"
400
+ && edge.to == "file:src/lib.rs"
401
+ }));
402
+ assert!(scan.graph.edges.iter().any(|edge| {
403
+ edge.kind == ArchitectureEdgeKind::Imports
404
+ && edge.from == "file:src/paths.rs"
405
+ && edge.to == "file:src/lib.rs"
406
+ }));
407
+ }
408
+
379
409
  #[test]
380
410
  fn validates_forbidden_layer_dependency_from_import_edges() {
381
411
  let repo = FixtureRepo::new();