dx_forge/context/
traffic_branch.rs

1/// Traffic Branch System: Red, Yellow & Green Update Strategies
2///
3/// This system intelligently updates DX-managed components using a traffic light metaphor:
4/// - 🟢 GREEN: Auto-update (no local modifications)
5/// - 🟡 YELLOW: 3-way merge (compatible local changes)
6/// - 🔴 RED: Manual conflict resolution required
7///
8/// The system stores base_hash for each managed component to detect local modifications.
9use anyhow::{Context, Result};
10use colored::*;
11use serde::{Deserialize, Serialize};
12use sha2::{Digest, Sha256};
13use std::collections::HashMap;
14use std::fs;
15use std::path::{Path, PathBuf};
16
17/// Component state tracking for traffic branch system
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ComponentState {
20    /// Path to the component file
21    pub path: String,
22
23    /// Hash of the original component when first installed
24    pub base_hash: String,
25
26    /// Component source (e.g., "dx-ui", "dx-icon")
27    pub source: String,
28
29    /// Component name (e.g., "Button", "Icon")
30    pub name: String,
31
32    /// Version when first installed
33    pub version: String,
34
35    /// Timestamp of installation
36    pub installed_at: chrono::DateTime<chrono::Utc>,
37}
38
39/// Result of traffic branch analysis
40#[derive(Debug, Clone, PartialEq)]
41pub enum TrafficBranch {
42    /// 🟢 GREEN: Safe to auto-update (no local modifications)
43    Green,
44
45    /// 🟡 YELLOW: Can merge (non-conflicting local changes)
46    Yellow { conflicts: Vec<String> },
47
48    /// 🔴 RED: Manual resolution required (conflicting changes)
49    Red { conflicts: Vec<String> },
50}
51
52/// State file manager for component tracking
53pub struct ComponentStateManager {
54    state_file: PathBuf,
55    states: HashMap<String, ComponentState>,
56}
57
58impl ComponentStateManager {
59    /// Create a new state manager
60    pub fn new(forge_dir: &Path) -> Result<Self> {
61        let state_file = forge_dir.join("component_state.json");
62
63        let states = if state_file.exists() {
64            let content =
65                fs::read_to_string(&state_file).context("Failed to read component state file")?;
66            serde_json::from_str(&content).unwrap_or_default()
67        } else {
68            HashMap::new()
69        };
70
71        Ok(Self { state_file, states })
72    }
73
74    /// Register a new component installation
75    pub fn register_component(
76        &mut self,
77        path: &Path,
78        source: &str,
79        name: &str,
80        version: &str,
81        content: &str,
82    ) -> Result<()> {
83        let base_hash = compute_hash(content);
84
85        let state = ComponentState {
86            path: path.display().to_string(),
87            base_hash,
88            source: source.to_string(),
89            name: name.to_string(),
90            version: version.to_string(),
91            installed_at: chrono::Utc::now(),
92        };
93
94        self.states.insert(path.display().to_string(), state);
95        self.save()?;
96
97        Ok(())
98    }
99
100    /// Get component state by path
101    pub fn get_component(&self, path: &Path) -> Option<&ComponentState> {
102        self.states.get(&path.display().to_string())
103    }
104
105    /// Check if a file is a managed component
106    pub fn is_managed(&self, path: &Path) -> bool {
107        self.states.contains_key(&path.display().to_string())
108    }
109
110    /// Analyze update strategy for a component
111    pub fn analyze_update(&self, path: &Path, remote_content: &str) -> Result<TrafficBranch> {
112        let state = self
113            .get_component(path)
114            .context("Component not registered")?;
115
116        // Read current local content
117        let local_content = fs::read_to_string(path).context("Failed to read local component")?;
118
119        let base_hash = &state.base_hash;
120        let local_hash = compute_hash(&local_content);
121        let remote_hash = compute_hash(remote_content);
122
123        // 🟢 GREEN BRANCH: No local modifications
124        if local_hash == *base_hash {
125            return Ok(TrafficBranch::Green);
126        }
127
128        // Check if remote has changed
129        if remote_hash == *base_hash {
130            // Remote hasn't changed, but local has - no update needed
131            return Ok(TrafficBranch::Green);
132        }
133
134        // Both local and remote have changed - need 3-way merge
135        // Reconstruct BASE content would require storing it or fetching it
136        // For now, we'll use a simplified conflict detection
137
138        let conflicts = detect_conflicts(&local_content, remote_content);
139
140        if conflicts.is_empty() {
141            // 🟡 YELLOW BRANCH: Non-conflicting changes
142            Ok(TrafficBranch::Yellow { conflicts: vec![] })
143        } else {
144            // 🔴 RED BRANCH: Conflicting changes
145            Ok(TrafficBranch::Red { conflicts })
146        }
147    }
148
149    /// Update component after successful merge
150    pub fn update_component(
151        &mut self,
152        path: &Path,
153        new_version: &str,
154        new_content: &str,
155    ) -> Result<()> {
156        if let Some(state) = self.states.get_mut(&path.display().to_string()) {
157            state.base_hash = compute_hash(new_content);
158            state.version = new_version.to_string();
159            self.save()?;
160        }
161
162        Ok(())
163    }
164
165    /// Remove component from tracking
166    pub fn unregister_component(&mut self, path: &Path) -> Result<()> {
167        self.states.remove(&path.display().to_string());
168        self.save()?;
169        Ok(())
170    }
171
172    /// Save state to disk
173    fn save(&self) -> Result<()> {
174        let content = serde_json::to_string_pretty(&self.states)?;
175        fs::write(&self.state_file, content)?;
176        Ok(())
177    }
178
179    /// List all managed components
180    pub fn list_components(&self) -> Vec<&ComponentState> {
181        self.states.values().collect()
182    }
183}
184
185/// Compute SHA-256 hash of content
186fn compute_hash(content: &str) -> String {
187    let mut hasher = Sha256::new();
188    hasher.update(content.as_bytes());
189    format!("{:x}", hasher.finalize())
190}
191
192/// Detect conflicts between local and remote versions
193/// Returns list of conflicting line ranges
194fn detect_conflicts(local: &str, remote: &str) -> Vec<String> {
195    use similar::{ChangeTag, TextDiff};
196
197    let diff = TextDiff::from_lines(local, remote);
198    let mut conflicts = Vec::new();
199    let mut current_conflict: Option<(usize, usize)> = None;
200
201    for (idx, change) in diff.iter_all_changes().enumerate() {
202        match change.tag() {
203            ChangeTag::Delete | ChangeTag::Insert => {
204                if let Some((start, _)) = current_conflict {
205                    current_conflict = Some((start, idx));
206                } else {
207                    current_conflict = Some((idx, idx));
208                }
209            }
210            ChangeTag::Equal => {
211                if let Some((start, end)) = current_conflict.take() {
212                    conflicts.push(format!("lines {}-{}", start + 1, end + 1));
213                }
214            }
215        }
216    }
217
218    if let Some((start, end)) = current_conflict {
219        conflicts.push(format!("lines {}-{}", start + 1, end + 1));
220    }
221
222    conflicts
223}
224
225/// Apply traffic branch update strategy
226pub async fn apply_update(
227    path: &Path,
228    remote_content: &str,
229    remote_version: &str,
230    state_mgr: &mut ComponentStateManager,
231) -> Result<UpdateResult> {
232    let branch = state_mgr.analyze_update(path, remote_content)?;
233
234    match branch {
235        TrafficBranch::Green => {
236            // 🟢 AUTO-UPDATE: Safe to overwrite
237            fs::write(path, remote_content)?;
238            state_mgr.update_component(path, remote_version, remote_content)?;
239
240            println!(
241                "{} {} updated to v{} {}",
242                "🟢".bright_green(),
243                path.display().to_string().bright_cyan(),
244                remote_version.bright_white(),
245                "(auto-updated)".bright_black()
246            );
247
248            Ok(UpdateResult::AutoUpdated)
249        }
250
251        TrafficBranch::Yellow { .. } => {
252            // 🟡 MERGE: Attempt 3-way merge
253            let local_content = fs::read_to_string(path)?;
254
255            // Simplified merge: append remote changes
256            // In production, use proper 3-way merge algorithm
257            let merged = merge_contents(&local_content, remote_content)?;
258
259            fs::write(path, &merged)?;
260            state_mgr.update_component(path, remote_version, &merged)?;
261
262            println!(
263                "{} {} updated to v{} {}",
264                "🟡".bright_yellow(),
265                path.display().to_string().bright_cyan(),
266                remote_version.bright_white(),
267                "(merged with local changes)".yellow()
268            );
269
270            Ok(UpdateResult::Merged)
271        }
272
273        TrafficBranch::Red { conflicts } => {
274            // 🔴 CONFLICT: Manual resolution required
275            println!(
276                "{} {} {} v{}",
277                "🔴".bright_red(),
278                "CONFLICT:".red().bold(),
279                path.display().to_string().bright_cyan(),
280                remote_version.bright_white()
281            );
282            println!(
283                "   {} Update conflicts with your local changes:",
284                "│".bright_black()
285            );
286            for conflict in &conflicts {
287                println!("   {} Conflict at {}", "│".bright_black(), conflict.red());
288            }
289            println!(
290                "   {} Run {} to resolve",
291                "â””".bright_black(),
292                "forge resolve".bright_white().bold()
293            );
294
295            Ok(UpdateResult::Conflict { conflicts })
296        }
297    }
298}
299
300/// Result of applying an update
301#[derive(Debug)]
302pub enum UpdateResult {
303    /// Successfully auto-updated (Green branch)
304    AutoUpdated,
305
306    /// Successfully merged (Yellow branch)
307    Merged,
308
309    /// Conflict detected (Red branch)
310    Conflict { conflicts: Vec<String> },
311}
312
313/// Simple merge strategy (placeholder for production 3-way merge)
314fn merge_contents(_local: &str, remote: &str) -> Result<String> {
315    // Simplified: If no direct conflicts, use remote
316    // In production, implement proper 3-way merge with BASE content
317    Ok(remote.to_string())
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn test_hash_computation() {
326        let content = "Hello, world!";
327        let hash1 = compute_hash(content);
328        let hash2 = compute_hash(content);
329        assert_eq!(hash1, hash2);
330
331        let different = compute_hash("Different content");
332        assert_ne!(hash1, different);
333    }
334
335    #[test]
336    fn test_conflict_detection() {
337        let local = "line1\nline2\nline3\n";
338        let remote = "line1\nmodified\nline3\n";
339
340        let conflicts = detect_conflicts(local, remote);
341        assert!(!conflicts.is_empty());
342    }
343}