@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
@@ -0,0 +1,316 @@
1
+ use std::collections::BTreeSet;
2
+ use std::fs;
3
+ use std::path::{Path, PathBuf};
4
+
5
+ use super::{external_target, language_for_path};
6
+ use crate::architecture::scan::ImportTarget;
7
+
8
+ pub(super) fn resolve_import(
9
+ root: &Path,
10
+ from_path: &str,
11
+ specifier: &str,
12
+ repository_files: &BTreeSet<String>,
13
+ ) -> ImportTarget {
14
+ let language = language_for_path(from_path);
15
+ if specifier.starts_with('.') {
16
+ if from_path.ends_with(".py") {
17
+ return resolve_python_relative(from_path, specifier, repository_files)
18
+ .map(ImportTarget::File)
19
+ .unwrap_or_else(|| ImportTarget::Unknown(specifier.to_string()));
20
+ }
21
+ return resolve_relative(from_path, specifier, repository_files, language)
22
+ .map(ImportTarget::File)
23
+ .unwrap_or_else(|| ImportTarget::Unknown(specifier.to_string()));
24
+ }
25
+ if from_path.ends_with(".rs") && is_rust_local(specifier) {
26
+ return resolve_rust_local(from_path, specifier, repository_files)
27
+ .map(ImportTarget::File)
28
+ .unwrap_or_else(|| ImportTarget::Unknown(specifier.to_string()));
29
+ }
30
+ if let Some(path) = resolve_repo_absolute(specifier, repository_files, language) {
31
+ return ImportTarget::File(path);
32
+ }
33
+ if from_path.ends_with(".go") {
34
+ if let Some(path) = resolve_go_module_path(root, from_path, specifier, repository_files) {
35
+ return ImportTarget::File(path);
36
+ }
37
+ }
38
+ external_target(specifier)
39
+ }
40
+
41
+ fn resolve_relative(
42
+ from_path: &str,
43
+ specifier: &str,
44
+ repository_files: &BTreeSet<String>,
45
+ language: Option<&str>,
46
+ ) -> Option<String> {
47
+ let base = Path::new(from_path)
48
+ .parent()
49
+ .unwrap_or_else(|| Path::new(""));
50
+ let candidate = normalize(base.join(specifier));
51
+ resolve_candidates(&candidate, repository_files, language)
52
+ }
53
+
54
+ fn resolve_python_relative(
55
+ from_path: &str,
56
+ specifier: &str,
57
+ repository_files: &BTreeSet<String>,
58
+ ) -> Option<String> {
59
+ let dots = specifier
60
+ .chars()
61
+ .take_while(|character| *character == '.')
62
+ .count();
63
+ let module = specifier[dots..].replace('.', "/");
64
+ let mut base = Path::new(from_path)
65
+ .parent()
66
+ .unwrap_or_else(|| Path::new(""));
67
+ for _ in 1..dots {
68
+ base = base.parent().unwrap_or_else(|| Path::new(""));
69
+ }
70
+ resolve_progressively(
71
+ &normalize(base.join(module)),
72
+ repository_files,
73
+ Some("python"),
74
+ '/',
75
+ )
76
+ }
77
+
78
+ fn resolve_repo_absolute(
79
+ specifier: &str,
80
+ repository_files: &BTreeSet<String>,
81
+ language: Option<&str>,
82
+ ) -> Option<String> {
83
+ let candidate = specifier.strip_prefix("@/").unwrap_or(specifier);
84
+ if candidate.starts_with("src/")
85
+ || candidate.starts_with("packages/")
86
+ || candidate.starts_with("apps/")
87
+ {
88
+ return resolve_candidates(candidate, repository_files, language);
89
+ }
90
+ let dotted_candidate = specifier.replace('.', "/");
91
+ if dotted_candidate.starts_with("src/")
92
+ || dotted_candidate.starts_with("packages/")
93
+ || dotted_candidate.starts_with("apps/")
94
+ {
95
+ return resolve_progressively(&dotted_candidate, repository_files, language, '/');
96
+ }
97
+ None
98
+ }
99
+
100
+ fn resolve_rust_local(
101
+ from_path: &str,
102
+ specifier: &str,
103
+ repository_files: &BTreeSet<String>,
104
+ ) -> Option<String> {
105
+ let mut parts = specifier
106
+ .split("::")
107
+ .map(str::trim)
108
+ .filter(|part| !part.is_empty())
109
+ .collect::<Vec<_>>();
110
+ if parts.is_empty() {
111
+ return None;
112
+ }
113
+ match parts[0] {
114
+ "crate" => {
115
+ parts.remove(0);
116
+ let crate_root = rust_crate_src_root(from_path);
117
+ resolve_progressively(
118
+ &normalize(Path::new(&crate_root).join(parts.join("/"))),
119
+ repository_files,
120
+ Some("rust"),
121
+ '/',
122
+ )
123
+ }
124
+ "self" => {
125
+ parts.remove(0);
126
+ let base = PathBuf::from(rust_module_directory(from_path));
127
+ resolve_progressively(
128
+ &normalize(base.join(parts.join("/"))),
129
+ repository_files,
130
+ Some("rust"),
131
+ '/',
132
+ )
133
+ }
134
+ "super" => {
135
+ let mut base = PathBuf::from(rust_module_directory(from_path));
136
+ while parts.first() == Some(&"super") {
137
+ parts.remove(0);
138
+ base.pop();
139
+ }
140
+ resolve_progressively(
141
+ &normalize(base.join(parts.join("/"))),
142
+ repository_files,
143
+ Some("rust"),
144
+ '/',
145
+ )
146
+ }
147
+ _ => None,
148
+ }
149
+ }
150
+
151
+ fn rust_module_directory(from_path: &str) -> String {
152
+ let source = Path::new(from_path);
153
+ let parent = source.parent().unwrap_or_else(|| Path::new(""));
154
+ let stem = source
155
+ .file_stem()
156
+ .and_then(|value| value.to_str())
157
+ .unwrap_or("");
158
+ if matches!(stem, "lib" | "main" | "mod") {
159
+ normalize(parent)
160
+ } else {
161
+ normalize(parent.join(stem))
162
+ }
163
+ }
164
+
165
+ fn rust_crate_src_root(from_path: &str) -> String {
166
+ let parts = from_path.split('/').collect::<Vec<_>>();
167
+ if let Some(index) = parts.iter().rposition(|part| *part == "src") {
168
+ return parts[..=index].join("/");
169
+ }
170
+ "src".to_string()
171
+ }
172
+
173
+ fn resolve_go_module_path(
174
+ root: &Path,
175
+ from_path: &str,
176
+ specifier: &str,
177
+ repository_files: &BTreeSet<String>,
178
+ ) -> Option<String> {
179
+ let module = go_module_for_file(root, from_path, repository_files)?;
180
+ let suffix = specifier.strip_prefix(&module)?;
181
+ let suffix = suffix.strip_prefix('/')?;
182
+ if suffix.is_empty() {
183
+ return None;
184
+ }
185
+ if let Some(path) = resolve_candidates(suffix, repository_files, Some("go")) {
186
+ return Some(path);
187
+ }
188
+ let package_prefix = format!("{suffix}/");
189
+ repository_files
190
+ .iter()
191
+ .find(|path| path.starts_with(&package_prefix) && path.ends_with(".go"))
192
+ .cloned()
193
+ }
194
+
195
+ fn go_module_for_file(
196
+ root: &Path,
197
+ from_path: &str,
198
+ repository_files: &BTreeSet<String>,
199
+ ) -> Option<String> {
200
+ let mut current = Path::new(from_path)
201
+ .parent()
202
+ .unwrap_or_else(|| Path::new(""));
203
+ loop {
204
+ let go_mod_path = normalize(current.join("go.mod"));
205
+ if repository_files.contains(&go_mod_path) {
206
+ return go_mod_module(root, &go_mod_path);
207
+ }
208
+ let Some(parent) = current.parent() else {
209
+ break;
210
+ };
211
+ if parent == current {
212
+ break;
213
+ }
214
+ current = parent;
215
+ }
216
+ None
217
+ }
218
+
219
+ fn go_mod_module(root: &Path, go_mod_path: &str) -> Option<String> {
220
+ let content = fs::read_to_string(root.join(go_mod_path)).ok()?;
221
+ content.lines().find_map(|line| {
222
+ let line = line.trim();
223
+ let module = line.strip_prefix("module ")?;
224
+ module.split_whitespace().next().map(ToString::to_string)
225
+ })
226
+ }
227
+
228
+ fn resolve_candidates(
229
+ candidate: &str,
230
+ repository_files: &BTreeSet<String>,
231
+ language: Option<&str>,
232
+ ) -> Option<String> {
233
+ candidate_paths(candidate, language)
234
+ .into_iter()
235
+ .find(|path| repository_files.contains(path))
236
+ }
237
+
238
+ fn resolve_progressively(
239
+ candidate: &str,
240
+ repository_files: &BTreeSet<String>,
241
+ language: Option<&str>,
242
+ separator: char,
243
+ ) -> Option<String> {
244
+ let mut current = candidate.to_string();
245
+ loop {
246
+ if let Some(path) = resolve_candidates(&current, repository_files, language) {
247
+ return Some(path);
248
+ }
249
+ let Some((parent, _)) = current.rsplit_once(separator) else {
250
+ return None;
251
+ };
252
+ current = parent.to_string();
253
+ }
254
+ }
255
+
256
+ fn candidate_paths(candidate: &str, language: Option<&str>) -> Vec<String> {
257
+ let extensions = extensions_for_language(language);
258
+ let mut candidates = extensions
259
+ .iter()
260
+ .map(|extension| format!("{candidate}{extension}"))
261
+ .collect::<Vec<_>>();
262
+ match language {
263
+ Some("rust") => candidates.push(format!("{candidate}/mod.rs")),
264
+ Some("python") => candidates.push(format!("{candidate}/__init__.py")),
265
+ Some("go") => {}
266
+ Some("typescript") | Some("javascript") => candidates.extend([
267
+ format!("{candidate}/index.ts"),
268
+ format!("{candidate}/index.tsx"),
269
+ format!("{candidate}/index.js"),
270
+ format!("{candidate}/index.jsx"),
271
+ ]),
272
+ _ => candidates.extend([
273
+ format!("{candidate}/index.ts"),
274
+ format!("{candidate}/index.tsx"),
275
+ format!("{candidate}/index.js"),
276
+ format!("{candidate}/index.jsx"),
277
+ format!("{candidate}/mod.rs"),
278
+ format!("{candidate}/__init__.py"),
279
+ ]),
280
+ }
281
+ candidates
282
+ }
283
+
284
+ fn extensions_for_language(language: Option<&str>) -> &'static [&'static str] {
285
+ match language {
286
+ Some("rust") => &["", ".rs"],
287
+ Some("python") => &["", ".py"],
288
+ Some("go") => &["", ".go"],
289
+ Some("typescript") => &["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
290
+ Some("javascript") => &["", ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"],
291
+ _ => &[
292
+ "", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".rs", ".py", ".go",
293
+ ],
294
+ }
295
+ }
296
+
297
+ fn is_rust_local(specifier: &str) -> bool {
298
+ specifier.starts_with("crate::")
299
+ || specifier.starts_with("self::")
300
+ || specifier.starts_with("super::")
301
+ || specifier.starts_with('.')
302
+ }
303
+
304
+ fn normalize(path: impl AsRef<Path>) -> String {
305
+ let mut normalized = PathBuf::new();
306
+ for component in path.as_ref().components() {
307
+ match component {
308
+ std::path::Component::ParentDir => {
309
+ normalized.pop();
310
+ }
311
+ std::path::Component::CurDir => {}
312
+ other => normalized.push(other.as_os_str()),
313
+ }
314
+ }
315
+ normalized.to_string_lossy().replace('\\', "/")
316
+ }
@@ -0,0 +1,75 @@
1
+ use std::collections::BTreeSet;
2
+ use std::path::Path;
3
+
4
+ use super::{ImportFact, ImportTarget};
5
+
6
+ mod extractors;
7
+ mod resolver;
8
+
9
+ pub(super) fn extract_imports(
10
+ root: &Path,
11
+ path: &str,
12
+ content: &str,
13
+ repository_files: &BTreeSet<String>,
14
+ ) -> Vec<ImportFact> {
15
+ let raw_imports = extractors::extract_raw_imports(path, content);
16
+ raw_imports
17
+ .into_iter()
18
+ .map(|raw| ImportFact {
19
+ target: resolver::resolve_import(root, path, &raw.specifier, repository_files),
20
+ specifier: raw.specifier,
21
+ source_range: Some(raw.source_range),
22
+ confidence: raw.confidence,
23
+ extractor: raw.extractor,
24
+ })
25
+ .collect()
26
+ }
27
+
28
+ pub(super) fn package_name(specifier: &str) -> String {
29
+ let specifier = specifier
30
+ .split_once(" as ")
31
+ .map(|(package, _alias)| package)
32
+ .unwrap_or(specifier)
33
+ .trim();
34
+ if let Some(rest) = specifier.strip_prefix('@') {
35
+ let mut parts = rest.split('/');
36
+ let scope = parts.next().unwrap_or_default();
37
+ let package = parts.next().unwrap_or_default();
38
+ return format!("@{scope}/{package}");
39
+ }
40
+ if specifier
41
+ .split('/')
42
+ .next()
43
+ .is_some_and(|prefix| prefix.contains('.'))
44
+ && specifier.contains('/')
45
+ {
46
+ return specifier.to_string();
47
+ }
48
+ specifier
49
+ .split("::")
50
+ .next()
51
+ .unwrap_or(specifier)
52
+ .split('/')
53
+ .next()
54
+ .unwrap_or(specifier)
55
+ .split('.')
56
+ .next()
57
+ .unwrap_or(specifier)
58
+ .to_string()
59
+ }
60
+
61
+ pub(super) fn external_target(specifier: &str) -> ImportTarget {
62
+ ImportTarget::ExternalDependency(package_name(specifier))
63
+ }
64
+
65
+ pub(super) fn language_for_path(path: &str) -> Option<&'static str> {
66
+ let extension = path.rsplit('.').next()?;
67
+ match extension {
68
+ "ts" | "tsx" => Some("typescript"),
69
+ "js" | "jsx" | "mjs" | "cjs" => Some("javascript"),
70
+ "rs" => Some("rust"),
71
+ "py" => Some("python"),
72
+ "go" => Some("go"),
73
+ _ => None,
74
+ }
75
+ }
@@ -10,6 +10,7 @@ use super::config::{read_architecture_config, ArchitectureConfig};
10
10
  use super::model::ArchitectureGraph;
11
11
 
12
12
  mod graph_builder;
13
+ mod imports;
13
14
  mod path_scan;
14
15
 
15
16
  #[derive(Debug, Clone, Default)]
@@ -42,6 +43,25 @@ pub struct FileFact {
42
43
  pub layers: Vec<String>,
43
44
  pub contexts: Vec<String>,
44
45
  pub ignored: Option<String>,
46
+ pub imports: Vec<ImportFact>,
47
+ }
48
+
49
+ #[derive(Debug, Clone, Serialize)]
50
+ #[serde(rename_all = "camelCase")]
51
+ pub struct ImportFact {
52
+ pub specifier: String,
53
+ pub target: ImportTarget,
54
+ pub source_range: Option<super::model::SourceRange>,
55
+ pub confidence: f32,
56
+ pub extractor: String,
57
+ }
58
+
59
+ #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
60
+ #[serde(rename_all = "camelCase")]
61
+ pub enum ImportTarget {
62
+ File(String),
63
+ ExternalDependency(String),
64
+ Unknown(String),
45
65
  }
46
66
 
47
67
  pub fn scan_architecture(
@@ -18,7 +18,7 @@ pub use model::{
18
18
  pub use output::{
19
19
  format_architecture_explain, format_architecture_scan, format_architecture_validation,
20
20
  ArchitectureAgentFeedback, ArchitectureValidation, ArchitectureViolation, Severity,
21
- ViolationSummary,
21
+ ViolationSummary, ARCHITECTURE_RULE_IDS,
22
22
  };
23
23
  pub use scan::{scan_architecture, ArchitectureScanOptions, ArchitectureScanReport};
24
24
 
@@ -27,6 +27,7 @@ pub use architecture::{
27
27
  ArchitectureGraph, ArchitectureMetadata, ArchitectureNode, ArchitectureNodeKind,
28
28
  ArchitectureScanOptions, ArchitectureScanReport, ArchitectureValidation, ArchitectureViolation,
29
29
  ContextConfig, LayerConfig, RuleConfig, Severity, SourceRange, ViolationSummary,
30
+ ARCHITECTURE_RULE_IDS,
30
31
  };
31
32
  pub use context::{
32
33
  select_context_for_changed_paths, select_context_for_prompt, ContextBudgetLedger,