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 pub fn source(&self) -> Option<&str> {
315 self.metadata.as_ref()
316 .and_then(|m| m.get("source"))
317 .and_then(|v| v.as_str())
318 }
319}
320
321#[derive(Debug, Clone)]
323pub struct TaskSpec {
324 pub title: String,
325 pub status: Option<NodeStatus>, pub tags: Vec<String>, pub deps: Vec<String>, }
329
330pub fn infer_node_type(id: &str) -> Option<&str> {
343 let prefix = id.split(':').next()?;
344 match prefix {
345 "file" => Some("file"),
346 "fn" | "func" => Some("function"),
347 "struct" | "class" => Some("class"),
348 "mod" | "module" => Some("module"),
349 "method" => Some("method"),
350 "trait" | "interface" => Some("trait"),
351 "enum" => Some("enum"),
352 "const" | "static" => Some("constant"),
353 "test" => Some("test"),
354 "impl" => Some("impl"),
355 _ => None,
356 }
357}
358
359impl Graph {
362 pub fn new() -> Self {
363 Self::default()
364 }
365
366 pub fn get_node(&self, id: &str) -> Option<&Node> {
369 self.nodes.iter().find(|n| n.id == id)
370 }
371
372 pub fn get_node_mut(&mut self, id: &str) -> Option<&mut Node> {
373 self.nodes.iter_mut().find(|n| n.id == id)
374 }
375
376 pub fn add_node(&mut self, node: Node) {
377 if self.get_node(&node.id).is_none() {
378 let mut node = node;
379 if node.node_type.is_none() {
381 if let Some(inferred) = infer_node_type(&node.id) {
382 node.node_type = Some(inferred.to_string());
383 }
384 }
385 self.nodes.push(node);
386 }
387 }
388
389 pub fn remove_node(&mut self, id: &str) -> Option<Node> {
390 let pos = self.nodes.iter().position(|n| n.id == id)?;
391 let node = self.nodes.remove(pos);
392 self.edges.retain(|e| e.from != id && e.to != id);
394 Some(node)
395 }
396
397 pub fn update_status(&mut self, id: &str, status: NodeStatus) -> bool {
398 if let Some(node) = self.get_node_mut(id) {
399 node.status = status;
400 true
401 } else {
402 false
403 }
404 }
405
406 pub fn add_edge(&mut self, edge: Edge) {
409 let exists = self.edges.iter().any(|e| {
411 e.from == edge.from && e.to == edge.to && e.relation == edge.relation
412 });
413 if !exists {
414 self.edges.push(edge);
415 }
416 }
417
418 pub fn remove_edge(&mut self, from: &str, to: &str, relation: Option<&str>) {
419 self.edges.retain(|e| {
420 !(e.from == from && e.to == to && relation.map_or(true, |r| e.relation == r))
421 });
422 }
423
424 pub fn add_edge_dedup(&mut self, edge: Edge) -> bool {
440 let exists = self.edges.iter().any(|e| {
441 e.from == edge.from && e.to == edge.to && e.relation == edge.relation
442 });
443 if !exists {
444 self.edges.push(edge);
445 true
446 } else {
447 false
448 }
449 }
450
451 fn ensure_unique_id(&self, base: String) -> String {
453 if self.get_node(&base).is_none() {
454 return base;
455 }
456 for i in 2..1000 {
457 let candidate = format!("{}-{}", base, i);
458 if self.get_node(&candidate).is_none() {
459 return candidate;
460 }
461 }
462 format!("{}-overflow", base)
463 }
464
465 pub fn add_feature(&mut self, name: &str, tasks: &[TaskSpec]) -> String {
473 use crate::slugify::slugify;
474
475 let feature_slug = slugify(name);
476 let feat_id = self.ensure_unique_id(format!("feat-{}", feature_slug));
477
478 let mut feat = Node::new(&feat_id, name);
479 feat.node_type = Some("feature".into());
480 feat.status = NodeStatus::Todo;
481 self.add_node(feat);
482
483 let mut task_ids: HashMap<String, String> = HashMap::new();
485
486 for spec in tasks {
487 let task_slug = slugify(&spec.title);
488 let base_id = format!("task-{}-{}", feature_slug, task_slug);
489 let task_id = self.ensure_unique_id(base_id);
490
491 let mut task = Node::new(&task_id, &spec.title);
492 task.node_type = Some("task".into());
493 task.status = spec.status.clone().unwrap_or(NodeStatus::Todo);
494 task.tags = spec.tags.clone();
495 self.add_node(task);
496
497 self.add_edge_dedup(Edge::new(&task_id, &feat_id, "implements"));
499
500 task_ids.insert(spec.title.clone(), task_id);
501 }
502
503 for spec in tasks {
505 if let Some(from_id) = task_ids.get(&spec.title) {
506 for dep_title in &spec.deps {
507 if let Some(to_id) = task_ids.get(dep_title.as_str()) {
508 self.add_edge_dedup(Edge::new(from_id, to_id, "depends_on"));
509 }
510 }
511 }
512 }
513
514 feat_id
515 }
516
517 pub fn add_task(
520 &mut self,
521 title: &str,
522 for_feature: Option<&str>,
523 depends_on: &[String],
524 tags: &[String],
525 priority: Option<u8>,
526 ) -> String {
527 use crate::slugify::slugify;
528
529 let task_slug = slugify(title);
530 let base_id = if let Some(feat_id) = for_feature {
531 let feat_slug = feat_id.strip_prefix("feat-").unwrap_or(feat_id);
533 format!("task-{}-{}", feat_slug, task_slug)
534 } else {
535 format!("task-{}", task_slug)
536 };
537 let task_id = self.ensure_unique_id(base_id);
538
539 let mut task = Node::new(&task_id, title);
540 task.node_type = Some("task".into());
541 task.status = NodeStatus::Todo;
542 task.tags = tags.to_vec();
543 task.priority = priority;
544 self.add_node(task);
545
546 if let Some(feat_id) = for_feature {
548 self.add_edge_dedup(Edge::new(&task_id, feat_id, "implements"));
549 }
550
551 for dep in depends_on {
553 let resolved = self.resolve_node(dep);
554 if let Some(dep_node) = resolved.first() {
555 let dep_id = dep_node.id.clone();
556 self.add_edge_dedup(Edge::new(&task_id, &dep_id, "depends_on"));
557 } else {
558 eprintln!("⚠ Could not resolve dependency: {}", dep);
559 }
560 }
561
562 task_id
563 }
564
565 pub fn merge_feature_nodes(&mut self, feature_id: &str, incoming: Graph) -> (usize, usize) {
575 let old_task_ids: Vec<String> = self.edges.iter()
577 .filter(|e| e.to == feature_id && e.relation == "implements")
578 .map(|e| e.from.clone())
579 .collect();
580
581 let removed = old_task_ids.len();
582
583 for id in &old_task_ids {
585 self.remove_node(id);
586 }
587
588 let incoming_node_ids: std::collections::HashSet<String> = incoming.nodes.iter()
590 .map(|n| n.id.clone())
591 .collect();
592 let added = incoming.nodes.len();
593
594 for node in incoming.nodes {
596 self.add_node(node);
597 }
598
599 for id in &incoming_node_ids {
601 self.add_edge_dedup(Edge::new(id, feature_id, "implements"));
602 }
603
604 for edge in incoming.edges {
606 self.add_edge_dedup(edge);
607 }
608
609 (removed, added)
610 }
611
612 pub fn resolve_node(&self, reference: &str) -> Vec<&Node> {
649 let reference_lower = reference.to_lowercase();
650
651 if let Some(node) = self.nodes.iter().find(|n| n.id == reference) {
653 return vec![node];
654 }
655
656 let exact_title: Vec<&Node> = self.nodes.iter()
658 .filter(|n| n.title.to_lowercase() == reference_lower)
659 .collect();
660 if !exact_title.is_empty() {
661 return exact_title;
662 }
663
664 let structural_segments = extract_segments(&reference_lower, &[':', '-', '/']);
666 if !structural_segments.is_empty() {
667 let matches: Vec<&Node> = self.nodes.iter()
668 .filter(|n| {
669 let id_segs = extract_segments(&n.id.to_lowercase(), &[':', '-', '/']);
670 let title_segs = extract_segments(&n.title.to_lowercase(), &[':', '-', '/']);
671 segments_match(&structural_segments, &id_segs) ||
672 segments_match(&structural_segments, &title_segs)
673 })
674 .collect();
675 if !matches.is_empty() {
676 return matches;
677 }
678 }
679
680 let word_segments = extract_segments(&reference_lower, &['_']);
682 if !word_segments.is_empty() {
683 let matches: Vec<&Node> = self.nodes.iter()
684 .filter(|n| {
685 let id_segs = extract_segments(&n.id.to_lowercase(), &['_']);
686 let title_segs = extract_segments(&n.title.to_lowercase(), &['_']);
687 segments_match(&word_segments, &id_segs) ||
688 segments_match(&word_segments, &title_segs)
689 })
690 .collect();
691 if !matches.is_empty() {
692 return matches;
693 }
694 }
695
696 let matches: Vec<&Node> = self.nodes.iter()
698 .filter(|n| {
699 n.file_path.as_ref()
700 .map(|fp| fp.to_lowercase().contains(&reference_lower))
701 .unwrap_or(false)
702 })
703 .collect();
704 if !matches.is_empty() {
705 return matches;
706 }
707
708 let matches: Vec<&Node> = self.nodes.iter()
710 .filter(|n| n.title.to_lowercase().contains(&reference_lower))
711 .collect();
712 if !matches.is_empty() {
713 return matches;
714 }
715
716 let matches: Vec<&Node> = self.nodes.iter()
718 .filter(|n| n.id.to_lowercase().contains(&reference_lower))
719 .collect();
720
721 matches
722 }
723
724 pub fn edges_from(&self, id: &str) -> Vec<&Edge> {
725 self.edges.iter().filter(|e| e.from == id).collect()
726 }
727
728 pub fn edges_to(&self, id: &str) -> Vec<&Edge> {
729 self.edges.iter().filter(|e| e.to == id).collect()
730 }
731
732 pub fn code_nodes(&self) -> Vec<&Node> {
736 self.nodes.iter().filter(|n| n.source.as_deref() == Some("extract")).collect()
737 }
738
739 pub fn project_nodes(&self) -> Vec<&Node> {
741 self.nodes.iter().filter(|n| {
743 n.source.as_deref().map_or(true, |s| s == "project")
744 }).collect()
745 }
746
747 pub fn code_edges(&self) -> Vec<&Edge> {
749 self.edges.iter().filter(|e| e.source() == Some("extract")).collect()
750 }
751
752 pub fn project_edges(&self) -> Vec<&Edge> {
754 self.edges.iter().filter(|e| {
755 let src = e.source();
756 src != Some("extract") && src != Some("auto-bridge")
757 }).collect()
758 }
759
760 pub fn bridge_edges(&self) -> Vec<&Edge> {
762 self.edges.iter().filter(|e| e.source() == Some("auto-bridge")).collect()
763 }
764
765 pub fn ready_tasks(&self) -> Vec<&Node> {
770 let status_map: HashMap<&str, &NodeStatus> = self
772 .nodes
773 .iter()
774 .map(|n| (n.id.as_str(), &n.status))
775 .collect();
776
777 let mut dep_edges: HashMap<&str, Vec<&Edge>> = HashMap::new();
779 for e in &self.edges {
780 if e.relation == "depends_on" {
781 dep_edges.entry(e.from.as_str()).or_default().push(e);
782 }
783 }
784
785 self.project_nodes()
786 .into_iter()
787 .filter(|n| n.status == NodeStatus::Todo)
788 .filter(|n| {
789 match dep_edges.get(n.id.as_str()) {
790 None => true, Some(deps) => deps.iter().all(|e| {
792 status_map
793 .get(e.to.as_str())
794 .map_or(true, |s| **s == NodeStatus::Done)
795 }),
796 }
797 })
798 .collect()
799 }
800
801 pub fn tasks_by_status(&self, status: &NodeStatus) -> Vec<&Node> {
803 self.nodes.iter().filter(|n| &n.status == status).collect()
804 }
805
806 pub fn summary(&self) -> GraphSummary {
808 let project_nodes = self.project_nodes();
809 let mut s = GraphSummary {
810 total_nodes: project_nodes.len(),
811 total_edges: self.edges.len(),
812 ..Default::default()
813 };
814 for n in &project_nodes {
815 match n.status {
816 NodeStatus::Todo => s.todo += 1,
817 NodeStatus::InProgress => s.in_progress += 1,
818 NodeStatus::Done => s.done += 1,
819 NodeStatus::Blocked => s.blocked += 1,
820 NodeStatus::Cancelled => s.cancelled += 1,
821 NodeStatus::Failed => s.failed += 1,
822 NodeStatus::NeedsResolution => s.needs_resolution += 1,
823 }
824 }
825 s.ready = self.ready_tasks().len();
826 s
827 }
828
829 pub fn summary_text(&self) -> String {
831 let s = self.summary();
832 let mut lines = vec![
833 format!("Graph: {} nodes, {} edges", s.total_nodes, s.total_edges),
834 ];
835
836 if s.total_nodes > 0 {
837 lines.push(format!(
838 "Status: {} todo, {} in-progress, {} done, {} blocked, {} cancelled",
839 s.todo, s.in_progress, s.done, s.blocked, s.cancelled
840 ));
841 lines.push(format!("Ready tasks: {}", s.ready));
842 }
843
844 if let Some(ref project) = self.project {
846 lines.insert(0, format!("Project: {}", project.name));
847 }
848
849 lines.join("\n")
850 }
851
852 pub fn health(&self) -> f64 {
861 if self.nodes.is_empty() {
862 return 0.0;
863 }
864
865 let s = self.summary();
866 let total = s.total_nodes as f64;
867
868 let progress = s.done as f64 / total;
870
871 let remaining = s.todo + s.in_progress;
873 let flow = if remaining == 0 {
874 1.0 } else if s.ready == 0 && s.todo > 0 {
876 0.0 } else {
878 (s.ready as f64) / (remaining as f64)
879 };
880
881 let connectivity = if self.nodes.len() > 1 {
883 let max_edges = self.nodes.len() * (self.nodes.len() - 1);
884 let actual = self.edges.len().min(max_edges);
885 (actual as f64 / max_edges as f64).min(1.0)
886 } else {
887 1.0 };
889
890 let blocked_ratio = s.blocked as f64 / total;
892 let blocked_penalty = 1.0 - blocked_ratio;
893
894 let health = 0.4 * progress + 0.3 * flow + 0.1 * connectivity + 0.2 * blocked_penalty;
896 health.clamp(0.0, 1.0)
897 }
898
899 pub fn mark_task_done(&mut self, node_id: &str) -> bool {
901 self.update_status(node_id, NodeStatus::Done)
902 }
903
904 pub fn get_executable_tasks(&self) -> Vec<Task> {
906 self.ready_tasks()
907 .into_iter()
908 .map(|node| Task {
909 id: node.id.clone(),
910 title: node.title.clone(),
911 description: node.description.clone(),
912 priority: node.priority,
913 })
914 .collect()
915 }
916}
917
918#[derive(Debug, Clone)]
920pub struct Task {
921 pub id: String,
922 pub title: String,
923 pub description: Option<String>,
924 pub priority: Option<u8>,
925}
926
927#[derive(Debug, Default)]
928pub struct GraphSummary {
929 pub total_nodes: usize,
930 pub total_edges: usize,
931 pub todo: usize,
932 pub in_progress: usize,
933 pub done: usize,
934 pub blocked: usize,
935 pub cancelled: usize,
936 pub failed: usize,
937 pub needs_resolution: usize,
938 pub ready: usize,
939}
940
941impl KnowledgeGraph for Graph {
944 fn get_knowledge_mut(&mut self, node_id: &str) -> Option<&mut KnowledgeNode> {
945 self.nodes.iter_mut()
946 .find(|n| n.id == node_id)
947 .map(|n| &mut n.knowledge)
948 }
949
950 fn get_knowledge(&self, node_id: &str) -> Option<&KnowledgeNode> {
951 self.nodes.iter()
952 .find(|n| n.id == node_id)
953 .map(|n| &n.knowledge)
954 }
955
956 fn get_incoming_edges(&self, node_id: &str) -> Vec<String> {
957 self.edges.iter()
958 .filter(|e| e.to == node_id)
959 .map(|e| e.from.clone())
960 .collect()
961 }
962}
963
964impl KnowledgeManagement for Graph {}
965
966impl std::fmt::Display for GraphSummary {
967 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
968 write!(
969 f,
970 "{} nodes, {} edges | todo={} progress={} done={} blocked={} failed={} cancelled={} | ready={}",
971 self.total_nodes, self.total_edges,
972 self.todo, self.in_progress, self.done, self.blocked, self.failed, self.cancelled,
973 self.ready,
974 )
975 }
976}
977
978
979fn extract_segments(text: &str, delimiters: &[char]) -> Vec<String> {
983 let mut segments = vec![text.to_string()];
984
985 for &delimiter in delimiters {
986 let mut new_segments = Vec::new();
987 for segment in segments {
988 new_segments.extend(
989 segment.split(delimiter)
990 .filter(|s| !s.is_empty())
991 .map(|s| s.to_string())
992 );
993 }
994 segments = new_segments;
995 }
996
997 segments
998}
999
1000fn segments_match(query_segments: &[String], target_segments: &[String]) -> bool {
1002 if query_segments.is_empty() {
1003 return false;
1004 }
1005 query_segments.iter().all(|q| target_segments.contains(q))
1006}
1007
1008#[cfg(test)]
1009mod layer_filter_tests {
1010 use super::*;
1011
1012 fn mixed_graph() -> Graph {
1013 let mut g = Graph::new();
1014 let mut task = Node::new("task-1", "My Task");
1016 task.source = Some("project".to_string());
1017 g.add_node(task);
1018
1019 let legacy = Node::new("legacy-1", "Legacy Task");
1020 g.add_node(legacy);
1022
1023 let mut code = Node::new("fn:main", "main function");
1025 code.source = Some("extract".to_string());
1026 code.node_type = Some("code".to_string());
1027 code.status = NodeStatus::Done;
1028 g.add_node(code);
1029
1030 let mut code2 = Node::new("struct:Config", "Config struct");
1031 code2.source = Some("extract".to_string());
1032 code2.node_type = Some("code".to_string());
1033 code2.status = NodeStatus::Done;
1034 g.add_node(code2);
1035
1036 g.add_edge(Edge::new("task-1", "legacy-1", "depends_on"));
1038
1039 let mut code_edge = Edge::new("fn:main", "struct:Config", "calls");
1040 code_edge.metadata = Some(serde_json::json!({"source": "extract"}));
1041 g.add_edge(code_edge);
1042
1043 let mut bridge = Edge::new("task-1", "fn:main", "maps_to");
1044 bridge.metadata = Some(serde_json::json!({"source": "auto-bridge"}));
1045 g.add_edge(bridge);
1046
1047 g
1048 }
1049
1050 #[test]
1051 fn test_edge_source() {
1052 let g = mixed_graph();
1053 let proj_edge = g.edges.iter().find(|e| e.relation == "depends_on").unwrap();
1055 assert_eq!(proj_edge.source(), None);
1056
1057 let code_edge = g.edges.iter().find(|e| e.relation == "calls").unwrap();
1058 assert_eq!(code_edge.source(), Some("extract"));
1059
1060 let bridge_edge = g.edges.iter().find(|e| e.relation == "maps_to").unwrap();
1061 assert_eq!(bridge_edge.source(), Some("auto-bridge"));
1062 }
1063
1064 #[test]
1065 fn test_code_nodes() {
1066 let g = mixed_graph();
1067 let cn = g.code_nodes();
1068 assert_eq!(cn.len(), 2);
1069 assert!(cn.iter().all(|n| n.source.as_deref() == Some("extract")));
1070 }
1071
1072 #[test]
1073 fn test_project_nodes() {
1074 let g = mixed_graph();
1075 let pn = g.project_nodes();
1076 assert_eq!(pn.len(), 2); assert!(pn.iter().any(|n| n.id == "task-1"));
1078 assert!(pn.iter().any(|n| n.id == "legacy-1"));
1079 }
1080
1081 #[test]
1082 fn test_code_edges() {
1083 let g = mixed_graph();
1084 assert_eq!(g.code_edges().len(), 1);
1085 }
1086
1087 #[test]
1088 fn test_project_edges() {
1089 let g = mixed_graph();
1090 assert_eq!(g.project_edges().len(), 1); }
1092
1093 #[test]
1094 fn test_bridge_edges() {
1095 let g = mixed_graph();
1096 assert_eq!(g.bridge_edges().len(), 1);
1097 }
1098
1099 #[test]
1100 fn test_summary_excludes_code_nodes() {
1101 let g = mixed_graph();
1102 let s = g.summary();
1103 assert_eq!(s.total_nodes, 2);
1105 }
1106
1107 #[test]
1108 fn test_ready_tasks_excludes_code_nodes() {
1109 let mut g = mixed_graph();
1110 g.update_status("legacy-1", NodeStatus::Done);
1112 let ready = g.ready_tasks();
1113 assert!(ready.iter().any(|n| n.id == "task-1"));
1115 assert!(!ready.iter().any(|n| n.source.as_deref() == Some("extract")));
1116 }
1117}
1118
1119#[cfg(test)]
1120mod add_edge_dedup_tests {
1121 use super::*;
1122
1123 #[test]
1124 fn test_new_edge_returns_true() {
1125 let mut g = Graph::new();
1126 g.add_node(Node::new("a", "A"));
1127 g.add_node(Node::new("b", "B"));
1128 let result = g.add_edge_dedup(Edge::new("a", "b", "depends_on"));
1129 assert!(result);
1130 assert_eq!(g.edges.len(), 1);
1131 }
1132
1133 #[test]
1134 fn test_duplicate_returns_false() {
1135 let mut g = Graph::new();
1136 g.add_node(Node::new("a", "A"));
1137 g.add_node(Node::new("b", "B"));
1138 g.add_edge_dedup(Edge::new("a", "b", "depends_on"));
1139 let result = g.add_edge_dedup(Edge::new("a", "b", "depends_on"));
1140 assert!(!result);
1141 assert_eq!(g.edges.len(), 1);
1142 }
1143
1144 #[test]
1145 fn test_same_from_to_different_relation() {
1146 let mut g = Graph::new();
1147 g.add_node(Node::new("a", "A"));
1148 g.add_node(Node::new("b", "B"));
1149 assert!(g.add_edge_dedup(Edge::new("a", "b", "depends_on")));
1150 assert!(g.add_edge_dedup(Edge::new("a", "b", "blocks")));
1151 assert_eq!(g.edges.len(), 2);
1152 }
1153
1154 #[test]
1155 fn test_same_from_relation_different_to() {
1156 let mut g = Graph::new();
1157 g.add_node(Node::new("a", "A"));
1158 g.add_node(Node::new("b", "B"));
1159 g.add_node(Node::new("c", "C"));
1160 assert!(g.add_edge_dedup(Edge::new("a", "b", "depends_on")));
1161 assert!(g.add_edge_dedup(Edge::new("a", "c", "depends_on")));
1162 assert_eq!(g.edges.len(), 2);
1163 }
1164}
1165
1166#[cfg(test)]
1167mod resolve_node_tests {
1168 use super::*;
1169
1170 fn test_graph() -> Graph {
1171 let mut g = Graph::new();
1172 g.add_node(Node::new("task-auth", "Auth Module"));
1173 g.add_node(Node::new("feat:auth:login", "Login Feature"));
1174 g.add_node(Node::new("validate_auth_token", "Token Validator"));
1175 g.add_node(Node::new("file:src/main.rs", "Main Entry"));
1176 g.add_node(Node::new("impl-auth-middleware", "User Login Flow"));
1177 g.add_node(Node::new("task-db", "Database Setup"));
1178 g
1179 }
1180
1181 #[test]
1182 fn test_exact_id_match() {
1183 let g = test_graph();
1184 let results = g.resolve_node("task-auth");
1185 assert_eq!(results.len(), 1);
1186 assert_eq!(results[0].id, "task-auth");
1187 }
1188
1189 #[test]
1190 fn test_exact_title_match_case_insensitive() {
1191 let g = test_graph();
1192 let results = g.resolve_node("auth module");
1193 assert_eq!(results.len(), 1);
1194 assert_eq!(results[0].id, "task-auth");
1195 }
1196
1197 #[test]
1198 fn test_structural_segment_colon() {
1199 let g = test_graph();
1200 let results = g.resolve_node("login");
1204 assert!(results.iter().any(|n| n.id == "feat:auth:login"));
1206 }
1207
1208 #[test]
1209 fn test_word_segment_underscore() {
1210 let g = test_graph();
1211 let results = g.resolve_node("validate");
1213 assert_eq!(results.len(), 1);
1214 assert_eq!(results[0].id, "validate_auth_token");
1215 }
1216
1217 #[test]
1218 fn test_file_path_match() {
1219 let g = test_graph();
1220 let results = g.resolve_node("main.rs");
1221 assert!(results.iter().any(|n| n.id == "file:src/main.rs"));
1222 }
1223
1224 #[test]
1225 fn test_title_substring() {
1226 let g = test_graph();
1227 let results = g.resolve_node("Login Flow");
1229 assert_eq!(results.len(), 1);
1230 assert_eq!(results[0].id, "impl-auth-middleware");
1231 }
1232
1233 #[test]
1234 fn test_id_substring() {
1235 let g = test_graph();
1236 let results = g.resolve_node("middleware");
1239 assert!(results.iter().any(|n| n.id == "impl-auth-middleware"));
1240 }
1241
1242 #[test]
1243 fn test_zero_matches() {
1244 let g = test_graph();
1245 let results = g.resolve_node("nonexistent_xyz");
1246 assert!(results.is_empty());
1247 }
1248
1249 #[test]
1250 fn test_tier_priority() {
1251 let mut g = Graph::new();
1253 g.add_node(Node::new("auth", "Something"));
1254 g.add_node(Node::new("other", "auth related"));
1255 let results = g.resolve_node("auth");
1256 assert_eq!(results.len(), 1);
1258 assert_eq!(results[0].id, "auth");
1259 }
1260
1261 #[test]
1262 fn test_multiple_matches_same_tier() {
1263 let mut g = Graph::new();
1264 g.add_node(Node::new("node-1", "Auth Login"));
1265 g.add_node(Node::new("node-2", "Auth Signup"));
1266 let results = g.resolve_node("auth");
1271 assert_eq!(results.len(), 2);
1272 }
1273}
1274
1275#[cfg(test)]
1276mod add_feature_tests {
1277 use super::*;
1278
1279 #[test]
1280 fn test_basic_feature_with_tasks() {
1281 let mut g = Graph::new();
1282 let tasks = vec![
1283 TaskSpec { title: "Design API".into(), status: None, tags: vec![], deps: vec![] },
1284 TaskSpec { title: "Write Tests".into(), status: None, tags: vec![], deps: vec![] },
1285 ];
1286 let feat_id = g.add_feature("User Auth", &tasks);
1287 assert_eq!(feat_id, "feat-user-auth");
1288
1289 let feat = g.get_node("feat-user-auth").unwrap();
1291 assert_eq!(feat.title, "User Auth");
1292 assert_eq!(feat.node_type.as_deref(), Some("feature"));
1293 assert_eq!(feat.status, NodeStatus::Todo);
1294
1295 assert!(g.get_node("task-user-auth-design-api").is_some());
1297 assert!(g.get_node("task-user-auth-write-tests").is_some());
1298
1299 let implements: Vec<_> = g.edges.iter()
1301 .filter(|e| e.relation == "implements" && e.to == "feat-user-auth")
1302 .collect();
1303 assert_eq!(implements.len(), 2);
1304 }
1305
1306 #[test]
1307 fn test_feature_with_deps() {
1308 let mut g = Graph::new();
1309 let tasks = vec![
1310 TaskSpec { title: "Setup DB".into(), status: None, tags: vec![], deps: vec![] },
1311 TaskSpec { title: "Write Schema".into(), status: None, tags: vec![], deps: vec!["Setup DB".into()] },
1312 TaskSpec { title: "Add Migrations".into(), status: None, tags: vec![], deps: vec!["Write Schema".into()] },
1313 ];
1314 let feat_id = g.add_feature("Database", &tasks);
1315 assert_eq!(feat_id, "feat-database");
1316
1317 let deps: Vec<_> = g.edges.iter()
1319 .filter(|e| e.relation == "depends_on")
1320 .collect();
1321 assert_eq!(deps.len(), 2);
1322
1323 assert!(g.edges.iter().any(|e| {
1325 e.from == "task-database-write-schema" && e.to == "task-database-setup-db" && e.relation == "depends_on"
1326 }));
1327
1328 assert!(g.edges.iter().any(|e| {
1330 e.from == "task-database-add-migrations" && e.to == "task-database-write-schema" && e.relation == "depends_on"
1331 }));
1332 }
1333
1334 #[test]
1335 fn test_feature_id_collision() {
1336 let mut g = Graph::new();
1337 let tasks = vec![
1338 TaskSpec { title: "Task A".into(), status: None, tags: vec![], deps: vec![] },
1339 ];
1340 let id1 = g.add_feature("Auth", &tasks);
1341 assert_eq!(id1, "feat-auth");
1342
1343 let id2 = g.add_feature("Auth", &[]);
1344 assert_eq!(id2, "feat-auth-2");
1345
1346 assert!(g.get_node("feat-auth").is_some());
1348 assert!(g.get_node("feat-auth-2").is_some());
1349 }
1350}
1351
1352#[cfg(test)]
1353mod add_task_tests {
1354 use super::*;
1355
1356 #[test]
1357 fn test_standalone_task() {
1358 let mut g = Graph::new();
1359 let task_id = g.add_task("Fix login bug", None, &[], &[], None);
1360 assert_eq!(task_id, "task-fix-login-bug");
1361
1362 let node = g.get_node(&task_id).unwrap();
1363 assert_eq!(node.title, "Fix login bug");
1364 assert_eq!(node.node_type.as_deref(), Some("task"));
1365 assert_eq!(node.status, NodeStatus::Todo);
1366
1367 assert!(g.edges.is_empty());
1369 }
1370
1371 #[test]
1372 fn test_task_with_feature() {
1373 let mut g = Graph::new();
1374 g.add_feature("Auth", &[]);
1376
1377 let task_id = g.add_task("Add OAuth", Some("feat-auth"), &[], &["backend".into()], Some(1));
1378 assert_eq!(task_id, "task-auth-add-oauth");
1379
1380 let node = g.get_node(&task_id).unwrap();
1381 assert_eq!(node.tags, vec!["backend".to_string()]);
1382 assert_eq!(node.priority, Some(1));
1383
1384 assert!(g.edges.iter().any(|e| {
1386 e.from == "task-auth-add-oauth" && e.to == "feat-auth" && e.relation == "implements"
1387 }));
1388 }
1389
1390 #[test]
1391 fn test_task_with_deps() {
1392 let mut g = Graph::new();
1393 g.add_node(Node::new("task-setup", "Setup Environment"));
1395 g.add_node(Node::new("task-config", "Write Config"));
1396
1397 let task_id = g.add_task("Deploy App", None, &["task-setup".into(), "Write Config".into()], &[], None);
1398 assert_eq!(task_id, "task-deploy-app");
1399
1400 let deps: Vec<_> = g.edges.iter()
1402 .filter(|e| e.from == "task-deploy-app" && e.relation == "depends_on")
1403 .collect();
1404 assert_eq!(deps.len(), 2);
1405 }
1406}
1407
1408#[cfg(test)]
1409mod merge_feature_nodes_tests {
1410 use super::*;
1411
1412 #[test]
1413 fn test_basic_merge() {
1414 let mut g = Graph::new();
1415 g.add_feature("Auth", &[
1417 TaskSpec { title: "Old Task 1".into(), status: None, tags: vec![], deps: vec![] },
1418 TaskSpec { title: "Old Task 2".into(), status: None, tags: vec![], deps: vec![] },
1419 ]);
1420
1421 let mut incoming = Graph::new();
1423 incoming.add_node({
1424 let mut n = Node::new("new-task-a", "New Task A");
1425 n.node_type = Some("task".into());
1426 n
1427 });
1428 incoming.add_node({
1429 let mut n = Node::new("new-task-b", "New Task B");
1430 n.node_type = Some("task".into());
1431 n
1432 });
1433
1434 let (removed, added) = g.merge_feature_nodes("feat-auth", incoming);
1435 assert_eq!(removed, 2);
1436 assert_eq!(added, 2);
1437
1438 assert!(g.get_node("task-auth-old-task-1").is_none());
1440 assert!(g.get_node("task-auth-old-task-2").is_none());
1441
1442 assert!(g.get_node("new-task-a").is_some());
1444 assert!(g.get_node("new-task-b").is_some());
1445
1446 assert!(g.get_node("feat-auth").is_some());
1448
1449 let implements: Vec<_> = g.edges.iter()
1451 .filter(|e| e.relation == "implements" && e.to == "feat-auth")
1452 .collect();
1453 assert_eq!(implements.len(), 2);
1454 }
1455
1456 #[test]
1457 fn test_edge_cascade() {
1458 let mut g = Graph::new();
1459 g.add_feature("Auth", &[
1460 TaskSpec { title: "Task X".into(), status: None, tags: vec![], deps: vec![] },
1461 ]);
1462
1463 g.add_edge(Edge::new("task-auth-task-x", "some-other-node", "related_to"));
1465 g.add_node(Node::new("some-other-node", "Other"));
1466
1467 assert!(g.edges.iter().any(|e| e.from == "task-auth-task-x"));
1469
1470 let (removed, _added) = g.merge_feature_nodes("feat-auth", Graph::new());
1471
1472 assert_eq!(removed, 1);
1473 assert!(g.get_node("task-auth-task-x").is_none());
1475 assert!(!g.edges.iter().any(|e| e.from == "task-auth-task-x" || e.to == "task-auth-task-x"));
1476 }
1477
1478 #[test]
1479 fn test_empty_merge() {
1480 let mut g = Graph::new();
1481 g.add_feature("Auth", &[
1482 TaskSpec { title: "Task 1".into(), status: None, tags: vec![], deps: vec![] },
1483 TaskSpec { title: "Task 2".into(), status: None, tags: vec![], deps: vec![] },
1484 ]);
1485
1486 let (removed, added) = g.merge_feature_nodes("feat-auth", Graph::new());
1487 assert_eq!(removed, 2);
1488 assert_eq!(added, 0);
1489
1490 assert!(g.get_node("feat-auth").is_some());
1492 let implements: Vec<_> = g.edges.iter()
1494 .filter(|e| e.relation == "implements" && e.to == "feat-auth")
1495 .collect();
1496 assert_eq!(implements.len(), 0);
1497 }
1498
1499 #[test]
1500 fn test_edge_dedup_on_merge() {
1501 let mut g = Graph::new();
1502 g.add_feature("Auth", &[]);
1503
1504 let mut incoming = Graph::new();
1505 incoming.add_node({
1506 let mut n = Node::new("task-new", "New Task");
1507 n.node_type = Some("task".into());
1508 n
1509 });
1510
1511 g.merge_feature_nodes("feat-auth", incoming.clone());
1513
1514 g.remove_node("task-new");
1517 let mut incoming2 = Graph::new();
1518 incoming2.add_node({
1519 let mut n = Node::new("task-new", "New Task");
1520 n.node_type = Some("task".into());
1521 n
1522 });
1523 g.merge_feature_nodes("feat-auth", incoming2);
1524
1525 let implements: Vec<_> = g.edges.iter()
1527 .filter(|e| e.from == "task-new" && e.to == "feat-auth" && e.relation == "implements")
1528 .collect();
1529 assert_eq!(implements.len(), 1);
1530 }
1531
1532 #[test]
1533 fn test_infer_node_type_known_prefixes() {
1534 assert_eq!(infer_node_type("file:src/main.rs"), Some("file"));
1535 assert_eq!(infer_node_type("fn:my_func"), Some("function"));
1536 assert_eq!(infer_node_type("func:my_func"), Some("function"));
1537 assert_eq!(infer_node_type("struct:MyStruct"), Some("class"));
1538 assert_eq!(infer_node_type("class:MyClass"), Some("class"));
1539 assert_eq!(infer_node_type("mod:mymod"), Some("module"));
1540 assert_eq!(infer_node_type("module:mymod"), Some("module"));
1541 assert_eq!(infer_node_type("method:do_thing"), Some("method"));
1542 assert_eq!(infer_node_type("trait:MyTrait"), Some("trait"));
1543 assert_eq!(infer_node_type("interface:IFoo"), Some("trait"));
1544 assert_eq!(infer_node_type("enum:Color"), Some("enum"));
1545 assert_eq!(infer_node_type("const:MAX_SIZE"), Some("constant"));
1546 assert_eq!(infer_node_type("static:INSTANCE"), Some("constant"));
1547 assert_eq!(infer_node_type("test:test_foo"), Some("test"));
1548 assert_eq!(infer_node_type("impl:MyStruct"), Some("impl"));
1549 }
1550
1551 #[test]
1552 fn test_infer_node_type_unknown_prefix() {
1553 assert_eq!(infer_node_type("task-auth-login"), None);
1554 assert_eq!(infer_node_type("feat-pipeline"), None);
1555 assert_eq!(infer_node_type("random-id"), None);
1556 assert_eq!(infer_node_type(""), None);
1557 }
1558
1559 #[test]
1560 fn test_infer_node_type_no_colon() {
1561 assert_eq!(infer_node_type("file"), Some("file"));
1564 assert_eq!(infer_node_type("something"), None);
1565 }
1566
1567 #[test]
1568 fn test_add_node_auto_infers_type() {
1569 let mut g = Graph::new();
1570 let node = Node::new("fn:process_data", "Process Data");
1571 assert!(node.node_type.is_none()); g.add_node(node);
1573 let added = g.get_node("fn:process_data").unwrap();
1574 assert_eq!(added.node_type.as_deref(), Some("function"));
1575 }
1576
1577 #[test]
1578 fn test_add_node_does_not_override_explicit_type() {
1579 let mut g = Graph::new();
1580 let mut node = Node::new("fn:process_data", "Process Data");
1581 node.node_type = Some("custom".to_string());
1582 g.add_node(node);
1583 let added = g.get_node("fn:process_data").unwrap();
1584 assert_eq!(added.node_type.as_deref(), Some("custom"));
1585 }
1586
1587 #[test]
1588 fn test_add_node_no_infer_for_unknown_prefix() {
1589 let mut g = Graph::new();
1590 let node = Node::new("task-auth-login", "Login task");
1591 g.add_node(node);
1592 let added = g.get_node("task-auth-login").unwrap();
1593 assert!(added.node_type.is_none());
1594 }
1595}