@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 CHANGED
@@ -76,7 +76,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
76
76
 
77
77
  [[package]]
78
78
  name = "naome-cli"
79
- version = "1.3.15"
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.15"
87
+ version = "1.3.16"
88
88
  dependencies = [
89
89
  "serde",
90
90
  "serde_json",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-cli"
3
- version = "1.3.15"
3
+ version = "1.3.16"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -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, format_architecture_scan,
6
- format_architecture_validation, scan_architecture, validate_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 {
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-core"
3
- version = "1.3.15"
3
+ version = "1.3.16"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -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, ArchitectureViolation,
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, format_architecture_validation,
20
- ArchitectureAgentFeedback, ArchitectureValidation, ArchitectureViolation, ViolationSummary,
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, format_architecture_scan,
25
- format_architecture_validation, scan_architecture, validate_architecture,
26
- ArchitectureAgentFeedback, ArchitectureConfig, ArchitectureEdge, ArchitectureEdgeKind,
27
- ArchitectureGraph, ArchitectureMetadata, ArchitectureNode, ArchitectureNodeKind,
28
- ArchitectureScanOptions, ArchitectureScanReport, ArchitectureValidation, ArchitectureViolation,
29
- ContextConfig, LayerConfig, RuleConfig, Severity, SourceRange, ViolationSummary,
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
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamentis/naome",
3
- "version": "1.3.15",
3
+ "version": "1.3.16",
4
4
  "description": "Native-first CLI for the NAOME agent harness.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",