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)]
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#[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 #[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
84 pub node_type: Option<String>,
85 #[serde(default, skip_serializing_if = "KnowledgeNode::is_empty")]
87 pub knowledge: KnowledgeNode,
88 #[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#[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 Failed,
142 #[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#[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
216impl Graph {
219 pub fn new() -> Self {
220 Self::default()
221 }
222
223 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 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 pub fn add_edge(&mut self, edge: Edge) {
259 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 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 pub fn tasks_by_status(&self, status: &NodeStatus) -> Vec<&Node> {
304 self.nodes.iter().filter(|n| &n.status == status).collect()
305 }
306
307 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 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 if let Some(ref project) = self.project {
346 lines.insert(0, format!("Project: {}", project.name));
347 }
348
349 lines.join("\n")
350 }
351
352 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 let progress = s.done as f64 / total;
370
371 let remaining = s.todo + s.in_progress;
373 let flow = if remaining == 0 {
374 1.0 } else if s.ready == 0 && s.todo > 0 {
376 0.0 } else {
378 (s.ready as f64) / (remaining as f64)
379 };
380
381 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 };
389
390 let blocked_ratio = s.blocked as f64 / total;
392 let blocked_penalty = 1.0 - blocked_ratio;
393
394 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 pub fn mark_task_done(&mut self, node_id: &str) -> bool {
401 self.update_status(node_id, NodeStatus::Done)
402 }
403
404 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#[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
441impl 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}