@lamentis/naome 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/Cargo.lock +2 -2
  2. package/Cargo.toml +1 -1
  3. package/LICENSE +180 -21
  4. package/README.md +49 -6
  5. package/bin/naome-node.js +44 -4
  6. package/bin/naome.js +54 -16
  7. package/crates/naome-cli/Cargo.toml +1 -1
  8. package/crates/naome-cli/src/check_commands.rs +135 -0
  9. package/crates/naome-cli/src/cli_args.rs +5 -0
  10. package/crates/naome-cli/src/dispatcher.rs +36 -0
  11. package/crates/naome-cli/src/install_bridge.rs +83 -0
  12. package/crates/naome-cli/src/main.rs +57 -341
  13. package/crates/naome-cli/src/prompt_commands.rs +68 -0
  14. package/crates/naome-cli/src/quality_commands.rs +141 -0
  15. package/crates/naome-cli/src/simple_commands.rs +53 -0
  16. package/crates/naome-cli/src/workflow_commands.rs +153 -0
  17. package/crates/naome-core/Cargo.toml +1 -1
  18. package/crates/naome-core/src/harness_health/integrity.rs +96 -0
  19. package/crates/naome-core/src/harness_health.rs +14 -126
  20. package/crates/naome-core/src/install_plan.rs +3 -0
  21. package/crates/naome-core/src/intent/classifier.rs +171 -0
  22. package/crates/naome-core/src/intent/envelope.rs +108 -0
  23. package/crates/naome-core/src/intent/legacy.rs +138 -0
  24. package/crates/naome-core/src/intent/legacy_response.rs +76 -0
  25. package/crates/naome-core/src/intent/model.rs +71 -0
  26. package/crates/naome-core/src/intent/patterns.rs +170 -0
  27. package/crates/naome-core/src/intent/resolver.rs +162 -0
  28. package/crates/naome-core/src/intent/resolver_active.rs +17 -0
  29. package/crates/naome-core/src/intent/resolver_baseline.rs +55 -0
  30. package/crates/naome-core/src/intent/resolver_catalog.rs +167 -0
  31. package/crates/naome-core/src/intent/resolver_policy.rs +72 -0
  32. package/crates/naome-core/src/intent/resolver_shared.rs +55 -0
  33. package/crates/naome-core/src/intent/risk.rs +40 -0
  34. package/crates/naome-core/src/intent/segment.rs +170 -0
  35. package/crates/naome-core/src/intent.rs +64 -879
  36. package/crates/naome-core/src/journal.rs +9 -20
  37. package/crates/naome-core/src/lib.rs +13 -0
  38. package/crates/naome-core/src/quality/adapters.rs +178 -0
  39. package/crates/naome-core/src/quality/baseline.rs +75 -0
  40. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +175 -0
  41. package/crates/naome-core/src/quality/checks/near_duplicates.rs +130 -0
  42. package/crates/naome-core/src/quality/checks.rs +228 -0
  43. package/crates/naome-core/src/quality/cleanup.rs +72 -0
  44. package/crates/naome-core/src/quality/config.rs +109 -0
  45. package/crates/naome-core/src/quality/mod.rs +90 -0
  46. package/crates/naome-core/src/quality/scanner/repo_paths.rs +103 -0
  47. package/crates/naome-core/src/quality/scanner.rs +367 -0
  48. package/crates/naome-core/src/quality/types.rs +289 -0
  49. package/crates/naome-core/src/route.rs +292 -17
  50. package/crates/naome-core/src/task_state/admission.rs +63 -0
  51. package/crates/naome-core/src/task_state/admission_proof.rs +72 -0
  52. package/crates/naome-core/src/task_state/api.rs +130 -0
  53. package/crates/naome-core/src/task_state/commit_gate.rs +138 -0
  54. package/crates/naome-core/src/task_state/compact_proof.rs +160 -0
  55. package/crates/naome-core/src/task_state/completed_refresh.rs +89 -0
  56. package/crates/naome-core/src/task_state/completion.rs +72 -0
  57. package/crates/naome-core/src/task_state/deleted_paths.rs +47 -0
  58. package/crates/naome-core/src/task_state/diff.rs +95 -0
  59. package/crates/naome-core/src/task_state/evidence.rs +154 -0
  60. package/crates/naome-core/src/task_state/git_io.rs +86 -0
  61. package/crates/naome-core/src/task_state/git_parse.rs +86 -0
  62. package/crates/naome-core/src/task_state/git_refs.rs +37 -0
  63. package/crates/naome-core/src/task_state/human_review_state.rs +31 -0
  64. package/crates/naome-core/src/task_state/mod.rs +38 -0
  65. package/crates/naome-core/src/task_state/process_guard.rs +40 -0
  66. package/crates/naome-core/src/task_state/progress.rs +123 -0
  67. package/crates/naome-core/src/task_state/proof.rs +139 -0
  68. package/crates/naome-core/src/task_state/proof_entry.rs +66 -0
  69. package/crates/naome-core/src/task_state/proof_model.rs +70 -0
  70. package/crates/naome-core/src/task_state/proof_sources.rs +76 -0
  71. package/crates/naome-core/src/task_state/push_gate.rs +49 -0
  72. package/crates/naome-core/src/task_state/reconcile.rs +7 -0
  73. package/crates/naome-core/src/task_state/repair.rs +168 -0
  74. package/crates/naome-core/src/task_state/shape.rs +117 -0
  75. package/crates/naome-core/src/task_state/task_diff_api.rs +170 -0
  76. package/crates/naome-core/src/task_state/task_records.rs +131 -0
  77. package/crates/naome-core/src/task_state/task_references.rs +126 -0
  78. package/crates/naome-core/src/task_state/types.rs +87 -0
  79. package/crates/naome-core/src/task_state/util.rs +137 -0
  80. package/crates/naome-core/src/verification/render.rs +122 -0
  81. package/crates/naome-core/src/verification.rs +176 -58
  82. package/crates/naome-core/src/verification_contract.rs +49 -21
  83. package/crates/naome-core/src/workflow/integrity.rs +123 -0
  84. package/crates/naome-core/src/workflow/integrity_normalize.rs +7 -0
  85. package/crates/naome-core/src/workflow/integrity_support.rs +110 -0
  86. package/crates/naome-core/src/workflow/mod.rs +18 -0
  87. package/crates/naome-core/src/workflow/mutation.rs +68 -0
  88. package/crates/naome-core/src/workflow/output.rs +111 -0
  89. package/crates/naome-core/src/workflow/phase_inference.rs +73 -0
  90. package/crates/naome-core/src/workflow/phases.rs +169 -0
  91. package/crates/naome-core/src/workflow/policy.rs +156 -0
  92. package/crates/naome-core/src/workflow/processes.rs +91 -0
  93. package/crates/naome-core/src/workflow/types.rs +42 -0
  94. package/crates/naome-core/tests/harness_health.rs +3 -0
  95. package/crates/naome-core/tests/intent.rs +97 -792
  96. package/crates/naome-core/tests/intent_support/mod.rs +133 -0
  97. package/crates/naome-core/tests/intent_v2.rs +90 -0
  98. package/crates/naome-core/tests/quality.rs +425 -0
  99. package/crates/naome-core/tests/route.rs +221 -4
  100. package/crates/naome-core/tests/task_state.rs +3 -0
  101. package/crates/naome-core/tests/task_state_compact.rs +110 -0
  102. package/crates/naome-core/tests/task_state_compact_support/mod.rs +5 -0
  103. package/crates/naome-core/tests/task_state_compact_support/repo.rs +130 -0
  104. package/crates/naome-core/tests/task_state_compact_support/states.rs +151 -0
  105. package/crates/naome-core/tests/workflow_integrity.rs +85 -0
  106. package/crates/naome-core/tests/workflow_policy.rs +139 -0
  107. package/crates/naome-core/tests/workflow_support/mod.rs +194 -0
  108. package/native/darwin-arm64/naome +0 -0
  109. package/native/linux-x64/naome +0 -0
  110. package/package.json +2 -2
  111. package/templates/naome-root/.naome/bin/check-harness-health.js +66 -85
  112. package/templates/naome-root/.naome/bin/check-task-state.js +9 -10
  113. package/templates/naome-root/.naome/bin/naome.js +34 -63
  114. package/templates/naome-root/.naome/manifest.json +20 -18
  115. package/templates/naome-root/.naome/repository-quality-baseline.json +5 -0
  116. package/templates/naome-root/.naome/repository-quality.json +24 -0
  117. package/templates/naome-root/.naome/task-contract.schema.json +93 -11
  118. package/templates/naome-root/.naome/upgrade-state.json +1 -1
  119. package/templates/naome-root/.naome/verification.json +37 -0
  120. package/templates/naome-root/AGENTS.md +3 -0
  121. package/templates/naome-root/docs/naome/agent-workflow.md +25 -12
  122. package/templates/naome-root/docs/naome/execution.md +25 -21
  123. package/templates/naome-root/docs/naome/index.md +4 -3
  124. package/templates/naome-root/docs/naome/repository-quality.md +43 -0
  125. package/templates/naome-root/docs/naome/testing.md +12 -0
  126. package/crates/naome-core/src/task_state.rs +0 -2210
@@ -0,0 +1,228 @@
1
+ mod duplicate_blocks;
2
+ mod near_duplicates;
3
+
4
+ use std::collections::HashSet;
5
+
6
+ use super::scanner::{stable_fingerprint, QualityContext};
7
+ use super::types::{QualityMode, QualityViolation};
8
+ use duplicate_blocks::DuplicateBlockCheck;
9
+ use near_duplicates::NearDuplicateFunctionCheck;
10
+
11
+ pub fn run_quality_checks(context: &QualityContext) -> Vec<QualityViolation> {
12
+ let checks = registry();
13
+ let disabled = context
14
+ .config
15
+ .disabled_checks
16
+ .iter()
17
+ .map(String::as_str)
18
+ .collect::<HashSet<_>>();
19
+ let mut violations = Vec::new();
20
+
21
+ for check in checks {
22
+ if !disabled.contains(check.id()) {
23
+ check.evaluate(context, &mut violations);
24
+ }
25
+ }
26
+
27
+ violations.sort_by(|left, right| {
28
+ left.path
29
+ .cmp(&right.path)
30
+ .then(left.check_id.cmp(&right.check_id))
31
+ .then(left.line.cmp(&right.line))
32
+ .then(left.fingerprint.cmp(&right.fingerprint))
33
+ });
34
+ violations
35
+ }
36
+
37
+ pub(super) trait QualityCheck {
38
+ fn id(&self) -> &'static str;
39
+ fn evaluate(&self, context: &QualityContext, violations: &mut Vec<QualityViolation>);
40
+ }
41
+
42
+ fn registry() -> Vec<Box<dyn QualityCheck>> {
43
+ vec![
44
+ Box::new(FileLengthCheck),
45
+ Box::new(DiffGrowthCheck),
46
+ Box::new(FunctionLengthCheck),
47
+ Box::new(TopLevelSymbolCountCheck),
48
+ Box::new(DuplicateBlockCheck),
49
+ Box::new(NearDuplicateFunctionCheck),
50
+ ]
51
+ }
52
+
53
+ struct FileLengthCheck;
54
+
55
+ impl QualityCheck for FileLengthCheck {
56
+ fn id(&self) -> &'static str {
57
+ "file-length"
58
+ }
59
+
60
+ fn evaluate(&self, context: &QualityContext, violations: &mut Vec<QualityViolation>) {
61
+ for file in context
62
+ .files
63
+ .iter()
64
+ .filter(|file| context.check_applies_to(self.id(), &file.path))
65
+ {
66
+ let limits = context.limits_for(&file.path);
67
+ let limit =
68
+ if context.mode == QualityMode::Changed && file.added_lines == file.line_count {
69
+ limits.max_new_file_lines
70
+ } else {
71
+ limits.max_file_lines
72
+ };
73
+ if file.line_count > limit {
74
+ violations.push(violation(
75
+ self.id(),
76
+ &file.path,
77
+ None,
78
+ format!(
79
+ "{} has {} lines, exceeding the configured limit of {}.",
80
+ file.path, file.line_count, limit
81
+ ),
82
+ Some(file.line_count as f64),
83
+ Some(limit as f64),
84
+ Vec::new(),
85
+ ));
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ struct DiffGrowthCheck;
92
+
93
+ impl QualityCheck for DiffGrowthCheck {
94
+ fn id(&self) -> &'static str {
95
+ "diff-growth"
96
+ }
97
+
98
+ fn evaluate(&self, context: &QualityContext, violations: &mut Vec<QualityViolation>) {
99
+ if context.mode != QualityMode::Changed {
100
+ return;
101
+ }
102
+ for file in context
103
+ .files
104
+ .iter()
105
+ .filter(|file| context.check_applies_to(self.id(), &file.path))
106
+ {
107
+ let limit = context.limits_for(&file.path).max_diff_added_lines;
108
+ if file.added_lines > limit {
109
+ violations.push(violation(
110
+ self.id(),
111
+ &file.path,
112
+ None,
113
+ format!(
114
+ "{} adds {} lines in this task, exceeding the configured limit of {}.",
115
+ file.path, file.added_lines, limit
116
+ ),
117
+ Some(file.added_lines as f64),
118
+ Some(limit as f64),
119
+ Vec::new(),
120
+ ));
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ struct FunctionLengthCheck;
127
+
128
+ impl QualityCheck for FunctionLengthCheck {
129
+ fn id(&self) -> &'static str {
130
+ "function-length"
131
+ }
132
+
133
+ fn evaluate(&self, context: &QualityContext, violations: &mut Vec<QualityViolation>) {
134
+ for file in context.files.iter().filter(|file| {
135
+ context.check_applies_to(self.id(), &file.path) && is_code_like_path(&file.path)
136
+ }) {
137
+ let limit = context.limits_for(&file.path).max_function_lines;
138
+ for symbol in &file.symbols {
139
+ if symbol.line_count() > limit {
140
+ violations.push(violation(
141
+ self.id(),
142
+ &file.path,
143
+ Some(symbol.start_line),
144
+ format!(
145
+ "{} {} has {} lines, exceeding the configured limit of {}.",
146
+ symbol.kind,
147
+ symbol.name,
148
+ symbol.line_count(),
149
+ limit
150
+ ),
151
+ Some(symbol.line_count() as f64),
152
+ Some(limit as f64),
153
+ Vec::new(),
154
+ ));
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ struct TopLevelSymbolCountCheck;
162
+
163
+ impl QualityCheck for TopLevelSymbolCountCheck {
164
+ fn id(&self) -> &'static str {
165
+ "top-level-symbol-count"
166
+ }
167
+
168
+ fn evaluate(&self, context: &QualityContext, violations: &mut Vec<QualityViolation>) {
169
+ for file in context.files.iter().filter(|file| {
170
+ context.check_applies_to(self.id(), &file.path) && is_code_like_path(&file.path)
171
+ }) {
172
+ let limit = context.limits_for(&file.path).max_top_level_symbols;
173
+ let count = file
174
+ .symbols
175
+ .iter()
176
+ .filter(|symbol| symbol.indent <= 2)
177
+ .count();
178
+ if count > limit {
179
+ violations.push(violation(
180
+ self.id(),
181
+ &file.path,
182
+ None,
183
+ format!(
184
+ "{} defines {} top-level symbols, exceeding the configured limit of {}.",
185
+ file.path, count, limit
186
+ ),
187
+ Some(count as f64),
188
+ Some(limit as f64),
189
+ Vec::new(),
190
+ ));
191
+ }
192
+ }
193
+ }
194
+ }
195
+
196
+ pub(super) fn is_code_like_path(path: &str) -> bool {
197
+ let lower = path.to_ascii_lowercase();
198
+ [
199
+ ".rs", ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".py", ".go", ".swift", ".kt",
200
+ ".java", ".c", ".cc", ".cpp", ".h", ".hpp", ".rb", ".php", ".cs", ".sh", ".bash", ".zsh",
201
+ ]
202
+ .iter()
203
+ .any(|extension| lower.ends_with(extension))
204
+ }
205
+
206
+ pub(super) fn violation(
207
+ check_id: &str,
208
+ path: &str,
209
+ line: Option<usize>,
210
+ message: String,
211
+ value: Option<f64>,
212
+ limit: Option<f64>,
213
+ related_paths: Vec<String>,
214
+ ) -> QualityViolation {
215
+ let line_part = line.map(|value| value.to_string()).unwrap_or_default();
216
+ QualityViolation {
217
+ check_id: check_id.to_string(),
218
+ severity: "blocking".to_string(),
219
+ path: path.to_string(),
220
+ line,
221
+ message: message.clone(),
222
+ value,
223
+ limit,
224
+ fingerprint: stable_fingerprint(&[check_id, path, &line_part, &message]),
225
+ related_paths,
226
+ baseline: false,
227
+ }
228
+ }
@@ -0,0 +1,72 @@
1
+ use std::collections::{BTreeMap, BTreeSet};
2
+
3
+ use super::types::{QualityCleanupPlan, QualityCleanupRoute, QualityCleanupTask, QualityViolation};
4
+
5
+ pub fn cleanup_plan_from_violations(violations: &[QualityViolation]) -> QualityCleanupPlan {
6
+ let mut grouped: BTreeMap<String, Vec<&QualityViolation>> = BTreeMap::new();
7
+ for violation in violations {
8
+ grouped
9
+ .entry(violation.path.clone())
10
+ .or_default()
11
+ .push(violation);
12
+ }
13
+
14
+ let mut tasks = grouped
15
+ .into_iter()
16
+ .map(|(path, violations)| {
17
+ let check_ids = violations
18
+ .iter()
19
+ .map(|violation| violation.check_id.clone())
20
+ .collect::<BTreeSet<_>>()
21
+ .into_iter()
22
+ .collect::<Vec<_>>();
23
+ QualityCleanupTask {
24
+ path,
25
+ violation_count: violations.len(),
26
+ summary: format!(
27
+ "Resolve {} repository-quality violation(s): {}.",
28
+ violations.len(),
29
+ check_ids.join(", ")
30
+ ),
31
+ check_ids,
32
+ }
33
+ })
34
+ .collect::<Vec<_>>();
35
+ tasks.sort_by(|left, right| {
36
+ right
37
+ .violation_count
38
+ .cmp(&left.violation_count)
39
+ .then(left.path.cmp(&right.path))
40
+ });
41
+
42
+ QualityCleanupPlan {
43
+ schema: "naome.quality-cleanup-plan.v1".to_string(),
44
+ tasks,
45
+ }
46
+ }
47
+
48
+ pub fn cleanup_route_for_path(
49
+ path: &str,
50
+ violations: Vec<QualityViolation>,
51
+ ) -> QualityCleanupRoute {
52
+ let mut related_paths = violations
53
+ .iter()
54
+ .flat_map(|violation| violation.related_paths.clone())
55
+ .collect::<BTreeSet<_>>()
56
+ .into_iter()
57
+ .collect::<Vec<_>>();
58
+ related_paths.retain(|related| related != path);
59
+ QualityCleanupRoute {
60
+ schema: "naome.quality-cleanup-route.v1".to_string(),
61
+ path: path.to_string(),
62
+ violations,
63
+ related_paths,
64
+ agent_instructions: format!(
65
+ "Reduce or split {path} until every repository-quality violation is gone. Prefer extracting reusable helpers/components over copying code. Keep behavior unchanged, add or preserve focused tests, then run naome quality check --changed before task completion."
66
+ ),
67
+ required_checks: vec![
68
+ "naome quality check --changed".to_string(),
69
+ "git diff --check".to_string(),
70
+ ],
71
+ }
72
+ }
@@ -0,0 +1,109 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+
4
+ use crate::models::NaomeError;
5
+
6
+ use super::adapters::{apply_enabled_adapters, detected_adapter_ids, validate_adapter_ids};
7
+ use super::scanner::collect_repo_paths;
8
+ use super::types::RepositoryQualityConfig;
9
+
10
+ const CONFIG_RELATIVE_PATH: &str = ".naome/repository-quality.json";
11
+
12
+ pub fn config_relative_path() -> &'static str {
13
+ CONFIG_RELATIVE_PATH
14
+ }
15
+
16
+ pub fn read_config(root: &Path) -> Result<RepositoryQualityConfig, NaomeError> {
17
+ let path = root.join(CONFIG_RELATIVE_PATH);
18
+ let config = if path.is_file() {
19
+ serde_json::from_str(&fs::read_to_string(path)?)?
20
+ } else {
21
+ generated_config(root)?
22
+ };
23
+ validate_config(&config)?;
24
+ apply_enabled_adapters(config)
25
+ }
26
+
27
+ pub fn write_default_config_if_missing(root: &Path) -> Result<bool, NaomeError> {
28
+ let path = root.join(CONFIG_RELATIVE_PATH);
29
+ if path.exists() {
30
+ return Ok(false);
31
+ }
32
+
33
+ if let Some(parent) = path.parent() {
34
+ fs::create_dir_all(parent)?;
35
+ }
36
+ let content = serde_json::to_string_pretty(&generated_config(root)?)?;
37
+ fs::write(path, format!("{content}\n"))?;
38
+ Ok(true)
39
+ }
40
+
41
+ fn generated_config(root: &Path) -> Result<RepositoryQualityConfig, NaomeError> {
42
+ let paths = collect_repo_paths(root)?;
43
+ let mut config = RepositoryQualityConfig::default();
44
+ config.enabled_adapters = detected_adapter_ids(&paths);
45
+ Ok(config)
46
+ }
47
+
48
+ fn validate_config(config: &RepositoryQualityConfig) -> Result<(), NaomeError> {
49
+ if config.schema != "naome.repository-quality.v1" {
50
+ return Err(NaomeError::new(
51
+ ".naome/repository-quality.json schema must be naome.repository-quality.v1.",
52
+ ));
53
+ }
54
+ if config.version != 1 {
55
+ return Err(NaomeError::new(
56
+ ".naome/repository-quality.json version must be 1.",
57
+ ));
58
+ }
59
+ if config.status != "ready" {
60
+ return Err(NaomeError::new(
61
+ ".naome/repository-quality.json status must be ready.",
62
+ ));
63
+ }
64
+ if config.limits.duplicate_block_lines < 3 {
65
+ return Err(NaomeError::new(
66
+ ".naome/repository-quality.json duplicateBlockLines must be at least 3.",
67
+ ));
68
+ }
69
+ if !(0.5..=1.0).contains(&config.limits.near_duplicate_similarity) {
70
+ return Err(NaomeError::new(
71
+ ".naome/repository-quality.json nearDuplicateSimilarity must be between 0.5 and 1.0.",
72
+ ));
73
+ }
74
+ validate_adapter_ids(&config.enabled_adapters)?;
75
+ for rule in &config.path_rules {
76
+ if rule.id.trim().is_empty() {
77
+ return Err(NaomeError::new(
78
+ ".naome/repository-quality.json pathRules entries must have a non-empty id.",
79
+ ));
80
+ }
81
+ if rule.paths.is_empty() {
82
+ return Err(NaomeError::new(format!(
83
+ ".naome/repository-quality.json pathRules.{} must list at least one path.",
84
+ rule.id
85
+ )));
86
+ }
87
+ if rule
88
+ .limits
89
+ .duplicate_block_lines
90
+ .is_some_and(|value| value < 3)
91
+ {
92
+ return Err(NaomeError::new(format!(
93
+ ".naome/repository-quality.json pathRules.{} duplicateBlockLines must be at least 3.",
94
+ rule.id
95
+ )));
96
+ }
97
+ if rule
98
+ .limits
99
+ .near_duplicate_similarity
100
+ .is_some_and(|value| !(0.5..=1.0).contains(&value))
101
+ {
102
+ return Err(NaomeError::new(format!(
103
+ ".naome/repository-quality.json pathRules.{} nearDuplicateSimilarity must be between 0.5 and 1.0.",
104
+ rule.id
105
+ )));
106
+ }
107
+ }
108
+ Ok(())
109
+ }
@@ -0,0 +1,90 @@
1
+ mod adapters;
2
+ mod baseline;
3
+ mod checks;
4
+ mod cleanup;
5
+ mod config;
6
+ mod scanner;
7
+ mod types;
8
+
9
+ use std::path::Path;
10
+
11
+ use crate::models::NaomeError;
12
+
13
+ pub use cleanup::{cleanup_plan_from_violations, cleanup_route_for_path};
14
+ pub use types::{
15
+ QualityCleanupPlan, QualityCleanupRoute, QualityCleanupTask, QualityInitResult, QualityMode,
16
+ QualityReport, QualitySummary, QualityViolation, RepositoryQualityConfig,
17
+ };
18
+
19
+ use self::baseline::{baseline_relative_path, read_baseline_fingerprints, write_baseline};
20
+ use self::checks::run_quality_checks;
21
+ use self::config::{config_relative_path, read_config, write_default_config_if_missing};
22
+ use self::scanner::scan_repository;
23
+
24
+ pub fn check_repository_quality(
25
+ root: &Path,
26
+ mode: QualityMode,
27
+ ) -> Result<QualityReport, NaomeError> {
28
+ let config = read_config(root)?;
29
+ let context = scan_repository(root, mode, config)?;
30
+ let baseline = read_baseline_fingerprints(root)?;
31
+ let mut violations = run_quality_checks(&context);
32
+ for violation in &mut violations {
33
+ violation.baseline = baseline.contains(&violation.fingerprint);
34
+ }
35
+ let blocking_violation_count = violations.len();
36
+ let baseline_violation_count = violations
37
+ .iter()
38
+ .filter(|violation| violation.baseline)
39
+ .count();
40
+ let ok = blocking_violation_count == 0;
41
+
42
+ Ok(QualityReport {
43
+ schema: "naome.repository-quality-report.v1".to_string(),
44
+ mode: mode.as_str().to_string(),
45
+ ok,
46
+ changed_paths: context.changed_paths.clone(),
47
+ scanned_paths: context.scanned_paths(),
48
+ summary: QualitySummary {
49
+ scanned_files: context.files.len(),
50
+ violation_count: violations.len(),
51
+ baseline_violation_count,
52
+ blocking_violation_count,
53
+ },
54
+ violations,
55
+ })
56
+ }
57
+
58
+ pub fn init_repository_quality(root: &Path) -> Result<QualityInitResult, NaomeError> {
59
+ 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)?;
62
+
63
+ Ok(QualityInitResult {
64
+ schema: "naome.repository-quality-init.v1".to_string(),
65
+ config_written,
66
+ baseline_written,
67
+ baseline_violations: report.violations.len(),
68
+ config_path: config_relative_path().to_string(),
69
+ baseline_path: baseline_relative_path().to_string(),
70
+ })
71
+ }
72
+
73
+ pub fn plan_quality_cleanup(root: &Path) -> Result<QualityCleanupPlan, NaomeError> {
74
+ let report = check_repository_quality(root, QualityMode::Report)?;
75
+ Ok(cleanup_plan_from_violations(&report.violations))
76
+ }
77
+
78
+ pub fn route_quality_cleanup(
79
+ root: &Path,
80
+ path: impl AsRef<str>,
81
+ ) -> Result<QualityCleanupRoute, NaomeError> {
82
+ let path = path.as_ref().replace('\\', "/");
83
+ let report = check_repository_quality(root, QualityMode::Report)?;
84
+ let violations = report
85
+ .violations
86
+ .into_iter()
87
+ .filter(|violation| violation.path == path)
88
+ .collect::<Vec<_>>();
89
+ Ok(cleanup_route_for_path(&path, violations))
90
+ }
@@ -0,0 +1,103 @@
1
+ use std::collections::HashMap;
2
+ use std::fs;
3
+ use std::path::Path;
4
+ use std::process::Command;
5
+
6
+ use crate::models::NaomeError;
7
+
8
+ pub(crate) fn collect_repo_paths(root: &Path) -> Result<Vec<String>, NaomeError> {
9
+ let output = Command::new("git")
10
+ .args([
11
+ "ls-files",
12
+ "-z",
13
+ "--cached",
14
+ "--others",
15
+ "--exclude-standard",
16
+ ])
17
+ .current_dir(root)
18
+ .output();
19
+ if let Ok(output) = output {
20
+ if output.status.success() {
21
+ let mut paths = output
22
+ .stdout
23
+ .split(|byte| *byte == 0)
24
+ .filter(|entry| !entry.is_empty())
25
+ .map(|entry| String::from_utf8_lossy(entry).replace('\\', "/"))
26
+ .collect::<Vec<_>>();
27
+ paths.sort();
28
+ paths.dedup();
29
+ return Ok(paths);
30
+ }
31
+ }
32
+
33
+ let mut paths = Vec::new();
34
+ collect_files_recursive(root, root, &mut paths)?;
35
+ paths.sort();
36
+ Ok(paths)
37
+ }
38
+
39
+ pub(super) fn added_lines_by_path(root: &Path) -> Result<HashMap<String, usize>, NaomeError> {
40
+ let mut added = HashMap::new();
41
+ for args in [
42
+ vec!["diff", "--numstat"],
43
+ vec!["diff", "--cached", "--numstat"],
44
+ ] {
45
+ let output = Command::new("git").args(args).current_dir(root).output()?;
46
+ if !output.status.success() {
47
+ continue;
48
+ }
49
+ for line in String::from_utf8_lossy(&output.stdout).lines() {
50
+ let mut parts = line.split('\t');
51
+ let Some(additions) = parts.next() else {
52
+ continue;
53
+ };
54
+ let _deletions = parts.next();
55
+ let Some(path) = parts.next() else { continue };
56
+ let Ok(count) = additions.parse::<usize>() else {
57
+ continue;
58
+ };
59
+ *added.entry(path.replace('\\', "/")).or_insert(0) += count;
60
+ }
61
+ }
62
+ for path in untracked_paths(root)? {
63
+ if let Ok(content) = fs::read_to_string(root.join(&path)) {
64
+ added.insert(path, content.lines().count());
65
+ }
66
+ }
67
+ Ok(added)
68
+ }
69
+
70
+ fn collect_files_recursive(
71
+ root: &Path,
72
+ dir: &Path,
73
+ paths: &mut Vec<String>,
74
+ ) -> Result<(), NaomeError> {
75
+ for entry in fs::read_dir(dir)? {
76
+ let entry = entry?;
77
+ let path = entry.path();
78
+ if path.is_dir() {
79
+ collect_files_recursive(root, &path, paths)?;
80
+ } else if path.is_file() {
81
+ if let Ok(relative) = path.strip_prefix(root) {
82
+ paths.push(relative.to_string_lossy().replace('\\', "/"));
83
+ }
84
+ }
85
+ }
86
+ Ok(())
87
+ }
88
+
89
+ fn untracked_paths(root: &Path) -> Result<Vec<String>, NaomeError> {
90
+ let output = Command::new("git")
91
+ .args(["ls-files", "--others", "--exclude-standard", "-z"])
92
+ .current_dir(root)
93
+ .output()?;
94
+ if !output.status.success() {
95
+ return Ok(Vec::new());
96
+ }
97
+ Ok(output
98
+ .stdout
99
+ .split(|byte| *byte == 0)
100
+ .filter(|entry| !entry.is_empty())
101
+ .map(|entry| String::from_utf8_lossy(entry).replace('\\', "/"))
102
+ .collect())
103
+ }