dx_forge/api/
branching.rs1use anyhow::Result;
4use std::path::PathBuf;
5use std::sync::{Arc, OnceLock};
6use parking_lot::RwLock;
7use std::collections::HashMap;
8
9#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum BranchColor {
21 Green, Yellow, Red, NoOpinion, }
26
27#[derive(Debug, Clone)]
29pub struct BranchingVote {
30 pub voter_id: String,
31 pub color: BranchColor,
32 pub reason: String,
33 pub confidence: f32, }
35
36static 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
61pub 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 let color = query_predicted_branch_color(&change.path)?;
73
74 match color {
75 BranchColor::Green => {
76 apply_file_change(&change)?;
78 applied_files.push(change.path.clone());
79 tracing::info!("🟢 Auto-applied: {:?}", change.path);
80 }
81 BranchColor::Yellow => {
82 tracing::warn!("🟡 Review recommended for: {:?}", change.path);
84 prompt_review_for_yellow_conflicts(vec![change.clone()])?;
85 apply_file_change(&change)?;
87 applied_files.push(change.path.clone());
88 }
89 BranchColor::Red => {
90 tracing::error!("🔴 Manual resolution required: {:?}", change.path);
92 automatically_reject_red_conflicts(vec![change.clone()])?;
93 }
94 BranchColor::NoOpinion => {
95 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
107pub 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
124pub 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
138pub 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
163pub 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
173pub fn prompt_review_for_yellow_conflicts(changes: Vec<FileChange>) -> Result<()> {
175 tracing::info!("🟡 Prompting review for {} yellow changes", changes.len());
176
177 Ok(())
181}
182
183pub 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
194pub 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 Ok(files.clone())
206 } else {
207 anyhow::bail!("No recent application to revert")
208 }
209}
210
211pub 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
228pub 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
241pub fn query_predicted_branch_color(file: &PathBuf) -> Result<BranchColor> {
243 let state = get_branching_state();
244 let state = state.read();
245
246 if let Some(votes) = state.votes.get(file) {
248 if votes.iter().any(|v| v.color == BranchColor::Red) {
250 return Ok(BranchColor::Red);
251 }
252
253 if votes.iter().any(|v| v.color == BranchColor::Yellow) {
255 return Ok(BranchColor::Yellow);
256 }
257
258 if votes.iter().all(|v| v.color == BranchColor::Green || v.color == BranchColor::NoOpinion) {
260 return Ok(BranchColor::Green);
261 }
262 }
263
264 Ok(BranchColor::Green)
266}
267
268pub 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
280pub 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
296pub 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
308fn apply_file_change(change: &FileChange) -> Result<()> {
310 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}