@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
@@ -0,0 +1,185 @@
1
+ use crate::architecture::output::ArchitectureViolation;
2
+ use crate::architecture::scan::{ArchitectureScanReport, ImportTarget};
3
+ use crate::paths;
4
+
5
+ pub(super) fn validate_external_dependency_policy(
6
+ scan: &ArchitectureScanReport,
7
+ violations: &mut Vec<ArchitectureViolation>,
8
+ rules_executed: &mut Vec<String>,
9
+ ) {
10
+ let rule = scan.config.rule("external_dependency_policy");
11
+ if !rule.enabled {
12
+ return;
13
+ }
14
+ rules_executed.push("arch.external_dependency_policy".to_string());
15
+ for fact in scan.file_facts.values() {
16
+ let owners = fact
17
+ .layers
18
+ .iter()
19
+ .chain(fact.contexts.iter())
20
+ .collect::<Vec<_>>();
21
+ for import in &fact.imports {
22
+ let ImportTarget::ExternalDependency(package) = &import.target else {
23
+ continue;
24
+ };
25
+ if is_standard_library_dependency(fact.language.as_deref(), package) {
26
+ continue;
27
+ }
28
+ for owner in &owners {
29
+ let Some(policy) = scan.config.external_dependencies.get(*owner) else {
30
+ continue;
31
+ };
32
+ if paths::matches_any(package, &policy.allow) {
33
+ continue;
34
+ }
35
+ violations.push(ArchitectureViolation {
36
+ id: "arch.external_dependency_policy".to_string(),
37
+ severity: rule.severity,
38
+ violation_type: "external_dependency_policy".to_string(),
39
+ message: format!(
40
+ "{} in {} imports external dependency {} without policy allowance.",
41
+ fact.path, owner, package
42
+ ),
43
+ from: Some(format!("file:{}", fact.path)),
44
+ to: Some(format!("external:{package}")),
45
+ path: Some(fact.path.clone()),
46
+ source_range: import.source_range.clone(),
47
+ suggestion: format!(
48
+ "Move {} behind an allowed adapter or add it to external_dependencies.{}.allow with intent.",
49
+ package, owner
50
+ ),
51
+ agent_instruction: format!(
52
+ "Do not import external dependency {} from {}; use an allowed boundary.",
53
+ package, owner
54
+ ),
55
+ });
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ fn is_standard_library_dependency(language: Option<&str>, package: &str) -> bool {
62
+ let root = package
63
+ .strip_prefix("node:")
64
+ .unwrap_or(package)
65
+ .split(['/', '.', ':'])
66
+ .next()
67
+ .unwrap_or(package);
68
+ match language {
69
+ Some("go") => !package
70
+ .split('/')
71
+ .next()
72
+ .is_some_and(|prefix| prefix.contains('.')),
73
+ Some("javascript") | Some("typescript") => NODE_BUILTINS.contains(&root),
74
+ Some("python") => PYTHON_STDLIB.contains(&root),
75
+ Some("rust") => matches!(root, "std" | "core" | "alloc" | "proc_macro" | "test"),
76
+ _ => false,
77
+ }
78
+ }
79
+
80
+ const NODE_BUILTINS: &[&str] = &[
81
+ "assert",
82
+ "async_hooks",
83
+ "buffer",
84
+ "child_process",
85
+ "cluster",
86
+ "console",
87
+ "constants",
88
+ "crypto",
89
+ "diagnostics_channel",
90
+ "dns",
91
+ "domain",
92
+ "events",
93
+ "fs",
94
+ "http",
95
+ "http2",
96
+ "https",
97
+ "inspector",
98
+ "module",
99
+ "net",
100
+ "os",
101
+ "path",
102
+ "perf_hooks",
103
+ "process",
104
+ "punycode",
105
+ "querystring",
106
+ "readline",
107
+ "repl",
108
+ "stream",
109
+ "string_decoder",
110
+ "timers",
111
+ "tls",
112
+ "trace_events",
113
+ "tty",
114
+ "url",
115
+ "util",
116
+ "v8",
117
+ "vm",
118
+ "wasi",
119
+ "worker_threads",
120
+ "zlib",
121
+ ];
122
+
123
+ const PYTHON_STDLIB: &[&str] = &[
124
+ "abc",
125
+ "argparse",
126
+ "array",
127
+ "ast",
128
+ "asyncio",
129
+ "base64",
130
+ "bisect",
131
+ "calendar",
132
+ "collections",
133
+ "concurrent",
134
+ "contextlib",
135
+ "copy",
136
+ "csv",
137
+ "dataclasses",
138
+ "datetime",
139
+ "decimal",
140
+ "email",
141
+ "enum",
142
+ "functools",
143
+ "glob",
144
+ "gzip",
145
+ "hashlib",
146
+ "heapq",
147
+ "hmac",
148
+ "html",
149
+ "http",
150
+ "importlib",
151
+ "inspect",
152
+ "io",
153
+ "itertools",
154
+ "json",
155
+ "logging",
156
+ "math",
157
+ "multiprocessing",
158
+ "operator",
159
+ "os",
160
+ "pathlib",
161
+ "pickle",
162
+ "platform",
163
+ "queue",
164
+ "random",
165
+ "re",
166
+ "shlex",
167
+ "shutil",
168
+ "signal",
169
+ "socket",
170
+ "sqlite3",
171
+ "statistics",
172
+ "string",
173
+ "subprocess",
174
+ "sys",
175
+ "tempfile",
176
+ "threading",
177
+ "time",
178
+ "traceback",
179
+ "typing",
180
+ "unittest",
181
+ "urllib",
182
+ "uuid",
183
+ "xml",
184
+ "zipfile",
185
+ ];
@@ -0,0 +1,177 @@
1
+ use std::collections::{BTreeMap, BTreeSet};
2
+
3
+ use crate::architecture::model::ArchitectureEdgeKind;
4
+ use crate::architecture::scan::ArchitectureScanReport;
5
+
6
+ #[derive(Debug, Clone)]
7
+ pub(super) struct ImportEdge<'a> {
8
+ pub(super) from_path: &'a str,
9
+ pub(super) to_path: &'a str,
10
+ pub(super) edge_index: usize,
11
+ }
12
+
13
+ pub(super) fn file_import_edges(scan: &ArchitectureScanReport) -> Vec<ImportEdge<'_>> {
14
+ scan.graph
15
+ .edges
16
+ .iter()
17
+ .enumerate()
18
+ .filter_map(|(edge_index, edge)| {
19
+ (edge.kind == ArchitectureEdgeKind::Imports)
20
+ .then(|| {
21
+ Some(ImportEdge {
22
+ from_path: edge.from.strip_prefix("file:")?,
23
+ to_path: edge.to.strip_prefix("file:")?,
24
+ edge_index,
25
+ })
26
+ })
27
+ .flatten()
28
+ })
29
+ .collect()
30
+ }
31
+
32
+ pub(super) fn file_import_adjacency(
33
+ scan: &ArchitectureScanReport,
34
+ ) -> BTreeMap<String, Vec<String>> {
35
+ file_adjacency(file_import_edges(scan).into_iter())
36
+ }
37
+
38
+ pub(super) fn file_cycle_adjacency(scan: &ArchitectureScanReport) -> BTreeMap<String, Vec<String>> {
39
+ file_adjacency(
40
+ file_import_edges(scan)
41
+ .into_iter()
42
+ .filter(|edge| is_cycle_dependency(scan, edge)),
43
+ )
44
+ }
45
+
46
+ fn file_adjacency<'a>(
47
+ edges: impl Iterator<Item = ImportEdge<'a>>,
48
+ ) -> BTreeMap<String, Vec<String>> {
49
+ let mut adjacency = BTreeMap::<String, BTreeSet<String>>::new();
50
+ for edge in edges {
51
+ adjacency
52
+ .entry(edge.from_path.to_string())
53
+ .or_default()
54
+ .insert(edge.to_path.to_string());
55
+ }
56
+ adjacency
57
+ .into_iter()
58
+ .map(|(from, targets)| (from, targets.into_iter().collect()))
59
+ .collect()
60
+ }
61
+
62
+ fn is_cycle_dependency(scan: &ArchitectureScanReport, edge: &ImportEdge<'_>) -> bool {
63
+ let graph_edge = &scan.graph.edges[edge.edge_index];
64
+ if graph_edge.metadata.language.as_deref() != Some("rust") {
65
+ return true;
66
+ }
67
+ let specifier = graph_edge
68
+ .metadata
69
+ .raw_origin
70
+ .get("specifier")
71
+ .and_then(serde_json::Value::as_str)
72
+ .unwrap_or_default();
73
+ !is_rust_module_cycle_exempt(edge.from_path, edge.to_path, specifier)
74
+ }
75
+
76
+ fn is_rust_module_cycle_exempt(from_path: &str, to_path: &str, specifier: &str) -> bool {
77
+ if !is_rust_module_family_reference(specifier) {
78
+ return false;
79
+ }
80
+ is_rust_parent_child_module_family(from_path, to_path)
81
+ }
82
+
83
+ fn is_rust_module_family_reference(specifier: &str) -> bool {
84
+ specifier.starts_with("self::")
85
+ || specifier.starts_with("super::")
86
+ || specifier.starts_with("crate::")
87
+ }
88
+
89
+ fn is_rust_parent_child_module_family(from_path: &str, to_path: &str) -> bool {
90
+ let Some(from_module) = rust_module_id(from_path) else {
91
+ return false;
92
+ };
93
+ let Some(to_module) = rust_module_id(to_path) else {
94
+ return false;
95
+ };
96
+ if from_module == to_module {
97
+ return true;
98
+ }
99
+ from_module
100
+ .strip_prefix(&to_module)
101
+ .is_some_and(|suffix| suffix.starts_with('/'))
102
+ || to_module
103
+ .strip_prefix(&from_module)
104
+ .is_some_and(|suffix| suffix.starts_with('/'))
105
+ }
106
+
107
+ fn rust_module_id(path: &str) -> Option<String> {
108
+ path.strip_suffix("/mod.rs")
109
+ .map(ToString::to_string)
110
+ .or_else(|| path.strip_suffix(".rs").map(ToString::to_string))
111
+ }
112
+
113
+ pub(super) fn strongly_connected_components(
114
+ adjacency: &BTreeMap<String, Vec<String>>,
115
+ ) -> Vec<Vec<String>> {
116
+ let mut state = TarjanState::default();
117
+ for node in adjacency.keys() {
118
+ if !state.indices.contains_key(node) {
119
+ strong_connect(node, adjacency, &mut state);
120
+ }
121
+ }
122
+ state
123
+ .components
124
+ .into_iter()
125
+ .filter(|component| {
126
+ component.len() > 1
127
+ || component.first().is_some_and(|node| {
128
+ adjacency
129
+ .get(node)
130
+ .is_some_and(|targets| targets.contains(node))
131
+ })
132
+ })
133
+ .collect()
134
+ }
135
+
136
+ #[derive(Default)]
137
+ struct TarjanState {
138
+ next_index: usize,
139
+ stack: Vec<String>,
140
+ on_stack: BTreeSet<String>,
141
+ indices: BTreeMap<String, usize>,
142
+ lowlinks: BTreeMap<String, usize>,
143
+ components: Vec<Vec<String>>,
144
+ }
145
+
146
+ fn strong_connect(node: &str, adjacency: &BTreeMap<String, Vec<String>>, state: &mut TarjanState) {
147
+ let index = state.next_index;
148
+ state.next_index += 1;
149
+ state.indices.insert(node.to_string(), index);
150
+ state.lowlinks.insert(node.to_string(), index);
151
+ state.stack.push(node.to_string());
152
+ state.on_stack.insert(node.to_string());
153
+
154
+ for target in adjacency.get(node).into_iter().flatten() {
155
+ if !state.indices.contains_key(target) {
156
+ strong_connect(target, adjacency, state);
157
+ let lowlink = state.lowlinks[node].min(state.lowlinks[target]);
158
+ state.lowlinks.insert(node.to_string(), lowlink);
159
+ } else if state.on_stack.contains(target) {
160
+ let lowlink = state.lowlinks[node].min(state.indices[target]);
161
+ state.lowlinks.insert(node.to_string(), lowlink);
162
+ }
163
+ }
164
+
165
+ if state.lowlinks[node] == state.indices[node] {
166
+ let mut component = Vec::new();
167
+ while let Some(member) = state.stack.pop() {
168
+ state.on_stack.remove(&member);
169
+ component.push(member.clone());
170
+ if member == node {
171
+ break;
172
+ }
173
+ }
174
+ component.sort();
175
+ state.components.push(component);
176
+ }
177
+ }
@@ -0,0 +1,89 @@
1
+ use std::collections::{BTreeMap, BTreeSet, VecDeque};
2
+
3
+ use crate::architecture::output::ArchitectureViolation;
4
+ use crate::architecture::scan::ArchitectureScanReport;
5
+
6
+ use super::graph;
7
+
8
+ pub(super) fn validate_transitive_layer_dependencies(
9
+ scan: &ArchitectureScanReport,
10
+ violations: &mut Vec<ArchitectureViolation>,
11
+ rules_executed: &mut Vec<String>,
12
+ ) {
13
+ let rule = scan
14
+ .config
15
+ .rule("no_transitive_forbidden_layer_dependencies");
16
+ if !rule.enabled {
17
+ return;
18
+ }
19
+ rules_executed.push("arch.no_transitive_forbidden_layer_dependencies".to_string());
20
+ let adjacency = graph::file_import_adjacency(scan);
21
+ for (from_path, from_fact) in &scan.file_facts {
22
+ for from_layer in &from_fact.layers {
23
+ let Some((target_path, target_layers)) =
24
+ first_forbidden_target(scan, &adjacency, from_path, from_layer)
25
+ else {
26
+ continue;
27
+ };
28
+ violations.push(ArchitectureViolation {
29
+ id: "arch.no_transitive_forbidden_layer_dependencies".to_string(),
30
+ severity: rule.severity,
31
+ violation_type: "transitive_forbidden_dependency".to_string(),
32
+ message: format!(
33
+ "{from_path} in layer {from_layer} transitively reaches {target_path} in forbidden layer {target_layers}."
34
+ ),
35
+ from: Some(format!("file:{from_path}")),
36
+ to: Some(format!("file:{target_path}")),
37
+ path: Some(from_path.clone()),
38
+ source_range: None,
39
+ suggestion: "Remove the indirect dependency chain or introduce an allowed boundary."
40
+ .to_string(),
41
+ agent_instruction:
42
+ "Do not reach forbidden layers indirectly; move the dependency behind an allowed interface."
43
+ .to_string(),
44
+ });
45
+ }
46
+ }
47
+ }
48
+
49
+ fn first_forbidden_target(
50
+ scan: &ArchitectureScanReport,
51
+ adjacency: &BTreeMap<String, Vec<String>>,
52
+ from_path: &str,
53
+ from_layer: &str,
54
+ ) -> Option<(String, String)> {
55
+ let mut queue = VecDeque::new();
56
+ let mut visited = BTreeSet::new();
57
+ for target in adjacency.get(from_path).into_iter().flatten() {
58
+ queue.push_back((target.clone(), 1usize));
59
+ }
60
+ while let Some((path, depth)) = queue.pop_front() {
61
+ if !visited.insert(path.clone()) {
62
+ continue;
63
+ }
64
+ if depth > 1 {
65
+ let Some(fact) = scan.file_facts.get(&path) else {
66
+ continue;
67
+ };
68
+ if !fact.layers.is_empty() && !layer_allowed(scan, from_layer, &fact.layers) {
69
+ return Some((path, fact.layers.join(", ")));
70
+ }
71
+ }
72
+ for target in adjacency.get(&path).into_iter().flatten() {
73
+ queue.push_back((target.clone(), depth + 1));
74
+ }
75
+ }
76
+ None
77
+ }
78
+
79
+ fn layer_allowed(scan: &ArchitectureScanReport, from_layer: &str, targets: &[String]) -> bool {
80
+ let allowed = scan
81
+ .config
82
+ .allowed_dependencies
83
+ .get(from_layer)
84
+ .map(Vec::as_slice)
85
+ .unwrap_or(&[]);
86
+ targets
87
+ .iter()
88
+ .any(|to_layer| from_layer == to_layer || allowed.contains(to_layer))
89
+ }
@@ -7,13 +7,25 @@ use super::output::{
7
7
  use super::scan::ArchitectureScanReport;
8
8
  use crate::architecture::model::ArchitectureEdgeKind;
9
9
 
10
+ mod budgets;
11
+ mod context;
12
+ mod cycles;
13
+ mod external;
14
+ mod graph;
15
+ mod transitive;
16
+
10
17
  pub fn validate_scan(scan: ArchitectureScanReport) -> ArchitectureValidation {
11
18
  let mut violations = Vec::new();
12
19
  let mut rules_executed = Vec::new();
13
20
 
14
- validate_max_file_lines(&scan, &mut violations, &mut rules_executed);
21
+ budgets::validate_file_size_budget(&scan, &mut violations, &mut rules_executed);
15
22
  validate_generated_manual_boundary(&scan, &mut violations, &mut rules_executed);
16
23
  validate_forbidden_layer_dependencies(&scan, &mut violations, &mut rules_executed);
24
+ context::validate_context_rules(&scan, &mut violations, &mut rules_executed);
25
+ cycles::validate_cycles(&scan, &mut violations, &mut rules_executed);
26
+ transitive::validate_transitive_layer_dependencies(&scan, &mut violations, &mut rules_executed);
27
+ budgets::validate_dependency_budgets(&scan, &mut violations, &mut rules_executed);
28
+ external::validate_external_dependency_policy(&scan, &mut violations, &mut rules_executed);
17
29
 
18
30
  violations.sort_by(|left, right| {
19
31
  (
@@ -118,44 +130,6 @@ fn validate_forbidden_layer_dependencies(
118
130
  }
119
131
  }
120
132
 
121
- fn validate_max_file_lines(
122
- scan: &ArchitectureScanReport,
123
- violations: &mut Vec<ArchitectureViolation>,
124
- rules_executed: &mut Vec<String>,
125
- ) {
126
- let rule = scan.config.rule("max_file_lines");
127
- if !rule.enabled {
128
- return;
129
- }
130
- rules_executed.push("arch.max_file_lines".to_string());
131
- let Some(limit) = rule.value else {
132
- return;
133
- };
134
- for fact in scan.file_facts.values() {
135
- if fact.line_count <= limit {
136
- continue;
137
- }
138
- violations.push(ArchitectureViolation {
139
- id: "arch.max_file_lines".to_string(),
140
- severity: rule.severity,
141
- violation_type: "file_size_budget".to_string(),
142
- message: format!(
143
- "{} has {} lines, exceeding the configured budget of {}.",
144
- fact.path, fact.line_count, limit
145
- ),
146
- from: Some(format!("file:{}", fact.path)),
147
- to: None,
148
- path: Some(fact.path.clone()),
149
- source_range: None,
150
- suggestion: "Split the file into cohesive modules or move generated content behind an explicit ignore rule.".to_string(),
151
- agent_instruction: format!(
152
- "Reduce {} below {} lines or add a justified generated-code ignore rule if it is not manually owned.",
153
- fact.path, limit
154
- ),
155
- });
156
- }
157
- }
158
-
159
133
  fn validate_generated_manual_boundary(
160
134
  scan: &ArchitectureScanReport,
161
135
  violations: &mut Vec<ArchitectureViolation>,
@@ -69,12 +69,6 @@ fn extract_rust(content: &str) -> Vec<RawImport> {
69
69
  .strip_prefix("use ")
70
70
  .or_else(|| rust_visible_use_value(trimmed))
71
71
  .map(rust_use_specifiers)
72
- .or_else(|| {
73
- trimmed
74
- .strip_prefix("mod ")
75
- .or_else(|| trimmed.strip_prefix("pub mod "))
76
- .map(|value| vec![format!("rustmod:{}", value.trim_end_matches(';').trim())])
77
- })
78
72
  .or_else(|| {
79
73
  trimmed
80
74
  .strip_prefix("extern crate ")
@@ -198,7 +192,10 @@ fn starts_javascript_pending_import(line: &str) -> bool {
198
192
  }
199
193
 
200
194
  fn is_comment_only(line: &str) -> bool {
201
- line.starts_with("//") || line.starts_with("/*") || line.starts_with('*') || line.starts_with('#')
195
+ line.starts_with("//")
196
+ || line.starts_with("/*")
197
+ || line.starts_with('*')
198
+ || line.starts_with('#')
202
199
  }
203
200
 
204
201
  fn rust_visible_use_value(line: &str) -> Option<&str> {
@@ -12,13 +12,6 @@ pub(super) fn resolve_import(
12
12
  repository_files: &BTreeSet<String>,
13
13
  ) -> ImportTarget {
14
14
  let language = language_for_path(from_path);
15
- if from_path.ends_with(".rs") {
16
- if let Some(module) = specifier.strip_prefix("rustmod:") {
17
- return resolve_rust_module_declaration(from_path, module, repository_files)
18
- .map(ImportTarget::File)
19
- .unwrap_or_else(|| ImportTarget::Unknown(specifier.to_string()));
20
- }
21
- }
22
15
  if specifier.starts_with('.') {
23
16
  if from_path.ends_with(".py") {
24
17
  return resolve_python_relative(from_path, specifier, repository_files)
@@ -155,19 +148,6 @@ fn resolve_rust_local(
155
148
  }
156
149
  }
157
150
 
158
- fn resolve_rust_module_declaration(
159
- from_path: &str,
160
- module: &str,
161
- repository_files: &BTreeSet<String>,
162
- ) -> Option<String> {
163
- let module = module.trim();
164
- if module.is_empty() || module.contains("::") || module.contains('/') {
165
- return None;
166
- }
167
- let module_base = normalize(Path::new(&rust_module_directory(from_path)).join(module));
168
- resolve_candidates(&module_base, repository_files, Some("rust"))
169
- }
170
-
171
151
  fn rust_module_directory(from_path: &str) -> String {
172
152
  let source = Path::new(from_path);
173
153
  let parent = source.parent().unwrap_or_else(|| Path::new(""));
@@ -217,7 +197,9 @@ fn go_module_for_file(
217
197
  from_path: &str,
218
198
  repository_files: &BTreeSet<String>,
219
199
  ) -> Option<String> {
220
- let mut current = Path::new(from_path).parent().unwrap_or_else(|| Path::new(""));
200
+ let mut current = Path::new(from_path)
201
+ .parent()
202
+ .unwrap_or_else(|| Path::new(""));
221
203
  loop {
222
204
  let go_mod_path = normalize(current.join("go.mod"));
223
205
  if repository_files.contains(&go_mod_path) {
@@ -26,12 +26,25 @@ pub(super) fn extract_imports(
26
26
  }
27
27
 
28
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();
29
34
  if let Some(rest) = specifier.strip_prefix('@') {
30
35
  let mut parts = rest.split('/');
31
36
  let scope = parts.next().unwrap_or_default();
32
37
  let package = parts.next().unwrap_or_default();
33
38
  return format!("@{scope}/{package}");
34
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
+ }
35
48
  specifier
36
49
  .split("::")
37
50
  .next()
@@ -39,6 +52,9 @@ pub(super) fn package_name(specifier: &str) -> String {
39
52
  .split('/')
40
53
  .next()
41
54
  .unwrap_or(specifier)
55
+ .split('.')
56
+ .next()
57
+ .unwrap_or(specifier)
42
58
  .to_string()
43
59
  }
44
60
 
@@ -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,