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