@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,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
+ }
@@ -5,13 +5,27 @@ use super::output::{
5
5
  Severity,
6
6
  };
7
7
  use super::scan::ArchitectureScanReport;
8
+ use crate::architecture::model::ArchitectureEdgeKind;
9
+
10
+ mod budgets;
11
+ mod context;
12
+ mod cycles;
13
+ mod external;
14
+ mod graph;
15
+ mod transitive;
8
16
 
9
17
  pub fn validate_scan(scan: ArchitectureScanReport) -> ArchitectureValidation {
10
18
  let mut violations = Vec::new();
11
19
  let mut rules_executed = Vec::new();
12
20
 
13
- validate_max_file_lines(&scan, &mut violations, &mut rules_executed);
21
+ budgets::validate_file_size_budget(&scan, &mut violations, &mut rules_executed);
14
22
  validate_generated_manual_boundary(&scan, &mut violations, &mut rules_executed);
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);
15
29
 
16
30
  violations.sort_by(|left, right| {
17
31
  (
@@ -46,41 +60,73 @@ pub fn validate_scan(scan: ArchitectureScanReport) -> ArchitectureValidation {
46
60
  }
47
61
  }
48
62
 
49
- fn validate_max_file_lines(
63
+ fn validate_forbidden_layer_dependencies(
50
64
  scan: &ArchitectureScanReport,
51
65
  violations: &mut Vec<ArchitectureViolation>,
52
66
  rules_executed: &mut Vec<String>,
53
67
  ) {
54
- let rule = scan.config.rule("max_file_lines");
68
+ let rule = scan.config.rule("no_forbidden_layer_dependencies");
55
69
  if !rule.enabled {
56
70
  return;
57
71
  }
58
- rules_executed.push("arch.max_file_lines".to_string());
59
- let Some(limit) = rule.value else {
60
- return;
61
- };
62
- for fact in scan.file_facts.values() {
63
- if fact.line_count <= limit {
72
+ rules_executed.push("arch.no_forbidden_layer_dependencies".to_string());
73
+ for edge in &scan.graph.edges {
74
+ if edge.kind != ArchitectureEdgeKind::Imports {
64
75
  continue;
65
76
  }
66
- violations.push(ArchitectureViolation {
67
- id: "arch.max_file_lines".to_string(),
68
- severity: rule.severity,
69
- violation_type: "file_size_budget".to_string(),
70
- message: format!(
71
- "{} has {} lines, exceeding the configured budget of {}.",
72
- fact.path, fact.line_count, limit
73
- ),
74
- from: Some(format!("file:{}", fact.path)),
75
- to: None,
76
- path: Some(fact.path.clone()),
77
- source_range: None,
78
- suggestion: "Split the file into cohesive modules or move generated content behind an explicit ignore rule.".to_string(),
79
- agent_instruction: format!(
80
- "Reduce {} below {} lines or add a justified generated-code ignore rule if it is not manually owned.",
81
- fact.path, limit
82
- ),
83
- });
77
+ let Some(from_path) = edge.from.strip_prefix("file:") else {
78
+ continue;
79
+ };
80
+ let Some(to_path) = edge.to.strip_prefix("file:") else {
81
+ continue;
82
+ };
83
+ let Some(from_fact) = scan.file_facts.get(from_path) else {
84
+ continue;
85
+ };
86
+ let Some(to_fact) = scan.file_facts.get(to_path) else {
87
+ continue;
88
+ };
89
+ if to_fact.layers.is_empty() {
90
+ continue;
91
+ }
92
+ for from_layer in &from_fact.layers {
93
+ let allowed_layers = scan
94
+ .config
95
+ .allowed_dependencies
96
+ .get(from_layer)
97
+ .map(Vec::as_slice)
98
+ .unwrap_or(&[]);
99
+ if to_fact
100
+ .layers
101
+ .iter()
102
+ .any(|to_layer| from_layer == to_layer || allowed_layers.contains(to_layer))
103
+ {
104
+ continue;
105
+ }
106
+ let target_layers = to_fact.layers.join(", ");
107
+ violations.push(ArchitectureViolation {
108
+ id: "arch.no_forbidden_layer_dependencies".to_string(),
109
+ severity: rule.severity,
110
+ violation_type: "forbidden_layer_dependency".to_string(),
111
+ message: format!(
112
+ "{} in layer {} imports {} in forbidden layer {}.",
113
+ from_path, from_layer, to_path, target_layers
114
+ ),
115
+ from: Some(edge.from.clone()),
116
+ to: Some(edge.to.clone()),
117
+ path: Some(from_path.to_string()),
118
+ source_range: edge.metadata.source_range.clone(),
119
+ suggestion: format!(
120
+ "Move the dependency behind an allowed layer boundary or change naome.arch.yaml allowed_dependencies if this architecture is intentional. {} currently allows: {}.",
121
+ from_layer,
122
+ allowed_layers.join(", ")
123
+ ),
124
+ agent_instruction: format!(
125
+ "Do not import {} from {}. Introduce an allowed boundary or invert the dependency before re-running architecture validation.",
126
+ target_layers, from_layer
127
+ ),
128
+ });
129
+ }
84
130
  }
85
131
  }
86
132
 
@@ -2,7 +2,7 @@ use serde_json::json;
2
2
 
3
3
  use crate::architecture::model::{
4
4
  ArchitectureEdge, ArchitectureEdgeKind, ArchitectureGraph, ArchitectureMetadata,
5
- ArchitectureNode, ArchitectureNodeKind,
5
+ ArchitectureNode, ArchitectureNodeKind, SourceRange,
6
6
  };
7
7
 
8
8
  pub(super) fn push_node(
@@ -22,6 +22,32 @@ pub(super) fn push_node(
22
22
  });
23
23
  }
24
24
 
25
+ pub(super) fn push_node_with_metadata(
26
+ graph: &mut ArchitectureGraph,
27
+ id: &str,
28
+ kind: ArchitectureNodeKind,
29
+ label: &str,
30
+ path: Option<String>,
31
+ language: Option<String>,
32
+ confidence: f32,
33
+ extractor: &str,
34
+ raw_origin: serde_json::Value,
35
+ ) {
36
+ graph.nodes.push(ArchitectureNode {
37
+ id: id.to_string(),
38
+ kind,
39
+ label: label.to_string(),
40
+ metadata: ArchitectureMetadata {
41
+ path,
42
+ language,
43
+ source_range: None,
44
+ confidence,
45
+ extractor: extractor.to_string(),
46
+ raw_origin,
47
+ },
48
+ });
49
+ }
50
+
25
51
  pub(super) fn push_edge(
26
52
  graph: &mut ArchitectureGraph,
27
53
  from: &str,
@@ -40,6 +66,42 @@ pub(super) fn push_edge(
40
66
  });
41
67
  }
42
68
 
69
+ pub(super) fn push_edge_with_metadata(
70
+ graph: &mut ArchitectureGraph,
71
+ from: &str,
72
+ to: &str,
73
+ kind: ArchitectureEdgeKind,
74
+ label: &str,
75
+ path: Option<String>,
76
+ language: Option<String>,
77
+ source_range: Option<SourceRange>,
78
+ confidence: f32,
79
+ extractor: &str,
80
+ raw_origin: serde_json::Value,
81
+ ) {
82
+ graph.edges.push(ArchitectureEdge {
83
+ id: format!(
84
+ "edge:{from}:{to}:{kind:?}:{}",
85
+ raw_origin
86
+ .get("specifier")
87
+ .and_then(serde_json::Value::as_str)
88
+ .unwrap_or(label)
89
+ ),
90
+ from: from.to_string(),
91
+ to: to.to_string(),
92
+ kind,
93
+ label: label.to_string(),
94
+ metadata: ArchitectureMetadata {
95
+ path,
96
+ language,
97
+ source_range,
98
+ confidence,
99
+ extractor: extractor.to_string(),
100
+ raw_origin,
101
+ },
102
+ });
103
+ }
104
+
43
105
  fn metadata(
44
106
  path: Option<String>,
45
107
  language: Option<String>,
@@ -1,13 +1,11 @@
1
1
  use std::collections::BTreeSet;
2
- use std::fs;
3
2
  use std::path::Path;
4
3
 
5
4
  use crate::architecture::config::ArchitectureConfig;
6
5
  use crate::architecture::scan::FileFact;
7
6
  use crate::paths;
8
7
 
9
- pub(super) fn file_fact(root: &Path, path: &str, config: &ArchitectureConfig) -> FileFact {
10
- let content = fs::read_to_string(root.join(path)).unwrap_or_default();
8
+ pub(super) fn file_fact(path: &str, content: &str, config: &ArchitectureConfig) -> FileFact {
11
9
  FileFact {
12
10
  path: path.to_string(),
13
11
  language: language_for_path(path),
@@ -27,6 +25,7 @@ pub(super) fn file_fact(root: &Path, path: &str, config: &ArchitectureConfig) ->
27
25
  .map(|(name, value)| (name, &value.paths)),
28
26
  ),
29
27
  ignored: ignore_reason(path, config),
28
+ imports: Vec::new(),
30
29
  }
31
30
  }
32
31