@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.
Files changed (57) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +108 -47
  3. package/bin/naome.js +16 -1
  4. package/crates/naome-cli/Cargo.toml +1 -1
  5. package/crates/naome-cli/src/dispatcher.rs +6 -2
  6. package/crates/naome-cli/src/main.rs +35 -23
  7. package/crates/naome-cli/src/quality_commands.rs +230 -11
  8. package/crates/naome-cli/src/workflow_commands.rs +21 -1
  9. package/crates/naome-core/Cargo.toml +1 -1
  10. package/crates/naome-core/src/git.rs +4 -2
  11. package/crates/naome-core/src/install_plan.rs +2 -0
  12. package/crates/naome-core/src/lib.rs +11 -7
  13. package/crates/naome-core/src/quality/baseline.rs +8 -0
  14. package/crates/naome-core/src/quality/cache.rs +153 -0
  15. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +25 -11
  16. package/crates/naome-core/src/quality/checks/near_duplicates.rs +4 -2
  17. package/crates/naome-core/src/quality/checks.rs +7 -8
  18. package/crates/naome-core/src/quality/cleanup.rs +36 -3
  19. package/crates/naome-core/src/quality/mod.rs +57 -9
  20. package/crates/naome-core/src/quality/scanner/analysis/normalize.rs +78 -0
  21. package/crates/naome-core/src/quality/scanner/analysis.rs +160 -0
  22. package/crates/naome-core/src/quality/scanner/repo_paths.rs +39 -3
  23. package/crates/naome-core/src/quality/scanner.rs +193 -220
  24. package/crates/naome-core/src/quality/semantic/checks.rs +134 -0
  25. package/crates/naome-core/src/quality/semantic/extract.rs +158 -0
  26. package/crates/naome-core/src/quality/semantic/model.rs +85 -0
  27. package/crates/naome-core/src/quality/semantic/route.rs +52 -0
  28. package/crates/naome-core/src/quality/semantic.rs +68 -0
  29. package/crates/naome-core/src/quality/structure/checks/directory.rs +9 -19
  30. package/crates/naome-core/src/quality/structure/checks.rs +1 -1
  31. package/crates/naome-core/src/quality/structure/classify.rs +52 -0
  32. package/crates/naome-core/src/quality/structure/mod.rs +2 -2
  33. package/crates/naome-core/src/quality/structure/model.rs +8 -1
  34. package/crates/naome-core/src/quality/types.rs +40 -2
  35. package/crates/naome-core/src/route/builtin_checks.rs +1 -15
  36. package/crates/naome-core/src/workflow/doctor.rs +144 -0
  37. package/crates/naome-core/src/workflow/mod.rs +2 -0
  38. package/crates/naome-core/src/workflow/mutation.rs +1 -2
  39. package/crates/naome-core/tests/install_plan.rs +2 -0
  40. package/crates/naome-core/tests/quality.rs +14 -5
  41. package/crates/naome-core/tests/quality_performance.rs +231 -0
  42. package/crates/naome-core/tests/quality_structure_policy.rs +19 -0
  43. package/crates/naome-core/tests/route_user_diff.rs +10 -6
  44. package/crates/naome-core/tests/semantic_legacy.rs +140 -0
  45. package/crates/naome-core/tests/workflow_doctor.rs +24 -0
  46. package/crates/naome-core/tests/workflow_policy.rs +6 -1
  47. package/installer/git-boundary.js +1 -0
  48. package/native/darwin-arm64/naome +0 -0
  49. package/native/linux-x64/naome +0 -0
  50. package/package.json +1 -1
  51. package/templates/naome-root/.naome/bin/check-harness-health.js +2 -2
  52. package/templates/naome-root/.naome/bin/check-task-state.js +2 -2
  53. package/templates/naome-root/.naome/bin/naome.js +11 -4
  54. package/templates/naome-root/.naome/manifest.json +2 -2
  55. package/templates/naome-root/.naomeignore +1 -0
  56. package/templates/naome-root/docs/naome/agent-workflow.md +16 -14
  57. package/templates/naome-root/docs/naome/repository-quality.md +63 -4
@@ -0,0 +1,160 @@
1
+ mod normalize;
2
+
3
+ use std::collections::{HashMap, HashSet};
4
+ use std::fs;
5
+ use std::path::Path;
6
+
7
+ use super::{FileAnalysis, NormalizedLine, SymbolAnalysis};
8
+ use crate::quality::cache::{content_hash, QualityCache};
9
+ use normalize::{normalize_line, token_set};
10
+
11
+ pub(super) fn analyze_repo_file(
12
+ root: &Path,
13
+ path: &str,
14
+ added_lines: &HashMap<String, usize>,
15
+ blob_hashes: &HashMap<String, String>,
16
+ cache: &QualityCache,
17
+ allow_cache: bool,
18
+ ) -> Option<(FileAnalysis, bool)> {
19
+ let full_path = root.join(path);
20
+ if !full_path.is_file() || is_binary_extension(path) {
21
+ return None;
22
+ }
23
+ if allow_cache {
24
+ if let Some(blob_hash) = blob_hashes.get(path) {
25
+ if let Some(cached) = cache.read(path, blob_hash) {
26
+ return Some((cached, true));
27
+ }
28
+ }
29
+ }
30
+ let content = fs::read_to_string(&full_path).ok()?;
31
+ let file = analyze_file(
32
+ path,
33
+ &content,
34
+ added_lines.get(path).copied().unwrap_or(0),
35
+ );
36
+ let hash = if allow_cache {
37
+ blob_hashes
38
+ .get(path)
39
+ .cloned()
40
+ .unwrap_or_else(|| content_hash(&content))
41
+ } else {
42
+ content_hash(&content)
43
+ };
44
+ let _ = cache.write(path, &hash, &file);
45
+ Some((file, false))
46
+ }
47
+
48
+ fn analyze_file(path: &str, content: &str, added_lines: usize) -> FileAnalysis {
49
+ let lines = content.lines().collect::<Vec<_>>();
50
+ let normalized_lines = lines
51
+ .iter()
52
+ .enumerate()
53
+ .filter_map(|(index, line)| normalize_line(line).map(|value| (index + 1, value)))
54
+ .map(|(line_number, value)| NormalizedLine { line_number, value })
55
+ .collect::<Vec<_>>();
56
+ let symbols = detect_symbols(&lines);
57
+ FileAnalysis {
58
+ path: path.to_string(),
59
+ line_count: lines.len(),
60
+ added_lines,
61
+ raw_lines: lines.iter().map(|line| (*line).to_string()).collect(),
62
+ normalized_lines,
63
+ symbols,
64
+ }
65
+ }
66
+
67
+ fn detect_symbols(lines: &[&str]) -> Vec<SymbolAnalysis> {
68
+ let mut starts = Vec::new();
69
+ for (index, line) in lines.iter().enumerate() {
70
+ let indent = indentation(line);
71
+ if let Some((kind, name)) = symbol_start(line.trim()) {
72
+ starts.push((index, indent, kind, name));
73
+ }
74
+ }
75
+
76
+ let mut symbols = Vec::new();
77
+ for (position, (start_index, indent, kind, name)) in starts.iter().enumerate() {
78
+ let end_index = starts
79
+ .iter()
80
+ .skip(position + 1)
81
+ .find(|(_, next_indent, _, _)| next_indent <= indent)
82
+ .map(|(next_index, _, _, _)| next_index.saturating_sub(1))
83
+ .unwrap_or_else(|| lines.len().saturating_sub(1));
84
+ let normalized_body = lines[*start_index..=end_index]
85
+ .iter()
86
+ .filter_map(|line| normalize_line(line))
87
+ .collect::<Vec<_>>();
88
+ let tokens = normalized_body
89
+ .iter()
90
+ .flat_map(|line| token_set(line))
91
+ .collect::<HashSet<_>>();
92
+ symbols.push(SymbolAnalysis {
93
+ kind: kind.clone(),
94
+ name: name.clone(),
95
+ start_line: start_index + 1,
96
+ end_line: end_index + 1,
97
+ indent: *indent,
98
+ tokens,
99
+ });
100
+ }
101
+ symbols
102
+ }
103
+
104
+ fn symbol_start(trimmed: &str) -> Option<(String, String)> {
105
+ let candidates = [
106
+ ("function", "function "),
107
+ ("function", "export function "),
108
+ ("function", "async function "),
109
+ ("function", "export async function "),
110
+ ("function", "def "),
111
+ ("function", "fn "),
112
+ ("function", "pub fn "),
113
+ ("function", "func "),
114
+ ("class", "class "),
115
+ ("struct", "struct "),
116
+ ("struct", "pub struct "),
117
+ ("enum", "enum "),
118
+ ("enum", "pub enum "),
119
+ ("impl", "impl "),
120
+ ];
121
+ for (kind, prefix) in candidates {
122
+ if let Some(rest) = trimmed.strip_prefix(prefix) {
123
+ return Some((kind.to_string(), symbol_name(rest)));
124
+ }
125
+ }
126
+ for prefix in ["const ", "let ", "export const ", "export let "] {
127
+ if let Some(rest) = trimmed.strip_prefix(prefix) {
128
+ if trimmed.contains("=>") || trimmed.contains("function") || trimmed.contains("React.")
129
+ {
130
+ return Some(("function".to_string(), symbol_name(rest)));
131
+ }
132
+ }
133
+ }
134
+ None
135
+ }
136
+
137
+ fn symbol_name(rest: &str) -> String {
138
+ rest.chars()
139
+ .take_while(|character| character.is_ascii_alphanumeric() || *character == '_')
140
+ .collect::<String>()
141
+ .trim_matches('_')
142
+ .to_string()
143
+ }
144
+
145
+ fn indentation(line: &str) -> usize {
146
+ line.chars()
147
+ .take_while(|character| character.is_whitespace())
148
+ .map(|character| if character == '\t' { 2 } else { 1 })
149
+ .sum()
150
+ }
151
+
152
+ fn is_binary_extension(path: &str) -> bool {
153
+ let lower = path.to_ascii_lowercase();
154
+ [
155
+ ".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".pdf", ".zip", ".gz", ".tgz", ".wasm",
156
+ ".dylib", ".so", ".dll", ".exe", ".bin",
157
+ ]
158
+ .iter()
159
+ .any(|extension| lower.ends_with(extension))
160
+ }
@@ -1,4 +1,4 @@
1
- use std::collections::HashMap;
1
+ use std::collections::{HashMap, HashSet};
2
2
  use std::fs;
3
3
  use std::path::Path;
4
4
  use std::process::Command;
@@ -36,7 +36,10 @@ pub(crate) fn collect_repo_paths(root: &Path) -> Result<Vec<String>, NaomeError>
36
36
  Ok(paths)
37
37
  }
38
38
 
39
- pub(super) fn added_lines_by_path(root: &Path) -> Result<HashMap<String, usize>, NaomeError> {
39
+ pub(super) fn added_lines_by_path(
40
+ root: &Path,
41
+ target_paths: &HashSet<String>,
42
+ ) -> Result<HashMap<String, usize>, NaomeError> {
40
43
  let mut added = HashMap::new();
41
44
  for args in [
42
45
  vec!["diff", "--numstat"],
@@ -53,13 +56,20 @@ pub(super) fn added_lines_by_path(root: &Path) -> Result<HashMap<String, usize>,
53
56
  };
54
57
  let _deletions = parts.next();
55
58
  let Some(path) = parts.next() else { continue };
59
+ let path = path.replace('\\', "/");
60
+ if !target_paths.contains(&path) {
61
+ continue;
62
+ }
56
63
  let Ok(count) = additions.parse::<usize>() else {
57
64
  continue;
58
65
  };
59
- *added.entry(path.replace('\\', "/")).or_insert(0) += count;
66
+ *added.entry(path).or_insert(0) += count;
60
67
  }
61
68
  }
62
69
  for path in untracked_paths(root)? {
70
+ if !target_paths.contains(&path) {
71
+ continue;
72
+ }
63
73
  if let Ok(content) = fs::read_to_string(root.join(&path)) {
64
74
  added.insert(path, content.lines().count());
65
75
  }
@@ -67,6 +77,32 @@ pub(super) fn added_lines_by_path(root: &Path) -> Result<HashMap<String, usize>,
67
77
  Ok(added)
68
78
  }
69
79
 
80
+ pub(super) fn tracked_blob_hashes(root: &Path) -> Result<HashMap<String, String>, NaomeError> {
81
+ let output = Command::new("git")
82
+ .args(["ls-files", "-s", "-z"])
83
+ .current_dir(root)
84
+ .output()?;
85
+ if !output.status.success() {
86
+ return Ok(HashMap::new());
87
+ }
88
+
89
+ let mut hashes = HashMap::new();
90
+ for entry in output.stdout.split(|byte| *byte == 0) {
91
+ if entry.is_empty() {
92
+ continue;
93
+ }
94
+ let entry = String::from_utf8_lossy(entry);
95
+ let Some((metadata, path)) = entry.split_once('\t') else {
96
+ continue;
97
+ };
98
+ let mut parts = metadata.split_whitespace();
99
+ let _mode = parts.next();
100
+ let Some(hash) = parts.next() else { continue };
101
+ hashes.insert(path.replace('\\', "/"), hash.to_string());
102
+ }
103
+ Ok(hashes)
104
+ }
105
+
70
106
  fn collect_files_recursive(
71
107
  root: &Path,
72
108
  dir: &Path,