@lamentis/naome 1.1.1 → 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-node.js +44 -4
- 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 +292 -17
- 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 +221 -4
- 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,367 @@
|
|
|
1
|
+
mod repo_paths;
|
|
2
|
+
|
|
3
|
+
use std::collections::{HashMap, HashSet};
|
|
4
|
+
use std::fs;
|
|
5
|
+
use std::path::Path;
|
|
6
|
+
|
|
7
|
+
use sha2::{Digest, Sha256};
|
|
8
|
+
|
|
9
|
+
use crate::{git, models::NaomeError, paths};
|
|
10
|
+
use repo_paths::added_lines_by_path;
|
|
11
|
+
pub(crate) use repo_paths::collect_repo_paths;
|
|
12
|
+
|
|
13
|
+
use super::types::{
|
|
14
|
+
default_generated_paths, default_ignored_paths, QualityLimits, QualityMode,
|
|
15
|
+
RepositoryQualityConfig,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
#[derive(Debug, Clone)]
|
|
19
|
+
pub struct QualityContext {
|
|
20
|
+
pub mode: QualityMode,
|
|
21
|
+
pub config: RepositoryQualityConfig,
|
|
22
|
+
pub changed_paths: Vec<String>,
|
|
23
|
+
pub target_paths: HashSet<String>,
|
|
24
|
+
pub files: Vec<FileAnalysis>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl QualityContext {
|
|
28
|
+
pub fn scanned_paths(&self) -> Vec<String> {
|
|
29
|
+
self.files.iter().map(|file| file.path.clone()).collect()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
pub fn applies_to(&self, path: &str) -> bool {
|
|
33
|
+
self.mode == QualityMode::Report || self.target_paths.contains(path)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pub fn check_applies_to(&self, check_id: &str, path: &str) -> bool {
|
|
37
|
+
self.applies_to(path) && self.config.check_enabled_for_path(check_id, path)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
pub fn limits_for(&self, path: &str) -> QualityLimits {
|
|
41
|
+
self.config.limits_for_path(path)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[derive(Debug, Clone)]
|
|
46
|
+
pub struct FileAnalysis {
|
|
47
|
+
pub path: String,
|
|
48
|
+
pub line_count: usize,
|
|
49
|
+
pub added_lines: usize,
|
|
50
|
+
pub normalized_lines: Vec<NormalizedLine>,
|
|
51
|
+
pub symbols: Vec<SymbolAnalysis>,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#[derive(Debug, Clone)]
|
|
55
|
+
pub struct NormalizedLine {
|
|
56
|
+
pub line_number: usize,
|
|
57
|
+
pub value: String,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[derive(Debug, Clone)]
|
|
61
|
+
pub struct SymbolAnalysis {
|
|
62
|
+
pub kind: String,
|
|
63
|
+
pub name: String,
|
|
64
|
+
pub start_line: usize,
|
|
65
|
+
pub end_line: usize,
|
|
66
|
+
pub indent: usize,
|
|
67
|
+
pub tokens: HashSet<String>,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
impl SymbolAnalysis {
|
|
71
|
+
pub fn line_count(&self) -> usize {
|
|
72
|
+
self.end_line.saturating_sub(self.start_line) + 1
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
pub fn scan_repository(
|
|
77
|
+
root: &Path,
|
|
78
|
+
mode: QualityMode,
|
|
79
|
+
config: RepositoryQualityConfig,
|
|
80
|
+
) -> Result<QualityContext, NaomeError> {
|
|
81
|
+
let changed_paths = git::changed_paths(root)?;
|
|
82
|
+
let target_paths = changed_paths.iter().cloned().collect::<HashSet<_>>();
|
|
83
|
+
let scan_paths = match mode {
|
|
84
|
+
QualityMode::Changed => changed_paths.clone(),
|
|
85
|
+
QualityMode::Report => collect_repo_paths(root)?,
|
|
86
|
+
};
|
|
87
|
+
let added_lines = added_lines_by_path(root)?;
|
|
88
|
+
let ignore_patterns = ignore_patterns(root, &config);
|
|
89
|
+
let mut files = Vec::new();
|
|
90
|
+
|
|
91
|
+
for path in scan_paths {
|
|
92
|
+
if should_skip_path(&path, &ignore_patterns) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if let Some(file) = analyze_repo_file(root, &path, &added_lines) {
|
|
96
|
+
files.push(file);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if mode == QualityMode::Changed {
|
|
101
|
+
let mut whole_repo_paths = collect_repo_paths(root)?;
|
|
102
|
+
whole_repo_paths.sort();
|
|
103
|
+
whole_repo_paths.dedup();
|
|
104
|
+
let scanned = files
|
|
105
|
+
.iter()
|
|
106
|
+
.map(|file| file.path.clone())
|
|
107
|
+
.collect::<HashSet<_>>();
|
|
108
|
+
for path in whole_repo_paths {
|
|
109
|
+
if scanned.contains(&path) || should_skip_path(&path, &ignore_patterns) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if let Some(file) = analyze_repo_file(root, &path, &added_lines) {
|
|
113
|
+
files.push(file);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
files.sort_by(|left, right| left.path.cmp(&right.path));
|
|
119
|
+
Ok(QualityContext {
|
|
120
|
+
mode,
|
|
121
|
+
config,
|
|
122
|
+
changed_paths,
|
|
123
|
+
target_paths,
|
|
124
|
+
files,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
pub fn stable_fingerprint(parts: &[&str]) -> String {
|
|
129
|
+
let mut hasher = Sha256::new();
|
|
130
|
+
for part in parts {
|
|
131
|
+
hasher.update(part.as_bytes());
|
|
132
|
+
hasher.update(b"\0");
|
|
133
|
+
}
|
|
134
|
+
format!("sha256:{:x}", hasher.finalize())
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fn analyze_repo_file(
|
|
138
|
+
root: &Path,
|
|
139
|
+
path: &str,
|
|
140
|
+
added_lines: &HashMap<String, usize>,
|
|
141
|
+
) -> Option<FileAnalysis> {
|
|
142
|
+
let full_path = root.join(path);
|
|
143
|
+
if !full_path.is_file() || is_binary_extension(path) {
|
|
144
|
+
return None;
|
|
145
|
+
}
|
|
146
|
+
let content = fs::read_to_string(&full_path).ok()?;
|
|
147
|
+
Some(analyze_file(
|
|
148
|
+
path,
|
|
149
|
+
&content,
|
|
150
|
+
added_lines.get(path).copied().unwrap_or(0),
|
|
151
|
+
))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fn analyze_file(path: &str, content: &str, added_lines: usize) -> FileAnalysis {
|
|
155
|
+
let lines = content.lines().collect::<Vec<_>>();
|
|
156
|
+
let normalized_lines = lines
|
|
157
|
+
.iter()
|
|
158
|
+
.enumerate()
|
|
159
|
+
.filter_map(|(index, line)| normalize_line(line).map(|value| (index + 1, value)))
|
|
160
|
+
.map(|(line_number, value)| NormalizedLine { line_number, value })
|
|
161
|
+
.collect::<Vec<_>>();
|
|
162
|
+
let symbols = detect_symbols(&lines);
|
|
163
|
+
FileAnalysis {
|
|
164
|
+
path: path.to_string(),
|
|
165
|
+
line_count: lines.len(),
|
|
166
|
+
added_lines,
|
|
167
|
+
normalized_lines,
|
|
168
|
+
symbols,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fn detect_symbols(lines: &[&str]) -> Vec<SymbolAnalysis> {
|
|
173
|
+
let mut starts = Vec::new();
|
|
174
|
+
for (index, line) in lines.iter().enumerate() {
|
|
175
|
+
let indent = indentation(line);
|
|
176
|
+
if let Some((kind, name)) = symbol_start(line.trim()) {
|
|
177
|
+
starts.push((index, indent, kind, name));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let mut symbols = Vec::new();
|
|
182
|
+
for (position, (start_index, indent, kind, name)) in starts.iter().enumerate() {
|
|
183
|
+
let end_index = starts
|
|
184
|
+
.iter()
|
|
185
|
+
.skip(position + 1)
|
|
186
|
+
.find(|(_, next_indent, _, _)| next_indent <= indent)
|
|
187
|
+
.map(|(next_index, _, _, _)| next_index.saturating_sub(1))
|
|
188
|
+
.unwrap_or_else(|| lines.len().saturating_sub(1));
|
|
189
|
+
let normalized_body = lines[*start_index..=end_index]
|
|
190
|
+
.iter()
|
|
191
|
+
.filter_map(|line| normalize_line(line))
|
|
192
|
+
.collect::<Vec<_>>();
|
|
193
|
+
let tokens = normalized_body
|
|
194
|
+
.iter()
|
|
195
|
+
.flat_map(|line| token_set(line))
|
|
196
|
+
.collect::<HashSet<_>>();
|
|
197
|
+
symbols.push(SymbolAnalysis {
|
|
198
|
+
kind: kind.clone(),
|
|
199
|
+
name: name.clone(),
|
|
200
|
+
start_line: start_index + 1,
|
|
201
|
+
end_line: end_index + 1,
|
|
202
|
+
indent: *indent,
|
|
203
|
+
tokens,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
symbols
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fn symbol_start(trimmed: &str) -> Option<(String, String)> {
|
|
210
|
+
let candidates = [
|
|
211
|
+
("function", "function "),
|
|
212
|
+
("function", "export function "),
|
|
213
|
+
("function", "async function "),
|
|
214
|
+
("function", "export async function "),
|
|
215
|
+
("function", "def "),
|
|
216
|
+
("function", "fn "),
|
|
217
|
+
("function", "pub fn "),
|
|
218
|
+
("function", "func "),
|
|
219
|
+
("class", "class "),
|
|
220
|
+
("struct", "struct "),
|
|
221
|
+
("struct", "pub struct "),
|
|
222
|
+
("enum", "enum "),
|
|
223
|
+
("enum", "pub enum "),
|
|
224
|
+
("impl", "impl "),
|
|
225
|
+
];
|
|
226
|
+
for (kind, prefix) in candidates {
|
|
227
|
+
if let Some(rest) = trimmed.strip_prefix(prefix) {
|
|
228
|
+
return Some((kind.to_string(), symbol_name(rest)));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
for prefix in ["const ", "let ", "export const ", "export let "] {
|
|
232
|
+
if let Some(rest) = trimmed.strip_prefix(prefix) {
|
|
233
|
+
if trimmed.contains("=>") || trimmed.contains("function") || trimmed.contains("React.")
|
|
234
|
+
{
|
|
235
|
+
return Some(("function".to_string(), symbol_name(rest)));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
None
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
fn symbol_name(rest: &str) -> String {
|
|
243
|
+
rest.chars()
|
|
244
|
+
.take_while(|character| character.is_ascii_alphanumeric() || *character == '_')
|
|
245
|
+
.collect::<String>()
|
|
246
|
+
.trim_matches('_')
|
|
247
|
+
.to_string()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
fn normalize_line(line: &str) -> Option<String> {
|
|
251
|
+
let trimmed = line.trim();
|
|
252
|
+
if trimmed.is_empty() || is_comment_only(trimmed) || is_generated_hash_mapping(trimmed) {
|
|
253
|
+
return None;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let mut normalized = String::new();
|
|
257
|
+
let mut in_string = false;
|
|
258
|
+
let mut quote = '\0';
|
|
259
|
+
let mut previous_space = false;
|
|
260
|
+
for character in trimmed.chars() {
|
|
261
|
+
if in_string {
|
|
262
|
+
if character == quote {
|
|
263
|
+
in_string = false;
|
|
264
|
+
normalized.push('S');
|
|
265
|
+
previous_space = false;
|
|
266
|
+
}
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if character == '"' || character == '\'' || character == '`' {
|
|
270
|
+
in_string = true;
|
|
271
|
+
quote = character;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
let next = if character.is_ascii_digit() {
|
|
275
|
+
'0'
|
|
276
|
+
} else if character.is_whitespace() {
|
|
277
|
+
' '
|
|
278
|
+
} else {
|
|
279
|
+
character.to_ascii_lowercase()
|
|
280
|
+
};
|
|
281
|
+
if next == ' ' {
|
|
282
|
+
if !previous_space {
|
|
283
|
+
normalized.push(next);
|
|
284
|
+
}
|
|
285
|
+
previous_space = true;
|
|
286
|
+
} else {
|
|
287
|
+
normalized.push(next);
|
|
288
|
+
previous_space = false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
let value = normalized.trim().to_string();
|
|
292
|
+
(!value.is_empty()).then_some(value)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
fn is_comment_only(trimmed: &str) -> bool {
|
|
296
|
+
trimmed.starts_with("//")
|
|
297
|
+
|| trimmed.starts_with('#')
|
|
298
|
+
|| trimmed.starts_with("/*")
|
|
299
|
+
|| trimmed.starts_with('*')
|
|
300
|
+
|| trimmed.starts_with("--")
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
fn is_generated_hash_mapping(trimmed: &str) -> bool {
|
|
304
|
+
let Some((key, value)) = trimmed.split_once(':') else {
|
|
305
|
+
return false;
|
|
306
|
+
};
|
|
307
|
+
key.trim_start().starts_with('"')
|
|
308
|
+
&& value.trim_start().starts_with("\"sha256:")
|
|
309
|
+
&& value.chars().filter(|character| *character == '"').count() >= 2
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
fn token_set(line: &str) -> Vec<String> {
|
|
313
|
+
line.split(|character: char| !character.is_ascii_alphanumeric() && character != '_')
|
|
314
|
+
.filter(|token| token.len() > 1)
|
|
315
|
+
.map(ToString::to_string)
|
|
316
|
+
.collect()
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
fn indentation(line: &str) -> usize {
|
|
320
|
+
line.chars()
|
|
321
|
+
.take_while(|character| character.is_whitespace())
|
|
322
|
+
.map(|character| if character == '\t' { 2 } else { 1 })
|
|
323
|
+
.sum()
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
fn ignore_patterns(root: &Path, config: &RepositoryQualityConfig) -> Vec<String> {
|
|
327
|
+
let mut patterns = Vec::new();
|
|
328
|
+
patterns.extend(read_naomeignore_patterns(root));
|
|
329
|
+
patterns.extend(default_ignored_paths());
|
|
330
|
+
patterns.extend(default_generated_paths());
|
|
331
|
+
patterns.extend(config.ignored_paths.clone());
|
|
332
|
+
patterns.extend(config.generated_paths.clone());
|
|
333
|
+
patterns
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
fn read_naomeignore_patterns(root: &Path) -> Vec<String> {
|
|
337
|
+
let Ok(content) = fs::read_to_string(root.join(".naomeignore")) else {
|
|
338
|
+
return Vec::new();
|
|
339
|
+
};
|
|
340
|
+
content
|
|
341
|
+
.lines()
|
|
342
|
+
.map(str::trim)
|
|
343
|
+
.filter(|line| !line.is_empty() && !line.starts_with('#') && !line.starts_with('!'))
|
|
344
|
+
.map(|pattern| {
|
|
345
|
+
let normalized = pattern.trim_start_matches("./").replace('\\', "/");
|
|
346
|
+
if normalized.ends_with('/') {
|
|
347
|
+
format!("{normalized}**")
|
|
348
|
+
} else {
|
|
349
|
+
normalized
|
|
350
|
+
}
|
|
351
|
+
})
|
|
352
|
+
.collect()
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
fn should_skip_path(path: &str, patterns: &[String]) -> bool {
|
|
356
|
+
paths::matches_any(path, patterns)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
fn is_binary_extension(path: &str) -> bool {
|
|
360
|
+
let lower = path.to_ascii_lowercase();
|
|
361
|
+
[
|
|
362
|
+
".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".pdf", ".zip", ".gz", ".tgz", ".wasm",
|
|
363
|
+
".dylib", ".so", ".dll", ".exe", ".bin",
|
|
364
|
+
]
|
|
365
|
+
.iter()
|
|
366
|
+
.any(|extension| lower.ends_with(extension))
|
|
367
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
2
|
+
|
|
3
|
+
use crate::paths;
|
|
4
|
+
|
|
5
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
6
|
+
pub enum QualityMode {
|
|
7
|
+
Changed,
|
|
8
|
+
Report,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
impl QualityMode {
|
|
12
|
+
pub fn as_str(self) -> &'static str {
|
|
13
|
+
match self {
|
|
14
|
+
Self::Changed => "changed",
|
|
15
|
+
Self::Report => "report",
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
21
|
+
#[serde(rename_all = "camelCase")]
|
|
22
|
+
pub struct QualityLimits {
|
|
23
|
+
pub max_file_lines: usize,
|
|
24
|
+
pub max_new_file_lines: usize,
|
|
25
|
+
pub max_diff_added_lines: usize,
|
|
26
|
+
pub max_function_lines: usize,
|
|
27
|
+
pub max_top_level_symbols: usize,
|
|
28
|
+
pub duplicate_block_lines: usize,
|
|
29
|
+
pub near_duplicate_similarity: f64,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl Default for QualityLimits {
|
|
33
|
+
fn default() -> Self {
|
|
34
|
+
Self {
|
|
35
|
+
max_file_lines: 450,
|
|
36
|
+
max_new_file_lines: 300,
|
|
37
|
+
max_diff_added_lines: 180,
|
|
38
|
+
max_function_lines: 100,
|
|
39
|
+
max_top_level_symbols: 30,
|
|
40
|
+
duplicate_block_lines: 10,
|
|
41
|
+
near_duplicate_similarity: 0.9,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
impl QualityLimits {
|
|
47
|
+
pub fn with_overrides(&self, overrides: &QualityLimitOverrides) -> Self {
|
|
48
|
+
Self {
|
|
49
|
+
max_file_lines: overrides.max_file_lines.unwrap_or(self.max_file_lines),
|
|
50
|
+
max_new_file_lines: overrides
|
|
51
|
+
.max_new_file_lines
|
|
52
|
+
.unwrap_or(self.max_new_file_lines),
|
|
53
|
+
max_diff_added_lines: overrides
|
|
54
|
+
.max_diff_added_lines
|
|
55
|
+
.unwrap_or(self.max_diff_added_lines),
|
|
56
|
+
max_function_lines: overrides
|
|
57
|
+
.max_function_lines
|
|
58
|
+
.unwrap_or(self.max_function_lines),
|
|
59
|
+
max_top_level_symbols: overrides
|
|
60
|
+
.max_top_level_symbols
|
|
61
|
+
.unwrap_or(self.max_top_level_symbols),
|
|
62
|
+
duplicate_block_lines: overrides
|
|
63
|
+
.duplicate_block_lines
|
|
64
|
+
.unwrap_or(self.duplicate_block_lines),
|
|
65
|
+
near_duplicate_similarity: overrides
|
|
66
|
+
.near_duplicate_similarity
|
|
67
|
+
.unwrap_or(self.near_duplicate_similarity),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
73
|
+
#[serde(rename_all = "camelCase")]
|
|
74
|
+
pub struct QualityLimitOverrides {
|
|
75
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
76
|
+
pub max_file_lines: Option<usize>,
|
|
77
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
78
|
+
pub max_new_file_lines: Option<usize>,
|
|
79
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
80
|
+
pub max_diff_added_lines: Option<usize>,
|
|
81
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
82
|
+
pub max_function_lines: Option<usize>,
|
|
83
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
84
|
+
pub max_top_level_symbols: Option<usize>,
|
|
85
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
86
|
+
pub duplicate_block_lines: Option<usize>,
|
|
87
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
88
|
+
pub near_duplicate_similarity: Option<f64>,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
92
|
+
#[serde(rename_all = "camelCase")]
|
|
93
|
+
pub struct QualityPathRule {
|
|
94
|
+
pub id: String,
|
|
95
|
+
pub paths: Vec<String>,
|
|
96
|
+
#[serde(default, skip_serializing_if = "QualityLimitOverrides::is_empty")]
|
|
97
|
+
pub limits: QualityLimitOverrides,
|
|
98
|
+
#[serde(default)]
|
|
99
|
+
pub disabled_checks: Vec<String>,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
impl QualityLimitOverrides {
|
|
103
|
+
pub fn is_empty(&self) -> bool {
|
|
104
|
+
self.max_file_lines.is_none()
|
|
105
|
+
&& self.max_new_file_lines.is_none()
|
|
106
|
+
&& self.max_diff_added_lines.is_none()
|
|
107
|
+
&& self.max_function_lines.is_none()
|
|
108
|
+
&& self.max_top_level_symbols.is_none()
|
|
109
|
+
&& self.duplicate_block_lines.is_none()
|
|
110
|
+
&& self.near_duplicate_similarity.is_none()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
115
|
+
#[serde(rename_all = "camelCase")]
|
|
116
|
+
pub struct RepositoryQualityConfig {
|
|
117
|
+
pub schema: String,
|
|
118
|
+
pub version: u32,
|
|
119
|
+
pub status: String,
|
|
120
|
+
pub limits: QualityLimits,
|
|
121
|
+
#[serde(default)]
|
|
122
|
+
pub enabled_adapters: Vec<String>,
|
|
123
|
+
#[serde(default)]
|
|
124
|
+
pub disabled_checks: Vec<String>,
|
|
125
|
+
#[serde(default)]
|
|
126
|
+
pub ignored_paths: Vec<String>,
|
|
127
|
+
#[serde(default)]
|
|
128
|
+
pub generated_paths: Vec<String>,
|
|
129
|
+
#[serde(default)]
|
|
130
|
+
pub path_rules: Vec<QualityPathRule>,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
impl Default for RepositoryQualityConfig {
|
|
134
|
+
fn default() -> Self {
|
|
135
|
+
Self {
|
|
136
|
+
schema: "naome.repository-quality.v1".to_string(),
|
|
137
|
+
version: 1,
|
|
138
|
+
status: "ready".to_string(),
|
|
139
|
+
limits: QualityLimits::default(),
|
|
140
|
+
enabled_adapters: Vec::new(),
|
|
141
|
+
disabled_checks: Vec::new(),
|
|
142
|
+
ignored_paths: default_ignored_paths(),
|
|
143
|
+
generated_paths: default_generated_paths(),
|
|
144
|
+
path_rules: Vec::new(),
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
impl RepositoryQualityConfig {
|
|
150
|
+
pub fn limits_for_path(&self, path: &str) -> QualityLimits {
|
|
151
|
+
self.path_rules
|
|
152
|
+
.iter()
|
|
153
|
+
.filter(|rule| paths::matches_any(path, &rule.paths))
|
|
154
|
+
.fold(self.limits.clone(), |limits, rule| {
|
|
155
|
+
limits.with_overrides(&rule.limits)
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
pub fn check_enabled_for_path(&self, check_id: &str, path: &str) -> bool {
|
|
160
|
+
!self
|
|
161
|
+
.disabled_checks
|
|
162
|
+
.iter()
|
|
163
|
+
.any(|disabled| disabled == check_id)
|
|
164
|
+
&& !self.path_rules.iter().any(|rule| {
|
|
165
|
+
paths::matches_any(path, &rule.paths)
|
|
166
|
+
&& rule
|
|
167
|
+
.disabled_checks
|
|
168
|
+
.iter()
|
|
169
|
+
.any(|disabled| disabled == check_id)
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#[derive(Debug, Clone, Serialize)]
|
|
175
|
+
#[serde(rename_all = "camelCase")]
|
|
176
|
+
pub struct QualityReport {
|
|
177
|
+
pub schema: String,
|
|
178
|
+
pub mode: String,
|
|
179
|
+
pub ok: bool,
|
|
180
|
+
pub changed_paths: Vec<String>,
|
|
181
|
+
pub scanned_paths: Vec<String>,
|
|
182
|
+
pub summary: QualitySummary,
|
|
183
|
+
pub violations: Vec<QualityViolation>,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#[derive(Debug, Clone, Serialize)]
|
|
187
|
+
#[serde(rename_all = "camelCase")]
|
|
188
|
+
pub struct QualitySummary {
|
|
189
|
+
pub scanned_files: usize,
|
|
190
|
+
pub violation_count: usize,
|
|
191
|
+
pub baseline_violation_count: usize,
|
|
192
|
+
pub blocking_violation_count: usize,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#[derive(Debug, Clone, Serialize)]
|
|
196
|
+
#[serde(rename_all = "camelCase")]
|
|
197
|
+
pub struct QualityViolation {
|
|
198
|
+
pub check_id: String,
|
|
199
|
+
pub severity: String,
|
|
200
|
+
pub path: String,
|
|
201
|
+
pub line: Option<usize>,
|
|
202
|
+
pub message: String,
|
|
203
|
+
pub value: Option<f64>,
|
|
204
|
+
pub limit: Option<f64>,
|
|
205
|
+
pub fingerprint: String,
|
|
206
|
+
pub related_paths: Vec<String>,
|
|
207
|
+
pub baseline: bool,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#[derive(Debug, Clone, Serialize)]
|
|
211
|
+
#[serde(rename_all = "camelCase")]
|
|
212
|
+
pub struct QualityInitResult {
|
|
213
|
+
pub schema: String,
|
|
214
|
+
pub config_written: bool,
|
|
215
|
+
pub baseline_written: bool,
|
|
216
|
+
pub baseline_violations: usize,
|
|
217
|
+
pub config_path: String,
|
|
218
|
+
pub baseline_path: String,
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#[derive(Debug, Clone, Serialize)]
|
|
222
|
+
#[serde(rename_all = "camelCase")]
|
|
223
|
+
pub struct QualityCleanupPlan {
|
|
224
|
+
pub schema: String,
|
|
225
|
+
pub tasks: Vec<QualityCleanupTask>,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#[derive(Debug, Clone, Serialize)]
|
|
229
|
+
#[serde(rename_all = "camelCase")]
|
|
230
|
+
pub struct QualityCleanupTask {
|
|
231
|
+
pub path: String,
|
|
232
|
+
pub violation_count: usize,
|
|
233
|
+
pub check_ids: Vec<String>,
|
|
234
|
+
pub summary: String,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#[derive(Debug, Clone, Serialize)]
|
|
238
|
+
#[serde(rename_all = "camelCase")]
|
|
239
|
+
pub struct QualityCleanupRoute {
|
|
240
|
+
pub schema: String,
|
|
241
|
+
pub path: String,
|
|
242
|
+
pub violations: Vec<QualityViolation>,
|
|
243
|
+
pub related_paths: Vec<String>,
|
|
244
|
+
pub agent_instructions: String,
|
|
245
|
+
pub required_checks: Vec<String>,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
pub fn default_ignored_paths() -> Vec<String> {
|
|
249
|
+
listed_paths(DEFAULT_IGNORED_PATHS)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
pub fn default_generated_paths() -> Vec<String> {
|
|
253
|
+
listed_paths(DEFAULT_GENERATED_PATHS)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const DEFAULT_IGNORED_PATHS: &str = r#"
|
|
257
|
+
.git/**
|
|
258
|
+
.naome/archive/**
|
|
259
|
+
.naome/task-state.json
|
|
260
|
+
.naome/task-journal.jsonl
|
|
261
|
+
.naome/repository-quality.json
|
|
262
|
+
.naome/repository-quality-baseline.json
|
|
263
|
+
node_modules/**
|
|
264
|
+
.npm/**
|
|
265
|
+
target/**
|
|
266
|
+
**/target/**
|
|
267
|
+
**/Cargo.lock
|
|
268
|
+
package-lock.json
|
|
269
|
+
pnpm-lock.yaml
|
|
270
|
+
yarn.lock
|
|
271
|
+
*.tgz
|
|
272
|
+
*.lock
|
|
273
|
+
"#;
|
|
274
|
+
|
|
275
|
+
const DEFAULT_GENERATED_PATHS: &str = r#"
|
|
276
|
+
**/*.min.js
|
|
277
|
+
**/*.map
|
|
278
|
+
**/dist/**
|
|
279
|
+
**/build/**
|
|
280
|
+
"#;
|
|
281
|
+
|
|
282
|
+
fn listed_paths(paths: &str) -> Vec<String> {
|
|
283
|
+
paths
|
|
284
|
+
.lines()
|
|
285
|
+
.map(str::trim)
|
|
286
|
+
.filter(|path| !path.is_empty())
|
|
287
|
+
.map(ToString::to_string)
|
|
288
|
+
.collect()
|
|
289
|
+
}
|