@lamentis/naome 1.3.16 → 1.4.0
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.
- package/Cargo.lock +2 -2
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/architecture_commands.rs +16 -2
- package/crates/naome-cli/src/architecture_init/infer.rs +131 -0
- package/crates/naome-cli/src/architecture_init/render.rs +56 -0
- package/crates/naome-cli/src/architecture_init/repository.rs +59 -0
- package/crates/naome-cli/src/architecture_init.rs +17 -0
- package/crates/naome-cli/src/main.rs +2 -1
- package/crates/naome-cli/tests/architecture_cli.rs +75 -0
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/architecture/config_findings/configuration/coverage.rs +81 -0
- package/crates/naome-core/src/architecture/config_findings/configuration/overlap.rs +117 -0
- package/crates/naome-core/src/architecture/config_findings/configuration.rs +12 -0
- package/crates/naome-core/src/architecture/config_findings/imports.rs +30 -0
- package/crates/naome-core/src/architecture/config_findings.rs +50 -0
- package/crates/naome-core/src/architecture/explain.rs +45 -0
- package/crates/naome-core/src/architecture/output.rs +211 -155
- package/crates/naome-core/src/architecture/rules.rs +4 -3
- package/crates/naome-core/src/architecture/scan/cache.rs +1 -1
- package/crates/naome-core/src/architecture/scan/imports/resolver/candidates.rs +71 -0
- package/crates/naome-core/src/architecture/scan/imports/resolver/js_ts_alias.rs +241 -0
- package/crates/naome-core/src/architecture/scan/imports/resolver.rs +162 -91
- package/crates/naome-core/src/architecture/scan.rs +20 -6
- package/crates/naome-core/src/architecture.rs +8 -3
- package/crates/naome-core/src/lib.rs +9 -7
- package/crates/naome-core/tests/architecture.rs +30 -0
- package/crates/naome-core/tests/architecture_acceptance.rs +304 -0
- package/crates/naome-core/tests/architecture_aliases.rs +101 -0
- package/crates/naome-core/tests/architecture_cache.rs +57 -0
- package/crates/naome-core/tests/architecture_config.rs +155 -1
- package/crates/naome-core/tests/architecture_rules.rs +32 -0
- package/crates/naome-core/tests/architecture_unresolved.rs +36 -0
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +1 -1
- package/templates/naome-root/.naome/bin/check-harness-health.js +1 -0
- package/templates/naome-root/.naome/bin/check-task-state.js +1 -0
- package/templates/naome-root/.naome/manifest.json +2 -2
- package/templates/naome-root/.naome/verification.json +6 -1
- package/templates/naome-root/docs/naome/architecture-fitness.md +76 -59
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
mod configuration;
|
|
2
|
+
mod imports;
|
|
3
|
+
|
|
4
|
+
use super::output::{ArchitectureAgentFeedback, ArchitectureConfigFinding};
|
|
5
|
+
use super::scan::ArchitectureScanReport;
|
|
6
|
+
|
|
7
|
+
pub fn architecture_config_agent_feedback(
|
|
8
|
+
findings: &[ArchitectureConfigFinding],
|
|
9
|
+
) -> Vec<ArchitectureAgentFeedback> {
|
|
10
|
+
findings
|
|
11
|
+
.iter()
|
|
12
|
+
.filter(|finding| finding.id == "arch.import.unresolved")
|
|
13
|
+
.map(|finding| ArchitectureAgentFeedback {
|
|
14
|
+
problem: finding.message.clone(),
|
|
15
|
+
repair: finding.suggestion.clone(),
|
|
16
|
+
files: finding
|
|
17
|
+
.subject
|
|
18
|
+
.strip_prefix("file:")
|
|
19
|
+
.map(|path| vec![path.to_string()])
|
|
20
|
+
.unwrap_or_default(),
|
|
21
|
+
must_not_do: vec![
|
|
22
|
+
"Do not leave unresolved imports hidden from architecture validation.".to_string(),
|
|
23
|
+
"Do not suppress architecture findings without an explicit ignore reason."
|
|
24
|
+
.to_string(),
|
|
25
|
+
],
|
|
26
|
+
})
|
|
27
|
+
.collect()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pub fn config_findings_for(scan: &ArchitectureScanReport) -> Vec<ArchitectureConfigFinding> {
|
|
31
|
+
let mut findings = Vec::new();
|
|
32
|
+
findings.extend(configuration::findings(scan));
|
|
33
|
+
findings.extend(imports::findings(scan));
|
|
34
|
+
findings.sort_by(|left, right| {
|
|
35
|
+
(
|
|
36
|
+
left.id.as_str(),
|
|
37
|
+
left.subject.as_str(),
|
|
38
|
+
left.message.as_str(),
|
|
39
|
+
)
|
|
40
|
+
.cmp(&(
|
|
41
|
+
right.id.as_str(),
|
|
42
|
+
right.subject.as_str(),
|
|
43
|
+
right.message.as_str(),
|
|
44
|
+
))
|
|
45
|
+
});
|
|
46
|
+
findings.dedup_by(|left, right| {
|
|
47
|
+
left.id == right.id && left.subject == right.subject && left.message == right.message
|
|
48
|
+
});
|
|
49
|
+
findings
|
|
50
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
use super::config_findings::config_findings_for;
|
|
2
|
+
use super::output::ARCHITECTURE_RULE_IDS;
|
|
3
|
+
use super::scan::ArchitectureScanReport;
|
|
4
|
+
|
|
5
|
+
pub fn format_architecture_explain(scan: &ArchitectureScanReport) -> String {
|
|
6
|
+
let findings = config_findings_for(scan);
|
|
7
|
+
let layers = scan
|
|
8
|
+
.config
|
|
9
|
+
.layers
|
|
10
|
+
.keys()
|
|
11
|
+
.cloned()
|
|
12
|
+
.collect::<Vec<_>>()
|
|
13
|
+
.join(", ");
|
|
14
|
+
let contexts = scan
|
|
15
|
+
.config
|
|
16
|
+
.contexts
|
|
17
|
+
.keys()
|
|
18
|
+
.cloned()
|
|
19
|
+
.collect::<Vec<_>>()
|
|
20
|
+
.join(", ");
|
|
21
|
+
let mut output = format!(
|
|
22
|
+
"NAOME Architecture Fitness\nrules: {}\nlayers: {}\ncontexts: {}\npath extractor: enabled for every repository\nimport extractors: typescript, javascript, rust, python, go, swift\n",
|
|
23
|
+
ARCHITECTURE_RULE_IDS.join(", "),
|
|
24
|
+
empty_label(&layers),
|
|
25
|
+
empty_label(&contexts)
|
|
26
|
+
);
|
|
27
|
+
if !findings.is_empty() {
|
|
28
|
+
output.push_str(&format!("configuration findings: {}\n", findings.len()));
|
|
29
|
+
for finding in findings.iter().take(5) {
|
|
30
|
+
output.push_str(&format!(
|
|
31
|
+
"- {} {}: {}\n",
|
|
32
|
+
finding.severity, finding.subject, finding.message
|
|
33
|
+
));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
output
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fn empty_label(value: &str) -> &str {
|
|
40
|
+
if value.is_empty() {
|
|
41
|
+
"<none>"
|
|
42
|
+
} else {
|
|
43
|
+
value
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
use serde::{Deserialize, Serialize};
|
|
2
|
+
use serde_json::{json, Value};
|
|
3
|
+
use std::collections::BTreeSet;
|
|
4
|
+
use std::path::Path;
|
|
2
5
|
|
|
3
6
|
use super::model::{Severity, SourceRange};
|
|
4
7
|
use super::scan::ArchitectureScanReport;
|
|
5
8
|
|
|
9
|
+
const SARIF_REPO_ROOT_BASE_ID: &str = "REPO_ROOT";
|
|
10
|
+
const ARCHITECTURE_CONFIG_PATH: &str = "naome.arch.yaml";
|
|
11
|
+
|
|
6
12
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
7
13
|
#[serde(rename_all = "camelCase")]
|
|
8
14
|
pub struct ViolationSummary {
|
|
@@ -86,138 +92,40 @@ pub fn architecture_agent_feedback(
|
|
|
86
92
|
.map(|violation| ArchitectureAgentFeedback {
|
|
87
93
|
problem: violation.message.clone(),
|
|
88
94
|
repair: violation.suggestion.clone(),
|
|
89
|
-
files: violation
|
|
90
|
-
must_not_do:
|
|
91
|
-
"Do not suppress architecture rules without an explicit ignore reason.".to_string(),
|
|
92
|
-
"Do not edit generated files unless the architecture config allows it.".to_string(),
|
|
93
|
-
],
|
|
95
|
+
files: feedback_files(violation),
|
|
96
|
+
must_not_do: feedback_must_not_do(violation),
|
|
94
97
|
})
|
|
95
98
|
.collect()
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
|
|
99
|
-
let mut
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
fn broad_layer_findings(scan: &ArchitectureScanReport) -> Vec<ArchitectureConfigFinding> {
|
|
110
|
-
let mut findings = Vec::new();
|
|
111
|
-
for (layer_name, layer) in &scan.config.layers {
|
|
112
|
-
let Some(broad_pattern) = layer
|
|
113
|
-
.paths
|
|
114
|
-
.iter()
|
|
115
|
-
.find(|pattern| is_broad_source_pattern(pattern))
|
|
101
|
+
fn feedback_files(violation: &ArchitectureViolation) -> Vec<String> {
|
|
102
|
+
let mut files = Vec::new();
|
|
103
|
+
if let Some(path) = &violation.path {
|
|
104
|
+
files.push(path.clone());
|
|
105
|
+
}
|
|
106
|
+
for endpoint in [&violation.from, &violation.to] {
|
|
107
|
+
let Some(path) = endpoint
|
|
108
|
+
.as_deref()
|
|
109
|
+
.and_then(|value| value.strip_prefix("file:"))
|
|
116
110
|
else {
|
|
117
111
|
continue;
|
|
118
112
|
};
|
|
119
|
-
|
|
120
|
-
.
|
|
121
|
-
.layers
|
|
122
|
-
.iter()
|
|
123
|
-
.filter(|(other_name, other_layer)| {
|
|
124
|
-
other_name.as_str() != layer_name.as_str()
|
|
125
|
-
&& other_layer
|
|
126
|
-
.paths
|
|
127
|
-
.iter()
|
|
128
|
-
.any(|pattern| pattern_is_narrower_than(pattern, broad_pattern))
|
|
129
|
-
})
|
|
130
|
-
.map(|(name, _)| name.clone())
|
|
131
|
-
.collect::<Vec<_>>();
|
|
132
|
-
if overlapping_layers.is_empty() {
|
|
133
|
-
continue;
|
|
113
|
+
if !files.iter().any(|existing| existing == path) {
|
|
114
|
+
files.push(path.to_string());
|
|
134
115
|
}
|
|
135
|
-
findings.push(ArchitectureConfigFinding {
|
|
136
|
-
id: "arch.config.broad_layer_overlap".to_string(),
|
|
137
|
-
severity: "warning".to_string(),
|
|
138
|
-
subject: format!("layer:{layer_name}"),
|
|
139
|
-
message: format!(
|
|
140
|
-
"Layer {layer_name} uses broad path pattern {broad_pattern} while narrower layers also match inside it: {}.",
|
|
141
|
-
overlapping_layers.join(", ")
|
|
142
|
-
),
|
|
143
|
-
suggestion: format!(
|
|
144
|
-
"Keep broad compatibility layers intentional and make allowed_dependencies explicit, or narrow {layer_name} so files do not inherit multiple architectural meanings by default."
|
|
145
|
-
),
|
|
146
|
-
agent_instruction: format!(
|
|
147
|
-
"Do not rely on broad layer {layer_name} to hide architecture boundaries; prefer narrower layer paths or explicit allow rules."
|
|
148
|
-
),
|
|
149
|
-
});
|
|
150
116
|
}
|
|
151
|
-
|
|
117
|
+
files
|
|
152
118
|
}
|
|
153
119
|
|
|
154
|
-
fn
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
.
|
|
161
|
-
.filter(|(_, context)| {
|
|
162
|
-
context
|
|
163
|
-
.paths
|
|
164
|
-
.iter()
|
|
165
|
-
.any(|pattern| is_broad_source_pattern(pattern))
|
|
166
|
-
})
|
|
167
|
-
.filter_map(|(context_name, context)| {
|
|
168
|
-
let broad_pattern = context
|
|
169
|
-
.paths
|
|
170
|
-
.iter()
|
|
171
|
-
.find(|pattern| is_broad_source_pattern(pattern))?;
|
|
172
|
-
let specific_contexts = scan
|
|
173
|
-
.config
|
|
174
|
-
.contexts
|
|
175
|
-
.iter()
|
|
176
|
-
.filter(|(other_name, other_context)| {
|
|
177
|
-
other_name.as_str() != context_name.as_str()
|
|
178
|
-
&& other_context
|
|
179
|
-
.paths
|
|
180
|
-
.iter()
|
|
181
|
-
.any(|pattern| pattern_is_narrower_than(pattern, broad_pattern))
|
|
182
|
-
})
|
|
183
|
-
.map(|(name, _)| name.clone())
|
|
184
|
-
.collect::<Vec<_>>();
|
|
185
|
-
if specific_contexts.is_empty() {
|
|
186
|
-
return None;
|
|
187
|
-
}
|
|
188
|
-
Some(ArchitectureConfigFinding {
|
|
189
|
-
id: "arch.config.catch_all_context_with_specific_contexts".to_string(),
|
|
190
|
-
severity: "warning".to_string(),
|
|
191
|
-
subject: format!("context:{context_name}"),
|
|
192
|
-
message: format!(
|
|
193
|
-
"Context {context_name} uses catch-all path pattern {broad_pattern} alongside narrower contexts: {}.",
|
|
194
|
-
specific_contexts.join(", ")
|
|
195
|
-
),
|
|
196
|
-
suggestion: format!(
|
|
197
|
-
"Treat {context_name} as a compatibility bucket only. Move shared code into explicit public APIs or replace the catch-all context with narrower bounded contexts."
|
|
198
|
-
),
|
|
199
|
-
agent_instruction: format!(
|
|
200
|
-
"Do not classify cross-context imports as safe only because {context_name} also matches them; model the real bounded context or public API."
|
|
201
|
-
),
|
|
202
|
-
})
|
|
203
|
-
})
|
|
204
|
-
.collect()
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
fn is_broad_source_pattern(pattern: &str) -> bool {
|
|
208
|
-
matches!(pattern, "**" | "**/*" | "src/**" | "packages/**/src/**")
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
fn pattern_is_narrower_than(pattern: &str, broad_pattern: &str) -> bool {
|
|
212
|
-
if pattern == broad_pattern {
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
match broad_pattern {
|
|
216
|
-
"**" | "**/*" => true,
|
|
217
|
-
"src/**" => pattern.starts_with("src/") && pattern != "src/**",
|
|
218
|
-
"packages/**/src/**" => pattern.starts_with("packages/") && pattern.contains("/src/"),
|
|
219
|
-
_ => false,
|
|
120
|
+
fn feedback_must_not_do(violation: &ArchitectureViolation) -> Vec<String> {
|
|
121
|
+
let mut items = vec![
|
|
122
|
+
"Do not suppress architecture rules without an explicit ignore reason.".to_string(),
|
|
123
|
+
"Do not edit generated files unless the architecture config allows it.".to_string(),
|
|
124
|
+
];
|
|
125
|
+
if violation.id == "arch.no_forbidden_layer_dependencies" {
|
|
126
|
+
items.push(violation.agent_instruction.clone());
|
|
220
127
|
}
|
|
128
|
+
items
|
|
221
129
|
}
|
|
222
130
|
|
|
223
131
|
pub fn summary_for(violations: &[ArchitectureViolation]) -> ViolationSummary {
|
|
@@ -315,44 +223,192 @@ pub fn format_architecture_scan(report: &ArchitectureScanReport) -> String {
|
|
|
315
223
|
output
|
|
316
224
|
}
|
|
317
225
|
|
|
318
|
-
pub fn
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
.
|
|
329
|
-
.
|
|
330
|
-
|
|
331
|
-
.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
226
|
+
pub fn architecture_validation_sarif(report: &ArchitectureValidation) -> Value {
|
|
227
|
+
architecture_validation_sarif_with_root(report, Path::new("/"))
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
pub fn architecture_validation_sarif_with_root(
|
|
231
|
+
report: &ArchitectureValidation,
|
|
232
|
+
repo_root: &Path,
|
|
233
|
+
) -> Value {
|
|
234
|
+
let mut rule_ids = ARCHITECTURE_RULE_IDS
|
|
235
|
+
.iter()
|
|
236
|
+
.map(|rule| rule.to_string())
|
|
237
|
+
.collect::<BTreeSet<_>>();
|
|
238
|
+
for finding in &report.config_findings {
|
|
239
|
+
rule_ids.insert(finding.id.clone());
|
|
240
|
+
}
|
|
241
|
+
for violation in &report.violations {
|
|
242
|
+
rule_ids.insert(violation.id.clone());
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let rules = rule_ids
|
|
246
|
+
.iter()
|
|
247
|
+
.map(|id| {
|
|
248
|
+
json!({
|
|
249
|
+
"id": id,
|
|
250
|
+
"name": id,
|
|
251
|
+
"shortDescription": {
|
|
252
|
+
"text": id
|
|
253
|
+
},
|
|
254
|
+
"helpUri": "https://github.com/Lamentis-O/naome/blob/main/docs/naome/architecture-fitness.md"
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
.collect::<Vec<_>>();
|
|
258
|
+
|
|
259
|
+
let mut results = Vec::new();
|
|
260
|
+
for violation in &report.violations {
|
|
261
|
+
results.push(sarif_result_for_violation(violation));
|
|
262
|
+
}
|
|
263
|
+
for finding in &report.config_findings {
|
|
264
|
+
results.push(sarif_result_for_config_finding(finding));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
json!({
|
|
268
|
+
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
|
|
269
|
+
"version": "2.1.0",
|
|
270
|
+
"runs": [
|
|
271
|
+
{
|
|
272
|
+
"tool": {
|
|
273
|
+
"driver": {
|
|
274
|
+
"name": "NAOME Architecture Fitness",
|
|
275
|
+
"informationUri": "https://github.com/Lamentis-O/naome",
|
|
276
|
+
"rules": rules
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
"originalUriBaseIds": {
|
|
280
|
+
"REPO_ROOT": {
|
|
281
|
+
"uri": file_uri_for_directory(repo_root)
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
"results": results,
|
|
285
|
+
"properties": {
|
|
286
|
+
"status": report.status,
|
|
287
|
+
"filesScanned": report.files_scanned,
|
|
288
|
+
"graphNodes": report.graph_nodes,
|
|
289
|
+
"graphEdges": report.graph_edges,
|
|
290
|
+
"changedOnlyRequested": report.changed_only_requested,
|
|
291
|
+
"changedOnlyMode": report.changed_only_mode,
|
|
292
|
+
"changedOnlyDegradedToFullScan": report.changed_only_degraded_to_full_scan,
|
|
293
|
+
"changedOnlyDegradationReason": report.changed_only_degradation_reason
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
]
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
fn sarif_result_for_violation(violation: &ArchitectureViolation) -> Value {
|
|
301
|
+
json!({
|
|
302
|
+
"ruleId": violation.id,
|
|
303
|
+
"level": sarif_level_for_severity(violation.severity),
|
|
304
|
+
"message": {
|
|
305
|
+
"text": violation.message
|
|
306
|
+
},
|
|
307
|
+
"locations": sarif_locations(
|
|
308
|
+
violation.path.as_deref().or_else(|| violation_file_endpoint(violation)),
|
|
309
|
+
violation.source_range.as_ref()
|
|
310
|
+
),
|
|
311
|
+
"properties": {
|
|
312
|
+
"type": violation.violation_type,
|
|
313
|
+
"from": violation.from,
|
|
314
|
+
"to": violation.to,
|
|
315
|
+
"suggestion": violation.suggestion,
|
|
316
|
+
"agentInstruction": violation.agent_instruction
|
|
347
317
|
}
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
fn sarif_result_for_config_finding(finding: &ArchitectureConfigFinding) -> Value {
|
|
322
|
+
let path = config_finding_path(finding);
|
|
323
|
+
json!({
|
|
324
|
+
"ruleId": finding.id,
|
|
325
|
+
"level": sarif_level_for_config_finding(&finding.severity),
|
|
326
|
+
"message": {
|
|
327
|
+
"text": finding.message
|
|
328
|
+
},
|
|
329
|
+
"locations": sarif_locations(Some(path.as_str()), None),
|
|
330
|
+
"properties": {
|
|
331
|
+
"subject": finding.subject,
|
|
332
|
+
"suggestion": finding.suggestion,
|
|
333
|
+
"agentInstruction": finding.agent_instruction
|
|
334
|
+
}
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
fn sarif_locations(path: Option<&str>, source_range: Option<&SourceRange>) -> Vec<Value> {
|
|
339
|
+
let Some(path) = path else {
|
|
340
|
+
return Vec::new();
|
|
341
|
+
};
|
|
342
|
+
let mut physical_location = json!({
|
|
343
|
+
"artifactLocation": {
|
|
344
|
+
"uri": path,
|
|
345
|
+
"uriBaseId": SARIF_REPO_ROOT_BASE_ID
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
if let Some(range) = source_range {
|
|
349
|
+
physical_location["region"] = json!({
|
|
350
|
+
"startLine": range.start_line,
|
|
351
|
+
"startColumn": range.start_column,
|
|
352
|
+
"endLine": range.end_line,
|
|
353
|
+
"endColumn": range.end_column
|
|
354
|
+
});
|
|
348
355
|
}
|
|
349
|
-
|
|
356
|
+
vec![json!({
|
|
357
|
+
"physicalLocation": physical_location
|
|
358
|
+
})]
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
fn violation_file_endpoint(violation: &ArchitectureViolation) -> Option<&str> {
|
|
362
|
+
[&violation.from, &violation.to]
|
|
363
|
+
.into_iter()
|
|
364
|
+
.flatten()
|
|
365
|
+
.find_map(|endpoint| endpoint.strip_prefix("file:"))
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
fn config_finding_path(finding: &ArchitectureConfigFinding) -> String {
|
|
369
|
+
finding
|
|
370
|
+
.subject
|
|
371
|
+
.strip_prefix("file:")
|
|
372
|
+
.unwrap_or(ARCHITECTURE_CONFIG_PATH)
|
|
373
|
+
.to_string()
|
|
350
374
|
}
|
|
351
375
|
|
|
352
|
-
fn
|
|
353
|
-
|
|
354
|
-
"
|
|
355
|
-
|
|
356
|
-
|
|
376
|
+
fn sarif_level_for_severity(severity: Severity) -> &'static str {
|
|
377
|
+
match severity {
|
|
378
|
+
Severity::Error => "error",
|
|
379
|
+
Severity::Warning => "warning",
|
|
380
|
+
Severity::Info => "note",
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
fn sarif_level_for_config_finding(severity: &str) -> &'static str {
|
|
385
|
+
match severity {
|
|
386
|
+
"error" => "error",
|
|
387
|
+
"warning" => "warning",
|
|
388
|
+
_ => "note",
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
fn file_uri_for_directory(path: &Path) -> String {
|
|
393
|
+
let mut normalized = path.to_string_lossy().replace('\\', "/");
|
|
394
|
+
if !normalized.starts_with('/') {
|
|
395
|
+
normalized.insert(0, '/');
|
|
396
|
+
}
|
|
397
|
+
while normalized.ends_with('/') && normalized.len() > 1 {
|
|
398
|
+
normalized.pop();
|
|
399
|
+
}
|
|
400
|
+
format!("file://{}/", encode_uri_path(&normalized))
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
fn encode_uri_path(path: &str) -> String {
|
|
404
|
+
let mut encoded = String::new();
|
|
405
|
+
for byte in path.as_bytes() {
|
|
406
|
+
match *byte {
|
|
407
|
+
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'/' | b'-' | b'.' | b'_' | b'~' => {
|
|
408
|
+
encoded.push(*byte as char)
|
|
409
|
+
}
|
|
410
|
+
value => encoded.push_str(&format!("%{value:02X}")),
|
|
411
|
+
}
|
|
357
412
|
}
|
|
413
|
+
encoded
|
|
358
414
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
use crate::paths;
|
|
2
2
|
|
|
3
|
+
use super::config_findings::{architecture_config_agent_feedback, config_findings_for};
|
|
3
4
|
use super::model::Severity;
|
|
4
5
|
use super::output::{
|
|
5
|
-
architecture_agent_feedback,
|
|
6
|
-
ArchitectureViolation,
|
|
6
|
+
architecture_agent_feedback, summary_for, ArchitectureValidation, ArchitectureViolation,
|
|
7
7
|
};
|
|
8
8
|
use super::scan::ArchitectureScanReport;
|
|
9
9
|
use crate::architecture::model::ArchitectureEdgeKind;
|
|
@@ -44,8 +44,9 @@ pub fn validate_scan(scan: ArchitectureScanReport) -> ArchitectureValidation {
|
|
|
44
44
|
});
|
|
45
45
|
let summary = summary_for(&violations);
|
|
46
46
|
let status = if summary.errors > 0 { "fail" } else { "pass" }.to_string();
|
|
47
|
-
let agent_feedback = architecture_agent_feedback(&violations);
|
|
48
47
|
let config_findings = config_findings_for(&scan);
|
|
48
|
+
let mut agent_feedback = architecture_agent_feedback(&violations);
|
|
49
|
+
agent_feedback.extend(architecture_config_agent_feedback(&config_findings));
|
|
49
50
|
|
|
50
51
|
ArchitectureValidation {
|
|
51
52
|
schema: "naome.arch.validation.v1".to_string(),
|
|
@@ -9,7 +9,7 @@ use super::FileFact;
|
|
|
9
9
|
use crate::models::NaomeError;
|
|
10
10
|
|
|
11
11
|
const CACHE_SCHEMA: &str = "naome.architecture-cache.v1";
|
|
12
|
-
const EXTRACTOR_VERSION: &str = "architecture-cache-v1.
|
|
12
|
+
const EXTRACTOR_VERSION: &str = "architecture-cache-v1.4.0";
|
|
13
13
|
const CACHE_PATH: &str = ".naome/cache/architecture/cache.json";
|
|
14
14
|
|
|
15
15
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
use std::collections::BTreeSet;
|
|
2
|
+
|
|
3
|
+
pub(super) fn resolve_candidates(
|
|
4
|
+
candidate: &str,
|
|
5
|
+
repository_files: &BTreeSet<String>,
|
|
6
|
+
language: Option<&str>,
|
|
7
|
+
) -> Option<String> {
|
|
8
|
+
candidate_paths(candidate, language)
|
|
9
|
+
.into_iter()
|
|
10
|
+
.find(|path| repository_files.contains(path))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
pub(super) fn resolve_progressively(
|
|
14
|
+
candidate: &str,
|
|
15
|
+
repository_files: &BTreeSet<String>,
|
|
16
|
+
language: Option<&str>,
|
|
17
|
+
separator: char,
|
|
18
|
+
) -> Option<String> {
|
|
19
|
+
let mut current = candidate.to_string();
|
|
20
|
+
loop {
|
|
21
|
+
if let Some(path) = resolve_candidates(¤t, repository_files, language) {
|
|
22
|
+
return Some(path);
|
|
23
|
+
}
|
|
24
|
+
let Some((parent, _)) = current.rsplit_once(separator) else {
|
|
25
|
+
return None;
|
|
26
|
+
};
|
|
27
|
+
current = parent.to_string();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn candidate_paths(candidate: &str, language: Option<&str>) -> Vec<String> {
|
|
32
|
+
let extensions = extensions_for_language(language);
|
|
33
|
+
let mut candidates = extensions
|
|
34
|
+
.iter()
|
|
35
|
+
.map(|extension| format!("{candidate}{extension}"))
|
|
36
|
+
.collect::<Vec<_>>();
|
|
37
|
+
match language {
|
|
38
|
+
Some("rust") => candidates.push(format!("{candidate}/mod.rs")),
|
|
39
|
+
Some("python") => candidates.push(format!("{candidate}/__init__.py")),
|
|
40
|
+
Some("go") => {}
|
|
41
|
+
Some("typescript") | Some("javascript") => candidates.extend([
|
|
42
|
+
format!("{candidate}/index.ts"),
|
|
43
|
+
format!("{candidate}/index.tsx"),
|
|
44
|
+
format!("{candidate}/index.js"),
|
|
45
|
+
format!("{candidate}/index.jsx"),
|
|
46
|
+
]),
|
|
47
|
+
_ => candidates.extend([
|
|
48
|
+
format!("{candidate}/index.ts"),
|
|
49
|
+
format!("{candidate}/index.tsx"),
|
|
50
|
+
format!("{candidate}/index.js"),
|
|
51
|
+
format!("{candidate}/index.jsx"),
|
|
52
|
+
format!("{candidate}/mod.rs"),
|
|
53
|
+
format!("{candidate}/__init__.py"),
|
|
54
|
+
]),
|
|
55
|
+
}
|
|
56
|
+
candidates
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fn extensions_for_language(language: Option<&str>) -> &'static [&'static str] {
|
|
60
|
+
match language {
|
|
61
|
+
Some("rust") => &["", ".rs"],
|
|
62
|
+
Some("python") => &["", ".py"],
|
|
63
|
+
Some("go") => &["", ".go"],
|
|
64
|
+
Some("swift") => &["", ".swift"],
|
|
65
|
+
Some("typescript") => &["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
|
|
66
|
+
Some("javascript") => &["", ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"],
|
|
67
|
+
_ => &[
|
|
68
|
+
"", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".rs", ".py", ".go", ".swift",
|
|
69
|
+
],
|
|
70
|
+
}
|
|
71
|
+
}
|