Skip to main content

nika_engine/dag/
stable.rs

1//! StableDag - petgraph::StableGraph wrapper for stable NodeIndex
2//!
3//! Provides stable NodeIndex values that persist after node deletion.
4//! Critical for @mention references where deleted messages should not
5//! invalidate existing references.
6//!
7//! # Why StableGraph?
8//!
9//! ```text
10//! DiGraph (before):
11//! ├── Node 0: "msg-001" (index=0)
12//! ├── Node 1: "msg-002" (index=1)  ← DELETE THIS
13//! └── Node 2: "msg-003" (index=2)  ← BECOMES index=1 ⚠️
14//!
15//! StableGraph:
16//! ├── Node 0: "msg-001" (index=0)
17//! ├── Node 1: DELETED
18//! └── Node 2: "msg-003" (index=2)  ← STAYS index=2 ✅
19//! ```
20
21use petgraph::stable_graph::{EdgeIndex, NodeIndex, StableGraph};
22use petgraph::Directed;
23use serde::{Deserialize, Serialize};
24
25/// Edge weight for flow dependencies.
26#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
27pub struct DagEdge {
28    /// Optional edge label for debugging
29    pub label: Option<String>,
30}
31
32/// Wrapper around petgraph::StableGraph for DAG operations.
33/// NodeIndex values remain stable after node deletion.
34///
35/// # Serialization Warning
36///
37/// While this struct derives Serialize/Deserialize via petgraph's `serde-1` feature,
38/// **NodeIndex values serialize as raw u32 integers**. These are NOT stable across:
39/// - Graph modifications (add/remove nodes)
40/// - Serialization roundtrips after deletion
41///
42/// For persistence, use stable identifiers (e.g., message IDs) instead of NodeIndex.
43/// The `index_map: HashMap<MessageId, NodeIndex>` pattern is recommended.
44#[derive(Debug, Clone)]
45pub struct StableDag<N> {
46    inner: StableGraph<N, DagEdge, Directed>,
47}
48
49// Manual Serialize impl to avoid lifetime issues with derive
50impl<N: Serialize> Serialize for StableDag<N> {
51    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
52    where
53        S: serde::Serializer,
54    {
55        self.inner.serialize(serializer)
56    }
57}
58
59// Manual Deserialize impl
60impl<'de, N: Deserialize<'de>> Deserialize<'de> for StableDag<N> {
61    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
62    where
63        D: serde::Deserializer<'de>,
64    {
65        let inner = StableGraph::<N, DagEdge, Directed>::deserialize(deserializer)?;
66        Ok(Self { inner })
67    }
68}
69
70impl<N> StableDag<N> {
71    /// Create an empty StableDag.
72    pub fn new() -> Self {
73        Self {
74            inner: StableGraph::new(),
75        }
76    }
77
78    /// Create with pre-allocated capacity for nodes and edges.
79    pub fn with_capacity(nodes: usize, edges: usize) -> Self {
80        Self {
81            inner: StableGraph::with_capacity(nodes, edges),
82        }
83    }
84
85    /// Get the number of nodes in the graph.
86    #[inline]
87    pub fn node_count(&self) -> usize {
88        self.inner.node_count()
89    }
90
91    /// Get the number of edges in the graph.
92    #[inline]
93    pub fn edge_count(&self) -> usize {
94        self.inner.edge_count()
95    }
96
97    // ═══════════════════════════════════════════════════════════════════════
98    // Node operations (Task 0.2)
99    // ═══════════════════════════════════════════════════════════════════════
100
101    /// Add a node and return its stable index.
102    /// The index remains valid even after other nodes are removed.
103    pub fn add_node(&mut self, weight: N) -> NodeIndex {
104        self.inner.add_node(weight)
105    }
106
107    /// Get node weight by index.
108    pub fn node_weight(&self, idx: NodeIndex) -> Option<&N> {
109        self.inner.node_weight(idx)
110    }
111
112    /// Get mutable node weight by index.
113    pub fn node_weight_mut(&mut self, idx: NodeIndex) -> Option<&mut N> {
114        self.inner.node_weight_mut(idx)
115    }
116
117    // ═══════════════════════════════════════════════════════════════════════
118    // Node removal (Task 0.3) — KEY: Other indices remain stable
119    // ═══════════════════════════════════════════════════════════════════════
120
121    /// Remove a node by index.
122    /// Returns the node weight if it existed.
123    /// **Other node indices remain stable** — this is the key invariant.
124    pub fn remove_node(&mut self, idx: NodeIndex) -> Option<N> {
125        self.inner.remove_node(idx)
126    }
127
128    /// Check if a node exists.
129    pub fn contains_node(&self, idx: NodeIndex) -> bool {
130        self.inner.contains_node(idx)
131    }
132
133    // ═══════════════════════════════════════════════════════════════════════
134    // Edge operations (Task 0.4)
135    // ═══════════════════════════════════════════════════════════════════════
136
137    /// Add a directed edge from `a` to `b`.
138    /// Returns None if either node doesn't exist.
139    pub fn add_edge(&mut self, a: NodeIndex, b: NodeIndex) -> Option<EdgeIndex> {
140        if !self.contains_node(a) || !self.contains_node(b) {
141            return None;
142        }
143        Some(self.inner.add_edge(a, b, DagEdge::default()))
144    }
145
146    /// Add a directed edge with a label.
147    pub fn add_edge_with_label(
148        &mut self,
149        a: NodeIndex,
150        b: NodeIndex,
151        label: impl Into<String>,
152    ) -> Option<EdgeIndex> {
153        if !self.contains_node(a) || !self.contains_node(b) {
154            return None;
155        }
156        Some(self.inner.add_edge(
157            a,
158            b,
159            DagEdge {
160                label: Some(label.into()),
161            },
162        ))
163    }
164
165    /// Check if an edge exists from `a` to `b`.
166    pub fn has_edge(&self, a: NodeIndex, b: NodeIndex) -> bool {
167        self.inner.find_edge(a, b).is_some()
168    }
169
170    /// Remove an edge between nodes.
171    /// Returns true if an edge was removed.
172    pub fn remove_edge(&mut self, a: NodeIndex, b: NodeIndex) -> bool {
173        if let Some(edge) = self.inner.find_edge(a, b) {
174            self.inner.remove_edge(edge);
175            true
176        } else {
177            false
178        }
179    }
180
181    // ═══════════════════════════════════════════════════════════════════════
182    // Task 2.10: Neighbor traversal for mention edges
183    // ═══════════════════════════════════════════════════════════════════════
184
185    /// Get neighbors in a specific direction.
186    ///
187    /// - `Incoming`: nodes that have edges TO this node (dependencies)
188    /// - `Outgoing`: nodes that have edges FROM this node (dependents)
189    pub fn neighbors_directed(
190        &self,
191        idx: NodeIndex,
192        direction: petgraph::Direction,
193    ) -> impl Iterator<Item = NodeIndex> + '_ {
194        self.inner.neighbors_directed(idx, direction)
195    }
196
197    /// Get incoming neighbors (dependencies).
198    pub fn incoming_neighbors(&self, idx: NodeIndex) -> impl Iterator<Item = NodeIndex> + '_ {
199        self.neighbors_directed(idx, petgraph::Direction::Incoming)
200    }
201
202    /// Get outgoing neighbors (dependents).
203    pub fn outgoing_neighbors(&self, idx: NodeIndex) -> impl Iterator<Item = NodeIndex> + '_ {
204        self.neighbors_directed(idx, petgraph::Direction::Outgoing)
205    }
206}
207
208impl<N> Default for StableDag<N> {
209    fn default() -> Self {
210        Self::new()
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_stable_graph_new_creates_empty_graph() {
220        let graph: StableDag<String> = StableDag::new();
221        assert_eq!(graph.node_count(), 0);
222        assert_eq!(graph.edge_count(), 0);
223    }
224
225    #[test]
226    fn test_stable_graph_with_capacity() {
227        let graph: StableDag<String> = StableDag::with_capacity(10, 20);
228        assert_eq!(graph.node_count(), 0);
229        // Capacity is internal optimization, not testable directly
230    }
231
232    #[test]
233    fn test_stable_graph_default_is_empty() {
234        let graph: StableDag<String> = StableDag::default();
235        assert_eq!(graph.node_count(), 0);
236        assert_eq!(graph.edge_count(), 0);
237    }
238
239    // ═══════════════════════════════════════════════════════════════════════
240    // Task 0.2: add_node() tests
241    // ═══════════════════════════════════════════════════════════════════════
242
243    #[test]
244    fn test_add_node_returns_stable_index() {
245        let mut graph: StableDag<String> = StableDag::new();
246
247        let idx1 = graph.add_node("msg-001".to_string());
248        let idx2 = graph.add_node("msg-002".to_string());
249
250        assert_eq!(idx1.index(), 0);
251        assert_eq!(idx2.index(), 1);
252        assert_eq!(graph.node_count(), 2);
253    }
254
255    #[test]
256    fn test_add_node_increments_count() {
257        let mut graph: StableDag<String> = StableDag::new();
258
259        assert_eq!(graph.node_count(), 0);
260        graph.add_node("a".to_string());
261        assert_eq!(graph.node_count(), 1);
262        graph.add_node("b".to_string());
263        assert_eq!(graph.node_count(), 2);
264    }
265
266    #[test]
267    fn test_node_weight_retrieves_value() {
268        let mut graph: StableDag<String> = StableDag::new();
269        let idx = graph.add_node("test-value".to_string());
270
271        assert_eq!(graph.node_weight(idx), Some(&"test-value".to_string()));
272    }
273
274    #[test]
275    fn test_node_weight_mut_allows_modification() {
276        let mut graph: StableDag<String> = StableDag::new();
277        let idx = graph.add_node("original".to_string());
278
279        if let Some(weight) = graph.node_weight_mut(idx) {
280            *weight = "modified".to_string();
281        }
282
283        assert_eq!(graph.node_weight(idx), Some(&"modified".to_string()));
284    }
285
286    // ═══════════════════════════════════════════════════════════════════════
287    // Task 0.3: remove_node() tests — INDEX STABILITY IS KEY
288    // ═══════════════════════════════════════════════════════════════════════
289
290    #[test]
291    fn test_remove_node_preserves_other_indices() {
292        let mut graph: StableDag<String> = StableDag::new();
293
294        let idx0 = graph.add_node("msg-001".to_string());
295        let idx1 = graph.add_node("msg-002".to_string());
296        let idx2 = graph.add_node("msg-003".to_string());
297
298        // Remove middle node
299        graph.remove_node(idx1);
300
301        // idx0 and idx2 should still be valid with same values
302        assert_eq!(graph.node_weight(idx0), Some(&"msg-001".to_string()));
303        assert_eq!(graph.node_weight(idx1), None); // Removed
304        assert_eq!(graph.node_weight(idx2), Some(&"msg-003".to_string()));
305
306        // Indices should NOT have shifted — THIS IS THE KEY INVARIANT
307        assert_eq!(idx0.index(), 0);
308        assert_eq!(idx2.index(), 2); // Still 2, not 1!
309    }
310
311    #[test]
312    fn test_remove_node_decrements_count() {
313        let mut graph: StableDag<String> = StableDag::new();
314
315        let idx = graph.add_node("test".to_string());
316        assert_eq!(graph.node_count(), 1);
317
318        graph.remove_node(idx);
319        assert_eq!(graph.node_count(), 0);
320    }
321
322    #[test]
323    fn test_remove_node_returns_weight() {
324        let mut graph: StableDag<String> = StableDag::new();
325        let idx = graph.add_node("value".to_string());
326
327        let removed = graph.remove_node(idx);
328        assert_eq!(removed, Some("value".to_string()));
329    }
330
331    #[test]
332    fn test_remove_node_missing_returns_none() {
333        let mut graph: StableDag<String> = StableDag::new();
334        let idx = graph.add_node("value".to_string());
335        graph.remove_node(idx);
336
337        // Second removal returns None
338        let removed = graph.remove_node(idx);
339        assert_eq!(removed, None);
340    }
341
342    #[test]
343    fn test_contains_node_after_removal() {
344        let mut graph: StableDag<String> = StableDag::new();
345        let idx = graph.add_node("test".to_string());
346
347        assert!(graph.contains_node(idx));
348        graph.remove_node(idx);
349        assert!(!graph.contains_node(idx));
350    }
351
352    // ═══════════════════════════════════════════════════════════════════════
353    // Task 0.4: Edge operations tests
354    // ═══════════════════════════════════════════════════════════════════════
355
356    #[test]
357    fn test_add_edge_creates_directed_edge() {
358        let mut graph: StableDag<String> = StableDag::new();
359
360        let a = graph.add_node("a".to_string());
361        let b = graph.add_node("b".to_string());
362
363        let edge = graph.add_edge(a, b);
364        assert!(edge.is_some());
365        assert_eq!(graph.edge_count(), 1);
366    }
367
368    #[test]
369    fn test_add_edge_invalid_node_returns_none() {
370        use petgraph::stable_graph::NodeIndex;
371
372        let mut graph: StableDag<String> = StableDag::new();
373
374        let a = graph.add_node("a".to_string());
375        let invalid = NodeIndex::new(999);
376
377        let edge = graph.add_edge(a, invalid);
378        assert!(edge.is_none());
379        assert_eq!(graph.edge_count(), 0);
380    }
381
382    #[test]
383    fn test_has_edge_checks_existence() {
384        let mut graph: StableDag<String> = StableDag::new();
385
386        let a = graph.add_node("a".to_string());
387        let b = graph.add_node("b".to_string());
388        let c = graph.add_node("c".to_string());
389
390        graph.add_edge(a, b);
391
392        assert!(graph.has_edge(a, b));
393        assert!(!graph.has_edge(b, a)); // Directed - no reverse
394        assert!(!graph.has_edge(a, c)); // No edge exists
395    }
396
397    #[test]
398    fn test_remove_edge_works() {
399        let mut graph: StableDag<String> = StableDag::new();
400
401        let a = graph.add_node("a".to_string());
402        let b = graph.add_node("b".to_string());
403
404        graph.add_edge(a, b);
405        assert!(graph.has_edge(a, b));
406        assert_eq!(graph.edge_count(), 1);
407
408        let removed = graph.remove_edge(a, b);
409        assert!(removed);
410        assert!(!graph.has_edge(a, b));
411        assert_eq!(graph.edge_count(), 0);
412    }
413
414    #[test]
415    fn test_remove_edge_nonexistent_returns_false() {
416        let mut graph: StableDag<String> = StableDag::new();
417
418        let a = graph.add_node("a".to_string());
419        let b = graph.add_node("b".to_string());
420
421        let removed = graph.remove_edge(a, b);
422        assert!(!removed);
423    }
424}