@lamentis/naome 1.1.2 → 1.2.1

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 (204) 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 +2 -1579
  6. package/bin/naome.js +68 -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 +37 -0
  11. package/crates/naome-cli/src/install_bridge.rs +83 -0
  12. package/crates/naome-cli/src/main.rs +60 -341
  13. package/crates/naome-cli/src/prompt_commands.rs +68 -0
  14. package/crates/naome-cli/src/quality_commands.rs +229 -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/decision/checks.rs +64 -0
  19. package/crates/naome-core/src/decision/idle.rs +67 -0
  20. package/crates/naome-core/src/decision/json.rs +36 -0
  21. package/crates/naome-core/src/decision/states.rs +165 -0
  22. package/crates/naome-core/src/decision.rs +131 -353
  23. package/crates/naome-core/src/harness_health/integrity.rs +96 -0
  24. package/crates/naome-core/src/harness_health.rs +14 -126
  25. package/crates/naome-core/src/install_plan.rs +5 -0
  26. package/crates/naome-core/src/intent/classifier.rs +171 -0
  27. package/crates/naome-core/src/intent/envelope.rs +108 -0
  28. package/crates/naome-core/src/intent/legacy.rs +138 -0
  29. package/crates/naome-core/src/intent/legacy_response.rs +76 -0
  30. package/crates/naome-core/src/intent/model.rs +71 -0
  31. package/crates/naome-core/src/intent/patterns.rs +170 -0
  32. package/crates/naome-core/src/intent/resolver.rs +162 -0
  33. package/crates/naome-core/src/intent/resolver_active.rs +17 -0
  34. package/crates/naome-core/src/intent/resolver_baseline.rs +55 -0
  35. package/crates/naome-core/src/intent/resolver_catalog.rs +167 -0
  36. package/crates/naome-core/src/intent/resolver_policy.rs +72 -0
  37. package/crates/naome-core/src/intent/resolver_shared.rs +55 -0
  38. package/crates/naome-core/src/intent/risk.rs +40 -0
  39. package/crates/naome-core/src/intent/segment.rs +170 -0
  40. package/crates/naome-core/src/intent.rs +64 -879
  41. package/crates/naome-core/src/journal.rs +9 -20
  42. package/crates/naome-core/src/lib.rs +15 -0
  43. package/crates/naome-core/src/paths.rs +3 -1
  44. package/crates/naome-core/src/quality/adapter_support.rs +89 -0
  45. package/crates/naome-core/src/quality/adapters.rs +131 -0
  46. package/crates/naome-core/src/quality/baseline.rs +75 -0
  47. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +175 -0
  48. package/crates/naome-core/src/quality/checks/near_duplicates.rs +130 -0
  49. package/crates/naome-core/src/quality/checks.rs +228 -0
  50. package/crates/naome-core/src/quality/cleanup.rs +84 -0
  51. package/crates/naome-core/src/quality/config.rs +102 -0
  52. package/crates/naome-core/src/quality/config_support.rs +24 -0
  53. package/crates/naome-core/src/quality/mod.rs +108 -0
  54. package/crates/naome-core/src/quality/scanner/repo_paths.rs +103 -0
  55. package/crates/naome-core/src/quality/scanner.rs +379 -0
  56. package/crates/naome-core/src/quality/structure/adapters.rs +84 -0
  57. package/crates/naome-core/src/quality/structure/checks/basic.rs +153 -0
  58. package/crates/naome-core/src/quality/structure/checks/directory.rs +144 -0
  59. package/crates/naome-core/src/quality/structure/checks/pairing.rs +63 -0
  60. package/crates/naome-core/src/quality/structure/checks.rs +124 -0
  61. package/crates/naome-core/src/quality/structure/classify/roles.rs +188 -0
  62. package/crates/naome-core/src/quality/structure/classify.rs +94 -0
  63. package/crates/naome-core/src/quality/structure/config.rs +89 -0
  64. package/crates/naome-core/src/quality/structure/defaults.rs +107 -0
  65. package/crates/naome-core/src/quality/structure/mod.rs +77 -0
  66. package/crates/naome-core/src/quality/structure/model.rs +124 -0
  67. package/crates/naome-core/src/quality/types.rs +292 -0
  68. package/crates/naome-core/src/route/builtin_checks.rs +155 -0
  69. package/crates/naome-core/src/route/builtin_context.rs +73 -0
  70. package/crates/naome-core/src/route/builtin_integrity.rs +49 -0
  71. package/crates/naome-core/src/route/builtin_require.rs +40 -0
  72. package/crates/naome-core/src/route/context.rs +180 -0
  73. package/crates/naome-core/src/route/execution.rs +96 -0
  74. package/crates/naome-core/src/route/execution_baselines.rs +146 -0
  75. package/crates/naome-core/src/route/execution_support.rs +57 -0
  76. package/crates/naome-core/src/route/execution_tasks.rs +71 -0
  77. package/crates/naome-core/src/route/git_ops.rs +72 -0
  78. package/crates/naome-core/src/route/quality_gate.rs +73 -0
  79. package/crates/naome-core/src/route/quality_gate_config.rs +126 -0
  80. package/crates/naome-core/src/route/quality_gate_snapshot.rs +69 -0
  81. package/crates/naome-core/src/route/worktree.rs +75 -0
  82. package/crates/naome-core/src/route/worktree_files.rs +32 -0
  83. package/crates/naome-core/src/route/worktree_plan.rs +131 -0
  84. package/crates/naome-core/src/route.rs +44 -1155
  85. package/crates/naome-core/src/task_state/admission.rs +63 -0
  86. package/crates/naome-core/src/task_state/admission_proof.rs +72 -0
  87. package/crates/naome-core/src/task_state/api.rs +130 -0
  88. package/crates/naome-core/src/task_state/commit_gate.rs +138 -0
  89. package/crates/naome-core/src/task_state/compact_proof.rs +160 -0
  90. package/crates/naome-core/src/task_state/completed_refresh.rs +89 -0
  91. package/crates/naome-core/src/task_state/completion.rs +72 -0
  92. package/crates/naome-core/src/task_state/deleted_paths.rs +47 -0
  93. package/crates/naome-core/src/task_state/diff.rs +95 -0
  94. package/crates/naome-core/src/task_state/evidence.rs +154 -0
  95. package/crates/naome-core/src/task_state/git_io.rs +86 -0
  96. package/crates/naome-core/src/task_state/git_parse.rs +86 -0
  97. package/crates/naome-core/src/task_state/git_refs.rs +37 -0
  98. package/crates/naome-core/src/task_state/human_review_state.rs +31 -0
  99. package/crates/naome-core/src/task_state/mod.rs +38 -0
  100. package/crates/naome-core/src/task_state/process_guard.rs +40 -0
  101. package/crates/naome-core/src/task_state/progress.rs +123 -0
  102. package/crates/naome-core/src/task_state/proof.rs +139 -0
  103. package/crates/naome-core/src/task_state/proof_entry.rs +66 -0
  104. package/crates/naome-core/src/task_state/proof_model.rs +70 -0
  105. package/crates/naome-core/src/task_state/proof_sources.rs +76 -0
  106. package/crates/naome-core/src/task_state/push_gate.rs +49 -0
  107. package/crates/naome-core/src/task_state/reconcile.rs +7 -0
  108. package/crates/naome-core/src/task_state/repair.rs +168 -0
  109. package/crates/naome-core/src/task_state/shape.rs +117 -0
  110. package/crates/naome-core/src/task_state/task_diff_api.rs +170 -0
  111. package/crates/naome-core/src/task_state/task_records.rs +131 -0
  112. package/crates/naome-core/src/task_state/task_references.rs +126 -0
  113. package/crates/naome-core/src/task_state/types.rs +87 -0
  114. package/crates/naome-core/src/task_state/util.rs +137 -0
  115. package/crates/naome-core/src/verification/render.rs +122 -0
  116. package/crates/naome-core/src/verification.rs +177 -58
  117. package/crates/naome-core/src/verification_contract.rs +49 -21
  118. package/crates/naome-core/src/workflow/integrity.rs +123 -0
  119. package/crates/naome-core/src/workflow/integrity_normalize.rs +7 -0
  120. package/crates/naome-core/src/workflow/integrity_support.rs +110 -0
  121. package/crates/naome-core/src/workflow/mod.rs +18 -0
  122. package/crates/naome-core/src/workflow/mutation.rs +68 -0
  123. package/crates/naome-core/src/workflow/output.rs +111 -0
  124. package/crates/naome-core/src/workflow/phase_inference.rs +73 -0
  125. package/crates/naome-core/src/workflow/phases.rs +169 -0
  126. package/crates/naome-core/src/workflow/policy.rs +156 -0
  127. package/crates/naome-core/src/workflow/processes.rs +91 -0
  128. package/crates/naome-core/src/workflow/types.rs +42 -0
  129. package/crates/naome-core/tests/decision.rs +24 -118
  130. package/crates/naome-core/tests/harness_health.rs +5 -0
  131. package/crates/naome-core/tests/intent.rs +97 -792
  132. package/crates/naome-core/tests/intent_support/mod.rs +133 -0
  133. package/crates/naome-core/tests/intent_v2.rs +90 -0
  134. package/crates/naome-core/tests/quality.rs +319 -0
  135. package/crates/naome-core/tests/quality_structure.rs +116 -0
  136. package/crates/naome-core/tests/quality_structure_adapters.rs +98 -0
  137. package/crates/naome-core/tests/quality_structure_policy.rs +125 -0
  138. package/crates/naome-core/tests/quality_structure_support/mod.rs +249 -0
  139. package/crates/naome-core/tests/repo_support/mod.rs +16 -0
  140. package/crates/naome-core/tests/repo_support/repo.rs +113 -0
  141. package/crates/naome-core/tests/repo_support/repo_factories.rs +99 -0
  142. package/crates/naome-core/tests/repo_support/repo_helpers.rs +123 -0
  143. package/crates/naome-core/tests/repo_support/routes.rs +81 -0
  144. package/crates/naome-core/tests/repo_support/verification.rs +168 -0
  145. package/crates/naome-core/tests/repo_support/verification_values.rs +135 -0
  146. package/crates/naome-core/tests/route.rs +1 -1476
  147. package/crates/naome-core/tests/route_baseline.rs +86 -0
  148. package/crates/naome-core/tests/route_completion.rs +141 -0
  149. package/crates/naome-core/tests/route_harness_refresh.rs +135 -0
  150. package/crates/naome-core/tests/route_user_diff.rs +198 -0
  151. package/crates/naome-core/tests/route_worktree.rs +54 -0
  152. package/crates/naome-core/tests/task_state.rs +60 -429
  153. package/crates/naome-core/tests/task_state_compact.rs +110 -0
  154. package/crates/naome-core/tests/task_state_compact_support/mod.rs +5 -0
  155. package/crates/naome-core/tests/task_state_compact_support/repo.rs +130 -0
  156. package/crates/naome-core/tests/task_state_compact_support/states.rs +151 -0
  157. package/crates/naome-core/tests/task_state_support/mod.rs +163 -0
  158. package/crates/naome-core/tests/task_state_support/states.rs +84 -0
  159. package/crates/naome-core/tests/verification.rs +4 -45
  160. package/crates/naome-core/tests/verification_contract.rs +22 -78
  161. package/crates/naome-core/tests/workflow_integrity.rs +85 -0
  162. package/crates/naome-core/tests/workflow_policy.rs +139 -0
  163. package/crates/naome-core/tests/workflow_support/mod.rs +194 -0
  164. package/installer/agents.js +90 -0
  165. package/installer/context.js +67 -0
  166. package/installer/filesystem.js +166 -0
  167. package/installer/flows.js +84 -0
  168. package/installer/git-boundary.js +170 -0
  169. package/installer/git-hook-content.js +36 -0
  170. package/installer/git-hooks.js +134 -0
  171. package/installer/git-local.js +2 -0
  172. package/installer/git-shared.js +35 -0
  173. package/installer/harness-file-ops.js +140 -0
  174. package/installer/harness-files.js +56 -0
  175. package/installer/harness-verification.js +123 -0
  176. package/installer/install-plan.js +66 -0
  177. package/installer/main.js +25 -0
  178. package/installer/manifest-state.js +167 -0
  179. package/installer/native-build.js +24 -0
  180. package/installer/native-format.js +6 -0
  181. package/installer/native.js +162 -0
  182. package/installer/output.js +131 -0
  183. package/installer/version.js +32 -0
  184. package/native/darwin-arm64/naome +0 -0
  185. package/native/linux-x64/naome +0 -0
  186. package/package.json +3 -2
  187. package/templates/naome-root/.naome/bin/check-harness-health.js +66 -85
  188. package/templates/naome-root/.naome/bin/check-task-state.js +9 -10
  189. package/templates/naome-root/.naome/bin/naome.js +51 -76
  190. package/templates/naome-root/.naome/manifest.json +22 -18
  191. package/templates/naome-root/.naome/repository-quality-baseline.json +5 -0
  192. package/templates/naome-root/.naome/repository-quality.json +24 -0
  193. package/templates/naome-root/.naome/repository-structure.json +90 -0
  194. package/templates/naome-root/.naome/task-contract.schema.json +93 -11
  195. package/templates/naome-root/.naome/upgrade-state.json +1 -1
  196. package/templates/naome-root/.naome/verification.json +38 -0
  197. package/templates/naome-root/AGENTS.md +3 -0
  198. package/templates/naome-root/docs/naome/agent-workflow.md +25 -12
  199. package/templates/naome-root/docs/naome/execution.md +25 -21
  200. package/templates/naome-root/docs/naome/index.md +5 -3
  201. package/templates/naome-root/docs/naome/repository-quality.md +46 -0
  202. package/templates/naome-root/docs/naome/repository-structure.md +51 -0
  203. package/templates/naome-root/docs/naome/testing.md +13 -0
  204. package/crates/naome-core/src/task_state.rs +0 -2210
@@ -0,0 +1,379 @@
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 repo_paths: Vec<String>,
24
+ pub target_paths: HashSet<String>,
25
+ pub files: Vec<FileAnalysis>,
26
+ }
27
+
28
+ impl QualityContext {
29
+ pub fn scanned_paths(&self) -> Vec<String> {
30
+ self.files.iter().map(|file| file.path.clone()).collect()
31
+ }
32
+
33
+ pub fn applies_to(&self, path: &str) -> bool {
34
+ self.mode == QualityMode::Report || self.target_paths.contains(path)
35
+ }
36
+
37
+ pub fn check_applies_to(&self, check_id: &str, path: &str) -> bool {
38
+ self.applies_to(path) && self.config.check_enabled_for_path(check_id, path)
39
+ }
40
+
41
+ pub fn limits_for(&self, path: &str) -> QualityLimits {
42
+ self.config.limits_for_path(path)
43
+ }
44
+ }
45
+
46
+ #[derive(Debug, Clone)]
47
+ pub struct FileAnalysis {
48
+ pub path: String,
49
+ pub line_count: usize,
50
+ pub added_lines: usize,
51
+ pub normalized_lines: Vec<NormalizedLine>,
52
+ pub symbols: Vec<SymbolAnalysis>,
53
+ }
54
+
55
+ #[derive(Debug, Clone)]
56
+ pub struct NormalizedLine {
57
+ pub line_number: usize,
58
+ pub value: String,
59
+ }
60
+
61
+ #[derive(Debug, Clone)]
62
+ pub struct SymbolAnalysis {
63
+ pub kind: String,
64
+ pub name: String,
65
+ pub start_line: usize,
66
+ pub end_line: usize,
67
+ pub indent: usize,
68
+ pub tokens: HashSet<String>,
69
+ }
70
+
71
+ impl SymbolAnalysis {
72
+ pub fn line_count(&self) -> usize {
73
+ self.end_line.saturating_sub(self.start_line) + 1
74
+ }
75
+ }
76
+
77
+ pub fn scan_repository(
78
+ root: &Path,
79
+ mode: QualityMode,
80
+ config: RepositoryQualityConfig,
81
+ ) -> Result<QualityContext, NaomeError> {
82
+ let changed_paths = git::changed_paths(root)?;
83
+ let target_paths = changed_paths.iter().cloned().collect::<HashSet<_>>();
84
+ let mut whole_repo_paths = collect_repo_paths(root)?;
85
+ whole_repo_paths.sort();
86
+ whole_repo_paths.dedup();
87
+ let scan_paths = match mode {
88
+ QualityMode::Changed => changed_paths.clone(),
89
+ QualityMode::Report => whole_repo_paths.clone(),
90
+ };
91
+ let added_lines = added_lines_by_path(root)?;
92
+ let ignore_patterns = ignore_patterns(root, &config);
93
+ let mut files = Vec::new();
94
+
95
+ for path in scan_paths {
96
+ if should_skip_path(&path, &ignore_patterns) {
97
+ continue;
98
+ }
99
+ if let Some(file) = analyze_repo_file(root, &path, &added_lines) {
100
+ files.push(file);
101
+ }
102
+ }
103
+
104
+ if mode == QualityMode::Changed {
105
+ let scanned = files
106
+ .iter()
107
+ .map(|file| file.path.clone())
108
+ .collect::<HashSet<_>>();
109
+ for path in &whole_repo_paths {
110
+ if scanned.contains(path) || should_skip_path(path, &ignore_patterns) {
111
+ continue;
112
+ }
113
+ if let Some(file) = analyze_repo_file(root, path, &added_lines) {
114
+ files.push(file);
115
+ }
116
+ }
117
+ }
118
+
119
+ files.sort_by(|left, right| left.path.cmp(&right.path));
120
+ Ok(QualityContext {
121
+ mode,
122
+ config,
123
+ changed_paths,
124
+ repo_paths: whole_repo_paths,
125
+ target_paths,
126
+ files,
127
+ })
128
+ }
129
+
130
+ pub fn stable_fingerprint(parts: &[&str]) -> String {
131
+ let mut hasher = Sha256::new();
132
+ for part in parts {
133
+ hasher.update(part.as_bytes());
134
+ hasher.update(b"\0");
135
+ }
136
+ format!("sha256:{:x}", hasher.finalize())
137
+ }
138
+
139
+ fn analyze_repo_file(
140
+ root: &Path,
141
+ path: &str,
142
+ added_lines: &HashMap<String, usize>,
143
+ ) -> Option<FileAnalysis> {
144
+ let full_path = root.join(path);
145
+ if !full_path.is_file() || is_binary_extension(path) {
146
+ return None;
147
+ }
148
+ let content = fs::read_to_string(&full_path).ok()?;
149
+ Some(analyze_file(
150
+ path,
151
+ &content,
152
+ added_lines.get(path).copied().unwrap_or(0),
153
+ ))
154
+ }
155
+
156
+ fn analyze_file(path: &str, content: &str, added_lines: usize) -> FileAnalysis {
157
+ let lines = content.lines().collect::<Vec<_>>();
158
+ let normalized_lines = lines
159
+ .iter()
160
+ .enumerate()
161
+ .filter_map(|(index, line)| normalize_line(line).map(|value| (index + 1, value)))
162
+ .map(|(line_number, value)| NormalizedLine { line_number, value })
163
+ .collect::<Vec<_>>();
164
+ let symbols = detect_symbols(&lines);
165
+ FileAnalysis {
166
+ path: path.to_string(),
167
+ line_count: lines.len(),
168
+ added_lines,
169
+ normalized_lines,
170
+ symbols,
171
+ }
172
+ }
173
+
174
+ fn detect_symbols(lines: &[&str]) -> Vec<SymbolAnalysis> {
175
+ let mut starts = Vec::new();
176
+ for (index, line) in lines.iter().enumerate() {
177
+ let indent = indentation(line);
178
+ if let Some((kind, name)) = symbol_start(line.trim()) {
179
+ starts.push((index, indent, kind, name));
180
+ }
181
+ }
182
+
183
+ let mut symbols = Vec::new();
184
+ for (position, (start_index, indent, kind, name)) in starts.iter().enumerate() {
185
+ let end_index = starts
186
+ .iter()
187
+ .skip(position + 1)
188
+ .find(|(_, next_indent, _, _)| next_indent <= indent)
189
+ .map(|(next_index, _, _, _)| next_index.saturating_sub(1))
190
+ .unwrap_or_else(|| lines.len().saturating_sub(1));
191
+ let normalized_body = lines[*start_index..=end_index]
192
+ .iter()
193
+ .filter_map(|line| normalize_line(line))
194
+ .collect::<Vec<_>>();
195
+ let tokens = normalized_body
196
+ .iter()
197
+ .flat_map(|line| token_set(line))
198
+ .collect::<HashSet<_>>();
199
+ symbols.push(SymbolAnalysis {
200
+ kind: kind.clone(),
201
+ name: name.clone(),
202
+ start_line: start_index + 1,
203
+ end_line: end_index + 1,
204
+ indent: *indent,
205
+ tokens,
206
+ });
207
+ }
208
+ symbols
209
+ }
210
+
211
+ fn symbol_start(trimmed: &str) -> Option<(String, String)> {
212
+ let candidates = [
213
+ ("function", "function "),
214
+ ("function", "export function "),
215
+ ("function", "async function "),
216
+ ("function", "export async function "),
217
+ ("function", "def "),
218
+ ("function", "fn "),
219
+ ("function", "pub fn "),
220
+ ("function", "func "),
221
+ ("class", "class "),
222
+ ("struct", "struct "),
223
+ ("struct", "pub struct "),
224
+ ("enum", "enum "),
225
+ ("enum", "pub enum "),
226
+ ("impl", "impl "),
227
+ ];
228
+ for (kind, prefix) in candidates {
229
+ if let Some(rest) = trimmed.strip_prefix(prefix) {
230
+ return Some((kind.to_string(), symbol_name(rest)));
231
+ }
232
+ }
233
+ for prefix in ["const ", "let ", "export const ", "export let "] {
234
+ if let Some(rest) = trimmed.strip_prefix(prefix) {
235
+ if trimmed.contains("=>") || trimmed.contains("function") || trimmed.contains("React.")
236
+ {
237
+ return Some(("function".to_string(), symbol_name(rest)));
238
+ }
239
+ }
240
+ }
241
+ None
242
+ }
243
+
244
+ fn symbol_name(rest: &str) -> String {
245
+ rest.chars()
246
+ .take_while(|character| character.is_ascii_alphanumeric() || *character == '_')
247
+ .collect::<String>()
248
+ .trim_matches('_')
249
+ .to_string()
250
+ }
251
+
252
+ fn normalize_line(line: &str) -> Option<String> {
253
+ let trimmed = line.trim();
254
+ if trimmed.is_empty()
255
+ || is_comment_only(trimmed)
256
+ || is_string_list_item(trimmed)
257
+ || is_generated_hash_mapping(trimmed)
258
+ {
259
+ return None;
260
+ }
261
+
262
+ let mut normalized = String::new();
263
+ let mut in_string = false;
264
+ let mut quote = '\0';
265
+ let mut previous_space = false;
266
+ for character in trimmed.chars() {
267
+ if in_string {
268
+ if character == quote {
269
+ in_string = false;
270
+ normalized.push('S');
271
+ previous_space = false;
272
+ }
273
+ continue;
274
+ }
275
+ if character == '"' || character == '\'' || character == '`' {
276
+ in_string = true;
277
+ quote = character;
278
+ continue;
279
+ }
280
+ let next = if character.is_ascii_digit() {
281
+ '0'
282
+ } else if character.is_whitespace() {
283
+ ' '
284
+ } else {
285
+ character.to_ascii_lowercase()
286
+ };
287
+ if next == ' ' {
288
+ if !previous_space {
289
+ normalized.push(next);
290
+ }
291
+ previous_space = true;
292
+ } else {
293
+ normalized.push(next);
294
+ previous_space = false;
295
+ }
296
+ }
297
+ let value = normalized.trim().to_string();
298
+ (!value.is_empty()).then_some(value)
299
+ }
300
+
301
+ fn is_comment_only(trimmed: &str) -> bool {
302
+ trimmed.starts_with("//")
303
+ || trimmed.starts_with('#')
304
+ || trimmed.starts_with("/*")
305
+ || trimmed.starts_with('*')
306
+ || trimmed.starts_with("--")
307
+ }
308
+
309
+ fn is_generated_hash_mapping(trimmed: &str) -> bool {
310
+ let Some((key, value)) = trimmed.split_once(':') else {
311
+ return false;
312
+ };
313
+ key.trim_start().starts_with('"')
314
+ && value.trim_start().starts_with("\"sha256:")
315
+ && value.chars().filter(|character| *character == '"').count() >= 2
316
+ }
317
+
318
+ fn is_string_list_item(trimmed: &str) -> bool {
319
+ let value = trimmed.trim_end_matches(',');
320
+ (value.starts_with('"') && value.ends_with('"'))
321
+ || (value.starts_with('\'') && value.ends_with('\''))
322
+ }
323
+
324
+ fn token_set(line: &str) -> Vec<String> {
325
+ line.split(|character: char| !character.is_ascii_alphanumeric() && character != '_')
326
+ .filter(|token| token.len() > 1)
327
+ .map(ToString::to_string)
328
+ .collect()
329
+ }
330
+
331
+ fn indentation(line: &str) -> usize {
332
+ line.chars()
333
+ .take_while(|character| character.is_whitespace())
334
+ .map(|character| if character == '\t' { 2 } else { 1 })
335
+ .sum()
336
+ }
337
+
338
+ fn ignore_patterns(root: &Path, config: &RepositoryQualityConfig) -> Vec<String> {
339
+ let mut patterns = Vec::new();
340
+ patterns.extend(read_naomeignore_patterns(root));
341
+ patterns.extend(default_ignored_paths());
342
+ patterns.extend(default_generated_paths());
343
+ patterns.extend(config.ignored_paths.clone());
344
+ patterns.extend(config.generated_paths.clone());
345
+ patterns
346
+ }
347
+
348
+ fn read_naomeignore_patterns(root: &Path) -> Vec<String> {
349
+ let Ok(content) = fs::read_to_string(root.join(".naomeignore")) else {
350
+ return Vec::new();
351
+ };
352
+ content
353
+ .lines()
354
+ .map(str::trim)
355
+ .filter(|line| !line.is_empty() && !line.starts_with('#') && !line.starts_with('!'))
356
+ .map(|pattern| {
357
+ let normalized = pattern.trim_start_matches("./").replace('\\', "/");
358
+ if normalized.ends_with('/') {
359
+ format!("{normalized}**")
360
+ } else {
361
+ normalized
362
+ }
363
+ })
364
+ .collect()
365
+ }
366
+
367
+ fn should_skip_path(path: &str, patterns: &[String]) -> bool {
368
+ paths::matches_any(path, patterns)
369
+ }
370
+
371
+ fn is_binary_extension(path: &str) -> bool {
372
+ let lower = path.to_ascii_lowercase();
373
+ [
374
+ ".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".pdf", ".zip", ".gz", ".tgz", ".wasm",
375
+ ".dylib", ".so", ".dll", ".exe", ".bin",
376
+ ]
377
+ .iter()
378
+ .any(|extension| lower.ends_with(extension))
379
+ }
@@ -0,0 +1,84 @@
1
+ use crate::models::NaomeError;
2
+
3
+ use crate::quality::adapter_support::{
4
+ detected_ids, detects_javascript_typescript_project, detects_rust_project, extend_unique,
5
+ find_adapter_by_id, validate_ids, AdapterDescriptor, RepoSignals,
6
+ };
7
+
8
+ use super::model::RepositoryStructureConfig;
9
+
10
+ const CONFIG_PATH: &str = ".naome/repository-structure.json";
11
+
12
+ struct StructureAdapter {
13
+ id: &'static str,
14
+ detect: fn(&RepoSignals<'_>) -> bool,
15
+ source_roots: &'static [&'static str],
16
+ test_roots: &'static [&'static str],
17
+ module_roots: &'static [&'static str],
18
+ allowed_root_files: &'static [&'static str],
19
+ }
20
+
21
+ impl AdapterDescriptor for StructureAdapter {
22
+ fn id(&self) -> &'static str {
23
+ self.id
24
+ }
25
+
26
+ fn detects(&self, signals: &RepoSignals<'_>) -> bool {
27
+ (self.detect)(signals)
28
+ }
29
+ }
30
+
31
+ pub fn detected_structure_adapter_ids(paths: &[String]) -> Vec<String> {
32
+ detected_ids(paths, registry())
33
+ }
34
+
35
+ pub fn apply_structure_adapters(
36
+ mut config: RepositoryStructureConfig,
37
+ ) -> Result<RepositoryStructureConfig, NaomeError> {
38
+ validate_structure_adapter_ids(&config.enabled_adapters)?;
39
+ for adapter_id in config.enabled_adapters.clone() {
40
+ let adapter = find_adapter_by_id(registry(), &adapter_id, CONFIG_PATH)?;
41
+ extend_unique(&mut config.source_roots, adapter.source_roots);
42
+ extend_unique(&mut config.test_roots, adapter.test_roots);
43
+ extend_unique(&mut config.module_roots, adapter.module_roots);
44
+ extend_unique(&mut config.allowed_root_files, adapter.allowed_root_files);
45
+ }
46
+ Ok(config)
47
+ }
48
+
49
+ pub fn validate_structure_adapter_ids(ids: &[String]) -> Result<(), NaomeError> {
50
+ validate_ids(ids, registry(), CONFIG_PATH)
51
+ }
52
+
53
+ fn registry() -> &'static [StructureAdapter] {
54
+ &[
55
+ StructureAdapter {
56
+ id: "rust",
57
+ detect: detects_rust_project,
58
+ source_roots: &["src/**", "crates/*/src/**", "**/crates/*/src/**"],
59
+ test_roots: &["tests/**", "crates/*/tests/**", "**/crates/*/tests/**"],
60
+ module_roots: &["src/**", "crates/*/src/**", "**/crates/*/src/**"],
61
+ allowed_root_files: &["Cargo.toml", "Cargo.lock"],
62
+ },
63
+ StructureAdapter {
64
+ id: "javascript-typescript",
65
+ detect: detects_javascript_typescript_project,
66
+ source_roots: &[
67
+ "src/**",
68
+ "app/**",
69
+ "pages/**",
70
+ "components/**",
71
+ "packages/*/src/**",
72
+ "**/packages/*/src/**",
73
+ ],
74
+ test_roots: &["test/**", "tests/**", "__tests__/**", "packages/*/tests/**"],
75
+ module_roots: &["src/**", "app/**", "packages/*/src/**"],
76
+ allowed_root_files: &[
77
+ "package.json",
78
+ "tsconfig.json",
79
+ "vite.config.ts",
80
+ "next.config.js",
81
+ ],
82
+ },
83
+ ]
84
+ }
@@ -0,0 +1,153 @@
1
+ use crate::paths;
2
+ use crate::quality::types::{QualityMode, QualityViolation};
3
+
4
+ use super::{applies, push};
5
+ use crate::quality::structure::model::{DirectoryRoleRule, RepositoryStructureModel};
6
+
7
+ pub(super) fn root_file_sprawl(
8
+ model: &RepositoryStructureModel,
9
+ mode: QualityMode,
10
+ violations: &mut Vec<QualityViolation>,
11
+ ) {
12
+ for path in model
13
+ .paths
14
+ .iter()
15
+ .filter(|path| applies(path, mode) && path.segments.len() == 1)
16
+ {
17
+ if paths::matches_any(&path.explanation.path, &model.config.allowed_root_files) {
18
+ continue;
19
+ }
20
+ push_single_path(
21
+ "root-file-sprawl",
22
+ &path.explanation.path,
23
+ format!(
24
+ "{} is a root-level file without a recognized root role; place new work inside a module, docs, config, or script directory.",
25
+ path.explanation.path
26
+ ),
27
+ violations,
28
+ );
29
+ }
30
+ }
31
+
32
+ pub(super) fn misplaced_files(
33
+ model: &RepositoryStructureModel,
34
+ mode: QualityMode,
35
+ violations: &mut Vec<QualityViolation>,
36
+ ) {
37
+ for path in model.paths.iter().filter(|path| applies(path, mode)) {
38
+ let role = path.explanation.role.as_str();
39
+ let misplaced_test =
40
+ role == "test" && !paths::matches_any(&path.explanation.path, &model.config.test_roots);
41
+ if misplaced_test {
42
+ push_single_path(
43
+ "misplaced-file-role",
44
+ &path.explanation.path,
45
+ format!(
46
+ "{} is test code outside a configured test root.",
47
+ path.explanation.path
48
+ ),
49
+ violations,
50
+ );
51
+ }
52
+ }
53
+ }
54
+
55
+ pub(super) fn directory_role_mixing(
56
+ model: &RepositoryStructureModel,
57
+ mode: QualityMode,
58
+ violations: &mut Vec<QualityViolation>,
59
+ ) {
60
+ for path in model.paths.iter().filter(|path| {
61
+ applies(path, mode)
62
+ && matches!(path.explanation.role.as_str(), "generated" | "artifact")
63
+ && paths::matches_any(&path.explanation.path, &model.config.source_roots)
64
+ }) {
65
+ if role_rule_allows_path(model, &path.explanation.path, &path.explanation.role) {
66
+ continue;
67
+ }
68
+ push_single_path(
69
+ "directory-role-mixing",
70
+ &path.explanation.path,
71
+ format!(
72
+ "{} is {} content under a source root.",
73
+ path.explanation.path, path.explanation.role
74
+ ),
75
+ violations,
76
+ );
77
+ }
78
+
79
+ for directory in &model.directories {
80
+ let rule = directory_role_rule_for(model, &directory.path);
81
+ let allowed_by_rule = rule
82
+ .filter(|rule| !rule.allowed_roles.is_empty())
83
+ .is_some_and(|rule| {
84
+ directory
85
+ .roles
86
+ .iter()
87
+ .all(|role| rule.allowed_roles.iter().any(|allowed| allowed == role))
88
+ });
89
+ let max_roles = rule
90
+ .and_then(|rule| rule.max_roles)
91
+ .unwrap_or(model.config.limits.max_directory_roles);
92
+ let incompatible = directory.roles.contains("source")
93
+ && (directory.roles.contains("generated")
94
+ || directory.roles.contains("artifact")
95
+ || directory.roles.contains("dependency/vendor"));
96
+ let too_many_roles = directory.roles.len() > max_roles
97
+ && (directory.roles.contains("source")
98
+ || directory.roles.contains("generated")
99
+ || directory.roles.contains("artifact"));
100
+ if (!incompatible || allowed_by_rule) && !too_many_roles {
101
+ continue;
102
+ }
103
+ for path in model.paths.iter().filter(|path| {
104
+ path.explanation.directory == directory.path
105
+ && applies(path, mode)
106
+ && path.explanation.role != "source"
107
+ }) {
108
+ push(
109
+ "directory-role-mixing",
110
+ &path.explanation.path,
111
+ format!(
112
+ "{} mixes incompatible directory roles: {}.",
113
+ directory.path,
114
+ directory
115
+ .roles
116
+ .iter()
117
+ .cloned()
118
+ .collect::<Vec<_>>()
119
+ .join(", ")
120
+ ),
121
+ vec![],
122
+ violations,
123
+ );
124
+ }
125
+ }
126
+ }
127
+
128
+ fn role_rule_allows_path(model: &RepositoryStructureModel, path: &str, role: &str) -> bool {
129
+ model.config.directory_role_rules.iter().any(|rule| {
130
+ !rule.allowed_roles.is_empty()
131
+ && paths::matches_any(path, &rule.paths)
132
+ && rule.allowed_roles.iter().any(|allowed| allowed == role)
133
+ })
134
+ }
135
+
136
+ fn directory_role_rule_for<'a>(
137
+ model: &'a RepositoryStructureModel,
138
+ directory: &str,
139
+ ) -> Option<&'a DirectoryRoleRule> {
140
+ model.config.directory_role_rules.iter().find(|rule| {
141
+ paths::matches_any(directory, &rule.paths)
142
+ || paths::matches_any(&format!("{directory}/_"), &rule.paths)
143
+ })
144
+ }
145
+
146
+ fn push_single_path(
147
+ check_id: &str,
148
+ path: &str,
149
+ message: String,
150
+ violations: &mut Vec<QualityViolation>,
151
+ ) {
152
+ push(check_id, path, message, vec![], violations);
153
+ }