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    /// Priority 0–255. SQLite stores as INTEGER; values outside 0–255 are clamped on read.
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub priority: Option<u8>,
83    /// Node type: task, file, component, feature, layer, etc.
84    #[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
85    pub node_type: Option<String>,
86    /// Knowledge storage: findings, file cache, and tool history.
87    #[serde(default, skip_serializing_if = "KnowledgeNode::is_empty")]
88    pub knowledge: KnowledgeNode,
89    /// Additional metadata.
90    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
91    pub metadata: HashMap<String, serde_json::Value>,
92
93    // ── Code-graph fields (populated by `gid extract`, None for task nodes) ──
94
95    /// File path relative to the project root.
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub file_path: Option<String>,
98    /// Programming language.
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub lang: Option<String>,
101    /// Start line number in the source file.
102    /// Note: stored as INTEGER in SQLite; clamped to usize range on read.
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub start_line: Option<usize>,
105    /// End line number in the source file.
106    /// Note: stored as INTEGER in SQLite; clamped to usize range on read.
107    #[serde(default, skip_serializing_if = "Option::is_none")]
108    pub end_line: Option<usize>,
109    /// Function/method signature.
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    pub signature: Option<String>,
112    /// Visibility: public, private, crate, etc.
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub visibility: Option<String>,
115    /// Documentation comment extracted from source.
116    #[serde(default, skip_serializing_if = "Option::is_none")]
117    pub doc_comment: Option<String>,
118    /// Hash of the body content (for change detection).
119    #[serde(default, skip_serializing_if = "Option::is_none")]
120    pub body_hash: Option<String>,
121    /// Code-level kind: Function, Struct, Impl, Trait, Enum, etc.
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub node_kind: Option<String>,
124
125    // ── Provenance fields ──
126
127    /// Owner of this node (person or team).
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub owner: Option<String>,
130    /// Source of this node (e.g., "extract", "manual", "import").
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub source: Option<String>,
133    /// Repository this node belongs to.
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub repo: Option<String>,
136
137    // ── Hierarchy & structure fields ──
138
139    /// Parent node ID (for hierarchical relationships).
140    #[serde(default, skip_serializing_if = "Option::is_none")]
141    pub parent_id: Option<String>,
142    /// Depth in the node hierarchy (0 = root).
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub depth: Option<u32>,
145
146    // ── Analysis fields ──
147
148    /// Complexity score (e.g., cyclomatic complexity).
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    pub complexity: Option<f64>,
151    /// Whether this node represents a public API surface.
152    #[serde(default, skip_serializing_if = "Option::is_none")]
153    pub is_public: Option<bool>,
154    /// Full body/content of the node (source code, description text, etc.).
155    #[serde(default, skip_serializing_if = "Option::is_none")]
156    pub body: Option<String>,
157
158    // ── Timestamps ──
159
160    /// ISO-8601 creation timestamp.
161    #[serde(default, skip_serializing_if = "Option::is_none")]
162    pub created_at: Option<String>,
163    /// ISO-8601 last-updated timestamp.
164    #[serde(default, skip_serializing_if = "Option::is_none")]
165    pub updated_at: Option<String>,
166}
167
168impl Node {
169    pub fn new(id: &str, title: &str) -> Self {
170        Self {
171            id: id.to_string(),
172            title: title.to_string(),
173            status: NodeStatus::Todo,
174            description: None,
175            assigned_to: None,
176            tags: Vec::new(),
177            priority: None,
178            node_type: None,
179            knowledge: KnowledgeNode::default(),
180            metadata: HashMap::new(),
181            file_path: None,
182            lang: None,
183            start_line: None,
184            end_line: None,
185            signature: None,
186            visibility: None,
187            doc_comment: None,
188            body_hash: None,
189            node_kind: None,
190            owner: None,
191            source: None,
192            repo: None,
193            parent_id: None,
194            depth: None,
195            complexity: None,
196            is_public: None,
197            body: None,
198            created_at: None,
199            updated_at: None,
200        }
201    }
202
203    pub fn with_description(mut self, desc: &str) -> Self {
204        self.description = Some(desc.to_string());
205        self
206    }
207
208    pub fn with_status(mut self, status: NodeStatus) -> Self {
209        self.status = status;
210        self
211    }
212
213    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
214        self.tags = tags;
215        self
216    }
217
218    pub fn with_priority(mut self, priority: u8) -> Self {
219        self.priority = Some(priority);
220        self
221    }
222}
223
224/// Status of a node.
225#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
226#[serde(rename_all = "lowercase")]
227pub enum NodeStatus {
228    Todo,
229    #[serde(alias = "in_progress", alias = "in-progress")]
230    InProgress,
231    Done,
232    Blocked,
233    Cancelled,
234    /// Task execution failed (verify failed, sub-agent error, etc.)
235    Failed,
236    /// Task needs human/re-planner intervention (merge conflict, structural issue)
237    #[serde(alias = "needs_resolution", alias = "needs-resolution")]
238    NeedsResolution,
239}
240
241impl Default for NodeStatus {
242    fn default() -> Self {
243        Self::Todo
244    }
245}
246
247impl std::fmt::Display for NodeStatus {
248    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249        match self {
250            NodeStatus::Todo => write!(f, "todo"),
251            NodeStatus::InProgress => write!(f, "in_progress"),
252            NodeStatus::Done => write!(f, "done"),
253            NodeStatus::Blocked => write!(f, "blocked"),
254            NodeStatus::Cancelled => write!(f, "cancelled"),
255            NodeStatus::Failed => write!(f, "failed"),
256            NodeStatus::NeedsResolution => write!(f, "needs_resolution"),
257        }
258    }
259}
260
261impl std::str::FromStr for NodeStatus {
262    type Err = anyhow::Error;
263    fn from_str(s: &str) -> Result<Self, Self::Err> {
264        match s {
265            "todo" => Ok(NodeStatus::Todo),
266            "in_progress" | "in-progress" => Ok(NodeStatus::InProgress),
267            "done" => Ok(NodeStatus::Done),
268            "blocked" => Ok(NodeStatus::Blocked),
269            "cancelled" => Ok(NodeStatus::Cancelled),
270            "failed" => Ok(NodeStatus::Failed),
271            "needs_resolution" | "needs-resolution" => Ok(NodeStatus::NeedsResolution),
272            _ => Err(anyhow::anyhow!("Unknown status: {}", s)),
273        }
274    }
275}
276
277/// An edge (relationship) between two nodes.
278#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct Edge {
280    pub from: String,
281    pub to: String,
282    #[serde(default = "default_relation")]
283    pub relation: String,
284    #[serde(default, skip_serializing_if = "Option::is_none")]
285    pub weight: Option<f64>,
286    #[serde(default, skip_serializing_if = "Option::is_none")]
287    pub confidence: Option<f64>,
288    /// Additional edge metadata, serialized as JSON in SQLite.
289    #[serde(default, skip_serializing_if = "Option::is_none")]
290    pub metadata: Option<serde_json::Value>,
291}
292
293fn default_relation() -> String {
294    "depends_on".to_string()
295}
296
297impl Edge {
298    pub fn new(from: &str, to: &str, relation: &str) -> Self {
299        Self {
300            from: from.to_string(),
301            to: to.to_string(),
302            relation: relation.to_string(),
303            weight: None,
304            confidence: None,
305            metadata: None,
306        }
307    }
308
309    pub fn depends_on(from: &str, to: &str) -> Self {
310        Self::new(from, to, "depends_on")
311    }
312}
313
314// ─── Graph operations ────────────────────────────────────────
315
316impl Graph {
317    pub fn new() -> Self {
318        Self::default()
319    }
320
321    // ── Node operations ──
322
323    pub fn get_node(&self, id: &str) -> Option<&Node> {
324        self.nodes.iter().find(|n| n.id == id)
325    }
326
327    pub fn get_node_mut(&mut self, id: &str) -> Option<&mut Node> {
328        self.nodes.iter_mut().find(|n| n.id == id)
329    }
330
331    pub fn add_node(&mut self, node: Node) {
332        if self.get_node(&node.id).is_none() {
333            self.nodes.push(node);
334        }
335    }
336
337    pub fn remove_node(&mut self, id: &str) -> Option<Node> {
338        let pos = self.nodes.iter().position(|n| n.id == id)?;
339        let node = self.nodes.remove(pos);
340        // Remove associated edges
341        self.edges.retain(|e| e.from != id && e.to != id);
342        Some(node)
343    }
344
345    pub fn update_status(&mut self, id: &str, status: NodeStatus) -> bool {
346        if let Some(node) = self.get_node_mut(id) {
347            node.status = status;
348            true
349        } else {
350            false
351        }
352    }
353
354    // ── Edge operations ──
355
356    pub fn add_edge(&mut self, edge: Edge) {
357        // Avoid duplicates
358        let exists = self.edges.iter().any(|e| {
359            e.from == edge.from && e.to == edge.to && e.relation == edge.relation
360        });
361        if !exists {
362            self.edges.push(edge);
363        }
364    }
365
366    pub fn remove_edge(&mut self, from: &str, to: &str, relation: Option<&str>) {
367        self.edges.retain(|e| {
368            !(e.from == from && e.to == to && relation.map_or(true, |r| e.relation == r))
369        });
370    }
371
372    pub fn edges_from(&self, id: &str) -> Vec<&Edge> {
373        self.edges.iter().filter(|e| e.from == id).collect()
374    }
375
376    pub fn edges_to(&self, id: &str) -> Vec<&Edge> {
377        self.edges.iter().filter(|e| e.to == id).collect()
378    }
379
380    // ── Query helpers ──
381
382    /// Get tasks that are ready (todo + all depends_on are done).
383    pub fn ready_tasks(&self) -> Vec<&Node> {
384        self.nodes
385            .iter()
386            .filter(|n| n.status == NodeStatus::Todo)
387            .filter(|n| {
388                let deps: Vec<&Edge> = self.edges_from(&n.id)
389                    .into_iter()
390                    .filter(|e| e.relation == "depends_on")
391                    .collect();
392                deps.iter().all(|e| {
393                    self.get_node(&e.to)
394                        .map_or(true, |dep| dep.status == NodeStatus::Done)
395                })
396            })
397            .collect()
398    }
399
400    /// Get tasks by status.
401    pub fn tasks_by_status(&self, status: &NodeStatus) -> Vec<&Node> {
402        self.nodes.iter().filter(|n| &n.status == status).collect()
403    }
404
405    /// Summary statistics.
406    pub fn summary(&self) -> GraphSummary {
407        let mut s = GraphSummary {
408            total_nodes: self.nodes.len(),
409            total_edges: self.edges.len(),
410            ..Default::default()
411        };
412        for n in &self.nodes {
413            match n.status {
414                NodeStatus::Todo => s.todo += 1,
415                NodeStatus::InProgress => s.in_progress += 1,
416                NodeStatus::Done => s.done += 1,
417                NodeStatus::Blocked => s.blocked += 1,
418                NodeStatus::Cancelled => s.cancelled += 1,
419                NodeStatus::Failed => s.failed += 1,
420                NodeStatus::NeedsResolution => s.needs_resolution += 1,
421            }
422        }
423        s.ready = self.ready_tasks().len();
424        s
425    }
426
427    /// Get a human-readable text summary of the graph state.
428    pub fn summary_text(&self) -> String {
429        let s = self.summary();
430        let mut lines = vec![
431            format!("Graph: {} nodes, {} edges", s.total_nodes, s.total_edges),
432        ];
433
434        if s.total_nodes > 0 {
435            lines.push(format!(
436                "Status: {} todo, {} in-progress, {} done, {} blocked, {} cancelled",
437                s.todo, s.in_progress, s.done, s.blocked, s.cancelled
438            ));
439            lines.push(format!("Ready tasks: {}", s.ready));
440        }
441
442        // Show project name if available
443        if let Some(ref project) = self.project {
444            lines.insert(0, format!("Project: {}", project.name));
445        }
446
447        lines.join("\n")
448    }
449
450    /// Calculate graph health score (0.0 to 1.0).
451    /// 
452    /// Health is based on:
453    /// - Progress: ratio of done tasks to total
454    /// - Flow: ratio of ready tasks to remaining (non-blocked) tasks
455    /// - Connectivity: graphs with edges are healthier than isolated nodes
456    /// 
457    /// Returns 1.0 for a fully complete graph, 0.0 for an empty or stuck graph.
458    pub fn health(&self) -> f64 {
459        if self.nodes.is_empty() {
460            return 0.0;
461        }
462
463        let s = self.summary();
464        let total = s.total_nodes as f64;
465
466        // Progress score: what fraction is done?
467        let progress = s.done as f64 / total;
468
469        // Flow score: are there ready tasks to work on? (avoid stuck graphs)
470        let remaining = s.todo + s.in_progress;
471        let flow = if remaining == 0 {
472            1.0 // All done, perfect flow
473        } else if s.ready == 0 && s.todo > 0 {
474            0.0 // Stuck: todos exist but none are ready (all blocked by dependencies)
475        } else {
476            (s.ready as f64) / (remaining as f64)
477        };
478
479        // Connectivity score: graphs with structure are healthier
480        let connectivity = if self.nodes.len() > 1 {
481            let max_edges = self.nodes.len() * (self.nodes.len() - 1);
482            let actual = self.edges.len().min(max_edges);
483            (actual as f64 / max_edges as f64).min(1.0)
484        } else {
485            1.0 // Single node is "connected"
486        };
487
488        // Blocked penalty: heavily blocked graphs are unhealthy
489        let blocked_ratio = s.blocked as f64 / total;
490        let blocked_penalty = 1.0 - blocked_ratio;
491
492        // Weighted combination
493        let health = 0.4 * progress + 0.3 * flow + 0.1 * connectivity + 0.2 * blocked_penalty;
494        health.clamp(0.0, 1.0)
495    }
496
497    /// Mark a task as done. Returns true if found and updated.
498    pub fn mark_task_done(&mut self, node_id: &str) -> bool {
499        self.update_status(node_id, NodeStatus::Done)
500    }
501
502    /// Get executable tasks (alias for ready_tasks, returns owned Task structs).
503    pub fn get_executable_tasks(&self) -> Vec<Task> {
504        self.ready_tasks()
505            .into_iter()
506            .map(|node| Task {
507                id: node.id.clone(),
508                title: node.title.clone(),
509                description: node.description.clone(),
510                priority: node.priority,
511            })
512            .collect()
513    }
514}
515
516/// A simplified task representation for execution.
517#[derive(Debug, Clone)]
518pub struct Task {
519    pub id: String,
520    pub title: String,
521    pub description: Option<String>,
522    pub priority: Option<u8>,
523}
524
525#[derive(Debug, Default)]
526pub struct GraphSummary {
527    pub total_nodes: usize,
528    pub total_edges: usize,
529    pub todo: usize,
530    pub in_progress: usize,
531    pub done: usize,
532    pub blocked: usize,
533    pub cancelled: usize,
534    pub failed: usize,
535    pub needs_resolution: usize,
536    pub ready: usize,
537}
538
539// Implement knowledge management for Graph so users can call
540// graph.store_finding(), graph.cache_file(), etc. directly.
541impl KnowledgeGraph for Graph {
542    fn get_knowledge_mut(&mut self, node_id: &str) -> Option<&mut KnowledgeNode> {
543        self.nodes.iter_mut()
544            .find(|n| n.id == node_id)
545            .map(|n| &mut n.knowledge)
546    }
547
548    fn get_knowledge(&self, node_id: &str) -> Option<&KnowledgeNode> {
549        self.nodes.iter()
550            .find(|n| n.id == node_id)
551            .map(|n| &n.knowledge)
552    }
553
554    fn get_incoming_edges(&self, node_id: &str) -> Vec<String> {
555        self.edges.iter()
556            .filter(|e| e.to == node_id)
557            .map(|e| e.from.clone())
558            .collect()
559    }
560}
561
562impl KnowledgeManagement for Graph {}
563
564impl std::fmt::Display for GraphSummary {
565    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
566        write!(
567            f,
568            "{} nodes, {} edges | todo={} progress={} done={} blocked={} failed={} cancelled={} | ready={}",
569            self.total_nodes, self.total_edges,
570            self.todo, self.in_progress, self.done, self.blocked, self.failed, self.cancelled,
571            self.ready,
572        )
573    }
574}