Skip to main content

gid_core/
graph.rs

1use std::collections::HashMap;
2use serde::{Deserialize, Serialize};
3use crate::task_graph_knowledge::{KnowledgeNode, KnowledgeGraph, KnowledgeManagement};
4
5/// A complete GID graph with nodes and edges.
6#[derive(Debug, Clone, Default, Serialize, Deserialize)]
7pub struct Graph {
8    #[serde(default)]
9    pub project: Option<ProjectMeta>,
10    #[serde(default)]
11    pub nodes: Vec<Node>,
12    #[serde(default)]
13    pub edges: Vec<Edge>,
14}
15
16/// Project metadata.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ProjectMeta {
19    pub name: String,
20    #[serde(default)]
21    pub description: Option<String>,
22}
23
24/// A node in the graph (task, code file, component, etc.)
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct Node {
27    pub id: String,
28    pub title: String,
29    #[serde(default)]
30    pub status: NodeStatus,
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub description: Option<String>,
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub assigned_to: Option<String>,
35    #[serde(default, skip_serializing_if = "Vec::is_empty")]
36    pub tags: Vec<String>,
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub priority: Option<u8>,
39    /// Node type: task, file, component, feature, layer, etc.
40    #[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
41    pub node_type: Option<String>,
42    /// Knowledge storage: findings, file cache, and tool history.
43    #[serde(default, skip_serializing_if = "KnowledgeNode::is_empty")]
44    pub knowledge: KnowledgeNode,
45    /// Additional metadata.
46    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
47    pub metadata: HashMap<String, serde_json::Value>,
48}
49
50impl Node {
51    pub fn new(id: &str, title: &str) -> Self {
52        Self {
53            id: id.to_string(),
54            title: title.to_string(),
55            status: NodeStatus::Todo,
56            description: None,
57            assigned_to: None,
58            tags: Vec::new(),
59            priority: None,
60            node_type: None,
61            knowledge: KnowledgeNode::default(),
62            metadata: HashMap::new(),
63        }
64    }
65
66    pub fn with_description(mut self, desc: &str) -> Self {
67        self.description = Some(desc.to_string());
68        self
69    }
70
71    pub fn with_status(mut self, status: NodeStatus) -> Self {
72        self.status = status;
73        self
74    }
75
76    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
77        self.tags = tags;
78        self
79    }
80
81    pub fn with_priority(mut self, priority: u8) -> Self {
82        self.priority = Some(priority);
83        self
84    }
85}
86
87/// Status of a node.
88#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
89#[serde(rename_all = "lowercase")]
90pub enum NodeStatus {
91    Todo,
92    #[serde(alias = "in_progress", alias = "in-progress")]
93    InProgress,
94    Done,
95    Blocked,
96    Cancelled,
97}
98
99impl Default for NodeStatus {
100    fn default() -> Self {
101        Self::Todo
102    }
103}
104
105impl std::fmt::Display for NodeStatus {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        match self {
108            NodeStatus::Todo => write!(f, "todo"),
109            NodeStatus::InProgress => write!(f, "in_progress"),
110            NodeStatus::Done => write!(f, "done"),
111            NodeStatus::Blocked => write!(f, "blocked"),
112            NodeStatus::Cancelled => write!(f, "cancelled"),
113        }
114    }
115}
116
117impl std::str::FromStr for NodeStatus {
118    type Err = anyhow::Error;
119    fn from_str(s: &str) -> Result<Self, Self::Err> {
120        match s {
121            "todo" => Ok(NodeStatus::Todo),
122            "in_progress" | "in-progress" => Ok(NodeStatus::InProgress),
123            "done" => Ok(NodeStatus::Done),
124            "blocked" => Ok(NodeStatus::Blocked),
125            "cancelled" => Ok(NodeStatus::Cancelled),
126            _ => Err(anyhow::anyhow!("Unknown status: {}", s)),
127        }
128    }
129}
130
131/// An edge (relationship) between two nodes.
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct Edge {
134    pub from: String,
135    pub to: String,
136    #[serde(default = "default_relation")]
137    pub relation: String,
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub weight: Option<f64>,
140}
141
142fn default_relation() -> String {
143    "depends_on".to_string()
144}
145
146impl Edge {
147    pub fn new(from: &str, to: &str, relation: &str) -> Self {
148        Self {
149            from: from.to_string(),
150            to: to.to_string(),
151            relation: relation.to_string(),
152            weight: None,
153        }
154    }
155
156    pub fn depends_on(from: &str, to: &str) -> Self {
157        Self::new(from, to, "depends_on")
158    }
159}
160
161// ─── Graph operations ────────────────────────────────────────
162
163impl Graph {
164    pub fn new() -> Self {
165        Self::default()
166    }
167
168    // ── Node operations ──
169
170    pub fn get_node(&self, id: &str) -> Option<&Node> {
171        self.nodes.iter().find(|n| n.id == id)
172    }
173
174    pub fn get_node_mut(&mut self, id: &str) -> Option<&mut Node> {
175        self.nodes.iter_mut().find(|n| n.id == id)
176    }
177
178    pub fn add_node(&mut self, node: Node) {
179        if self.get_node(&node.id).is_none() {
180            self.nodes.push(node);
181        }
182    }
183
184    pub fn remove_node(&mut self, id: &str) -> Option<Node> {
185        let pos = self.nodes.iter().position(|n| n.id == id)?;
186        let node = self.nodes.remove(pos);
187        // Remove associated edges
188        self.edges.retain(|e| e.from != id && e.to != id);
189        Some(node)
190    }
191
192    pub fn update_status(&mut self, id: &str, status: NodeStatus) -> bool {
193        if let Some(node) = self.get_node_mut(id) {
194            node.status = status;
195            true
196        } else {
197            false
198        }
199    }
200
201    // ── Edge operations ──
202
203    pub fn add_edge(&mut self, edge: Edge) {
204        // Avoid duplicates
205        let exists = self.edges.iter().any(|e| {
206            e.from == edge.from && e.to == edge.to && e.relation == edge.relation
207        });
208        if !exists {
209            self.edges.push(edge);
210        }
211    }
212
213    pub fn remove_edge(&mut self, from: &str, to: &str, relation: Option<&str>) {
214        self.edges.retain(|e| {
215            !(e.from == from && e.to == to && relation.map_or(true, |r| e.relation == r))
216        });
217    }
218
219    pub fn edges_from(&self, id: &str) -> Vec<&Edge> {
220        self.edges.iter().filter(|e| e.from == id).collect()
221    }
222
223    pub fn edges_to(&self, id: &str) -> Vec<&Edge> {
224        self.edges.iter().filter(|e| e.to == id).collect()
225    }
226
227    // ── Query helpers ──
228
229    /// Get tasks that are ready (todo + all depends_on are done).
230    pub fn ready_tasks(&self) -> Vec<&Node> {
231        self.nodes
232            .iter()
233            .filter(|n| n.status == NodeStatus::Todo)
234            .filter(|n| {
235                let deps: Vec<&Edge> = self.edges_from(&n.id)
236                    .into_iter()
237                    .filter(|e| e.relation == "depends_on")
238                    .collect();
239                deps.iter().all(|e| {
240                    self.get_node(&e.to)
241                        .map_or(true, |dep| dep.status == NodeStatus::Done)
242                })
243            })
244            .collect()
245    }
246
247    /// Get tasks by status.
248    pub fn tasks_by_status(&self, status: &NodeStatus) -> Vec<&Node> {
249        self.nodes.iter().filter(|n| &n.status == status).collect()
250    }
251
252    /// Summary statistics.
253    pub fn summary(&self) -> GraphSummary {
254        let mut s = GraphSummary {
255            total_nodes: self.nodes.len(),
256            total_edges: self.edges.len(),
257            ..Default::default()
258        };
259        for n in &self.nodes {
260            match n.status {
261                NodeStatus::Todo => s.todo += 1,
262                NodeStatus::InProgress => s.in_progress += 1,
263                NodeStatus::Done => s.done += 1,
264                NodeStatus::Blocked => s.blocked += 1,
265                NodeStatus::Cancelled => s.cancelled += 1,
266            }
267        }
268        s.ready = self.ready_tasks().len();
269        s
270    }
271
272    /// Get a human-readable text summary of the graph state.
273    pub fn summary_text(&self) -> String {
274        let s = self.summary();
275        let mut lines = vec![
276            format!("Graph: {} nodes, {} edges", s.total_nodes, s.total_edges),
277        ];
278
279        if s.total_nodes > 0 {
280            lines.push(format!(
281                "Status: {} todo, {} in-progress, {} done, {} blocked, {} cancelled",
282                s.todo, s.in_progress, s.done, s.blocked, s.cancelled
283            ));
284            lines.push(format!("Ready tasks: {}", s.ready));
285        }
286
287        // Show project name if available
288        if let Some(ref project) = self.project {
289            lines.insert(0, format!("Project: {}", project.name));
290        }
291
292        lines.join("\n")
293    }
294
295    /// Calculate graph health score (0.0 to 1.0).
296    /// 
297    /// Health is based on:
298    /// - Progress: ratio of done tasks to total
299    /// - Flow: ratio of ready tasks to remaining (non-blocked) tasks
300    /// - Connectivity: graphs with edges are healthier than isolated nodes
301    /// 
302    /// Returns 1.0 for a fully complete graph, 0.0 for an empty or stuck graph.
303    pub fn health(&self) -> f64 {
304        if self.nodes.is_empty() {
305            return 0.0;
306        }
307
308        let s = self.summary();
309        let total = s.total_nodes as f64;
310
311        // Progress score: what fraction is done?
312        let progress = s.done as f64 / total;
313
314        // Flow score: are there ready tasks to work on? (avoid stuck graphs)
315        let remaining = s.todo + s.in_progress;
316        let flow = if remaining == 0 {
317            1.0 // All done, perfect flow
318        } else if s.ready == 0 && s.todo > 0 {
319            0.0 // Stuck: todos exist but none are ready (all blocked by dependencies)
320        } else {
321            (s.ready as f64) / (remaining as f64)
322        };
323
324        // Connectivity score: graphs with structure are healthier
325        let connectivity = if self.nodes.len() > 1 {
326            let max_edges = self.nodes.len() * (self.nodes.len() - 1);
327            let actual = self.edges.len().min(max_edges);
328            (actual as f64 / max_edges as f64).min(1.0)
329        } else {
330            1.0 // Single node is "connected"
331        };
332
333        // Blocked penalty: heavily blocked graphs are unhealthy
334        let blocked_ratio = s.blocked as f64 / total;
335        let blocked_penalty = 1.0 - blocked_ratio;
336
337        // Weighted combination
338        let health = 0.4 * progress + 0.3 * flow + 0.1 * connectivity + 0.2 * blocked_penalty;
339        health.clamp(0.0, 1.0)
340    }
341
342    /// Mark a task as done. Returns true if found and updated.
343    pub fn mark_task_done(&mut self, node_id: &str) -> bool {
344        self.update_status(node_id, NodeStatus::Done)
345    }
346
347    /// Get executable tasks (alias for ready_tasks, returns owned Task structs).
348    pub fn get_executable_tasks(&self) -> Vec<Task> {
349        self.ready_tasks()
350            .into_iter()
351            .map(|node| Task {
352                id: node.id.clone(),
353                title: node.title.clone(),
354                description: node.description.clone(),
355                priority: node.priority,
356            })
357            .collect()
358    }
359}
360
361/// A simplified task representation for execution.
362#[derive(Debug, Clone)]
363pub struct Task {
364    pub id: String,
365    pub title: String,
366    pub description: Option<String>,
367    pub priority: Option<u8>,
368}
369
370#[derive(Debug, Default)]
371pub struct GraphSummary {
372    pub total_nodes: usize,
373    pub total_edges: usize,
374    pub todo: usize,
375    pub in_progress: usize,
376    pub done: usize,
377    pub blocked: usize,
378    pub cancelled: usize,
379    pub ready: usize,
380}
381
382// Implement knowledge management for Graph so users can call
383// graph.store_finding(), graph.cache_file(), etc. directly.
384impl KnowledgeGraph for Graph {
385    fn get_knowledge_mut(&mut self, node_id: &str) -> Option<&mut KnowledgeNode> {
386        self.nodes.iter_mut()
387            .find(|n| n.id == node_id)
388            .map(|n| &mut n.knowledge)
389    }
390
391    fn get_knowledge(&self, node_id: &str) -> Option<&KnowledgeNode> {
392        self.nodes.iter()
393            .find(|n| n.id == node_id)
394            .map(|n| &n.knowledge)
395    }
396
397    fn get_incoming_edges(&self, node_id: &str) -> Vec<String> {
398        self.edges.iter()
399            .filter(|e| e.to == node_id)
400            .map(|e| e.from.clone())
401            .collect()
402    }
403}
404
405impl KnowledgeManagement for Graph {}
406
407impl std::fmt::Display for GraphSummary {
408    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409        write!(
410            f,
411            "{} nodes, {} edges | todo={} progress={} done={} blocked={} cancelled={} | ready={}",
412            self.total_nodes, self.total_edges,
413            self.todo, self.in_progress, self.done, self.blocked, self.cancelled,
414            self.ready,
415        )
416    }
417}