use crate::graph::{Graph, Node, Edge, NodeStatus};
use crate::code_graph::{CodeGraph, NodeKind, EdgeRelation};
use std::collections::HashMap;
#[deprecated(since = "0.3.1", note = "Use unify::codegraph_to_graph_nodes() + unify::merge_code_layer() instead")]
pub fn build_unified_graph(code_graph: &CodeGraph, task_graph: &Graph) -> Graph {
let mut nodes = Vec::new();
let mut seen_node_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut seen_edges: std::collections::HashSet<(String, String, String)> = std::collections::HashSet::new();
let mut edges = Vec::new();
for task_node in &task_graph.nodes {
seen_node_ids.insert(task_node.id.clone());
}
for code_node in &code_graph.nodes {
let id = code_node_to_task_id(&code_node.id);
if !seen_node_ids.insert(id.clone()) {
continue;
}
let node_type = match code_node.kind {
NodeKind::File => "file",
NodeKind::Class | NodeKind::Interface | NodeKind::Enum | NodeKind::TypeAlias | NodeKind::Trait => "class",
NodeKind::Function | NodeKind::Constant => "function",
NodeKind::Module => "module",
};
let mut metadata = HashMap::new();
metadata.insert("original_id".to_string(), serde_json::json!(code_node.id));
metadata.insert("file_path".to_string(), serde_json::json!(code_node.file_path));
if let Some(line) = code_node.line {
metadata.insert("line".to_string(), serde_json::json!(line));
}
if let Some(ref sig) = code_node.signature {
metadata.insert("signature".to_string(), serde_json::json!(sig));
}
nodes.push(Node {
id,
title: code_node.name.clone(),
status: NodeStatus::Done,
description: code_node.docstring.clone(),
assigned_to: None,
tags: if code_node.is_test { vec!["test".to_string()] } else { vec![] },
priority: None,
node_type: Some(node_type.to_string()),
knowledge: Default::default(),
metadata,
file_path: Some(code_node.file_path.clone()),
lang: None,
start_line: code_node.line.map(|l| l as usize),
end_line: None,
signature: code_node.signature.clone(),
visibility: None,
doc_comment: None,
body_hash: None,
node_kind: Some(format!("{:?}", code_node.kind)),
owner: None,
source: Some("extract".to_string()),
repo: None,
parent_id: None,
depth: None,
complexity: None,
is_public: None,
body: None,
created_at: None,
updated_at: None,
});
}
for task_node in &task_graph.nodes {
nodes.push(task_node.clone());
}
for code_edge in &code_graph.edges {
let from = code_node_to_task_id(&code_edge.from);
let to = code_node_to_task_id(&code_edge.to);
if !seen_node_ids.contains(&from) || !seen_node_ids.contains(&to) {
continue;
}
let relation = match code_edge.relation {
EdgeRelation::Imports => "imports",
EdgeRelation::Inherits => "inherits",
EdgeRelation::DefinedIn => "defined_in",
EdgeRelation::Calls => "calls",
EdgeRelation::TestsFor => "tests",
EdgeRelation::Overrides => "overrides",
EdgeRelation::Implements => "implements",
EdgeRelation::BelongsTo => "belongs_to",
EdgeRelation::TypeReference => "type_reference",
};
let edge_key = (from.clone(), to.clone(), relation.to_string());
if !seen_edges.insert(edge_key) {
continue; }
edges.push(Edge {
from,
to,
relation: relation.to_string(),
weight: Some(code_edge.weight as f64),
confidence: if code_edge.confidence > 0.0 {
Some(code_edge.confidence as f64)
} else {
None
},
metadata: None,
});
}
for task_edge in &task_graph.edges {
let edge_key = (task_edge.from.clone(), task_edge.to.clone(), task_edge.relation.clone());
if !seen_edges.insert(edge_key) {
continue;
}
edges.push(task_edge.clone());
}
let mut unified = Graph {
project: task_graph.project.clone(),
nodes,
edges,
};
crate::unify::generate_bridge_edges(&mut unified);
unified
}
#[deprecated(since = "0.3.1", note = "Use unify::codegraph_to_graph_nodes() + unify::merge_code_layer() with pre-filtered nodes instead")]
pub fn merge_relevant_code(
code_graph: &CodeGraph,
task_graph: &mut Graph,
keywords: &[&str],
max_nodes: usize,
) {
let relevant = code_graph.find_relevant_nodes(keywords);
let task_ids: std::collections::HashSet<String> = task_graph.nodes.iter()
.map(|n| n.id.clone())
.collect();
let mut added = 0;
for code_node in relevant {
if added >= max_nodes {
break;
}
let id = code_node_to_task_id(&code_node.id);
if task_ids.contains(&id) {
continue;
}
let node_type = match code_node.kind {
NodeKind::File => "file",
NodeKind::Class | NodeKind::Interface | NodeKind::Enum | NodeKind::TypeAlias | NodeKind::Trait => "class",
NodeKind::Function | NodeKind::Constant => "function",
NodeKind::Module => "module",
};
let mut metadata = HashMap::new();
metadata.insert("file_path".to_string(), serde_json::json!(code_node.file_path));
if let Some(line) = code_node.line {
metadata.insert("line".to_string(), serde_json::json!(line));
}
task_graph.add_node(Node {
id: id.clone(),
title: code_node.name.clone(),
status: NodeStatus::Done,
description: code_node.docstring.clone(),
assigned_to: None,
tags: vec!["code".to_string()],
priority: None,
node_type: Some(node_type.to_string()),
knowledge: Default::default(),
metadata,
file_path: Some(code_node.file_path.clone()),
lang: None,
start_line: code_node.line.map(|l| l as usize),
end_line: None,
signature: code_node.signature.clone(),
visibility: None,
doc_comment: None,
body_hash: None,
node_kind: Some(format!("{:?}", code_node.kind)),
owner: None,
source: Some("extract".to_string()),
repo: None,
parent_id: None,
depth: None,
complexity: None,
is_public: None,
body: None,
created_at: None,
updated_at: None,
});
added += 1;
}
}
#[deprecated(since = "0.3.1", note = "Use unify::merge_code_layer() which auto-creates bridge edges via auto_bridge_edges()")]
pub fn link_tasks_to_code(code_graph: &CodeGraph, task_graph: &mut Graph) {
let code_files: std::collections::HashSet<String> = code_graph.nodes.iter()
.filter(|n| n.kind == NodeKind::File)
.map(|n| n.file_path.clone())
.collect();
let mut edges_to_add = Vec::new();
for task in &task_graph.nodes {
let text = format!("{} {}", task.title, task.description.as_deref().unwrap_or(""));
for file_path in &code_files {
let file_name = file_path.rsplit('/').next().unwrap_or(file_path);
if text.contains(file_name) || text.contains(file_path) {
let code_id = code_node_to_task_id(&format!("file:{}", file_path));
let exists = task_graph.edges.iter().any(|e| {
e.from == task.id && e.to == code_id
});
if !exists {
edges_to_add.push(Edge {
from: task.id.clone(),
to: code_id,
relation: "relates_to".to_string(),
weight: None,
confidence: None,
metadata: None,
});
}
}
}
}
for edge in edges_to_add {
task_graph.add_edge(edge);
}
}
fn code_node_to_task_id(code_id: &str) -> String {
code_id
.replace("file:", "code:")
.replace("class:", "code:")
.replace("func:", "code:")
.replace("method:", "code:")
.replace("module_ref:", "code:")
.replace('/', "_")
.replace(':', "_")
}
#[derive(Debug, Default)]
pub struct UnifiedStats {
pub total_nodes: usize,
pub code_nodes: usize,
pub task_nodes: usize,
pub total_edges: usize,
pub code_edges: usize,
pub task_edges: usize,
pub cross_edges: usize, }
impl UnifiedStats {
pub fn from_graph(graph: &Graph) -> Self {
let code_node_ids: std::collections::HashSet<&str> = graph.nodes.iter()
.filter(|n| n.id.starts_with("code:") ||
n.node_type.as_deref() == Some("file") ||
n.node_type.as_deref() == Some("class") ||
n.node_type.as_deref() == Some("function"))
.map(|n| n.id.as_str())
.collect();
let code_nodes = code_node_ids.len();
let task_nodes = graph.nodes.len() - code_nodes;
let mut code_edges = 0;
let mut task_edges = 0;
let mut cross_edges = 0;
for edge in &graph.edges {
let from_is_code = code_node_ids.contains(edge.from.as_str());
let to_is_code = code_node_ids.contains(edge.to.as_str());
match (from_is_code, to_is_code) {
(true, true) => code_edges += 1,
(false, false) => task_edges += 1,
_ => cross_edges += 1,
}
}
Self {
total_nodes: graph.nodes.len(),
code_nodes,
task_nodes,
total_edges: graph.edges.len(),
code_edges,
task_edges,
cross_edges,
}
}
}
impl std::fmt::Display for UnifiedStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Unified graph: {} nodes ({} code, {} tasks), {} edges ({} code, {} task, {} cross)",
self.total_nodes, self.code_nodes, self.task_nodes,
self.total_edges, self.code_edges, self.task_edges, self.cross_edges
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::ProjectMeta;
use crate::code_graph::CodeNode;
#[test]
fn test_build_unified_graph() {
let mut code_graph = CodeGraph::default();
code_graph.nodes.push(CodeNode {
id: "file:src/main.rs".into(),
kind: NodeKind::File,
name: "main.rs".into(),
file_path: "src/main.rs".into(),
line: None,
decorators: vec![],
signature: None,
docstring: None,
line_count: 0,
is_test: false,
visibility: None,
lang: None,
body_hash: None,
end_line: None,
complexity: None,
});
code_graph.nodes.push(CodeNode {
id: "func:src/main.rs:main".into(),
kind: NodeKind::Function,
name: "main".into(),
file_path: "src/main.rs".into(),
line: Some(1),
decorators: vec![],
signature: Some("fn main()".into()),
docstring: None,
line_count: 10,
is_test: false,
visibility: None,
lang: None,
body_hash: None,
end_line: None,
complexity: None,
});
let task_graph = Graph {
project: Some(ProjectMeta {
name: "test".into(),
description: Some("Test project".into()),
}),
nodes: vec![
Node::new("task1", "Implement feature"),
],
edges: vec![],
};
let unified = build_unified_graph(&code_graph, &task_graph);
assert_eq!(unified.nodes.len(), 3); assert!(unified.nodes.iter().any(|n| n.title == "main"));
assert!(unified.nodes.iter().any(|n| n.title == "Implement feature"));
}
#[test]
fn test_code_node_to_task_id() {
assert_eq!(code_node_to_task_id("file:src/main.rs"), "code_src_main.rs");
assert_eq!(code_node_to_task_id("class:src/lib.rs:MyClass"), "code_src_lib.rs_MyClass");
assert_eq!(code_node_to_task_id("func:test.py:my_func"), "code_test.py_my_func");
}
}