1use 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#[derive(Clone)]
19pub struct ExecutionContext {
20 pub repo_root: PathBuf,
22
23 pub forge_path: PathBuf,
25
26 pub current_branch: Option<String>,
28
29 pub changed_files: Vec<PathBuf>,
31
32 pub shared_state: Arc<RwLock<HashMap<String, serde_json::Value>>>,
34
35 pub traffic_analyzer: Arc<dyn TrafficAnalyzer + Send + Sync>,
37
38 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 pub fn new(repo_root: PathBuf, forge_path: PathBuf) -> Self {
57 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 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 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 pub fn find_patterns(&self, _pattern: &str) -> Result<Vec<PatternMatch>> {
93 Ok(Vec::new())
95 }
96}
97
98#[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#[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
143pub trait DxTool: Send + Sync {
145 fn name(&self) -> &str;
147
148 fn version(&self) -> &str;
150
151 fn priority(&self) -> u32;
153
154 fn execute(&mut self, context: &ExecutionContext) -> Result<ToolOutput>;
156
157 fn should_run(&self, _context: &ExecutionContext) -> bool {
159 true
160 }
161
162 fn dependencies(&self) -> Vec<String> {
164 Vec::new()
165 }
166
167 fn before_execute(&mut self, _context: &ExecutionContext) -> Result<()> {
169 Ok(())
170 }
171
172 fn after_execute(&mut self, _context: &ExecutionContext, _output: &ToolOutput) -> Result<()> {
174 Ok(())
175 }
176
177 fn on_error(&mut self, _context: &ExecutionContext, _error: &anyhow::Error) -> Result<()> {
179 Ok(())
180 }
181
182 fn timeout_seconds(&self) -> u64 {
184 60
185 }
186}
187
188#[derive(Debug, Clone, PartialEq)]
193pub enum TrafficBranch {
194 Green,
196
197 Yellow { conflicts: Vec<Conflict> },
199
200 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
211pub trait TrafficAnalyzer {
213 fn analyze(&self, file: &Path) -> Result<TrafficBranch>;
214 fn can_auto_merge(&self, conflicts: &[Conflict]) -> bool;
215}
216
217pub struct DefaultTrafficAnalyzer;
219
220impl TrafficAnalyzer for DefaultTrafficAnalyzer {
221 fn analyze(&self, _file: &Path) -> Result<TrafficBranch> {
222 Ok(TrafficBranch::Green)
224 }
225
226 fn can_auto_merge(&self, conflicts: &[Conflict]) -> bool {
227 conflicts.is_empty()
228 }
229}
230
231#[derive(Debug, Clone)]
233pub struct OrchestratorConfig {
234 pub parallel: bool,
236
237 pub fail_fast: bool,
239
240 pub max_concurrent: usize,
242
243 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
258pub struct Orchestrator {
260 tools: Vec<Box<dyn DxTool>>,
261 context: ExecutionContext,
262 config: OrchestratorConfig,
263}
264
265impl Orchestrator {
266 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 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 pub fn set_config(&mut self, config: OrchestratorConfig) {
292 self.config = config;
293 }
294
295 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 pub fn execute_all(&mut self) -> Result<Vec<ToolOutput>> {
310 self.tools.sort_by_key(|t| t.priority());
312
313 self.validate_dependencies()?;
315
316 self.check_circular_dependencies()?;
318
319 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 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 fn execute_tool_with_hooks(tool: &mut Box<dyn DxTool>, context: &ExecutionContext) -> Result<ToolOutput> {
367 let start = std::time::Instant::now();
368
369 tool.before_execute(context)?;
371
372 let result = if tool.timeout_seconds() > 0 {
374 tool.execute(context)
376 } else {
377 tool.execute(context)
378 };
379
380 match result {
382 Ok(mut output) => {
383 output.duration_ms = start.elapsed().as_millis() as u64;
384
385 tool.after_execute(context, &output)?;
387
388 Ok(output)
389 }
390 Err(e) => {
391 tool.on_error(context, &e)?;
393 Err(e)
394 }
395 }
396 }
397
398 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 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 pub fn context(&self) -> &ExecutionContext {
460 &self.context
461 }
462
463 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}