1mod algorithm;
26mod registry;
27mod transitive;
28mod types;
29
30use anyhow::Result;
31use std::collections::HashMap;
32use std::time::{Duration, Instant};
33
34use crate::resolver::ResolutionCore;
35use crate::resolver::version_resolver::VersionResolutionService;
36use crate::version::conflict::{ConflictDetector, VersionConflict};
37
38pub use registry::{ResourceParams, parse_resource_id_string, resource_id_to_string};
39pub use types::{BacktrackingIteration, BacktrackingResult, TerminationReason, VersionUpdate};
40
41use registry::{ResourceRegistry, TransitiveChangeTracker};
42
43const MAX_ATTEMPTS: usize = 100;
45
46const MAX_DURATION: Duration = Duration::from_secs(10);
48
49const MAX_ITERATIONS: usize = 10;
51
52pub struct BacktrackingResolver<'a> {
57 core: &'a ResolutionCore,
59
60 version_service: &'a mut VersionResolutionService,
62
63 max_attempts: usize,
65
66 timeout: Duration,
68
69 start_time: Instant,
71
72 attempts: usize,
74
75 change_tracker: TransitiveChangeTracker,
77
78 iteration_history: Vec<BacktrackingIteration>,
80
81 max_iterations: usize,
83
84 resource_registry: ResourceRegistry,
86}
87
88impl<'a> BacktrackingResolver<'a> {
89 pub fn new(
91 core: &'a ResolutionCore,
92 version_service: &'a mut VersionResolutionService,
93 ) -> Self {
94 Self {
95 core,
96 version_service,
97 max_attempts: MAX_ATTEMPTS,
98 timeout: MAX_DURATION,
99 start_time: Instant::now(),
100 attempts: 0,
101 change_tracker: TransitiveChangeTracker::new(),
102 iteration_history: Vec::new(),
103 max_iterations: MAX_ITERATIONS,
104 resource_registry: ResourceRegistry::new(),
105 }
106 }
107
108 pub fn populate_from_conflict_detector(&mut self, conflict_detector: &ConflictDetector) {
113 let requirements = conflict_detector.requirements();
114
115 let mut skipped_count = 0;
116 let mut processed_count = 0;
117
118 for (resource_id, reqs) in requirements {
119 if resource_id_to_string(resource_id).is_err() {
120 let tool_info =
121 resource_id.tool().map(|t| format!("tool: {}", t)).unwrap_or_default();
122 let type_info = format!("type: {}", resource_id.resource_type());
123
124 tracing::warn!(
125 "Skipping resource without source: {} (name: {}, {}, {} requirements: {})",
126 resource_id,
127 resource_id.name(),
128 type_info,
129 tool_info,
130 reqs.len()
131 );
132
133 skipped_count += 1;
134 continue;
135 }
136
137 processed_count += 1;
138
139 for req in reqs {
140 self.resource_registry.add_or_update_resource(ResourceParams {
141 resource_id: resource_id.clone(),
142 version: req.requirement.clone(),
143 sha: req.resolved_sha.clone(),
144 version_constraint: req.requirement.clone(),
145 required_by: req.required_by.clone(),
146 });
147 }
148 }
149
150 if skipped_count > 0 {
151 tracing::info!(
152 "Population complete: processed {} resources, skipped {} without source",
153 processed_count,
154 skipped_count
155 );
156 } else {
157 tracing::debug!(
158 "Population complete: processed {} resources, no local resources skipped",
159 processed_count
160 );
161 }
162 }
163
164 #[allow(dead_code)]
166 pub fn with_limits(
167 core: &'a ResolutionCore,
168 version_service: &'a mut VersionResolutionService,
169 max_attempts: usize,
170 timeout: Duration,
171 ) -> Self {
172 Self {
173 core,
174 version_service,
175 max_attempts,
176 timeout,
177 start_time: Instant::now(),
178 attempts: 0,
179 change_tracker: TransitiveChangeTracker::new(),
180 iteration_history: Vec::new(),
181 max_iterations: MAX_ITERATIONS,
182 resource_registry: ResourceRegistry::new(),
183 }
184 }
185
186 pub async fn resolve_conflicts(
188 &mut self,
189 initial_conflicts: &[VersionConflict],
190 ) -> Result<BacktrackingResult> {
191 tracing::debug!(
192 "Starting iterative backtracking for {} conflict(s), limits: {} iterations, {} attempts, {}s timeout",
193 initial_conflicts.len(),
194 self.max_iterations,
195 self.max_attempts,
196 self.timeout.as_secs()
197 );
198
199 let mut current_conflicts = initial_conflicts.to_vec();
200 let mut all_updates = Vec::new();
201 let mut total_transitive = 0;
202
203 for iteration_num in 1..=self.max_iterations {
204 tracing::debug!("=== Backtracking iteration {} ===", iteration_num);
205 tracing::debug!("Processing {} conflict(s)", current_conflicts.len());
206
207 if self.start_time.elapsed() > self.timeout {
208 tracing::warn!("Backtracking timeout after {:?}", self.start_time.elapsed());
209 return Ok(self.build_result(
210 false,
211 all_updates,
212 total_transitive,
213 TerminationReason::Timeout,
214 ));
215 }
216
217 let mut iteration_updates = Vec::new();
218 for conflict in ¤t_conflicts {
219 match self.resolve_single_conflict(conflict).await? {
220 Some(update) => {
221 tracing::debug!(
222 "Resolved conflict for {}: {} → {}",
223 conflict.resource,
224 update.old_version,
225 update.new_version
226 );
227 iteration_updates.push(update);
228 }
229 None => {
230 tracing::debug!("Could not resolve conflict for {}", conflict.resource);
231 return Ok(self.build_result(
232 false,
233 all_updates,
234 total_transitive,
235 TerminationReason::NoCompatibleVersion,
236 ));
237 }
238 }
239 }
240
241 if iteration_updates.is_empty() {
242 tracing::debug!("No updates found in iteration {}", iteration_num);
243 return Ok(self.build_result(
244 false,
245 all_updates,
246 total_transitive,
247 TerminationReason::NoCompatibleVersion,
248 ));
249 }
250
251 for update in &iteration_updates {
252 self.change_tracker.record_change(
253 &update.resource_id,
254 &update.old_version,
255 &update.new_version,
256 &update.new_sha,
257 update.variant_inputs.clone(),
258 );
259
260 self.resource_registry.update_version_and_sha(
261 &update.resource_id,
262 update.new_version.clone(),
263 update.new_sha.clone(),
264 );
265 }
266
267 all_updates.extend(iteration_updates.clone());
268
269 tracing::debug!(
270 "Re-extracting transitive deps for {} changed resource(s)",
271 self.change_tracker.get_changed_resources().len()
272 );
273 let transitive_count = transitive::reextract_transitive_deps(
274 self.core,
275 self.version_service,
276 &mut self.change_tracker,
277 )
278 .await?;
279 total_transitive += transitive_count;
280
281 if transitive_count > 0 {
282 tracing::debug!("Re-resolved {} transitive dependency(ies)", transitive_count);
283 }
284
285 let new_conflicts = transitive::detect_conflicts_after_changes(&self.resource_registry);
286 tracing::debug!(
287 "After iteration {}: {} conflict(s) remaining",
288 iteration_num,
289 new_conflicts.len()
290 );
291
292 self.iteration_history.push(BacktrackingIteration {
293 iteration: iteration_num,
294 conflicts: current_conflicts.clone(),
295 updates: iteration_updates,
296 transitive_reresolutions: transitive_count,
297 made_progress: !new_conflicts.is_empty() || transitive_count > 0,
298 });
299
300 if new_conflicts.is_empty() {
301 tracing::info!(
302 "✓ Resolved all conflicts after {} iteration(s), {} version update(s), {} transitive re-resolution(s)",
303 iteration_num,
304 all_updates.len(),
305 total_transitive
306 );
307 return Ok(self.build_result(
308 true,
309 all_updates,
310 total_transitive,
311 TerminationReason::Success,
312 ));
313 }
314
315 if algorithm::conflicts_equal(¤t_conflicts, &new_conflicts) {
316 tracing::warn!(
317 "No progress made in iteration {}: same conflicts remain",
318 iteration_num
319 );
320 return Ok(self.build_result(
321 false,
322 all_updates,
323 total_transitive,
324 TerminationReason::NoProgress,
325 ));
326 }
327
328 if self.detect_oscillation(&new_conflicts) {
329 tracing::warn!("Oscillation detected in iteration {}", iteration_num);
330 return Ok(self.build_result(
331 false,
332 all_updates,
333 total_transitive,
334 TerminationReason::Oscillation,
335 ));
336 }
337
338 current_conflicts = new_conflicts;
339 }
340
341 tracing::warn!(
342 "Reached max iterations ({}) without resolving all conflicts. {} conflict(s) remaining",
343 self.max_iterations,
344 current_conflicts.len()
345 );
346 Ok(self.build_result(
347 false,
348 all_updates,
349 total_transitive,
350 TerminationReason::MaxIterations,
351 ))
352 }
353
354 async fn resolve_single_conflict(
356 &mut self,
357 conflict: &VersionConflict,
358 ) -> Result<Option<VersionUpdate>> {
359 let source_name = conflict
360 .resource
361 .source()
362 .ok_or_else(|| anyhow::anyhow!("Resource {} has no source", conflict.resource))?;
363
364 let mut sha_groups: HashMap<&str, Vec<&crate::version::conflict::ConflictingRequirement>> =
365 HashMap::new();
366 for req in &conflict.conflicting_requirements {
367 sha_groups.entry(req.resolved_sha.as_str()).or_default().push(req);
368 }
369
370 let target_sha = algorithm::select_target_sha(&sha_groups)?;
371
372 tracing::debug!(
373 "Target SHA for {}: {} ({} requirements)",
374 conflict.resource,
375 &target_sha[..8.min(target_sha.len())],
376 sha_groups.get(target_sha).map_or(0, |v| v.len())
377 );
378
379 let requirements_to_update: Vec<&crate::version::conflict::ConflictingRequirement> =
380 conflict
381 .conflicting_requirements
382 .iter()
383 .filter(|req| req.resolved_sha != target_sha)
384 .collect();
385
386 if requirements_to_update.is_empty() {
387 return Ok(None);
388 }
389
390 let req_to_update = requirements_to_update[0];
391
392 if req_to_update.required_by == "manifest" {
393 algorithm::find_alternative_for_direct_dependency(
394 self.core,
395 self.version_service,
396 source_name,
397 req_to_update,
398 target_sha,
399 &mut self.attempts,
400 self.max_attempts,
401 self.start_time,
402 self.timeout,
403 )
404 .await
405 } else {
406 algorithm::find_alternative_for_transitive(
407 self.core,
408 self.version_service,
409 source_name,
410 req_to_update,
411 target_sha,
412 &mut self.attempts,
413 self.max_attempts,
414 self.start_time,
415 self.timeout,
416 )
417 .await
418 }
419 }
420
421 fn detect_oscillation(&self, current_conflicts: &[VersionConflict]) -> bool {
423 for iteration in &self.iteration_history {
424 if algorithm::conflicts_equal(&iteration.conflicts, current_conflicts) {
425 tracing::warn!(
426 "Oscillation detected: conflicts match iteration {}",
427 iteration.iteration
428 );
429 return true;
430 }
431 }
432 false
433 }
434
435 fn build_result(
436 &self,
437 resolved: bool,
438 updates: Vec<VersionUpdate>,
439 total_transitive: usize,
440 termination_reason: TerminationReason,
441 ) -> BacktrackingResult {
442 BacktrackingResult {
443 resolved,
444 updates,
445 iterations: self.iteration_history.len(),
446 attempted_versions: self.attempts,
447 iteration_history: self.iteration_history.clone(),
448 total_transitive_reresolutions: total_transitive,
449 termination_reason,
450 }
451 }
452}