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