@lamentis/naome 1.2.0 → 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 (139) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +108 -47
  3. package/bin/naome-node.js +2 -1579
  4. package/bin/naome.js +34 -5
  5. package/crates/naome-cli/Cargo.toml +1 -1
  6. package/crates/naome-cli/src/dispatcher.rs +7 -2
  7. package/crates/naome-cli/src/main.rs +37 -22
  8. package/crates/naome-cli/src/quality_commands.rs +317 -10
  9. package/crates/naome-cli/src/workflow_commands.rs +21 -1
  10. package/crates/naome-core/Cargo.toml +1 -1
  11. package/crates/naome-core/src/decision/checks.rs +64 -0
  12. package/crates/naome-core/src/decision/idle.rs +67 -0
  13. package/crates/naome-core/src/decision/json.rs +36 -0
  14. package/crates/naome-core/src/decision/states.rs +165 -0
  15. package/crates/naome-core/src/decision.rs +131 -353
  16. package/crates/naome-core/src/git.rs +4 -2
  17. package/crates/naome-core/src/install_plan.rs +4 -0
  18. package/crates/naome-core/src/lib.rs +12 -6
  19. package/crates/naome-core/src/paths.rs +3 -1
  20. package/crates/naome-core/src/quality/adapter_support.rs +89 -0
  21. package/crates/naome-core/src/quality/adapters.rs +20 -67
  22. package/crates/naome-core/src/quality/baseline.rs +8 -0
  23. package/crates/naome-core/src/quality/cache.rs +153 -0
  24. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +25 -11
  25. package/crates/naome-core/src/quality/checks/near_duplicates.rs +4 -2
  26. package/crates/naome-core/src/quality/checks.rs +7 -8
  27. package/crates/naome-core/src/quality/cleanup.rs +48 -3
  28. package/crates/naome-core/src/quality/config.rs +8 -15
  29. package/crates/naome-core/src/quality/config_support.rs +24 -0
  30. package/crates/naome-core/src/quality/mod.rs +72 -6
  31. package/crates/naome-core/src/quality/scanner/analysis/normalize.rs +78 -0
  32. package/crates/naome-core/src/quality/scanner/analysis.rs +160 -0
  33. package/crates/naome-core/src/quality/scanner/repo_paths.rs +39 -3
  34. package/crates/naome-core/src/quality/scanner.rs +200 -215
  35. package/crates/naome-core/src/quality/semantic/checks.rs +134 -0
  36. package/crates/naome-core/src/quality/semantic/extract.rs +158 -0
  37. package/crates/naome-core/src/quality/semantic/model.rs +85 -0
  38. package/crates/naome-core/src/quality/semantic/route.rs +52 -0
  39. package/crates/naome-core/src/quality/semantic.rs +68 -0
  40. package/crates/naome-core/src/quality/structure/adapters.rs +84 -0
  41. package/crates/naome-core/src/quality/structure/checks/basic.rs +153 -0
  42. package/crates/naome-core/src/quality/structure/checks/directory.rs +134 -0
  43. package/crates/naome-core/src/quality/structure/checks/pairing.rs +63 -0
  44. package/crates/naome-core/src/quality/structure/checks.rs +124 -0
  45. package/crates/naome-core/src/quality/structure/classify/roles.rs +188 -0
  46. package/crates/naome-core/src/quality/structure/classify.rs +146 -0
  47. package/crates/naome-core/src/quality/structure/config.rs +89 -0
  48. package/crates/naome-core/src/quality/structure/defaults.rs +107 -0
  49. package/crates/naome-core/src/quality/structure/mod.rs +77 -0
  50. package/crates/naome-core/src/quality/structure/model.rs +131 -0
  51. package/crates/naome-core/src/quality/types.rs +43 -2
  52. package/crates/naome-core/src/route/builtin_checks.rs +141 -0
  53. package/crates/naome-core/src/route/builtin_context.rs +73 -0
  54. package/crates/naome-core/src/route/builtin_integrity.rs +49 -0
  55. package/crates/naome-core/src/route/builtin_require.rs +40 -0
  56. package/crates/naome-core/src/route/context.rs +180 -0
  57. package/crates/naome-core/src/route/execution.rs +96 -0
  58. package/crates/naome-core/src/route/execution_baselines.rs +146 -0
  59. package/crates/naome-core/src/route/execution_support.rs +57 -0
  60. package/crates/naome-core/src/route/execution_tasks.rs +71 -0
  61. package/crates/naome-core/src/route/git_ops.rs +72 -0
  62. package/crates/naome-core/src/route/quality_gate.rs +73 -0
  63. package/crates/naome-core/src/route/quality_gate_config.rs +126 -0
  64. package/crates/naome-core/src/route/quality_gate_snapshot.rs +69 -0
  65. package/crates/naome-core/src/route/worktree.rs +75 -0
  66. package/crates/naome-core/src/route/worktree_files.rs +32 -0
  67. package/crates/naome-core/src/route/worktree_plan.rs +131 -0
  68. package/crates/naome-core/src/route.rs +44 -1217
  69. package/crates/naome-core/src/verification.rs +1 -0
  70. package/crates/naome-core/src/workflow/doctor.rs +144 -0
  71. package/crates/naome-core/src/workflow/mod.rs +2 -0
  72. package/crates/naome-core/src/workflow/mutation.rs +1 -2
  73. package/crates/naome-core/tests/decision.rs +24 -118
  74. package/crates/naome-core/tests/harness_health.rs +2 -0
  75. package/crates/naome-core/tests/install_plan.rs +2 -0
  76. package/crates/naome-core/tests/quality.rs +26 -123
  77. package/crates/naome-core/tests/quality_performance.rs +231 -0
  78. package/crates/naome-core/tests/quality_structure.rs +116 -0
  79. package/crates/naome-core/tests/quality_structure_adapters.rs +98 -0
  80. package/crates/naome-core/tests/quality_structure_policy.rs +144 -0
  81. package/crates/naome-core/tests/quality_structure_support/mod.rs +249 -0
  82. package/crates/naome-core/tests/repo_support/mod.rs +16 -0
  83. package/crates/naome-core/tests/repo_support/repo.rs +113 -0
  84. package/crates/naome-core/tests/repo_support/repo_factories.rs +99 -0
  85. package/crates/naome-core/tests/repo_support/repo_helpers.rs +123 -0
  86. package/crates/naome-core/tests/repo_support/routes.rs +81 -0
  87. package/crates/naome-core/tests/repo_support/verification.rs +168 -0
  88. package/crates/naome-core/tests/repo_support/verification_values.rs +135 -0
  89. package/crates/naome-core/tests/route.rs +1 -1376
  90. package/crates/naome-core/tests/route_baseline.rs +86 -0
  91. package/crates/naome-core/tests/route_completion.rs +141 -0
  92. package/crates/naome-core/tests/route_harness_refresh.rs +135 -0
  93. package/crates/naome-core/tests/route_user_diff.rs +202 -0
  94. package/crates/naome-core/tests/route_worktree.rs +54 -0
  95. package/crates/naome-core/tests/semantic_legacy.rs +140 -0
  96. package/crates/naome-core/tests/task_state.rs +60 -432
  97. package/crates/naome-core/tests/task_state_compact_support/repo.rs +1 -1
  98. package/crates/naome-core/tests/task_state_support/mod.rs +163 -0
  99. package/crates/naome-core/tests/task_state_support/states.rs +84 -0
  100. package/crates/naome-core/tests/verification.rs +4 -45
  101. package/crates/naome-core/tests/verification_contract.rs +22 -78
  102. package/crates/naome-core/tests/workflow_doctor.rs +24 -0
  103. package/crates/naome-core/tests/workflow_policy.rs +6 -1
  104. package/crates/naome-core/tests/workflow_support/mod.rs +1 -1
  105. package/installer/agents.js +90 -0
  106. package/installer/context.js +67 -0
  107. package/installer/filesystem.js +166 -0
  108. package/installer/flows.js +84 -0
  109. package/installer/git-boundary.js +171 -0
  110. package/installer/git-hook-content.js +36 -0
  111. package/installer/git-hooks.js +134 -0
  112. package/installer/git-local.js +2 -0
  113. package/installer/git-shared.js +35 -0
  114. package/installer/harness-file-ops.js +140 -0
  115. package/installer/harness-files.js +56 -0
  116. package/installer/harness-verification.js +123 -0
  117. package/installer/install-plan.js +66 -0
  118. package/installer/main.js +25 -0
  119. package/installer/manifest-state.js +167 -0
  120. package/installer/native-build.js +24 -0
  121. package/installer/native-format.js +6 -0
  122. package/installer/native.js +162 -0
  123. package/installer/output.js +131 -0
  124. package/installer/version.js +32 -0
  125. package/native/darwin-arm64/naome +0 -0
  126. package/native/linux-x64/naome +0 -0
  127. package/package.json +2 -1
  128. package/templates/naome-root/.naome/bin/check-harness-health.js +3 -3
  129. package/templates/naome-root/.naome/bin/check-task-state.js +3 -3
  130. package/templates/naome-root/.naome/bin/naome.js +32 -21
  131. package/templates/naome-root/.naome/manifest.json +5 -3
  132. package/templates/naome-root/.naome/repository-structure.json +90 -0
  133. package/templates/naome-root/.naome/verification.json +1 -0
  134. package/templates/naome-root/.naomeignore +1 -0
  135. package/templates/naome-root/docs/naome/agent-workflow.md +16 -14
  136. package/templates/naome-root/docs/naome/index.md +4 -3
  137. package/templates/naome-root/docs/naome/repository-quality.md +66 -4
  138. package/templates/naome-root/docs/naome/repository-structure.md +51 -0
  139. package/templates/naome-root/docs/naome/testing.md +2 -1
@@ -1,9 +1,14 @@
1
+ mod adapter_support;
1
2
  mod adapters;
2
3
  mod baseline;
4
+ mod cache;
3
5
  mod checks;
4
6
  mod cleanup;
5
7
  mod config;
8
+ mod config_support;
6
9
  mod scanner;
10
+ mod semantic;
11
+ mod structure;
7
12
  mod types;
8
13
 
9
14
  use std::path::Path;
@@ -11,15 +16,30 @@ use std::path::Path;
11
16
  use crate::models::NaomeError;
12
17
 
13
18
  pub use cleanup::{cleanup_plan_from_violations, cleanup_route_for_path};
19
+ pub use cache::{clear_quality_cache, quality_cache_status, QualityCacheStatus};
20
+ pub use semantic::{semantic_route_for_finding, SemanticFinding, SemanticReport};
21
+ pub use structure::{
22
+ explain_repository_structure, RepositoryStructureConfig, StructurePathExplanation,
23
+ };
14
24
  pub use types::{
15
- QualityCleanupPlan, QualityCleanupRoute, QualityCleanupTask, QualityInitResult, QualityMode,
16
- QualityReport, QualitySummary, QualityViolation, RepositoryQualityConfig,
25
+ QualityCleanupPlan, QualityCleanupRoute, QualityCleanupTask, QualityInitMode,
26
+ QualityInitResult, QualityMode, QualityReport, QualitySummary, QualityViolation,
27
+ RepositoryQualityConfig,
17
28
  };
18
29
 
19
- use self::baseline::{baseline_relative_path, read_baseline_fingerprints, write_baseline};
30
+ use self::baseline::{
31
+ baseline_relative_path, read_baseline_fingerprints, write_baseline,
32
+ write_empty_baseline_if_missing,
33
+ };
20
34
  use self::checks::run_quality_checks;
21
35
  use self::config::{config_relative_path, read_config, write_default_config_if_missing};
36
+ use self::scanner::collect_repo_paths;
22
37
  use self::scanner::scan_repository;
38
+ use self::semantic::run_semantic_checks;
39
+ use self::structure::{
40
+ run_repository_structure_checks, structure_config_relative_path,
41
+ write_default_structure_config_if_missing,
42
+ };
23
43
 
24
44
  pub fn check_repository_quality(
25
45
  root: &Path,
@@ -29,6 +49,7 @@ pub fn check_repository_quality(
29
49
  let context = scan_repository(root, mode, config)?;
30
50
  let baseline = read_baseline_fingerprints(root)?;
31
51
  let mut violations = run_quality_checks(&context);
52
+ violations.extend(run_repository_structure_checks(root, &context, &baseline)?);
32
53
  for violation in &mut violations {
33
54
  violation.baseline = baseline.contains(&violation.fingerprint);
34
55
  }
@@ -38,6 +59,10 @@ pub fn check_repository_quality(
38
59
  .filter(|violation| violation.baseline)
39
60
  .count();
40
61
  let ok = blocking_violation_count == 0;
62
+ let mut reason_codes = context.reason_codes.clone();
63
+ if mode == QualityMode::Report {
64
+ reason_codes.push("deep_checks_skipped".to_string());
65
+ }
41
66
 
42
67
  Ok(QualityReport {
43
68
  schema: "naome.repository-quality-report.v1".to_string(),
@@ -47,25 +72,60 @@ pub fn check_repository_quality(
47
72
  scanned_paths: context.scanned_paths(),
48
73
  summary: QualitySummary {
49
74
  scanned_files: context.files.len(),
75
+ scanned_path_count: context.scanned_paths().len(),
50
76
  violation_count: violations.len(),
51
77
  baseline_violation_count,
52
78
  blocking_violation_count,
79
+ truncated: context.truncated,
80
+ reason_codes,
81
+ cache_hits: context.cache_hits,
82
+ cache_misses: context.cache_misses,
53
83
  },
54
84
  violations,
55
85
  })
56
86
  }
57
87
 
58
88
  pub fn init_repository_quality(root: &Path) -> Result<QualityInitResult, NaomeError> {
89
+ init_repository_quality_with_mode(root, QualityInitMode::SeedOnly)
90
+ }
91
+
92
+ pub fn init_repository_quality_with_mode(
93
+ root: &Path,
94
+ mode: QualityInitMode,
95
+ ) -> Result<QualityInitResult, NaomeError> {
59
96
  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)?;
97
+ let structure_config_written = {
98
+ let repo_paths = collect_repo_paths(root)?;
99
+ write_default_structure_config_if_missing(root, &repo_paths)?
100
+ };
101
+ let (baseline_written, baseline_violations, baseline_pending) = match mode {
102
+ QualityInitMode::SeedOnly => {
103
+ write_empty_baseline_if_missing(root)?;
104
+ (false, 0, true)
105
+ }
106
+ QualityInitMode::Baseline | QualityInitMode::DeepBaseline => {
107
+ let report = check_repository_quality(
108
+ root,
109
+ if mode == QualityInitMode::DeepBaseline {
110
+ QualityMode::DeepReport
111
+ } else {
112
+ QualityMode::Report
113
+ },
114
+ )?;
115
+ (write_baseline(root, &report.violations)?, report.violations.len(), false)
116
+ }
117
+ };
62
118
 
63
119
  Ok(QualityInitResult {
64
120
  schema: "naome.repository-quality-init.v1".to_string(),
121
+ mode: mode.as_str().to_string(),
65
122
  config_written,
123
+ structure_config_written,
66
124
  baseline_written,
67
- baseline_violations: report.violations.len(),
125
+ baseline_pending,
126
+ baseline_violations,
68
127
  config_path: config_relative_path().to_string(),
128
+ structure_config_path: structure_config_relative_path().to_string(),
69
129
  baseline_path: baseline_relative_path().to_string(),
70
130
  })
71
131
  }
@@ -88,3 +148,9 @@ pub fn route_quality_cleanup(
88
148
  .collect::<Vec<_>>();
89
149
  Ok(cleanup_route_for_path(&path, violations))
90
150
  }
151
+
152
+ pub fn check_semantic_legacy(root: &Path, mode: QualityMode) -> Result<SemanticReport, NaomeError> {
153
+ let config = read_config(root)?;
154
+ let context = scan_repository(root, mode, config)?;
155
+ Ok(run_semantic_checks(&context))
156
+ }
@@ -0,0 +1,78 @@
1
+ pub(super) fn normalize_line(line: &str) -> Option<String> {
2
+ let trimmed = line.trim();
3
+ if trimmed.is_empty()
4
+ || is_comment_only(trimmed)
5
+ || is_string_list_item(trimmed)
6
+ || is_generated_hash_mapping(trimmed)
7
+ {
8
+ return None;
9
+ }
10
+
11
+ let mut normalized = String::new();
12
+ let mut in_string = false;
13
+ let mut quote = '\0';
14
+ let mut previous_space = false;
15
+ for character in trimmed.chars() {
16
+ if in_string {
17
+ if character == quote {
18
+ in_string = false;
19
+ normalized.push('S');
20
+ previous_space = false;
21
+ }
22
+ continue;
23
+ }
24
+ if character == '"' || character == '\'' || character == '`' {
25
+ in_string = true;
26
+ quote = character;
27
+ continue;
28
+ }
29
+ let next = if character.is_ascii_digit() {
30
+ '0'
31
+ } else if character.is_whitespace() {
32
+ ' '
33
+ } else {
34
+ character.to_ascii_lowercase()
35
+ };
36
+ if next == ' ' {
37
+ if !previous_space {
38
+ normalized.push(next);
39
+ }
40
+ previous_space = true;
41
+ } else {
42
+ normalized.push(next);
43
+ previous_space = false;
44
+ }
45
+ }
46
+ let value = normalized.trim().to_string();
47
+ (!value.is_empty()).then_some(value)
48
+ }
49
+
50
+ pub(super) fn token_set(line: &str) -> Vec<String> {
51
+ line.split(|character: char| !character.is_ascii_alphanumeric() && character != '_')
52
+ .filter(|token| token.len() > 1)
53
+ .map(ToString::to_string)
54
+ .collect()
55
+ }
56
+
57
+ fn is_comment_only(trimmed: &str) -> bool {
58
+ trimmed.starts_with("//")
59
+ || trimmed.starts_with('#')
60
+ || trimmed.starts_with("/*")
61
+ || trimmed.starts_with('*')
62
+ || trimmed.starts_with("--")
63
+ }
64
+
65
+ fn is_generated_hash_mapping(trimmed: &str) -> bool {
66
+ let Some((key, value)) = trimmed.split_once(':') else {
67
+ return false;
68
+ };
69
+ key.trim_start().starts_with('"')
70
+ && value.trim_start().starts_with("\"sha256:")
71
+ && value.chars().filter(|character| *character == '"').count() >= 2
72
+ }
73
+
74
+ fn is_string_list_item(trimmed: &str) -> bool {
75
+ let value = trimmed.trim_end_matches(',');
76
+ (value.starts_with('"') && value.ends_with('"'))
77
+ || (value.starts_with('\'') && value.ends_with('\''))
78
+ }
@@ -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,