dx_forge/
orchestrator.rs

1//! Simple Orchestrator - Only controls WHEN to run tools
2//!
3//! Tools are self-contained and know:
4//! - What files to process
5//! - When they should run
6//! - What patterns to detect
7//!
8//! Forge just detects changes and asks: "Should you run?"
9
10use anyhow::Result;
11use parking_lot::RwLock;
12use serde::{Deserialize, Serialize};
13use std::collections::{HashMap, HashSet};
14use std::path::{Path, PathBuf};
15use std::sync::Arc;
16
17/// Tool execution context shared across all tools
18#[derive(Clone)]
19pub struct ExecutionContext {
20    /// Repository root path
21    pub repo_root: PathBuf,
22
23    /// Forge storage path (.dx/forge)
24    pub forge_path: PathBuf,
25
26    /// Current Git branch
27    pub current_branch: Option<String>,
28
29    /// Changed files in this execution
30    pub changed_files: Vec<PathBuf>,
31
32    /// Shared state between tools
33    pub shared_state: Arc<RwLock<HashMap<String, serde_json::Value>>>,
34
35    /// Traffic branch analyzer
36    pub traffic_analyzer: Arc<dyn TrafficAnalyzer + Send + Sync>,
37
38    /// Component state manager for traffic branch system
39    pub component_manager: Option<Arc<RwLock<crate::context::ComponentStateManager>>>,
40}
41
42impl std::fmt::Debug for ExecutionContext {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        f.debug_struct("ExecutionContext")
45            .field("repo_root", &self.repo_root)
46            .field("forge_path", &self.forge_path)
47            .field("current_branch", &self.current_branch)
48            .field("changed_files", &self.changed_files)
49            .field("traffic_analyzer", &"<dyn TrafficAnalyzer>")
50            .finish()
51    }
52}
53
54impl ExecutionContext {
55    /// Create a new execution context
56    pub fn new(repo_root: PathBuf, forge_path: PathBuf) -> Self {
57        // Try to create component state manager
58        let component_manager = crate::context::ComponentStateManager::new(&forge_path)
59            .ok()
60            .map(|mgr| Arc::new(RwLock::new(mgr)));
61
62        Self {
63            repo_root,
64            forge_path,
65            current_branch: None,
66            changed_files: Vec::new(),
67            shared_state: Arc::new(RwLock::new(HashMap::new())),
68            traffic_analyzer: Arc::new(DefaultTrafficAnalyzer),
69            component_manager,
70        }
71    }
72
73    /// Set a shared value
74    pub fn set<T: Serialize>(&self, key: impl Into<String>, value: T) -> Result<()> {
75        let json = serde_json::to_value(value)?;
76        self.shared_state.write().insert(key.into(), json);
77        Ok(())
78    }
79
80    /// Get a shared value
81    pub fn get<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Result<Option<T>> {
82        let state = self.shared_state.read();
83        if let Some(value) = state.get(key) {
84            let result = serde_json::from_value(value.clone())?;
85            Ok(Some(result))
86        } else {
87            Ok(None)
88        }
89    }
90
91    /// Find regex patterns in a file
92    pub fn find_patterns(&self, _pattern: &str) -> Result<Vec<PatternMatch>> {
93        // Implementation will be added
94        Ok(Vec::new())
95    }
96}
97
98/// Pattern match result
99#[derive(Debug, Clone)]
100pub struct PatternMatch {
101    pub file: PathBuf,
102    pub line: usize,
103    pub col: usize,
104    pub text: String,
105    pub captures: Vec<String>,
106}
107
108/// Output from tool execution
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct ToolOutput {
111    pub success: bool,
112    pub files_modified: Vec<PathBuf>,
113    pub files_created: Vec<PathBuf>,
114    pub files_deleted: Vec<PathBuf>,
115    pub message: String,
116    pub duration_ms: u64,
117}
118
119impl ToolOutput {
120    pub fn success() -> Self {
121        Self {
122            success: true,
123            files_modified: Vec::new(),
124            files_created: Vec::new(),
125            files_deleted: Vec::new(),
126            message: "Success".to_string(),
127            duration_ms: 0,
128        }
129    }
130
131    pub fn failure(message: impl Into<String>) -> Self {
132        Self {
133            success: false,
134            files_modified: Vec::new(),
135            files_created: Vec::new(),
136            files_deleted: Vec::new(),
137            message: message.into(),
138            duration_ms: 0,
139        }
140    }
141}
142
143/// Main DX tool trait - all tools must implement this
144pub trait DxTool: Send + Sync {
145    /// Tool name (e.g., "dx-ui", "dx-style")
146    fn name(&self) -> &str;
147
148    /// Tool version
149    fn version(&self) -> &str;
150
151    /// Execution priority (lower = executes earlier)
152    fn priority(&self) -> u32;
153
154    /// Execute the tool
155    fn execute(&mut self, context: &ExecutionContext) -> Result<ToolOutput>;
156
157    /// Check if tool should run (optional pre-check)
158    fn should_run(&self, _context: &ExecutionContext) -> bool {
159        true
160    }
161
162    /// Tool dependencies (must run after these tools)
163    fn dependencies(&self) -> Vec<String> {
164        Vec::new()
165    }
166
167    /// Before execution hook (setup, validation)
168    fn before_execute(&mut self, _context: &ExecutionContext) -> Result<()> {
169        Ok(())
170    }
171
172    /// After execution hook (cleanup, reporting)
173    fn after_execute(&mut self, _context: &ExecutionContext, _output: &ToolOutput) -> Result<()> {
174        Ok(())
175    }
176
177    /// On error hook (rollback, cleanup)
178    fn on_error(&mut self, _context: &ExecutionContext, _error: &anyhow::Error) -> Result<()> {
179        Ok(())
180    }
181
182    /// Execution timeout in seconds (0 = no timeout)
183    fn timeout_seconds(&self) -> u64 {
184        60
185    }
186}
187
188// Tools are self-contained - no manifests needed
189// Each tool knows what to do and when to run
190
191/// Traffic branch analysis result
192#[derive(Debug, Clone, PartialEq)]
193pub enum TrafficBranch {
194    /// 🟢 Green: Safe to auto-update
195    Green,
196
197    /// 🟡 Yellow: Can merge with conflicts
198    Yellow { conflicts: Vec<Conflict> },
199
200    /// 🔴 Red: Manual resolution required
201    Red { conflicts: Vec<Conflict> },
202}
203
204#[derive(Debug, Clone, PartialEq)]
205pub struct Conflict {
206    pub path: PathBuf,
207    pub line: usize,
208    pub reason: String,
209}
210
211/// Traffic branch analyzer trait
212pub trait TrafficAnalyzer {
213    fn analyze(&self, file: &Path) -> Result<TrafficBranch>;
214    fn can_auto_merge(&self, conflicts: &[Conflict]) -> bool;
215}
216
217/// Default traffic analyzer implementation
218pub struct DefaultTrafficAnalyzer;
219
220impl TrafficAnalyzer for DefaultTrafficAnalyzer {
221    fn analyze(&self, _file: &Path) -> Result<TrafficBranch> {
222        // TODO: Implement actual analysis
223        Ok(TrafficBranch::Green)
224    }
225
226    fn can_auto_merge(&self, conflicts: &[Conflict]) -> bool {
227        conflicts.is_empty()
228    }
229}
230
231/// Orchestration configuration
232#[derive(Debug, Clone)]
233pub struct OrchestratorConfig {
234    /// Enable parallel execution
235    pub parallel: bool,
236
237    /// Fail fast on first error
238    pub fail_fast: bool,
239
240    /// Maximum concurrent tools (for parallel mode)
241    pub max_concurrent: usize,
242
243    /// Enable traffic branch safety checks
244    pub traffic_branch_enabled: bool,
245}
246
247impl Default for OrchestratorConfig {
248    fn default() -> Self {
249        Self {
250            parallel: false,
251            fail_fast: true,
252            max_concurrent: 4,
253            traffic_branch_enabled: true,
254        }
255    }
256}
257
258/// Simple orchestrator - just coordinates tool execution timing
259pub struct Orchestrator {
260    tools: Vec<Box<dyn DxTool>>,
261    context: ExecutionContext,
262    config: OrchestratorConfig,
263}
264
265impl Orchestrator {
266    /// Create a new orchestrator
267    pub fn new(repo_root: impl Into<PathBuf>) -> Result<Self> {
268        let repo_root = repo_root.into();
269        let forge_path = repo_root.join(".dx/forge");
270
271        Ok(Self {
272            tools: Vec::new(),
273            context: ExecutionContext::new(repo_root, forge_path),
274            config: OrchestratorConfig::default(),
275        })
276    }
277
278    /// Create orchestrator with custom configuration
279    pub fn with_config(repo_root: impl Into<PathBuf>, config: OrchestratorConfig) -> Result<Self> {
280        let repo_root = repo_root.into();
281        let forge_path = repo_root.join(".dx/forge");
282
283        Ok(Self {
284            tools: Vec::new(),
285            context: ExecutionContext::new(repo_root, forge_path),
286            config,
287        })
288    }
289
290    /// Update configuration
291    pub fn set_config(&mut self, config: OrchestratorConfig) {
292        self.config = config;
293    }
294
295    /// Register a tool (tools configure themselves)
296    pub fn register_tool(&mut self, tool: Box<dyn DxTool>) -> Result<()> {
297        let name = tool.name().to_string();
298        println!(
299            "📦 Registered tool: {} v{} (priority: {})",
300            name,
301            tool.version(),
302            tool.priority()
303        );
304        self.tools.push(tool);
305        Ok(())
306    }
307
308    /// Execute all registered tools in priority order
309    pub fn execute_all(&mut self) -> Result<Vec<ToolOutput>> {
310        // Sort tools by priority
311        self.tools.sort_by_key(|t| t.priority());
312
313        // Check dependencies
314        self.validate_dependencies()?;
315
316        // Check for circular dependencies
317        self.check_circular_dependencies()?;
318
319        // Execute tools
320        let mut outputs = Vec::new();
321        let context = self.context.clone();
322
323        for tool in &mut self.tools {
324            if !tool.should_run(&context) {
325                println!("⏭️  Skipping {}: pre-check failed", tool.name());
326                continue;
327            }
328
329            println!(
330                "🚀 Executing: {} v{} (priority: {})",
331                tool.name(),
332                tool.version(),
333                tool.priority()
334            );
335
336            // Execute with lifecycle hooks
337            match Self::execute_tool_with_hooks(tool, &context) {
338                Ok(output) => {
339                    if output.success {
340                        println!("✅ {} completed in {}ms", tool.name(), output.duration_ms);
341                    } else {
342                        println!("❌ {} failed: {}", tool.name(), output.message);
343                        
344                        if self.config.fail_fast {
345                            return Err(anyhow::anyhow!("Tool {} failed: {}", tool.name(), output.message));
346                        }
347                    }
348                    outputs.push(output);
349                }
350                Err(e) => {
351                    println!("💥 {} error: {}", tool.name(), e);
352                    
353                    if self.config.fail_fast {
354                        return Err(e);
355                    }
356                    
357                    outputs.push(ToolOutput::failure(format!("Error: {}", e)));
358                }
359            }
360        }
361
362        Ok(outputs)
363    }
364
365    /// Execute tool with lifecycle hooks and error handling
366    fn execute_tool_with_hooks(tool: &mut Box<dyn DxTool>, context: &ExecutionContext) -> Result<ToolOutput> {
367        let start = std::time::Instant::now();
368
369        // Before hook
370        tool.before_execute(context)?;
371
372        // Execute with timeout
373        let result = if tool.timeout_seconds() > 0 {
374            // For now, execute directly (timeout would require tokio runtime)
375            tool.execute(context)
376        } else {
377            tool.execute(context)
378        };
379
380        // Handle result
381        match result {
382            Ok(mut output) => {
383                output.duration_ms = start.elapsed().as_millis() as u64;
384                
385                // After hook
386                tool.after_execute(context, &output)?;
387                
388                Ok(output)
389            }
390            Err(e) => {
391                // Error hook
392                tool.on_error(context, &e)?;
393                Err(e)
394            }
395        }
396    }
397
398    /// Check for circular dependencies
399    fn check_circular_dependencies(&self) -> Result<()> {
400        let mut visited = HashSet::new();
401        let mut stack = HashSet::new();
402
403        for tool in &self.tools {
404            if !visited.contains(tool.name()) {
405                self.check_circular_deps_recursive(tool.name(), &mut visited, &mut stack)?;
406            }
407        }
408
409        Ok(())
410    }
411
412    fn check_circular_deps_recursive(
413        &self,
414        tool_name: &str,
415        visited: &mut HashSet<String>,
416        stack: &mut HashSet<String>,
417    ) -> Result<()> {
418        visited.insert(tool_name.to_string());
419        stack.insert(tool_name.to_string());
420
421        if let Some(tool) = self.tools.iter().find(|t| t.name() == tool_name) {
422            for dep in tool.dependencies() {
423                if !visited.contains(&dep) {
424                    self.check_circular_deps_recursive(&dep, visited, stack)?;
425                } else if stack.contains(&dep) {
426                    return Err(anyhow::anyhow!(
427                        "Circular dependency detected: {} -> {}",
428                        tool_name,
429                        dep
430                    ));
431                }
432            }
433        }
434
435        stack.remove(tool_name);
436        Ok(())
437    }
438
439    /// Validate tool dependencies
440    fn validate_dependencies(&self) -> Result<()> {
441        let tool_names: HashSet<String> = self.tools.iter().map(|t| t.name().to_string()).collect();
442
443        for tool in &self.tools {
444            for dep in tool.dependencies() {
445                if !tool_names.contains(&dep) {
446                    anyhow::bail!(
447                        "Tool '{}' requires '{}' but it's not registered",
448                        tool.name(),
449                        dep
450                    );
451                }
452            }
453        }
454
455        Ok(())
456    }
457
458    /// Get execution context
459    pub fn context(&self) -> &ExecutionContext {
460        &self.context
461    }
462
463    /// Get mutable context
464    pub fn context_mut(&mut self) -> &mut ExecutionContext {
465        &mut self.context
466    }
467}
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472
473    struct MockTool {
474        name: String,
475        priority: u32,
476    }
477
478    impl DxTool for MockTool {
479        fn name(&self) -> &str {
480            &self.name
481        }
482
483        fn version(&self) -> &str {
484            "1.0.0"
485        }
486
487        fn priority(&self) -> u32 {
488            self.priority
489        }
490
491        fn execute(&mut self, _ctx: &ExecutionContext) -> Result<ToolOutput> {
492            Ok(ToolOutput::success())
493        }
494    }
495
496    #[test]
497    fn test_orchestrator_priority_order() {
498        let mut orch = Orchestrator::new("/tmp/test").unwrap();
499
500        orch.register_tool(Box::new(MockTool {
501            name: "tool-c".into(),
502            priority: 30,
503        }))
504        .unwrap();
505        orch.register_tool(Box::new(MockTool {
506            name: "tool-a".into(),
507            priority: 10,
508        }))
509        .unwrap();
510        orch.register_tool(Box::new(MockTool {
511            name: "tool-b".into(),
512            priority: 20,
513        }))
514        .unwrap();
515
516        let outputs = orch.execute_all().unwrap();
517
518        assert_eq!(outputs.len(), 3);
519        assert!(outputs.iter().all(|o| o.success));
520    }
521}