1use std::collections::HashMap;
2use serde::{Deserialize, Serialize};
3use crate::task_graph_knowledge::{KnowledgeNode, KnowledgeGraph, KnowledgeManagement};
4
5#[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#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ProjectMeta {
19 pub name: String,
20 #[serde(default)]
21 pub description: Option<String>,
22}
23
24#[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 #[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
41 pub node_type: Option<String>,
42 #[serde(default, skip_serializing_if = "KnowledgeNode::is_empty")]
44 pub knowledge: KnowledgeNode,
45 #[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#[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#[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
161impl Graph {
164 pub fn new() -> Self {
165 Self::default()
166 }
167
168 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 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 pub fn add_edge(&mut self, edge: Edge) {
204 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 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 pub fn tasks_by_status(&self, status: &NodeStatus) -> Vec<&Node> {
249 self.nodes.iter().filter(|n| &n.status == status).collect()
250 }
251
252 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 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 if let Some(ref project) = self.project {
289 lines.insert(0, format!("Project: {}", project.name));
290 }
291
292 lines.join("\n")
293 }
294
295 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 let progress = s.done as f64 / total;
313
314 let remaining = s.todo + s.in_progress;
316 let flow = if remaining == 0 {
317 1.0 } else if s.ready == 0 && s.todo > 0 {
319 0.0 } else {
321 (s.ready as f64) / (remaining as f64)
322 };
323
324 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 };
332
333 let blocked_ratio = s.blocked as f64 / total;
335 let blocked_penalty = 1.0 - blocked_ratio;
336
337 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 pub fn mark_task_done(&mut self, node_id: &str) -> bool {
344 self.update_status(node_id, NodeStatus::Done)
345 }
346
347 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#[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
382impl 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}