Skip to main content

nodex_core/query/
traverse.rs

1use crate::model::Graph;
2use std::collections::BTreeSet;
3
4/// Find all nodes that link TO the given node.
5pub fn find_backlinks(graph: &Graph, target_id: &str) -> Vec<BacklinkEntry> {
6    graph
7        .incoming_edges(target_id)
8        .iter()
9        .filter_map(|edge| {
10            let source = graph.node(&edge.source)?;
11            Some(BacklinkEntry {
12                id: source.id.clone(),
13                title: source.title.clone(),
14                relation: edge.relation.clone(),
15                location: edge.location.clone(),
16            })
17        })
18        .collect()
19}
20
21#[derive(Debug, serde::Serialize)]
22pub struct BacklinkEntry {
23    pub id: String,
24    pub title: String,
25    pub relation: String,
26    pub location: String,
27}
28
29/// Walk the supersession chain forward from a node (oldest → newest).
30pub fn find_chain(graph: &Graph, start_id: &str) -> Vec<ChainEntry> {
31    let mut chain = Vec::new();
32    let mut visited = BTreeSet::new();
33    let mut current_id = start_id.to_string();
34
35    loop {
36        if visited.contains(&current_id) {
37            break; // Cycle guard (shouldn't happen — DAG validated at build)
38        }
39        visited.insert(current_id.clone());
40
41        let Some(node) = graph.node(&current_id) else {
42            break;
43        };
44
45        chain.push(ChainEntry {
46            id: node.id.clone(),
47            title: node.title.clone(),
48            status: node.status.to_string(),
49        });
50
51        match &node.superseded_by {
52            Some(next) => current_id = next.clone(),
53            None => break,
54        }
55    }
56
57    chain
58}
59
60#[derive(Debug, serde::Serialize)]
61pub struct ChainEntry {
62    pub id: String,
63    pub title: String,
64    pub status: String,
65}
66
67/// Find a node's full detail with incoming and outgoing edges,
68/// or `None` if the id is not in the graph.
69pub fn find_node_detail(graph: &Graph, id: &str) -> Option<NodeDetail> {
70    let node = graph.node(id)?;
71
72    let incoming: Vec<EdgeSummary> = graph
73        .incoming_edges(id)
74        .iter()
75        .map(|e| EdgeSummary {
76            node_id: e.source.clone(),
77            relation: e.relation.clone(),
78            confidence: e.confidence.to_string(),
79        })
80        .collect();
81
82    let outgoing: Vec<EdgeSummary> = graph
83        .outgoing_edges(id)
84        .iter()
85        .filter_map(|e| {
86            Some(EdgeSummary {
87                node_id: e.target.id()?.to_string(),
88                relation: e.relation.clone(),
89                confidence: e.confidence.to_string(),
90            })
91        })
92        .collect();
93
94    Some(NodeDetail {
95        node: node.clone(),
96        incoming,
97        outgoing,
98    })
99}
100
101#[derive(Debug, serde::Serialize)]
102pub struct NodeDetail {
103    pub node: crate::model::Node,
104    pub incoming: Vec<EdgeSummary>,
105    pub outgoing: Vec<EdgeSummary>,
106}
107
108#[derive(Debug, serde::Serialize)]
109pub struct EdgeSummary {
110    pub node_id: String,
111    pub relation: String,
112    pub confidence: String,
113}