agpm_cli/resolver/backtracking/
registry.rs

1//! Resource registry for tracking resources during backtracking.
2//!
3//! This module provides data structures for tracking resources and their dependency
4//! relationships during conflict resolution. It maintains a complete view of all
5//! resources in the dependency graph, enabling accurate conflict detection after
6//! backtracking changes versions.
7
8use anyhow::Result;
9use std::collections::HashMap;
10
11use crate::lockfile::ResourceId;
12
13/// Tracks resources whose versions changed during backtracking.
14///
15/// These resources need their transitive dependencies re-extracted and re-resolved
16/// because changing a resource's version may change which transitive dependencies
17/// it declares.
18#[derive(Debug, Clone)]
19pub struct TransitiveChangeTracker {
20    /// Map: resource_id → (old_version, new_version, new_sha, variant_inputs)
21    changed_resources: HashMap<String, (String, String, String, Option<serde_json::Value>)>,
22}
23
24impl TransitiveChangeTracker {
25    pub fn new() -> Self {
26        Self {
27            changed_resources: HashMap::new(),
28        }
29    }
30
31    pub fn record_change(
32        &mut self,
33        resource_id: &str,
34        old_version: &str,
35        new_version: &str,
36        new_sha: &str,
37        variant_inputs: Option<serde_json::Value>,
38    ) {
39        self.changed_resources.insert(
40            resource_id.to_string(),
41            (old_version.to_string(), new_version.to_string(), new_sha.to_string(), variant_inputs),
42        );
43    }
44
45    pub fn get_changed_resources(
46        &self,
47    ) -> &HashMap<String, (String, String, String, Option<serde_json::Value>)> {
48        &self.changed_resources
49    }
50
51    pub fn clear(&mut self) {
52        self.changed_resources.clear();
53    }
54}
55
56impl Default for TransitiveChangeTracker {
57    fn default() -> Self {
58        Self::new()
59    }
60}
61
62/// Parameters for adding or updating a resource in the registry.
63#[derive(Debug, Clone)]
64pub struct ResourceParams {
65    pub resource_id: ResourceId,
66    pub version: String,
67    pub sha: String,
68    pub version_constraint: String,
69    pub required_by: String,
70}
71
72/// Entry for a single resource in the registry.
73#[derive(Debug, Clone)]
74pub struct ResourceEntry {
75    /// Full ResourceId structure - used for ConflictDetector
76    pub resource_id: ResourceId,
77
78    /// Current version (may change during backtracking)
79    pub version: String,
80
81    /// Resolved SHA for this version
82    pub sha: String,
83
84    /// Version constraint originally requested
85    pub version_constraint: String,
86
87    /// Resources that depend on this one
88    pub required_by: Vec<String>,
89}
90
91/// Tracks all resources and their dependency relationships for conflict detection.
92///
93/// This registry maintains a complete view of all resources in the dependency graph,
94/// including their current versions, SHAs, and required_by relationships. This enables
95/// accurate conflict detection after backtracking changes versions.
96#[derive(Debug, Clone)]
97pub struct ResourceRegistry {
98    /// Map: resource_id → ResourceEntry
99    resources: HashMap<String, ResourceEntry>,
100}
101
102impl ResourceRegistry {
103    pub fn new() -> Self {
104        Self {
105            resources: HashMap::new(),
106        }
107    }
108
109    /// Add or update a resource in the registry.
110    ///
111    /// If the resource already exists, updates its version and SHA, and adds the
112    /// required_by entry if not already present.
113    pub fn add_or_update_resource(&mut self, params: ResourceParams) {
114        let ResourceParams {
115            resource_id,
116            version,
117            sha,
118            version_constraint,
119            required_by,
120        } = params;
121
122        // Convert ResourceId to string for HashMap key
123        let resource_id_string =
124            resource_id_to_string(&resource_id).expect("ResourceId should have a valid source");
125
126        self.resources
127            .entry(resource_id_string.clone())
128            .and_modify(|entry| {
129                entry.version = version.clone();
130                entry.sha = sha.clone();
131                if !entry.required_by.contains(&required_by) {
132                    entry.required_by.push(required_by.clone());
133                }
134            })
135            .or_insert_with(|| ResourceEntry {
136                resource_id: resource_id.clone(),
137                version,
138                sha,
139                version_constraint,
140                required_by: vec![required_by],
141            });
142    }
143
144    /// Iterate over all resources in the registry.
145    pub fn all_resources(&self) -> impl Iterator<Item = &ResourceEntry> {
146        self.resources.values()
147    }
148
149    /// Update the version and SHA for an existing resource.
150    ///
151    /// This is used during backtracking when a resource's version changes.
152    /// The required_by relationships and version_constraint are preserved.
153    pub fn update_version_and_sha(
154        &mut self,
155        resource_id: &str,
156        new_version: String,
157        new_sha: String,
158    ) {
159        if let Some(entry) = self.resources.get_mut(resource_id) {
160            entry.version = new_version;
161            entry.sha = new_sha;
162        }
163    }
164}
165
166impl Default for ResourceRegistry {
167    fn default() -> Self {
168        Self::new()
169    }
170}
171
172/// Convert a resource_id string (format: "source:path") into components.
173pub fn parse_resource_id_string(resource_id: &str) -> Result<(&str, &str)> {
174    let parts: Vec<&str> = resource_id.splitn(2, ':').collect();
175    if parts.len() != 2 {
176        return Err(anyhow::anyhow!("Invalid resource_id format: {}", resource_id));
177    }
178    Ok((parts[0], parts[1]))
179}
180
181/// Convert a ResourceId to the legacy string format "source:name".
182pub fn resource_id_to_string(resource_id: &ResourceId) -> Result<String> {
183    let source = resource_id
184        .source()
185        .ok_or_else(|| anyhow::anyhow!("Resource {} has no source", resource_id))?;
186    Ok(format!("{}:{}", source, resource_id.name()))
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_parse_resource_id() {
195        let (source, path) = parse_resource_id_string("community:agents/helper.md").unwrap();
196        assert_eq!(source, "community");
197        assert_eq!(path, "agents/helper.md");
198    }
199
200    #[test]
201    fn test_parse_resource_id_invalid() {
202        let result = parse_resource_id_string("invalid");
203        assert!(result.is_err());
204    }
205}