Skip to main content

gid_core/
refactor.rs

1//! Graph refactoring operations.
2//!
3//! Preview and apply structural changes: rename, merge, split, extract.
4
5use serde::{Deserialize, Serialize};
6use crate::graph::{Graph, Node, Edge};
7
8/// A preview of changes that a refactoring operation would make.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct RefactorPreview {
11    /// Operation type
12    pub operation: String,
13    /// Changes to be made
14    pub changes: Vec<Change>,
15    /// Node IDs affected
16    pub affected_nodes: Vec<String>,
17    /// Edge count affected
18    pub affected_edges: usize,
19}
20
21impl std::fmt::Display for RefactorPreview {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        writeln!(f, "📋 {} Preview", self.operation)?;
24        writeln!(f, "═══════════════════════════════════════════════════")?;
25        
26        for change in &self.changes {
27            writeln!(f, "{}", change)?;
28        }
29        
30        writeln!(f)?;
31        writeln!(f, "Affected: {} nodes, {} edges", 
32            self.affected_nodes.len(), 
33            self.affected_edges
34        )?;
35        
36        Ok(())
37    }
38}
39
40/// A single change in a refactoring operation.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct Change {
43    /// Type of change
44    pub change_type: ChangeType,
45    /// Description of the change
46    pub description: String,
47    /// Before value (if applicable)
48    pub before: Option<String>,
49    /// After value (if applicable)
50    pub after: Option<String>,
51}
52
53impl std::fmt::Display for Change {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        let icon = match self.change_type {
56            ChangeType::RenameNode | ChangeType::UpdateTitle => "✏️ ",
57            ChangeType::DeleteNode => "🗑️ ",
58            ChangeType::CreateNode => "➕",
59            ChangeType::UpdateEdge => "🔗",
60            ChangeType::DeleteEdge => "✂️ ",
61            ChangeType::CreateEdge => "🔗",
62            ChangeType::MergeNode => "🔀",
63            ChangeType::SplitNode => "✂️ ",
64        };
65        
66        write!(f, "{} {}", icon, self.description)?;
67        
68        if let (Some(before), Some(after)) = (&self.before, &self.after) {
69            write!(f, "\n      {} → {}", before, after)?;
70        }
71        
72        Ok(())
73    }
74}
75
76/// Type of change in a refactoring operation.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78#[serde(rename_all = "snake_case")]
79pub enum ChangeType {
80    RenameNode,
81    UpdateTitle,
82    DeleteNode,
83    CreateNode,
84    UpdateEdge,
85    DeleteEdge,
86    CreateEdge,
87    MergeNode,
88    SplitNode,
89}
90
91/// Definition for how to split a node.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct SplitDefinition {
94    /// New node ID
95    pub id: String,
96    /// New node title
97    pub title: String,
98    /// Optional description
99    pub description: Option<String>,
100    /// Tags to inherit or add
101    pub tags: Vec<String>,
102}
103
104// ═══════════════════════════════════════════════════════════════════════════════
105// Rename Operations
106// ═══════════════════════════════════════════════════════════════════════════════
107
108/// Preview what would change if a node is renamed.
109pub fn preview_rename(graph: &Graph, old_id: &str, new_id: &str) -> Option<RefactorPreview> {
110    // Check node exists
111    graph.get_node(old_id)?;
112    
113    let mut changes = Vec::new();
114    let mut affected_edges = 0;
115    
116    // Node rename
117    changes.push(Change {
118        change_type: ChangeType::RenameNode,
119        description: format!("Rename node ID"),
120        before: Some(old_id.to_string()),
121        after: Some(new_id.to_string()),
122    });
123    
124    // Find affected edges
125    for edge in &graph.edges {
126        if edge.from == old_id {
127            changes.push(Change {
128                change_type: ChangeType::UpdateEdge,
129                description: format!("Update edge source"),
130                before: Some(format!("{} → {}", edge.from, edge.to)),
131                after: Some(format!("{} → {}", new_id, edge.to)),
132            });
133            affected_edges += 1;
134        }
135        if edge.to == old_id {
136            changes.push(Change {
137                change_type: ChangeType::UpdateEdge,
138                description: format!("Update edge target"),
139                before: Some(format!("{} → {}", edge.from, edge.to)),
140                after: Some(format!("{} → {}", edge.from, new_id)),
141            });
142            affected_edges += 1;
143        }
144    }
145    
146    Some(RefactorPreview {
147        operation: "Rename".to_string(),
148        changes,
149        affected_nodes: vec![old_id.to_string()],
150        affected_edges,
151    })
152}
153
154/// Apply a node rename operation.
155pub fn apply_rename(graph: &mut Graph, old_id: &str, new_id: &str) -> bool {
156    // Check source exists and target doesn't
157    if graph.get_node(old_id).is_none() || graph.get_node(new_id).is_some() {
158        return false;
159    }
160    
161    // Rename node
162    if let Some(node) = graph.get_node_mut(old_id) {
163        node.id = new_id.to_string();
164    }
165    
166    // Update all edges
167    for edge in &mut graph.edges {
168        if edge.from == old_id {
169            edge.from = new_id.to_string();
170        }
171        if edge.to == old_id {
172            edge.to = new_id.to_string();
173        }
174    }
175    
176    true
177}
178
179// ═══════════════════════════════════════════════════════════════════════════════
180// Merge Operations
181// ═══════════════════════════════════════════════════════════════════════════════
182
183/// Preview what would change if two nodes are merged.
184pub fn preview_merge(
185    graph: &Graph, 
186    node_a: &str, 
187    node_b: &str, 
188    new_id: &str
189) -> Option<RefactorPreview> {
190    let a = graph.get_node(node_a)?;
191    let b = graph.get_node(node_b)?;
192    
193    let mut changes = Vec::new();
194    let mut affected_edges = 0;
195    
196    // Merge description
197    let merged_title = format!("{} + {}", a.title, b.title);
198    changes.push(Change {
199        change_type: ChangeType::MergeNode,
200        description: format!("Create merged node '{}'", new_id),
201        before: Some(format!("'{}', '{}'", node_a, node_b)),
202        after: Some(merged_title.clone()),
203    });
204    
205    // Delete original nodes
206    changes.push(Change {
207        change_type: ChangeType::DeleteNode,
208        description: format!("Remove node '{}'", node_a),
209        before: Some(node_a.to_string()),
210        after: None,
211    });
212    changes.push(Change {
213        change_type: ChangeType::DeleteNode,
214        description: format!("Remove node '{}'", node_b),
215        before: Some(node_b.to_string()),
216        after: None,
217    });
218    
219    // Count affected edges (edges to/from either node)
220    for edge in &graph.edges {
221        if edge.from == node_a || edge.from == node_b 
222            || edge.to == node_a || edge.to == node_b 
223        {
224            affected_edges += 1;
225        }
226    }
227    
228    changes.push(Change {
229        change_type: ChangeType::UpdateEdge,
230        description: format!("Redirect {} edges to new merged node", affected_edges),
231        before: None,
232        after: None,
233    });
234    
235    Some(RefactorPreview {
236        operation: "Merge".to_string(),
237        changes,
238        affected_nodes: vec![node_a.to_string(), node_b.to_string()],
239        affected_edges,
240    })
241}
242
243/// Apply a merge operation.
244pub fn apply_merge(
245    graph: &mut Graph, 
246    node_a: &str, 
247    node_b: &str, 
248    new_id: &str
249) -> bool {
250    let a = match graph.get_node(node_a) {
251        Some(n) => n.clone(),
252        None => return false,
253    };
254    let b = match graph.get_node(node_b) {
255        Some(n) => n.clone(),
256        None => return false,
257    };
258    
259    // Create merged node
260    let mut merged = Node::new(new_id, &format!("{} + {}", a.title, b.title));
261    
262    // Merge descriptions
263    merged.description = match (a.description, b.description) {
264        (Some(da), Some(db)) => Some(format!("{}\n\n{}", da, db)),
265        (Some(d), None) | (None, Some(d)) => Some(d),
266        (None, None) => None,
267    };
268    
269    // Merge tags (dedupe)
270    let mut tags: Vec<String> = a.tags.into_iter().chain(b.tags).collect();
271    tags.sort();
272    tags.dedup();
273    merged.tags = tags;
274    
275    // Use more "done" status
276    merged.status = if a.status == crate::graph::NodeStatus::Done 
277        || b.status == crate::graph::NodeStatus::Done 
278    {
279        crate::graph::NodeStatus::Done
280    } else if a.status == crate::graph::NodeStatus::InProgress 
281        || b.status == crate::graph::NodeStatus::InProgress 
282    {
283        crate::graph::NodeStatus::InProgress
284    } else {
285        a.status
286    };
287    
288    // Add merged node
289    graph.add_node(merged);
290    
291    // Update edges to point to new node
292    for edge in &mut graph.edges {
293        if edge.from == node_a || edge.from == node_b {
294            edge.from = new_id.to_string();
295        }
296        if edge.to == node_a || edge.to == node_b {
297            edge.to = new_id.to_string();
298        }
299    }
300    
301    // Remove duplicate edges (same from, to, relation)
302    let mut seen = std::collections::HashSet::new();
303    graph.edges.retain(|e| {
304        seen.insert((e.from.clone(), e.to.clone(), e.relation.clone()))
305    });
306    
307    // Remove self-referential edges
308    graph.edges.retain(|e| e.from != e.to);
309    
310    // Remove original nodes
311    graph.remove_node(node_a);
312    graph.remove_node(node_b);
313    
314    true
315}
316
317// ═══════════════════════════════════════════════════════════════════════════════
318// Split Operations
319// ═══════════════════════════════════════════════════════════════════════════════
320
321/// Preview what would change if a node is split.
322pub fn preview_split(
323    graph: &Graph,
324    node_id: &str,
325    splits: &[SplitDefinition],
326) -> Option<RefactorPreview> {
327    let _node = graph.get_node(node_id)?;
328    
329    let mut changes = Vec::new();
330    
331    // Delete original node
332    changes.push(Change {
333        change_type: ChangeType::SplitNode,
334        description: format!("Split node '{}' into {} parts", node_id, splits.len()),
335        before: Some(format!("'{}'", node_id)),
336        after: Some(splits.iter().map(|s| s.id.as_str()).collect::<Vec<_>>().join(", ")),
337    });
338    
339    // Create new nodes
340    for split in splits {
341        changes.push(Change {
342            change_type: ChangeType::CreateNode,
343            description: format!("Create node '{}'", split.id),
344            before: None,
345            after: Some(split.title.clone()),
346        });
347    }
348    
349    // Count affected edges
350    let affected_edges = graph.edges.iter()
351        .filter(|e| e.from == node_id || e.to == node_id)
352        .count();
353    
354    if affected_edges > 0 {
355        changes.push(Change {
356            change_type: ChangeType::UpdateEdge,
357            description: format!("Note: {} edges need manual reassignment", affected_edges),
358            before: None,
359            after: None,
360        });
361    }
362    
363    Some(RefactorPreview {
364        operation: "Split".to_string(),
365        changes,
366        affected_nodes: std::iter::once(node_id.to_string())
367            .chain(splits.iter().map(|s| s.id.clone()))
368            .collect(),
369        affected_edges,
370    })
371}
372
373/// Apply a split operation.
374/// Returns the IDs of created nodes.
375pub fn apply_split(
376    graph: &mut Graph,
377    node_id: &str,
378    splits: &[SplitDefinition],
379) -> Vec<String> {
380    let original = match graph.get_node(node_id) {
381        Some(n) => n.clone(),
382        None => return Vec::new(),
383    };
384    
385    let mut created = Vec::new();
386    
387    // Create new nodes
388    for (i, split) in splits.iter().enumerate() {
389        let mut new_node = Node::new(&split.id, &split.title);
390        new_node.description = split.description.clone()
391            .or_else(|| original.description.clone());
392        new_node.status = original.status.clone();
393        new_node.node_type = original.node_type.clone();
394        
395        // Merge tags
396        let mut tags = original.tags.clone();
397        tags.extend(split.tags.clone());
398        tags.sort();
399        tags.dedup();
400        new_node.tags = tags;
401        
402        graph.add_node(new_node);
403        created.push(split.id.clone());
404        
405        // First split inherits incoming edges, all inherit outgoing edges
406        // This is a heuristic; user may need to adjust
407        if i == 0 {
408            // Redirect incoming edges to first split
409            for edge in &mut graph.edges {
410                if edge.to == node_id {
411                    edge.to = split.id.clone();
412                }
413            }
414        }
415    }
416    
417    // Redirect outgoing edges to first split (simple heuristic)
418    if let Some(first) = splits.first() {
419        for edge in &mut graph.edges {
420            if edge.from == node_id {
421                edge.from = first.id.clone();
422            }
423        }
424    }
425    
426    // Remove original node
427    graph.remove_node(node_id);
428    
429    created
430}
431
432// ═══════════════════════════════════════════════════════════════════════════════
433// Extract Operations
434// ═══════════════════════════════════════════════════════════════════════════════
435
436/// Preview extracting nodes into a new parent/group.
437pub fn preview_extract(
438    graph: &Graph,
439    node_ids: &[String],
440    new_parent_id: &str,
441    new_parent_title: &str,
442) -> Option<RefactorPreview> {
443    // Verify all nodes exist
444    for id in node_ids {
445        graph.get_node(id)?;
446    }
447    
448    let mut changes = Vec::new();
449    
450    // Create parent node
451    changes.push(Change {
452        change_type: ChangeType::CreateNode,
453        description: format!("Create parent node '{}'", new_parent_id),
454        before: None,
455        after: Some(new_parent_title.to_string()),
456    });
457    
458    // Add contains edges
459    for id in node_ids {
460        changes.push(Change {
461            change_type: ChangeType::CreateEdge,
462            description: format!("Add contains edge to '{}'", id),
463            before: None,
464            after: Some(format!("{} → {}", new_parent_id, id)),
465        });
466    }
467    
468    Some(RefactorPreview {
469        operation: "Extract".to_string(),
470        changes,
471        affected_nodes: std::iter::once(new_parent_id.to_string())
472            .chain(node_ids.iter().cloned())
473            .collect(),
474        affected_edges: node_ids.len(),
475    })
476}
477
478/// Apply an extract operation.
479pub fn apply_extract(
480    graph: &mut Graph,
481    node_ids: &[String],
482    new_parent_id: &str,
483    new_parent_title: &str,
484) -> bool {
485    // Verify all nodes exist
486    for id in node_ids {
487        if graph.get_node(id).is_none() {
488            return false;
489        }
490    }
491    
492    // Create parent node
493    let mut parent = Node::new(new_parent_id, new_parent_title);
494    parent.node_type = Some("module".to_string());
495    graph.add_node(parent);
496    
497    // Add contains edges
498    for id in node_ids {
499        graph.add_edge(Edge::new(new_parent_id, id, "contains"));
500    }
501    
502    true
503}
504
505// ═══════════════════════════════════════════════════════════════════════════════
506// Utility Operations
507// ═══════════════════════════════════════════════════════════════════════════════
508
509/// Update a node's title.
510pub fn update_title(graph: &mut Graph, node_id: &str, new_title: &str) -> bool {
511    if let Some(node) = graph.get_node_mut(node_id) {
512        node.title = new_title.to_string();
513        true
514    } else {
515        false
516    }
517}
518
519/// Move a node to a different layer.
520pub fn move_to_layer(graph: &mut Graph, node_id: &str, layer: &str) -> bool {
521    if let Some(node) = graph.get_node_mut(node_id) {
522        node.metadata.insert("layer".to_string(), serde_json::json!(layer));
523        true
524    } else {
525        false
526    }
527}
528
529#[cfg(test)]
530mod tests {
531    use super::*;
532    
533    #[test]
534    fn test_preview_rename() {
535        let mut graph = Graph::new();
536        graph.add_node(Node::new("old", "Old Node"));
537        graph.add_node(Node::new("other", "Other"));
538        graph.add_edge(Edge::depends_on("other", "old"));
539        
540        let preview = preview_rename(&graph, "old", "new").unwrap();
541        assert_eq!(preview.operation, "Rename");
542        assert_eq!(preview.affected_edges, 1);
543    }
544    
545    #[test]
546    fn test_apply_rename() {
547        let mut graph = Graph::new();
548        graph.add_node(Node::new("old", "Old Node"));
549        graph.add_node(Node::new("other", "Other"));
550        graph.add_edge(Edge::depends_on("other", "old"));
551        
552        assert!(apply_rename(&mut graph, "old", "new"));
553        assert!(graph.get_node("old").is_none());
554        assert!(graph.get_node("new").is_some());
555        assert_eq!(graph.edges[0].to, "new");
556    }
557    
558    #[test]
559    fn test_apply_merge() {
560        let mut graph = Graph::new();
561        graph.add_node(Node::new("a", "Node A").with_tags(vec!["tag1".to_string()]));
562        graph.add_node(Node::new("b", "Node B").with_tags(vec!["tag2".to_string()]));
563        graph.add_node(Node::new("c", "Node C"));
564        graph.add_edge(Edge::depends_on("c", "a"));
565        
566        assert!(apply_merge(&mut graph, "a", "b", "merged"));
567        
568        assert!(graph.get_node("a").is_none());
569        assert!(graph.get_node("b").is_none());
570        
571        let merged = graph.get_node("merged").unwrap();
572        assert_eq!(merged.tags.len(), 2);
573        
574        // Edge should point to merged node
575        assert_eq!(graph.edges[0].to, "merged");
576    }
577    
578    #[test]
579    fn test_apply_split() {
580        let mut graph = Graph::new();
581        graph.add_node(Node::new("original", "Original Node")
582            .with_description("Description")
583            .with_tags(vec!["tag1".to_string()]));
584        graph.add_node(Node::new("dep", "Dependency"));
585        graph.add_edge(Edge::depends_on("original", "dep"));
586        
587        let splits = vec![
588            SplitDefinition {
589                id: "part1".to_string(),
590                title: "Part 1".to_string(),
591                description: None,
592                tags: vec![],
593            },
594            SplitDefinition {
595                id: "part2".to_string(),
596                title: "Part 2".to_string(),
597                description: Some("Custom desc".to_string()),
598                tags: vec!["new_tag".to_string()],
599            },
600        ];
601        
602        let created = apply_split(&mut graph, "original", &splits);
603        assert_eq!(created.len(), 2);
604        assert!(graph.get_node("original").is_none());
605        assert!(graph.get_node("part1").is_some());
606        assert!(graph.get_node("part2").is_some());
607    }
608    
609    #[test]
610    fn test_apply_extract() {
611        let mut graph = Graph::new();
612        graph.add_node(Node::new("a", "A"));
613        graph.add_node(Node::new("b", "B"));
614        graph.add_node(Node::new("c", "C"));
615        
616        assert!(apply_extract(
617            &mut graph,
618            &["a".to_string(), "b".to_string()],
619            "module_ab",
620            "Module AB"
621        ));
622        
623        assert!(graph.get_node("module_ab").is_some());
624        
625        // Check contains edges
626        let contains_edges: Vec<_> = graph.edges.iter()
627            .filter(|e| e.relation == "contains" && e.from == "module_ab")
628            .collect();
629        assert_eq!(contains_edges.len(), 2);
630    }
631}