@lamentis/naome 1.3.11 → 1.3.13

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 (41) 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 +244 -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/graph_builder.rs +130 -30
  18. package/crates/naome-core/src/architecture/scan/imports/extractors/swift.rs +48 -0
  19. package/crates/naome-core/src/architecture/scan/imports/extractors.rs +7 -7
  20. package/crates/naome-core/src/architecture/scan/imports/resolver.rs +44 -22
  21. package/crates/naome-core/src/architecture/scan/imports.rs +17 -0
  22. package/crates/naome-core/src/architecture/scan/manifest/common.rs +102 -0
  23. package/crates/naome-core/src/architecture/scan/manifest/parsers/json.rs +46 -0
  24. package/crates/naome-core/src/architecture/scan/manifest/parsers/other.rs +280 -0
  25. package/crates/naome-core/src/architecture/scan/manifest/parsers/toml.rs +184 -0
  26. package/crates/naome-core/src/architecture/scan/manifest/parsers.rs +3 -0
  27. package/crates/naome-core/src/architecture/scan/manifest.rs +33 -0
  28. package/crates/naome-core/src/architecture/scan.rs +27 -1
  29. package/crates/naome-core/src/architecture.rs +1 -1
  30. package/crates/naome-core/src/lib.rs +1 -0
  31. package/crates/naome-core/tests/architecture.rs +53 -85
  32. package/crates/naome-core/tests/architecture_manifests.rs +289 -0
  33. package/crates/naome-core/tests/architecture_rules.rs +498 -0
  34. package/crates/naome-core/tests/architecture_support/mod.rs +80 -0
  35. package/crates/naome-core/tests/architecture_swift.rs +111 -0
  36. package/installer/harness-files.js +3 -3
  37. package/native/darwin-arm64/naome +0 -0
  38. package/native/linux-x64/naome +0 -0
  39. package/package.json +1 -1
  40. package/templates/naome-root/.naome/manifest.json +2 -2
  41. package/templates/naome-root/docs/naome/architecture-fitness.md +61 -8
@@ -0,0 +1,280 @@
1
+ use crate::architecture::scan::{ManifestDependency, ManifestFact};
2
+
3
+ use super::super::common::{dependency, fact, fallback_package_name, quoted_value};
4
+
5
+ pub(in crate::architecture::scan::manifest) fn go_mod(
6
+ path: &str,
7
+ content: &str,
8
+ ) -> Option<ManifestFact> {
9
+ let package_name = content
10
+ .lines()
11
+ .find_map(|line| line.trim().strip_prefix("module "))
12
+ .map(str::trim)
13
+ .filter(|value| !value.is_empty())
14
+ .map(str::to_string)
15
+ .unwrap_or_else(|| fallback_package_name(path, "go"));
16
+ let mut dependencies = Vec::new();
17
+ let mut in_require_block = false;
18
+ for raw_line in content.lines() {
19
+ let line = raw_line.split("//").next().unwrap_or_default().trim();
20
+ if line == "require (" {
21
+ in_require_block = true;
22
+ continue;
23
+ }
24
+ if in_require_block && line == ")" {
25
+ in_require_block = false;
26
+ continue;
27
+ }
28
+ let require_line = line.strip_prefix("require ").unwrap_or(line);
29
+ if !in_require_block && !line.starts_with("require ") {
30
+ continue;
31
+ }
32
+ if let Some((name, version)) = first_two_fields(require_line) {
33
+ dependencies.push(dependency(name, "runtime", version, 0.95));
34
+ }
35
+ }
36
+ Some(fact(path, "go", package_name, dependencies, 0.95))
37
+ }
38
+
39
+ pub(in crate::architecture::scan::manifest) fn pom_xml(
40
+ path: &str,
41
+ content: &str,
42
+ ) -> Option<ManifestFact> {
43
+ let project_content = xml_without_parent_blocks(content);
44
+ let package_group = first_xml_tag(&project_content, "groupId")
45
+ .or_else(|| first_xml_tag(content, "groupId"))
46
+ .unwrap_or_else(|| "maven".to_string());
47
+ let package_artifact = first_xml_tag(&project_content, "artifactId")
48
+ .unwrap_or_else(|| fallback_package_name(path, "maven"));
49
+ let package_name = format!("{package_group}:{package_artifact}");
50
+ let dependency_content = xml_without_blocks(
51
+ &xml_without_blocks(content, "dependencyManagement"),
52
+ "build",
53
+ );
54
+ let dependencies = project_dependency_sections(&dependency_content)
55
+ .into_iter()
56
+ .flat_map(|section| section.split("<dependency>").skip(1).collect::<Vec<_>>())
57
+ .filter_map(|block| {
58
+ let group = first_xml_tag(block, "groupId")?;
59
+ let artifact = first_xml_tag(block, "artifactId")?;
60
+ Some(dependency(
61
+ format!("{group}:{artifact}"),
62
+ first_xml_tag(block, "scope").unwrap_or_else(|| "runtime".into()),
63
+ first_xml_tag(block, "version"),
64
+ 0.75,
65
+ ))
66
+ })
67
+ .collect();
68
+ Some(fact(path, "maven", package_name, dependencies, 0.75))
69
+ }
70
+
71
+ pub(in crate::architecture::scan::manifest) fn build_gradle(
72
+ path: &str,
73
+ content: &str,
74
+ ) -> Option<ManifestFact> {
75
+ let package_name = fallback_package_name(path, "gradle");
76
+ let dependencies = content.lines().filter_map(gradle_dependency).collect();
77
+ Some(fact(path, "gradle", package_name, dependencies, 0.7))
78
+ }
79
+
80
+ pub(in crate::architecture::scan::manifest) fn package_swift(
81
+ path: &str,
82
+ content: &str,
83
+ ) -> Option<ManifestFact> {
84
+ let active_content = swift_active_content(content);
85
+ let package_name = swift_argument_value(&active_content, "name")
86
+ .unwrap_or_else(|| fallback_package_name(path, "swift"));
87
+ let dependencies = active_content
88
+ .split(".package")
89
+ .skip(1)
90
+ .filter_map(|block| {
91
+ let url = swift_argument_value(block, "url")?;
92
+ Some(dependency(
93
+ package_name_from_url(&url),
94
+ "runtime",
95
+ swift_package_version(block),
96
+ 0.8,
97
+ ))
98
+ })
99
+ .collect();
100
+ Some(fact(path, "swift", package_name, dependencies, 0.8))
101
+ }
102
+
103
+ pub(in crate::architecture::scan::manifest) fn xcode_project(
104
+ path: &str,
105
+ content: &str,
106
+ ) -> Option<ManifestFact> {
107
+ let package_name = fallback_package_name(path, "xcode");
108
+ let dependencies = content
109
+ .lines()
110
+ .filter_map(|line| {
111
+ let url = line
112
+ .trim()
113
+ .strip_prefix("repositoryURL = ")
114
+ .and_then(quoted_value)?;
115
+ Some(dependency(
116
+ package_name_from_url(&url),
117
+ "runtime",
118
+ None,
119
+ 0.65,
120
+ ))
121
+ })
122
+ .collect();
123
+ Some(fact(path, "xcode", package_name, dependencies, 0.65))
124
+ }
125
+
126
+ fn first_two_fields(line: &str) -> Option<(String, Option<String>)> {
127
+ let mut parts = line.split_whitespace();
128
+ Some((parts.next()?.to_string(), parts.next().map(str::to_string)))
129
+ }
130
+
131
+ fn first_xml_tag(content: &str, tag: &str) -> Option<String> {
132
+ let open = format!("<{tag}>");
133
+ let close = format!("</{tag}>");
134
+ content
135
+ .split_once(&open)?
136
+ .1
137
+ .split_once(&close)
138
+ .map(|(value, _)| value.trim().to_string())
139
+ .filter(|value| !value.is_empty())
140
+ }
141
+
142
+ fn xml_without_parent_blocks(content: &str) -> String {
143
+ xml_without_blocks(content, "parent")
144
+ }
145
+
146
+ fn xml_without_blocks(content: &str, tag: &str) -> String {
147
+ let open = format!("<{tag}>");
148
+ let close = format!("</{tag}>");
149
+ let mut remaining = content;
150
+ let mut output = String::new();
151
+ while let Some((before, after_open)) = remaining.split_once(&open) {
152
+ output.push_str(before);
153
+ let Some((_, after_close)) = after_open.split_once(&close) else {
154
+ output.push_str(after_open);
155
+ return output;
156
+ };
157
+ remaining = after_close;
158
+ }
159
+ output.push_str(remaining);
160
+ output
161
+ }
162
+
163
+ fn project_dependency_sections(content: &str) -> Vec<&str> {
164
+ content
165
+ .split("<dependencies>")
166
+ .skip(1)
167
+ .filter_map(|block| block.split_once("</dependencies>").map(|(block, _)| block))
168
+ .collect()
169
+ }
170
+
171
+ fn gradle_dependency(line: &str) -> Option<ManifestDependency> {
172
+ let trimmed = line.split("//").next().unwrap_or_default().trim();
173
+ if trimmed.is_empty() {
174
+ return None;
175
+ }
176
+ let kind = [
177
+ "implementation",
178
+ "api",
179
+ "compileOnly",
180
+ "runtimeOnly",
181
+ "testImplementation",
182
+ ]
183
+ .into_iter()
184
+ .find(|kind| trimmed.contains(&format!("{kind} ")) || trimmed.contains(&format!("{kind}(")))?;
185
+ let coordinate_text = trimmed
186
+ .split_once(&format!("{kind} "))
187
+ .map(|(_, value)| value.trim())
188
+ .or_else(|| {
189
+ trimmed
190
+ .split_once(&format!("{kind}("))
191
+ .map(|(_, value)| value.trim_end_matches(')').trim())
192
+ })?;
193
+ if let Some(coordinate) = quoted_value(coordinate_text) {
194
+ return gradle_coordinate_dependency(&coordinate, kind);
195
+ }
196
+ let group = gradle_map_value(coordinate_text, "group")?;
197
+ let artifact = gradle_map_value(coordinate_text, "name")?;
198
+ Some(dependency(
199
+ format!("{group}:{artifact}"),
200
+ kind,
201
+ gradle_map_value(coordinate_text, "version"),
202
+ 0.7,
203
+ ))
204
+ }
205
+
206
+ fn gradle_coordinate_dependency(coordinate: &str, kind: &str) -> Option<ManifestDependency> {
207
+ let mut parts = coordinate.split(':');
208
+ let group = parts.next()?;
209
+ let artifact = parts.next()?;
210
+ Some(dependency(
211
+ format!("{group}:{artifact}"),
212
+ kind,
213
+ parts.next().map(str::to_string),
214
+ 0.7,
215
+ ))
216
+ }
217
+
218
+ fn gradle_map_value(text: &str, key: &str) -> Option<String> {
219
+ text.split(',').find_map(|part| {
220
+ let (name, value) = part.trim().split_once(':')?;
221
+ if name.trim() == key {
222
+ quoted_value(value.trim())
223
+ } else {
224
+ None
225
+ }
226
+ })
227
+ }
228
+
229
+ fn swift_argument_value(content: &str, key: &str) -> Option<String> {
230
+ let marker = format!("{key}:");
231
+ let (_, value) = content.split_once(&marker)?;
232
+ quoted_value(value.trim())
233
+ }
234
+
235
+ fn swift_active_content(content: &str) -> String {
236
+ content
237
+ .lines()
238
+ .map(strip_swift_line_comment)
239
+ .collect::<Vec<_>>()
240
+ .join("\n")
241
+ }
242
+
243
+ fn strip_swift_line_comment(line: &str) -> &str {
244
+ let mut in_quote = false;
245
+ let mut escaped = false;
246
+ let mut previous = '\0';
247
+ for (index, character) in line.char_indices() {
248
+ if in_quote {
249
+ if character == '"' && !escaped {
250
+ in_quote = false;
251
+ }
252
+ escaped = character == '\\' && !escaped;
253
+ } else if character == '"' {
254
+ in_quote = true;
255
+ escaped = false;
256
+ } else if previous == '/' && character == '/' {
257
+ return &line[..index - 1];
258
+ }
259
+ if character != '\\' {
260
+ escaped = false;
261
+ }
262
+ previous = character;
263
+ }
264
+ line
265
+ }
266
+
267
+ fn swift_package_version(block: &str) -> Option<String> {
268
+ swift_argument_value(block, "from")
269
+ .or_else(|| swift_argument_value(block, "exact"))
270
+ .or_else(|| swift_argument_value(block, "branch"))
271
+ }
272
+
273
+ fn package_name_from_url(url: &str) -> String {
274
+ url.trim_end_matches(".git")
275
+ .trim_end_matches('/')
276
+ .rsplit('/')
277
+ .next()
278
+ .unwrap_or(url)
279
+ .to_string()
280
+ }
@@ -0,0 +1,184 @@
1
+ use crate::architecture::scan::{ManifestDependency, ManifestFact};
2
+
3
+ use super::super::common::{
4
+ clean_toml_line, dependency, fact, fallback_package_name, quoted_value,
5
+ };
6
+
7
+ pub(in crate::architecture::scan::manifest) fn cargo_toml(
8
+ path: &str,
9
+ content: &str,
10
+ ) -> Option<ManifestFact> {
11
+ let package_name = toml_string(content, "package", "name")
12
+ .unwrap_or_else(|| fallback_package_name(path, "cargo"));
13
+ let dependencies = toml_dependency_sections(
14
+ content,
15
+ &[
16
+ ("dependencies", "runtime"),
17
+ ("dev-dependencies", "development"),
18
+ ("build-dependencies", "build"),
19
+ ],
20
+ );
21
+ Some(fact(path, "cargo", package_name, dependencies, 0.9))
22
+ }
23
+
24
+ pub(in crate::architecture::scan::manifest) fn pyproject_toml(
25
+ path: &str,
26
+ content: &str,
27
+ ) -> Option<ManifestFact> {
28
+ let package_name = toml_string(content, "project", "name")
29
+ .unwrap_or_else(|| fallback_package_name(path, "python"));
30
+ let mut dependencies = toml_string_array(content, "project", "dependencies")
31
+ .into_iter()
32
+ .map(|value| dependency(python_dependency_name(&value), "runtime", Some(value), 0.85))
33
+ .collect::<Vec<_>>();
34
+ dependencies.extend(
35
+ toml_dependency_sections(content, &[("tool.poetry.dependencies", "runtime")])
36
+ .into_iter()
37
+ .filter(|dependency| dependency.name != "python"),
38
+ );
39
+ Some(fact(path, "python", package_name, dependencies, 0.85))
40
+ }
41
+
42
+ fn toml_string(content: &str, section: &str, key: &str) -> Option<String> {
43
+ section_value(content, section, key).and_then(quoted_value)
44
+ }
45
+
46
+ fn toml_string_array(content: &str, section: &str, key: &str) -> Vec<String> {
47
+ let mut current_section = "";
48
+ let mut collecting = false;
49
+ let mut array = String::new();
50
+ for line in content.lines().map(clean_toml_line) {
51
+ if !collecting {
52
+ if let Some(section_name) = line
53
+ .strip_prefix('[')
54
+ .and_then(|line| line.strip_suffix(']'))
55
+ {
56
+ current_section = section_name.trim();
57
+ continue;
58
+ }
59
+ if current_section != section {
60
+ continue;
61
+ }
62
+ let Some(value) = line
63
+ .strip_prefix(key)
64
+ .and_then(|line| line.trim().strip_prefix('='))
65
+ else {
66
+ continue;
67
+ };
68
+ array.push_str(value.trim());
69
+ if value.contains(']') {
70
+ break;
71
+ }
72
+ collecting = value.trim_start().starts_with('[');
73
+ continue;
74
+ }
75
+ array.push(' ');
76
+ array.push_str(line);
77
+ if line.contains(']') {
78
+ break;
79
+ }
80
+ }
81
+ array
82
+ .trim()
83
+ .trim_start_matches('[')
84
+ .trim_end_matches(']')
85
+ .split(',')
86
+ .filter_map(quoted_value)
87
+ .collect()
88
+ }
89
+
90
+ fn section_value<'a>(content: &'a str, section: &str, key: &str) -> Option<&'a str> {
91
+ let mut current_section = "";
92
+ for line in content.lines().map(clean_toml_line) {
93
+ if let Some(section_name) = line
94
+ .strip_prefix('[')
95
+ .and_then(|line| line.strip_suffix(']'))
96
+ {
97
+ current_section = section_name.trim();
98
+ continue;
99
+ }
100
+ if current_section == section {
101
+ if let Some(value) = line
102
+ .strip_prefix(key)
103
+ .and_then(|line| line.trim().strip_prefix('='))
104
+ {
105
+ return Some(value.trim());
106
+ }
107
+ }
108
+ }
109
+ None
110
+ }
111
+
112
+ fn toml_dependency_sections(content: &str, sections: &[(&str, &str)]) -> Vec<ManifestDependency> {
113
+ let mut current_kind = None;
114
+ let mut table_dependency = None;
115
+ let mut dependencies = Vec::new();
116
+ for line in content.lines().map(clean_toml_line) {
117
+ if let Some(section_name) = line
118
+ .strip_prefix('[')
119
+ .and_then(|line| line.strip_suffix(']'))
120
+ {
121
+ push_table_dependency(&mut dependencies, table_dependency.take());
122
+ let section_name = section_name.trim();
123
+ current_kind = sections
124
+ .iter()
125
+ .find(|(section, _)| *section == section_name)
126
+ .map(|(_, kind)| *kind);
127
+ if current_kind.is_none() {
128
+ table_dependency = dependency_table(section_name, sections);
129
+ }
130
+ continue;
131
+ }
132
+ if let Some((name, kind, version)) = table_dependency.as_mut() {
133
+ if let Some((key, value)) = line.split_once('=') {
134
+ if key.trim() == "version" {
135
+ *version = quoted_value(value.trim());
136
+ }
137
+ }
138
+ if !name.is_empty() && !kind.is_empty() {
139
+ continue;
140
+ }
141
+ }
142
+ if let (Some(kind), Some((name, value))) = (current_kind, line.split_once('=')) {
143
+ dependencies.push(dependency(
144
+ name.trim().to_string(),
145
+ kind,
146
+ quoted_value(value.trim()),
147
+ 0.85,
148
+ ));
149
+ }
150
+ }
151
+ push_table_dependency(&mut dependencies, table_dependency);
152
+ dependencies
153
+ }
154
+
155
+ fn dependency_table<'a>(
156
+ section_name: &str,
157
+ sections: &[(&str, &'a str)],
158
+ ) -> Option<(String, &'a str, Option<String>)> {
159
+ sections.iter().find_map(|(section, kind)| {
160
+ let dependency_name = section_name.strip_prefix(&format!("{section}."))?;
161
+ if dependency_name.is_empty() || dependency_name.contains('.') {
162
+ return None;
163
+ }
164
+ Some((dependency_name.to_string(), *kind, None))
165
+ })
166
+ }
167
+
168
+ fn push_table_dependency(
169
+ dependencies: &mut Vec<ManifestDependency>,
170
+ table_dependency: Option<(String, &str, Option<String>)>,
171
+ ) {
172
+ if let Some((name, kind, version)) = table_dependency {
173
+ dependencies.push(dependency(name, kind, version, 0.85));
174
+ }
175
+ }
176
+
177
+ fn python_dependency_name(value: &str) -> String {
178
+ value
179
+ .split(['<', '>', '=', '!', '~', '[', ';', ' '])
180
+ .next()
181
+ .unwrap_or(value)
182
+ .trim()
183
+ .to_string()
184
+ }
@@ -0,0 +1,3 @@
1
+ pub(super) mod json;
2
+ pub(super) mod other;
3
+ pub(super) mod toml;
@@ -0,0 +1,33 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+
4
+ use super::ManifestFact;
5
+
6
+ mod common;
7
+ mod parsers;
8
+
9
+ use common::{manifest_kind, ManifestKind};
10
+
11
+ pub(super) fn extract_manifests(root: &Path, files: &[String]) -> Vec<ManifestFact> {
12
+ let mut manifests = files
13
+ .iter()
14
+ .filter_map(|path| extract_manifest(root, path))
15
+ .collect::<Vec<_>>();
16
+ manifests.sort_by(|left, right| left.path.cmp(&right.path));
17
+ manifests
18
+ }
19
+
20
+ fn extract_manifest(root: &Path, path: &str) -> Option<ManifestFact> {
21
+ let kind = manifest_kind(path)?;
22
+ let content = fs::read_to_string(root.join(path)).ok()?;
23
+ match kind {
24
+ ManifestKind::PackageJson => parsers::json::package_json(path, &content),
25
+ ManifestKind::CargoToml => parsers::toml::cargo_toml(path, &content),
26
+ ManifestKind::PyprojectToml => parsers::toml::pyproject_toml(path, &content),
27
+ ManifestKind::GoMod => parsers::other::go_mod(path, &content),
28
+ ManifestKind::PomXml => parsers::other::pom_xml(path, &content),
29
+ ManifestKind::BuildGradle => parsers::other::build_gradle(path, &content),
30
+ ManifestKind::PackageSwift => parsers::other::package_swift(path, &content),
31
+ ManifestKind::XcodeProject => parsers::other::xcode_project(path, &content),
32
+ }
33
+ }
@@ -11,6 +11,7 @@ use super::model::ArchitectureGraph;
11
11
 
12
12
  mod graph_builder;
13
13
  mod imports;
14
+ mod manifest;
14
15
  mod path_scan;
15
16
 
16
17
  #[derive(Debug, Clone, Default)]
@@ -32,6 +33,8 @@ pub struct ArchitectureScanReport {
32
33
  pub config: ArchitectureConfig,
33
34
  #[serde(skip_serializing)]
34
35
  pub file_facts: BTreeMap<String, FileFact>,
36
+ #[serde(skip_serializing)]
37
+ pub manifest_facts: Vec<ManifestFact>,
35
38
  }
36
39
 
37
40
  #[derive(Debug, Clone, Serialize)]
@@ -64,6 +67,26 @@ pub enum ImportTarget {
64
67
  Unknown(String),
65
68
  }
66
69
 
70
+ #[derive(Debug, Clone, Serialize)]
71
+ #[serde(rename_all = "camelCase")]
72
+ pub struct ManifestFact {
73
+ pub path: String,
74
+ pub ecosystem: String,
75
+ pub package_name: String,
76
+ pub dependencies: Vec<ManifestDependency>,
77
+ pub confidence: f32,
78
+ pub extractor: String,
79
+ }
80
+
81
+ #[derive(Debug, Clone, Serialize)]
82
+ #[serde(rename_all = "camelCase")]
83
+ pub struct ManifestDependency {
84
+ pub name: String,
85
+ pub dependency_kind: String,
86
+ pub version: Option<String>,
87
+ pub confidence: f32,
88
+ }
89
+
67
90
  pub fn scan_architecture(
68
91
  root: &Path,
69
92
  options: ArchitectureScanOptions,
@@ -71,7 +94,9 @@ pub fn scan_architecture(
71
94
  let config = read_architecture_config(root, options.config_path.as_deref())?;
72
95
  let changed_paths = changed_paths(root, options.changed_only)?;
73
96
  let files = path_scan::repository_files(root, &config)?;
74
- let (mut graph, file_facts) = graph_builder::build_path_graph(root, files, &config);
97
+ let manifest_facts = manifest::extract_manifests(root, &files);
98
+ let (mut graph, file_facts) =
99
+ graph_builder::build_path_graph(root, files, &config, &manifest_facts);
75
100
 
76
101
  graph.sort_stable();
77
102
  Ok(ArchitectureScanReport {
@@ -83,6 +108,7 @@ pub fn scan_architecture(
83
108
  changed_paths,
84
109
  config,
85
110
  file_facts,
111
+ manifest_facts,
86
112
  })
87
113
  }
88
114
 
@@ -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,