@lamentis/naome 1.3.15 → 1.3.16
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 +5 -4
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/architecture/output.rs +165 -2
- package/crates/naome-core/src/architecture/rules.rs +4 -1
- package/crates/naome-core/src/architecture.rs +3 -3
- package/crates/naome-core/src/lib.rs +7 -7
- package/crates/naome-core/tests/architecture_config.rs +67 -0
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +1 -1
package/Cargo.lock
CHANGED
|
@@ -76,7 +76,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|
|
76
76
|
|
|
77
77
|
[[package]]
|
|
78
78
|
name = "naome-cli"
|
|
79
|
-
version = "1.3.
|
|
79
|
+
version = "1.3.16"
|
|
80
80
|
dependencies = [
|
|
81
81
|
"naome-core",
|
|
82
82
|
"serde_json",
|
|
@@ -84,7 +84,7 @@ dependencies = [
|
|
|
84
84
|
|
|
85
85
|
[[package]]
|
|
86
86
|
name = "naome-core"
|
|
87
|
-
version = "1.3.
|
|
87
|
+
version = "1.3.16"
|
|
88
88
|
dependencies = [
|
|
89
89
|
"serde",
|
|
90
90
|
"serde_json",
|
|
@@ -2,9 +2,9 @@ use std::fs;
|
|
|
2
2
|
use std::path::{Path, PathBuf};
|
|
3
3
|
|
|
4
4
|
use naome_core::{
|
|
5
|
-
default_architecture_config_text, format_architecture_explain,
|
|
6
|
-
format_architecture_validation, scan_architecture,
|
|
7
|
-
ArchitectureScanOptions, ARCHITECTURE_RULE_IDS,
|
|
5
|
+
config_findings_for, default_architecture_config_text, format_architecture_explain,
|
|
6
|
+
format_architecture_scan, format_architecture_validation, scan_architecture,
|
|
7
|
+
validate_architecture, ArchitectureScanOptions, ARCHITECTURE_RULE_IDS,
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
use crate::cli_args::{has_flag, option_value};
|
|
@@ -53,7 +53,8 @@ fn run_arch_explain(root: &Path, args: &[String]) -> Result<(), Box<dyn std::err
|
|
|
53
53
|
"layers": scan.config.layers.keys().collect::<Vec<_>>(),
|
|
54
54
|
"contexts": scan.config.contexts.keys().collect::<Vec<_>>(),
|
|
55
55
|
"rules": ARCHITECTURE_RULE_IDS,
|
|
56
|
-
"extractors": ["path", "typescript", "javascript", "rust", "python", "go", "swift"]
|
|
56
|
+
"extractors": ["path", "typescript", "javascript", "rust", "python", "go", "swift"],
|
|
57
|
+
"configFindings": config_findings_for(&scan)
|
|
57
58
|
}))?
|
|
58
59
|
);
|
|
59
60
|
} else {
|
|
@@ -34,6 +34,17 @@ pub struct ArchitectureAgentFeedback {
|
|
|
34
34
|
pub must_not_do: Vec<String>,
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
38
|
+
#[serde(rename_all = "camelCase")]
|
|
39
|
+
pub struct ArchitectureConfigFinding {
|
|
40
|
+
pub id: String,
|
|
41
|
+
pub severity: String,
|
|
42
|
+
pub subject: String,
|
|
43
|
+
pub message: String,
|
|
44
|
+
pub suggestion: String,
|
|
45
|
+
pub agent_instruction: String,
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
38
49
|
#[serde(rename_all = "camelCase")]
|
|
39
50
|
pub struct ArchitectureValidation {
|
|
@@ -48,6 +59,7 @@ pub struct ArchitectureValidation {
|
|
|
48
59
|
pub changed_only_degraded_to_full_scan: bool,
|
|
49
60
|
pub changed_only_mode: String,
|
|
50
61
|
pub changed_only_degradation_reason: Option<String>,
|
|
62
|
+
pub config_findings: Vec<ArchitectureConfigFinding>,
|
|
51
63
|
pub violations: Vec<ArchitectureViolation>,
|
|
52
64
|
#[serde(rename = "agent_feedback")]
|
|
53
65
|
pub agent_feedback: Vec<ArchitectureAgentFeedback>,
|
|
@@ -83,6 +95,131 @@ pub fn architecture_agent_feedback(
|
|
|
83
95
|
.collect()
|
|
84
96
|
}
|
|
85
97
|
|
|
98
|
+
pub fn config_findings_for(scan: &ArchitectureScanReport) -> Vec<ArchitectureConfigFinding> {
|
|
99
|
+
let mut findings = Vec::new();
|
|
100
|
+
findings.extend(broad_layer_findings(scan));
|
|
101
|
+
findings.extend(catch_all_context_findings(scan));
|
|
102
|
+
findings.sort_by(|left, right| {
|
|
103
|
+
(left.id.as_str(), left.subject.as_str()).cmp(&(right.id.as_str(), right.subject.as_str()))
|
|
104
|
+
});
|
|
105
|
+
findings.dedup_by(|left, right| left.id == right.id && left.subject == right.subject);
|
|
106
|
+
findings
|
|
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))
|
|
116
|
+
else {
|
|
117
|
+
continue;
|
|
118
|
+
};
|
|
119
|
+
let overlapping_layers = scan
|
|
120
|
+
.config
|
|
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;
|
|
134
|
+
}
|
|
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
|
+
}
|
|
151
|
+
findings
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fn catch_all_context_findings(scan: &ArchitectureScanReport) -> Vec<ArchitectureConfigFinding> {
|
|
155
|
+
if scan.config.contexts.len() <= 1 {
|
|
156
|
+
return Vec::new();
|
|
157
|
+
}
|
|
158
|
+
scan.config
|
|
159
|
+
.contexts
|
|
160
|
+
.iter()
|
|
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,
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
86
223
|
pub fn summary_for(violations: &[ArchitectureViolation]) -> ViolationSummary {
|
|
87
224
|
ViolationSummary {
|
|
88
225
|
errors: violations
|
|
@@ -128,6 +265,21 @@ pub fn format_architecture_validation(report: &ArchitectureValidation) -> String
|
|
|
128
265
|
lines.push("changed-only requested: using incremental architecture cache".to_string());
|
|
129
266
|
}
|
|
130
267
|
|
|
268
|
+
if !report.config_findings.is_empty() {
|
|
269
|
+
lines.push(format!(
|
|
270
|
+
"configuration findings: {}",
|
|
271
|
+
report.config_findings.len()
|
|
272
|
+
));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
for finding in report.config_findings.iter().take(5) {
|
|
276
|
+
lines.push(format!(
|
|
277
|
+
"- Config {} {} {}",
|
|
278
|
+
finding.severity, finding.subject, finding.message
|
|
279
|
+
));
|
|
280
|
+
lines.push(format!(" fix: {}", finding.suggestion));
|
|
281
|
+
}
|
|
282
|
+
|
|
131
283
|
for violation in report.violations.iter().take(10) {
|
|
132
284
|
lines.push(format!(
|
|
133
285
|
"- {:?} {} {}",
|
|
@@ -164,6 +316,7 @@ pub fn format_architecture_scan(report: &ArchitectureScanReport) -> String {
|
|
|
164
316
|
}
|
|
165
317
|
|
|
166
318
|
pub fn format_architecture_explain(scan: &ArchitectureScanReport) -> String {
|
|
319
|
+
let findings = config_findings_for(scan);
|
|
167
320
|
let layers = scan
|
|
168
321
|
.config
|
|
169
322
|
.layers
|
|
@@ -178,12 +331,22 @@ pub fn format_architecture_explain(scan: &ArchitectureScanReport) -> String {
|
|
|
178
331
|
.cloned()
|
|
179
332
|
.collect::<Vec<_>>()
|
|
180
333
|
.join(", ");
|
|
181
|
-
format!(
|
|
334
|
+
let mut output = format!(
|
|
182
335
|
"NAOME Architecture Fitness\nrules: {}\nlayers: {}\ncontexts: {}\npath extractor: enabled for every repository\nimport extractors: typescript, javascript, rust, python, go, swift\n",
|
|
183
336
|
ARCHITECTURE_RULE_IDS.join(", "),
|
|
184
337
|
empty_label(&layers),
|
|
185
338
|
empty_label(&contexts)
|
|
186
|
-
)
|
|
339
|
+
);
|
|
340
|
+
if !findings.is_empty() {
|
|
341
|
+
output.push_str(&format!("configuration findings: {}\n", findings.len()));
|
|
342
|
+
for finding in findings.iter().take(5) {
|
|
343
|
+
output.push_str(&format!(
|
|
344
|
+
"- {} {}: {}\n",
|
|
345
|
+
finding.severity, finding.subject, finding.message
|
|
346
|
+
));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
output
|
|
187
350
|
}
|
|
188
351
|
|
|
189
352
|
fn empty_label(value: &str) -> &str {
|
|
@@ -2,7 +2,8 @@ use crate::paths;
|
|
|
2
2
|
|
|
3
3
|
use super::model::Severity;
|
|
4
4
|
use super::output::{
|
|
5
|
-
architecture_agent_feedback, summary_for, ArchitectureValidation,
|
|
5
|
+
architecture_agent_feedback, config_findings_for, summary_for, ArchitectureValidation,
|
|
6
|
+
ArchitectureViolation,
|
|
6
7
|
};
|
|
7
8
|
use super::scan::ArchitectureScanReport;
|
|
8
9
|
use crate::architecture::model::ArchitectureEdgeKind;
|
|
@@ -44,6 +45,7 @@ pub fn validate_scan(scan: ArchitectureScanReport) -> ArchitectureValidation {
|
|
|
44
45
|
let summary = summary_for(&violations);
|
|
45
46
|
let status = if summary.errors > 0 { "fail" } else { "pass" }.to_string();
|
|
46
47
|
let agent_feedback = architecture_agent_feedback(&violations);
|
|
48
|
+
let config_findings = config_findings_for(&scan);
|
|
47
49
|
|
|
48
50
|
ArchitectureValidation {
|
|
49
51
|
schema: "naome.arch.validation.v1".to_string(),
|
|
@@ -57,6 +59,7 @@ pub fn validate_scan(scan: ArchitectureScanReport) -> ArchitectureValidation {
|
|
|
57
59
|
changed_only_degraded_to_full_scan: scan.changed_only_degraded_to_full_scan,
|
|
58
60
|
changed_only_mode: scan.changed_only_mode,
|
|
59
61
|
changed_only_degradation_reason: scan.changed_only_degradation_reason,
|
|
62
|
+
config_findings,
|
|
60
63
|
violations,
|
|
61
64
|
agent_feedback,
|
|
62
65
|
}
|
|
@@ -16,9 +16,9 @@ pub use model::{
|
|
|
16
16
|
ArchitectureNode, ArchitectureNodeKind, Severity, SourceRange,
|
|
17
17
|
};
|
|
18
18
|
pub use output::{
|
|
19
|
-
format_architecture_explain, format_architecture_scan,
|
|
20
|
-
|
|
21
|
-
ARCHITECTURE_RULE_IDS,
|
|
19
|
+
config_findings_for, format_architecture_explain, format_architecture_scan,
|
|
20
|
+
format_architecture_validation, ArchitectureAgentFeedback, ArchitectureConfigFinding,
|
|
21
|
+
ArchitectureValidation, ArchitectureViolation, ViolationSummary, ARCHITECTURE_RULE_IDS,
|
|
22
22
|
};
|
|
23
23
|
pub use scan::{scan_architecture, ArchitectureScanOptions, ArchitectureScanReport};
|
|
24
24
|
|
|
@@ -21,13 +21,13 @@ mod verification_contract_policy;
|
|
|
21
21
|
mod workflow;
|
|
22
22
|
|
|
23
23
|
pub use architecture::{
|
|
24
|
-
default_architecture_config_text, format_architecture_explain,
|
|
25
|
-
format_architecture_validation, scan_architecture,
|
|
26
|
-
ArchitectureAgentFeedback, ArchitectureConfig,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
ARCHITECTURE_RULE_IDS,
|
|
24
|
+
config_findings_for, default_architecture_config_text, format_architecture_explain,
|
|
25
|
+
format_architecture_scan, format_architecture_validation, scan_architecture,
|
|
26
|
+
validate_architecture, ArchitectureAgentFeedback, ArchitectureConfig,
|
|
27
|
+
ArchitectureConfigFinding, ArchitectureEdge, ArchitectureEdgeKind, ArchitectureGraph,
|
|
28
|
+
ArchitectureMetadata, ArchitectureNode, ArchitectureNodeKind, ArchitectureScanOptions,
|
|
29
|
+
ArchitectureScanReport, ArchitectureValidation, ArchitectureViolation, ContextConfig,
|
|
30
|
+
LayerConfig, RuleConfig, Severity, SourceRange, ViolationSummary, ARCHITECTURE_RULE_IDS,
|
|
31
31
|
};
|
|
32
32
|
pub use context::{
|
|
33
33
|
select_context_for_changed_paths, select_context_for_prompt, ContextBudgetLedger,
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
use naome_core::{format_architecture_validation, validate_architecture, ArchitectureScanOptions};
|
|
2
|
+
|
|
3
|
+
mod architecture_support;
|
|
4
|
+
|
|
5
|
+
use architecture_support::FixtureRepo;
|
|
6
|
+
|
|
7
|
+
#[test]
|
|
8
|
+
fn validation_reports_risky_broad_architecture_config_without_failing() {
|
|
9
|
+
let repo = FixtureRepo::new();
|
|
10
|
+
repo.write(
|
|
11
|
+
"naome.arch.yaml",
|
|
12
|
+
r#"
|
|
13
|
+
layers:
|
|
14
|
+
application:
|
|
15
|
+
paths:
|
|
16
|
+
- "src/**"
|
|
17
|
+
domain:
|
|
18
|
+
paths:
|
|
19
|
+
- "src/domain/**"
|
|
20
|
+
infrastructure:
|
|
21
|
+
paths:
|
|
22
|
+
- "src/infrastructure/**"
|
|
23
|
+
allowed_dependencies:
|
|
24
|
+
application:
|
|
25
|
+
- domain
|
|
26
|
+
- infrastructure
|
|
27
|
+
domain:
|
|
28
|
+
infrastructure:
|
|
29
|
+
- domain
|
|
30
|
+
contexts:
|
|
31
|
+
default:
|
|
32
|
+
paths:
|
|
33
|
+
- "src/**"
|
|
34
|
+
public_api:
|
|
35
|
+
- "src/index.ts"
|
|
36
|
+
billing:
|
|
37
|
+
paths:
|
|
38
|
+
- "src/billing/**"
|
|
39
|
+
public_api:
|
|
40
|
+
- "src/billing/index.ts"
|
|
41
|
+
rules:
|
|
42
|
+
no_forbidden_layer_dependencies:
|
|
43
|
+
enabled: true
|
|
44
|
+
severity: error
|
|
45
|
+
"#,
|
|
46
|
+
);
|
|
47
|
+
repo.write("src/domain/event.ts", "export const event = 1;\n");
|
|
48
|
+
repo.write("src/infrastructure/db.ts", "export const db = 1;\n");
|
|
49
|
+
repo.write("src/billing/index.ts", "export const billing = 1;\n");
|
|
50
|
+
|
|
51
|
+
let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
|
|
52
|
+
let output = format_architecture_validation(&report);
|
|
53
|
+
|
|
54
|
+
assert_eq!(report.status, "pass");
|
|
55
|
+
assert_eq!(report.summary.warnings, 0);
|
|
56
|
+
assert!(report.config_findings.iter().any(|finding| {
|
|
57
|
+
finding.id == "arch.config.broad_layer_overlap"
|
|
58
|
+
&& finding.subject == "layer:application"
|
|
59
|
+
&& finding.severity == "warning"
|
|
60
|
+
}));
|
|
61
|
+
assert!(report.config_findings.iter().any(|finding| {
|
|
62
|
+
finding.id == "arch.config.catch_all_context_with_specific_contexts"
|
|
63
|
+
&& finding.subject == "context:default"
|
|
64
|
+
&& finding.severity == "warning"
|
|
65
|
+
}));
|
|
66
|
+
assert!(output.contains("configuration findings: 2"));
|
|
67
|
+
}
|
|
Binary file
|
package/native/linux-x64/naome
CHANGED
|
Binary file
|