Skip to main content

ryo_executor/executor/
blueprint.rs

1//! ParallelBlueprint: Execution plan with dependency graph and conflict detection
2//!
3//! ```text
4//! Vec<MutationSpec> + DependencyGraph → ParallelBlueprint
5//!                                            │
6//!                    ┌───────────────────────┴───────────────────────┐
7//!                    ↓                                               ↓
8//!          [conflicts.is_empty()]                         [conflicts detected]
9//!                    ↓                                               ↓
10//!           Autonomous Execution                      Escalate to High-level LLM
11//! ```
12
13use super::conflict;
14use super::spec::MutationSpec;
15use std::collections::{HashMap, HashSet};
16
17/// Parallel execution blueprint
18#[derive(Debug, Clone)]
19pub struct ParallelBlueprint {
20    /// Mutation specifications to execute
21    pub mutations: Vec<MutationSpec>,
22
23    /// Dependency graph for ordering
24    pub deps: DependencyGraph,
25
26    /// Conflicts detected at plan time
27    pub conflicts: Vec<Conflict>,
28}
29
30impl ParallelBlueprint {
31    /// Create a new empty blueprint
32    pub fn new() -> Self {
33        Self {
34            mutations: vec![],
35            deps: DependencyGraph::new(),
36            conflicts: vec![],
37        }
38    }
39
40    /// Create blueprint from mutations (auto-detects conflicts)
41    pub fn from_mutations(mutations: Vec<MutationSpec>) -> Self {
42        let mut builder = BlueprintBuilder::new();
43        for spec in mutations {
44            builder.add(spec);
45        }
46        builder.build()
47    }
48
49    /// Check if escalation to high-level LLM is needed
50    pub fn needs_escalation(&self) -> bool {
51        !self.conflicts.is_empty()
52    }
53
54    /// Get mutations ready to execute (all dependencies satisfied)
55    pub fn ready_set(&self, completed: &HashSet<usize>) -> Vec<usize> {
56        self.deps.ready_set(self.mutations.len(), completed)
57    }
58
59    /// Calculate parallelism factor (1.0 = sequential, N = N parallel tracks)
60    pub fn parallelism(&self) -> f64 {
61        if self.mutations.is_empty() {
62            return 1.0;
63        }
64
65        let critical_path = self.deps.critical_path_length(self.mutations.len());
66        if critical_path == 0 {
67            self.mutations.len() as f64
68        } else {
69            self.mutations.len() as f64 / critical_path as f64
70        }
71    }
72
73    /// Get topological levels for parallel execution
74    pub fn topological_levels(&self) -> Vec<Vec<usize>> {
75        self.deps.topological_levels(self.mutations.len())
76    }
77}
78
79impl Default for ParallelBlueprint {
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85/// Dependency graph for mutation ordering
86#[derive(Debug, Clone, Default)]
87pub struct DependencyGraph {
88    /// Forward edges: idx → Vec<depends_on_idx>
89    edges: HashMap<usize, Vec<usize>>,
90}
91
92impl DependencyGraph {
93    pub fn new() -> Self {
94        Self::default()
95    }
96
97    /// Add dependency: `from` depends on `to` (to must complete before from)
98    pub fn add_dependency(&mut self, from: usize, to: usize) {
99        self.edges.entry(from).or_default().push(to);
100    }
101
102    /// Get all dependencies for a mutation
103    pub fn dependencies_of(&self, idx: usize) -> &[usize] {
104        self.edges.get(&idx).map(|v| v.as_slice()).unwrap_or(&[])
105    }
106
107    /// Check if mutation is ready (all dependencies completed)
108    pub fn is_ready(&self, idx: usize, completed: &HashSet<usize>) -> bool {
109        self.dependencies_of(idx)
110            .iter()
111            .all(|dep| completed.contains(dep))
112    }
113
114    /// Get all mutations ready to execute
115    pub fn ready_set(&self, total: usize, completed: &HashSet<usize>) -> Vec<usize> {
116        (0..total)
117            .filter(|idx| !completed.contains(idx) && self.is_ready(*idx, completed))
118            .collect()
119    }
120
121    /// Check if there's an ordering between any pair of indices
122    pub fn has_ordering(&self, indices: &[usize]) -> bool {
123        for &a in indices {
124            for &b in indices {
125                if a != b && self.depends_on(a, b) {
126                    return true;
127                }
128            }
129        }
130        false
131    }
132
133    /// Check if `a` depends on `b` (directly or transitively)
134    pub fn depends_on(&self, a: usize, b: usize) -> bool {
135        let mut visited = HashSet::new();
136        self.depends_on_recursive(a, b, &mut visited)
137    }
138
139    fn depends_on_recursive(&self, a: usize, b: usize, visited: &mut HashSet<usize>) -> bool {
140        if visited.contains(&a) {
141            return false;
142        }
143        visited.insert(a);
144
145        for &dep in self.dependencies_of(a) {
146            if dep == b || self.depends_on_recursive(dep, b, visited) {
147                return true;
148            }
149        }
150        false
151    }
152
153    /// Calculate critical path length
154    pub fn critical_path_length(&self, total: usize) -> usize {
155        let mut memo: HashMap<usize, usize> = HashMap::new();
156
157        fn dfs(idx: usize, graph: &DependencyGraph, memo: &mut HashMap<usize, usize>) -> usize {
158            if let Some(&cached) = memo.get(&idx) {
159                return cached;
160            }
161
162            let max_dep = graph
163                .dependencies_of(idx)
164                .iter()
165                .map(|&dep| dfs(dep, graph, memo))
166                .max()
167                .unwrap_or(0);
168
169            let length = max_dep + 1;
170            memo.insert(idx, length);
171            length
172        }
173
174        (0..total)
175            .map(|idx| dfs(idx, self, &mut memo))
176            .max()
177            .unwrap_or(0)
178    }
179
180    /// Get topological levels (mutations in same level can run in parallel)
181    pub fn topological_levels(&self, total: usize) -> Vec<Vec<usize>> {
182        let mut levels: Vec<Vec<usize>> = vec![];
183        let mut completed: HashSet<usize> = HashSet::new();
184
185        while completed.len() < total {
186            let ready = self.ready_set(total, &completed);
187            if ready.is_empty() {
188                // Cycle detected or all done
189                break;
190            }
191
192            for &idx in &ready {
193                completed.insert(idx);
194            }
195            levels.push(ready);
196        }
197
198        levels
199    }
200}
201
202/// Conflict detected during blueprint planning
203#[derive(Debug, Clone)]
204pub struct Conflict {
205    /// Type of conflict
206    pub kind: ConflictKind,
207
208    /// Indices of involved mutations
209    pub involved: Vec<usize>,
210
211    /// Human-readable question for escalation
212    pub question: String,
213}
214
215/// Types of conflicts
216#[derive(Debug, Clone)]
217pub enum ConflictKind {
218    /// Same target modified by multiple mutations
219    SameTarget { target: String },
220
221    /// Order changes semantics
222    OrderDependent { reason: String },
223
224    /// One mutation deletes what another references
225    DeleteReference {
226        deleted: String,
227        referenced_by: String,
228    },
229}
230
231/// Builder for creating ParallelBlueprint
232#[derive(Debug, Default)]
233pub struct BlueprintBuilder {
234    specs: Vec<MutationSpec>,
235    deps: DependencyGraph,
236}
237
238impl BlueprintBuilder {
239    pub fn new() -> Self {
240        Self::default()
241    }
242
243    /// Add a mutation spec
244    pub fn add(&mut self, spec: MutationSpec) -> &mut Self {
245        self.specs.push(spec);
246        self
247    }
248
249    /// Add dependency between mutations
250    pub fn add_dependency(&mut self, from: usize, to: usize) -> &mut Self {
251        self.deps.add_dependency(from, to);
252        self
253    }
254
255    /// Build the blueprint with conflict detection
256    pub fn build(self) -> ParallelBlueprint {
257        let conflicts = detect_conflicts(&self.specs, &self.deps);
258
259        ParallelBlueprint {
260            mutations: self.specs,
261            deps: self.deps,
262            conflicts,
263        }
264    }
265}
266
267/// Detect conflicts in mutation list using conflict::specs_conflict
268fn detect_conflicts(specs: &[MutationSpec], deps: &DependencyGraph) -> Vec<Conflict> {
269    let mut conflicts = vec![];
270
271    // Use conflict::find_conflicting_pairs for pair-wise conflict detection
272    let conflicting_pairs = conflict::find_conflicting_pairs(specs);
273
274    for (i, j) in conflicting_pairs {
275        // Skip if dependency ordering exists
276        if deps.has_ordering(&[i, j]) {
277            continue;
278        }
279
280        // Create conflict record
281        conflicts.push(Conflict {
282            kind: ConflictKind::SameTarget {
283                target: format!("spec[{}] vs spec[{}]", i, j),
284            },
285            involved: vec![i, j],
286            question: format!(
287                "Specs {} and {} conflict without defined order. Which should be applied first?",
288                i, j
289            ),
290        });
291    }
292
293    conflicts
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use crate::executor::spec::{MutationTargetSymbol, Scope};
300    use ryo_symbol::SymbolId;
301
302    /// Create a dummy SymbolId for testing
303    fn dummy_id(index: u32) -> SymbolId {
304        SymbolId::parse(&format!("{}v1", index)).expect("valid dummy id")
305    }
306
307    #[test]
308    fn test_dependency_graph_ready_set() {
309        let mut deps = DependencyGraph::new();
310        // 0 depends on nothing
311        // 1 depends on 0
312        // 2 depends on nothing
313        deps.add_dependency(1, 0);
314
315        let completed = HashSet::new();
316        let ready = deps.ready_set(3, &completed);
317        assert!(ready.contains(&0));
318        assert!(!ready.contains(&1)); // depends on 0
319        assert!(ready.contains(&2));
320
321        let mut completed = HashSet::new();
322        completed.insert(0);
323        let ready = deps.ready_set(3, &completed);
324        assert!(ready.contains(&1)); // now ready
325        assert!(ready.contains(&2));
326    }
327
328    #[test]
329    fn test_topological_levels() {
330        let mut deps = DependencyGraph::new();
331        // Level 0: 0, 2
332        // Level 1: 1 (depends on 0)
333        // Level 2: 3 (depends on 1)
334        deps.add_dependency(1, 0);
335        deps.add_dependency(3, 1);
336
337        let levels = deps.topological_levels(4);
338        assert_eq!(levels.len(), 3);
339        assert!(levels[0].contains(&0));
340        assert!(levels[0].contains(&2));
341        assert!(levels[1].contains(&1));
342        assert!(levels[2].contains(&3));
343    }
344
345    #[test]
346    fn test_same_target_conflict_detection() {
347        let specs = vec![
348            MutationSpec::ChangeVisibility {
349                target: MutationTargetSymbol::ById(dummy_id(1)),
350                visibility: crate::executor::spec::Visibility::Pub,
351            },
352            MutationSpec::AddDerive {
353                target: MutationTargetSymbol::ById(dummy_id(1)),
354                derives: vec!["Debug".to_string()],
355            },
356        ];
357
358        let blueprint = ParallelBlueprint::from_mutations(specs);
359        // Both target "Config" without ordering - should conflict
360        assert!(!blueprint.conflicts.is_empty());
361    }
362
363    #[test]
364    fn test_rename_chain_conflict() {
365        use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
366
367        // Create dummy symbol IDs for testing
368        let mut registry = SymbolRegistry::new();
369        let path_a = SymbolPath::parse("test_crate::A").unwrap();
370        let symbol_a = registry.register(path_a, SymbolKind::Struct).unwrap();
371
372        let specs = vec![
373            MutationSpec::Rename {
374                target: MutationTargetSymbol::ById(symbol_a),
375                to: "B".to_string(),
376                scope: Scope::Project,
377            },
378            MutationSpec::Rename {
379                target: MutationTargetSymbol::ById(symbol_a),
380                to: "C".to_string(),
381                scope: Scope::Project,
382            },
383        ];
384
385        let blueprint = ParallelBlueprint::from_mutations(specs);
386        // Two renames on same target should conflict
387        assert!(
388            !blueprint.conflicts.is_empty(),
389            "Expected conflict for two renames on same target"
390        );
391    }
392
393    #[test]
394    fn test_no_conflict_with_ordering() {
395        use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
396
397        // Create dummy symbol IDs for testing
398        let mut registry = SymbolRegistry::new();
399        let path_a = SymbolPath::parse("test_crate::A").unwrap();
400        let path_b = SymbolPath::parse("test_crate::B").unwrap();
401        let symbol_a = registry.register(path_a, SymbolKind::Struct).unwrap();
402        let _symbol_b = registry.register(path_b, SymbolKind::Struct).unwrap();
403
404        let mut builder = BlueprintBuilder::new();
405        builder.add(MutationSpec::Rename {
406            target: MutationTargetSymbol::ById(symbol_a),
407            to: "B".to_string(),
408            scope: Scope::Project,
409        });
410        builder.add(MutationSpec::Rename {
411            target: MutationTargetSymbol::ById(symbol_a),
412            to: "C".to_string(),
413            scope: Scope::Project,
414        });
415        // Explicit ordering: 1 depends on 0
416        builder.add_dependency(1, 0);
417
418        let blueprint = builder.build();
419        // With ordering, no conflict
420        assert!(blueprint.conflicts.is_empty());
421    }
422
423    #[test]
424    fn test_delete_reference_conflict() {
425        use ryo_source::ItemKind;
426
427        let specs = vec![
428            MutationSpec::RemoveItem {
429                target: MutationTargetSymbol::ById(dummy_id(1)),
430                item_kind: ItemKind::Struct,
431            },
432            MutationSpec::AddDerive {
433                target: MutationTargetSymbol::ById(dummy_id(1)),
434                derives: vec!["Debug".to_string()],
435            },
436        ];
437
438        let blueprint = ParallelBlueprint::from_mutations(specs);
439        // RemoveItem + AddDerive on same target should conflict
440        assert!(
441            !blueprint.conflicts.is_empty(),
442            "Expected conflict: RemoveItem + AddDerive on same target"
443        );
444    }
445
446    #[test]
447    fn test_visibility_conflict() {
448        use crate::executor::spec::Visibility;
449
450        let specs = vec![
451            MutationSpec::ChangeVisibility {
452                target: MutationTargetSymbol::ById(dummy_id(1)),
453                visibility: Visibility::Pub,
454            },
455            MutationSpec::ChangeVisibility {
456                target: MutationTargetSymbol::ById(dummy_id(1)),
457                visibility: Visibility::PubCrate,
458            },
459        ];
460
461        let blueprint = ParallelBlueprint::from_mutations(specs);
462        // Both try to change Config's visibility to different values
463        assert!(!blueprint.conflicts.is_empty());
464    }
465
466    #[test]
467    fn test_visibility_no_conflict_same_value() {
468        use crate::executor::spec::Visibility;
469
470        let specs = vec![
471            MutationSpec::ChangeVisibility {
472                target: MutationTargetSymbol::ById(dummy_id(1)),
473                visibility: Visibility::Pub,
474            },
475            MutationSpec::ChangeVisibility {
476                target: MutationTargetSymbol::ById(dummy_id(1)),
477                visibility: Visibility::Pub,
478            },
479        ];
480
481        let blueprint = ParallelBlueprint::from_mutations(specs);
482        // Same visibility - no conflict (idempotent)
483        let vis_conflicts: Vec<_> = blueprint
484            .conflicts
485            .iter()
486            .filter(|c| {
487                matches!(c.kind, ConflictKind::SameTarget { ref target } if target == "Config")
488                    && c.question.contains("visibility")
489            })
490            .collect();
491        assert!(vis_conflicts.is_empty());
492    }
493}