@lamentis/naome 1.1.2 → 1.2.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/Cargo.toml +1 -1
- package/LICENSE +180 -21
- package/README.md +49 -6
- package/bin/naome.js +54 -16
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/check_commands.rs +135 -0
- package/crates/naome-cli/src/cli_args.rs +5 -0
- package/crates/naome-cli/src/dispatcher.rs +36 -0
- package/crates/naome-cli/src/install_bridge.rs +83 -0
- package/crates/naome-cli/src/main.rs +57 -341
- package/crates/naome-cli/src/prompt_commands.rs +68 -0
- package/crates/naome-cli/src/quality_commands.rs +141 -0
- package/crates/naome-cli/src/simple_commands.rs +53 -0
- package/crates/naome-cli/src/workflow_commands.rs +153 -0
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/harness_health/integrity.rs +96 -0
- package/crates/naome-core/src/harness_health.rs +14 -126
- package/crates/naome-core/src/install_plan.rs +3 -0
- package/crates/naome-core/src/intent/classifier.rs +171 -0
- package/crates/naome-core/src/intent/envelope.rs +108 -0
- package/crates/naome-core/src/intent/legacy.rs +138 -0
- package/crates/naome-core/src/intent/legacy_response.rs +76 -0
- package/crates/naome-core/src/intent/model.rs +71 -0
- package/crates/naome-core/src/intent/patterns.rs +170 -0
- package/crates/naome-core/src/intent/resolver.rs +162 -0
- package/crates/naome-core/src/intent/resolver_active.rs +17 -0
- package/crates/naome-core/src/intent/resolver_baseline.rs +55 -0
- package/crates/naome-core/src/intent/resolver_catalog.rs +167 -0
- package/crates/naome-core/src/intent/resolver_policy.rs +72 -0
- package/crates/naome-core/src/intent/resolver_shared.rs +55 -0
- package/crates/naome-core/src/intent/risk.rs +40 -0
- package/crates/naome-core/src/intent/segment.rs +170 -0
- package/crates/naome-core/src/intent.rs +64 -879
- package/crates/naome-core/src/journal.rs +9 -20
- package/crates/naome-core/src/lib.rs +13 -0
- package/crates/naome-core/src/quality/adapters.rs +178 -0
- package/crates/naome-core/src/quality/baseline.rs +75 -0
- package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +175 -0
- package/crates/naome-core/src/quality/checks/near_duplicates.rs +130 -0
- package/crates/naome-core/src/quality/checks.rs +228 -0
- package/crates/naome-core/src/quality/cleanup.rs +72 -0
- package/crates/naome-core/src/quality/config.rs +109 -0
- package/crates/naome-core/src/quality/mod.rs +90 -0
- package/crates/naome-core/src/quality/scanner/repo_paths.rs +103 -0
- package/crates/naome-core/src/quality/scanner.rs +367 -0
- package/crates/naome-core/src/quality/types.rs +289 -0
- package/crates/naome-core/src/route.rs +62 -0
- package/crates/naome-core/src/task_state/admission.rs +63 -0
- package/crates/naome-core/src/task_state/admission_proof.rs +72 -0
- package/crates/naome-core/src/task_state/api.rs +130 -0
- package/crates/naome-core/src/task_state/commit_gate.rs +138 -0
- package/crates/naome-core/src/task_state/compact_proof.rs +160 -0
- package/crates/naome-core/src/task_state/completed_refresh.rs +89 -0
- package/crates/naome-core/src/task_state/completion.rs +72 -0
- package/crates/naome-core/src/task_state/deleted_paths.rs +47 -0
- package/crates/naome-core/src/task_state/diff.rs +95 -0
- package/crates/naome-core/src/task_state/evidence.rs +154 -0
- package/crates/naome-core/src/task_state/git_io.rs +86 -0
- package/crates/naome-core/src/task_state/git_parse.rs +86 -0
- package/crates/naome-core/src/task_state/git_refs.rs +37 -0
- package/crates/naome-core/src/task_state/human_review_state.rs +31 -0
- package/crates/naome-core/src/task_state/mod.rs +38 -0
- package/crates/naome-core/src/task_state/process_guard.rs +40 -0
- package/crates/naome-core/src/task_state/progress.rs +123 -0
- package/crates/naome-core/src/task_state/proof.rs +139 -0
- package/crates/naome-core/src/task_state/proof_entry.rs +66 -0
- package/crates/naome-core/src/task_state/proof_model.rs +70 -0
- package/crates/naome-core/src/task_state/proof_sources.rs +76 -0
- package/crates/naome-core/src/task_state/push_gate.rs +49 -0
- package/crates/naome-core/src/task_state/reconcile.rs +7 -0
- package/crates/naome-core/src/task_state/repair.rs +168 -0
- package/crates/naome-core/src/task_state/shape.rs +117 -0
- package/crates/naome-core/src/task_state/task_diff_api.rs +170 -0
- package/crates/naome-core/src/task_state/task_records.rs +131 -0
- package/crates/naome-core/src/task_state/task_references.rs +126 -0
- package/crates/naome-core/src/task_state/types.rs +87 -0
- package/crates/naome-core/src/task_state/util.rs +137 -0
- package/crates/naome-core/src/verification/render.rs +122 -0
- package/crates/naome-core/src/verification.rs +176 -58
- package/crates/naome-core/src/verification_contract.rs +49 -21
- package/crates/naome-core/src/workflow/integrity.rs +123 -0
- package/crates/naome-core/src/workflow/integrity_normalize.rs +7 -0
- package/crates/naome-core/src/workflow/integrity_support.rs +110 -0
- package/crates/naome-core/src/workflow/mod.rs +18 -0
- package/crates/naome-core/src/workflow/mutation.rs +68 -0
- package/crates/naome-core/src/workflow/output.rs +111 -0
- package/crates/naome-core/src/workflow/phase_inference.rs +73 -0
- package/crates/naome-core/src/workflow/phases.rs +169 -0
- package/crates/naome-core/src/workflow/policy.rs +156 -0
- package/crates/naome-core/src/workflow/processes.rs +91 -0
- package/crates/naome-core/src/workflow/types.rs +42 -0
- package/crates/naome-core/tests/harness_health.rs +3 -0
- package/crates/naome-core/tests/intent.rs +97 -792
- package/crates/naome-core/tests/intent_support/mod.rs +133 -0
- package/crates/naome-core/tests/intent_v2.rs +90 -0
- package/crates/naome-core/tests/quality.rs +425 -0
- package/crates/naome-core/tests/route.rs +88 -188
- package/crates/naome-core/tests/task_state.rs +3 -0
- package/crates/naome-core/tests/task_state_compact.rs +110 -0
- package/crates/naome-core/tests/task_state_compact_support/mod.rs +5 -0
- package/crates/naome-core/tests/task_state_compact_support/repo.rs +130 -0
- package/crates/naome-core/tests/task_state_compact_support/states.rs +151 -0
- package/crates/naome-core/tests/workflow_integrity.rs +85 -0
- package/crates/naome-core/tests/workflow_policy.rs +139 -0
- package/crates/naome-core/tests/workflow_support/mod.rs +194 -0
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +2 -2
- package/templates/naome-root/.naome/bin/check-harness-health.js +66 -85
- package/templates/naome-root/.naome/bin/check-task-state.js +9 -10
- package/templates/naome-root/.naome/bin/naome.js +34 -63
- package/templates/naome-root/.naome/manifest.json +20 -18
- package/templates/naome-root/.naome/repository-quality-baseline.json +5 -0
- package/templates/naome-root/.naome/repository-quality.json +24 -0
- package/templates/naome-root/.naome/task-contract.schema.json +93 -11
- package/templates/naome-root/.naome/upgrade-state.json +1 -1
- package/templates/naome-root/.naome/verification.json +37 -0
- package/templates/naome-root/AGENTS.md +3 -0
- package/templates/naome-root/docs/naome/agent-workflow.md +25 -12
- package/templates/naome-root/docs/naome/execution.md +25 -21
- package/templates/naome-root/docs/naome/index.md +4 -3
- package/templates/naome-root/docs/naome/repository-quality.md +43 -0
- package/templates/naome-root/docs/naome/testing.md +12 -0
- package/crates/naome-core/src/task_state.rs +0 -2210
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
mod duplicate_blocks;
|
|
2
|
+
mod near_duplicates;
|
|
3
|
+
|
|
4
|
+
use std::collections::HashSet;
|
|
5
|
+
|
|
6
|
+
use super::scanner::{stable_fingerprint, QualityContext};
|
|
7
|
+
use super::types::{QualityMode, QualityViolation};
|
|
8
|
+
use duplicate_blocks::DuplicateBlockCheck;
|
|
9
|
+
use near_duplicates::NearDuplicateFunctionCheck;
|
|
10
|
+
|
|
11
|
+
pub fn run_quality_checks(context: &QualityContext) -> Vec<QualityViolation> {
|
|
12
|
+
let checks = registry();
|
|
13
|
+
let disabled = context
|
|
14
|
+
.config
|
|
15
|
+
.disabled_checks
|
|
16
|
+
.iter()
|
|
17
|
+
.map(String::as_str)
|
|
18
|
+
.collect::<HashSet<_>>();
|
|
19
|
+
let mut violations = Vec::new();
|
|
20
|
+
|
|
21
|
+
for check in checks {
|
|
22
|
+
if !disabled.contains(check.id()) {
|
|
23
|
+
check.evaluate(context, &mut violations);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
violations.sort_by(|left, right| {
|
|
28
|
+
left.path
|
|
29
|
+
.cmp(&right.path)
|
|
30
|
+
.then(left.check_id.cmp(&right.check_id))
|
|
31
|
+
.then(left.line.cmp(&right.line))
|
|
32
|
+
.then(left.fingerprint.cmp(&right.fingerprint))
|
|
33
|
+
});
|
|
34
|
+
violations
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pub(super) trait QualityCheck {
|
|
38
|
+
fn id(&self) -> &'static str;
|
|
39
|
+
fn evaluate(&self, context: &QualityContext, violations: &mut Vec<QualityViolation>);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn registry() -> Vec<Box<dyn QualityCheck>> {
|
|
43
|
+
vec![
|
|
44
|
+
Box::new(FileLengthCheck),
|
|
45
|
+
Box::new(DiffGrowthCheck),
|
|
46
|
+
Box::new(FunctionLengthCheck),
|
|
47
|
+
Box::new(TopLevelSymbolCountCheck),
|
|
48
|
+
Box::new(DuplicateBlockCheck),
|
|
49
|
+
Box::new(NearDuplicateFunctionCheck),
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
struct FileLengthCheck;
|
|
54
|
+
|
|
55
|
+
impl QualityCheck for FileLengthCheck {
|
|
56
|
+
fn id(&self) -> &'static str {
|
|
57
|
+
"file-length"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fn evaluate(&self, context: &QualityContext, violations: &mut Vec<QualityViolation>) {
|
|
61
|
+
for file in context
|
|
62
|
+
.files
|
|
63
|
+
.iter()
|
|
64
|
+
.filter(|file| context.check_applies_to(self.id(), &file.path))
|
|
65
|
+
{
|
|
66
|
+
let limits = context.limits_for(&file.path);
|
|
67
|
+
let limit =
|
|
68
|
+
if context.mode == QualityMode::Changed && file.added_lines == file.line_count {
|
|
69
|
+
limits.max_new_file_lines
|
|
70
|
+
} else {
|
|
71
|
+
limits.max_file_lines
|
|
72
|
+
};
|
|
73
|
+
if file.line_count > limit {
|
|
74
|
+
violations.push(violation(
|
|
75
|
+
self.id(),
|
|
76
|
+
&file.path,
|
|
77
|
+
None,
|
|
78
|
+
format!(
|
|
79
|
+
"{} has {} lines, exceeding the configured limit of {}.",
|
|
80
|
+
file.path, file.line_count, limit
|
|
81
|
+
),
|
|
82
|
+
Some(file.line_count as f64),
|
|
83
|
+
Some(limit as f64),
|
|
84
|
+
Vec::new(),
|
|
85
|
+
));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
struct DiffGrowthCheck;
|
|
92
|
+
|
|
93
|
+
impl QualityCheck for DiffGrowthCheck {
|
|
94
|
+
fn id(&self) -> &'static str {
|
|
95
|
+
"diff-growth"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fn evaluate(&self, context: &QualityContext, violations: &mut Vec<QualityViolation>) {
|
|
99
|
+
if context.mode != QualityMode::Changed {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
for file in context
|
|
103
|
+
.files
|
|
104
|
+
.iter()
|
|
105
|
+
.filter(|file| context.check_applies_to(self.id(), &file.path))
|
|
106
|
+
{
|
|
107
|
+
let limit = context.limits_for(&file.path).max_diff_added_lines;
|
|
108
|
+
if file.added_lines > limit {
|
|
109
|
+
violations.push(violation(
|
|
110
|
+
self.id(),
|
|
111
|
+
&file.path,
|
|
112
|
+
None,
|
|
113
|
+
format!(
|
|
114
|
+
"{} adds {} lines in this task, exceeding the configured limit of {}.",
|
|
115
|
+
file.path, file.added_lines, limit
|
|
116
|
+
),
|
|
117
|
+
Some(file.added_lines as f64),
|
|
118
|
+
Some(limit as f64),
|
|
119
|
+
Vec::new(),
|
|
120
|
+
));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
struct FunctionLengthCheck;
|
|
127
|
+
|
|
128
|
+
impl QualityCheck for FunctionLengthCheck {
|
|
129
|
+
fn id(&self) -> &'static str {
|
|
130
|
+
"function-length"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fn evaluate(&self, context: &QualityContext, violations: &mut Vec<QualityViolation>) {
|
|
134
|
+
for file in context.files.iter().filter(|file| {
|
|
135
|
+
context.check_applies_to(self.id(), &file.path) && is_code_like_path(&file.path)
|
|
136
|
+
}) {
|
|
137
|
+
let limit = context.limits_for(&file.path).max_function_lines;
|
|
138
|
+
for symbol in &file.symbols {
|
|
139
|
+
if symbol.line_count() > limit {
|
|
140
|
+
violations.push(violation(
|
|
141
|
+
self.id(),
|
|
142
|
+
&file.path,
|
|
143
|
+
Some(symbol.start_line),
|
|
144
|
+
format!(
|
|
145
|
+
"{} {} has {} lines, exceeding the configured limit of {}.",
|
|
146
|
+
symbol.kind,
|
|
147
|
+
symbol.name,
|
|
148
|
+
symbol.line_count(),
|
|
149
|
+
limit
|
|
150
|
+
),
|
|
151
|
+
Some(symbol.line_count() as f64),
|
|
152
|
+
Some(limit as f64),
|
|
153
|
+
Vec::new(),
|
|
154
|
+
));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
struct TopLevelSymbolCountCheck;
|
|
162
|
+
|
|
163
|
+
impl QualityCheck for TopLevelSymbolCountCheck {
|
|
164
|
+
fn id(&self) -> &'static str {
|
|
165
|
+
"top-level-symbol-count"
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fn evaluate(&self, context: &QualityContext, violations: &mut Vec<QualityViolation>) {
|
|
169
|
+
for file in context.files.iter().filter(|file| {
|
|
170
|
+
context.check_applies_to(self.id(), &file.path) && is_code_like_path(&file.path)
|
|
171
|
+
}) {
|
|
172
|
+
let limit = context.limits_for(&file.path).max_top_level_symbols;
|
|
173
|
+
let count = file
|
|
174
|
+
.symbols
|
|
175
|
+
.iter()
|
|
176
|
+
.filter(|symbol| symbol.indent <= 2)
|
|
177
|
+
.count();
|
|
178
|
+
if count > limit {
|
|
179
|
+
violations.push(violation(
|
|
180
|
+
self.id(),
|
|
181
|
+
&file.path,
|
|
182
|
+
None,
|
|
183
|
+
format!(
|
|
184
|
+
"{} defines {} top-level symbols, exceeding the configured limit of {}.",
|
|
185
|
+
file.path, count, limit
|
|
186
|
+
),
|
|
187
|
+
Some(count as f64),
|
|
188
|
+
Some(limit as f64),
|
|
189
|
+
Vec::new(),
|
|
190
|
+
));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
pub(super) fn is_code_like_path(path: &str) -> bool {
|
|
197
|
+
let lower = path.to_ascii_lowercase();
|
|
198
|
+
[
|
|
199
|
+
".rs", ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".py", ".go", ".swift", ".kt",
|
|
200
|
+
".java", ".c", ".cc", ".cpp", ".h", ".hpp", ".rb", ".php", ".cs", ".sh", ".bash", ".zsh",
|
|
201
|
+
]
|
|
202
|
+
.iter()
|
|
203
|
+
.any(|extension| lower.ends_with(extension))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
pub(super) fn violation(
|
|
207
|
+
check_id: &str,
|
|
208
|
+
path: &str,
|
|
209
|
+
line: Option<usize>,
|
|
210
|
+
message: String,
|
|
211
|
+
value: Option<f64>,
|
|
212
|
+
limit: Option<f64>,
|
|
213
|
+
related_paths: Vec<String>,
|
|
214
|
+
) -> QualityViolation {
|
|
215
|
+
let line_part = line.map(|value| value.to_string()).unwrap_or_default();
|
|
216
|
+
QualityViolation {
|
|
217
|
+
check_id: check_id.to_string(),
|
|
218
|
+
severity: "blocking".to_string(),
|
|
219
|
+
path: path.to_string(),
|
|
220
|
+
line,
|
|
221
|
+
message: message.clone(),
|
|
222
|
+
value,
|
|
223
|
+
limit,
|
|
224
|
+
fingerprint: stable_fingerprint(&[check_id, path, &line_part, &message]),
|
|
225
|
+
related_paths,
|
|
226
|
+
baseline: false,
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
2
|
+
|
|
3
|
+
use super::types::{QualityCleanupPlan, QualityCleanupRoute, QualityCleanupTask, QualityViolation};
|
|
4
|
+
|
|
5
|
+
pub fn cleanup_plan_from_violations(violations: &[QualityViolation]) -> QualityCleanupPlan {
|
|
6
|
+
let mut grouped: BTreeMap<String, Vec<&QualityViolation>> = BTreeMap::new();
|
|
7
|
+
for violation in violations {
|
|
8
|
+
grouped
|
|
9
|
+
.entry(violation.path.clone())
|
|
10
|
+
.or_default()
|
|
11
|
+
.push(violation);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let mut tasks = grouped
|
|
15
|
+
.into_iter()
|
|
16
|
+
.map(|(path, violations)| {
|
|
17
|
+
let check_ids = violations
|
|
18
|
+
.iter()
|
|
19
|
+
.map(|violation| violation.check_id.clone())
|
|
20
|
+
.collect::<BTreeSet<_>>()
|
|
21
|
+
.into_iter()
|
|
22
|
+
.collect::<Vec<_>>();
|
|
23
|
+
QualityCleanupTask {
|
|
24
|
+
path,
|
|
25
|
+
violation_count: violations.len(),
|
|
26
|
+
summary: format!(
|
|
27
|
+
"Resolve {} repository-quality violation(s): {}.",
|
|
28
|
+
violations.len(),
|
|
29
|
+
check_ids.join(", ")
|
|
30
|
+
),
|
|
31
|
+
check_ids,
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
.collect::<Vec<_>>();
|
|
35
|
+
tasks.sort_by(|left, right| {
|
|
36
|
+
right
|
|
37
|
+
.violation_count
|
|
38
|
+
.cmp(&left.violation_count)
|
|
39
|
+
.then(left.path.cmp(&right.path))
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
QualityCleanupPlan {
|
|
43
|
+
schema: "naome.quality-cleanup-plan.v1".to_string(),
|
|
44
|
+
tasks,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pub fn cleanup_route_for_path(
|
|
49
|
+
path: &str,
|
|
50
|
+
violations: Vec<QualityViolation>,
|
|
51
|
+
) -> QualityCleanupRoute {
|
|
52
|
+
let mut related_paths = violations
|
|
53
|
+
.iter()
|
|
54
|
+
.flat_map(|violation| violation.related_paths.clone())
|
|
55
|
+
.collect::<BTreeSet<_>>()
|
|
56
|
+
.into_iter()
|
|
57
|
+
.collect::<Vec<_>>();
|
|
58
|
+
related_paths.retain(|related| related != path);
|
|
59
|
+
QualityCleanupRoute {
|
|
60
|
+
schema: "naome.quality-cleanup-route.v1".to_string(),
|
|
61
|
+
path: path.to_string(),
|
|
62
|
+
violations,
|
|
63
|
+
related_paths,
|
|
64
|
+
agent_instructions: format!(
|
|
65
|
+
"Reduce or split {path} until every repository-quality violation is gone. Prefer extracting reusable helpers/components over copying code. Keep behavior unchanged, add or preserve focused tests, then run naome quality check --changed before task completion."
|
|
66
|
+
),
|
|
67
|
+
required_checks: vec![
|
|
68
|
+
"naome quality check --changed".to_string(),
|
|
69
|
+
"git diff --check".to_string(),
|
|
70
|
+
],
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
use std::fs;
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
|
|
4
|
+
use crate::models::NaomeError;
|
|
5
|
+
|
|
6
|
+
use super::adapters::{apply_enabled_adapters, detected_adapter_ids, validate_adapter_ids};
|
|
7
|
+
use super::scanner::collect_repo_paths;
|
|
8
|
+
use super::types::RepositoryQualityConfig;
|
|
9
|
+
|
|
10
|
+
const CONFIG_RELATIVE_PATH: &str = ".naome/repository-quality.json";
|
|
11
|
+
|
|
12
|
+
pub fn config_relative_path() -> &'static str {
|
|
13
|
+
CONFIG_RELATIVE_PATH
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
pub fn read_config(root: &Path) -> Result<RepositoryQualityConfig, NaomeError> {
|
|
17
|
+
let path = root.join(CONFIG_RELATIVE_PATH);
|
|
18
|
+
let config = if path.is_file() {
|
|
19
|
+
serde_json::from_str(&fs::read_to_string(path)?)?
|
|
20
|
+
} else {
|
|
21
|
+
generated_config(root)?
|
|
22
|
+
};
|
|
23
|
+
validate_config(&config)?;
|
|
24
|
+
apply_enabled_adapters(config)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
pub fn write_default_config_if_missing(root: &Path) -> Result<bool, NaomeError> {
|
|
28
|
+
let path = root.join(CONFIG_RELATIVE_PATH);
|
|
29
|
+
if path.exists() {
|
|
30
|
+
return Ok(false);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if let Some(parent) = path.parent() {
|
|
34
|
+
fs::create_dir_all(parent)?;
|
|
35
|
+
}
|
|
36
|
+
let content = serde_json::to_string_pretty(&generated_config(root)?)?;
|
|
37
|
+
fs::write(path, format!("{content}\n"))?;
|
|
38
|
+
Ok(true)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fn generated_config(root: &Path) -> Result<RepositoryQualityConfig, NaomeError> {
|
|
42
|
+
let paths = collect_repo_paths(root)?;
|
|
43
|
+
let mut config = RepositoryQualityConfig::default();
|
|
44
|
+
config.enabled_adapters = detected_adapter_ids(&paths);
|
|
45
|
+
Ok(config)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fn validate_config(config: &RepositoryQualityConfig) -> Result<(), NaomeError> {
|
|
49
|
+
if config.schema != "naome.repository-quality.v1" {
|
|
50
|
+
return Err(NaomeError::new(
|
|
51
|
+
".naome/repository-quality.json schema must be naome.repository-quality.v1.",
|
|
52
|
+
));
|
|
53
|
+
}
|
|
54
|
+
if config.version != 1 {
|
|
55
|
+
return Err(NaomeError::new(
|
|
56
|
+
".naome/repository-quality.json version must be 1.",
|
|
57
|
+
));
|
|
58
|
+
}
|
|
59
|
+
if config.status != "ready" {
|
|
60
|
+
return Err(NaomeError::new(
|
|
61
|
+
".naome/repository-quality.json status must be ready.",
|
|
62
|
+
));
|
|
63
|
+
}
|
|
64
|
+
if config.limits.duplicate_block_lines < 3 {
|
|
65
|
+
return Err(NaomeError::new(
|
|
66
|
+
".naome/repository-quality.json duplicateBlockLines must be at least 3.",
|
|
67
|
+
));
|
|
68
|
+
}
|
|
69
|
+
if !(0.5..=1.0).contains(&config.limits.near_duplicate_similarity) {
|
|
70
|
+
return Err(NaomeError::new(
|
|
71
|
+
".naome/repository-quality.json nearDuplicateSimilarity must be between 0.5 and 1.0.",
|
|
72
|
+
));
|
|
73
|
+
}
|
|
74
|
+
validate_adapter_ids(&config.enabled_adapters)?;
|
|
75
|
+
for rule in &config.path_rules {
|
|
76
|
+
if rule.id.trim().is_empty() {
|
|
77
|
+
return Err(NaomeError::new(
|
|
78
|
+
".naome/repository-quality.json pathRules entries must have a non-empty id.",
|
|
79
|
+
));
|
|
80
|
+
}
|
|
81
|
+
if rule.paths.is_empty() {
|
|
82
|
+
return Err(NaomeError::new(format!(
|
|
83
|
+
".naome/repository-quality.json pathRules.{} must list at least one path.",
|
|
84
|
+
rule.id
|
|
85
|
+
)));
|
|
86
|
+
}
|
|
87
|
+
if rule
|
|
88
|
+
.limits
|
|
89
|
+
.duplicate_block_lines
|
|
90
|
+
.is_some_and(|value| value < 3)
|
|
91
|
+
{
|
|
92
|
+
return Err(NaomeError::new(format!(
|
|
93
|
+
".naome/repository-quality.json pathRules.{} duplicateBlockLines must be at least 3.",
|
|
94
|
+
rule.id
|
|
95
|
+
)));
|
|
96
|
+
}
|
|
97
|
+
if rule
|
|
98
|
+
.limits
|
|
99
|
+
.near_duplicate_similarity
|
|
100
|
+
.is_some_and(|value| !(0.5..=1.0).contains(&value))
|
|
101
|
+
{
|
|
102
|
+
return Err(NaomeError::new(format!(
|
|
103
|
+
".naome/repository-quality.json pathRules.{} nearDuplicateSimilarity must be between 0.5 and 1.0.",
|
|
104
|
+
rule.id
|
|
105
|
+
)));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
Ok(())
|
|
109
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
mod adapters;
|
|
2
|
+
mod baseline;
|
|
3
|
+
mod checks;
|
|
4
|
+
mod cleanup;
|
|
5
|
+
mod config;
|
|
6
|
+
mod scanner;
|
|
7
|
+
mod types;
|
|
8
|
+
|
|
9
|
+
use std::path::Path;
|
|
10
|
+
|
|
11
|
+
use crate::models::NaomeError;
|
|
12
|
+
|
|
13
|
+
pub use cleanup::{cleanup_plan_from_violations, cleanup_route_for_path};
|
|
14
|
+
pub use types::{
|
|
15
|
+
QualityCleanupPlan, QualityCleanupRoute, QualityCleanupTask, QualityInitResult, QualityMode,
|
|
16
|
+
QualityReport, QualitySummary, QualityViolation, RepositoryQualityConfig,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
use self::baseline::{baseline_relative_path, read_baseline_fingerprints, write_baseline};
|
|
20
|
+
use self::checks::run_quality_checks;
|
|
21
|
+
use self::config::{config_relative_path, read_config, write_default_config_if_missing};
|
|
22
|
+
use self::scanner::scan_repository;
|
|
23
|
+
|
|
24
|
+
pub fn check_repository_quality(
|
|
25
|
+
root: &Path,
|
|
26
|
+
mode: QualityMode,
|
|
27
|
+
) -> Result<QualityReport, NaomeError> {
|
|
28
|
+
let config = read_config(root)?;
|
|
29
|
+
let context = scan_repository(root, mode, config)?;
|
|
30
|
+
let baseline = read_baseline_fingerprints(root)?;
|
|
31
|
+
let mut violations = run_quality_checks(&context);
|
|
32
|
+
for violation in &mut violations {
|
|
33
|
+
violation.baseline = baseline.contains(&violation.fingerprint);
|
|
34
|
+
}
|
|
35
|
+
let blocking_violation_count = violations.len();
|
|
36
|
+
let baseline_violation_count = violations
|
|
37
|
+
.iter()
|
|
38
|
+
.filter(|violation| violation.baseline)
|
|
39
|
+
.count();
|
|
40
|
+
let ok = blocking_violation_count == 0;
|
|
41
|
+
|
|
42
|
+
Ok(QualityReport {
|
|
43
|
+
schema: "naome.repository-quality-report.v1".to_string(),
|
|
44
|
+
mode: mode.as_str().to_string(),
|
|
45
|
+
ok,
|
|
46
|
+
changed_paths: context.changed_paths.clone(),
|
|
47
|
+
scanned_paths: context.scanned_paths(),
|
|
48
|
+
summary: QualitySummary {
|
|
49
|
+
scanned_files: context.files.len(),
|
|
50
|
+
violation_count: violations.len(),
|
|
51
|
+
baseline_violation_count,
|
|
52
|
+
blocking_violation_count,
|
|
53
|
+
},
|
|
54
|
+
violations,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
pub fn init_repository_quality(root: &Path) -> Result<QualityInitResult, NaomeError> {
|
|
59
|
+
let config_written = write_default_config_if_missing(root)?;
|
|
60
|
+
let report = check_repository_quality(root, QualityMode::Report)?;
|
|
61
|
+
let baseline_written = write_baseline(root, &report.violations)?;
|
|
62
|
+
|
|
63
|
+
Ok(QualityInitResult {
|
|
64
|
+
schema: "naome.repository-quality-init.v1".to_string(),
|
|
65
|
+
config_written,
|
|
66
|
+
baseline_written,
|
|
67
|
+
baseline_violations: report.violations.len(),
|
|
68
|
+
config_path: config_relative_path().to_string(),
|
|
69
|
+
baseline_path: baseline_relative_path().to_string(),
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
pub fn plan_quality_cleanup(root: &Path) -> Result<QualityCleanupPlan, NaomeError> {
|
|
74
|
+
let report = check_repository_quality(root, QualityMode::Report)?;
|
|
75
|
+
Ok(cleanup_plan_from_violations(&report.violations))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
pub fn route_quality_cleanup(
|
|
79
|
+
root: &Path,
|
|
80
|
+
path: impl AsRef<str>,
|
|
81
|
+
) -> Result<QualityCleanupRoute, NaomeError> {
|
|
82
|
+
let path = path.as_ref().replace('\\', "/");
|
|
83
|
+
let report = check_repository_quality(root, QualityMode::Report)?;
|
|
84
|
+
let violations = report
|
|
85
|
+
.violations
|
|
86
|
+
.into_iter()
|
|
87
|
+
.filter(|violation| violation.path == path)
|
|
88
|
+
.collect::<Vec<_>>();
|
|
89
|
+
Ok(cleanup_route_for_path(&path, violations))
|
|
90
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
use std::fs;
|
|
3
|
+
use std::path::Path;
|
|
4
|
+
use std::process::Command;
|
|
5
|
+
|
|
6
|
+
use crate::models::NaomeError;
|
|
7
|
+
|
|
8
|
+
pub(crate) fn collect_repo_paths(root: &Path) -> Result<Vec<String>, NaomeError> {
|
|
9
|
+
let output = Command::new("git")
|
|
10
|
+
.args([
|
|
11
|
+
"ls-files",
|
|
12
|
+
"-z",
|
|
13
|
+
"--cached",
|
|
14
|
+
"--others",
|
|
15
|
+
"--exclude-standard",
|
|
16
|
+
])
|
|
17
|
+
.current_dir(root)
|
|
18
|
+
.output();
|
|
19
|
+
if let Ok(output) = output {
|
|
20
|
+
if output.status.success() {
|
|
21
|
+
let mut paths = output
|
|
22
|
+
.stdout
|
|
23
|
+
.split(|byte| *byte == 0)
|
|
24
|
+
.filter(|entry| !entry.is_empty())
|
|
25
|
+
.map(|entry| String::from_utf8_lossy(entry).replace('\\', "/"))
|
|
26
|
+
.collect::<Vec<_>>();
|
|
27
|
+
paths.sort();
|
|
28
|
+
paths.dedup();
|
|
29
|
+
return Ok(paths);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let mut paths = Vec::new();
|
|
34
|
+
collect_files_recursive(root, root, &mut paths)?;
|
|
35
|
+
paths.sort();
|
|
36
|
+
Ok(paths)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
pub(super) fn added_lines_by_path(root: &Path) -> Result<HashMap<String, usize>, NaomeError> {
|
|
40
|
+
let mut added = HashMap::new();
|
|
41
|
+
for args in [
|
|
42
|
+
vec!["diff", "--numstat"],
|
|
43
|
+
vec!["diff", "--cached", "--numstat"],
|
|
44
|
+
] {
|
|
45
|
+
let output = Command::new("git").args(args).current_dir(root).output()?;
|
|
46
|
+
if !output.status.success() {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
|
50
|
+
let mut parts = line.split('\t');
|
|
51
|
+
let Some(additions) = parts.next() else {
|
|
52
|
+
continue;
|
|
53
|
+
};
|
|
54
|
+
let _deletions = parts.next();
|
|
55
|
+
let Some(path) = parts.next() else { continue };
|
|
56
|
+
let Ok(count) = additions.parse::<usize>() else {
|
|
57
|
+
continue;
|
|
58
|
+
};
|
|
59
|
+
*added.entry(path.replace('\\', "/")).or_insert(0) += count;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
for path in untracked_paths(root)? {
|
|
63
|
+
if let Ok(content) = fs::read_to_string(root.join(&path)) {
|
|
64
|
+
added.insert(path, content.lines().count());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
Ok(added)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn collect_files_recursive(
|
|
71
|
+
root: &Path,
|
|
72
|
+
dir: &Path,
|
|
73
|
+
paths: &mut Vec<String>,
|
|
74
|
+
) -> Result<(), NaomeError> {
|
|
75
|
+
for entry in fs::read_dir(dir)? {
|
|
76
|
+
let entry = entry?;
|
|
77
|
+
let path = entry.path();
|
|
78
|
+
if path.is_dir() {
|
|
79
|
+
collect_files_recursive(root, &path, paths)?;
|
|
80
|
+
} else if path.is_file() {
|
|
81
|
+
if let Ok(relative) = path.strip_prefix(root) {
|
|
82
|
+
paths.push(relative.to_string_lossy().replace('\\', "/"));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
Ok(())
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fn untracked_paths(root: &Path) -> Result<Vec<String>, NaomeError> {
|
|
90
|
+
let output = Command::new("git")
|
|
91
|
+
.args(["ls-files", "--others", "--exclude-standard", "-z"])
|
|
92
|
+
.current_dir(root)
|
|
93
|
+
.output()?;
|
|
94
|
+
if !output.status.success() {
|
|
95
|
+
return Ok(Vec::new());
|
|
96
|
+
}
|
|
97
|
+
Ok(output
|
|
98
|
+
.stdout
|
|
99
|
+
.split(|byte| *byte == 0)
|
|
100
|
+
.filter(|entry| !entry.is_empty())
|
|
101
|
+
.map(|entry| String::from_utf8_lossy(entry).replace('\\', "/"))
|
|
102
|
+
.collect())
|
|
103
|
+
}
|