@lamentis/naome 1.2.1 → 1.3.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 (155) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +117 -47
  3. package/bin/naome.js +65 -12
  4. package/crates/naome-cli/Cargo.toml +1 -1
  5. package/crates/naome-cli/src/context_commands.rs +47 -0
  6. package/crates/naome-cli/src/dispatcher.rs +12 -2
  7. package/crates/naome-cli/src/main.rs +78 -29
  8. package/crates/naome-cli/src/quality_commands.rs +238 -34
  9. package/crates/naome-cli/src/quality_output.rs +34 -0
  10. package/crates/naome-cli/src/quality_reconcile_command.rs +45 -0
  11. package/crates/naome-cli/src/repository_model_commands.rs +84 -0
  12. package/crates/naome-cli/src/task_commands.rs +62 -0
  13. package/crates/naome-cli/src/workflow_commands.rs +120 -3
  14. package/crates/naome-core/Cargo.toml +1 -1
  15. package/crates/naome-core/src/context/helpers.rs +75 -0
  16. package/crates/naome-core/src/context/select.rs +134 -0
  17. package/crates/naome-core/src/context/types.rs +43 -0
  18. package/crates/naome-core/src/context.rs +6 -0
  19. package/crates/naome-core/src/decision/states.rs +1 -1
  20. package/crates/naome-core/src/decision.rs +4 -1
  21. package/crates/naome-core/src/git.rs +4 -2
  22. package/crates/naome-core/src/install_plan.rs +20 -0
  23. package/crates/naome-core/src/journal.rs +2 -7
  24. package/crates/naome-core/src/lib.rs +35 -8
  25. package/crates/naome-core/src/quality/adapter_ios.rs +131 -0
  26. package/crates/naome-core/src/quality/adapter_support.rs +67 -0
  27. package/crates/naome-core/src/quality/adapters.rs +81 -18
  28. package/crates/naome-core/src/quality/baseline.rs +8 -0
  29. package/crates/naome-core/src/quality/cache.rs +151 -0
  30. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +19 -8
  31. package/crates/naome-core/src/quality/checks/near_duplicates.rs +4 -2
  32. package/crates/naome-core/src/quality/checks.rs +7 -8
  33. package/crates/naome-core/src/quality/cleanup.rs +36 -3
  34. package/crates/naome-core/src/quality/config.rs +21 -3
  35. package/crates/naome-core/src/quality/mod.rs +189 -10
  36. package/crates/naome-core/src/quality/reconcile.rs +138 -0
  37. package/crates/naome-core/src/quality/reconcile_anchors.rs +64 -0
  38. package/crates/naome-core/src/quality/scanner/analysis/normalize.rs +78 -0
  39. package/crates/naome-core/src/quality/scanner/analysis.rs +175 -0
  40. package/crates/naome-core/src/quality/scanner/repo_paths.rs +39 -3
  41. package/crates/naome-core/src/quality/scanner.rs +235 -217
  42. package/crates/naome-core/src/quality/semantic/checks.rs +151 -0
  43. package/crates/naome-core/src/quality/semantic/extract.rs +158 -0
  44. package/crates/naome-core/src/quality/semantic/model.rs +85 -0
  45. package/crates/naome-core/src/quality/semantic/route.rs +52 -0
  46. package/crates/naome-core/src/quality/semantic.rs +68 -0
  47. package/crates/naome-core/src/quality/structure/adapter_ios.rs +149 -0
  48. package/crates/naome-core/src/quality/structure/adapters.rs +60 -42
  49. package/crates/naome-core/src/quality/structure/checks/directory.rs +13 -21
  50. package/crates/naome-core/src/quality/structure/checks.rs +1 -1
  51. package/crates/naome-core/src/quality/structure/classify/roles.rs +51 -5
  52. package/crates/naome-core/src/quality/structure/classify.rs +52 -0
  53. package/crates/naome-core/src/quality/structure/config.rs +24 -3
  54. package/crates/naome-core/src/quality/structure/mod.rs +5 -2
  55. package/crates/naome-core/src/quality/structure/model.rs +8 -1
  56. package/crates/naome-core/src/quality/types.rs +59 -2
  57. package/crates/naome-core/src/repository_model/detect.rs +188 -0
  58. package/crates/naome-core/src/repository_model/explain.rs +121 -0
  59. package/crates/naome-core/src/repository_model/path_scan.rs +67 -0
  60. package/crates/naome-core/src/repository_model/path_support.rs +59 -0
  61. package/crates/naome-core/src/repository_model/types.rs +152 -0
  62. package/crates/naome-core/src/repository_model/world.rs +48 -0
  63. package/crates/naome-core/src/repository_model/world_adapters.rs +145 -0
  64. package/crates/naome-core/src/repository_model/world_path_facts.rs +55 -0
  65. package/crates/naome-core/src/repository_model/world_paths.rs +168 -0
  66. package/crates/naome-core/src/repository_model.rs +164 -0
  67. package/crates/naome-core/src/route/builtin_checks.rs +41 -16
  68. package/crates/naome-core/src/task_ledger/import.rs +142 -0
  69. package/crates/naome-core/src/task_ledger/model.rs +13 -0
  70. package/crates/naome-core/src/task_ledger/proof_record.rs +52 -0
  71. package/crates/naome-core/src/task_ledger/read.rs +118 -0
  72. package/crates/naome-core/src/task_ledger/render.rs +55 -0
  73. package/crates/naome-core/src/task_ledger/write.rs +38 -0
  74. package/crates/naome-core/src/task_ledger.rs +48 -0
  75. package/crates/naome-core/src/task_state/api.rs +4 -2
  76. package/crates/naome-core/src/task_state/completed_refresh.rs +5 -16
  77. package/crates/naome-core/src/task_state/diff.rs +2 -2
  78. package/crates/naome-core/src/task_state/evidence.rs +8 -3
  79. package/crates/naome-core/src/task_state/mod.rs +1 -1
  80. package/crates/naome-core/src/task_state/progress.rs +13 -0
  81. package/crates/naome-core/src/task_state/proof_model.rs +8 -8
  82. package/crates/naome-core/src/task_state/repair.rs +2 -2
  83. package/crates/naome-core/src/task_state/task_diff_api.rs +9 -18
  84. package/crates/naome-core/src/task_state/types.rs +24 -0
  85. package/crates/naome-core/src/verification.rs +29 -18
  86. package/crates/naome-core/src/workflow/agent/capability.rs +194 -0
  87. package/crates/naome-core/src/workflow/agent/context_delta.rs +42 -0
  88. package/crates/naome-core/src/workflow/agent/decision.rs +32 -0
  89. package/crates/naome-core/src/workflow/agent/execution.rs +80 -0
  90. package/crates/naome-core/src/workflow/agent/proof.rs +24 -0
  91. package/crates/naome-core/src/workflow/agent/support.rs +58 -0
  92. package/crates/naome-core/src/workflow/agent/watchdog.rs +47 -0
  93. package/crates/naome-core/src/workflow/agent.rs +34 -0
  94. package/crates/naome-core/src/workflow/agent_types.rs +105 -0
  95. package/crates/naome-core/src/workflow/doctor.rs +183 -0
  96. package/crates/naome-core/src/workflow/mod.rs +13 -0
  97. package/crates/naome-core/src/workflow/mutation.rs +1 -2
  98. package/crates/naome-core/src/workflow/output.rs +8 -2
  99. package/crates/naome-core/src/workflow/phase_inference.rs +1 -1
  100. package/crates/naome-core/tests/context.rs +99 -0
  101. package/crates/naome-core/tests/harness_health.rs +4 -0
  102. package/crates/naome-core/tests/install_plan.rs +14 -0
  103. package/crates/naome-core/tests/quality.rs +190 -5
  104. package/crates/naome-core/tests/quality_performance.rs +268 -0
  105. package/crates/naome-core/tests/quality_structure_adapters.rs +39 -0
  106. package/crates/naome-core/tests/quality_structure_policy.rs +19 -0
  107. package/crates/naome-core/tests/repo_support/mod.rs +5 -1
  108. package/crates/naome-core/tests/repo_support/verification_values.rs +55 -0
  109. package/crates/naome-core/tests/repository_model.rs +281 -0
  110. package/crates/naome-core/tests/route_user_diff.rs +59 -7
  111. package/crates/naome-core/tests/semantic_legacy.rs +174 -0
  112. package/crates/naome-core/tests/task_ledger.rs +328 -0
  113. package/crates/naome-core/tests/task_state.rs +28 -0
  114. package/crates/naome-core/tests/verification.rs +29 -36
  115. package/crates/naome-core/tests/workflow_agent.rs +233 -0
  116. package/crates/naome-core/tests/workflow_agent_support/mod.rs +159 -0
  117. package/crates/naome-core/tests/workflow_doctor.rs +45 -0
  118. package/crates/naome-core/tests/workflow_policy.rs +6 -1
  119. package/installer/codex-hooks.js +121 -0
  120. package/installer/context.js +10 -0
  121. package/installer/filesystem.js +4 -0
  122. package/installer/flows.js +8 -4
  123. package/installer/git-boundary.js +1 -0
  124. package/installer/harness-files.js +6 -0
  125. package/installer/install-plan.js +4 -0
  126. package/installer/main.js +1 -1
  127. package/installer/native.js +1 -1
  128. package/native/darwin-arm64/naome +0 -0
  129. package/native/linux-x64/naome +0 -0
  130. package/package.json +1 -1
  131. package/templates/naome-root/.codex/config.toml +2 -0
  132. package/templates/naome-root/.codex/hooks.json +70 -0
  133. package/templates/naome-root/.naome/bin/check-harness-health.js +8 -6
  134. package/templates/naome-root/.naome/bin/check-task-state.js +12 -7
  135. package/templates/naome-root/.naome/bin/codex-hook-io.js +122 -0
  136. package/templates/naome-root/.naome/bin/codex-hook-policy.js +180 -0
  137. package/templates/naome-root/.naome/bin/codex-hook-runtime.js +174 -0
  138. package/templates/naome-root/.naome/bin/codex-hook.js +6 -0
  139. package/templates/naome-root/.naome/bin/naome.js +45 -7
  140. package/templates/naome-root/.naome/manifest.json +12 -6
  141. package/templates/naome-root/.naome/repository-model.json +6 -0
  142. package/templates/naome-root/.naome/repository-quality.json +3 -1
  143. package/templates/naome-root/.naome/verification.json +15 -1
  144. package/templates/naome-root/.naomeignore +1 -0
  145. package/templates/naome-root/AGENTS.md +38 -83
  146. package/templates/naome-root/docs/naome/agent-workflow.md +66 -28
  147. package/templates/naome-root/docs/naome/codex-hooks.md +82 -0
  148. package/templates/naome-root/docs/naome/context-economy.md +73 -0
  149. package/templates/naome-root/docs/naome/first-run.md +25 -14
  150. package/templates/naome-root/docs/naome/index.md +18 -10
  151. package/templates/naome-root/docs/naome/repository-model.md +92 -0
  152. package/templates/naome-root/docs/naome/repository-quality.md +104 -5
  153. package/templates/naome-root/docs/naome/repository-structure.md +10 -3
  154. package/templates/naome-root/docs/naome/task-ledger.md +71 -0
  155. package/templates/naome-root/docs/naome/testing.md +16 -3
@@ -15,6 +15,12 @@ pub fn config_relative_path() -> &'static str {
15
15
  }
16
16
 
17
17
  pub fn read_config(root: &Path) -> Result<RepositoryQualityConfig, NaomeError> {
18
+ let config = read_policy_config(root)?;
19
+ validate_config(&config)?;
20
+ apply_enabled_adapters(config)
21
+ }
22
+
23
+ pub(super) fn read_policy_config(root: &Path) -> Result<RepositoryQualityConfig, NaomeError> {
18
24
  let path = root.join(CONFIG_RELATIVE_PATH);
19
25
  let config = if path.is_file() {
20
26
  serde_json::from_str(&fs::read_to_string(path)?)?
@@ -22,7 +28,20 @@ pub fn read_config(root: &Path) -> Result<RepositoryQualityConfig, NaomeError> {
22
28
  generated_config(root)?
23
29
  };
24
30
  validate_config(&config)?;
25
- apply_enabled_adapters(config)
31
+ Ok(config)
32
+ }
33
+
34
+ pub(super) fn write_policy_config(
35
+ root: &Path,
36
+ config: &RepositoryQualityConfig,
37
+ ) -> Result<(), NaomeError> {
38
+ let path = root.join(CONFIG_RELATIVE_PATH);
39
+ if let Some(parent) = path.parent() {
40
+ fs::create_dir_all(parent)?;
41
+ }
42
+ let content = serde_json::to_string_pretty(config)?;
43
+ fs::write(path, format!("{content}\n"))?;
44
+ Ok(())
26
45
  }
27
46
 
28
47
  pub fn write_default_config_if_missing(root: &Path) -> Result<bool, NaomeError> {
@@ -34,8 +53,7 @@ pub fn write_default_config_if_missing(root: &Path) -> Result<bool, NaomeError>
34
53
  if let Some(parent) = path.parent() {
35
54
  fs::create_dir_all(parent)?;
36
55
  }
37
- let content = serde_json::to_string_pretty(&generated_config(root)?)?;
38
- fs::write(path, format!("{content}\n"))?;
56
+ write_policy_config(root, &generated_config(root)?)?;
39
57
  Ok(true)
40
58
  }
41
59
 
@@ -1,31 +1,47 @@
1
+ mod adapter_ios;
1
2
  mod adapter_support;
2
3
  mod adapters;
3
4
  mod baseline;
5
+ mod cache;
4
6
  mod checks;
5
7
  mod cleanup;
6
8
  mod config;
7
9
  mod config_support;
10
+ mod reconcile;
11
+ mod reconcile_anchors;
8
12
  mod scanner;
13
+ mod semantic;
9
14
  mod structure;
10
15
  mod types;
11
16
 
12
17
  use std::path::Path;
13
18
 
14
19
  use crate::models::NaomeError;
20
+ use crate::repository_model_drift;
15
21
 
22
+ pub use cache::{clear_quality_cache, quality_cache_status, QualityCacheStatus};
16
23
  pub use cleanup::{cleanup_plan_from_violations, cleanup_route_for_path};
24
+ pub use reconcile::reconcile_repository_quality;
25
+ pub use semantic::{semantic_route_for_finding, SemanticFinding, SemanticReport};
17
26
  pub use structure::{
18
27
  explain_repository_structure, RepositoryStructureConfig, StructurePathExplanation,
19
28
  };
20
29
  pub use types::{
21
- QualityCleanupPlan, QualityCleanupRoute, QualityCleanupTask, QualityInitResult, QualityMode,
22
- QualityReport, QualitySummary, QualityViolation, RepositoryQualityConfig,
30
+ QualityCleanupPlan, QualityCleanupRoute, QualityCleanupTask, QualityInitMode,
31
+ QualityInitResult, QualityMode, QualityReconcileReport, QualityReport, QualitySummary,
32
+ QualityViolation, RepositoryQualityConfig,
23
33
  };
24
34
 
25
- use self::baseline::{baseline_relative_path, read_baseline_fingerprints, write_baseline};
35
+ use self::baseline::{
36
+ baseline_relative_path, read_baseline_fingerprints, write_baseline,
37
+ write_empty_baseline_if_missing,
38
+ };
26
39
  use self::checks::run_quality_checks;
27
40
  use self::config::{config_relative_path, read_config, write_default_config_if_missing};
28
- use self::scanner::scan_repository;
41
+ use self::reconcile::stale_adapter_policy_violations;
42
+ use self::scanner::{collect_repo_paths, stable_fingerprint};
43
+ use self::scanner::{scan_repository, scan_repository_paths};
44
+ use self::semantic::run_semantic_checks;
29
45
  use self::structure::{
30
46
  run_repository_structure_checks, structure_config_relative_path,
31
47
  write_default_structure_config_if_missing,
@@ -37,9 +53,37 @@ pub fn check_repository_quality(
37
53
  ) -> Result<QualityReport, NaomeError> {
38
54
  let config = read_config(root)?;
39
55
  let context = scan_repository(root, mode, config)?;
56
+ quality_report_from_context(root, context)
57
+ }
58
+
59
+ pub fn check_repository_quality_paths(
60
+ root: &Path,
61
+ paths: &[impl AsRef<str>],
62
+ ) -> Result<QualityReport, NaomeError> {
63
+ let config = read_config(root)?;
64
+ let paths = paths
65
+ .iter()
66
+ .map(|path| path.as_ref().to_string())
67
+ .collect::<Vec<_>>();
68
+ let context = scan_repository_paths(root, config, &paths)?;
69
+ quality_report_from_context(root, context)
70
+ }
71
+
72
+ fn quality_report_from_context(
73
+ root: &Path,
74
+ context: self::scanner::QualityContext,
75
+ ) -> Result<QualityReport, NaomeError> {
76
+ let mode = context.mode;
40
77
  let baseline = read_baseline_fingerprints(root)?;
41
78
  let mut violations = run_quality_checks(&context);
42
79
  violations.extend(run_repository_structure_checks(root, &context, &baseline)?);
80
+ violations.extend(stale_adapter_policy_violations(
81
+ root,
82
+ mode,
83
+ &context.changed_paths,
84
+ )?);
85
+ violations.extend(stale_repository_model_violations(root)?);
86
+ violations.extend(semantic_changed_violations(&context));
43
87
  for violation in &mut violations {
44
88
  violation.baseline = baseline.contains(&violation.fingerprint);
45
89
  }
@@ -49,6 +93,10 @@ pub fn check_repository_quality(
49
93
  .filter(|violation| violation.baseline)
50
94
  .count();
51
95
  let ok = blocking_violation_count == 0;
96
+ let mut reason_codes = context.reason_codes.clone();
97
+ if mode == QualityMode::Report {
98
+ reason_codes.push("deep_checks_skipped".to_string());
99
+ }
52
100
 
53
101
  Ok(QualityReport {
54
102
  schema: "naome.repository-quality-report.v1".to_string(),
@@ -58,30 +106,142 @@ pub fn check_repository_quality(
58
106
  scanned_paths: context.scanned_paths(),
59
107
  summary: QualitySummary {
60
108
  scanned_files: context.files.len(),
109
+ scanned_path_count: context.scanned_paths().len(),
61
110
  violation_count: violations.len(),
62
111
  baseline_violation_count,
63
112
  blocking_violation_count,
113
+ truncated: context.truncated,
114
+ reason_codes: with_repository_model_reason_codes(reason_codes, &violations),
115
+ cache_hits: context.cache_hits,
116
+ cache_misses: context.cache_misses,
64
117
  },
65
118
  violations,
66
119
  })
67
120
  }
68
121
 
122
+ fn semantic_changed_violations(context: &self::scanner::QualityContext) -> Vec<QualityViolation> {
123
+ if !context.mode.is_changed() {
124
+ return Vec::new();
125
+ }
126
+ run_semantic_checks(context)
127
+ .findings
128
+ .iter()
129
+ .filter_map(|finding| semantic_finding_violation(context, finding))
130
+ .collect()
131
+ }
132
+
133
+ fn semantic_finding_violation(
134
+ context: &self::scanner::QualityContext,
135
+ finding: &SemanticFinding,
136
+ ) -> Option<QualityViolation> {
137
+ let primary = finding
138
+ .occurrences
139
+ .iter()
140
+ .find(|occurrence| context.applies_to(&occurrence.path))
141
+ .or_else(|| finding.occurrences.first())?;
142
+ let related_paths = finding
143
+ .occurrences
144
+ .iter()
145
+ .map(|occurrence| occurrence.path.clone())
146
+ .filter(|path| path != &primary.path)
147
+ .collect::<Vec<_>>();
148
+ Some(QualityViolation {
149
+ check_id: format!("semantic-{}", finding.kind),
150
+ severity: "blocking".to_string(),
151
+ path: primary.path.clone(),
152
+ line: Some(primary.start_line),
153
+ message: finding.summary.clone(),
154
+ value: Some(finding.confidence),
155
+ limit: None,
156
+ fingerprint: finding.id.clone(),
157
+ related_paths,
158
+ baseline: false,
159
+ })
160
+ }
161
+
162
+ fn stale_repository_model_violations(root: &Path) -> Result<Vec<QualityViolation>, NaomeError> {
163
+ let drift = repository_model_drift(root)?;
164
+ if !drift.model_present || !drift.stale {
165
+ return Ok(Vec::new());
166
+ }
167
+ let message = drift
168
+ .messages
169
+ .first()
170
+ .cloned()
171
+ .unwrap_or_else(|| "NAOME repository model is stale.".to_string());
172
+ Ok(vec![QualityViolation {
173
+ check_id: "repository-model-stale".to_string(),
174
+ severity: "blocking".to_string(),
175
+ path: drift.model_path,
176
+ line: None,
177
+ message: message.clone(),
178
+ value: None,
179
+ limit: None,
180
+ fingerprint: stable_fingerprint(&["repository-model-stale", &message]),
181
+ related_paths: drift.related_paths,
182
+ baseline: false,
183
+ }])
184
+ }
185
+
186
+ fn with_repository_model_reason_codes(
187
+ mut reason_codes: Vec<String>,
188
+ violations: &[QualityViolation],
189
+ ) -> Vec<String> {
190
+ if violations
191
+ .iter()
192
+ .any(|violation| violation.check_id == "repository-model-stale")
193
+ && !reason_codes
194
+ .iter()
195
+ .any(|code| code == "repository_model_stale")
196
+ {
197
+ reason_codes.push("repository_model_stale".to_string());
198
+ }
199
+ reason_codes
200
+ }
201
+
69
202
  pub fn init_repository_quality(root: &Path) -> Result<QualityInitResult, NaomeError> {
203
+ init_repository_quality_with_mode(root, QualityInitMode::SeedOnly)
204
+ }
205
+
206
+ pub fn init_repository_quality_with_mode(
207
+ root: &Path,
208
+ mode: QualityInitMode,
209
+ ) -> Result<QualityInitResult, NaomeError> {
70
210
  let config_written = write_default_config_if_missing(root)?;
71
211
  let structure_config_written = {
72
- let config = read_config(root)?;
73
- let context = scan_repository(root, QualityMode::Report, config)?;
74
- write_default_structure_config_if_missing(root, &context.repo_paths)?
212
+ let repo_paths = collect_repo_paths(root)?;
213
+ write_default_structure_config_if_missing(root, &repo_paths)?
214
+ };
215
+ let (baseline_written, baseline_violations, baseline_pending) = match mode {
216
+ QualityInitMode::SeedOnly => {
217
+ write_empty_baseline_if_missing(root)?;
218
+ (false, 0, true)
219
+ }
220
+ QualityInitMode::Baseline | QualityInitMode::DeepBaseline => {
221
+ let report = check_repository_quality(
222
+ root,
223
+ if mode == QualityInitMode::DeepBaseline {
224
+ QualityMode::DeepReport
225
+ } else {
226
+ QualityMode::Report
227
+ },
228
+ )?;
229
+ (
230
+ write_baseline(root, &report.violations)?,
231
+ report.violations.len(),
232
+ false,
233
+ )
234
+ }
75
235
  };
76
- let report = check_repository_quality(root, QualityMode::Report)?;
77
- let baseline_written = write_baseline(root, &report.violations)?;
78
236
 
79
237
  Ok(QualityInitResult {
80
238
  schema: "naome.repository-quality-init.v1".to_string(),
239
+ mode: mode.as_str().to_string(),
81
240
  config_written,
82
241
  structure_config_written,
83
242
  baseline_written,
84
- baseline_violations: report.violations.len(),
243
+ baseline_pending,
244
+ baseline_violations,
85
245
  config_path: config_relative_path().to_string(),
86
246
  structure_config_path: structure_config_relative_path().to_string(),
87
247
  baseline_path: baseline_relative_path().to_string(),
@@ -106,3 +266,22 @@ pub fn route_quality_cleanup(
106
266
  .collect::<Vec<_>>();
107
267
  Ok(cleanup_route_for_path(&path, violations))
108
268
  }
269
+
270
+ pub fn check_semantic_legacy(root: &Path, mode: QualityMode) -> Result<SemanticReport, NaomeError> {
271
+ let config = read_config(root)?;
272
+ let context = scan_repository(root, mode, config)?;
273
+ Ok(run_semantic_checks(&context))
274
+ }
275
+
276
+ pub fn check_semantic_legacy_paths(
277
+ root: &Path,
278
+ paths: &[impl AsRef<str>],
279
+ ) -> Result<SemanticReport, NaomeError> {
280
+ let config = read_config(root)?;
281
+ let paths = paths
282
+ .iter()
283
+ .map(|path| path.as_ref().to_string())
284
+ .collect::<Vec<_>>();
285
+ let context = scan_repository_paths(root, config, &paths)?;
286
+ Ok(run_semantic_checks(&context))
287
+ }
@@ -0,0 +1,138 @@
1
+ use std::path::Path;
2
+
3
+ use crate::models::NaomeError;
4
+
5
+ use super::adapters::detected_adapter_ids;
6
+ use super::config::{config_relative_path, read_policy_config, write_policy_config};
7
+ use super::reconcile_anchors::changed_paths_introduce_adapter;
8
+ use super::scanner::{collect_repo_paths, stable_fingerprint};
9
+ use super::structure::{
10
+ detected_structure_adapter_ids, read_policy_structure_config, structure_config_relative_path,
11
+ write_policy_structure_config,
12
+ };
13
+ use super::types::{QualityMode, QualityReconcileReport, QualityViolation};
14
+
15
+ pub fn reconcile_repository_quality(
16
+ root: &Path,
17
+ write: bool,
18
+ ) -> Result<QualityReconcileReport, NaomeError> {
19
+ let paths = collect_repo_paths(root)?;
20
+ let mut quality_config = read_policy_config(root)?;
21
+ let mut structure_config = read_policy_structure_config(root, &paths)?;
22
+ let detected_quality_adapters = detected_adapter_ids(&paths);
23
+ let detected_structure_adapters = detected_structure_adapter_ids(&paths);
24
+ let missing_quality_adapters =
25
+ missing_adapters(&detected_quality_adapters, &quality_config.enabled_adapters);
26
+ let missing_structure_adapters = missing_adapters(
27
+ &detected_structure_adapters,
28
+ &structure_config.enabled_adapters,
29
+ );
30
+ let stale = !missing_quality_adapters.is_empty() || !missing_structure_adapters.is_empty();
31
+ let mut updated_paths = Vec::new();
32
+
33
+ if write && stale {
34
+ for adapter in &missing_quality_adapters {
35
+ quality_config.enabled_adapters.push(adapter.clone());
36
+ }
37
+ for adapter in &missing_structure_adapters {
38
+ structure_config.enabled_adapters.push(adapter.clone());
39
+ }
40
+ quality_config.enabled_adapters.sort();
41
+ quality_config.enabled_adapters.dedup();
42
+ structure_config.enabled_adapters.sort();
43
+ structure_config.enabled_adapters.dedup();
44
+
45
+ if !missing_quality_adapters.is_empty() {
46
+ write_policy_config(root, &quality_config)?;
47
+ updated_paths.push(config_relative_path().to_string());
48
+ }
49
+ if !missing_structure_adapters.is_empty() {
50
+ write_policy_structure_config(root, &structure_config)?;
51
+ updated_paths.push(structure_config_relative_path().to_string());
52
+ }
53
+ }
54
+
55
+ let stale_after_write = stale && !write;
56
+ let mut reason_codes = Vec::new();
57
+ if stale {
58
+ reason_codes.push("adapter_policy_stale".to_string());
59
+ }
60
+ if write && !updated_paths.is_empty() {
61
+ reason_codes.push("adapter_policy_updated".to_string());
62
+ }
63
+
64
+ Ok(QualityReconcileReport {
65
+ schema: "naome.repository-quality-reconcile.v1".to_string(),
66
+ ok: !stale_after_write,
67
+ stale: stale_after_write,
68
+ write,
69
+ detected_quality_adapters,
70
+ enabled_quality_adapters: quality_config.enabled_adapters,
71
+ missing_quality_adapters: if write {
72
+ Vec::new()
73
+ } else {
74
+ missing_quality_adapters
75
+ },
76
+ detected_structure_adapters,
77
+ enabled_structure_adapters: structure_config.enabled_adapters,
78
+ missing_structure_adapters: if write {
79
+ Vec::new()
80
+ } else {
81
+ missing_structure_adapters
82
+ },
83
+ updated_paths,
84
+ reason_codes,
85
+ })
86
+ }
87
+
88
+ pub(crate) fn stale_adapter_policy_violations(
89
+ root: &Path,
90
+ mode: QualityMode,
91
+ changed_paths: &[String],
92
+ ) -> Result<Vec<QualityViolation>, NaomeError> {
93
+ let report = reconcile_repository_quality(root, false)?;
94
+ if report.ok {
95
+ return Ok(Vec::new());
96
+ }
97
+
98
+ let mut missing = report.missing_quality_adapters.clone();
99
+ for adapter in &report.missing_structure_adapters {
100
+ if !missing.contains(adapter) {
101
+ missing.push(adapter.clone());
102
+ }
103
+ }
104
+ if mode.is_changed() {
105
+ missing.retain(|adapter| changed_paths_introduce_adapter(adapter, changed_paths));
106
+ }
107
+ if missing.is_empty() {
108
+ return Ok(Vec::new());
109
+ }
110
+ missing.sort();
111
+
112
+ let message = format!(
113
+ "Repository tech-stack signals require adapter policy reconciliation. Missing adapter(s): {}. Run `naome quality reconcile --write`.",
114
+ missing.join(", ")
115
+ );
116
+ let check_id = "adapter-policy-stale";
117
+ let path = config_relative_path();
118
+ Ok(vec![QualityViolation {
119
+ check_id: check_id.to_string(),
120
+ severity: "error".to_string(),
121
+ path: path.to_string(),
122
+ line: None,
123
+ message: message.clone(),
124
+ value: Some(missing.len() as f64),
125
+ limit: Some(0.0),
126
+ fingerprint: stable_fingerprint(&[check_id, path, &missing.join(","), &message]),
127
+ related_paths: vec![structure_config_relative_path().to_string()],
128
+ baseline: false,
129
+ }])
130
+ }
131
+
132
+ fn missing_adapters(detected: &[String], enabled: &[String]) -> Vec<String> {
133
+ detected
134
+ .iter()
135
+ .filter(|adapter| !enabled.contains(adapter))
136
+ .cloned()
137
+ .collect()
138
+ }
@@ -0,0 +1,64 @@
1
+ pub(super) fn changed_paths_introduce_adapter(adapter: &str, changed_paths: &[String]) -> bool {
2
+ match adapter {
3
+ "rust" => changed_paths
4
+ .iter()
5
+ .any(|path| path.ends_with("Cargo.toml")),
6
+ "javascript-typescript" => changed_paths.iter().any(|path| {
7
+ path.ends_with("package.json")
8
+ || path.ends_with("tsconfig.json")
9
+ || path.ends_with("jsconfig.json")
10
+ }),
11
+ "swift" => changed_paths
12
+ .iter()
13
+ .any(|path| path.ends_with("Package.swift") || path.ends_with(".swift")),
14
+ "xcode" => changed_paths
15
+ .iter()
16
+ .any(|path| path.contains(".xcodeproj/") || path.contains(".xcworkspace/")),
17
+ "xctest" => changed_paths.iter().any(|path| {
18
+ let lower = path.to_ascii_lowercase();
19
+ lower.contains("/tests/")
20
+ && (lower.ends_with("tests.swift") || lower.ends_with("uitests.swift"))
21
+ }),
22
+ "swiftui" => changed_paths.iter().any(|path| {
23
+ path.ends_with("App.swift")
24
+ || path.ends_with("View.swift")
25
+ || path.contains("Preview Content/")
26
+ }),
27
+ "ios-app-structure" => changed_paths.iter().any(|path| {
28
+ path.ends_with("AppDelegate.swift")
29
+ || path.ends_with("SceneDelegate.swift")
30
+ || path.ends_with("Info.plist")
31
+ || path.contains(".xcassets/")
32
+ || path.ends_with(".entitlements")
33
+ }),
34
+ "swift-package" => changed_paths.iter().any(|path| {
35
+ path.ends_with("Package.swift")
36
+ || path.starts_with("Sources/")
37
+ || path.contains("/Sources/")
38
+ || path.starts_with("Tests/")
39
+ || path.contains("/Tests/")
40
+ }),
41
+ "ios-resources" => changed_paths.iter().any(|path| {
42
+ path.contains(".xcassets/")
43
+ || path.ends_with(".strings")
44
+ || path.ends_with(".stringsdict")
45
+ || path.ends_with(".plist")
46
+ || path.ends_with(".entitlements")
47
+ || path.ends_with(".storyboard")
48
+ || path.ends_with(".xib")
49
+ }),
50
+ "generated-ios" => changed_paths.iter().any(generated_ios_path),
51
+ _ => false,
52
+ }
53
+ }
54
+
55
+ fn generated_ios_path(path: &String) -> bool {
56
+ let lower = path.to_ascii_lowercase();
57
+ lower.contains("/generated/")
58
+ || lower.contains("/swiftgen/")
59
+ || lower.contains("/sourcery/")
60
+ || lower.ends_with("r.generated.swift")
61
+ || lower.ends_with(".generated.swift")
62
+ || lower.ends_with(".pb.swift")
63
+ || lower.ends_with(".grpc.swift")
64
+ }
@@ -0,0 +1,78 @@
1
+ pub(super) fn normalize_line(line: &str) -> Option<String> {
2
+ let trimmed = line.trim();
3
+ if trimmed.is_empty()
4
+ || is_comment_only(trimmed)
5
+ || is_string_list_item(trimmed)
6
+ || is_generated_hash_mapping(trimmed)
7
+ {
8
+ return None;
9
+ }
10
+
11
+ let mut normalized = String::new();
12
+ let mut in_string = false;
13
+ let mut quote = '\0';
14
+ let mut previous_space = false;
15
+ for character in trimmed.chars() {
16
+ if in_string {
17
+ if character == quote {
18
+ in_string = false;
19
+ normalized.push('S');
20
+ previous_space = false;
21
+ }
22
+ continue;
23
+ }
24
+ if character == '"' || character == '\'' || character == '`' {
25
+ in_string = true;
26
+ quote = character;
27
+ continue;
28
+ }
29
+ let next = if character.is_ascii_digit() {
30
+ '0'
31
+ } else if character.is_whitespace() {
32
+ ' '
33
+ } else {
34
+ character.to_ascii_lowercase()
35
+ };
36
+ if next == ' ' {
37
+ if !previous_space {
38
+ normalized.push(next);
39
+ }
40
+ previous_space = true;
41
+ } else {
42
+ normalized.push(next);
43
+ previous_space = false;
44
+ }
45
+ }
46
+ let value = normalized.trim().to_string();
47
+ (!value.is_empty()).then_some(value)
48
+ }
49
+
50
+ pub(super) fn token_set(line: &str) -> Vec<String> {
51
+ line.split(|character: char| !character.is_ascii_alphanumeric() && character != '_')
52
+ .filter(|token| token.len() > 1)
53
+ .map(ToString::to_string)
54
+ .collect()
55
+ }
56
+
57
+ fn is_comment_only(trimmed: &str) -> bool {
58
+ trimmed.starts_with("//")
59
+ || trimmed.starts_with('#')
60
+ || trimmed.starts_with("/*")
61
+ || trimmed.starts_with('*')
62
+ || trimmed.starts_with("--")
63
+ }
64
+
65
+ fn is_generated_hash_mapping(trimmed: &str) -> bool {
66
+ let Some((key, value)) = trimmed.split_once(':') else {
67
+ return false;
68
+ };
69
+ key.trim_start().starts_with('"')
70
+ && value.trim_start().starts_with("\"sha256:")
71
+ && value.chars().filter(|character| *character == '"').count() >= 2
72
+ }
73
+
74
+ fn is_string_list_item(trimmed: &str) -> bool {
75
+ let value = trimmed.trim_end_matches(',');
76
+ (value.starts_with('"') && value.ends_with('"'))
77
+ || (value.starts_with('\'') && value.ends_with('\''))
78
+ }