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///
19/// The execution context provides tools with access to repository state,
20/// file changes, shared data, and traffic branch analysis. It serves as
21/// the communication hub between tools and the orchestrator.
22///
23/// # Fields
24///
25/// - `repo_root`: Absolute path to the repository root
26/// - `forge_path`: Path to Forge data directory (.dx/forge)
27/// - `current_branch`: Git branch name (if in a git repo)
28/// - `changed_files`: Files modified in this execution cycle
29/// - `shared_state`: Thread-safe storage for inter-tool communication
30/// - `traffic_analyzer`: Analyzes file changes for merge safety
31/// - `component_manager`: Manages component state for traffic branches
32#[derive(Clone)]
33pub struct ExecutionContext {
34 /// Repository root path
35 pub repo_root: PathBuf,
36
37 /// Forge storage path (.dx/forge)
38 pub forge_path: PathBuf,
39
40 /// Current Git branch
41 pub current_branch: Option<String>,
42
43 /// Changed files in this execution
44 pub changed_files: Vec<PathBuf>,
45
46 /// Shared state between tools
47 pub shared_state: Arc<RwLock<HashMap<String, serde_json::Value>>>,
48
49 /// Traffic branch analyzer
50 pub traffic_analyzer: Arc<dyn TrafficAnalyzer + Send + Sync>,
51
52 /// Component state manager for traffic branch system
53 pub component_manager: Option<Arc<RwLock<crate::context::ComponentStateManager>>>,
54}
55
56impl std::fmt::Debug for ExecutionContext {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 f.debug_struct("ExecutionContext")
59 .field("repo_root", &self.repo_root)
60 .field("forge_path", &self.forge_path)
61 .field("current_branch", &self.current_branch)
62 .field("changed_files", &self.changed_files)
63 .field("traffic_analyzer", &"<dyn TrafficAnalyzer>")
64 .finish()
65 }
66}
67
68impl ExecutionContext {
69 /// Create a new execution context
70 pub fn new(repo_root: PathBuf, forge_path: PathBuf) -> Self {
71 // Try to create component state manager
72 let component_manager = crate::context::ComponentStateManager::new(&forge_path)
73 .ok()
74 .map(|mgr| Arc::new(RwLock::new(mgr)));
75
76 Self {
77 repo_root,
78 forge_path,
79 current_branch: None,
80 changed_files: Vec::new(),
81 shared_state: Arc::new(RwLock::new(HashMap::new())),
82 traffic_analyzer: Arc::new(DefaultTrafficAnalyzer),
83 component_manager,
84 }
85 }
86
87 /// Set a shared value
88 pub fn set<T: Serialize>(&self, key: impl Into<String>, value: T) -> Result<()> {
89 let json = serde_json::to_value(value)?;
90 self.shared_state.write().insert(key.into(), json);
91 Ok(())
92 }
93
94 /// Get a shared value
95 pub fn get<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Result<Option<T>> {
96 let state = self.shared_state.read();
97 if let Some(value) = state.get(key) {
98 let result = serde_json::from_value(value.clone())?;
99 Ok(Some(result))
100 } else {
101 Ok(None)
102 }
103 }
104
105 /// Find regex patterns in a file
106 pub fn find_patterns(&self, _pattern: &str) -> Result<Vec<PatternMatch>> {
107 // Implementation will be added
108 Ok(Vec::new())
109 }
110}
111
112/// Pattern match result
113#[derive(Debug, Clone)]
114pub struct PatternMatch {
115 pub file: PathBuf,
116 pub line: usize,
117 pub col: usize,
118 pub text: String,
119 pub captures: Vec<String>,
120}
121
122/// Output from tool execution
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct ToolOutput {
125 pub success: bool,
126 pub files_modified: Vec<PathBuf>,
127 pub files_created: Vec<PathBuf>,
128 pub files_deleted: Vec<PathBuf>,
129 pub message: String,
130 pub duration_ms: u64,
131}
132
133impl ToolOutput {
134 pub fn success() -> Self {
135 Self {
136 success: true,
137 files_modified: Vec::new(),
138 files_created: Vec::new(),
139 files_deleted: Vec::new(),
140 message: "Success".to_string(),
141 duration_ms: 0,
142 }
143 }
144
145 pub fn failure(message: impl Into<String>) -> Self {
146 Self {
147 success: false,
148 files_modified: Vec::new(),
149 files_created: Vec::new(),
150 files_deleted: Vec::new(),
151 message: message.into(),
152 duration_ms: 0,
153 }
154 }
155}
156
157/// Main DX tool trait - all tools must implement this
158///
159/// # Overview
160///
161/// The `DxTool` trait provides the core interface for all DX tools in the Forge ecosystem.
162/// Tools are self-contained units that know what files to process, when to run, and how to
163/// integrate with the broader toolchain.
164///
165/// # Lifecycle
166///
167/// Tool execution follows this lifecycle:
168/// 1. `should_run()` - Check if tool should execute
169/// 2. `before_execute()` - Setup and validation
170/// 3. `execute()` - Main tool logic
171/// 4. `after_execute()` - Cleanup and reporting (on success)
172/// 5. `on_error()` - Error handling (on failure)
173///
174/// # Example
175///
176/// ```rust,no_run
177/// use dx_forge::{DxTool, ExecutionContext, ToolOutput};
178/// use anyhow::Result;
179///
180/// struct MyCustomTool {
181/// enabled: bool,
182/// }
183///
184/// impl DxTool for MyCustomTool {
185/// fn name(&self) -> &str { "my-custom-tool" }
186/// fn version(&self) -> &str { "1.0.0" }
187/// fn priority(&self) -> u32 { 50 }
188///
189/// fn execute(&mut self, ctx: &ExecutionContext) -> Result<ToolOutput> {
190/// // Your tool logic here
191/// Ok(ToolOutput::success())
192/// }
193///
194/// fn should_run(&self, _ctx: &ExecutionContext) -> bool {
195/// self.enabled
196/// }
197/// }
198/// ```
199pub trait DxTool: Send + Sync {
200 /// Tool name (e.g., "dx-ui", "dx-style")
201 ///
202 /// This should be a unique identifier for your tool. By convention,
203 /// DX tools use the format "dx-{category}" (e.g., dx-ui, dx-icons, dx-style).
204 fn name(&self) -> &str;
205
206 /// Tool version using semantic versioning
207 ///
208 /// The version should follow semver format (e.g., "1.2.3").
209 /// This is used for dependency resolution and compatibility checking.
210 fn version(&self) -> &str;
211
212 /// Execution priority (lower number = executes earlier)
213 ///
214 /// Tools are executed in priority order. Common priority values:
215 /// - 0-20: Infrastructure tools (code generation, schema validation)
216 /// - 21-50: Component tools (UI, icons, styles)
217 /// - 51-100: Post-processing tools (optimization, bundling)
218 ///
219 /// Default priority is typically 50 for most tools.
220 fn priority(&self) -> u32;
221
222 /// Execute the tool's main logic
223 ///
224 /// This is where the core functionality of your tool should be implemented.
225 /// The execution context provides access to repository state, file changes,
226 /// and shared state between tools.
227 ///
228 /// # Arguments
229 ///
230 /// * `context` - Execution context with repo info and shared state
231 ///
232 /// # Returns
233 ///
234 /// Returns a `ToolOutput` containing execution results, modified files, and status.
235 ///
236 /// # Errors
237 ///
238 /// Return an error if execution fails. The orchestrator will handle cleanup
239 /// and invoke the `on_error` hook.
240 fn execute(&mut self, context: &ExecutionContext) -> Result<ToolOutput>;
241
242 /// Check if tool should run (optional pre-check)
243 ///
244 /// Override this method to implement custom logic for determining whether
245 /// the tool should execute. This is called before `before_execute()`.
246 ///
247 /// # Arguments
248 ///
249 /// * `_context` - Execution context for checking conditions
250 ///
251 /// # Returns
252 ///
253 /// `true` if the tool should execute, `false` to skip execution
254 ///
255 /// # Example
256 ///
257 /// ```rust,no_run
258 /// fn should_run(&self, ctx: &ExecutionContext) -> bool {
259 /// // Only run if TypeScript files changed
260 /// ctx.changed_files.iter().any(|f| f.extension().map_or(false, |e| e == "ts"))
261 /// }
262 /// ```
263 fn should_run(&self, _context: &ExecutionContext) -> bool {
264 true
265 }
266
267 /// Tool dependencies (must run after these tools)
268 ///
269 /// Specify tools that must execute before this tool. Dependencies are validated
270 /// before execution begins, and circular dependencies are detected.
271 ///
272 /// # Returns
273 ///
274 /// Vector of tool names this tool depends on
275 ///
276 /// # Example
277 ///
278 /// ```rust,no_run
279 /// fn dependencies(&self) -> Vec<String> {
280 /// vec!["dx-codegen".to_string(), "dx-schema".to_string()]
281 /// }
282 /// ```
283 fn dependencies(&self) -> Vec<String> {
284 Vec::new()
285 }
286
287 /// Before execution hook (setup, validation)
288 ///
289 /// Called before `execute()`. Use this for:
290 /// - Validating preconditions
291 /// - Setting up temporary resources
292 /// - Checking file permissions
293 /// - Loading configuration
294 ///
295 /// # Errors
296 ///
297 /// Return an error to prevent execution and skip to `on_error`
298 fn before_execute(&mut self, _context: &ExecutionContext) -> Result<()> {
299 Ok(())
300 }
301
302 /// After execution hook (cleanup, reporting)
303 ///
304 /// Called after successful `execute()`. Use this for:
305 /// - Cleaning up temporary files
306 /// - Generating reports
307 /// - Updating shared state
308 /// - Sending notifications
309 ///
310 /// # Arguments
311 ///
312 /// * `_output` - The output from successful execution
313 fn after_execute(&mut self, _context: &ExecutionContext, _output: &ToolOutput) -> Result<()> {
314 Ok(())
315 }
316
317 /// On error hook (rollback, cleanup)
318 ///
319 /// Called when `execute()` or `before_execute()` fails. Use this for:
320 /// - Rolling back partial changes
321 /// - Cleaning up resources
322 /// - Logging detailed error info
323 /// - Sending error notifications
324 ///
325 /// # Arguments
326 ///
327 /// * `_error` - The error that occurred
328 fn on_error(&mut self, _context: &ExecutionContext, _error: &anyhow::Error) -> Result<()> {
329 Ok(())
330 }
331
332 /// Execution timeout in seconds (0 = no timeout)
333 ///
334 /// Specifies the maximum time this tool should be allowed to run.
335 /// Note: Timeout enforcement for synchronous tools is not yet implemented.
336 /// Future versions will use thread-based timeouts or async execution.
337 ///
338 /// # Returns
339 ///
340 /// Timeout duration in seconds, or 0 for no timeout
341 fn timeout_seconds(&self) -> u64 {
342 60
343 }
344}
345
346// Tools are self-contained - no manifests needed
347// Each tool knows what to do and when to run
348
349/// Traffic branch analysis result
350///
351/// Forge uses a "traffic light" system to categorize file changes by risk level:
352///
353/// - **🟢 Green**: Safe to auto-merge (docs, tests, styles, assets)
354/// - **🟡 Yellow**: Reviewable conflicts (code changes that may conflict)
355/// - **🔴 Red**: Manual resolution required (API changes, schemas, migrations)
356///
357/// This system prevents breaking changes from being automatically merged while
358/// allowing safe updates to proceed without manual intervention.
359///
360/// # Example
361///
362/// ```rust,no_run
363/// use dx_forge::{TrafficBranch, TrafficAnalyzer, DefaultTrafficAnalyzer};
364/// use std::path::Path;
365///
366/// let analyzer = DefaultTrafficAnalyzer;
367/// let result = analyzer.analyze(Path::new("src/api/types.ts")).unwrap();
368///
369/// match result {
370/// TrafficBranch::Green => println!("Safe to auto-merge"),
371/// TrafficBranch::Yellow { conflicts } => {
372/// println!("Review {} potential conflicts", conflicts.len())
373/// }
374/// TrafficBranch::Red { conflicts } => {
375/// println!("Manual resolution required for {} conflicts", conflicts.len())
376/// }
377/// }
378/// ```
379#[derive(Debug, Clone, PartialEq)]
380pub enum TrafficBranch {
381 /// 🟢 Green: Safe to auto-update
382 Green,
383
384 /// 🟡 Yellow: Can merge with conflicts
385 Yellow { conflicts: Vec<Conflict> },
386
387 /// 🔴 Red: Manual resolution required
388 Red { conflicts: Vec<Conflict> },
389}
390
391#[derive(Debug, Clone, PartialEq)]
392pub struct Conflict {
393 pub path: PathBuf,
394 pub line: usize,
395 pub reason: String,
396}
397
398/// Traffic branch analyzer trait
399pub trait TrafficAnalyzer {
400 fn analyze(&self, file: &Path) -> Result<TrafficBranch>;
401 fn can_auto_merge(&self, conflicts: &[Conflict]) -> bool;
402}
403
404/// Default traffic analyzer implementation
405pub struct DefaultTrafficAnalyzer;
406
407impl TrafficAnalyzer for DefaultTrafficAnalyzer {
408 fn analyze(&self, file: &Path) -> Result<TrafficBranch> {
409 // Analyze file to determine traffic branch
410 let extension = file
411 .extension()
412 .and_then(|e| e.to_str())
413 .unwrap_or("");
414
415 // 🟢 Green: Auto-update (safe files that don't affect APIs or types)
416 let green_patterns = [
417 "md", "txt", "json", // Documentation and config
418 "css", "scss", "less", // Styles
419 "png", "jpg", "svg", "ico", // Assets
420 "test.ts", "test.js", "spec.ts", "spec.js", // Tests
421 ];
422
423 // 🔴 Red: Manual resolution (breaking changes, API modifications)
424 let red_patterns = [
425 "proto", // Protocol buffers
426 "graphql", "gql", // GraphQL schemas
427 "sql", // Database migrations
428 ];
429
430 // Check if file matches green patterns
431 if green_patterns.iter().any(|p| extension.ends_with(p)) {
432 return Ok(TrafficBranch::Green);
433 }
434
435 // Check if file matches red patterns
436 if red_patterns.iter().any(|p| extension.ends_with(p)) {
437 let conflict = Conflict {
438 path: file.to_path_buf(),
439 line: 0,
440 reason: format!("Breaking change potential: {} file modification", extension),
441 };
442 return Ok(TrafficBranch::Red {
443 conflicts: vec![conflict],
444 });
445 }
446
447 // 🟡 Yellow: Merge required (code files that may have conflicts)
448 // ts, tsx, js, jsx, rs, go, py, etc.
449 if matches!(
450 extension,
451 "ts" | "tsx" | "js" | "jsx" | "rs" | "go" | "py" | "java" | "cpp" | "c" | "h"
452 ) {
453 // Check for API-related indicators in the file path
454 let path_str = file.to_string_lossy().to_lowercase();
455
456 if path_str.contains("api")
457 || path_str.contains("interface")
458 || path_str.contains("types")
459 || path_str.contains("schema")
460 {
461 // Potential API changes - Red
462 let conflict = Conflict {
463 path: file.to_path_buf(),
464 line: 0,
465 reason: "API/Type definition file modification".to_string(),
466 };
467 return Ok(TrafficBranch::Red {
468 conflicts: vec![conflict],
469 });
470 }
471
472 // Regular code file - Yellow (may have merge conflicts)
473 return Ok(TrafficBranch::Yellow {
474 conflicts: vec![],
475 });
476 }
477
478 // Default to Yellow for unknown file types
479 Ok(TrafficBranch::Yellow {
480 conflicts: vec![],
481 })
482 }
483
484 fn can_auto_merge(&self, conflicts: &[Conflict]) -> bool {
485 conflicts.is_empty()
486 }
487}
488
489/// Orchestration configuration
490#[derive(Debug, Clone)]
491pub struct OrchestratorConfig {
492 /// Enable parallel execution
493 pub parallel: bool,
494
495 /// Fail fast on first error
496 pub fail_fast: bool,
497
498 /// Maximum concurrent tools (for parallel mode)
499 pub max_concurrent: usize,
500
501 /// Enable traffic branch safety checks
502 pub traffic_branch_enabled: bool,
503}
504
505impl Default for OrchestratorConfig {
506 fn default() -> Self {
507 Self {
508 parallel: false,
509 fail_fast: true,
510 max_concurrent: 4,
511 traffic_branch_enabled: true,
512 }
513 }
514}
515
516/// Simple orchestrator - coordinates tool execution timing
517///
518/// The Orchestrator manages the execution lifecycle of DX tools, handling:
519///
520/// - **Tool Registration**: Register tools for execution
521/// - **Priority Ordering**: Execute tools in priority order (lower first)
522/// - **Dependency Resolution**: Validate and resolve tool dependencies
523/// - **Circular Dependency Detection**: Prevent infinite dependency loops
524/// - **Lifecycle Hooks**: Invoke before/after/error hooks
525/// - **Parallel Execution**: Support concurrent tool execution (optional)
526/// - **Traffic Branch Integration**: Analyze file changes for merge safety
527/// - **Error Handling**: Fail-fast or continue-on-error modes
528///
529/// # Example
530///
531/// ```rust,no_run
532/// use dx_forge::{Orchestrator, DxTool, ExecutionContext, ToolOutput};
533/// use anyhow::Result;
534///
535/// struct MyTool;
536/// impl DxTool for MyTool {
537/// fn name(&self) -> &str { "my-tool" }
538/// fn version(&self) -> &str { "1.0.0" }
539/// fn priority(&self) -> u32 { 50 }
540/// fn execute(&mut self, _ctx: &ExecutionContext) -> Result<ToolOutput> {
541/// Ok(ToolOutput::success())
542/// }
543/// }
544///
545/// fn main() -> Result<()> {
546/// let mut orch = Orchestrator::new(".")?;
547/// orch.register_tool(Box::new(MyTool))?;
548/// let results = orch.execute_all()?;
549/// println!("Executed {} tools", results.len());
550/// Ok(())
551/// }
552/// ```
553pub struct Orchestrator {
554 tools: Vec<Box<dyn DxTool>>,
555 context: ExecutionContext,
556 config: OrchestratorConfig,
557}
558
559impl Orchestrator {
560 /// Create a new orchestrator
561 pub fn new(repo_root: impl Into<PathBuf>) -> Result<Self> {
562 let repo_root = repo_root.into();
563 let forge_path = repo_root.join(".dx/forge");
564
565 Ok(Self {
566 tools: Vec::new(),
567 context: ExecutionContext::new(repo_root, forge_path),
568 config: OrchestratorConfig::default(),
569 })
570 }
571
572 /// Create orchestrator with custom configuration
573 pub fn with_config(repo_root: impl Into<PathBuf>, config: OrchestratorConfig) -> Result<Self> {
574 let repo_root = repo_root.into();
575 let forge_path = repo_root.join(".dx/forge");
576
577 Ok(Self {
578 tools: Vec::new(),
579 context: ExecutionContext::new(repo_root, forge_path),
580 config,
581 })
582 }
583
584 /// Update configuration
585 pub fn set_config(&mut self, config: OrchestratorConfig) {
586 self.config = config;
587 }
588
589 /// Register a tool (tools configure themselves)
590 pub fn register_tool(&mut self, tool: Box<dyn DxTool>) -> Result<()> {
591 let name = tool.name().to_string();
592 tracing::info!(
593 "📦 Registered tool: {} v{} (priority: {})",
594 name,
595 tool.version(),
596 tool.priority()
597 );
598 self.tools.push(tool);
599 Ok(())
600 }
601
602 /// Execute all registered tools in priority order
603 pub fn execute_all(&mut self) -> Result<Vec<ToolOutput>> {
604 let start_time = std::time::Instant::now();
605 tracing::info!("🎼 Orchestrator starting execution of {} tools", self.tools.len());
606
607 // Sort tools by priority
608 self.tools.sort_by_key(|t| t.priority());
609
610 // Check dependencies
611 tracing::debug!("🔍 Validating tool dependencies...");
612 self.validate_dependencies()?;
613
614 // Check for circular dependencies
615 tracing::debug!("🔄 Checking for circular dependencies...");
616 self.check_circular_dependencies()?;
617
618 tracing::debug!(
619 "📋 Execution order: {}",
620 self.tools
621 .iter()
622 .map(|t| format!("{}(p:{})", t.name(), t.priority()))
623 .collect::<Vec<_>>()
624 .join(" → ")
625 );
626
627 // Execute tools based on parallel configuration
628 let outputs = if self.config.parallel {
629 self.execute_parallel()
630 } else {
631 self.execute_sequential()
632 }?;
633
634 let duration = start_time.elapsed();
635 let success_count = outputs.iter().filter(|o| o.success).count();
636 let failed_count = outputs.len() - success_count;
637
638 tracing::info!(
639 "🏁 Orchestration complete in {:.2}s: {} succeeded, {} failed",
640 duration.as_secs_f64(),
641 success_count,
642 failed_count
643 );
644
645 Ok(outputs)
646 }
647
648 /// Execute tools sequentially in priority order
649 fn execute_sequential(&mut self) -> Result<Vec<ToolOutput>> {
650 let mut outputs = Vec::new();
651 let context = self.context.clone();
652 let total_tools = self.tools.len();
653 let mut executed = 0;
654 let mut skipped = 0;
655 let mut failed = 0;
656
657 for tool in &mut self.tools {
658 if !tool.should_run(&context) {
659 tracing::info!("⏭️ Skipping {}: pre-check failed", tool.name());
660 skipped += 1;
661 continue;
662 }
663
664 tracing::info!(
665 "🚀 Executing: {} v{} (priority: {}, {}/{})",
666 tool.name(),
667 tool.version(),
668 tool.priority(),
669 executed + 1,
670 total_tools
671 );
672
673 // Execute with lifecycle hooks
674 match Self::execute_tool_with_hooks(tool, &context) {
675 Ok(output) => {
676 if output.success {
677 executed += 1;
678 tracing::info!("✅ {} completed in {}ms", tool.name(), output.duration_ms);
679 } else {
680 failed += 1;
681 tracing::error!("❌ {} failed: {}", tool.name(), output.message);
682
683 if self.config.fail_fast {
684 tracing::error!("💥 Fail-fast enabled, stopping orchestration");
685 return Err(anyhow::anyhow!("Tool {} failed: {}", tool.name(), output.message));
686 }
687 }
688 outputs.push(output);
689 }
690 Err(e) => {
691 failed += 1;
692 tracing::error!("💥 {} error: {}", tool.name(), e);
693
694 if self.config.fail_fast {
695 tracing::error!("💥 Fail-fast enabled, stopping orchestration");
696 return Err(e);
697 }
698
699 outputs.push(ToolOutput::failure(format!("Error: {}", e)));
700 }
701 }
702 }
703
704 tracing::info!(
705 "📊 Sequential execution complete: {} executed, {} skipped, {} failed",
706 executed,
707 skipped,
708 failed
709 );
710
711 Ok(outputs)
712 }
713
714 /// Execute tools in parallel where possible, respecting dependencies
715 fn execute_parallel(&mut self) -> Result<Vec<ToolOutput>> {
716 tracing::info!("🚀 Parallel execution mode (max {} concurrent)", self.config.max_concurrent);
717
718 // Build dependency graph
719 let dep_graph = self.build_dependency_graph();
720
721 // Group tools into execution waves (tools that can run concurrently)
722 let waves = self.compute_execution_waves(&dep_graph)?;
723
724 tracing::debug!("📊 Execution waves: {}", waves.len());
725 for (i, wave) in waves.iter().enumerate() {
726 tracing::debug!(" Wave {}: {} tools", i + 1, wave.len());
727 }
728
729 let mut all_outputs = Vec::new();
730 let context = self.context.clone();
731
732 // Execute each wave in parallel
733 for (wave_idx, wave_tools) in waves.into_iter().enumerate() {
734 tracing::info!("🌊 Executing wave {} with {} tools", wave_idx + 1, wave_tools.len());
735
736 let mut wave_outputs = Vec::new();
737
738 // For now, execute wave tools sequentially (true parallel requires async DxTool trait)
739 // Future enhancement: Use thread pool or async execution
740 for tool_idx in wave_tools {
741 let tool = &mut self.tools[tool_idx];
742
743 if !tool.should_run(&context) {
744 tracing::info!("⏭️ Skipping {}: pre-check failed", tool.name());
745 continue;
746 }
747
748 tracing::info!("🚀 Executing: {} v{}", tool.name(), tool.version());
749
750 match Self::execute_tool_with_hooks(tool, &context) {
751 Ok(output) => {
752 if output.success {
753 tracing::info!("✅ {} completed in {}ms", tool.name(), output.duration_ms);
754 } else {
755 tracing::error!("❌ {} failed: {}", tool.name(), output.message);
756
757 if self.config.fail_fast {
758 return Err(anyhow::anyhow!("Tool {} failed: {}", tool.name(), output.message));
759 }
760 }
761 wave_outputs.push(output);
762 }
763 Err(e) => {
764 tracing::error!("💥 {} error: {}", tool.name(), e);
765
766 if self.config.fail_fast {
767 return Err(e);
768 }
769
770 wave_outputs.push(ToolOutput::failure(format!("Error: {}", e)));
771 }
772 }
773 }
774
775 all_outputs.extend(wave_outputs);
776 }
777
778 Ok(all_outputs)
779 }
780
781 /// Build a dependency graph for tools
782 fn build_dependency_graph(&self) -> HashMap<String, HashSet<String>> {
783 let mut graph = HashMap::new();
784
785 for tool in &self.tools {
786 let deps: HashSet<String> = tool.dependencies().into_iter().collect();
787 graph.insert(tool.name().to_string(), deps);
788 }
789
790 graph
791 }
792
793 /// Compute execution waves based on dependency graph
794 /// Tools in the same wave have no dependencies on each other
795 fn compute_execution_waves(&self, dep_graph: &HashMap<String, HashSet<String>>) -> Result<Vec<Vec<usize>>> {
796 let mut waves: Vec<Vec<usize>> = Vec::new();
797 let mut completed: HashSet<String> = HashSet::new();
798 let mut remaining: Vec<usize> = (0..self.tools.len()).collect();
799
800 while !remaining.is_empty() {
801 let mut current_wave = Vec::new();
802 let mut next_remaining = Vec::new();
803
804 for &idx in &remaining {
805 let tool = &self.tools[idx];
806 let tool_name = tool.name().to_string();
807
808 // Check if all dependencies are completed
809 let deps = dep_graph.get(&tool_name).cloned().unwrap_or_default();
810 let all_deps_met = deps.iter().all(|dep| completed.contains(dep));
811
812 if all_deps_met {
813 current_wave.push(idx);
814 completed.insert(tool_name);
815 } else {
816 next_remaining.push(idx);
817 }
818 }
819
820 if current_wave.is_empty() && !remaining.is_empty() {
821 // No progress - likely a circular dependency or missing dependency
822 let unmet: Vec<String> = remaining.iter()
823 .map(|&idx| self.tools[idx].name().to_string())
824 .collect();
825 return Err(anyhow::anyhow!(
826 "Cannot resolve dependencies for tools: {}",
827 unmet.join(", ")
828 ));
829 }
830
831 if !current_wave.is_empty() {
832 waves.push(current_wave);
833 }
834
835 remaining = next_remaining;
836 }
837
838 Ok(waves)
839 }
840
841 /// Execute tool with lifecycle hooks and error handling
842 fn execute_tool_with_hooks(tool: &mut Box<dyn DxTool>, context: &ExecutionContext) -> Result<ToolOutput> {
843 let start = std::time::Instant::now();
844 let tool_name = tool.name().to_string();
845
846 // Before hook
847 tracing::debug!("📝 Running before_execute hook for {}", tool_name);
848 tool.before_execute(context)?;
849
850 // Execute with timeout
851 // Note: Since the DxTool trait's execute method is synchronous,
852 // we can't use async timeout without significant refactoring.
853 // Future improvement: make DxTool async or use thread-based timeout
854 let result = if tool.timeout_seconds() > 0 {
855 tracing::debug!(
856 "⏱️ Executing {} with {}s timeout (note: timeout monitoring not yet implemented for sync tools)",
857 tool_name,
858 tool.timeout_seconds()
859 );
860 tool.execute(context)
861 } else {
862 tracing::debug!("🚀 Executing {} without timeout", tool_name);
863 tool.execute(context)
864 };
865
866 // Handle result
867 match result {
868 Ok(mut output) => {
869 let duration = start.elapsed();
870 output.duration_ms = duration.as_millis() as u64;
871
872 tracing::info!(
873 "✅ {} completed successfully in {:.2}s",
874 tool_name,
875 duration.as_secs_f64()
876 );
877
878 if !output.files_modified.is_empty() {
879 tracing::debug!(" 📝 Modified {} files", output.files_modified.len());
880 }
881 if !output.files_created.is_empty() {
882 tracing::debug!(" ✨ Created {} files", output.files_created.len());
883 }
884 if !output.files_deleted.is_empty() {
885 tracing::debug!(" 🗑️ Deleted {} files", output.files_deleted.len());
886 }
887
888 // After hook
889 tracing::debug!("📝 Running after_execute hook for {}", tool_name);
890 tool.after_execute(context, &output)?;
891
892 Ok(output)
893 }
894 Err(e) => {
895 let duration = start.elapsed();
896 tracing::error!(
897 "❌ {} failed after {:.2}s: {}",
898 tool_name,
899 duration.as_secs_f64(),
900 e
901 );
902
903 // Error hook
904 tracing::debug!("📝 Running on_error hook for {}", tool_name);
905 tool.on_error(context, &e)?;
906 Err(e)
907 }
908 }
909 }
910
911 /// Check for circular dependencies
912 fn check_circular_dependencies(&self) -> Result<()> {
913 let mut visited = HashSet::new();
914 let mut stack = HashSet::new();
915
916 for tool in &self.tools {
917 if !visited.contains(tool.name()) {
918 self.check_circular_deps_recursive(tool.name(), &mut visited, &mut stack)?;
919 }
920 }
921
922 Ok(())
923 }
924
925 fn check_circular_deps_recursive(
926 &self,
927 tool_name: &str,
928 visited: &mut HashSet<String>,
929 stack: &mut HashSet<String>,
930 ) -> Result<()> {
931 visited.insert(tool_name.to_string());
932 stack.insert(tool_name.to_string());
933
934 if let Some(tool) = self.tools.iter().find(|t| t.name() == tool_name) {
935 for dep in tool.dependencies() {
936 if !visited.contains(&dep) {
937 self.check_circular_deps_recursive(&dep, visited, stack)?;
938 } else if stack.contains(&dep) {
939 return Err(anyhow::anyhow!(
940 "Circular dependency detected: {} -> {}",
941 tool_name,
942 dep
943 ));
944 }
945 }
946 }
947
948 stack.remove(tool_name);
949 Ok(())
950 }
951
952 /// Validate tool dependencies
953 fn validate_dependencies(&self) -> Result<()> {
954 let tool_names: HashSet<String> = self.tools.iter().map(|t| t.name().to_string()).collect();
955
956 for tool in &self.tools {
957 for dep in tool.dependencies() {
958 if !tool_names.contains(&dep) {
959 anyhow::bail!(
960 "Tool '{}' requires '{}' but it's not registered",
961 tool.name(),
962 dep
963 );
964 }
965 }
966 }
967
968 Ok(())
969 }
970
971 /// Get execution context
972 pub fn context(&self) -> &ExecutionContext {
973 &self.context
974 }
975
976 /// Get mutable context
977 pub fn context_mut(&mut self) -> &mut ExecutionContext {
978 &mut self.context
979 }
980}
981
982#[cfg(test)]
983mod tests {
984 use super::*;
985
986 struct MockTool {
987 name: String,
988 priority: u32,
989 }
990
991 impl DxTool for MockTool {
992 fn name(&self) -> &str {
993 &self.name
994 }
995
996 fn version(&self) -> &str {
997 "1.0.0"
998 }
999
1000 fn priority(&self) -> u32 {
1001 self.priority
1002 }
1003
1004 fn execute(&mut self, _ctx: &ExecutionContext) -> Result<ToolOutput> {
1005 Ok(ToolOutput::success())
1006 }
1007 }
1008
1009 #[test]
1010 fn test_orchestrator_priority_order() {
1011 let mut orch = Orchestrator::new("/tmp/test").unwrap();
1012
1013 orch.register_tool(Box::new(MockTool {
1014 name: "tool-c".into(),
1015 priority: 30,
1016 }))
1017 .unwrap();
1018 orch.register_tool(Box::new(MockTool {
1019 name: "tool-a".into(),
1020 priority: 10,
1021 }))
1022 .unwrap();
1023 orch.register_tool(Box::new(MockTool {
1024 name: "tool-b".into(),
1025 priority: 20,
1026 }))
1027 .unwrap();
1028
1029 let outputs = orch.execute_all().unwrap();
1030
1031 assert_eq!(outputs.len(), 3);
1032 assert!(outputs.iter().all(|o| o.success));
1033 }
1034}