@lamentis/naome 1.3.0 → 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 (137) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +11 -2
  3. package/bin/naome.js +62 -24
  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 +6 -0
  7. package/crates/naome-cli/src/main.rs +43 -6
  8. package/crates/naome-cli/src/quality_commands.rs +31 -46
  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 +100 -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/install_plan.rs +18 -0
  22. package/crates/naome-core/src/journal.rs +2 -7
  23. package/crates/naome-core/src/lib.rs +33 -10
  24. package/crates/naome-core/src/quality/adapter_ios.rs +131 -0
  25. package/crates/naome-core/src/quality/adapter_support.rs +67 -0
  26. package/crates/naome-core/src/quality/adapters.rs +81 -18
  27. package/crates/naome-core/src/quality/cache.rs +7 -9
  28. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +4 -7
  29. package/crates/naome-core/src/quality/config.rs +21 -3
  30. package/crates/naome-core/src/quality/mod.rs +138 -7
  31. package/crates/naome-core/src/quality/reconcile.rs +138 -0
  32. package/crates/naome-core/src/quality/reconcile_anchors.rs +64 -0
  33. package/crates/naome-core/src/quality/scanner/analysis.rs +20 -5
  34. package/crates/naome-core/src/quality/scanner.rs +62 -17
  35. package/crates/naome-core/src/quality/semantic/checks.rs +17 -0
  36. package/crates/naome-core/src/quality/semantic/route.rs +1 -1
  37. package/crates/naome-core/src/quality/structure/adapter_ios.rs +149 -0
  38. package/crates/naome-core/src/quality/structure/adapters.rs +60 -42
  39. package/crates/naome-core/src/quality/structure/checks/directory.rs +6 -4
  40. package/crates/naome-core/src/quality/structure/classify/roles.rs +51 -5
  41. package/crates/naome-core/src/quality/structure/config.rs +24 -3
  42. package/crates/naome-core/src/quality/structure/mod.rs +3 -0
  43. package/crates/naome-core/src/quality/types.rs +20 -1
  44. package/crates/naome-core/src/repository_model/detect.rs +188 -0
  45. package/crates/naome-core/src/repository_model/explain.rs +121 -0
  46. package/crates/naome-core/src/repository_model/path_scan.rs +67 -0
  47. package/crates/naome-core/src/repository_model/path_support.rs +59 -0
  48. package/crates/naome-core/src/repository_model/types.rs +152 -0
  49. package/crates/naome-core/src/repository_model/world.rs +48 -0
  50. package/crates/naome-core/src/repository_model/world_adapters.rs +145 -0
  51. package/crates/naome-core/src/repository_model/world_path_facts.rs +55 -0
  52. package/crates/naome-core/src/repository_model/world_paths.rs +168 -0
  53. package/crates/naome-core/src/repository_model.rs +164 -0
  54. package/crates/naome-core/src/route/builtin_checks.rs +40 -1
  55. package/crates/naome-core/src/task_ledger/import.rs +142 -0
  56. package/crates/naome-core/src/task_ledger/model.rs +13 -0
  57. package/crates/naome-core/src/task_ledger/proof_record.rs +52 -0
  58. package/crates/naome-core/src/task_ledger/read.rs +118 -0
  59. package/crates/naome-core/src/task_ledger/render.rs +55 -0
  60. package/crates/naome-core/src/task_ledger/write.rs +38 -0
  61. package/crates/naome-core/src/task_ledger.rs +48 -0
  62. package/crates/naome-core/src/task_state/api.rs +4 -2
  63. package/crates/naome-core/src/task_state/completed_refresh.rs +5 -16
  64. package/crates/naome-core/src/task_state/diff.rs +2 -2
  65. package/crates/naome-core/src/task_state/evidence.rs +8 -3
  66. package/crates/naome-core/src/task_state/mod.rs +1 -1
  67. package/crates/naome-core/src/task_state/progress.rs +13 -0
  68. package/crates/naome-core/src/task_state/proof_model.rs +8 -8
  69. package/crates/naome-core/src/task_state/repair.rs +2 -2
  70. package/crates/naome-core/src/task_state/task_diff_api.rs +9 -18
  71. package/crates/naome-core/src/task_state/types.rs +24 -0
  72. package/crates/naome-core/src/verification.rs +29 -18
  73. package/crates/naome-core/src/workflow/agent/capability.rs +194 -0
  74. package/crates/naome-core/src/workflow/agent/context_delta.rs +42 -0
  75. package/crates/naome-core/src/workflow/agent/decision.rs +32 -0
  76. package/crates/naome-core/src/workflow/agent/execution.rs +80 -0
  77. package/crates/naome-core/src/workflow/agent/proof.rs +24 -0
  78. package/crates/naome-core/src/workflow/agent/support.rs +58 -0
  79. package/crates/naome-core/src/workflow/agent/watchdog.rs +47 -0
  80. package/crates/naome-core/src/workflow/agent.rs +34 -0
  81. package/crates/naome-core/src/workflow/agent_types.rs +105 -0
  82. package/crates/naome-core/src/workflow/doctor.rs +39 -0
  83. package/crates/naome-core/src/workflow/mod.rs +11 -0
  84. package/crates/naome-core/src/workflow/output.rs +8 -2
  85. package/crates/naome-core/src/workflow/phase_inference.rs +1 -1
  86. package/crates/naome-core/tests/context.rs +99 -0
  87. package/crates/naome-core/tests/harness_health.rs +4 -0
  88. package/crates/naome-core/tests/install_plan.rs +12 -0
  89. package/crates/naome-core/tests/quality.rs +178 -2
  90. package/crates/naome-core/tests/quality_performance.rs +39 -2
  91. package/crates/naome-core/tests/quality_structure_adapters.rs +39 -0
  92. package/crates/naome-core/tests/repo_support/mod.rs +5 -1
  93. package/crates/naome-core/tests/repo_support/verification_values.rs +55 -0
  94. package/crates/naome-core/tests/repository_model.rs +281 -0
  95. package/crates/naome-core/tests/route_user_diff.rs +49 -1
  96. package/crates/naome-core/tests/semantic_legacy.rs +72 -38
  97. package/crates/naome-core/tests/task_ledger.rs +328 -0
  98. package/crates/naome-core/tests/task_state.rs +28 -0
  99. package/crates/naome-core/tests/verification.rs +29 -36
  100. package/crates/naome-core/tests/workflow_agent.rs +233 -0
  101. package/crates/naome-core/tests/workflow_agent_support/mod.rs +159 -0
  102. package/crates/naome-core/tests/workflow_doctor.rs +21 -0
  103. package/installer/codex-hooks.js +121 -0
  104. package/installer/context.js +10 -0
  105. package/installer/filesystem.js +4 -0
  106. package/installer/flows.js +8 -4
  107. package/installer/harness-files.js +6 -0
  108. package/installer/install-plan.js +4 -0
  109. package/installer/main.js +1 -1
  110. package/installer/native.js +1 -1
  111. package/native/darwin-arm64/naome +0 -0
  112. package/native/linux-x64/naome +0 -0
  113. package/package.json +1 -1
  114. package/templates/naome-root/.codex/config.toml +2 -0
  115. package/templates/naome-root/.codex/hooks.json +70 -0
  116. package/templates/naome-root/.naome/bin/check-harness-health.js +8 -6
  117. package/templates/naome-root/.naome/bin/check-task-state.js +12 -7
  118. package/templates/naome-root/.naome/bin/codex-hook-io.js +122 -0
  119. package/templates/naome-root/.naome/bin/codex-hook-policy.js +180 -0
  120. package/templates/naome-root/.naome/bin/codex-hook-runtime.js +174 -0
  121. package/templates/naome-root/.naome/bin/codex-hook.js +6 -0
  122. package/templates/naome-root/.naome/bin/naome.js +35 -4
  123. package/templates/naome-root/.naome/manifest.json +12 -6
  124. package/templates/naome-root/.naome/repository-model.json +6 -0
  125. package/templates/naome-root/.naome/repository-quality.json +3 -1
  126. package/templates/naome-root/.naome/verification.json +15 -1
  127. package/templates/naome-root/AGENTS.md +38 -83
  128. package/templates/naome-root/docs/naome/agent-workflow.md +54 -18
  129. package/templates/naome-root/docs/naome/codex-hooks.md +82 -0
  130. package/templates/naome-root/docs/naome/context-economy.md +73 -0
  131. package/templates/naome-root/docs/naome/first-run.md +25 -14
  132. package/templates/naome-root/docs/naome/index.md +18 -10
  133. package/templates/naome-root/docs/naome/repository-model.md +92 -0
  134. package/templates/naome-root/docs/naome/repository-quality.md +47 -7
  135. package/templates/naome-root/docs/naome/repository-structure.md +10 -3
  136. package/templates/naome-root/docs/naome/task-ledger.md +71 -0
  137. package/templates/naome-root/docs/naome/testing.md +16 -3
@@ -1,3 +1,4 @@
1
+ mod adapter_ios;
1
2
  mod adapter_support;
2
3
  mod adapters;
3
4
  mod baseline;
@@ -6,6 +7,8 @@ mod checks;
6
7
  mod cleanup;
7
8
  mod config;
8
9
  mod config_support;
10
+ mod reconcile;
11
+ mod reconcile_anchors;
9
12
  mod scanner;
10
13
  mod semantic;
11
14
  mod structure;
@@ -14,17 +17,19 @@ mod types;
14
17
  use std::path::Path;
15
18
 
16
19
  use crate::models::NaomeError;
20
+ use crate::repository_model_drift;
17
21
 
18
- pub use cleanup::{cleanup_plan_from_violations, cleanup_route_for_path};
19
22
  pub use cache::{clear_quality_cache, quality_cache_status, QualityCacheStatus};
23
+ pub use cleanup::{cleanup_plan_from_violations, cleanup_route_for_path};
24
+ pub use reconcile::reconcile_repository_quality;
20
25
  pub use semantic::{semantic_route_for_finding, SemanticFinding, SemanticReport};
21
26
  pub use structure::{
22
27
  explain_repository_structure, RepositoryStructureConfig, StructurePathExplanation,
23
28
  };
24
29
  pub use types::{
25
30
  QualityCleanupPlan, QualityCleanupRoute, QualityCleanupTask, QualityInitMode,
26
- QualityInitResult, QualityMode, QualityReport, QualitySummary, QualityViolation,
27
- RepositoryQualityConfig,
31
+ QualityInitResult, QualityMode, QualityReconcileReport, QualityReport, QualitySummary,
32
+ QualityViolation, RepositoryQualityConfig,
28
33
  };
29
34
 
30
35
  use self::baseline::{
@@ -33,8 +38,9 @@ use self::baseline::{
33
38
  };
34
39
  use self::checks::run_quality_checks;
35
40
  use self::config::{config_relative_path, read_config, write_default_config_if_missing};
36
- use self::scanner::collect_repo_paths;
37
- 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};
38
44
  use self::semantic::run_semantic_checks;
39
45
  use self::structure::{
40
46
  run_repository_structure_checks, structure_config_relative_path,
@@ -47,9 +53,37 @@ pub fn check_repository_quality(
47
53
  ) -> Result<QualityReport, NaomeError> {
48
54
  let config = read_config(root)?;
49
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;
50
77
  let baseline = read_baseline_fingerprints(root)?;
51
78
  let mut violations = run_quality_checks(&context);
52
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));
53
87
  for violation in &mut violations {
54
88
  violation.baseline = baseline.contains(&violation.fingerprint);
55
89
  }
@@ -77,7 +111,7 @@ pub fn check_repository_quality(
77
111
  baseline_violation_count,
78
112
  blocking_violation_count,
79
113
  truncated: context.truncated,
80
- reason_codes,
114
+ reason_codes: with_repository_model_reason_codes(reason_codes, &violations),
81
115
  cache_hits: context.cache_hits,
82
116
  cache_misses: context.cache_misses,
83
117
  },
@@ -85,6 +119,86 @@ pub fn check_repository_quality(
85
119
  })
86
120
  }
87
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
+
88
202
  pub fn init_repository_quality(root: &Path) -> Result<QualityInitResult, NaomeError> {
89
203
  init_repository_quality_with_mode(root, QualityInitMode::SeedOnly)
90
204
  }
@@ -112,7 +226,11 @@ pub fn init_repository_quality_with_mode(
112
226
  QualityMode::Report
113
227
  },
114
228
  )?;
115
- (write_baseline(root, &report.violations)?, report.violations.len(), false)
229
+ (
230
+ write_baseline(root, &report.violations)?,
231
+ report.violations.len(),
232
+ false,
233
+ )
116
234
  }
117
235
  };
118
236
 
@@ -154,3 +272,16 @@ pub fn check_semantic_legacy(root: &Path, mode: QualityMode) -> Result<SemanticR
154
272
  let context = scan_repository(root, mode, config)?;
155
273
  Ok(run_semantic_checks(&context))
156
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
+ }
@@ -28,11 +28,7 @@ pub(super) fn analyze_repo_file(
28
28
  }
29
29
  }
30
30
  let content = fs::read_to_string(&full_path).ok()?;
31
- let file = analyze_file(
32
- path,
33
- &content,
34
- added_lines.get(path).copied().unwrap_or(0),
35
- );
31
+ let file = analyze_file(path, &content, added_lines.get(path).copied().unwrap_or(0));
36
32
  let hash = if allow_cache {
37
33
  blob_hashes
38
34
  .get(path)
@@ -110,6 +106,8 @@ fn symbol_start(trimmed: &str) -> Option<(String, String)> {
110
106
  ("function", "def "),
111
107
  ("function", "fn "),
112
108
  ("function", "pub fn "),
109
+ ("function", "pub(crate) fn "),
110
+ ("function", "pub(super) fn "),
113
111
  ("function", "func "),
114
112
  ("class", "class "),
115
113
  ("struct", "struct "),
@@ -123,6 +121,17 @@ fn symbol_start(trimmed: &str) -> Option<(String, String)> {
123
121
  return Some((kind.to_string(), symbol_name(rest)));
124
122
  }
125
123
  }
124
+ for visibility in ["", "pub ", "pub(crate) ", "pub(super) "] {
125
+ for (kind, keyword) in [("const", "const "), ("static", "static ")] {
126
+ let prefix = format!("{visibility}{keyword}");
127
+ if let Some(rest) = trimmed.strip_prefix(&prefix) {
128
+ if !looks_like_typed_rust_item(rest) {
129
+ continue;
130
+ }
131
+ return Some((kind.to_string(), symbol_name(rest)));
132
+ }
133
+ }
134
+ }
126
135
  for prefix in ["const ", "let ", "export const ", "export let "] {
127
136
  if let Some(rest) = trimmed.strip_prefix(prefix) {
128
137
  if trimmed.contains("=>") || trimmed.contains("function") || trimmed.contains("React.")
@@ -134,6 +143,12 @@ fn symbol_start(trimmed: &str) -> Option<(String, String)> {
134
143
  None
135
144
  }
136
145
 
146
+ fn looks_like_typed_rust_item(rest: &str) -> bool {
147
+ rest.split('=').next().is_some_and(|before_assignment| {
148
+ before_assignment.contains(':') && !before_assignment.trim().is_empty()
149
+ })
150
+ }
151
+
137
152
  fn symbol_name(rest: &str) -> String {
138
153
  rest.chars()
139
154
  .take_while(|character| character.is_ascii_alphanumeric() || *character == '_')
@@ -93,12 +93,36 @@ pub fn scan_repository(
93
93
  config: RepositoryQualityConfig,
94
94
  ) -> Result<QualityContext, NaomeError> {
95
95
  let changed_paths = git::changed_paths(root)?;
96
+ scan_repository_with_targets(root, mode, config, changed_paths, true)
97
+ }
98
+
99
+ pub fn scan_repository_paths(
100
+ root: &Path,
101
+ config: RepositoryQualityConfig,
102
+ paths: &[String],
103
+ ) -> Result<QualityContext, NaomeError> {
104
+ let mut target_paths = paths
105
+ .iter()
106
+ .map(|path| normalize_target_path(path))
107
+ .collect::<Result<Vec<_>, _>>()?;
108
+ target_paths.sort();
109
+ target_paths.dedup();
110
+ scan_repository_with_targets(root, QualityMode::PathScoped, config, target_paths, false)
111
+ }
112
+
113
+ fn scan_repository_with_targets(
114
+ root: &Path,
115
+ mode: QualityMode,
116
+ config: RepositoryQualityConfig,
117
+ changed_paths: Vec<String>,
118
+ include_comparison_files: bool,
119
+ ) -> Result<QualityContext, NaomeError> {
96
120
  let target_paths = changed_paths.iter().cloned().collect::<HashSet<_>>();
97
121
  let mut whole_repo_paths = collect_repo_paths(root)?;
98
122
  whole_repo_paths.sort();
99
123
  whole_repo_paths.dedup();
100
124
  let scan_paths = match mode {
101
- QualityMode::ChangedFast => changed_paths.clone(),
125
+ QualityMode::ChangedFast | QualityMode::PathScoped => changed_paths.clone(),
102
126
  QualityMode::Report | QualityMode::DeepReport => whole_repo_paths.clone(),
103
127
  };
104
128
  let added_lines = if mode.is_changed() {
@@ -121,18 +145,22 @@ pub fn scan_repository(
121
145
  &ignore_patterns,
122
146
  &mut state,
123
147
  );
124
- let comparison_files = analyze_comparison_paths(
125
- root,
126
- mode,
127
- &whole_repo_paths,
128
- &target_paths,
129
- &comparison_added_lines,
130
- &blob_hashes,
131
- &cache,
132
- &ignore_patterns,
133
- files.len(),
134
- &mut state,
135
- );
148
+ let comparison_files = if include_comparison_files {
149
+ analyze_comparison_paths(
150
+ root,
151
+ mode,
152
+ &whole_repo_paths,
153
+ &target_paths,
154
+ &comparison_added_lines,
155
+ &blob_hashes,
156
+ &cache,
157
+ &ignore_patterns,
158
+ files.len(),
159
+ &mut state,
160
+ )
161
+ } else {
162
+ Vec::new()
163
+ };
136
164
 
137
165
  files.sort_by(|left, right| left.path.cmp(&right.path));
138
166
  Ok(QualityContext {
@@ -150,6 +178,17 @@ pub fn scan_repository(
150
178
  })
151
179
  }
152
180
 
181
+ fn normalize_target_path(path: &str) -> Result<String, NaomeError> {
182
+ let normalized = path.trim_start_matches("./").replace('\\', "/");
183
+ let has_parent_segment = normalized.split('/').any(|segment| segment == "..");
184
+ if normalized.is_empty() || normalized.starts_with('/') || has_parent_segment {
185
+ return Err(NaomeError::new(format!(
186
+ "quality path must be a repository-relative path: {path}"
187
+ )));
188
+ }
189
+ Ok(normalized)
190
+ }
191
+
153
192
  #[derive(Default)]
154
193
  struct ScanState {
155
194
  cache_hits: usize,
@@ -196,9 +235,14 @@ fn analyze_primary_paths(
196
235
  state.truncate("max_file_bytes");
197
236
  continue;
198
237
  }
199
- if let Some((file, cache_hit)) =
200
- analyze_repo_file(root, path, added_lines, blob_hashes, cache, !mode.is_changed())
201
- {
238
+ if let Some((file, cache_hit)) = analyze_repo_file(
239
+ root,
240
+ path,
241
+ added_lines,
242
+ blob_hashes,
243
+ cache,
244
+ !mode.is_changed(),
245
+ ) {
202
246
  state.record_cache_result(cache_hit);
203
247
  files.push(file);
204
248
  }
@@ -258,6 +302,7 @@ pub fn stable_fingerprint(parts: &[&str]) -> String {
258
302
  fn max_scanned_files(mode: QualityMode) -> usize {
259
303
  match mode {
260
304
  QualityMode::ChangedFast => 2_000,
305
+ QualityMode::PathScoped => 128,
261
306
  QualityMode::Report => 5_000,
262
307
  QualityMode::DeepReport => usize::MAX,
263
308
  }
@@ -265,7 +310,7 @@ fn max_scanned_files(mode: QualityMode) -> usize {
265
310
 
266
311
  fn max_file_bytes(mode: QualityMode) -> u64 {
267
312
  match mode {
268
- QualityMode::ChangedFast => 512 * 1024,
313
+ QualityMode::ChangedFast | QualityMode::PathScoped => 512 * 1024,
269
314
  QualityMode::Report => 1024 * 1024,
270
315
  QualityMode::DeepReport => 8 * 1024 * 1024,
271
316
  }
@@ -112,6 +112,9 @@ fn has_legacy_fixture_signal(keys: &BTreeSet<String>) -> bool {
112
112
  }
113
113
 
114
114
  fn is_shared_fixture_factory(candidate: &ObjectCandidate) -> bool {
115
+ if is_shared_fixture_path(&candidate.path) {
116
+ return true;
117
+ }
115
118
  let Some(symbol) = &candidate.symbol else {
116
119
  return false;
117
120
  };
@@ -123,6 +126,20 @@ fn is_shared_fixture_factory(candidate: &ObjectCandidate) -> bool {
123
126
  .any(|marker| normalized.contains(marker))
124
127
  }
125
128
 
129
+ fn is_shared_fixture_path(path: &str) -> bool {
130
+ let normalized = path.replace('\\', "/").to_ascii_lowercase();
131
+ [
132
+ "/fixtures/",
133
+ "/fixture/",
134
+ "/support/",
135
+ "_support/",
136
+ "-support/",
137
+ "/test-support.",
138
+ ]
139
+ .iter()
140
+ .any(|marker| normalized.contains(marker))
141
+ }
142
+
126
143
  fn group_applies_to_mode(context: &QualityContext, group: &[&ObjectCandidate]) -> bool {
127
144
  if context.mode.is_deep() {
128
145
  return true;
@@ -4,7 +4,7 @@ use crate::quality::types::QualityMode;
4
4
 
5
5
  pub(super) fn finding_mode(context: &QualityContext) -> &'static str {
6
6
  match context.mode {
7
- QualityMode::ChangedFast => "changed-blocking",
7
+ QualityMode::ChangedFast | QualityMode::PathScoped => "changed-blocking",
8
8
  QualityMode::Report => "report",
9
9
  QualityMode::DeepReport => "deep-report",
10
10
  }