use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::task_graph_knowledge::{KnowledgeNode, KnowledgeGraph, KnowledgeManagement};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Graph {
#[serde(default)]
pub project: Option<ProjectMeta>,
#[serde(default)]
pub nodes: Vec<Node>,
#[serde(default)]
pub edges: Vec<Edge>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectMeta {
pub name: String,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Node {
pub id: String,
pub title: String,
#[serde(default)]
pub status: NodeStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub assigned_to: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub priority: Option<u8>,
#[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
pub node_type: Option<String>,
#[serde(default, flatten)]
pub knowledge: KnowledgeNode,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, serde_json::Value>,
}
impl Node {
pub fn new(id: &str, title: &str) -> Self {
Self {
id: id.to_string(),
title: title.to_string(),
status: NodeStatus::Todo,
description: None,
assigned_to: None,
tags: Vec::new(),
priority: None,
node_type: None,
knowledge: KnowledgeNode::default(),
metadata: HashMap::new(),
}
}
pub fn with_description(mut self, desc: &str) -> Self {
self.description = Some(desc.to_string());
self
}
pub fn with_status(mut self, status: NodeStatus) -> Self {
self.status = status;
self
}
pub fn with_tags(mut self, tags: Vec<String>) -> Self {
self.tags = tags;
self
}
pub fn with_priority(mut self, priority: u8) -> Self {
self.priority = Some(priority);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum NodeStatus {
Todo,
#[serde(alias = "in_progress", alias = "in-progress")]
InProgress,
Done,
Blocked,
Cancelled,
}
impl Default for NodeStatus {
fn default() -> Self {
Self::Todo
}
}
impl std::fmt::Display for NodeStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NodeStatus::Todo => write!(f, "todo"),
NodeStatus::InProgress => write!(f, "in_progress"),
NodeStatus::Done => write!(f, "done"),
NodeStatus::Blocked => write!(f, "blocked"),
NodeStatus::Cancelled => write!(f, "cancelled"),
}
}
}
impl std::str::FromStr for NodeStatus {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"todo" => Ok(NodeStatus::Todo),
"in_progress" | "in-progress" => Ok(NodeStatus::InProgress),
"done" => Ok(NodeStatus::Done),
"blocked" => Ok(NodeStatus::Blocked),
"cancelled" => Ok(NodeStatus::Cancelled),
_ => Err(anyhow::anyhow!("Unknown status: {}", s)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Edge {
pub from: String,
pub to: String,
#[serde(default = "default_relation")]
pub relation: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub weight: Option<f64>,
}
fn default_relation() -> String {
"depends_on".to_string()
}
impl Edge {
pub fn new(from: &str, to: &str, relation: &str) -> Self {
Self {
from: from.to_string(),
to: to.to_string(),
relation: relation.to_string(),
weight: None,
}
}
pub fn depends_on(from: &str, to: &str) -> Self {
Self::new(from, to, "depends_on")
}
}
impl Graph {
pub fn new() -> Self {
Self::default()
}
pub fn get_node(&self, id: &str) -> Option<&Node> {
self.nodes.iter().find(|n| n.id == id)
}
pub fn get_node_mut(&mut self, id: &str) -> Option<&mut Node> {
self.nodes.iter_mut().find(|n| n.id == id)
}
pub fn add_node(&mut self, node: Node) {
if self.get_node(&node.id).is_none() {
self.nodes.push(node);
}
}
pub fn remove_node(&mut self, id: &str) -> Option<Node> {
let pos = self.nodes.iter().position(|n| n.id == id)?;
let node = self.nodes.remove(pos);
self.edges.retain(|e| e.from != id && e.to != id);
Some(node)
}
pub fn update_status(&mut self, id: &str, status: NodeStatus) -> bool {
if let Some(node) = self.get_node_mut(id) {
node.status = status;
true
} else {
false
}
}
pub fn add_edge(&mut self, edge: Edge) {
let exists = self.edges.iter().any(|e| {
e.from == edge.from && e.to == edge.to && e.relation == edge.relation
});
if !exists {
self.edges.push(edge);
}
}
pub fn remove_edge(&mut self, from: &str, to: &str, relation: Option<&str>) {
self.edges.retain(|e| {
!(e.from == from && e.to == to && relation.map_or(true, |r| e.relation == r))
});
}
pub fn edges_from(&self, id: &str) -> Vec<&Edge> {
self.edges.iter().filter(|e| e.from == id).collect()
}
pub fn edges_to(&self, id: &str) -> Vec<&Edge> {
self.edges.iter().filter(|e| e.to == id).collect()
}
pub fn ready_tasks(&self) -> Vec<&Node> {
self.nodes
.iter()
.filter(|n| n.status == NodeStatus::Todo)
.filter(|n| {
let deps: Vec<&Edge> = self.edges_from(&n.id)
.into_iter()
.filter(|e| e.relation == "depends_on")
.collect();
deps.iter().all(|e| {
self.get_node(&e.to)
.map_or(true, |dep| dep.status == NodeStatus::Done)
})
})
.collect()
}
pub fn tasks_by_status(&self, status: &NodeStatus) -> Vec<&Node> {
self.nodes.iter().filter(|n| &n.status == status).collect()
}
pub fn summary(&self) -> GraphSummary {
let mut s = GraphSummary {
total_nodes: self.nodes.len(),
total_edges: self.edges.len(),
..Default::default()
};
for n in &self.nodes {
match n.status {
NodeStatus::Todo => s.todo += 1,
NodeStatus::InProgress => s.in_progress += 1,
NodeStatus::Done => s.done += 1,
NodeStatus::Blocked => s.blocked += 1,
NodeStatus::Cancelled => s.cancelled += 1,
}
}
s.ready = self.ready_tasks().len();
s
}
pub fn summary_text(&self) -> String {
let s = self.summary();
let mut lines = vec![
format!("Graph: {} nodes, {} edges", s.total_nodes, s.total_edges),
];
if s.total_nodes > 0 {
lines.push(format!(
"Status: {} todo, {} in-progress, {} done, {} blocked, {} cancelled",
s.todo, s.in_progress, s.done, s.blocked, s.cancelled
));
lines.push(format!("Ready tasks: {}", s.ready));
}
if let Some(ref project) = self.project {
lines.insert(0, format!("Project: {}", project.name));
}
lines.join("\n")
}
pub fn health(&self) -> f64 {
if self.nodes.is_empty() {
return 0.0;
}
let s = self.summary();
let total = s.total_nodes as f64;
let progress = s.done as f64 / total;
let remaining = s.todo + s.in_progress;
let flow = if remaining == 0 {
1.0 } else if s.ready == 0 && s.todo > 0 {
0.0 } else {
(s.ready as f64) / (remaining as f64)
};
let connectivity = if self.nodes.len() > 1 {
let max_edges = self.nodes.len() * (self.nodes.len() - 1);
let actual = self.edges.len().min(max_edges);
(actual as f64 / max_edges as f64).min(1.0)
} else {
1.0 };
let blocked_ratio = s.blocked as f64 / total;
let blocked_penalty = 1.0 - blocked_ratio;
let health = 0.4 * progress + 0.3 * flow + 0.1 * connectivity + 0.2 * blocked_penalty;
health.clamp(0.0, 1.0)
}
pub fn mark_task_done(&mut self, node_id: &str) -> bool {
self.update_status(node_id, NodeStatus::Done)
}
pub fn get_executable_tasks(&self) -> Vec<Task> {
self.ready_tasks()
.into_iter()
.map(|node| Task {
id: node.id.clone(),
title: node.title.clone(),
description: node.description.clone(),
priority: node.priority,
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct Task {
pub id: String,
pub title: String,
pub description: Option<String>,
pub priority: Option<u8>,
}
#[derive(Debug, Default)]
pub struct GraphSummary {
pub total_nodes: usize,
pub total_edges: usize,
pub todo: usize,
pub in_progress: usize,
pub done: usize,
pub blocked: usize,
pub cancelled: usize,
pub ready: usize,
}
impl KnowledgeGraph for Graph {
fn get_knowledge_mut(&mut self, node_id: &str) -> Option<&mut KnowledgeNode> {
self.nodes.iter_mut()
.find(|n| n.id == node_id)
.map(|n| &mut n.knowledge)
}
fn get_knowledge(&self, node_id: &str) -> Option<&KnowledgeNode> {
self.nodes.iter()
.find(|n| n.id == node_id)
.map(|n| &n.knowledge)
}
fn get_incoming_edges(&self, node_id: &str) -> Vec<String> {
self.edges.iter()
.filter(|e| e.to == node_id)
.map(|e| e.from.clone())
.collect()
}
}
impl KnowledgeManagement for Graph {}
impl std::fmt::Display for GraphSummary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} nodes, {} edges | todo={} progress={} done={} blocked={} cancelled={} | ready={}",
self.total_nodes, self.total_edges,
self.todo, self.in_progress, self.done, self.blocked, self.cancelled,
self.ready,
)
}
}