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")]
82 pub priority: Option<u8>,
83 #[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
85 pub node_type: Option<String>,
86 #[serde(default, skip_serializing_if = "KnowledgeNode::is_empty")]
88 pub knowledge: KnowledgeNode,
89 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
91 pub metadata: HashMap<String, serde_json::Value>,
92
93 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub file_path: Option<String>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub lang: Option<String>,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub start_line: Option<usize>,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub end_line: Option<usize>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub signature: Option<String>,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub visibility: Option<String>,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub doc_comment: Option<String>,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub body_hash: Option<String>,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub node_kind: Option<String>,
124
125 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub owner: Option<String>,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub source: Option<String>,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub repo: Option<String>,
136
137 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub parent_id: Option<String>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub depth: Option<u32>,
145
146 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub complexity: Option<f64>,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub is_public: Option<bool>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub body: Option<String>,
157
158 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub created_at: Option<String>,
163 #[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#[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 Failed,
236 #[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#[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 #[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
314impl Graph {
317 pub fn new() -> Self {
318 Self::default()
319 }
320
321 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 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 pub fn add_edge(&mut self, edge: Edge) {
357 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 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 pub fn tasks_by_status(&self, status: &NodeStatus) -> Vec<&Node> {
402 self.nodes.iter().filter(|n| &n.status == status).collect()
403 }
404
405 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 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 if let Some(ref project) = self.project {
444 lines.insert(0, format!("Project: {}", project.name));
445 }
446
447 lines.join("\n")
448 }
449
450 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 let progress = s.done as f64 / total;
468
469 let remaining = s.todo + s.in_progress;
471 let flow = if remaining == 0 {
472 1.0 } else if s.ready == 0 && s.todo > 0 {
474 0.0 } else {
476 (s.ready as f64) / (remaining as f64)
477 };
478
479 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 };
487
488 let blocked_ratio = s.blocked as f64 / total;
490 let blocked_penalty = 1.0 - blocked_ratio;
491
492 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 pub fn mark_task_done(&mut self, node_id: &str) -> bool {
499 self.update_status(node_id, NodeStatus::Done)
500 }
501
502 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#[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
539impl 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}