1use crate::colony::Colony;
8use phago_core::topology::TopologyGraph;
9use phago_core::types::*;
10use serde::{Deserialize, Serialize};
11use std::path::Path;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct GraphState {
16 pub nodes: Vec<SerializedNode>,
17 pub edges: Vec<SerializedEdge>,
18 pub metadata: SessionMetadata,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct SerializedNode {
24 pub label: String,
25 pub node_type: String,
26 pub access_count: u64,
27 pub position_x: f64,
28 pub position_y: f64,
29 #[serde(default)]
30 pub created_tick: u64,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct SerializedEdge {
36 pub from_label: String,
37 pub to_label: String,
38 pub weight: f64,
39 pub co_activations: u64,
40 #[serde(default)]
41 pub created_tick: u64,
42 #[serde(default)]
43 pub last_activated_tick: u64,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct SessionMetadata {
49 pub session_id: String,
50 pub tick: u64,
51 pub node_count: usize,
52 pub edge_count: usize,
53 pub files_indexed: Vec<String>,
54}
55
56pub fn save_session(colony: &Colony, path: &Path, files_indexed: &[String]) -> std::io::Result<()> {
58 let graph = colony.substrate().graph();
59 let all_nodes = graph.all_nodes();
60
61 let nodes: Vec<SerializedNode> = all_nodes.iter()
62 .filter_map(|nid| graph.get_node(nid))
63 .map(|n| SerializedNode {
64 label: n.label.clone(),
65 node_type: format!("{:?}", n.node_type),
66 access_count: n.access_count,
67 position_x: n.position.x,
68 position_y: n.position.y,
69 created_tick: n.created_tick,
70 })
71 .collect();
72
73 let edges: Vec<SerializedEdge> = graph.all_edges().iter()
74 .filter_map(|(from, to, edge)| {
75 let from_label = graph.get_node(from)?.label.clone();
76 let to_label = graph.get_node(to)?.label.clone();
77 Some(SerializedEdge {
78 from_label,
79 to_label,
80 weight: edge.weight,
81 co_activations: edge.co_activations,
82 created_tick: edge.created_tick,
83 last_activated_tick: edge.last_activated_tick,
84 })
85 })
86 .collect();
87
88 let state = GraphState {
89 metadata: SessionMetadata {
90 session_id: uuid::Uuid::new_v4().to_string(),
91 tick: colony.stats().tick,
92 node_count: nodes.len(),
93 edge_count: edges.len(),
94 files_indexed: files_indexed.to_vec(),
95 },
96 nodes,
97 edges,
98 };
99
100 let json = serde_json::to_string_pretty(&state)
101 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
102
103 if let Some(parent) = path.parent() {
105 std::fs::create_dir_all(parent)?;
106 }
107
108 std::fs::write(path, json)
109}
110
111pub fn load_session(path: &Path) -> std::io::Result<GraphState> {
113 let json = std::fs::read_to_string(path)?;
114 serde_json::from_str(&json)
115 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
116}
117
118pub fn restore_into_colony(colony: &mut Colony, state: &GraphState) {
121 use phago_core::substrate::Substrate;
122 use std::collections::HashMap;
123
124 let mut label_to_id: HashMap<String, NodeId> = HashMap::new();
125
126 for node in &state.nodes {
128 let node_type = match node.node_type.as_str() {
129 "Concept" => NodeType::Concept,
130 "Insight" => NodeType::Insight,
131 "Anomaly" => NodeType::Anomaly,
132 _ => NodeType::Concept,
133 };
134
135 let data = NodeData {
136 id: NodeId::new(),
137 label: node.label.clone(),
138 node_type,
139 position: Position::new(node.position_x, node.position_y),
140 access_count: node.access_count,
141 created_tick: node.created_tick,
142 };
143 let id = colony.substrate_mut().add_node(data);
144 label_to_id.insert(node.label.clone(), id);
145 }
146
147 for edge in &state.edges {
149 if let (Some(&from_id), Some(&to_id)) = (
150 label_to_id.get(&edge.from_label),
151 label_to_id.get(&edge.to_label),
152 ) {
153 colony.substrate_mut().set_edge(from_id, to_id, EdgeData {
154 weight: edge.weight,
155 co_activations: edge.co_activations,
156 created_tick: edge.created_tick,
157 last_activated_tick: edge.last_activated_tick,
158 });
159 }
160 }
161
162 let target_tick = state.metadata.tick;
165 while colony.stats().tick < target_tick {
166 colony.substrate_mut().advance_tick();
167 }
168}
169
170pub fn verify_fidelity(original: &Colony, restored: &Colony) -> (bool, usize, usize, usize, usize) {
172 let orig_nodes = original.substrate().graph().node_count();
173 let orig_edges = original.substrate().graph().edge_count();
174 let rest_nodes = restored.substrate().graph().node_count();
175 let rest_edges = restored.substrate().graph().edge_count();
176 let identical = orig_nodes == rest_nodes && orig_edges == rest_edges;
177 (identical, orig_nodes, orig_edges, rest_nodes, rest_edges)
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use crate::colony::Colony;
184
185 #[test]
186 fn save_load_roundtrip() {
187 let mut colony = Colony::new();
188 colony.ingest_document("test", "cell membrane protein", Position::new(0.0, 0.0));
189
190 use phago_agents::digester::Digester;
191 colony.spawn(Box::new(Digester::new(Position::new(0.0, 0.0)).with_max_idle(80)));
192 colony.run(15);
193
194 let tmp = std::env::temp_dir().join("phago_session_test.json");
195 save_session(&colony, &tmp, &["test.rs".to_string()]).unwrap();
196
197 let state = load_session(&tmp).unwrap();
198 assert!(!state.nodes.is_empty());
199 assert!(state.metadata.node_count > 0);
200
201 let mut restored = Colony::new();
203 restore_into_colony(&mut restored, &state);
204
205 let (_identical, orig_n, _orig_e, rest_n, rest_e) = verify_fidelity(&colony, &restored);
206 assert_eq!(orig_n, rest_n, "Node count should match");
207 assert!(rest_e > 0, "Restored colony should have edges");
209
210 std::fs::remove_file(&tmp).ok();
211 }
212}