@lamentis/naome 1.2.1 → 1.3.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/README.md +108 -47
- package/bin/naome.js +16 -1
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/dispatcher.rs +6 -2
- package/crates/naome-cli/src/main.rs +35 -23
- package/crates/naome-cli/src/quality_commands.rs +230 -11
- package/crates/naome-cli/src/workflow_commands.rs +21 -1
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/git.rs +4 -2
- package/crates/naome-core/src/install_plan.rs +2 -0
- package/crates/naome-core/src/lib.rs +11 -7
- package/crates/naome-core/src/quality/baseline.rs +8 -0
- package/crates/naome-core/src/quality/cache.rs +153 -0
- package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +25 -11
- package/crates/naome-core/src/quality/checks/near_duplicates.rs +4 -2
- package/crates/naome-core/src/quality/checks.rs +7 -8
- package/crates/naome-core/src/quality/cleanup.rs +36 -3
- package/crates/naome-core/src/quality/mod.rs +57 -9
- package/crates/naome-core/src/quality/scanner/analysis/normalize.rs +78 -0
- package/crates/naome-core/src/quality/scanner/analysis.rs +160 -0
- package/crates/naome-core/src/quality/scanner/repo_paths.rs +39 -3
- package/crates/naome-core/src/quality/scanner.rs +193 -220
- package/crates/naome-core/src/quality/semantic/checks.rs +134 -0
- package/crates/naome-core/src/quality/semantic/extract.rs +158 -0
- package/crates/naome-core/src/quality/semantic/model.rs +85 -0
- package/crates/naome-core/src/quality/semantic/route.rs +52 -0
- package/crates/naome-core/src/quality/semantic.rs +68 -0
- package/crates/naome-core/src/quality/structure/checks/directory.rs +9 -19
- package/crates/naome-core/src/quality/structure/checks.rs +1 -1
- package/crates/naome-core/src/quality/structure/classify.rs +52 -0
- package/crates/naome-core/src/quality/structure/mod.rs +2 -2
- package/crates/naome-core/src/quality/structure/model.rs +8 -1
- package/crates/naome-core/src/quality/types.rs +40 -2
- package/crates/naome-core/src/route/builtin_checks.rs +1 -15
- package/crates/naome-core/src/workflow/doctor.rs +144 -0
- package/crates/naome-core/src/workflow/mod.rs +2 -0
- package/crates/naome-core/src/workflow/mutation.rs +1 -2
- package/crates/naome-core/tests/install_plan.rs +2 -0
- package/crates/naome-core/tests/quality.rs +14 -5
- package/crates/naome-core/tests/quality_performance.rs +231 -0
- package/crates/naome-core/tests/quality_structure_policy.rs +19 -0
- package/crates/naome-core/tests/route_user_diff.rs +10 -6
- package/crates/naome-core/tests/semantic_legacy.rs +140 -0
- package/crates/naome-core/tests/workflow_doctor.rs +24 -0
- package/crates/naome-core/tests/workflow_policy.rs +6 -1
- package/installer/git-boundary.js +1 -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 +2 -2
- package/templates/naome-root/.naome/bin/check-task-state.js +2 -2
- package/templates/naome-root/.naome/bin/naome.js +11 -4
- package/templates/naome-root/.naome/manifest.json +2 -2
- package/templates/naome-root/.naomeignore +1 -0
- package/templates/naome-root/docs/naome/agent-workflow.md +16 -14
- package/templates/naome-root/docs/naome/repository-quality.md +63 -4
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
use std::collections::BTreeSet;
|
|
2
|
+
|
|
3
|
+
use super::model::ObjectCandidate;
|
|
4
|
+
use crate::quality::scanner::{stable_fingerprint, FileAnalysis, SymbolAnalysis};
|
|
5
|
+
|
|
6
|
+
pub(super) fn extract_object_candidates(file: &FileAnalysis) -> Vec<ObjectCandidate> {
|
|
7
|
+
let mut candidates = Vec::new();
|
|
8
|
+
let mut start: Option<usize> = None;
|
|
9
|
+
let mut depth = 0isize;
|
|
10
|
+
|
|
11
|
+
for (index, line) in file.raw_lines.iter().enumerate() {
|
|
12
|
+
let delta = brace_delta(line);
|
|
13
|
+
if start.is_none() && line.contains('{') && looks_like_object_start(line) {
|
|
14
|
+
start = Some(index);
|
|
15
|
+
}
|
|
16
|
+
if start.is_some() {
|
|
17
|
+
depth += delta;
|
|
18
|
+
if depth <= 0 {
|
|
19
|
+
if let Some(start_index) = start.take() {
|
|
20
|
+
push_candidate(file, start_index, index, &mut candidates);
|
|
21
|
+
}
|
|
22
|
+
depth = 0;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
candidates
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fn push_candidate(
|
|
31
|
+
file: &FileAnalysis,
|
|
32
|
+
start_index: usize,
|
|
33
|
+
end_index: usize,
|
|
34
|
+
candidates: &mut Vec<ObjectCandidate>,
|
|
35
|
+
) {
|
|
36
|
+
let line_count = end_index.saturating_sub(start_index) + 1;
|
|
37
|
+
if line_count < 8 {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
let keys = extract_keys(&file.raw_lines[start_index..=end_index]);
|
|
41
|
+
if keys.len() < 3 {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
let shape = keys.iter().cloned().collect::<Vec<_>>().join("|");
|
|
45
|
+
let shape_hash = stable_fingerprint(&["semantic-shape", &shape]);
|
|
46
|
+
let start_line = start_index + 1;
|
|
47
|
+
candidates.push(ObjectCandidate {
|
|
48
|
+
path: file.path.clone(),
|
|
49
|
+
start_line,
|
|
50
|
+
end_line: end_index + 1,
|
|
51
|
+
symbol: nearest_symbol(&file.symbols, start_line),
|
|
52
|
+
keys,
|
|
53
|
+
shape_hash,
|
|
54
|
+
line_count,
|
|
55
|
+
in_test_context: is_test_context(&file.path),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fn extract_keys(lines: &[String]) -> BTreeSet<String> {
|
|
60
|
+
lines
|
|
61
|
+
.iter()
|
|
62
|
+
.filter_map(|line| key_from_line(line))
|
|
63
|
+
.collect::<BTreeSet<_>>()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn key_from_line(line: &str) -> Option<String> {
|
|
67
|
+
let trimmed = line.trim().trim_start_matches(['{', ',', '[']);
|
|
68
|
+
if trimmed.starts_with("//") || trimmed.starts_with('#') {
|
|
69
|
+
return None;
|
|
70
|
+
}
|
|
71
|
+
let (raw_key, _) = trimmed.split_once(':')?;
|
|
72
|
+
let key = raw_key.trim().trim_matches('"').trim_matches('\'').trim();
|
|
73
|
+
let valid = !key.is_empty()
|
|
74
|
+
&& key.len() <= 64
|
|
75
|
+
&& key.chars().all(|character| {
|
|
76
|
+
character.is_ascii_alphanumeric() || character == '_' || character == '-'
|
|
77
|
+
});
|
|
78
|
+
valid.then(|| key.to_ascii_lowercase())
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fn brace_delta(line: &str) -> isize {
|
|
82
|
+
let mut delta = 0isize;
|
|
83
|
+
let mut in_string = false;
|
|
84
|
+
let mut quote = '\0';
|
|
85
|
+
let mut escaped = false;
|
|
86
|
+
for character in line.chars() {
|
|
87
|
+
if in_string {
|
|
88
|
+
if escaped {
|
|
89
|
+
escaped = false;
|
|
90
|
+
} else if character == '\\' {
|
|
91
|
+
escaped = true;
|
|
92
|
+
} else if character == quote {
|
|
93
|
+
in_string = false;
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if character == '"' || character == '\'' || character == '`' {
|
|
98
|
+
in_string = true;
|
|
99
|
+
quote = character;
|
|
100
|
+
} else if character == '{' {
|
|
101
|
+
delta += 1;
|
|
102
|
+
} else if character == '}' {
|
|
103
|
+
delta -= 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
delta
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fn looks_like_object_start(line: &str) -> bool {
|
|
110
|
+
let trimmed = line.trim();
|
|
111
|
+
if starts_code_block(trimmed) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
trimmed.starts_with('{')
|
|
115
|
+
|| trimmed.ends_with('{')
|
|
116
|
+
|| trimmed.contains("= {")
|
|
117
|
+
|| trimmed.contains(": {")
|
|
118
|
+
|| trimmed.contains("({")
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fn starts_code_block(trimmed: &str) -> bool {
|
|
122
|
+
[
|
|
123
|
+
"fn ",
|
|
124
|
+
"pub fn ",
|
|
125
|
+
"function ",
|
|
126
|
+
"async function ",
|
|
127
|
+
"if ",
|
|
128
|
+
"for ",
|
|
129
|
+
"while ",
|
|
130
|
+
"match ",
|
|
131
|
+
"impl ",
|
|
132
|
+
"mod ",
|
|
133
|
+
"class ",
|
|
134
|
+
"struct ",
|
|
135
|
+
"enum ",
|
|
136
|
+
]
|
|
137
|
+
.iter()
|
|
138
|
+
.any(|prefix| trimmed.starts_with(prefix))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fn nearest_symbol(symbols: &[SymbolAnalysis], line: usize) -> Option<String> {
|
|
142
|
+
symbols
|
|
143
|
+
.iter()
|
|
144
|
+
.filter(|symbol| symbol.start_line <= line)
|
|
145
|
+
.max_by_key(|symbol| symbol.start_line)
|
|
146
|
+
.map(|symbol| symbol.name.clone())
|
|
147
|
+
.filter(|name| !name.is_empty())
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fn is_test_context(path: &str) -> bool {
|
|
151
|
+
let lower = path.to_ascii_lowercase();
|
|
152
|
+
lower.contains("/test")
|
|
153
|
+
|| lower.contains("test/")
|
|
154
|
+
|| lower.contains(".test.")
|
|
155
|
+
|| lower.contains(".spec.")
|
|
156
|
+
|| lower.contains("fixture")
|
|
157
|
+
|| lower.contains("support")
|
|
158
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
use std::collections::BTreeSet;
|
|
2
|
+
|
|
3
|
+
use serde::Serialize;
|
|
4
|
+
|
|
5
|
+
#[derive(Debug, Clone, Serialize)]
|
|
6
|
+
#[serde(rename_all = "camelCase")]
|
|
7
|
+
pub struct SemanticReport {
|
|
8
|
+
pub schema: String,
|
|
9
|
+
pub mode: String,
|
|
10
|
+
pub ok: bool,
|
|
11
|
+
pub changed_paths: Vec<String>,
|
|
12
|
+
pub summary: SemanticSummary,
|
|
13
|
+
pub findings: Vec<SemanticFinding>,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
#[derive(Debug, Clone, Serialize)]
|
|
17
|
+
#[serde(rename_all = "camelCase")]
|
|
18
|
+
pub struct SemanticSummary {
|
|
19
|
+
pub scanned_files: usize,
|
|
20
|
+
pub scanned_path_count: usize,
|
|
21
|
+
pub finding_count: usize,
|
|
22
|
+
pub blocking_finding_count: usize,
|
|
23
|
+
pub truncated: bool,
|
|
24
|
+
pub reason_codes: Vec<String>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#[derive(Debug, Clone, Serialize)]
|
|
28
|
+
#[serde(rename_all = "camelCase")]
|
|
29
|
+
pub struct SemanticFinding {
|
|
30
|
+
pub id: String,
|
|
31
|
+
pub kind: String,
|
|
32
|
+
pub confidence: f64,
|
|
33
|
+
pub severity: String,
|
|
34
|
+
pub mode: String,
|
|
35
|
+
pub summary: String,
|
|
36
|
+
pub occurrences: Vec<SemanticOccurrence>,
|
|
37
|
+
pub cleanup_route: SemanticCleanupRoute,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[derive(Debug, Clone, Serialize)]
|
|
41
|
+
#[serde(rename_all = "camelCase")]
|
|
42
|
+
pub struct SemanticOccurrence {
|
|
43
|
+
pub path: String,
|
|
44
|
+
pub start_line: usize,
|
|
45
|
+
pub end_line: usize,
|
|
46
|
+
pub symbol: Option<String>,
|
|
47
|
+
pub shape_hash: String,
|
|
48
|
+
pub key_count: usize,
|
|
49
|
+
pub line_count: usize,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[derive(Debug, Clone, Serialize)]
|
|
53
|
+
#[serde(rename_all = "camelCase")]
|
|
54
|
+
pub struct SemanticCleanupRoute {
|
|
55
|
+
pub intent: String,
|
|
56
|
+
pub target_suggestion: String,
|
|
57
|
+
pub agent_instructions: Vec<String>,
|
|
58
|
+
pub required_checks: Vec<String>,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#[derive(Debug, Clone)]
|
|
62
|
+
pub(super) struct ObjectCandidate {
|
|
63
|
+
pub path: String,
|
|
64
|
+
pub start_line: usize,
|
|
65
|
+
pub end_line: usize,
|
|
66
|
+
pub symbol: Option<String>,
|
|
67
|
+
pub keys: BTreeSet<String>,
|
|
68
|
+
pub shape_hash: String,
|
|
69
|
+
pub line_count: usize,
|
|
70
|
+
pub in_test_context: bool,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
impl ObjectCandidate {
|
|
74
|
+
pub(super) fn occurrence(&self) -> SemanticOccurrence {
|
|
75
|
+
SemanticOccurrence {
|
|
76
|
+
path: self.path.clone(),
|
|
77
|
+
start_line: self.start_line,
|
|
78
|
+
end_line: self.end_line,
|
|
79
|
+
symbol: self.symbol.clone(),
|
|
80
|
+
shape_hash: self.shape_hash.clone(),
|
|
81
|
+
key_count: self.keys.len(),
|
|
82
|
+
line_count: self.line_count,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
use super::model::{SemanticCleanupRoute, SemanticOccurrence};
|
|
2
|
+
use crate::quality::scanner::QualityContext;
|
|
3
|
+
use crate::quality::types::QualityMode;
|
|
4
|
+
|
|
5
|
+
pub(super) fn finding_mode(context: &QualityContext) -> &'static str {
|
|
6
|
+
match context.mode {
|
|
7
|
+
QualityMode::ChangedFast => "changed-blocking",
|
|
8
|
+
QualityMode::Report => "report",
|
|
9
|
+
QualityMode::DeepReport => "deep-report",
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
pub(super) fn cleanup_route(
|
|
14
|
+
intent: &str,
|
|
15
|
+
occurrences: &[SemanticOccurrence],
|
|
16
|
+
first_instruction: &str,
|
|
17
|
+
) -> SemanticCleanupRoute {
|
|
18
|
+
let target_suggestion = target_suggestion(occurrences);
|
|
19
|
+
SemanticCleanupRoute {
|
|
20
|
+
intent: intent.to_string(),
|
|
21
|
+
target_suggestion,
|
|
22
|
+
agent_instructions: vec![
|
|
23
|
+
first_instruction.to_string(),
|
|
24
|
+
"Update every occurrence listed in this finding group; do not leave parallel inline copies behind.".to_string(),
|
|
25
|
+
"Preserve behavior and keep version-specific or schema-specific differences explicit as parameters or named fixtures.".to_string(),
|
|
26
|
+
"Do not auto-remove compatibility coverage unless focused tests prove the behavior remains covered.".to_string(),
|
|
27
|
+
],
|
|
28
|
+
required_checks: vec![
|
|
29
|
+
"naome semantic check --changed".to_string(),
|
|
30
|
+
"naome quality check --changed".to_string(),
|
|
31
|
+
"git diff --check".to_string(),
|
|
32
|
+
],
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn target_suggestion(occurrences: &[SemanticOccurrence]) -> String {
|
|
37
|
+
let Some(first) = occurrences.first() else {
|
|
38
|
+
return "test-support".to_string();
|
|
39
|
+
};
|
|
40
|
+
let directory = first
|
|
41
|
+
.path
|
|
42
|
+
.rsplit_once('/')
|
|
43
|
+
.map(|(directory, _)| directory)
|
|
44
|
+
.unwrap_or(".");
|
|
45
|
+
if directory == "." {
|
|
46
|
+
"test-support".to_string()
|
|
47
|
+
} else if directory.contains("script") {
|
|
48
|
+
format!("{directory}/test-support")
|
|
49
|
+
} else {
|
|
50
|
+
format!("{directory}/fixtures")
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
mod checks;
|
|
2
|
+
mod extract;
|
|
3
|
+
mod model;
|
|
4
|
+
mod route;
|
|
5
|
+
|
|
6
|
+
use checks::{copied_config_findings, inline_legacy_fixture_findings};
|
|
7
|
+
use extract::extract_object_candidates;
|
|
8
|
+
pub use model::{SemanticFinding, SemanticReport};
|
|
9
|
+
|
|
10
|
+
use super::scanner::QualityContext;
|
|
11
|
+
use model::SemanticSummary;
|
|
12
|
+
|
|
13
|
+
pub fn run_semantic_checks(context: &QualityContext) -> SemanticReport {
|
|
14
|
+
let mut candidates = context
|
|
15
|
+
.files
|
|
16
|
+
.iter()
|
|
17
|
+
.flat_map(extract_object_candidates)
|
|
18
|
+
.collect::<Vec<_>>();
|
|
19
|
+
candidates.sort_by(|left, right| {
|
|
20
|
+
left.path
|
|
21
|
+
.cmp(&right.path)
|
|
22
|
+
.then(left.start_line.cmp(&right.start_line))
|
|
23
|
+
.then(left.shape_hash.cmp(&right.shape_hash))
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
let mut findings = Vec::new();
|
|
27
|
+
findings.extend(copied_config_findings(context, &candidates));
|
|
28
|
+
findings.extend(inline_legacy_fixture_findings(context, &candidates));
|
|
29
|
+
findings.sort_by(|left, right| {
|
|
30
|
+
left.kind
|
|
31
|
+
.cmp(&right.kind)
|
|
32
|
+
.then(left.occurrences[0].path.cmp(&right.occurrences[0].path))
|
|
33
|
+
.then(
|
|
34
|
+
left.occurrences[0]
|
|
35
|
+
.start_line
|
|
36
|
+
.cmp(&right.occurrences[0].start_line),
|
|
37
|
+
)
|
|
38
|
+
.then(left.id.cmp(&right.id))
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
let blocking_finding_count = findings.len();
|
|
42
|
+
SemanticReport {
|
|
43
|
+
schema: "naome.semantic-legacy-report.v1".to_string(),
|
|
44
|
+
mode: context.mode.as_str().to_string(),
|
|
45
|
+
ok: blocking_finding_count == 0,
|
|
46
|
+
changed_paths: context.changed_paths.clone(),
|
|
47
|
+
summary: SemanticSummary {
|
|
48
|
+
scanned_files: context.files.len(),
|
|
49
|
+
scanned_path_count: context.scanned_paths().len(),
|
|
50
|
+
finding_count: findings.len(),
|
|
51
|
+
blocking_finding_count,
|
|
52
|
+
truncated: context.truncated,
|
|
53
|
+
reason_codes: context.reason_codes.clone(),
|
|
54
|
+
},
|
|
55
|
+
findings,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
pub fn semantic_route_for_finding(
|
|
60
|
+
report: &SemanticReport,
|
|
61
|
+
finding_id: &str,
|
|
62
|
+
) -> Option<SemanticFinding> {
|
|
63
|
+
report
|
|
64
|
+
.findings
|
|
65
|
+
.iter()
|
|
66
|
+
.find(|finding| finding.id == finding_id)
|
|
67
|
+
.cloned()
|
|
68
|
+
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
use std::collections::BTreeMap;
|
|
2
|
-
|
|
3
1
|
use crate::quality::structure::model::{RepositoryStructureModel, StructurePath};
|
|
4
2
|
use crate::quality::types::{QualityMode, QualityViolation};
|
|
5
3
|
|
|
@@ -38,7 +36,7 @@ pub(super) fn directory_size(
|
|
|
38
36
|
if directory.file_count <= model.config.limits.max_directory_files {
|
|
39
37
|
continue;
|
|
40
38
|
}
|
|
41
|
-
if mode
|
|
39
|
+
if mode.is_changed() && directory.direct_changed_paths.is_empty() {
|
|
42
40
|
continue;
|
|
43
41
|
}
|
|
44
42
|
push_with_limit(
|
|
@@ -85,20 +83,11 @@ pub(super) fn case_collisions(
|
|
|
85
83
|
mode: QualityMode,
|
|
86
84
|
violations: &mut Vec<QualityViolation>,
|
|
87
85
|
) {
|
|
88
|
-
|
|
89
|
-
for path in &model.paths {
|
|
90
|
-
groups
|
|
91
|
-
.entry(path.explanation.path.to_ascii_lowercase())
|
|
92
|
-
.or_default()
|
|
93
|
-
.push(path.explanation.path.clone());
|
|
94
|
-
}
|
|
95
|
-
for group in groups.values().filter(|group| group.len() > 1) {
|
|
86
|
+
for group in model.lowercase_paths.values().filter(|group| group.len() > 1) {
|
|
96
87
|
let changed_group = group.iter().any(|path| {
|
|
97
|
-
model.
|
|
98
|
-
candidate.explanation.path == *path && candidate.explanation.changed
|
|
99
|
-
})
|
|
88
|
+
model.changed_paths.contains(path)
|
|
100
89
|
});
|
|
101
|
-
if mode
|
|
90
|
+
if mode.is_changed() && !changed_group {
|
|
102
91
|
continue;
|
|
103
92
|
}
|
|
104
93
|
for path in group {
|
|
@@ -134,10 +123,11 @@ fn related_module_paths(model: &RepositoryStructureModel, path: &StructurePath)
|
|
|
134
123
|
return Vec::new();
|
|
135
124
|
};
|
|
136
125
|
model
|
|
137
|
-
.
|
|
138
|
-
.
|
|
139
|
-
.
|
|
140
|
-
.
|
|
126
|
+
.module_paths
|
|
127
|
+
.get(module)
|
|
128
|
+
.into_iter()
|
|
129
|
+
.flatten()
|
|
130
|
+
.cloned()
|
|
141
131
|
.filter(|candidate| candidate != &path.explanation.path)
|
|
142
132
|
.take(10)
|
|
143
133
|
.collect()
|
|
@@ -53,7 +53,7 @@ fn check_enabled(model: &RepositoryStructureModel, check_id: &str) -> bool {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
fn applies(path: &StructurePath, mode: QualityMode) -> bool {
|
|
56
|
-
mode
|
|
56
|
+
!mode.is_changed() || path.explanation.changed
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
fn push(
|
|
@@ -24,10 +24,16 @@ pub fn build_structure_model(
|
|
|
24
24
|
.collect::<Vec<_>>();
|
|
25
25
|
paths.sort_by(|left, right| left.explanation.path.cmp(&right.explanation.path));
|
|
26
26
|
let directories = directories_for(&paths);
|
|
27
|
+
let indexes = indexes_for(&paths);
|
|
27
28
|
RepositoryStructureModel {
|
|
28
29
|
config,
|
|
29
30
|
paths,
|
|
30
31
|
directories,
|
|
32
|
+
module_paths: indexes.module_paths,
|
|
33
|
+
directory_paths: indexes.directory_paths,
|
|
34
|
+
role_paths: indexes.role_paths,
|
|
35
|
+
lowercase_paths: indexes.lowercase_paths,
|
|
36
|
+
changed_paths: indexes.changed_paths,
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
39
|
|
|
@@ -92,3 +98,49 @@ fn directory_of(path: &str) -> String {
|
|
|
92
98
|
.map(|(directory, _)| directory.to_string())
|
|
93
99
|
.unwrap_or_else(|| ".".to_string())
|
|
94
100
|
}
|
|
101
|
+
|
|
102
|
+
struct StructureIndexes {
|
|
103
|
+
module_paths: BTreeMap<String, Vec<String>>,
|
|
104
|
+
directory_paths: BTreeMap<String, Vec<String>>,
|
|
105
|
+
role_paths: BTreeMap<String, Vec<String>>,
|
|
106
|
+
lowercase_paths: BTreeMap<String, Vec<String>>,
|
|
107
|
+
changed_paths: BTreeSet<String>,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fn indexes_for(paths: &[StructurePath]) -> StructureIndexes {
|
|
111
|
+
let mut module_paths = BTreeMap::new();
|
|
112
|
+
let mut directory_paths = BTreeMap::new();
|
|
113
|
+
let mut role_paths = BTreeMap::new();
|
|
114
|
+
let mut lowercase_paths = BTreeMap::new();
|
|
115
|
+
let mut changed_paths = BTreeSet::new();
|
|
116
|
+
for path in paths {
|
|
117
|
+
let value = path.explanation.path.clone();
|
|
118
|
+
if let Some(module) = &path.explanation.module {
|
|
119
|
+
push_index(&mut module_paths, module, &value);
|
|
120
|
+
}
|
|
121
|
+
push_index(&mut directory_paths, &path.explanation.directory, &value);
|
|
122
|
+
push_index(&mut role_paths, &path.explanation.role, &value);
|
|
123
|
+
push_index(
|
|
124
|
+
&mut lowercase_paths,
|
|
125
|
+
&path.explanation.path.to_ascii_lowercase(),
|
|
126
|
+
&value,
|
|
127
|
+
);
|
|
128
|
+
if path.explanation.changed {
|
|
129
|
+
changed_paths.insert(value);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
StructureIndexes {
|
|
133
|
+
module_paths,
|
|
134
|
+
directory_paths,
|
|
135
|
+
role_paths,
|
|
136
|
+
lowercase_paths,
|
|
137
|
+
changed_paths,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fn push_index(index: &mut BTreeMap<String, Vec<String>>, key: &str, value: &str) {
|
|
142
|
+
index
|
|
143
|
+
.entry(key.to_string())
|
|
144
|
+
.or_default()
|
|
145
|
+
.push(value.to_string());
|
|
146
|
+
}
|
|
@@ -11,7 +11,7 @@ use std::path::Path;
|
|
|
11
11
|
use crate::models::NaomeError;
|
|
12
12
|
|
|
13
13
|
use super::scanner::QualityContext;
|
|
14
|
-
use super::types::
|
|
14
|
+
use super::types::QualityViolation;
|
|
15
15
|
use checks::run_structure_checks;
|
|
16
16
|
use classify::build_structure_model;
|
|
17
17
|
use config::read_structure_config;
|
|
@@ -33,7 +33,7 @@ pub fn run_repository_structure_checks(
|
|
|
33
33
|
.find(|file| file.path == violation.path)
|
|
34
34
|
.is_some_and(|file| !file.symbols.is_empty())
|
|
35
35
|
});
|
|
36
|
-
if context.mode
|
|
36
|
+
if !context.mode.is_changed() {
|
|
37
37
|
for violation in &mut violations {
|
|
38
38
|
violation.baseline = baseline_fingerprints.contains(&violation.fingerprint);
|
|
39
39
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
use std::collections::BTreeSet;
|
|
1
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
2
2
|
|
|
3
3
|
use serde::{Deserialize, Serialize};
|
|
4
4
|
|
|
@@ -121,4 +121,11 @@ pub struct RepositoryStructureModel {
|
|
|
121
121
|
pub config: RepositoryStructureConfig,
|
|
122
122
|
pub paths: Vec<StructurePath>,
|
|
123
123
|
pub directories: Vec<StructureDirectory>,
|
|
124
|
+
pub module_paths: BTreeMap<String, Vec<String>>,
|
|
125
|
+
#[allow(dead_code)]
|
|
126
|
+
pub directory_paths: BTreeMap<String, Vec<String>>,
|
|
127
|
+
#[allow(dead_code)]
|
|
128
|
+
pub role_paths: BTreeMap<String, Vec<String>>,
|
|
129
|
+
pub lowercase_paths: BTreeMap<String, Vec<String>>,
|
|
130
|
+
pub changed_paths: BTreeSet<String>,
|
|
124
131
|
}
|
|
@@ -4,15 +4,45 @@ use crate::paths;
|
|
|
4
4
|
|
|
5
5
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
6
6
|
pub enum QualityMode {
|
|
7
|
-
|
|
7
|
+
ChangedFast,
|
|
8
8
|
Report,
|
|
9
|
+
DeepReport,
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
impl QualityMode {
|
|
13
|
+
#[allow(non_upper_case_globals)]
|
|
14
|
+
pub const Changed: Self = Self::ChangedFast;
|
|
15
|
+
|
|
12
16
|
pub fn as_str(self) -> &'static str {
|
|
13
17
|
match self {
|
|
14
|
-
Self::
|
|
18
|
+
Self::ChangedFast => "changed",
|
|
15
19
|
Self::Report => "report",
|
|
20
|
+
Self::DeepReport => "deep-report",
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
pub fn is_changed(self) -> bool {
|
|
25
|
+
self == Self::ChangedFast
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pub fn is_deep(self) -> bool {
|
|
29
|
+
self == Self::DeepReport
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
34
|
+
pub enum QualityInitMode {
|
|
35
|
+
SeedOnly,
|
|
36
|
+
Baseline,
|
|
37
|
+
DeepBaseline,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
impl QualityInitMode {
|
|
41
|
+
pub fn as_str(self) -> &'static str {
|
|
42
|
+
match self {
|
|
43
|
+
Self::SeedOnly => "init",
|
|
44
|
+
Self::Baseline => "baseline",
|
|
45
|
+
Self::DeepBaseline => "deep-baseline",
|
|
16
46
|
}
|
|
17
47
|
}
|
|
18
48
|
}
|
|
@@ -187,9 +217,14 @@ pub struct QualityReport {
|
|
|
187
217
|
#[serde(rename_all = "camelCase")]
|
|
188
218
|
pub struct QualitySummary {
|
|
189
219
|
pub scanned_files: usize,
|
|
220
|
+
pub scanned_path_count: usize,
|
|
190
221
|
pub violation_count: usize,
|
|
191
222
|
pub baseline_violation_count: usize,
|
|
192
223
|
pub blocking_violation_count: usize,
|
|
224
|
+
pub truncated: bool,
|
|
225
|
+
pub reason_codes: Vec<String>,
|
|
226
|
+
pub cache_hits: usize,
|
|
227
|
+
pub cache_misses: usize,
|
|
193
228
|
}
|
|
194
229
|
|
|
195
230
|
#[derive(Debug, Clone, Serialize)]
|
|
@@ -211,9 +246,11 @@ pub struct QualityViolation {
|
|
|
211
246
|
#[serde(rename_all = "camelCase")]
|
|
212
247
|
pub struct QualityInitResult {
|
|
213
248
|
pub schema: String,
|
|
249
|
+
pub mode: String,
|
|
214
250
|
pub config_written: bool,
|
|
215
251
|
pub structure_config_written: bool,
|
|
216
252
|
pub baseline_written: bool,
|
|
253
|
+
pub baseline_pending: bool,
|
|
217
254
|
pub baseline_violations: usize,
|
|
218
255
|
pub config_path: String,
|
|
219
256
|
pub structure_config_path: String,
|
|
@@ -258,6 +295,7 @@ pub fn default_generated_paths() -> Vec<String> {
|
|
|
258
295
|
const DEFAULT_IGNORED_PATHS: &str = r#"
|
|
259
296
|
.git/**
|
|
260
297
|
.naome/archive/**
|
|
298
|
+
.naome/cache/**
|
|
261
299
|
.naome/task-state.json
|
|
262
300
|
.naome/task-journal.jsonl
|
|
263
301
|
.naome/repository-quality.json
|
|
@@ -18,16 +18,6 @@ pub(super) fn run_quality_check(
|
|
|
18
18
|
check: &QualityCheck,
|
|
19
19
|
) -> Result<(), NaomeError> {
|
|
20
20
|
match check_id {
|
|
21
|
-
"installer-tests" => require_builtin_quality_check(
|
|
22
|
-
check_id,
|
|
23
|
-
check,
|
|
24
|
-
"npm run test:naome-installer",
|
|
25
|
-
),
|
|
26
|
-
"rust-build" => require_builtin_quality_check(check_id, check, "npm run build:rust"),
|
|
27
|
-
"decision-engine-tests" => {
|
|
28
|
-
require_builtin_quality_check(check_id, check, "npm run test:decision-engine")
|
|
29
|
-
}
|
|
30
|
-
"package-dry-run" => require_builtin_quality_check(check_id, check, "npm run pack:dry-run"),
|
|
31
21
|
"diff-check" => {
|
|
32
22
|
require_builtin_quality_check(check_id, check, "git diff --check")?;
|
|
33
23
|
let output = git_output(root, &["diff", "--check"])?;
|
|
@@ -46,10 +36,6 @@ pub(super) fn run_quality_check(
|
|
|
46
36
|
)?;
|
|
47
37
|
run_harness_health_check(root)
|
|
48
38
|
}
|
|
49
|
-
"dogfood-health" => {
|
|
50
|
-
require_builtin_quality_check(check_id, check, "npm run dogfood:health")?;
|
|
51
|
-
run_harness_health_check(root)
|
|
52
|
-
}
|
|
53
39
|
"task-state-check" => {
|
|
54
40
|
require_builtin_quality_check(check_id, check, "npm run check:task-state")?;
|
|
55
41
|
run_template_task_state_check(root)
|
|
@@ -85,7 +71,7 @@ pub(super) fn run_quality_check(
|
|
|
85
71
|
}
|
|
86
72
|
|
|
87
73
|
fn run_repository_quality_check(root: &Path) -> Result<(), NaomeError> {
|
|
88
|
-
let report = check_repository_quality(root, QualityMode::
|
|
74
|
+
let report = check_repository_quality(root, QualityMode::ChangedFast)?;
|
|
89
75
|
if report.ok {
|
|
90
76
|
return Ok(());
|
|
91
77
|
}
|