Walk backward from a node through its causal chain. Returns node IDs in reverse-causal order: the target node first, then its causes, then their causes, etc. Useful for answering "why did this change happen?" Stops at the graph boundary (goal nodes or nodes with no incoming edges). Avoids cycles (though the graph should be acyclic by construction).
(&self, start_node_id: &str)
| 402 | /// Stops at the graph boundary (goal nodes or nodes with no incoming edges). |
| 403 | /// Avoids cycles (though the graph should be acyclic by construction). |
| 404 | pub fn walk_backward(&self, start_node_id: &str) -> Vec<String> { |
| 405 | let mut result = Vec::new(); |
| 406 | let mut visited = std::collections::HashSet::new(); |
| 407 | let mut queue = std::collections::VecDeque::new(); |
| 408 | |
| 409 | queue.push_back(start_node_id.to_string()); |
| 410 | visited.insert(start_node_id.to_string()); |
| 411 | |
| 412 | while let Some(node_id) = queue.pop_front() { |
| 413 | result.push(node_id.clone()); |
| 414 | |
| 415 | // Find all edges pointing TO this node (its causes) |
| 416 | for edge in &self.edges { |
| 417 | if edge.to == node_id && !visited.contains(&edge.from) { |
| 418 | visited.insert(edge.from.clone()); |
| 419 | queue.push_back(edge.from.clone()); |
| 420 | } |
| 421 | } |
| 422 | } |
| 423 | |
| 424 | result |
| 425 | } |
| 426 | } |
| 427 | |
| 428 | impl fmt::Display for ProvenanceGraph { |