1use serde::{Deserialize, Serialize};
6use crate::graph::{Graph, Node, Edge};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct RefactorPreview {
11 pub operation: String,
13 pub changes: Vec<Change>,
15 pub affected_nodes: Vec<String>,
17 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#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct Change {
43 pub change_type: ChangeType,
45 pub description: String,
47 pub before: Option<String>,
49 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct SplitDefinition {
94 pub id: String,
96 pub title: String,
98 pub description: Option<String>,
100 pub tags: Vec<String>,
102}
103
104pub fn preview_rename(graph: &Graph, old_id: &str, new_id: &str) -> Option<RefactorPreview> {
110 graph.get_node(old_id)?;
112
113 let mut changes = Vec::new();
114 let mut affected_edges = 0;
115
116 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 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
154pub fn apply_rename(graph: &mut Graph, old_id: &str, new_id: &str) -> bool {
156 if graph.get_node(old_id).is_none() || graph.get_node(new_id).is_some() {
158 return false;
159 }
160
161 if let Some(node) = graph.get_node_mut(old_id) {
163 node.id = new_id.to_string();
164 }
165
166 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
179pub 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 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 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 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
243pub 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 let mut merged = Node::new(new_id, &format!("{} + {}", a.title, b.title));
261
262 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 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 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 graph.add_node(merged);
290
291 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 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 graph.edges.retain(|e| e.from != e.to);
309
310 graph.remove_node(node_a);
312 graph.remove_node(node_b);
313
314 true
315}
316
317pub 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 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 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 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
373pub 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 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 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 if i == 0 {
408 for edge in &mut graph.edges {
410 if edge.to == node_id {
411 edge.to = split.id.clone();
412 }
413 }
414 }
415 }
416
417 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 graph.remove_node(node_id);
428
429 created
430}
431
432pub fn preview_extract(
438 graph: &Graph,
439 node_ids: &[String],
440 new_parent_id: &str,
441 new_parent_title: &str,
442) -> Option<RefactorPreview> {
443 for id in node_ids {
445 graph.get_node(id)?;
446 }
447
448 let mut changes = Vec::new();
449
450 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 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
478pub fn apply_extract(
480 graph: &mut Graph,
481 node_ids: &[String],
482 new_parent_id: &str,
483 new_parent_title: &str,
484) -> bool {
485 for id in node_ids {
487 if graph.get_node(id).is_none() {
488 return false;
489 }
490 }
491
492 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 for id in node_ids {
499 graph.add_edge(Edge::new(new_parent_id, id, "contains"));
500 }
501
502 true
503}
504
505pub 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
519pub 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 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 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}