@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,144 @@
1
+ use std::collections::BTreeMap;
2
+
3
+ use crate::quality::structure::model::{RepositoryStructureModel, StructurePath};
4
+ use crate::quality::types::{QualityMode, QualityViolation};
5
+
6
+ use super::{applies, push, push_with_limit};
7
+
8
+ const DUMPING_GROUND_NAMES: &[&str] = &["utils", "helpers", "common", "shared", "misc", "lib"];
9
+
10
+ pub(super) fn dumping_ground_directories(
11
+ model: &RepositoryStructureModel,
12
+ mode: QualityMode,
13
+ violations: &mut Vec<QualityViolation>,
14
+ ) {
15
+ for path in model.paths.iter().filter(|path| applies(path, mode)) {
16
+ if path.explanation.role != "source" || !is_dumping_ground_path(path) {
17
+ continue;
18
+ }
19
+ push(
20
+ "dumping-ground-directory",
21
+ &path.explanation.path,
22
+ format!(
23
+ "{} adds source logic under a generic dumping-ground directory; prefer a named module directory.",
24
+ path.explanation.path
25
+ ),
26
+ related_module_paths(model, path),
27
+ violations,
28
+ );
29
+ }
30
+ }
31
+
32
+ pub(super) fn directory_size(
33
+ model: &RepositoryStructureModel,
34
+ mode: QualityMode,
35
+ violations: &mut Vec<QualityViolation>,
36
+ ) {
37
+ for directory in &model.directories {
38
+ if directory.file_count <= model.config.limits.max_directory_files {
39
+ continue;
40
+ }
41
+ if mode == QualityMode::Changed && directory.direct_changed_paths.is_empty() {
42
+ continue;
43
+ }
44
+ push_with_limit(
45
+ "directory-size",
46
+ &directory.path,
47
+ format!(
48
+ "{} contains {} direct files, exceeding the configured limit of {}.",
49
+ directory.path, directory.file_count, model.config.limits.max_directory_files
50
+ ),
51
+ directory.file_count as f64,
52
+ model.config.limits.max_directory_files as f64,
53
+ directory.direct_changed_paths.clone(),
54
+ violations,
55
+ );
56
+ }
57
+ }
58
+
59
+ pub(super) fn path_depth(
60
+ model: &RepositoryStructureModel,
61
+ mode: QualityMode,
62
+ violations: &mut Vec<QualityViolation>,
63
+ ) {
64
+ for path in model.paths.iter().filter(|path| applies(path, mode)) {
65
+ let depth = path.segments.len();
66
+ if depth > model.config.limits.max_path_depth {
67
+ push_with_limit(
68
+ "path-depth",
69
+ &path.explanation.path,
70
+ format!(
71
+ "{} has path depth {}, exceeding the configured limit of {}.",
72
+ path.explanation.path, depth, model.config.limits.max_path_depth
73
+ ),
74
+ depth as f64,
75
+ model.config.limits.max_path_depth as f64,
76
+ vec![],
77
+ violations,
78
+ );
79
+ }
80
+ }
81
+ }
82
+
83
+ pub(super) fn case_collisions(
84
+ model: &RepositoryStructureModel,
85
+ mode: QualityMode,
86
+ violations: &mut Vec<QualityViolation>,
87
+ ) {
88
+ let mut groups: BTreeMap<String, Vec<String>> = BTreeMap::new();
89
+ for path in &model.paths {
90
+ groups
91
+ .entry(path.explanation.path.to_ascii_lowercase())
92
+ .or_default()
93
+ .push(path.explanation.path.clone());
94
+ }
95
+ for group in groups.values().filter(|group| group.len() > 1) {
96
+ let changed_group = group.iter().any(|path| {
97
+ model.paths.iter().any(|candidate| {
98
+ candidate.explanation.path == *path && candidate.explanation.changed
99
+ })
100
+ });
101
+ if mode == QualityMode::Changed && !changed_group {
102
+ continue;
103
+ }
104
+ for path in group {
105
+ push(
106
+ "case-collision",
107
+ path,
108
+ format!(
109
+ "{} collides with another path on case-insensitive filesystems.",
110
+ path
111
+ ),
112
+ group
113
+ .iter()
114
+ .filter(|other| *other != path)
115
+ .cloned()
116
+ .collect(),
117
+ violations,
118
+ );
119
+ }
120
+ }
121
+ }
122
+
123
+ fn is_dumping_ground_path(path: &StructurePath) -> bool {
124
+ path.segments.iter().enumerate().any(|(index, segment)| {
125
+ index > 0
126
+ && DUMPING_GROUND_NAMES
127
+ .iter()
128
+ .any(|name| segment.eq_ignore_ascii_case(name))
129
+ })
130
+ }
131
+
132
+ fn related_module_paths(model: &RepositoryStructureModel, path: &StructurePath) -> Vec<String> {
133
+ let Some(module) = &path.explanation.module else {
134
+ return Vec::new();
135
+ };
136
+ model
137
+ .paths
138
+ .iter()
139
+ .filter(|candidate| candidate.explanation.module.as_ref() == Some(module))
140
+ .map(|candidate| candidate.explanation.path.clone())
141
+ .filter(|candidate| candidate != &path.explanation.path)
142
+ .take(10)
143
+ .collect()
144
+ }
@@ -0,0 +1,63 @@
1
+ use std::collections::BTreeSet;
2
+
3
+ use crate::quality::structure::model::{RepositoryStructureModel, StructurePath};
4
+ use crate::quality::types::{QualityMode, QualityViolation};
5
+
6
+ use super::{applies, push};
7
+
8
+ pub(super) fn test_source_pairing(
9
+ model: &RepositoryStructureModel,
10
+ mode: QualityMode,
11
+ violations: &mut Vec<QualityViolation>,
12
+ ) {
13
+ for source in model.paths.iter().filter(|path| {
14
+ path.explanation.role == "source" && applies(path, mode) && !is_entrypoint(path)
15
+ }) {
16
+ if has_related_test(model, source) {
17
+ continue;
18
+ }
19
+ push(
20
+ "test-source-pairing",
21
+ &source.explanation.path,
22
+ format!(
23
+ "{} changed without a nearby or module-matched test file.",
24
+ source.explanation.path
25
+ ),
26
+ vec![],
27
+ violations,
28
+ );
29
+ }
30
+ }
31
+
32
+ fn has_related_test(model: &RepositoryStructureModel, source: &StructurePath) -> bool {
33
+ let tokens = source_tokens(source);
34
+ model.paths.iter().any(|candidate| {
35
+ candidate.explanation.role == "test"
36
+ && tokens.iter().any(|token| {
37
+ candidate
38
+ .explanation
39
+ .path
40
+ .to_ascii_lowercase()
41
+ .contains(token)
42
+ })
43
+ })
44
+ }
45
+
46
+ fn source_tokens(source: &StructurePath) -> BTreeSet<String> {
47
+ source
48
+ .segments
49
+ .iter()
50
+ .flat_map(|segment| segment.split(['.', '-', '_']))
51
+ .map(str::to_ascii_lowercase)
52
+ .filter(|token| token.len() > 2 && token != "src" && token != "mod")
53
+ .collect()
54
+ }
55
+
56
+ fn is_entrypoint(path: &StructurePath) -> bool {
57
+ path.segments.last().is_some_and(|file| {
58
+ matches!(
59
+ file.as_str(),
60
+ "main.rs" | "main.go" | "index.js" | "index.ts"
61
+ )
62
+ })
63
+ }
@@ -0,0 +1,124 @@
1
+ mod basic;
2
+ mod directory;
3
+ mod pairing;
4
+
5
+ use super::model::{RepositoryStructureModel, StructurePath};
6
+ use crate::quality::scanner::stable_fingerprint;
7
+ use crate::quality::types::{QualityMode, QualityViolation};
8
+
9
+ pub fn run_structure_checks(
10
+ model: &RepositoryStructureModel,
11
+ mode: QualityMode,
12
+ ) -> Vec<QualityViolation> {
13
+ let mut violations = Vec::new();
14
+ if check_enabled(model, "root-file-sprawl") {
15
+ basic::root_file_sprawl(model, mode, &mut violations);
16
+ }
17
+ if check_enabled(model, "misplaced-file-role") {
18
+ basic::misplaced_files(model, mode, &mut violations);
19
+ }
20
+ if check_enabled(model, "directory-role-mixing") {
21
+ basic::directory_role_mixing(model, mode, &mut violations);
22
+ }
23
+ if check_enabled(model, "dumping-ground-directory") {
24
+ directory::dumping_ground_directories(model, mode, &mut violations);
25
+ }
26
+ if check_enabled(model, "directory-size") {
27
+ directory::directory_size(model, mode, &mut violations);
28
+ }
29
+ if check_enabled(model, "path-depth") {
30
+ directory::path_depth(model, mode, &mut violations);
31
+ }
32
+ if check_enabled(model, "case-collision") {
33
+ directory::case_collisions(model, mode, &mut violations);
34
+ }
35
+ if check_enabled(model, "test-source-pairing") {
36
+ pairing::test_source_pairing(model, mode, &mut violations);
37
+ }
38
+ violations.sort_by(|left, right| {
39
+ left.path
40
+ .cmp(&right.path)
41
+ .then(left.check_id.cmp(&right.check_id))
42
+ .then(left.fingerprint.cmp(&right.fingerprint))
43
+ });
44
+ violations
45
+ }
46
+
47
+ fn check_enabled(model: &RepositoryStructureModel, check_id: &str) -> bool {
48
+ !model
49
+ .config
50
+ .disabled_checks
51
+ .iter()
52
+ .any(|disabled| disabled == check_id)
53
+ }
54
+
55
+ fn applies(path: &StructurePath, mode: QualityMode) -> bool {
56
+ mode == QualityMode::Report || path.explanation.changed
57
+ }
58
+
59
+ fn push(
60
+ check_id: &str,
61
+ path: &str,
62
+ message: String,
63
+ related_paths: Vec<String>,
64
+ violations: &mut Vec<QualityViolation>,
65
+ ) {
66
+ push_with_optional_limit(
67
+ check_id,
68
+ path,
69
+ message,
70
+ None,
71
+ None,
72
+ related_paths,
73
+ violations,
74
+ );
75
+ }
76
+
77
+ fn push_with_limit(
78
+ check_id: &str,
79
+ path: &str,
80
+ message: String,
81
+ value: f64,
82
+ limit: f64,
83
+ related_paths: Vec<String>,
84
+ violations: &mut Vec<QualityViolation>,
85
+ ) {
86
+ push_with_optional_limit(
87
+ check_id,
88
+ path,
89
+ message,
90
+ Some(value),
91
+ Some(limit),
92
+ related_paths,
93
+ violations,
94
+ );
95
+ }
96
+
97
+ fn push_with_optional_limit(
98
+ check_id: &str,
99
+ path: &str,
100
+ message: String,
101
+ value: Option<f64>,
102
+ limit: Option<f64>,
103
+ related_paths: Vec<String>,
104
+ violations: &mut Vec<QualityViolation>,
105
+ ) {
106
+ if violations
107
+ .iter()
108
+ .any(|existing| existing.check_id == check_id && existing.path == path)
109
+ {
110
+ return;
111
+ }
112
+ violations.push(QualityViolation {
113
+ check_id: check_id.to_string(),
114
+ severity: "blocking".to_string(),
115
+ path: path.to_string(),
116
+ line: None,
117
+ message: message.clone(),
118
+ value,
119
+ limit,
120
+ fingerprint: stable_fingerprint(&[check_id, path, &message]),
121
+ related_paths,
122
+ baseline: false,
123
+ });
124
+ }
@@ -0,0 +1,188 @@
1
+ use crate::paths;
2
+
3
+ use crate::quality::structure::model::RepositoryStructureConfig;
4
+
5
+ pub fn role_for(path: &str, segments: &[String], config: &RepositoryStructureConfig) -> String {
6
+ let lower = path.to_ascii_lowercase();
7
+ if has_segment(segments, &["node_modules", "vendor", "third_party"]) {
8
+ return "dependency/vendor".to_string();
9
+ }
10
+ if paths::matches_any(path, &config.generated_roots)
11
+ || has_segment(segments, &["generated", "__generated__", "codegen"])
12
+ {
13
+ return "generated".to_string();
14
+ }
15
+ if paths::matches_any(path, &config.artifact_roots) || is_artifact_path(&lower) {
16
+ return "artifact".to_string();
17
+ }
18
+ if paths::matches_any(path, &config.test_roots) || is_test_path(&lower, segments) {
19
+ return "test".to_string();
20
+ }
21
+ if paths::matches_any(path, &config.docs_roots) || is_docs_path(&lower, segments) {
22
+ return "docs".to_string();
23
+ }
24
+ if is_config_path(path, &lower, segments, config) {
25
+ return "config".to_string();
26
+ }
27
+ if is_script_path(&lower, segments) {
28
+ return "script".to_string();
29
+ }
30
+ if paths::matches_any(path, &config.source_roots) || language_for(path).is_some() {
31
+ return "source".to_string();
32
+ }
33
+ "unknown".to_string()
34
+ }
35
+
36
+ pub fn module_for(
37
+ path: &str,
38
+ segments: &[String],
39
+ config: &RepositoryStructureConfig,
40
+ ) -> Option<String> {
41
+ for root in &config.module_roots {
42
+ if !paths::matches_any(path, &[root.clone()]) {
43
+ continue;
44
+ }
45
+ if let Some(module) = module_from_wildcard_root(segments, root) {
46
+ return Some(module);
47
+ }
48
+ let root_prefix = root.trim_end_matches("/**");
49
+ let after_root = path.strip_prefix(root_prefix).unwrap_or(path);
50
+ let candidate = after_root.trim_start_matches('/').split('/').next()?;
51
+ if !candidate.is_empty() && candidate.contains('.') {
52
+ return stem(candidate);
53
+ }
54
+ if !candidate.is_empty() {
55
+ return Some(candidate.to_string());
56
+ }
57
+ }
58
+ if segments.len() >= 3 && (segments[0] == "packages" || segments[0] == "crates") {
59
+ return Some(segments[1].clone());
60
+ }
61
+ segments
62
+ .iter()
63
+ .rev()
64
+ .nth(1)
65
+ .filter(|segment| !is_role_segment(segment))
66
+ .cloned()
67
+ }
68
+
69
+ fn module_from_wildcard_root(segments: &[String], root: &str) -> Option<String> {
70
+ let root_segments = root.trim_end_matches("/**").split('/').collect::<Vec<_>>();
71
+ for (index, segment) in root_segments.iter().enumerate() {
72
+ if *segment == "*" {
73
+ return segments
74
+ .get(index)
75
+ .filter(|segment| !segment.is_empty())
76
+ .cloned();
77
+ }
78
+ }
79
+
80
+ if root_segments.first() == Some(&"**") {
81
+ let anchor = root_segments
82
+ .iter()
83
+ .skip(1)
84
+ .find(|segment| !segment.contains('*'))?;
85
+ if let Some(src_index) = segments.iter().position(|segment| segment == anchor) {
86
+ return src_index
87
+ .checked_sub(1)
88
+ .and_then(|module_index| segments.get(module_index))
89
+ .filter(|segment| !segment.is_empty())
90
+ .cloned();
91
+ }
92
+ }
93
+
94
+ None
95
+ }
96
+
97
+ pub fn layer_for(path: &str, role: &str, config: &RepositoryStructureConfig) -> String {
98
+ config
99
+ .layer_rules
100
+ .iter()
101
+ .find(|rule| paths::matches_any(path, &rule.paths))
102
+ .map(|rule| rule.layer.clone())
103
+ .unwrap_or_else(|| role.to_string())
104
+ }
105
+
106
+ pub fn language_for(path: &str) -> Option<String> {
107
+ let lower = path.to_ascii_lowercase();
108
+ let language = if lower.ends_with(".rs") {
109
+ "rust"
110
+ } else if lower.ends_with(".ts") || lower.ends_with(".tsx") {
111
+ "typescript"
112
+ } else if lower.ends_with(".js")
113
+ || lower.ends_with(".jsx")
114
+ || lower.ends_with(".mjs")
115
+ || lower.ends_with(".cjs")
116
+ {
117
+ "javascript"
118
+ } else if lower.ends_with(".py") {
119
+ "python"
120
+ } else if lower.ends_with(".go") {
121
+ "go"
122
+ } else if lower.ends_with(".swift") {
123
+ "swift"
124
+ } else {
125
+ return None;
126
+ };
127
+ Some(language.to_string())
128
+ }
129
+
130
+ fn is_test_path(lower: &str, segments: &[String]) -> bool {
131
+ lower.contains(".test.")
132
+ || lower.contains(".spec.")
133
+ || lower.ends_with("_test.rs")
134
+ || has_segment(segments, &["test", "tests", "__tests__"])
135
+ }
136
+
137
+ fn is_docs_path(lower: &str, segments: &[String]) -> bool {
138
+ lower.ends_with(".md")
139
+ || lower.ends_with(".mdx")
140
+ || lower.ends_with(".rst")
141
+ || has_segment(segments, &["docs", "doc"])
142
+ }
143
+
144
+ fn is_config_path(
145
+ path: &str,
146
+ lower: &str,
147
+ segments: &[String],
148
+ config: &RepositoryStructureConfig,
149
+ ) -> bool {
150
+ paths::matches_any(path, &config.allowed_root_files)
151
+ || segments.len() == 1 && (lower.starts_with('.') || lower.ends_with(".toml"))
152
+ || lower.ends_with(".json")
153
+ || lower.ends_with(".yaml")
154
+ || lower.ends_with(".yml")
155
+ }
156
+
157
+ fn is_script_path(lower: &str, segments: &[String]) -> bool {
158
+ has_segment(segments, &["scripts", "bin"]) || lower.ends_with(".sh") || lower.ends_with(".zsh")
159
+ }
160
+
161
+ fn is_artifact_path(lower: &str) -> bool {
162
+ lower.ends_with(".tmp")
163
+ || lower.ends_with(".log")
164
+ || lower.ends_with(".map")
165
+ || lower.ends_with(".min.js")
166
+ }
167
+
168
+ fn has_segment(segments: &[String], expected: &[&str]) -> bool {
169
+ segments.iter().any(|segment| {
170
+ let lower = segment.to_ascii_lowercase();
171
+ expected.iter().any(|value| *value == lower)
172
+ })
173
+ }
174
+
175
+ fn is_role_segment(segment: &str) -> bool {
176
+ matches!(
177
+ segment,
178
+ "src" | "test" | "tests" | "docs" | "doc" | "scripts" | "generated"
179
+ )
180
+ }
181
+
182
+ fn stem(file_name: &str) -> Option<String> {
183
+ file_name
184
+ .split('.')
185
+ .next()
186
+ .filter(|value| !value.is_empty())
187
+ .map(ToString::to_string)
188
+ }
@@ -0,0 +1,94 @@
1
+ mod roles;
2
+
3
+ use std::collections::{BTreeMap, BTreeSet, HashSet};
4
+
5
+ use crate::paths;
6
+
7
+ use super::model::{
8
+ RepositoryStructureConfig, RepositoryStructureModel, StructureDirectory, StructurePath,
9
+ StructurePathExplanation,
10
+ };
11
+ use roles::{language_for, layer_for, module_for, role_for};
12
+
13
+ pub fn build_structure_model(
14
+ config: RepositoryStructureConfig,
15
+ repo_paths: &[String],
16
+ changed_paths: &[String],
17
+ baseline_fingerprints: &HashSet<String>,
18
+ ) -> RepositoryStructureModel {
19
+ let changed = changed_paths.iter().cloned().collect::<HashSet<_>>();
20
+ let mut paths = repo_paths
21
+ .iter()
22
+ .filter(|path| !paths::matches_any(path, &config.ignored_paths))
23
+ .map(|path| classify_path(path, &config, changed.contains(path), baseline_fingerprints))
24
+ .collect::<Vec<_>>();
25
+ paths.sort_by(|left, right| left.explanation.path.cmp(&right.explanation.path));
26
+ let directories = directories_for(&paths);
27
+ RepositoryStructureModel {
28
+ config,
29
+ paths,
30
+ directories,
31
+ }
32
+ }
33
+
34
+ fn classify_path(
35
+ path: &str,
36
+ config: &RepositoryStructureConfig,
37
+ changed: bool,
38
+ _baseline_fingerprints: &HashSet<String>,
39
+ ) -> StructurePath {
40
+ let segments = path.split('/').map(ToString::to_string).collect::<Vec<_>>();
41
+ let directory = directory_of(path);
42
+ let role = role_for(path, &segments, config);
43
+ let module = module_for(path, &segments, config);
44
+ let language = language_for(path);
45
+ let layer = layer_for(path, &role, config);
46
+ let generated = role == "generated";
47
+ StructurePath {
48
+ explanation: StructurePathExplanation {
49
+ path: path.to_string(),
50
+ role,
51
+ module,
52
+ layer,
53
+ language,
54
+ generated,
55
+ debt: false,
56
+ changed,
57
+ directory,
58
+ },
59
+ segments,
60
+ }
61
+ }
62
+
63
+ fn directories_for(paths: &[StructurePath]) -> Vec<StructureDirectory> {
64
+ let mut dirs: BTreeMap<String, StructureDirectory> = BTreeMap::new();
65
+ for path in paths {
66
+ let dir = path.explanation.directory.clone();
67
+ let entry = dirs
68
+ .entry(dir.clone())
69
+ .or_insert_with(|| StructureDirectory {
70
+ path: dir,
71
+ roles: BTreeSet::new(),
72
+ modules: BTreeSet::new(),
73
+ file_count: 0,
74
+ direct_changed_paths: Vec::new(),
75
+ });
76
+ entry.file_count += 1;
77
+ entry.roles.insert(path.explanation.role.clone());
78
+ if let Some(module) = &path.explanation.module {
79
+ entry.modules.insert(module.clone());
80
+ }
81
+ if path.explanation.changed {
82
+ entry
83
+ .direct_changed_paths
84
+ .push(path.explanation.path.clone());
85
+ }
86
+ }
87
+ dirs.into_values().collect()
88
+ }
89
+
90
+ fn directory_of(path: &str) -> String {
91
+ path.rsplit_once('/')
92
+ .map(|(directory, _)| directory.to_string())
93
+ .unwrap_or_else(|| ".".to_string())
94
+ }
@@ -0,0 +1,89 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+
4
+ use crate::models::NaomeError;
5
+
6
+ use super::adapters::{
7
+ apply_structure_adapters, detected_structure_adapter_ids, validate_structure_adapter_ids,
8
+ };
9
+ use crate::quality::config_support::validate_ready_schema;
10
+
11
+ use super::model::RepositoryStructureConfig;
12
+
13
+ const CONFIG_RELATIVE_PATH: &str = ".naome/repository-structure.json";
14
+
15
+ pub fn structure_config_relative_path() -> &'static str {
16
+ CONFIG_RELATIVE_PATH
17
+ }
18
+
19
+ pub fn read_structure_config(
20
+ root: &Path,
21
+ paths: &[String],
22
+ ) -> Result<RepositoryStructureConfig, NaomeError> {
23
+ let path = root.join(CONFIG_RELATIVE_PATH);
24
+ let config = if path.is_file() {
25
+ serde_json::from_str(&fs::read_to_string(path)?)?
26
+ } else {
27
+ generated_structure_config(paths)
28
+ };
29
+ validate_structure_config(&config)?;
30
+ apply_structure_adapters(config)
31
+ }
32
+
33
+ pub fn write_default_structure_config_if_missing(
34
+ root: &Path,
35
+ paths: &[String],
36
+ ) -> Result<bool, NaomeError> {
37
+ let path = root.join(CONFIG_RELATIVE_PATH);
38
+ if path.exists() {
39
+ return Ok(false);
40
+ }
41
+ if let Some(parent) = path.parent() {
42
+ fs::create_dir_all(parent)?;
43
+ }
44
+ let content = serde_json::to_string_pretty(&generated_structure_config(paths))?;
45
+ fs::write(path, format!("{content}\n"))?;
46
+ Ok(true)
47
+ }
48
+
49
+ fn generated_structure_config(paths: &[String]) -> RepositoryStructureConfig {
50
+ let mut config = RepositoryStructureConfig::default();
51
+ config.enabled_adapters = detected_structure_adapter_ids(paths);
52
+ config
53
+ }
54
+
55
+ fn validate_structure_config(config: &RepositoryStructureConfig) -> Result<(), NaomeError> {
56
+ validate_ready_schema(
57
+ CONFIG_RELATIVE_PATH,
58
+ &config.schema,
59
+ "naome.repository-structure.v1",
60
+ config.version,
61
+ &config.status,
62
+ )?;
63
+ validate_structure_adapter_ids(&config.enabled_adapters)?;
64
+ if config.limits.max_directory_files == 0 {
65
+ return Err(NaomeError::new(
66
+ ".naome/repository-structure.json maxDirectoryFiles must be greater than 0.",
67
+ ));
68
+ }
69
+ if config.limits.max_path_depth == 0 {
70
+ return Err(NaomeError::new(
71
+ ".naome/repository-structure.json maxPathDepth must be greater than 0.",
72
+ ));
73
+ }
74
+ for rule in &config.directory_role_rules {
75
+ if rule.id.trim().is_empty() || rule.paths.is_empty() {
76
+ return Err(NaomeError::new(
77
+ ".naome/repository-structure.json directoryRoleRules require id and paths.",
78
+ ));
79
+ }
80
+ }
81
+ for rule in &config.layer_rules {
82
+ if rule.id.trim().is_empty() || rule.paths.is_empty() || rule.layer.trim().is_empty() {
83
+ return Err(NaomeError::new(
84
+ ".naome/repository-structure.json layerRules require id, paths, and layer.",
85
+ ));
86
+ }
87
+ }
88
+ Ok(())
89
+ }