@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
use std::path::Path;
|
|
2
2
|
|
|
3
3
|
use naome_core::{
|
|
4
|
-
classify_mutations, refresh_integrity, safe_rg_args, tracked_process_report,
|
|
4
|
+
classify_mutations, doctor_report, refresh_integrity, safe_rg_args, tracked_process_report,
|
|
5
5
|
validate_search_command, verification_phase_plan,
|
|
6
6
|
};
|
|
7
7
|
|
|
@@ -83,6 +83,26 @@ pub fn run_workflow_command(
|
|
|
83
83
|
Ok(())
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
pub fn run_doctor(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
|
87
|
+
let report = doctor_report(root)?;
|
|
88
|
+
if args.iter().any(|arg| arg == "--json") {
|
|
89
|
+
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
90
|
+
} else {
|
|
91
|
+
println!(
|
|
92
|
+
"NAOME doctor: {}",
|
|
93
|
+
if report.ok { "ok" } else { "attention needed" }
|
|
94
|
+
);
|
|
95
|
+
println!("{}", report.next_action);
|
|
96
|
+
if !report.recommended_check_ids.is_empty() {
|
|
97
|
+
println!(
|
|
98
|
+
"Recommended checks: {}",
|
|
99
|
+
report.recommended_check_ids.join(", ")
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
Ok(())
|
|
104
|
+
}
|
|
105
|
+
|
|
86
106
|
fn run_check_search(
|
|
87
107
|
root: &Path,
|
|
88
108
|
args: &[String],
|
|
@@ -63,12 +63,14 @@ fn read_naomeignore_patterns(root: &Path) -> Vec<String> {
|
|
|
63
63
|
return Vec::new();
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
-
content
|
|
66
|
+
let mut patterns = content
|
|
67
67
|
.lines()
|
|
68
68
|
.map(str::trim)
|
|
69
69
|
.filter(|line| !line.is_empty() && !line.starts_with('#') && !line.starts_with('!'))
|
|
70
70
|
.map(normalize_ignore_pattern)
|
|
71
|
-
.collect()
|
|
71
|
+
.collect::<Vec<_>>();
|
|
72
|
+
patterns.push(".naome/cache/**".to_string());
|
|
73
|
+
patterns
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
fn normalize_ignore_pattern(pattern: &str) -> String {
|
|
@@ -50,6 +50,7 @@ pub const LOCAL_NATIVE_BINARY_PATHS: &[&str] = &[
|
|
|
50
50
|
".naome/bin/naome-rust",
|
|
51
51
|
".naome/bin/naome-rust.exe",
|
|
52
52
|
".naome/archive",
|
|
53
|
+
".naome/cache",
|
|
53
54
|
".naome/task-journal.jsonl",
|
|
54
55
|
];
|
|
55
56
|
|
|
@@ -72,6 +73,7 @@ pub fn install_plan(harness_version: impl Into<String>) -> InstallPlan {
|
|
|
72
73
|
"# NAOME local machine-owned harness files.",
|
|
73
74
|
".naome/archive/",
|
|
74
75
|
".naome/bin/naome-rust*",
|
|
76
|
+
".naome/cache/",
|
|
75
77
|
".naome/task-journal.jsonl",
|
|
76
78
|
];
|
|
77
79
|
git_exclude_entries.extend_from_slice(LOCAL_ONLY_MACHINE_OWNED_PATHS);
|
|
@@ -21,10 +21,13 @@ pub use intent::{evaluate_intent, format_intent, IntentDecision, PromptEvidence}
|
|
|
21
21
|
pub use journal::{append_task_journal, TaskJournalEntry};
|
|
22
22
|
pub use models::{Decision, NaomeError};
|
|
23
23
|
pub use quality::{
|
|
24
|
-
check_repository_quality,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
check_repository_quality, check_semantic_legacy, explain_repository_structure,
|
|
25
|
+
clear_quality_cache, init_repository_quality, init_repository_quality_with_mode,
|
|
26
|
+
plan_quality_cleanup, quality_cache_status, route_quality_cleanup,
|
|
27
|
+
semantic_route_for_finding, QualityCacheStatus, QualityCleanupPlan, QualityCleanupRoute,
|
|
28
|
+
QualityCleanupTask, QualityInitMode, QualityInitResult, QualityMode, QualityReport,
|
|
29
|
+
QualitySummary, QualityViolation, RepositoryQualityConfig, RepositoryStructureConfig,
|
|
30
|
+
SemanticFinding, SemanticReport, StructurePathExplanation,
|
|
28
31
|
};
|
|
29
32
|
pub use route::{evaluate_route, explain_route, ExplainDecision, RouteDecision, RouteOptions};
|
|
30
33
|
pub use task_state::{
|
|
@@ -34,8 +37,9 @@ pub use task_state::{
|
|
|
34
37
|
pub use verification::seed_builtin_verification_checks;
|
|
35
38
|
pub use verification_contract::validate_verification_contract;
|
|
36
39
|
pub use workflow::{
|
|
37
|
-
classify_mutations, refresh_integrity, safe_rg_args, summarize_command_output,
|
|
40
|
+
classify_mutations, doctor_report, refresh_integrity, safe_rg_args, summarize_command_output,
|
|
38
41
|
tracked_process_report, validate_read_boundaries, validate_search_command,
|
|
39
|
-
verification_phase_plan, CommandCheckResult, CommandOutputSummary,
|
|
40
|
-
MutationClassification, ProcessReport, ReadActivity,
|
|
42
|
+
verification_phase_plan, CommandCheckResult, CommandOutputSummary, DoctorReport, DoctorSection,
|
|
43
|
+
IntegrityRefreshReport, MutationClassification, ProcessReport, ReadActivity,
|
|
44
|
+
RepositoryPolicySection, VerificationPhasePlan, WorkflowFinding,
|
|
41
45
|
};
|
|
@@ -73,3 +73,11 @@ pub fn write_baseline(root: &Path, violations: &[QualityViolation]) -> Result<bo
|
|
|
73
73
|
}
|
|
74
74
|
Ok(changed)
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
pub fn write_empty_baseline_if_missing(root: &Path) -> Result<bool, NaomeError> {
|
|
78
|
+
let path = root.join(BASELINE_RELATIVE_PATH);
|
|
79
|
+
if path.is_file() {
|
|
80
|
+
return Ok(false);
|
|
81
|
+
}
|
|
82
|
+
write_baseline(root, &[])
|
|
83
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
use std::fs;
|
|
2
|
+
use std::path::{Path, PathBuf};
|
|
3
|
+
|
|
4
|
+
use serde::{Deserialize, Serialize};
|
|
5
|
+
use sha2::{Digest, Sha256};
|
|
6
|
+
|
|
7
|
+
use crate::models::NaomeError;
|
|
8
|
+
|
|
9
|
+
use super::scanner::FileAnalysis;
|
|
10
|
+
use super::types::RepositoryQualityConfig;
|
|
11
|
+
|
|
12
|
+
const CACHE_RELATIVE_PATH: &str = ".naome/cache/quality";
|
|
13
|
+
const CACHE_SCHEMA: &str = "naome.quality-cache-entry.v1";
|
|
14
|
+
const ADAPTER_VERSION: &str = "quality-core-v1";
|
|
15
|
+
|
|
16
|
+
#[derive(Debug, Clone)]
|
|
17
|
+
pub(crate) struct QualityCache {
|
|
18
|
+
root: PathBuf,
|
|
19
|
+
config_hash: String,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
23
|
+
#[serde(rename_all = "camelCase")]
|
|
24
|
+
struct CacheEntry {
|
|
25
|
+
schema: String,
|
|
26
|
+
naome_version: String,
|
|
27
|
+
config_hash: String,
|
|
28
|
+
adapter_version: String,
|
|
29
|
+
path: String,
|
|
30
|
+
content_hash: String,
|
|
31
|
+
analysis: FileAnalysis,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[derive(Debug, Clone, Serialize)]
|
|
35
|
+
#[serde(rename_all = "camelCase")]
|
|
36
|
+
pub struct QualityCacheStatus {
|
|
37
|
+
pub schema: String,
|
|
38
|
+
pub path: String,
|
|
39
|
+
pub entry_count: usize,
|
|
40
|
+
pub bytes: u64,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
impl QualityCache {
|
|
44
|
+
pub(crate) fn new(root: &Path, config: &RepositoryQualityConfig) -> Self {
|
|
45
|
+
Self {
|
|
46
|
+
root: root.to_path_buf(),
|
|
47
|
+
config_hash: config_hash(config),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
pub(crate) fn read(&self, path: &str, content_hash: &str) -> Option<FileAnalysis> {
|
|
52
|
+
let entry = fs::read_to_string(self.entry_path(path, content_hash)).ok()?;
|
|
53
|
+
let entry: CacheEntry = serde_json::from_str(&entry).ok()?;
|
|
54
|
+
if entry.schema == CACHE_SCHEMA
|
|
55
|
+
&& entry.naome_version == env!("CARGO_PKG_VERSION")
|
|
56
|
+
&& entry.config_hash == self.config_hash
|
|
57
|
+
&& entry.adapter_version == ADAPTER_VERSION
|
|
58
|
+
&& entry.path == path
|
|
59
|
+
&& entry.content_hash == content_hash
|
|
60
|
+
{
|
|
61
|
+
Some(entry.analysis)
|
|
62
|
+
} else {
|
|
63
|
+
None
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
pub(crate) fn write(
|
|
68
|
+
&self,
|
|
69
|
+
path: &str,
|
|
70
|
+
content_hash: &str,
|
|
71
|
+
analysis: &FileAnalysis,
|
|
72
|
+
) -> Result<(), NaomeError> {
|
|
73
|
+
let cache_path = self.entry_path(path, content_hash);
|
|
74
|
+
if let Some(parent) = cache_path.parent() {
|
|
75
|
+
fs::create_dir_all(parent)?;
|
|
76
|
+
}
|
|
77
|
+
let entry = CacheEntry {
|
|
78
|
+
schema: CACHE_SCHEMA.to_string(),
|
|
79
|
+
naome_version: env!("CARGO_PKG_VERSION").to_string(),
|
|
80
|
+
config_hash: self.config_hash.clone(),
|
|
81
|
+
adapter_version: ADAPTER_VERSION.to_string(),
|
|
82
|
+
path: path.to_string(),
|
|
83
|
+
content_hash: content_hash.to_string(),
|
|
84
|
+
analysis: analysis.clone(),
|
|
85
|
+
};
|
|
86
|
+
let content = format!("{}\n", serde_json::to_string(&entry)?);
|
|
87
|
+
if fs::read_to_string(&cache_path).map_or(true, |current| current != content) {
|
|
88
|
+
fs::write(cache_path, content)?;
|
|
89
|
+
}
|
|
90
|
+
Ok(())
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fn entry_path(&self, path: &str, content_hash: &str) -> PathBuf {
|
|
94
|
+
self.root
|
|
95
|
+
.join(CACHE_RELATIVE_PATH)
|
|
96
|
+
.join(stable_key(&[
|
|
97
|
+
env!("CARGO_PKG_VERSION"),
|
|
98
|
+
&self.config_hash,
|
|
99
|
+
ADAPTER_VERSION,
|
|
100
|
+
path,
|
|
101
|
+
content_hash,
|
|
102
|
+
]))
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
pub fn quality_cache_status(root: &Path) -> Result<QualityCacheStatus, NaomeError> {
|
|
107
|
+
let path = root.join(CACHE_RELATIVE_PATH);
|
|
108
|
+
let mut entry_count = 0;
|
|
109
|
+
let mut bytes = 0;
|
|
110
|
+
if path.is_dir() {
|
|
111
|
+
for entry in fs::read_dir(&path)? {
|
|
112
|
+
let entry = entry?;
|
|
113
|
+
let metadata = entry.metadata()?;
|
|
114
|
+
if metadata.is_file() {
|
|
115
|
+
entry_count += 1;
|
|
116
|
+
bytes += metadata.len();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
Ok(QualityCacheStatus {
|
|
121
|
+
schema: "naome.quality-cache-status.v1".to_string(),
|
|
122
|
+
path: CACHE_RELATIVE_PATH.to_string(),
|
|
123
|
+
entry_count,
|
|
124
|
+
bytes,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
pub fn clear_quality_cache(root: &Path) -> Result<QualityCacheStatus, NaomeError> {
|
|
129
|
+
let path = root.join(CACHE_RELATIVE_PATH);
|
|
130
|
+
if path.exists() {
|
|
131
|
+
fs::remove_dir_all(&path)?;
|
|
132
|
+
}
|
|
133
|
+
quality_cache_status(root)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
pub(crate) fn content_hash(content: &str) -> String {
|
|
137
|
+
stable_key(&[content])
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fn config_hash(config: &RepositoryQualityConfig) -> String {
|
|
141
|
+
serde_json::to_string(config)
|
|
142
|
+
.map(|content| stable_key(&[&content]))
|
|
143
|
+
.unwrap_or_else(|_| stable_key(&["invalid-config"]))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fn stable_key(parts: &[&str]) -> String {
|
|
147
|
+
let mut hasher = Sha256::new();
|
|
148
|
+
for part in parts {
|
|
149
|
+
hasher.update(part.as_bytes());
|
|
150
|
+
hasher.update(b"\0");
|
|
151
|
+
}
|
|
152
|
+
format!("{:x}.json", hasher.finalize())
|
|
153
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
use std::collections::{HashMap, HashSet};
|
|
2
2
|
|
|
3
|
-
use
|
|
3
|
+
use sha2::{Digest, Sha256};
|
|
4
|
+
|
|
5
|
+
use super::super::scanner::{stable_fingerprint, NormalizedLine, QualityContext};
|
|
4
6
|
use super::super::types::QualityViolation;
|
|
5
7
|
use super::{is_code_like_path, QualityCheck};
|
|
6
8
|
|
|
@@ -12,22 +14,23 @@ impl QualityCheck for DuplicateBlockCheck {
|
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
fn evaluate(&self, context: &QualityContext, violations: &mut Vec<QualityViolation>) {
|
|
17
|
+
if context.mode == super::super::types::QualityMode::Report {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
15
20
|
let mut occurrences: HashMap<String, Vec<DuplicateOccurrence>> = HashMap::new();
|
|
16
|
-
for file in context
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
for file in context
|
|
22
|
+
.comparison_candidate_files()
|
|
23
|
+
.filter(|file| {
|
|
24
|
+
is_code_like_path(&file.path)
|
|
25
|
+
&& context.config.check_enabled_for_path(self.id(), &file.path)
|
|
26
|
+
})
|
|
27
|
+
{
|
|
20
28
|
let window = context.limits_for(&file.path).duplicate_block_lines;
|
|
21
29
|
if file.normalized_lines.len() < window {
|
|
22
30
|
continue;
|
|
23
31
|
}
|
|
24
32
|
for lines in file.normalized_lines.windows(window) {
|
|
25
|
-
let
|
|
26
|
-
.iter()
|
|
27
|
-
.map(|line| line.value.as_str())
|
|
28
|
-
.collect::<Vec<_>>()
|
|
29
|
-
.join("\n");
|
|
30
|
-
let fingerprint = stable_fingerprint(&[self.id(), &joined]);
|
|
33
|
+
let fingerprint = window_fingerprint(self.id(), lines);
|
|
31
34
|
occurrences
|
|
32
35
|
.entry(fingerprint.clone())
|
|
33
36
|
.or_default()
|
|
@@ -72,6 +75,17 @@ impl QualityCheck for DuplicateBlockCheck {
|
|
|
72
75
|
}
|
|
73
76
|
}
|
|
74
77
|
|
|
78
|
+
fn window_fingerprint(check_id: &str, lines: &[NormalizedLine]) -> String {
|
|
79
|
+
let mut hasher = Sha256::new();
|
|
80
|
+
hasher.update(check_id.as_bytes());
|
|
81
|
+
hasher.update(b"\0");
|
|
82
|
+
for line in lines {
|
|
83
|
+
hasher.update(line.value.as_bytes());
|
|
84
|
+
hasher.update(b"\n");
|
|
85
|
+
}
|
|
86
|
+
format!("sha256:{:x}", hasher.finalize())
|
|
87
|
+
}
|
|
88
|
+
|
|
75
89
|
#[derive(Debug, Clone)]
|
|
76
90
|
struct DuplicateOccurrence {
|
|
77
91
|
path: String,
|
|
@@ -12,6 +12,9 @@ impl QualityCheck for NearDuplicateFunctionCheck {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
fn evaluate(&self, context: &QualityContext, violations: &mut Vec<QualityViolation>) {
|
|
15
|
+
if context.mode == super::super::types::QualityMode::Report {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
15
18
|
let symbols = collect_function_occurrences(context, self.id());
|
|
16
19
|
let mut emitted = HashSet::new();
|
|
17
20
|
|
|
@@ -59,8 +62,7 @@ fn collect_function_occurrences<'a>(
|
|
|
59
62
|
check_id: &str,
|
|
60
63
|
) -> Vec<FunctionOccurrence<'a>> {
|
|
61
64
|
context
|
|
62
|
-
.
|
|
63
|
-
.iter()
|
|
65
|
+
.comparison_candidate_files()
|
|
64
66
|
.filter(|file| {
|
|
65
67
|
is_code_like_path(&file.path)
|
|
66
68
|
&& context.config.check_enabled_for_path(check_id, &file.path)
|
|
@@ -4,7 +4,7 @@ mod near_duplicates;
|
|
|
4
4
|
use std::collections::HashSet;
|
|
5
5
|
|
|
6
6
|
use super::scanner::{stable_fingerprint, QualityContext};
|
|
7
|
-
use super::types::
|
|
7
|
+
use super::types::QualityViolation;
|
|
8
8
|
use duplicate_blocks::DuplicateBlockCheck;
|
|
9
9
|
use near_duplicates::NearDuplicateFunctionCheck;
|
|
10
10
|
|
|
@@ -64,12 +64,11 @@ impl QualityCheck for FileLengthCheck {
|
|
|
64
64
|
.filter(|file| context.check_applies_to(self.id(), &file.path))
|
|
65
65
|
{
|
|
66
66
|
let limits = context.limits_for(&file.path);
|
|
67
|
-
let limit =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
};
|
|
67
|
+
let limit = if context.mode.is_changed() && file.added_lines == file.line_count {
|
|
68
|
+
limits.max_new_file_lines
|
|
69
|
+
} else {
|
|
70
|
+
limits.max_file_lines
|
|
71
|
+
};
|
|
73
72
|
if file.line_count > limit {
|
|
74
73
|
violations.push(violation(
|
|
75
74
|
self.id(),
|
|
@@ -96,7 +95,7 @@ impl QualityCheck for DiffGrowthCheck {
|
|
|
96
95
|
}
|
|
97
96
|
|
|
98
97
|
fn evaluate(&self, context: &QualityContext, violations: &mut Vec<QualityViolation>) {
|
|
99
|
-
if context.mode
|
|
98
|
+
if !context.mode.is_changed() {
|
|
100
99
|
return;
|
|
101
100
|
}
|
|
102
101
|
for file in context
|
|
@@ -68,17 +68,50 @@ pub fn cleanup_route_for_path(
|
|
|
68
68
|
} else {
|
|
69
69
|
"repository quality"
|
|
70
70
|
};
|
|
71
|
+
let agent_instructions = cleanup_instructions(path, topic, &violations);
|
|
71
72
|
QualityCleanupRoute {
|
|
72
73
|
schema: "naome.quality-cleanup-route.v1".to_string(),
|
|
73
74
|
path: path.to_string(),
|
|
74
75
|
violations,
|
|
75
76
|
related_paths,
|
|
76
|
-
agent_instructions
|
|
77
|
-
"Reduce or split {path} until every {topic} violation is gone. Prefer named modules and reusable helpers/components over dumping logic into generic directories. Keep behavior unchanged, add or preserve focused tests, then run naome quality check --changed before task completion."
|
|
78
|
-
),
|
|
77
|
+
agent_instructions,
|
|
79
78
|
required_checks: vec![
|
|
80
79
|
"naome quality check --changed".to_string(),
|
|
81
80
|
"git diff --check".to_string(),
|
|
82
81
|
],
|
|
83
82
|
}
|
|
84
83
|
}
|
|
84
|
+
|
|
85
|
+
fn cleanup_instructions(path: &str, topic: &str, violations: &[QualityViolation]) -> String {
|
|
86
|
+
let mut instructions = Vec::new();
|
|
87
|
+
instructions.push(format!(
|
|
88
|
+
"Clean up {path} until every {topic} violation is gone while preserving behavior."
|
|
89
|
+
));
|
|
90
|
+
for check_id in violations
|
|
91
|
+
.iter()
|
|
92
|
+
.map(|violation| violation.check_id.as_str())
|
|
93
|
+
.collect::<BTreeSet<_>>()
|
|
94
|
+
{
|
|
95
|
+
instructions.push(match check_id {
|
|
96
|
+
"test-source-pairing" => "Create or update a nearby or module-matched test that exercises the changed source behavior.".to_string(),
|
|
97
|
+
"dumping-ground-directory" => "Move feature logic out of generic utils/helpers/common/shared directories into a named module, or extract a reusable helper with a clear owner.".to_string(),
|
|
98
|
+
"directory-role-mixing" => "Separate source, generated, artifact, and dependency/vendor roles into directories that match repository policy.".to_string(),
|
|
99
|
+
"misplaced-file-role" => "Move the file to a configured root for its role, such as source, test, docs, config, or script.".to_string(),
|
|
100
|
+
"root-file-sprawl" => "Move new root-level work into an existing module, docs, config, or script directory unless it is a recognized root manifest.".to_string(),
|
|
101
|
+
"directory-size" => "Split the directory by module or responsibility until direct file count is below the configured limit.".to_string(),
|
|
102
|
+
"path-depth" => "Shorten the path by moving the file closer to its owning module boundary.".to_string(),
|
|
103
|
+
"case-collision" => "Rename colliding paths so they are distinct on case-insensitive filesystems.".to_string(),
|
|
104
|
+
"file-length" => "Reduce or split the file into focused modules with stable public behavior.".to_string(),
|
|
105
|
+
"function-length" => "Extract cohesive helper functions or components until each function fits the configured limit.".to_string(),
|
|
106
|
+
"top-level-symbols" => "Group related symbols into smaller modules and export only the intended public surface.".to_string(),
|
|
107
|
+
"duplicate-block" | "near-duplicate-function" => "Extract shared behavior into a reusable helper, fixture, builder, or component used by all duplicate call sites.".to_string(),
|
|
108
|
+
"diff-growth" => "Reduce the diff by narrowing scope, splitting generated changes, or moving unrelated cleanup to a separate task.".to_string(),
|
|
109
|
+
_ => format!("Resolve the {check_id} finding without weakening generic policy."),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
instructions.push(
|
|
113
|
+
"Run naome quality check --changed and git diff --check before task completion."
|
|
114
|
+
.to_string(),
|
|
115
|
+
);
|
|
116
|
+
instructions.join(" ")
|
|
117
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
mod adapter_support;
|
|
2
2
|
mod adapters;
|
|
3
3
|
mod baseline;
|
|
4
|
+
mod cache;
|
|
4
5
|
mod checks;
|
|
5
6
|
mod cleanup;
|
|
6
7
|
mod config;
|
|
7
8
|
mod config_support;
|
|
8
9
|
mod scanner;
|
|
10
|
+
mod semantic;
|
|
9
11
|
mod structure;
|
|
10
12
|
mod types;
|
|
11
13
|
|
|
@@ -14,18 +16,26 @@ use std::path::Path;
|
|
|
14
16
|
use crate::models::NaomeError;
|
|
15
17
|
|
|
16
18
|
pub use cleanup::{cleanup_plan_from_violations, cleanup_route_for_path};
|
|
19
|
+
pub use cache::{clear_quality_cache, quality_cache_status, QualityCacheStatus};
|
|
20
|
+
pub use semantic::{semantic_route_for_finding, SemanticFinding, SemanticReport};
|
|
17
21
|
pub use structure::{
|
|
18
22
|
explain_repository_structure, RepositoryStructureConfig, StructurePathExplanation,
|
|
19
23
|
};
|
|
20
24
|
pub use types::{
|
|
21
|
-
QualityCleanupPlan, QualityCleanupRoute, QualityCleanupTask,
|
|
22
|
-
QualityReport, QualitySummary, QualityViolation,
|
|
25
|
+
QualityCleanupPlan, QualityCleanupRoute, QualityCleanupTask, QualityInitMode,
|
|
26
|
+
QualityInitResult, QualityMode, QualityReport, QualitySummary, QualityViolation,
|
|
27
|
+
RepositoryQualityConfig,
|
|
23
28
|
};
|
|
24
29
|
|
|
25
|
-
use self::baseline::{
|
|
30
|
+
use self::baseline::{
|
|
31
|
+
baseline_relative_path, read_baseline_fingerprints, write_baseline,
|
|
32
|
+
write_empty_baseline_if_missing,
|
|
33
|
+
};
|
|
26
34
|
use self::checks::run_quality_checks;
|
|
27
35
|
use self::config::{config_relative_path, read_config, write_default_config_if_missing};
|
|
36
|
+
use self::scanner::collect_repo_paths;
|
|
28
37
|
use self::scanner::scan_repository;
|
|
38
|
+
use self::semantic::run_semantic_checks;
|
|
29
39
|
use self::structure::{
|
|
30
40
|
run_repository_structure_checks, structure_config_relative_path,
|
|
31
41
|
write_default_structure_config_if_missing,
|
|
@@ -49,6 +59,10 @@ pub fn check_repository_quality(
|
|
|
49
59
|
.filter(|violation| violation.baseline)
|
|
50
60
|
.count();
|
|
51
61
|
let ok = blocking_violation_count == 0;
|
|
62
|
+
let mut reason_codes = context.reason_codes.clone();
|
|
63
|
+
if mode == QualityMode::Report {
|
|
64
|
+
reason_codes.push("deep_checks_skipped".to_string());
|
|
65
|
+
}
|
|
52
66
|
|
|
53
67
|
Ok(QualityReport {
|
|
54
68
|
schema: "naome.repository-quality-report.v1".to_string(),
|
|
@@ -58,30 +72,58 @@ pub fn check_repository_quality(
|
|
|
58
72
|
scanned_paths: context.scanned_paths(),
|
|
59
73
|
summary: QualitySummary {
|
|
60
74
|
scanned_files: context.files.len(),
|
|
75
|
+
scanned_path_count: context.scanned_paths().len(),
|
|
61
76
|
violation_count: violations.len(),
|
|
62
77
|
baseline_violation_count,
|
|
63
78
|
blocking_violation_count,
|
|
79
|
+
truncated: context.truncated,
|
|
80
|
+
reason_codes,
|
|
81
|
+
cache_hits: context.cache_hits,
|
|
82
|
+
cache_misses: context.cache_misses,
|
|
64
83
|
},
|
|
65
84
|
violations,
|
|
66
85
|
})
|
|
67
86
|
}
|
|
68
87
|
|
|
69
88
|
pub fn init_repository_quality(root: &Path) -> Result<QualityInitResult, NaomeError> {
|
|
89
|
+
init_repository_quality_with_mode(root, QualityInitMode::SeedOnly)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
pub fn init_repository_quality_with_mode(
|
|
93
|
+
root: &Path,
|
|
94
|
+
mode: QualityInitMode,
|
|
95
|
+
) -> Result<QualityInitResult, NaomeError> {
|
|
70
96
|
let config_written = write_default_config_if_missing(root)?;
|
|
71
97
|
let structure_config_written = {
|
|
72
|
-
let
|
|
73
|
-
|
|
74
|
-
|
|
98
|
+
let repo_paths = collect_repo_paths(root)?;
|
|
99
|
+
write_default_structure_config_if_missing(root, &repo_paths)?
|
|
100
|
+
};
|
|
101
|
+
let (baseline_written, baseline_violations, baseline_pending) = match mode {
|
|
102
|
+
QualityInitMode::SeedOnly => {
|
|
103
|
+
write_empty_baseline_if_missing(root)?;
|
|
104
|
+
(false, 0, true)
|
|
105
|
+
}
|
|
106
|
+
QualityInitMode::Baseline | QualityInitMode::DeepBaseline => {
|
|
107
|
+
let report = check_repository_quality(
|
|
108
|
+
root,
|
|
109
|
+
if mode == QualityInitMode::DeepBaseline {
|
|
110
|
+
QualityMode::DeepReport
|
|
111
|
+
} else {
|
|
112
|
+
QualityMode::Report
|
|
113
|
+
},
|
|
114
|
+
)?;
|
|
115
|
+
(write_baseline(root, &report.violations)?, report.violations.len(), false)
|
|
116
|
+
}
|
|
75
117
|
};
|
|
76
|
-
let report = check_repository_quality(root, QualityMode::Report)?;
|
|
77
|
-
let baseline_written = write_baseline(root, &report.violations)?;
|
|
78
118
|
|
|
79
119
|
Ok(QualityInitResult {
|
|
80
120
|
schema: "naome.repository-quality-init.v1".to_string(),
|
|
121
|
+
mode: mode.as_str().to_string(),
|
|
81
122
|
config_written,
|
|
82
123
|
structure_config_written,
|
|
83
124
|
baseline_written,
|
|
84
|
-
|
|
125
|
+
baseline_pending,
|
|
126
|
+
baseline_violations,
|
|
85
127
|
config_path: config_relative_path().to_string(),
|
|
86
128
|
structure_config_path: structure_config_relative_path().to_string(),
|
|
87
129
|
baseline_path: baseline_relative_path().to_string(),
|
|
@@ -106,3 +148,9 @@ pub fn route_quality_cleanup(
|
|
|
106
148
|
.collect::<Vec<_>>();
|
|
107
149
|
Ok(cleanup_route_for_path(&path, violations))
|
|
108
150
|
}
|
|
151
|
+
|
|
152
|
+
pub fn check_semantic_legacy(root: &Path, mode: QualityMode) -> Result<SemanticReport, NaomeError> {
|
|
153
|
+
let config = read_config(root)?;
|
|
154
|
+
let context = scan_repository(root, mode, config)?;
|
|
155
|
+
Ok(run_semantic_checks(&context))
|
|
156
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
pub(super) fn normalize_line(line: &str) -> Option<String> {
|
|
2
|
+
let trimmed = line.trim();
|
|
3
|
+
if trimmed.is_empty()
|
|
4
|
+
|| is_comment_only(trimmed)
|
|
5
|
+
|| is_string_list_item(trimmed)
|
|
6
|
+
|| is_generated_hash_mapping(trimmed)
|
|
7
|
+
{
|
|
8
|
+
return None;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let mut normalized = String::new();
|
|
12
|
+
let mut in_string = false;
|
|
13
|
+
let mut quote = '\0';
|
|
14
|
+
let mut previous_space = false;
|
|
15
|
+
for character in trimmed.chars() {
|
|
16
|
+
if in_string {
|
|
17
|
+
if character == quote {
|
|
18
|
+
in_string = false;
|
|
19
|
+
normalized.push('S');
|
|
20
|
+
previous_space = false;
|
|
21
|
+
}
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if character == '"' || character == '\'' || character == '`' {
|
|
25
|
+
in_string = true;
|
|
26
|
+
quote = character;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
let next = if character.is_ascii_digit() {
|
|
30
|
+
'0'
|
|
31
|
+
} else if character.is_whitespace() {
|
|
32
|
+
' '
|
|
33
|
+
} else {
|
|
34
|
+
character.to_ascii_lowercase()
|
|
35
|
+
};
|
|
36
|
+
if next == ' ' {
|
|
37
|
+
if !previous_space {
|
|
38
|
+
normalized.push(next);
|
|
39
|
+
}
|
|
40
|
+
previous_space = true;
|
|
41
|
+
} else {
|
|
42
|
+
normalized.push(next);
|
|
43
|
+
previous_space = false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
let value = normalized.trim().to_string();
|
|
47
|
+
(!value.is_empty()).then_some(value)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pub(super) fn token_set(line: &str) -> Vec<String> {
|
|
51
|
+
line.split(|character: char| !character.is_ascii_alphanumeric() && character != '_')
|
|
52
|
+
.filter(|token| token.len() > 1)
|
|
53
|
+
.map(ToString::to_string)
|
|
54
|
+
.collect()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn is_comment_only(trimmed: &str) -> bool {
|
|
58
|
+
trimmed.starts_with("//")
|
|
59
|
+
|| trimmed.starts_with('#')
|
|
60
|
+
|| trimmed.starts_with("/*")
|
|
61
|
+
|| trimmed.starts_with('*')
|
|
62
|
+
|| trimmed.starts_with("--")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fn is_generated_hash_mapping(trimmed: &str) -> bool {
|
|
66
|
+
let Some((key, value)) = trimmed.split_once(':') else {
|
|
67
|
+
return false;
|
|
68
|
+
};
|
|
69
|
+
key.trim_start().starts_with('"')
|
|
70
|
+
&& value.trim_start().starts_with("\"sha256:")
|
|
71
|
+
&& value.chars().filter(|character| *character == '"').count() >= 2
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fn is_string_list_item(trimmed: &str) -> bool {
|
|
75
|
+
let value = trimmed.trim_end_matches(',');
|
|
76
|
+
(value.starts_with('"') && value.ends_with('"'))
|
|
77
|
+
|| (value.starts_with('\'') && value.ends_with('\''))
|
|
78
|
+
}
|