Skip to main content

radiate_gp/collections/graphs/
transaction.rs

1//! Transactional editing for `Graph<T>`.
2//!
3//! A `GraphTransaction` records reversible mutations (add/remove nodes and edges,
4//! direction changes), can mark cycles, and validates the graph on commit. If a commit
5//! fails, the graph is automatically rolled back and "replay" steps are returned so
6//! you can re-apply the same changes later (e.g., after adjustments or in a new context).
7//!
8//! Key features:
9//! - Atomic commit/rollback semantics
10//! - Cycle marking: nodes in detected cycles are marked `Direction::Backward`
11//! - Validation integration via `Valid`
12//! - Deterministic tests via `random_provider::set_seed(...)`
13//! - Repair of invalid nodes (e.g., missing connections) before final validation in `try_commit()`
14//!
15//! Typical flow:
16//! 1) Build with `push(...)`, `attach(...)`, `detach(...)`, `change_direction(...)`
17//! 2) `commit()`, `commit_with(...)`, or `try_commit()` to finalize
18//! 3) On invalid commit, use returned `replay` to re-apply later with `replay(...)`
19use super::{Direction, Graph, GraphNode};
20use crate::{Arity, NodeType, graphs::node::InnovationId, node::Node};
21use radiate_core::{RdRand, Valid, random_provider};
22use radiate_utils::SortedBuffer;
23use std::{fmt::Debug, ops::Deref};
24
25const SOURCE_NODE_TYPES: &[NodeType] = &[NodeType::Input, NodeType::Vertex, NodeType::Edge];
26const TARGET_NODE_TYPES: &[NodeType] = &[NodeType::Output, NodeType::Vertex, NodeType::Edge];
27const MAX_REPAIR_ATTEMPTS: usize = 10;
28const MAX_SOURCE_ATTEMPTS: usize = 10;
29
30/// A single reversible mutation applied during a transaction.
31///
32/// This is a structural log of intent (what you asked to do), used for introspection and
33/// reporting back to callers. Reversal is handled by `rollback()` which produces [ReplayStep]s.
34#[derive(Debug, Clone)]
35pub enum MutationStep {
36    AddNode(usize),
37    AddEdge(usize, usize),
38    RemoveEdge(usize, usize),
39    DirectionChange {
40        index: usize,
41        previous_direction: Direction,
42    },
43    InnovationChange {
44        node_idx: usize,
45        previous_innovation: Option<InnovationId>,
46    },
47}
48
49/// A replayable step produced by `rollback()` to restore the effects that were undone.
50///
51/// Unlike [MutationStep], this is designed for re-applying operational effects on another
52/// transaction via `replay(...)`. For `AddNode`, the index is informational; re-application
53/// uses the provided [GraphNode] if present.
54#[derive(Clone)]
55pub enum ReplayStep<T> {
56    AddNode(usize, Option<GraphNode<T>>),
57    AddEdge(usize, usize),
58    RemoveEdge(usize, usize),
59    DirectionChange(usize, Direction),
60    InnovationChange(usize, Option<InnovationId>),
61}
62
63/// Result of finalizing a transaction.
64///
65/// - `Valid(steps)`: the graph remained valid after cycle marking (and optional custom validation),
66///   and no rollback occurred.
67/// - `Invalid(steps, replay)`: validation failed; the graph was rolled back to its original state,
68///   and `replay` contains steps to re-apply the effects elsewhere or later.
69pub enum TransactionResult<T> {
70    Valid(Vec<MutationStep>),
71    Invalid(Vec<MutationStep>, Vec<ReplayStep<T>>),
72}
73
74impl<T> TransactionResult<T> {
75    pub fn is_valid(&self) -> bool {
76        matches!(self, TransactionResult::Valid(_))
77    }
78
79    pub fn is_invalid(&self) -> bool {
80        matches!(self, TransactionResult::Invalid(_, _))
81    }
82
83    pub fn replay(&self, graph: &mut Graph<T>)
84    where
85        T: Clone,
86    {
87        if let TransactionResult::Invalid(_, replay_steps) = self {
88            let mut transaction = GraphTransaction::new(graph);
89            transaction.replay(replay_steps.clone());
90        }
91    }
92}
93
94/// A declarative plan for inserting a node between two nodes.
95///
96/// Consumers must execute these steps themselves (e.g., with `attach`/`detach`) before committing.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum InsertStep {
99    Detach(usize, usize),
100    Connect(usize, usize),
101    NewStructure(usize, usize, usize, NodeType),
102    Invalid,
103}
104
105/// Tracks reversible changes to a `Graph<T>` and provides commit/rollback.
106///
107/// Usage:
108/// - Mutate via `push(...)`, `attach(...)`, `detach(...)`, `change_direction(...)`.
109/// - Call `commit()`, `commit_with(...)`, or `try_commit()` to finalize.
110/// - On invalid commit, the graph is rolled back and you receive `replay` steps you can pass to
111///   `replay(...)` in a fresh transaction.
112pub struct GraphTransaction<'a, T> {
113    graph: &'a mut Graph<T>,
114    steps: Vec<MutationStep>,
115    effects: SortedBuffer<usize>,
116}
117
118impl<'a, T> GraphTransaction<'a, T> {
119    pub fn new(graph: &'a mut Graph<T>) -> Self {
120        GraphTransaction {
121            graph,
122            steps: Vec::with_capacity(5),
123            effects: SortedBuffer::new(),
124        }
125    }
126
127    /// Finalize the transaction:
128    /// - Marks cycles (`set_cycles()`).
129    /// - Validates with `Graph<T>: Valid`.
130    /// - If valid, returns `Valid(steps)`.
131    /// - If invalid, rolls back and returns `Invalid(steps, replay)`.
132    pub fn commit(self) -> TransactionResult<T> {
133        self.commit_internal::<fn(&Graph<T>) -> bool>(None)
134    }
135
136    /// Like `commit()`, but also requires `validator(&Graph<T>)` to pass.
137    ///
138    /// This is useful for domain-specific acceptance checks in addition to structural validity.
139    pub fn commit_with(self, validator: impl Fn(&Graph<T>) -> bool) -> TransactionResult<T> {
140        self.commit_internal(Some(validator))
141    }
142
143    /// Attempt to commit the transaction, repairing invalid nodes if possible.
144    /// - Calls `repair_invalid_nodes()` up to `MAX_REPAIR_ATTEMPTS` times.
145    ///
146    /// Note: This may produce different graphs on each call due to possible random repairs.
147    pub fn try_commit(mut self) -> TransactionResult<T> {
148        let mut repaired = false;
149        let mut attempts = 0;
150
151        self.set_cycles();
152        while !repaired && attempts < MAX_REPAIR_ATTEMPTS {
153            repaired = self.repair_invalid_nodes();
154            if repaired {
155                self.set_cycles();
156            }
157
158            attempts += 1;
159        }
160
161        self.commit()
162    }
163
164    /// Append a node to the graph and record the change. Returns the new node's index.
165    pub fn push(&mut self, node: impl Into<GraphNode<T>>) -> usize {
166        let index = self.graph.len();
167        self.steps.push(MutationStep::AddNode(index));
168        self.graph.push(node);
169        SortedBuffer::insert_sorted_unique(&mut self.effects, index);
170        index
171    }
172
173    /// Create an edge from `from` to `to` and record the change.
174    pub fn attach(&mut self, from: usize, to: usize) {
175        self.steps.push(MutationStep::AddEdge(from, to));
176        self.graph.attach(from, to);
177        SortedBuffer::insert_sorted_unique(&mut self.effects, from);
178        SortedBuffer::insert_sorted_unique(&mut self.effects, to);
179    }
180
181    /// Remove an edge from `from` to `to` and record the change.
182    pub fn detach(&mut self, from: usize, to: usize) {
183        self.steps.push(MutationStep::RemoveEdge(from, to));
184        self.graph.detach(from, to);
185        SortedBuffer::insert_sorted_unique(&mut self.effects, from);
186        SortedBuffer::insert_sorted_unique(&mut self.effects, to);
187    }
188
189    /// Change the direction of the node at `index` if it differs from its current direction.
190    ///
191    /// Records the previous direction so the change can be rolled back or replayed.
192    pub fn change_direction(&mut self, index: usize, direction: Direction) {
193        if let Some(node) = self.graph.get_mut(index) {
194            if node.direction() == direction {
195                return;
196            }
197
198            self.steps.push(MutationStep::DirectionChange {
199                index,
200                previous_direction: node.direction(),
201            });
202            node.set_direction(direction);
203        }
204    }
205
206    /// Undo all recorded changes in reverse order, mutating the graph back to its original state.
207    ///
208    /// Returns a sequence of `ReplayStep`s that can be fed to `replay(...)` on a new transaction
209    /// to re-apply the same operational effects.
210    pub fn rollback(self) -> Vec<ReplayStep<T>> {
211        let mut replay_steps = Vec::new();
212        for step in self.steps.into_iter().rev() {
213            match step {
214                MutationStep::AddNode(_) => {
215                    let added_node = self.graph.pop();
216                    replay_steps.push(ReplayStep::AddNode(self.graph.len(), added_node));
217                }
218                MutationStep::AddEdge(from, to) => {
219                    self.graph.detach(from, to);
220                    replay_steps.push(ReplayStep::AddEdge(from, to));
221                }
222                MutationStep::RemoveEdge(from, to) => {
223                    self.graph.attach(from, to);
224                    replay_steps.push(ReplayStep::RemoveEdge(from, to));
225                }
226                MutationStep::DirectionChange {
227                    index,
228                    previous_direction,
229                    ..
230                } => {
231                    if let Some(node) = self.graph.get_mut(index) {
232                        let prev_dir = node.direction();
233                        node.set_direction(previous_direction);
234                        replay_steps.push(ReplayStep::DirectionChange(index, prev_dir));
235                    }
236                }
237                MutationStep::InnovationChange {
238                    node_idx,
239                    previous_innovation,
240                } => {
241                    if let Some(node) = self.graph.get_mut(node_idx) {
242                        let current_innovation = node.innovation();
243                        node.set_innovation(previous_innovation);
244                        replay_steps
245                            .push(ReplayStep::InnovationChange(node_idx, current_innovation));
246                    }
247                }
248            }
249        }
250
251        replay_steps.reverse();
252        replay_steps
253    }
254
255    /// Apply `ReplayStep`s (typically from a prior `rollback()`) to this transaction/graph.
256    ///
257    /// Steps are recorded as normal mutations (so they can be committed or rolled back again).
258    pub fn replay(&mut self, steps: Vec<ReplayStep<T>>) {
259        for step in steps {
260            match step {
261                ReplayStep::AddNode(_, node) => {
262                    if let Some(node) = node {
263                        self.push(node);
264                    }
265                }
266                ReplayStep::AddEdge(from, to) => {
267                    self.attach(from, to);
268                }
269                ReplayStep::RemoveEdge(from, to) => {
270                    self.detach(from, to);
271                }
272                ReplayStep::DirectionChange(index, direction) => {
273                    self.change_direction(index, direction);
274                }
275                ReplayStep::InnovationChange(node_idx, innovation) => {
276                    self.set_innovation(node_idx, innovation);
277                }
278            }
279        }
280    }
281
282    /// Mark cycle participation for nodes touched in this transaction:
283    /// - Nodes without cycles are set to `Direction::Forward`.
284    /// - Nodes in cycles (via `get_cycles(idx)`) are set to `Direction::Backward`.
285    pub fn set_cycles(&mut self) {
286        let effects = self.effects.clone();
287
288        for &idx in effects.iter() {
289            let node_cycles = self.graph.get_cycles(idx);
290
291            if node_cycles.is_empty() {
292                self.change_direction(idx, Direction::Forward);
293            } else {
294                for cycle_idx in node_cycles {
295                    self.change_direction(cycle_idx, Direction::Backward);
296                }
297            }
298        }
299    }
300
301    pub fn set_innovation(&mut self, node_idx: usize, innovation: Option<InnovationId>) {
302        if let Some(node) = self.graph.get_mut(node_idx) {
303            let previous_innovation = node.innovation();
304            node.set_innovation(innovation);
305            self.steps.push(MutationStep::InnovationChange {
306                node_idx,
307                previous_innovation,
308            });
309        }
310    }
311
312    /// Compute the steps needed to insert `new_node_idx` between `source_idx` and `target_idx`.
313    ///
314    /// Behavior:
315    /// - If `new_node` has `Arity::Zero` and `target` is not locked, connect `new_node -> target`.
316    /// - If `source` is an `Edge`, re-route its single outgoing through `new_node`.
317    /// - If `target` is an `Edge` or is locked, detach one incoming and rewire via `new_node`.
318    /// - Otherwise connect `source -> new_node -> target`.
319    #[inline]
320    pub fn get_insertion_steps(
321        &self,
322        source_idx: usize,
323        target_idx: usize,
324        new_node_idx: usize,
325        rand: &mut RdRand,
326    ) -> Vec<InsertStep> {
327        let target_node = self.graph.get(target_idx).unwrap();
328        let source_node = self.graph.get(source_idx).unwrap();
329        let new_node = self.graph.get(new_node_idx).unwrap();
330
331        let mut steps = Vec::with_capacity(4);
332
333        let source_is_edge = source_node.node_type() == NodeType::Edge;
334        let target_is_edge = target_node.node_type() == NodeType::Edge;
335        let new_node_arity = new_node.arity();
336
337        if new_node_arity == Arity::Zero && !target_node.is_locked() {
338            steps.push(InsertStep::Connect(new_node_idx, target_idx));
339            return steps;
340        }
341
342        if source_is_edge {
343            let source_outgoing = *rand.choose(source_node.outgoing());
344
345            if source_outgoing == new_node_idx {
346                steps.push(InsertStep::Connect(source_idx, new_node_idx));
347            } else {
348                steps.push(InsertStep::Connect(source_idx, new_node_idx));
349                steps.push(InsertStep::Connect(new_node_idx, source_outgoing));
350                steps.push(InsertStep::Detach(source_idx, source_outgoing));
351                steps.push(InsertStep::NewStructure(
352                    source_idx,
353                    new_node_idx,
354                    source_outgoing,
355                    source_node.node_type(),
356                ));
357            }
358        } else if target_is_edge || target_node.is_locked() {
359            let target_incoming = *rand.choose(target_node.incoming());
360
361            if target_incoming == new_node_idx {
362                steps.push(InsertStep::Connect(target_incoming, new_node_idx));
363            } else {
364                steps.push(InsertStep::Connect(target_incoming, new_node_idx));
365                steps.push(InsertStep::Connect(new_node_idx, target_idx));
366                steps.push(InsertStep::Detach(target_incoming, target_idx));
367                steps.push(InsertStep::NewStructure(
368                    target_incoming,
369                    new_node_idx,
370                    target_idx,
371                    target_node.node_type(),
372                ));
373            }
374        } else {
375            steps.push(InsertStep::Connect(source_idx, new_node_idx));
376            steps.push(InsertStep::Connect(new_node_idx, target_idx));
377            steps.push(InsertStep::NewStructure(
378                source_idx,
379                new_node_idx,
380                target_idx,
381                new_node.node_type(),
382            ));
383        }
384
385        steps
386    }
387
388    /// The below functions are used to get random nodes from the graph. These are useful for
389    /// creating connections between nodes. Neither of these functions will return an edge node.
390    /// This is because edge nodes are not valid source or target nodes for connections as they
391    /// only allow one incoming and one outgoing connection, thus they can't be used to create
392    /// new connections. Instead, edge nodes are used to represent the weights of the connections
393    ///
394    /// Get a random node that can be used as a source node for a connection.
395    /// A source node can be either an input or a vertex node.
396    #[inline]
397    pub fn random_source_node(&self, rand: &mut RdRand) -> Option<&GraphNode<T>> {
398        self.random_node_of_type(SOURCE_NODE_TYPES, rand)
399    }
400
401    /// Get a random node that can be used as a target node for a connection.
402    /// A target node can be either an output or a vertex node.
403    #[inline]
404    pub fn random_target_node(&self, rand: &mut RdRand) -> Option<&GraphNode<T>> {
405        self.random_node_of_type(TARGET_NODE_TYPES, rand)
406    }
407
408    #[inline]
409    pub fn unique_random_source_node(
410        &self,
411        arity: &Arity,
412        rand: &mut RdRand,
413    ) -> Option<Vec<&GraphNode<T>>> {
414        let needed_insertions = match arity {
415            Arity::Exact(n) => *n,
416            _ => 1,
417        };
418
419        if self.graph.len() < needed_insertions {
420            return None;
421        }
422
423        if needed_insertions == 1 {
424            return self.random_source_node(rand).map(|n| vec![n]);
425        }
426
427        let mut sorted = SortedBuffer::new();
428
429        let mut attempts = 0;
430        while sorted.len() < needed_insertions && attempts < MAX_SOURCE_ATTEMPTS {
431            if let Some(node) = self.random_source_node(rand) {
432                SortedBuffer::insert_sorted_unique(&mut sorted, node.index());
433            }
434
435            attempts += 1;
436        }
437
438        Some(
439            sorted
440                .iter()
441                .map(|&idx| &self.graph[idx])
442                .collect::<Vec<&GraphNode<T>>>(),
443        )
444    }
445
446    /// Get a random target node that satisfies the provided filter function.
447    /// This is essentially a filtered version of the above function `random_target_node`.
448    #[inline]
449    pub fn random_target_node_where<F>(&self, rand: &mut RdRand, filter: F) -> Option<&GraphNode<T>>
450    where
451        F: Fn(&GraphNode<T>) -> bool,
452    {
453        let candidates = self
454            .iter()
455            .filter(|node| TARGET_NODE_TYPES.contains(&node.node_type()) && filter(node))
456            .collect::<Vec<&GraphNode<T>>>();
457
458        if candidates.is_empty() {
459            return None;
460        }
461
462        Some(*rand.choose(&candidates))
463    }
464
465    /// Get a random source node that satisfies the provided filter function.
466    /// This is essentially a filtered version of the above function `random_source_node`.
467    #[inline]
468    fn random_source_node_where<F>(&self, rand: &mut RdRand, filter: F) -> Option<&GraphNode<T>>
469    where
470        F: Fn(&GraphNode<T>) -> bool,
471    {
472        let candidates = self
473            .iter()
474            .filter(|node| SOURCE_NODE_TYPES.contains(&node.node_type()) && filter(node))
475            .collect::<Vec<&GraphNode<T>>>();
476
477        if candidates.is_empty() {
478            return None;
479        }
480
481        Some(*rand.choose(&candidates))
482    }
483
484    fn repair_invalid_nodes(&mut self) -> bool {
485        if self.is_valid() {
486            return false;
487        }
488
489        let mut repaired = false;
490
491        let invalid_nodes = self
492            .iter()
493            .filter(|node| !node.is_valid())
494            .map(|n| n.index())
495            .collect::<Vec<usize>>();
496
497        for idx in invalid_nodes.iter() {
498            let arity = self.graph[*idx].arity();
499            match arity {
500                Arity::Zero if self.repair_zero_arity_node(*idx) => {
501                    repaired = true;
502                }
503                Arity::Exact(_) if self.repair_exact_arity_node(*idx) => {
504                    repaired = true;
505                }
506                _ => {}
507            }
508        }
509
510        repaired
511    }
512
513    fn repair_zero_arity_node(&mut self, node_idx: usize) -> bool {
514        let node = self.graph.get(node_idx).unwrap();
515        if node.arity() != Arity::Zero {
516            return false;
517        }
518
519        if node.outgoing().is_empty() {
520            let random_target = random_provider::with_rng(|rand| {
521                self.random_target_node_where(rand, |n| !n.is_locked() && n.index() != node_idx)
522                    .map(|n| n.index())
523            });
524
525            if let Some(target) = random_target {
526                self.attach(node.index(), target);
527
528                if !self.graph[node_idx].outgoing().is_empty() {
529                    return true;
530                }
531            }
532        }
533
534        false
535    }
536
537    fn repair_exact_arity_node(&mut self, node_idx: usize) -> bool {
538        let arity = self.graph[node_idx].arity();
539        if let Arity::Exact(n) = arity {
540            let current_incoming = self.graph[node_idx].incoming().len();
541            if current_incoming < n {
542                let needed = n - current_incoming;
543
544                let available_sources = random_provider::with_rng(|rand| {
545                    (0..needed)
546                        .filter_map(|_| {
547                            self.random_source_node_where(rand, |n| {
548                                !n.is_locked() && n.index() != node_idx
549                            })
550                            .map(|n| n.index())
551                        })
552                        .collect::<Vec<usize>>()
553                });
554
555                for src in available_sources {
556                    self.attach(src, node_idx);
557                }
558
559                if self.graph[node_idx].incoming().len() == n {
560                    return true;
561                }
562            } else if current_incoming > n {
563                let to_detach = current_incoming - n;
564
565                let valid_incoming = self.graph[node_idx]
566                    .incoming()
567                    .iter()
568                    .cloned()
569                    .filter(|incoming| self.graph[*incoming].outgoing().len() > 1)
570                    .collect::<Vec<usize>>();
571
572                let rand_indices = random_provider::shuffled_indices(0..valid_incoming.len());
573                let rand_indices = &rand_indices[0..to_detach];
574
575                for &i in rand_indices.iter() {
576                    let source_idx = valid_incoming[i];
577                    self.detach(source_idx, node_idx);
578                }
579
580                if self.graph[node_idx].incoming().len() == n {
581                    return true;
582                }
583            }
584        }
585
586        false
587    }
588
589    /// Helper functions to get a random node of the specified type. If no nodes of the specified
590    /// type are found, the function will try to get a random node of a different type.
591    /// If no nodes are found, the function will panic.
592    #[inline]
593    fn random_node_of_type(
594        &self,
595        node_types: &[NodeType],
596        rand: &mut RdRand,
597    ) -> Option<&GraphNode<T>> {
598        if node_types.is_empty() {
599            return None;
600        }
601
602        let gene_node_type = rand.choose(node_types);
603
604        let genes = match gene_node_type {
605            NodeType::Input => self
606                .iter()
607                .filter(|node| node.node_type() == NodeType::Input)
608                .collect::<Vec<&GraphNode<T>>>(),
609            NodeType::Output => self
610                .iter()
611                .filter(|node| node.node_type() == NodeType::Output)
612                .collect::<Vec<&GraphNode<T>>>(),
613            NodeType::Vertex => self
614                .iter()
615                .filter(|node| node.node_type() == NodeType::Vertex)
616                .collect::<Vec<&GraphNode<T>>>(),
617            NodeType::Edge => self
618                .iter()
619                .filter(|node| node.node_type() == NodeType::Edge)
620                .collect::<Vec<&GraphNode<T>>>(),
621            _ => vec![],
622        };
623
624        if genes.is_empty() {
625            return self.random_node_of_type(
626                node_types
627                    .iter()
628                    .filter(|nt| *nt != gene_node_type)
629                    .cloned()
630                    .collect::<Vec<NodeType>>()
631                    .as_slice(),
632                rand,
633            );
634        }
635
636        Some(*rand.choose(&genes))
637    }
638
639    fn commit_internal<F: Fn(&Graph<T>) -> bool>(
640        mut self,
641        validator: Option<F>,
642    ) -> TransactionResult<T> {
643        self.set_cycles();
644        let result_steps = self.steps.iter().map(|step| (*step).clone()).collect();
645
646        if let Some(validator) = validator {
647            return if validator(self.graph) && self.is_valid() {
648                TransactionResult::Valid(result_steps)
649            } else {
650                let replay_steps = self.rollback();
651                TransactionResult::Invalid(result_steps, replay_steps)
652            };
653        }
654
655        if self.is_valid() {
656            TransactionResult::Valid(result_steps)
657        } else {
658            let replay_steps = self.rollback();
659            TransactionResult::Invalid(result_steps, replay_steps)
660        }
661    }
662}
663
664impl<T> Deref for GraphTransaction<'_, T> {
665    type Target = Graph<T>;
666
667    fn deref(&self) -> &Self::Target {
668        self.graph
669    }
670}
671
672#[cfg(test)]
673mod tests {
674    use super::{GraphTransaction, InsertStep, MutationStep, TransactionResult};
675    use crate::collections::graphs::{Direction, Graph, GraphNode, InnovationId};
676    use crate::{Arity, Node, NodeType};
677    use radiate_core::{Valid, random_provider};
678
679    fn assert_has_direction_change(steps: &[MutationStep], idxs: &[usize]) {
680        let mut seen = vec![];
681        for s in steps {
682            if let MutationStep::DirectionChange { index, .. } = s {
683                seen.push(*index);
684            }
685        }
686        for idx in idxs {
687            assert!(
688                seen.contains(idx),
689                "Expected DirectionChange for node {} not found in steps: {:?}",
690                idx,
691                steps
692            );
693        }
694    }
695
696    #[test]
697    fn commit_valid_add_and_attach() {
698        let mut g = Graph::<i32>::default();
699        let mut tx = GraphTransaction::new(&mut g);
700
701        let i = tx.push((0, NodeType::Input, 0));
702        let o = tx.push((1, NodeType::Output, 1));
703        tx.attach(i, o);
704
705        match tx.commit() {
706            TransactionResult::Valid(steps) => {
707                assert_eq!(steps.len(), 3);
708                assert!(matches!(steps[0], MutationStep::AddNode(0)));
709                assert!(matches!(steps[1], MutationStep::AddNode(1)));
710                assert!(matches!(steps[2], MutationStep::AddEdge(0, 1)));
711                assert!(g.is_valid());
712                assert_eq!(g[0].outgoing().len(), 1);
713                assert_eq!(g[1].incoming().len(), 1);
714                assert_eq!(g[0].direction(), Direction::Forward);
715                assert_eq!(g[1].direction(), Direction::Forward);
716            }
717            _ => panic!("expected Valid"),
718        }
719    }
720
721    #[test]
722    fn commit_invalid_rolls_back_and_replay_restores() {
723        let mut g = Graph::<i32>::default();
724
725        // Build: Input -> Vertex(arity=2) -> Output (invalid: vertex missing one incoming)
726        let mut tx = GraphTransaction::new(&mut g);
727        let input = tx.push((0, NodeType::Input, 0));
728        let vertex = tx.push((1, NodeType::Vertex, 1, Arity::Exact(2)));
729        let output = tx.push((2, NodeType::Output, 2));
730
731        tx.attach(input, vertex);
732        tx.attach(vertex, output);
733
734        let (steps, replay) = match tx.commit() {
735            TransactionResult::Invalid(steps, replay) => (steps, replay),
736            _ => panic!("expected Invalid"),
737        };
738
739        // Graph must be rolled back to original state (empty)
740        assert_eq!(g.len(), 0, "graph should be rolled back to empty");
741        assert!(g.is_valid());
742
743        // Reapply the changes using replay steps
744        let mut tx2 = GraphTransaction::new(&mut g);
745        tx2.replay(replay);
746
747        assert_eq!(g.len(), 3);
748        assert_eq!(g[0].node_type(), NodeType::Input);
749        assert_eq!(g[1].node_type(), NodeType::Vertex);
750        assert_eq!(g[2].node_type(), NodeType::Output);
751        assert!(g[0].outgoing().contains(&1));
752        assert!(g[1].incoming().contains(&0));
753        assert!(g[1].outgoing().contains(&2));
754        assert!(g[2].incoming().contains(&1));
755
756        // Sanity: original mutation steps captured structure we tried
757        assert!(steps.iter().any(|s| matches!(s, MutationStep::AddNode(0))));
758        assert!(steps.iter().any(|s| matches!(s, MutationStep::AddNode(1))));
759        assert!(steps.iter().any(|s| matches!(s, MutationStep::AddNode(2))));
760        assert!(
761            steps
762                .iter()
763                .any(|s| matches!(s, MutationStep::AddEdge(0, 1)))
764        );
765        assert!(
766            steps
767                .iter()
768                .any(|s| matches!(s, MutationStep::AddEdge(1, 2)))
769        );
770    }
771
772    #[test]
773    fn commit_sets_cycles_and_marks_backward() {
774        let mut g = Graph::<i32>::default();
775        let mut tx = GraphTransaction::new(&mut g);
776
777        let a = tx.push((0, NodeType::Vertex, 10));
778        let b = tx.push((1, NodeType::Vertex, 20));
779        tx.attach(a, b);
780        tx.attach(b, a); // creates cycle {0,1}
781
782        match tx.commit() {
783            TransactionResult::Valid(steps) => {
784                assert!(g.is_valid());
785                // Both nodes in the cycle should be marked Backward
786                assert_eq!(g[0].direction(), Direction::Backward);
787                assert_eq!(g[1].direction(), Direction::Backward);
788                // And the mutation steps should include direction changes
789                assert_has_direction_change(&steps, &[0, 1]);
790            }
791            _ => panic!("expected Valid"),
792        }
793    }
794
795    #[test]
796    fn insertion_steps_new_zero_arity_connects_to_target_when_unlocked() {
797        let mut g = Graph::<i32>::default();
798        let mut tx = GraphTransaction::new(&mut g);
799
800        let src = tx.push((0, NodeType::Input, 0));
801        let tgt = tx.push((1, NodeType::Vertex, 1)); // Arity::Any => not locked
802        let newn = tx.push((2, NodeType::Input, 2)); // Arity::Zero
803
804        let steps = random_provider::with_rng(|r| tx.get_insertion_steps(src, tgt, newn, r));
805        assert_eq!(steps, vec![InsertStep::Connect(newn, tgt)]);
806    }
807
808    #[test]
809    fn insertion_steps_source_is_edge_with_single_outgoing_equal_new() {
810        let mut g = Graph::<i32>::default();
811        let mut tx = GraphTransaction::new(&mut g);
812
813        // source edge with outgoing already pointing to new node
814        let source = tx
815            .push(GraphNode::with_arity(0, NodeType::Edge, 0, Arity::Exact(1)).with_outgoing([2]));
816        let target = tx.push((1, NodeType::Vertex, 1));
817        let newn = tx.push((2, NodeType::Vertex, 2));
818
819        let steps = random_provider::with_rng(|r| tx.get_insertion_steps(source, target, newn, r));
820        assert_eq!(steps, vec![InsertStep::Connect(source, newn)]);
821    }
822
823    #[test]
824    fn insertion_steps_source_is_edge_redirects_through_new() {
825        let mut g = Graph::<i32>::default();
826        let mut tx = GraphTransaction::new(&mut g);
827
828        // source edge with single outgoing to target (not new)
829        let source = tx
830            .push(GraphNode::with_arity(0, NodeType::Edge, 0, Arity::Exact(1)).with_outgoing([1]));
831        let target = tx.push((1, NodeType::Vertex, 1));
832        let newn = tx.push((2, NodeType::Vertex, 2));
833
834        let steps = random_provider::with_rng(|r| tx.get_insertion_steps(source, target, newn, r));
835        assert_eq!(
836            steps[..3],
837            vec![
838                InsertStep::Connect(source, newn),
839                InsertStep::Connect(newn, target),
840                InsertStep::Detach(source, target),
841            ]
842        );
843    }
844
845    #[test]
846    fn insertion_steps_target_locked_prefers_detach_rewire() {
847        let mut g = Graph::<i32>::default();
848        let mut tx = GraphTransaction::new(&mut g);
849
850        // target is "locked": Arity::Exact(1) with exactly one incoming; ensure not an Edge type
851        // by keeping outgoing empty.
852        let source = tx.push((0, NodeType::Vertex, 0));
853        let target = tx.push(
854            GraphNode::with_arity(1, NodeType::Vertex, 1, Arity::Exact(1)).with_incoming([0]),
855        );
856        let newn = tx.push((2, NodeType::Vertex, 2));
857
858        let steps = random_provider::with_rng(|r| tx.get_insertion_steps(source, target, newn, r));
859        assert_eq!(
860            steps[..3],
861            vec![
862                InsertStep::Connect(source, newn),
863                InsertStep::Connect(newn, target),
864                InsertStep::Detach(source, target),
865            ]
866        );
867    }
868
869    #[test]
870    fn random_node_helpers_can_return_edges_when_only_edges_exist() {
871        random_provider::seed(1337);
872        random_provider::with_rng(|rand| {
873            let mut g = Graph::<i32>::default();
874            let mut tx = GraphTransaction::new(&mut g);
875
876            // Only edge nodes exist; helpers should still return something (and it will be an Edge).
877            tx.push((0, NodeType::Edge, 0, Arity::Exact(1)));
878            tx.push((1, NodeType::Edge, 1, Arity::Exact(1)));
879
880            let src = tx.random_source_node(rand).unwrap();
881            let tgt = tx.random_target_node(rand).unwrap();
882
883            assert_eq!(src.node_type(), NodeType::Edge);
884            assert_eq!(tgt.node_type(), NodeType::Edge);
885        });
886    }
887
888    #[test]
889    fn rollback_restores_previous_innovation_and_replay_reapplies() {
890        let mut g = Graph::<i32>::default();
891        let initial = InnovationId::new();
892        let updated = InnovationId::new();
893
894        {
895            let mut tx = GraphTransaction::new(&mut g);
896            let input = tx.push((0, NodeType::Input, 0));
897            let output = tx.push((1, NodeType::Output, 1));
898            tx.attach(input, output);
899            tx.set_innovation(input, Some(initial));
900            assert!(matches!(tx.commit(), TransactionResult::Valid(_)));
901        }
902        assert_eq!(g[0].innovation(), Some(initial));
903
904        let replay = {
905            let mut tx = GraphTransaction::new(&mut g);
906            tx.set_innovation(0, Some(updated));
907            match tx.commit_with(|_| false) {
908                TransactionResult::Invalid(_, replay) => replay,
909                _ => panic!("expected forced rejection"),
910            }
911        };
912
913        assert_eq!(
914            g[0].innovation(),
915            Some(initial),
916            "rollback should restore previous innovation, not clear it"
917        );
918
919        {
920            let mut tx = GraphTransaction::new(&mut g);
921            tx.replay(replay);
922            assert!(matches!(tx.commit(), TransactionResult::Valid(_)));
923        }
924        assert_eq!(
925            g[0].innovation(),
926            Some(updated),
927            "replay should reapply the innovation change"
928        );
929    }
930}