@lamentis/naome 1.3.13 → 1.3.14
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 +1 -1
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/architecture/output.rs +26 -4
- package/crates/naome-core/src/architecture/rules.rs +2 -0
- package/crates/naome-core/src/architecture/scan/cache.rs +145 -0
- package/crates/naome-core/src/architecture/scan/graph_builder.rs +31 -10
- package/crates/naome-core/src/architecture/scan.rs +232 -14
- package/crates/naome-core/tests/architecture_cache.rs +212 -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/manifest.json +1 -1
- package/templates/naome-root/docs/naome/architecture-fitness.md +29 -8
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.14"
|
|
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.14"
|
|
88
88
|
dependencies = [
|
|
89
89
|
"serde",
|
|
90
90
|
"serde_json",
|
|
@@ -53,7 +53,7 @@ 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"]
|
|
56
|
+
"extractors": ["path", "typescript", "javascript", "rust", "python", "go", "swift"]
|
|
57
57
|
}))?
|
|
58
58
|
);
|
|
59
59
|
} else {
|
|
@@ -54,6 +54,8 @@ pub struct ArchitectureValidation {
|
|
|
54
54
|
pub rules_executed: Vec<String>,
|
|
55
55
|
pub changed_only_requested: bool,
|
|
56
56
|
pub changed_only_degraded_to_full_scan: bool,
|
|
57
|
+
pub changed_only_mode: String,
|
|
58
|
+
pub changed_only_degradation_reason: Option<String>,
|
|
57
59
|
pub violations: Vec<ArchitectureViolation>,
|
|
58
60
|
#[serde(rename = "agent_feedback")]
|
|
59
61
|
pub agent_feedback: Vec<ArchitectureAgentFeedback>,
|
|
@@ -134,7 +136,15 @@ pub fn format_architecture_validation(report: &ArchitectureValidation) -> String
|
|
|
134
136
|
];
|
|
135
137
|
|
|
136
138
|
if report.changed_only_degraded_to_full_scan {
|
|
137
|
-
|
|
139
|
+
let reason = report
|
|
140
|
+
.changed_only_degradation_reason
|
|
141
|
+
.as_deref()
|
|
142
|
+
.unwrap_or("soundness");
|
|
143
|
+
lines.push(format!(
|
|
144
|
+
"changed-only requested: degraded to full scan for soundness ({reason})"
|
|
145
|
+
));
|
|
146
|
+
} else if report.changed_only_requested {
|
|
147
|
+
lines.push("changed-only requested: using incremental architecture cache".to_string());
|
|
138
148
|
}
|
|
139
149
|
|
|
140
150
|
for violation in report.violations.iter().take(10) {
|
|
@@ -152,12 +162,24 @@ pub fn format_architecture_validation(report: &ArchitectureValidation) -> String
|
|
|
152
162
|
}
|
|
153
163
|
|
|
154
164
|
pub fn format_architecture_scan(report: &ArchitectureScanReport) -> String {
|
|
155
|
-
format!(
|
|
165
|
+
let mut output = format!(
|
|
156
166
|
"NAOME architecture scan\nfiles scanned: {}\ngraph nodes: {}\ngraph edges: {}\n",
|
|
157
167
|
report.files_scanned,
|
|
158
168
|
report.graph.nodes.len(),
|
|
159
169
|
report.graph.edges.len()
|
|
160
|
-
)
|
|
170
|
+
);
|
|
171
|
+
if report.changed_only_degraded_to_full_scan {
|
|
172
|
+
let reason = report
|
|
173
|
+
.changed_only_degradation_reason
|
|
174
|
+
.as_deref()
|
|
175
|
+
.unwrap_or("soundness");
|
|
176
|
+
output.push_str(&format!(
|
|
177
|
+
"changed-only requested: degraded to full scan for soundness ({reason})\n"
|
|
178
|
+
));
|
|
179
|
+
} else if report.changed_only_requested {
|
|
180
|
+
output.push_str("changed-only requested: using incremental architecture cache\n");
|
|
181
|
+
}
|
|
182
|
+
output
|
|
161
183
|
}
|
|
162
184
|
|
|
163
185
|
pub fn format_architecture_explain(scan: &ArchitectureScanReport) -> String {
|
|
@@ -176,7 +198,7 @@ pub fn format_architecture_explain(scan: &ArchitectureScanReport) -> String {
|
|
|
176
198
|
.collect::<Vec<_>>()
|
|
177
199
|
.join(", ");
|
|
178
200
|
format!(
|
|
179
|
-
"NAOME Architecture Fitness\nrules: {}\nlayers: {}\ncontexts: {}\npath extractor: enabled for every repository\nimport extractors: typescript, javascript, rust, python, go\n",
|
|
201
|
+
"NAOME Architecture Fitness\nrules: {}\nlayers: {}\ncontexts: {}\npath extractor: enabled for every repository\nimport extractors: typescript, javascript, rust, python, go, swift\n",
|
|
180
202
|
ARCHITECTURE_RULE_IDS.join(", "),
|
|
181
203
|
empty_label(&layers),
|
|
182
204
|
empty_label(&contexts)
|
|
@@ -55,6 +55,8 @@ pub fn validate_scan(scan: ArchitectureScanReport) -> ArchitectureValidation {
|
|
|
55
55
|
rules_executed,
|
|
56
56
|
changed_only_requested: scan.changed_only_requested,
|
|
57
57
|
changed_only_degraded_to_full_scan: scan.changed_only_degraded_to_full_scan,
|
|
58
|
+
changed_only_mode: scan.changed_only_mode,
|
|
59
|
+
changed_only_degradation_reason: scan.changed_only_degradation_reason,
|
|
58
60
|
violations,
|
|
59
61
|
agent_feedback,
|
|
60
62
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
2
|
+
use std::fs;
|
|
3
|
+
use std::path::Path;
|
|
4
|
+
|
|
5
|
+
use serde::{Deserialize, Serialize};
|
|
6
|
+
use sha2::{Digest, Sha256};
|
|
7
|
+
|
|
8
|
+
use super::FileFact;
|
|
9
|
+
use crate::models::NaomeError;
|
|
10
|
+
|
|
11
|
+
const CACHE_SCHEMA: &str = "naome.architecture-cache.v1";
|
|
12
|
+
const EXTRACTOR_VERSION: &str = "architecture-cache-v1.3.14";
|
|
13
|
+
const CACHE_PATH: &str = ".naome/cache/architecture/cache.json";
|
|
14
|
+
|
|
15
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
16
|
+
#[serde(rename_all = "camelCase")]
|
|
17
|
+
pub(super) struct ArchitectureCache {
|
|
18
|
+
pub schema: String,
|
|
19
|
+
pub extractor_version: String,
|
|
20
|
+
pub config_hash: String,
|
|
21
|
+
pub file_list_hash: String,
|
|
22
|
+
pub files: BTreeMap<String, CachedFileFact>,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
26
|
+
#[serde(rename_all = "camelCase")]
|
|
27
|
+
pub(super) struct CachedFileFact {
|
|
28
|
+
pub content_hash: String,
|
|
29
|
+
pub fact: FileFact,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl ArchitectureCache {
|
|
33
|
+
pub(super) fn is_compatible(&self, config_hash: &str, file_list_hash: &str) -> CacheStatus {
|
|
34
|
+
if self.schema != CACHE_SCHEMA || self.extractor_version != EXTRACTOR_VERSION {
|
|
35
|
+
return CacheStatus::Miss("cache_miss");
|
|
36
|
+
}
|
|
37
|
+
if self.config_hash != config_hash {
|
|
38
|
+
return CacheStatus::Miss("config_changed");
|
|
39
|
+
}
|
|
40
|
+
if self.file_list_hash != file_list_hash {
|
|
41
|
+
return CacheStatus::Miss("file_set_changed");
|
|
42
|
+
}
|
|
43
|
+
CacheStatus::Hit
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
48
|
+
pub(super) enum CacheStatus {
|
|
49
|
+
Hit,
|
|
50
|
+
Miss(&'static str),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pub(super) fn read(root: &Path) -> Option<ArchitectureCache> {
|
|
54
|
+
let content = fs::read_to_string(root.join(CACHE_PATH)).ok()?;
|
|
55
|
+
serde_json::from_str(&content).ok()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
pub(super) fn write(
|
|
59
|
+
root: &Path,
|
|
60
|
+
config_hash: String,
|
|
61
|
+
file_list_hash: String,
|
|
62
|
+
file_facts: &BTreeMap<String, FileFact>,
|
|
63
|
+
) -> Result<(), NaomeError> {
|
|
64
|
+
let cache = ArchitectureCache {
|
|
65
|
+
schema: CACHE_SCHEMA.to_string(),
|
|
66
|
+
extractor_version: EXTRACTOR_VERSION.to_string(),
|
|
67
|
+
config_hash,
|
|
68
|
+
file_list_hash,
|
|
69
|
+
files: file_facts
|
|
70
|
+
.iter()
|
|
71
|
+
.filter_map(|(path, fact)| {
|
|
72
|
+
let content_hash = content_hash(root, path).ok()?;
|
|
73
|
+
Some((
|
|
74
|
+
path.clone(),
|
|
75
|
+
CachedFileFact {
|
|
76
|
+
content_hash,
|
|
77
|
+
fact: fact.clone(),
|
|
78
|
+
},
|
|
79
|
+
))
|
|
80
|
+
})
|
|
81
|
+
.collect(),
|
|
82
|
+
};
|
|
83
|
+
let path = root.join(CACHE_PATH);
|
|
84
|
+
if let Some(parent) = path.parent() {
|
|
85
|
+
fs::create_dir_all(parent)?;
|
|
86
|
+
}
|
|
87
|
+
fs::write(path, serde_json::to_string_pretty(&cache)?)?;
|
|
88
|
+
Ok(())
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
pub(super) fn config_hash(
|
|
92
|
+
root: &Path,
|
|
93
|
+
explicit_path: Option<&Path>,
|
|
94
|
+
default_content: &str,
|
|
95
|
+
) -> Result<String, NaomeError> {
|
|
96
|
+
let path = explicit_path
|
|
97
|
+
.map(Path::to_path_buf)
|
|
98
|
+
.unwrap_or_else(|| root.join("naome.arch.yaml"));
|
|
99
|
+
let source = if path.exists() {
|
|
100
|
+
fs::read_to_string(&path)?
|
|
101
|
+
} else {
|
|
102
|
+
default_content.to_string()
|
|
103
|
+
};
|
|
104
|
+
let label = display_path(root, &path);
|
|
105
|
+
Ok(stable_hash([
|
|
106
|
+
b"architecture-config-v1".as_slice(),
|
|
107
|
+
label.as_bytes(),
|
|
108
|
+
source.as_bytes(),
|
|
109
|
+
]))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
pub(super) fn file_list_hash(files: &[String]) -> String {
|
|
113
|
+
let mut hasher = Sha256::new();
|
|
114
|
+
hasher.update(b"architecture-file-list-v1");
|
|
115
|
+
for path in files {
|
|
116
|
+
hasher.update([0]);
|
|
117
|
+
hasher.update(path.as_bytes());
|
|
118
|
+
}
|
|
119
|
+
format!("sha256:{:x}", hasher.finalize())
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
pub(super) fn content_hash(root: &Path, path: &str) -> Result<String, NaomeError> {
|
|
123
|
+
let bytes = fs::read(root.join(path))?;
|
|
124
|
+
Ok(stable_hash([b"architecture-file-v1".as_slice(), &bytes]))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
pub(super) fn changed_set(changed_paths: &[String]) -> BTreeSet<String> {
|
|
128
|
+
changed_paths.iter().cloned().collect()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fn stable_hash<'a>(parts: impl IntoIterator<Item = &'a [u8]>) -> String {
|
|
132
|
+
let mut hasher = Sha256::new();
|
|
133
|
+
for part in parts {
|
|
134
|
+
hasher.update(part);
|
|
135
|
+
hasher.update([0]);
|
|
136
|
+
}
|
|
137
|
+
format!("sha256:{:x}", hasher.finalize())
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fn display_path(root: &Path, path: &Path) -> String {
|
|
141
|
+
path.strip_prefix(root)
|
|
142
|
+
.unwrap_or(path)
|
|
143
|
+
.to_string_lossy()
|
|
144
|
+
.replace('\\', "/")
|
|
145
|
+
}
|
|
@@ -18,25 +18,46 @@ pub(super) fn build_path_graph(
|
|
|
18
18
|
config: &ArchitectureConfig,
|
|
19
19
|
manifests: &[ManifestFact],
|
|
20
20
|
) -> (ArchitectureGraph, BTreeMap<String, FileFact>) {
|
|
21
|
-
let mut graph = ArchitectureGraph::default();
|
|
22
21
|
let mut file_facts = BTreeMap::new();
|
|
23
|
-
let mut emitted_external_nodes = BTreeSet::new();
|
|
24
22
|
let file_set = files.iter().cloned().collect::<BTreeSet<_>>();
|
|
23
|
+
for path in &files {
|
|
24
|
+
file_facts.insert(path.clone(), scan_file_fact(root, path, config, &file_set));
|
|
25
|
+
}
|
|
26
|
+
let graph = build_graph_from_facts(&files, &file_facts, config, manifests);
|
|
27
|
+
(graph, file_facts)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pub(super) fn scan_file_fact(
|
|
31
|
+
root: &Path,
|
|
32
|
+
path: &str,
|
|
33
|
+
config: &ArchitectureConfig,
|
|
34
|
+
file_set: &BTreeSet<String>,
|
|
35
|
+
) -> FileFact {
|
|
36
|
+
let content = fs::read_to_string(root.join(path)).unwrap_or_default();
|
|
37
|
+
let mut fact = facts::file_fact(path, &content, config);
|
|
38
|
+
fact.imports = imports::extract_imports(root, path, &content, file_set);
|
|
39
|
+
fact
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub(super) fn build_graph_from_facts(
|
|
43
|
+
files: &[String],
|
|
44
|
+
file_facts: &BTreeMap<String, FileFact>,
|
|
45
|
+
config: &ArchitectureConfig,
|
|
46
|
+
manifests: &[ManifestFact],
|
|
47
|
+
) -> ArchitectureGraph {
|
|
48
|
+
let mut graph = ArchitectureGraph::default();
|
|
49
|
+
let mut emitted_external_nodes = BTreeSet::new();
|
|
25
50
|
|
|
26
51
|
push_repository_and_policy_nodes(&mut graph, config);
|
|
27
|
-
push_directories(&mut graph,
|
|
52
|
+
push_directories(&mut graph, files);
|
|
28
53
|
push_manifests(&mut graph, manifests, &mut emitted_external_nodes);
|
|
29
54
|
|
|
30
|
-
for
|
|
31
|
-
let content = fs::read_to_string(root.join(&path)).unwrap_or_default();
|
|
32
|
-
let mut fact = facts::file_fact(&path, &content, config);
|
|
33
|
-
fact.imports = imports::extract_imports(root, &path, &content, &file_set);
|
|
55
|
+
for fact in file_facts.values() {
|
|
34
56
|
push_file(&mut graph, &fact);
|
|
35
|
-
file_facts.insert(path, fact);
|
|
36
57
|
}
|
|
37
58
|
|
|
38
|
-
push_imports(&mut graph,
|
|
39
|
-
|
|
59
|
+
push_imports(&mut graph, file_facts, &mut emitted_external_nodes);
|
|
60
|
+
graph
|
|
40
61
|
}
|
|
41
62
|
|
|
42
63
|
fn push_repository_and_policy_nodes(graph: &mut ArchitectureGraph, config: &ArchitectureConfig) {
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
use std::collections::BTreeMap;
|
|
2
2
|
use std::path::{Path, PathBuf};
|
|
3
3
|
|
|
4
|
-
use serde::Serialize;
|
|
4
|
+
use serde::{Deserialize, Serialize};
|
|
5
5
|
|
|
6
6
|
use crate::git;
|
|
7
7
|
use crate::models::NaomeError;
|
|
8
8
|
|
|
9
|
-
use
|
|
9
|
+
use cache::CacheStatus;
|
|
10
|
+
|
|
11
|
+
use super::config::{
|
|
12
|
+
default_architecture_config_text, read_architecture_config, ArchitectureConfig,
|
|
13
|
+
};
|
|
10
14
|
use super::model::ArchitectureGraph;
|
|
11
15
|
|
|
16
|
+
mod cache;
|
|
12
17
|
mod graph_builder;
|
|
13
18
|
mod imports;
|
|
14
19
|
mod manifest;
|
|
@@ -28,6 +33,8 @@ pub struct ArchitectureScanReport {
|
|
|
28
33
|
pub files_scanned: usize,
|
|
29
34
|
pub changed_only_requested: bool,
|
|
30
35
|
pub changed_only_degraded_to_full_scan: bool,
|
|
36
|
+
pub changed_only_mode: String,
|
|
37
|
+
pub changed_only_degradation_reason: Option<String>,
|
|
31
38
|
pub changed_paths: Vec<String>,
|
|
32
39
|
#[serde(skip_serializing)]
|
|
33
40
|
pub config: ArchitectureConfig,
|
|
@@ -37,7 +44,7 @@ pub struct ArchitectureScanReport {
|
|
|
37
44
|
pub manifest_facts: Vec<ManifestFact>,
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
#[derive(Debug, Clone, Serialize)]
|
|
47
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
41
48
|
#[serde(rename_all = "camelCase")]
|
|
42
49
|
pub struct FileFact {
|
|
43
50
|
pub path: String,
|
|
@@ -49,7 +56,7 @@ pub struct FileFact {
|
|
|
49
56
|
pub imports: Vec<ImportFact>,
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
#[derive(Debug, Clone, Serialize)]
|
|
59
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
53
60
|
#[serde(rename_all = "camelCase")]
|
|
54
61
|
pub struct ImportFact {
|
|
55
62
|
pub specifier: String,
|
|
@@ -59,7 +66,7 @@ pub struct ImportFact {
|
|
|
59
66
|
pub extractor: String,
|
|
60
67
|
}
|
|
61
68
|
|
|
62
|
-
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
|
69
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
63
70
|
#[serde(rename_all = "camelCase")]
|
|
64
71
|
pub enum ImportTarget {
|
|
65
72
|
File(String),
|
|
@@ -67,7 +74,7 @@ pub enum ImportTarget {
|
|
|
67
74
|
Unknown(String),
|
|
68
75
|
}
|
|
69
76
|
|
|
70
|
-
#[derive(Debug, Clone, Serialize)]
|
|
77
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
71
78
|
#[serde(rename_all = "camelCase")]
|
|
72
79
|
pub struct ManifestFact {
|
|
73
80
|
pub path: String,
|
|
@@ -78,7 +85,7 @@ pub struct ManifestFact {
|
|
|
78
85
|
pub extractor: String,
|
|
79
86
|
}
|
|
80
87
|
|
|
81
|
-
#[derive(Debug, Clone, Serialize)]
|
|
88
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
82
89
|
#[serde(rename_all = "camelCase")]
|
|
83
90
|
pub struct ManifestDependency {
|
|
84
91
|
pub name: String,
|
|
@@ -94,24 +101,235 @@ pub fn scan_architecture(
|
|
|
94
101
|
let config = read_architecture_config(root, options.config_path.as_deref())?;
|
|
95
102
|
let changed_paths = changed_paths(root, options.changed_only)?;
|
|
96
103
|
let files = path_scan::repository_files(root, &config)?;
|
|
104
|
+
let config_hash = cache::config_hash(
|
|
105
|
+
root,
|
|
106
|
+
options.config_path.as_deref(),
|
|
107
|
+
default_architecture_config_text(),
|
|
108
|
+
)?;
|
|
109
|
+
let file_list_hash = cache::file_list_hash(&files);
|
|
97
110
|
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);
|
|
100
111
|
|
|
112
|
+
let scan = if options.changed_only {
|
|
113
|
+
changed_only_scan(
|
|
114
|
+
root,
|
|
115
|
+
files,
|
|
116
|
+
&config,
|
|
117
|
+
&manifest_facts,
|
|
118
|
+
&changed_paths,
|
|
119
|
+
config_hash,
|
|
120
|
+
file_list_hash,
|
|
121
|
+
)?
|
|
122
|
+
} else {
|
|
123
|
+
full_scan(
|
|
124
|
+
root,
|
|
125
|
+
files,
|
|
126
|
+
&config,
|
|
127
|
+
&manifest_facts,
|
|
128
|
+
config_hash,
|
|
129
|
+
file_list_hash,
|
|
130
|
+
false,
|
|
131
|
+
None,
|
|
132
|
+
"full_scan",
|
|
133
|
+
)?
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
Ok(scan.with_changed_paths(changed_paths))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fn changed_only_scan(
|
|
140
|
+
root: &Path,
|
|
141
|
+
files: Vec<String>,
|
|
142
|
+
config: &ArchitectureConfig,
|
|
143
|
+
manifest_facts: &[ManifestFact],
|
|
144
|
+
changed_paths: &[String],
|
|
145
|
+
config_hash: String,
|
|
146
|
+
file_list_hash: String,
|
|
147
|
+
) -> Result<ArchitectureScanReport, NaomeError> {
|
|
148
|
+
let Some(cache) = cache::read(root) else {
|
|
149
|
+
return degraded_full_scan(
|
|
150
|
+
root,
|
|
151
|
+
files,
|
|
152
|
+
config,
|
|
153
|
+
manifest_facts,
|
|
154
|
+
config_hash,
|
|
155
|
+
file_list_hash,
|
|
156
|
+
"cache_miss",
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
match cache.is_compatible(&config_hash, &file_list_hash) {
|
|
160
|
+
CacheStatus::Miss(reason) => degraded_full_scan(
|
|
161
|
+
root,
|
|
162
|
+
files,
|
|
163
|
+
config,
|
|
164
|
+
manifest_facts,
|
|
165
|
+
config_hash,
|
|
166
|
+
file_list_hash,
|
|
167
|
+
reason,
|
|
168
|
+
),
|
|
169
|
+
CacheStatus::Hit => incremental_scan(
|
|
170
|
+
root,
|
|
171
|
+
files,
|
|
172
|
+
config,
|
|
173
|
+
manifest_facts,
|
|
174
|
+
changed_paths,
|
|
175
|
+
config_hash,
|
|
176
|
+
file_list_hash,
|
|
177
|
+
cache,
|
|
178
|
+
),
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fn incremental_scan(
|
|
183
|
+
root: &Path,
|
|
184
|
+
files: Vec<String>,
|
|
185
|
+
config: &ArchitectureConfig,
|
|
186
|
+
manifest_facts: &[ManifestFact],
|
|
187
|
+
changed_paths: &[String],
|
|
188
|
+
config_hash: String,
|
|
189
|
+
file_list_hash: String,
|
|
190
|
+
cache: cache::ArchitectureCache,
|
|
191
|
+
) -> Result<ArchitectureScanReport, NaomeError> {
|
|
192
|
+
let file_set = files.iter().cloned().collect();
|
|
193
|
+
let changed = cache::changed_set(changed_paths);
|
|
194
|
+
if import_resolver_context_changed(changed_paths) {
|
|
195
|
+
return degraded_full_scan(
|
|
196
|
+
root,
|
|
197
|
+
files,
|
|
198
|
+
config,
|
|
199
|
+
manifest_facts,
|
|
200
|
+
config_hash,
|
|
201
|
+
file_list_hash,
|
|
202
|
+
"resolver_context_changed",
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if cached_clean_content_changed(root, &files, &changed, &cache) {
|
|
206
|
+
return degraded_full_scan(
|
|
207
|
+
root,
|
|
208
|
+
files,
|
|
209
|
+
config,
|
|
210
|
+
manifest_facts,
|
|
211
|
+
config_hash,
|
|
212
|
+
file_list_hash,
|
|
213
|
+
"content_changed",
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
let mut file_facts = BTreeMap::new();
|
|
217
|
+
let mut scanned = 0;
|
|
218
|
+
|
|
219
|
+
for path in &files {
|
|
220
|
+
if changed.contains(path) || !cache.files.contains_key(path) {
|
|
221
|
+
let fact = graph_builder::scan_file_fact(root, path, config, &file_set);
|
|
222
|
+
scanned += 1;
|
|
223
|
+
file_facts.insert(path.clone(), fact);
|
|
224
|
+
} else if let Some(cached) = cache.files.get(path) {
|
|
225
|
+
file_facts.insert(path.clone(), cached.fact.clone());
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let mut graph =
|
|
230
|
+
graph_builder::build_graph_from_facts(&files, &file_facts, config, manifest_facts);
|
|
101
231
|
graph.sort_stable();
|
|
232
|
+
let _ = cache::write(root, config_hash, file_list_hash, &file_facts);
|
|
102
233
|
Ok(ArchitectureScanReport {
|
|
103
234
|
schema: "naome.arch.scan.v1".to_string(),
|
|
104
|
-
files_scanned:
|
|
235
|
+
files_scanned: scanned,
|
|
105
236
|
graph,
|
|
106
|
-
changed_only_requested:
|
|
107
|
-
changed_only_degraded_to_full_scan:
|
|
108
|
-
|
|
109
|
-
|
|
237
|
+
changed_only_requested: true,
|
|
238
|
+
changed_only_degraded_to_full_scan: false,
|
|
239
|
+
changed_only_mode: "incremental_cache".to_string(),
|
|
240
|
+
changed_only_degradation_reason: None,
|
|
241
|
+
changed_paths: Vec::new(),
|
|
242
|
+
config: config.clone(),
|
|
110
243
|
file_facts,
|
|
244
|
+
manifest_facts: manifest_facts.to_vec(),
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
fn cached_clean_content_changed(
|
|
249
|
+
root: &Path,
|
|
250
|
+
files: &[String],
|
|
251
|
+
changed: &std::collections::BTreeSet<String>,
|
|
252
|
+
cache: &cache::ArchitectureCache,
|
|
253
|
+
) -> bool {
|
|
254
|
+
files.iter().any(|path| {
|
|
255
|
+
!changed.contains(path)
|
|
256
|
+
&& cache.files.get(path).is_some_and(|cached| {
|
|
257
|
+
cache::content_hash(root, path)
|
|
258
|
+
.map(|current| current != cached.content_hash)
|
|
259
|
+
.unwrap_or(true)
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
fn import_resolver_context_changed(changed_paths: &[String]) -> bool {
|
|
265
|
+
changed_paths.iter().any(|path| {
|
|
266
|
+
Path::new(path)
|
|
267
|
+
.file_name()
|
|
268
|
+
.and_then(|value| value.to_str())
|
|
269
|
+
.is_some_and(|name| matches!(name, "go.mod" | "tsconfig.json" | "jsconfig.json"))
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
fn degraded_full_scan(
|
|
274
|
+
root: &Path,
|
|
275
|
+
files: Vec<String>,
|
|
276
|
+
config: &ArchitectureConfig,
|
|
277
|
+
manifest_facts: &[ManifestFact],
|
|
278
|
+
config_hash: String,
|
|
279
|
+
file_list_hash: String,
|
|
280
|
+
reason: &'static str,
|
|
281
|
+
) -> Result<ArchitectureScanReport, NaomeError> {
|
|
282
|
+
full_scan(
|
|
283
|
+
root,
|
|
284
|
+
files,
|
|
285
|
+
config,
|
|
111
286
|
manifest_facts,
|
|
287
|
+
config_hash,
|
|
288
|
+
file_list_hash,
|
|
289
|
+
true,
|
|
290
|
+
Some(reason),
|
|
291
|
+
"degraded_full_scan",
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
fn full_scan(
|
|
296
|
+
root: &Path,
|
|
297
|
+
files: Vec<String>,
|
|
298
|
+
config: &ArchitectureConfig,
|
|
299
|
+
manifest_facts: &[ManifestFact],
|
|
300
|
+
config_hash: String,
|
|
301
|
+
file_list_hash: String,
|
|
302
|
+
changed_only_degraded_to_full_scan: bool,
|
|
303
|
+
changed_only_degradation_reason: Option<&str>,
|
|
304
|
+
changed_only_mode: &str,
|
|
305
|
+
) -> Result<ArchitectureScanReport, NaomeError> {
|
|
306
|
+
let (mut graph, file_facts) =
|
|
307
|
+
graph_builder::build_path_graph(root, files, config, manifest_facts);
|
|
308
|
+
graph.sort_stable();
|
|
309
|
+
let files_scanned = file_facts.len();
|
|
310
|
+
let _ = cache::write(root, config_hash, file_list_hash, &file_facts);
|
|
311
|
+
Ok(ArchitectureScanReport {
|
|
312
|
+
schema: "naome.arch.scan.v1".to_string(),
|
|
313
|
+
files_scanned,
|
|
314
|
+
graph,
|
|
315
|
+
changed_only_requested: changed_only_degraded_to_full_scan,
|
|
316
|
+
changed_only_degraded_to_full_scan,
|
|
317
|
+
changed_only_mode: changed_only_mode.to_string(),
|
|
318
|
+
changed_only_degradation_reason: changed_only_degradation_reason.map(str::to_string),
|
|
319
|
+
changed_paths: Vec::new(),
|
|
320
|
+
config: config.clone(),
|
|
321
|
+
file_facts,
|
|
322
|
+
manifest_facts: manifest_facts.to_vec(),
|
|
112
323
|
})
|
|
113
324
|
}
|
|
114
325
|
|
|
326
|
+
impl ArchitectureScanReport {
|
|
327
|
+
fn with_changed_paths(mut self, changed_paths: Vec<String>) -> Self {
|
|
328
|
+
self.changed_paths = changed_paths;
|
|
329
|
+
self
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
115
333
|
fn changed_paths(root: &Path, changed_only: bool) -> Result<Vec<String>, NaomeError> {
|
|
116
334
|
if changed_only {
|
|
117
335
|
git::changed_paths(root)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
use naome_core::{validate_architecture, ArchitectureScanOptions};
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
use std::process::Command;
|
|
4
|
+
|
|
5
|
+
mod architecture_support;
|
|
6
|
+
|
|
7
|
+
use architecture_support::FixtureRepo;
|
|
8
|
+
|
|
9
|
+
#[test]
|
|
10
|
+
fn changed_only_uses_incremental_cache_after_initial_safe_full_scan() {
|
|
11
|
+
let repo = FixtureRepo::new();
|
|
12
|
+
repo.write(
|
|
13
|
+
"naome.arch.yaml",
|
|
14
|
+
forbidden_domain_to_infrastructure_config(),
|
|
15
|
+
);
|
|
16
|
+
repo.write("src/domain/event.ts", "export const event = 1;\n");
|
|
17
|
+
repo.write("src/infrastructure/db.ts", "export const db = 1;\n");
|
|
18
|
+
repo.init_git();
|
|
19
|
+
|
|
20
|
+
let initial = validate_architecture(repo.path(), changed_only()).unwrap();
|
|
21
|
+
|
|
22
|
+
assert!(initial.changed_only_requested);
|
|
23
|
+
assert!(initial.changed_only_degraded_to_full_scan);
|
|
24
|
+
assert_eq!(initial.changed_only_mode, "degraded_full_scan");
|
|
25
|
+
assert_eq!(
|
|
26
|
+
initial.changed_only_degradation_reason.as_deref(),
|
|
27
|
+
Some("cache_miss")
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
repo.write(
|
|
31
|
+
"src/domain/event.ts",
|
|
32
|
+
"import { db } from '../infrastructure/db';\nexport const event = db;\n",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
let incremental = validate_architecture(repo.path(), changed_only()).unwrap();
|
|
36
|
+
|
|
37
|
+
assert!(incremental.changed_only_requested);
|
|
38
|
+
assert!(!incremental.changed_only_degraded_to_full_scan);
|
|
39
|
+
assert_eq!(incremental.changed_only_mode, "incremental_cache");
|
|
40
|
+
assert_eq!(incremental.changed_only_degradation_reason, None);
|
|
41
|
+
assert_eq!(incremental.files_scanned, 1);
|
|
42
|
+
assert_eq!(incremental.summary.errors, 1);
|
|
43
|
+
assert_eq!(
|
|
44
|
+
incremental.violations[0].path.as_deref(),
|
|
45
|
+
Some("src/domain/event.ts")
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#[test]
|
|
50
|
+
fn changed_only_degrades_when_architecture_config_changes() {
|
|
51
|
+
let repo = FixtureRepo::new();
|
|
52
|
+
repo.write(
|
|
53
|
+
"naome.arch.yaml",
|
|
54
|
+
forbidden_domain_to_infrastructure_config(),
|
|
55
|
+
);
|
|
56
|
+
repo.write("src/domain/event.ts", "export const event = 1;\n");
|
|
57
|
+
repo.write("src/infrastructure/db.ts", "export const db = 1;\n");
|
|
58
|
+
repo.init_git();
|
|
59
|
+
validate_architecture(repo.path(), changed_only()).unwrap();
|
|
60
|
+
|
|
61
|
+
repo.write(
|
|
62
|
+
"naome.arch.yaml",
|
|
63
|
+
"layers:\n application:\n paths:\n - \"src/**\"\n",
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
let report = validate_architecture(repo.path(), changed_only()).unwrap();
|
|
67
|
+
|
|
68
|
+
assert!(report.changed_only_degraded_to_full_scan);
|
|
69
|
+
assert_eq!(report.changed_only_mode, "degraded_full_scan");
|
|
70
|
+
assert_eq!(
|
|
71
|
+
report.changed_only_degradation_reason.as_deref(),
|
|
72
|
+
Some("config_changed")
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[test]
|
|
77
|
+
fn changed_only_degrades_when_repository_file_set_changes() {
|
|
78
|
+
let repo = FixtureRepo::new();
|
|
79
|
+
repo.write(
|
|
80
|
+
"naome.arch.yaml",
|
|
81
|
+
forbidden_domain_to_infrastructure_config(),
|
|
82
|
+
);
|
|
83
|
+
repo.write("src/domain/event.ts", "export const event = 1;\n");
|
|
84
|
+
repo.write("src/infrastructure/db.ts", "export const db = 1;\n");
|
|
85
|
+
repo.init_git();
|
|
86
|
+
validate_architecture(repo.path(), changed_only()).unwrap();
|
|
87
|
+
|
|
88
|
+
repo.write("src/domain/new.ts", "export const fresh = 1;\n");
|
|
89
|
+
|
|
90
|
+
let report = validate_architecture(repo.path(), changed_only()).unwrap();
|
|
91
|
+
|
|
92
|
+
assert!(report.changed_only_degraded_to_full_scan);
|
|
93
|
+
assert_eq!(report.changed_only_mode, "degraded_full_scan");
|
|
94
|
+
assert_eq!(
|
|
95
|
+
report.changed_only_degradation_reason.as_deref(),
|
|
96
|
+
Some("file_set_changed")
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[test]
|
|
101
|
+
fn changed_only_degrades_when_cached_clean_file_content_changed() {
|
|
102
|
+
let repo = FixtureRepo::new();
|
|
103
|
+
repo.write(
|
|
104
|
+
"naome.arch.yaml",
|
|
105
|
+
forbidden_domain_to_infrastructure_config(),
|
|
106
|
+
);
|
|
107
|
+
repo.write("src/domain/event.ts", "export const event = 1;\n");
|
|
108
|
+
repo.write("src/infrastructure/db.ts", "export const db = 1;\n");
|
|
109
|
+
repo.init_git();
|
|
110
|
+
validate_architecture(repo.path(), changed_only()).unwrap();
|
|
111
|
+
|
|
112
|
+
repo.write(
|
|
113
|
+
"src/domain/event.ts",
|
|
114
|
+
"import { db } from '../infrastructure/db';\nexport const event = db;\n",
|
|
115
|
+
);
|
|
116
|
+
commit_all(repo.path(), "content change after cache");
|
|
117
|
+
|
|
118
|
+
let report = validate_architecture(repo.path(), changed_only()).unwrap();
|
|
119
|
+
|
|
120
|
+
assert!(report.changed_only_requested);
|
|
121
|
+
assert!(report.changed_only_degraded_to_full_scan);
|
|
122
|
+
assert_eq!(report.changed_only_mode, "degraded_full_scan");
|
|
123
|
+
assert_eq!(
|
|
124
|
+
report.changed_only_degradation_reason.as_deref(),
|
|
125
|
+
Some("content_changed")
|
|
126
|
+
);
|
|
127
|
+
assert_eq!(report.summary.errors, 1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#[test]
|
|
131
|
+
fn changed_only_degrades_when_go_module_context_changes() {
|
|
132
|
+
let repo = FixtureRepo::new();
|
|
133
|
+
repo.write(
|
|
134
|
+
"naome.arch.yaml",
|
|
135
|
+
forbidden_domain_to_infrastructure_config(),
|
|
136
|
+
);
|
|
137
|
+
repo.write("go.mod", "module example.com/old\n\ngo 1.22\n");
|
|
138
|
+
repo.write(
|
|
139
|
+
"src/domain/event.go",
|
|
140
|
+
"package domain\n\nimport \"example.com/new/src/infrastructure/db\"\n\nvar Event = db.Value\n",
|
|
141
|
+
);
|
|
142
|
+
repo.write(
|
|
143
|
+
"src/infrastructure/db/db.go",
|
|
144
|
+
"package db\n\nvar Value = 1\n",
|
|
145
|
+
);
|
|
146
|
+
repo.init_git();
|
|
147
|
+
let initial = validate_architecture(repo.path(), changed_only()).unwrap();
|
|
148
|
+
assert_eq!(initial.summary.errors, 0);
|
|
149
|
+
|
|
150
|
+
repo.write("go.mod", "module example.com/new\n\ngo 1.22\n");
|
|
151
|
+
|
|
152
|
+
let report = validate_architecture(repo.path(), changed_only()).unwrap();
|
|
153
|
+
|
|
154
|
+
assert!(report.changed_only_requested);
|
|
155
|
+
assert!(report.changed_only_degraded_to_full_scan);
|
|
156
|
+
assert_eq!(report.changed_only_mode, "degraded_full_scan");
|
|
157
|
+
assert_eq!(
|
|
158
|
+
report.changed_only_degradation_reason.as_deref(),
|
|
159
|
+
Some("resolver_context_changed")
|
|
160
|
+
);
|
|
161
|
+
assert_eq!(report.summary.errors, 1);
|
|
162
|
+
assert_eq!(
|
|
163
|
+
report.violations[0].path.as_deref(),
|
|
164
|
+
Some("src/domain/event.go")
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fn changed_only() -> ArchitectureScanOptions {
|
|
169
|
+
ArchitectureScanOptions {
|
|
170
|
+
changed_only: true,
|
|
171
|
+
..ArchitectureScanOptions::default()
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
fn forbidden_domain_to_infrastructure_config() -> &'static str {
|
|
176
|
+
r#"layers:
|
|
177
|
+
domain:
|
|
178
|
+
paths:
|
|
179
|
+
- "src/domain/**"
|
|
180
|
+
infrastructure:
|
|
181
|
+
paths:
|
|
182
|
+
- "src/infrastructure/**"
|
|
183
|
+
allowed_dependencies:
|
|
184
|
+
domain:
|
|
185
|
+
infrastructure:
|
|
186
|
+
- domain
|
|
187
|
+
rules:
|
|
188
|
+
no_forbidden_layer_dependencies:
|
|
189
|
+
enabled: true
|
|
190
|
+
severity: error
|
|
191
|
+
"#
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fn commit_all(root: &Path, message: &str) {
|
|
195
|
+
run_git(root, &["add", "."]);
|
|
196
|
+
run_git(root, &["commit", "-m", message]);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
fn run_git(root: &Path, args: &[&str]) {
|
|
200
|
+
let output = Command::new("git")
|
|
201
|
+
.args(args)
|
|
202
|
+
.current_dir(root)
|
|
203
|
+
.output()
|
|
204
|
+
.unwrap();
|
|
205
|
+
assert!(
|
|
206
|
+
output.status.success(),
|
|
207
|
+
"git {:?} failed: {}{}",
|
|
208
|
+
args,
|
|
209
|
+
String::from_utf8_lossy(&output.stdout),
|
|
210
|
+
String::from_utf8_lossy(&output.stderr)
|
|
211
|
+
);
|
|
212
|
+
}
|
|
Binary file
|
package/native/linux-x64/naome
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
".naome/task-contract.schema.json": "sha256:1b3b62350328d0d6d660e36d1d1baaa2b88718530db774f9ab2a9e2fcba369c8",
|
|
10
10
|
"AGENTS.md": "sha256:e8b2fc786c1c72b69ba8f2b2ffce4f459e799c7453ce9ff4a9f6448a8f9e6b4f",
|
|
11
11
|
"docs/naome/agent-workflow.md": "sha256:0be1c29adfbcd3fd73c4f904080ffc67237692fe413871a30243538c4db38ac7",
|
|
12
|
-
"docs/naome/architecture-fitness.md": "sha256:
|
|
12
|
+
"docs/naome/architecture-fitness.md": "sha256:88aa979f61f82c2802c416af2c374ba041c94157189d59048d6889b07034d394",
|
|
13
13
|
"docs/naome/context-economy.md": "sha256:3ed5075815ecf4ada46a5e65438769310307c35759fcd46b13dc0b96e02bebd9",
|
|
14
14
|
"docs/naome/execution.md": "sha256:bfc5d55838942ec8e3d790b59e3c634ff5bf6a2298265cef3dca9788a097eafb",
|
|
15
15
|
"docs/naome/first-run.md": "sha256:1466ce8c65e19a1514885f917db14e8a772350e3f6d1c03a66326963365919e1",
|
|
@@ -14,8 +14,9 @@ facts instead of prompt text.
|
|
|
14
14
|
- `naome arch validate` runs architecture rules with human output.
|
|
15
15
|
- `naome arch validate --json` emits stable machine-readable output.
|
|
16
16
|
- `naome arch validate --agent-feedback` emits compact repair instructions.
|
|
17
|
-
- `naome arch validate --changed-only` uses changed paths
|
|
18
|
-
safely degrades to a full scan
|
|
17
|
+
- `naome arch validate --changed-only` uses changed paths with the persistent
|
|
18
|
+
architecture cache when available and safely degrades to a full scan when the
|
|
19
|
+
cache cannot prove graph-level soundness.
|
|
19
20
|
|
|
20
21
|
## Graph Model
|
|
21
22
|
|
|
@@ -127,17 +128,37 @@ ignore:
|
|
|
127
128
|
Agents should run `naome arch validate --changed-only --json` after changing
|
|
128
129
|
source or template files. JSON output includes status, severity counts,
|
|
129
130
|
violations, concrete suggestions, and `agentFeedback` entries optimized for
|
|
130
|
-
repair loops.
|
|
131
|
+
repair loops. It also reports `changedOnlyMode` and
|
|
132
|
+
`changedOnlyDegradationReason`, so agents can distinguish an incremental cache
|
|
133
|
+
run from a deliberate full-scan fallback.
|
|
134
|
+
|
|
135
|
+
## Incremental Cache
|
|
136
|
+
|
|
137
|
+
NAOME stores architecture file facts in `.naome/cache/architecture/cache.json`.
|
|
138
|
+
Cache entries are keyed by file path and content hash, and the cache is tied to
|
|
139
|
+
the effective architecture config hash plus the repository file-list hash.
|
|
140
|
+
|
|
141
|
+
Changed-only validation is sound by construction:
|
|
142
|
+
|
|
143
|
+
- the first run without a compatible cache performs a full scan and records
|
|
144
|
+
`changedOnlyMode: "degraded_full_scan"` with reason `cache_miss`;
|
|
145
|
+
- config changes perform a full scan with reason `config_changed`;
|
|
146
|
+
- added or deleted repository files perform a full scan with reason
|
|
147
|
+
`file_set_changed`;
|
|
148
|
+
- when config and file set are unchanged, NAOME reuses cached facts for
|
|
149
|
+
untouched files, rescans changed files, rebuilds the full graph, and runs all
|
|
150
|
+
graph-level rules against that graph.
|
|
131
151
|
|
|
132
152
|
## CI Integration
|
|
133
153
|
|
|
134
154
|
Use `node .naome/bin/naome.js arch validate --changed-only` as a fast gate.
|
|
135
|
-
The
|
|
136
|
-
|
|
155
|
+
The command is deterministic in both cache-backed and fallback modes; CI can
|
|
156
|
+
inspect the JSON fields above when it needs to distinguish performance from
|
|
157
|
+
soundness fallbacks.
|
|
137
158
|
|
|
138
159
|
## Language Support
|
|
139
160
|
|
|
140
|
-
The v1.3.
|
|
161
|
+
The v1.3.14 foundation classifies TypeScript, JavaScript, Rust, Python, Go,
|
|
141
162
|
Java, Kotlin, and Swift files by path extension. It extracts import facts for
|
|
142
163
|
TypeScript, JavaScript, Rust, Python, Go, and Swift, resolves relative imports
|
|
143
164
|
and simple repository-absolute aliases where the language supports them, and
|
|
@@ -158,5 +179,5 @@ policies.
|
|
|
158
179
|
|
|
159
180
|
This release intentionally keeps validation file-graph based. Manifest
|
|
160
181
|
extractors are dependency-owner oriented and do not yet parse every build-tool
|
|
161
|
-
feature. SARIF output
|
|
162
|
-
|
|
182
|
+
feature. SARIF output and deeper symbol-level call analysis remain planned
|
|
183
|
+
follow-up slices before v1.4.0.
|