dx_forge/api/
branching.rs

1//! Safe File Application with Enterprise-Grade Branching Decision Engine APIs
2
3use anyhow::Result;
4use std::path::PathBuf;
5use std::sync::{Arc, OnceLock};
6use parking_lot::RwLock;
7use std::collections::HashMap;
8
9/// File change representation
10#[derive(Debug, Clone)]
11pub struct FileChange {
12    pub path: PathBuf,
13    pub old_content: Option<String>,
14    pub new_content: String,
15    pub tool_id: String,
16}
17
18/// Branching vote colors
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum BranchColor {
21    Green,      // Auto-approve
22    Yellow,     // Review recommended
23    Red,        // Manual resolution required
24    NoOpinion,  // Abstain from voting
25}
26
27/// Branching vote from a tool
28#[derive(Debug, Clone)]
29pub struct BranchingVote {
30    pub voter_id: String,
31    pub color: BranchColor,
32    pub reason: String,
33    pub confidence: f32,  // 0.0 to 1.0
34}
35
36/// Branching engine state
37static BRANCHING_STATE: OnceLock<Arc<RwLock<BranchingState>>> = OnceLock::new();
38
39struct BranchingState {
40    voters: Vec<String>,
41    pending_changes: Vec<FileChange>,
42    votes: HashMap<PathBuf, Vec<BranchingVote>>,
43    last_application: Option<Vec<PathBuf>>,
44}
45
46impl Default for BranchingState {
47    fn default() -> Self {
48        Self {
49            voters: Vec::new(),
50            pending_changes: Vec::new(),
51            votes: HashMap::new(),
52            last_application: None,
53        }
54    }
55}
56
57fn get_branching_state() -> Arc<RwLock<BranchingState>> {
58    BRANCHING_STATE.get_or_init(|| Arc::new(RwLock::new(BranchingState::default()))).clone()
59}
60
61/// Primary API — full branching resolution + telemetry
62pub fn apply_changes(changes: Vec<FileChange>) -> Result<Vec<PathBuf>> {
63    tracing::info!("📝 Applying {} changes with branching safety", changes.len());
64    
65    let state = get_branching_state();
66    let mut state = state.write();
67    
68    let mut applied_files = Vec::new();
69    
70    for change in changes {
71        // Collect votes for this change
72        let color = query_predicted_branch_color(&change.path)?;
73        
74        match color {
75            BranchColor::Green => {
76                // Auto-apply
77                apply_file_change(&change)?;
78                applied_files.push(change.path.clone());
79                tracing::info!("🟢 Auto-applied: {:?}", change.path);
80            }
81            BranchColor::Yellow => {
82                // Review recommended
83                tracing::warn!("🟡 Review recommended for: {:?}", change.path);
84                prompt_review_for_yellow_conflicts(vec![change.clone()])?;
85                // After review, apply
86                apply_file_change(&change)?;
87                applied_files.push(change.path.clone());
88            }
89            BranchColor::Red => {
90                // Manual resolution required
91                tracing::error!("🔴 Manual resolution required: {:?}", change.path);
92                automatically_reject_red_conflicts(vec![change.clone()])?;
93            }
94            BranchColor::NoOpinion => {
95                // Default to yellow behavior
96                apply_file_change(&change)?;
97                applied_files.push(change.path.clone());
98            }
99        }
100    }
101    
102    state.last_application = Some(applied_files.clone());
103    
104    Ok(applied_files)
105}
106
107/// Fast path when tool knows its changes are safe
108pub fn apply_changes_with_preapproved_votes(changes: Vec<FileChange>) -> Result<Vec<PathBuf>> {
109    tracing::info!("⚡ Fast-path applying {} pre-approved changes", changes.len());
110    
111    let mut applied_files = Vec::new();
112    
113    for change in changes {
114        apply_file_change(&change)?;
115        applied_files.push(change.path.clone());
116    }
117    
118    let state = get_branching_state();
119    state.write().last_application = Some(applied_files.clone());
120    
121    Ok(applied_files)
122}
123
124/// Only forge core or `dx apply --force`
125pub fn apply_changes_force_unchecked(changes: Vec<FileChange>) -> Result<Vec<PathBuf>> {
126    tracing::warn!("⚠️  FORCE APPLYING {} changes WITHOUT SAFETY CHECKS", changes.len());
127    
128    let mut applied_files = Vec::new();
129    
130    for change in changes {
131        apply_file_change(&change)?;
132        applied_files.push(change.path.clone());
133    }
134    
135    Ok(applied_files)
136}
137
138/// Dry-run with full diff, colors, and risk score
139pub fn preview_proposed_changes(changes: Vec<FileChange>) -> Result<String> {
140    let mut preview = String::new();
141    
142    preview.push_str("╔══════════════════════════════════════════════════════════════╗\n");
143    preview.push_str("║          PROPOSED CHANGES PREVIEW                            ║\n");
144    preview.push_str("╚══════════════════════════════════════════════════════════════╝\n\n");
145    
146    for change in &changes {
147        let color = query_predicted_branch_color(&change.path)?;
148        let color_icon = match color {
149            BranchColor::Green => "🟢",
150            BranchColor::Yellow => "🟡",
151            BranchColor::Red => "🔴",
152            BranchColor::NoOpinion => "⚪",
153        };
154        
155        preview.push_str(&format!("{} {:?}\n", color_icon, change.path));
156        preview.push_str(&format!("   Tool: {}\n", change.tool_id));
157        preview.push_str(&format!("   Risk: {:?}\n\n", color));
158    }
159    
160    Ok(preview)
161}
162
163/// Auto-accept green conflicts
164pub fn automatically_accept_green_conflicts(changes: Vec<FileChange>) -> Result<Vec<PathBuf>> {
165    let green_changes: Vec<FileChange> = changes.into_iter()
166        .filter(|c| query_predicted_branch_color(&c.path).ok() == Some(BranchColor::Green))
167        .collect();
168    
169    tracing::info!("🟢 Auto-accepting {} green changes", green_changes.len());
170    apply_changes_with_preapproved_votes(green_changes)
171}
172
173/// Opens rich inline LSP review UI
174pub fn prompt_review_for_yellow_conflicts(changes: Vec<FileChange>) -> Result<()> {
175    tracing::info!("🟡 Prompting review for {} yellow changes", changes.len());
176    
177    // TODO: Open LSP review UI
178    // This would integrate with the editor to show inline diffs
179    
180    Ok(())
181}
182
183/// Auto-reject red conflicts
184pub fn automatically_reject_red_conflicts(changes: Vec<FileChange>) -> Result<()> {
185    tracing::error!("🔴 Rejecting {} red changes", changes.len());
186    
187    for change in changes {
188        tracing::error!("  ❌ {:?} - Manual resolution required", change.path);
189    }
190    
191    Ok(())
192}
193
194/// Undo for cart removal or failed scaffolding
195pub fn revert_most_recent_application() -> Result<Vec<PathBuf>> {
196    let state = get_branching_state();
197    let state = state.read();
198    
199    if let Some(files) = &state.last_application {
200        tracing::info!("🔙 Reverting {} files", files.len());
201        
202        // TODO: Implement actual file reversion
203        // This would restore from backup or git
204        
205        Ok(files.clone())
206    } else {
207        anyhow::bail!("No recent application to revert")
208    }
209}
210
211// ========================================================================
212// Branching Decision Engine
213// ========================================================================
214
215/// Vote Green/Yellow/Red/NoOpinion on a FileChange
216pub fn submit_branching_vote(file: &PathBuf, vote: BranchingVote) -> Result<()> {
217    let state = get_branching_state();
218    let mut state = state.write();
219    
220    state.votes
221        .entry(file.clone())
222        .or_insert_with(Vec::new)
223        .push(vote);
224    
225    Ok(())
226}
227
228/// ui, auth, style, security, check, etc.
229pub fn register_permanent_branching_voter(voter_id: String) -> Result<()> {
230    let state = get_branching_state();
231    let mut state = state.write();
232    
233    if !state.voters.contains(&voter_id) {
234        tracing::info!("🗳️  Registered permanent voter: {}", voter_id);
235        state.voters.push(voter_id);
236    }
237    
238    Ok(())
239}
240
241/// Simulate outcome without applying
242pub fn query_predicted_branch_color(file: &PathBuf) -> Result<BranchColor> {
243    let state = get_branching_state();
244    let state = state.read();
245    
246    // Get votes for this file
247    if let Some(votes) = state.votes.get(file) {
248        // Check for any Red votes (veto)
249        if votes.iter().any(|v| v.color == BranchColor::Red) {
250            return Ok(BranchColor::Red);
251        }
252        
253        // Check for Yellow votes
254        if votes.iter().any(|v| v.color == BranchColor::Yellow) {
255            return Ok(BranchColor::Yellow);
256        }
257        
258        // All Green
259        if votes.iter().all(|v| v.color == BranchColor::Green || v.color == BranchColor::NoOpinion) {
260            return Ok(BranchColor::Green);
261        }
262    }
263    
264    // Default to Green if no votes
265    Ok(BranchColor::Green)
266}
267
268/// True iff every voter returned Green
269pub fn is_change_guaranteed_safe(file: &PathBuf) -> Result<bool> {
270    let state = get_branching_state();
271    let state = state.read();
272    
273    if let Some(votes) = state.votes.get(file) {
274        Ok(votes.iter().all(|v| v.color == BranchColor::Green))
275    } else {
276        Ok(false)
277    }
278}
279
280/// Hard block — highest priority Red vote
281pub fn issue_immediate_veto(file: &PathBuf, voter_id: &str, reason: &str) -> Result<()> {
282    let vote = BranchingVote {
283        voter_id: voter_id.to_string(),
284        color: BranchColor::Red,
285        reason: reason.to_string(),
286        confidence: 1.0,
287    };
288    
289    tracing::error!("🚫 VETO issued for {:?} by {}: {}", file, voter_id, reason);
290    
291    submit_branching_vote(file, vote)?;
292    
293    Ok(())
294}
295
296/// Called before cart commit or variant switch
297pub fn reset_branching_engine_state() -> Result<()> {
298    let state = get_branching_state();
299    let mut state = state.write();
300    
301    tracing::info!("🔄 Resetting branching engine state");
302    state.votes.clear();
303    state.pending_changes.clear();
304    
305    Ok(())
306}
307
308// Helper function
309fn apply_file_change(change: &FileChange) -> Result<()> {
310    // TODO: Actually write file
311    tracing::debug!("💾 Writing file: {:?}", change.path);
312    Ok(())
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318    
319    #[test]
320    fn test_branching_votes() {
321        let file = PathBuf::from("test.ts");
322        
323        let vote = BranchingVote {
324            voter_id: "test-voter".to_string(),
325            color: BranchColor::Green,
326            reason: "Test vote".to_string(),
327            confidence: 0.9,
328        };
329        
330        submit_branching_vote(&file, vote).unwrap();
331        
332        let color = query_predicted_branch_color(&file).unwrap();
333        assert_eq!(color, BranchColor::Green);
334    }
335}