@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.
Files changed (126) hide show
  1. package/Cargo.lock +2 -2
  2. package/Cargo.toml +1 -1
  3. package/LICENSE +180 -21
  4. package/README.md +49 -6
  5. package/bin/naome-node.js +44 -4
  6. package/bin/naome.js +54 -16
  7. package/crates/naome-cli/Cargo.toml +1 -1
  8. package/crates/naome-cli/src/check_commands.rs +135 -0
  9. package/crates/naome-cli/src/cli_args.rs +5 -0
  10. package/crates/naome-cli/src/dispatcher.rs +36 -0
  11. package/crates/naome-cli/src/install_bridge.rs +83 -0
  12. package/crates/naome-cli/src/main.rs +57 -341
  13. package/crates/naome-cli/src/prompt_commands.rs +68 -0
  14. package/crates/naome-cli/src/quality_commands.rs +141 -0
  15. package/crates/naome-cli/src/simple_commands.rs +53 -0
  16. package/crates/naome-cli/src/workflow_commands.rs +153 -0
  17. package/crates/naome-core/Cargo.toml +1 -1
  18. package/crates/naome-core/src/harness_health/integrity.rs +96 -0
  19. package/crates/naome-core/src/harness_health.rs +14 -126
  20. package/crates/naome-core/src/install_plan.rs +3 -0
  21. package/crates/naome-core/src/intent/classifier.rs +171 -0
  22. package/crates/naome-core/src/intent/envelope.rs +108 -0
  23. package/crates/naome-core/src/intent/legacy.rs +138 -0
  24. package/crates/naome-core/src/intent/legacy_response.rs +76 -0
  25. package/crates/naome-core/src/intent/model.rs +71 -0
  26. package/crates/naome-core/src/intent/patterns.rs +170 -0
  27. package/crates/naome-core/src/intent/resolver.rs +162 -0
  28. package/crates/naome-core/src/intent/resolver_active.rs +17 -0
  29. package/crates/naome-core/src/intent/resolver_baseline.rs +55 -0
  30. package/crates/naome-core/src/intent/resolver_catalog.rs +167 -0
  31. package/crates/naome-core/src/intent/resolver_policy.rs +72 -0
  32. package/crates/naome-core/src/intent/resolver_shared.rs +55 -0
  33. package/crates/naome-core/src/intent/risk.rs +40 -0
  34. package/crates/naome-core/src/intent/segment.rs +170 -0
  35. package/crates/naome-core/src/intent.rs +64 -879
  36. package/crates/naome-core/src/journal.rs +9 -20
  37. package/crates/naome-core/src/lib.rs +13 -0
  38. package/crates/naome-core/src/quality/adapters.rs +178 -0
  39. package/crates/naome-core/src/quality/baseline.rs +75 -0
  40. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +175 -0
  41. package/crates/naome-core/src/quality/checks/near_duplicates.rs +130 -0
  42. package/crates/naome-core/src/quality/checks.rs +228 -0
  43. package/crates/naome-core/src/quality/cleanup.rs +72 -0
  44. package/crates/naome-core/src/quality/config.rs +109 -0
  45. package/crates/naome-core/src/quality/mod.rs +90 -0
  46. package/crates/naome-core/src/quality/scanner/repo_paths.rs +103 -0
  47. package/crates/naome-core/src/quality/scanner.rs +367 -0
  48. package/crates/naome-core/src/quality/types.rs +289 -0
  49. package/crates/naome-core/src/route.rs +292 -17
  50. package/crates/naome-core/src/task_state/admission.rs +63 -0
  51. package/crates/naome-core/src/task_state/admission_proof.rs +72 -0
  52. package/crates/naome-core/src/task_state/api.rs +130 -0
  53. package/crates/naome-core/src/task_state/commit_gate.rs +138 -0
  54. package/crates/naome-core/src/task_state/compact_proof.rs +160 -0
  55. package/crates/naome-core/src/task_state/completed_refresh.rs +89 -0
  56. package/crates/naome-core/src/task_state/completion.rs +72 -0
  57. package/crates/naome-core/src/task_state/deleted_paths.rs +47 -0
  58. package/crates/naome-core/src/task_state/diff.rs +95 -0
  59. package/crates/naome-core/src/task_state/evidence.rs +154 -0
  60. package/crates/naome-core/src/task_state/git_io.rs +86 -0
  61. package/crates/naome-core/src/task_state/git_parse.rs +86 -0
  62. package/crates/naome-core/src/task_state/git_refs.rs +37 -0
  63. package/crates/naome-core/src/task_state/human_review_state.rs +31 -0
  64. package/crates/naome-core/src/task_state/mod.rs +38 -0
  65. package/crates/naome-core/src/task_state/process_guard.rs +40 -0
  66. package/crates/naome-core/src/task_state/progress.rs +123 -0
  67. package/crates/naome-core/src/task_state/proof.rs +139 -0
  68. package/crates/naome-core/src/task_state/proof_entry.rs +66 -0
  69. package/crates/naome-core/src/task_state/proof_model.rs +70 -0
  70. package/crates/naome-core/src/task_state/proof_sources.rs +76 -0
  71. package/crates/naome-core/src/task_state/push_gate.rs +49 -0
  72. package/crates/naome-core/src/task_state/reconcile.rs +7 -0
  73. package/crates/naome-core/src/task_state/repair.rs +168 -0
  74. package/crates/naome-core/src/task_state/shape.rs +117 -0
  75. package/crates/naome-core/src/task_state/task_diff_api.rs +170 -0
  76. package/crates/naome-core/src/task_state/task_records.rs +131 -0
  77. package/crates/naome-core/src/task_state/task_references.rs +126 -0
  78. package/crates/naome-core/src/task_state/types.rs +87 -0
  79. package/crates/naome-core/src/task_state/util.rs +137 -0
  80. package/crates/naome-core/src/verification/render.rs +122 -0
  81. package/crates/naome-core/src/verification.rs +176 -58
  82. package/crates/naome-core/src/verification_contract.rs +49 -21
  83. package/crates/naome-core/src/workflow/integrity.rs +123 -0
  84. package/crates/naome-core/src/workflow/integrity_normalize.rs +7 -0
  85. package/crates/naome-core/src/workflow/integrity_support.rs +110 -0
  86. package/crates/naome-core/src/workflow/mod.rs +18 -0
  87. package/crates/naome-core/src/workflow/mutation.rs +68 -0
  88. package/crates/naome-core/src/workflow/output.rs +111 -0
  89. package/crates/naome-core/src/workflow/phase_inference.rs +73 -0
  90. package/crates/naome-core/src/workflow/phases.rs +169 -0
  91. package/crates/naome-core/src/workflow/policy.rs +156 -0
  92. package/crates/naome-core/src/workflow/processes.rs +91 -0
  93. package/crates/naome-core/src/workflow/types.rs +42 -0
  94. package/crates/naome-core/tests/harness_health.rs +3 -0
  95. package/crates/naome-core/tests/intent.rs +97 -792
  96. package/crates/naome-core/tests/intent_support/mod.rs +133 -0
  97. package/crates/naome-core/tests/intent_v2.rs +90 -0
  98. package/crates/naome-core/tests/quality.rs +425 -0
  99. package/crates/naome-core/tests/route.rs +221 -4
  100. package/crates/naome-core/tests/task_state.rs +3 -0
  101. package/crates/naome-core/tests/task_state_compact.rs +110 -0
  102. package/crates/naome-core/tests/task_state_compact_support/mod.rs +5 -0
  103. package/crates/naome-core/tests/task_state_compact_support/repo.rs +130 -0
  104. package/crates/naome-core/tests/task_state_compact_support/states.rs +151 -0
  105. package/crates/naome-core/tests/workflow_integrity.rs +85 -0
  106. package/crates/naome-core/tests/workflow_policy.rs +139 -0
  107. package/crates/naome-core/tests/workflow_support/mod.rs +194 -0
  108. package/native/darwin-arm64/naome +0 -0
  109. package/native/linux-x64/naome +0 -0
  110. package/package.json +2 -2
  111. package/templates/naome-root/.naome/bin/check-harness-health.js +66 -85
  112. package/templates/naome-root/.naome/bin/check-task-state.js +9 -10
  113. package/templates/naome-root/.naome/bin/naome.js +34 -63
  114. package/templates/naome-root/.naome/manifest.json +20 -18
  115. package/templates/naome-root/.naome/repository-quality-baseline.json +5 -0
  116. package/templates/naome-root/.naome/repository-quality.json +24 -0
  117. package/templates/naome-root/.naome/task-contract.schema.json +93 -11
  118. package/templates/naome-root/.naome/upgrade-state.json +1 -1
  119. package/templates/naome-root/.naome/verification.json +37 -0
  120. package/templates/naome-root/AGENTS.md +3 -0
  121. package/templates/naome-root/docs/naome/agent-workflow.md +25 -12
  122. package/templates/naome-root/docs/naome/execution.md +25 -21
  123. package/templates/naome-root/docs/naome/index.md +4 -3
  124. package/templates/naome-root/docs/naome/repository-quality.md +43 -0
  125. package/templates/naome-root/docs/naome/testing.md +12 -0
  126. 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
+ }