Skip to main content

active_core/
graph.rs

1//! Dependency-graph queries (C-049, spec/graph.md).
2//!
3//! Pure functions over the IR DAG (`Module.graph`, edges
4//! dependency → dependent): impact/why traversals, node labels,
5//! and Mermaid/DOT/JSON renderings. Labels are names only — values
6//! (and secrets) never appear in graph output.
7
8use crate::ir::{Module, Node, NodeId};
9use petgraph::Direction;
10use std::collections::{HashMap, HashSet};
11
12/// Resolve a user key like the evaluator does: table fields win
13/// ties, single segments also see `let` bindings.
14pub fn resolve_key(module: &Module, key: &str) -> Option<NodeId> {
15    module.resolve_value_path(key)
16}
17
18/// Display label for a node: dotted path, binding name, or
19/// `node{id}` for unmapped internals.
20pub fn node_label(module: &Module, id: NodeId) -> String {
21    let mut paths: HashMap<NodeId, String> = HashMap::new();
22    for (name, binding) in &module.bindings {
23        paths.insert(*binding, name.clone());
24    }
25    for (node, path) in module.value_paths() {
26        paths.insert(node, path);
27    }
28    paths
29        .get(&id)
30        .cloned()
31        .unwrap_or_else(|| format!("node{}", id.0))
32}
33
34/// Transitive dependents of a node (outgoing edges), excluding the
35/// node itself, in first-visit order. Cycles terminate via the
36/// visited set (queries never evaluate, so cyclic graphs answer).
37pub fn transitive_dependents(module: &Module, id: NodeId) -> Vec<NodeId> {
38    walk(module, id, Direction::Outgoing)
39}
40
41/// Transitive dependencies of a node (incoming edges), excluding
42/// the node itself, in first-visit order.
43pub fn transitive_dependencies(module: &Module, id: NodeId) -> Vec<NodeId> {
44    walk(module, id, Direction::Incoming)
45}
46
47fn walk(module: &Module, start: NodeId, direction: Direction) -> Vec<NodeId> {
48    let Some(&start_idx) = module.node_to_graph.get(&start) else {
49        return Vec::new();
50    };
51    let mut seen = HashSet::from([start]);
52    let mut out = Vec::new();
53    let mut stack = vec![start_idx];
54    while let Some(idx) = stack.pop() {
55        // Petgraph neighbor order follows edge insertion, which is
56        // parse order — deterministic for a given document. Callers
57        // needing path order sort by label themselves.
58        let mut neighbors: Vec<_> = module.graph.neighbors_directed(idx, direction).collect();
59        neighbors.reverse();
60        for neighbor in neighbors {
61            let id = module.graph[neighbor];
62            if seen.insert(id) {
63                out.push(id);
64                stack.push(neighbor);
65            }
66        }
67    }
68    out
69}
70
71/// Sorted dotted paths of a node's transitive dependents,
72/// excluding the node itself and table nodes: a changed leaf
73/// trivially rebuilds its parents, which would otherwise put every
74/// table and the root in every answer. Patch and value nodes stay —
75/// those are the consumers that matter.
76pub fn impact_paths(module: &Module, id: NodeId) -> Vec<String> {
77    let mut paths: Vec<String> = transitive_dependents(module, id)
78        .into_iter()
79        .filter(|node| !matches!(module.get(*node), Some(Node::Table { .. })))
80        .map(|node| node_label(module, node))
81        .collect();
82    paths.sort();
83    paths.dedup();
84    paths
85}
86
87/// Sorted dotted paths of a node's transitive dependencies.
88pub fn why_paths(module: &Module, id: NodeId) -> Vec<String> {
89    let mut paths: Vec<String> = transitive_dependencies(module, id)
90        .into_iter()
91        .map(|node| node_label(module, node))
92        .collect();
93    paths.sort();
94    paths
95}
96
97/// Arena-stable node id for renderings (`n<index>`).
98fn render_id(id: NodeId) -> String {
99    format!("n{}", id.0)
100}
101
102fn sorted_nodes(module: &Module) -> Vec<(String, String)> {
103    let mut nodes: Vec<(String, String)> = module
104        .node_to_graph
105        .keys()
106        .map(|id| (render_id(*id), node_label(module, *id)))
107        .collect();
108    nodes.sort();
109    nodes
110}
111
112fn sorted_edges(module: &Module) -> Vec<(String, String)> {
113    let mut edges = Vec::new();
114    for edge in module.graph.raw_edges() {
115        let from = render_id(module.graph[edge.source()]);
116        let to = render_id(module.graph[edge.target()]);
117        edges.push((from, to));
118    }
119    edges.sort();
120    edges
121}
122
123/// Mermaid flowchart of the full graph (deterministic).
124pub fn to_mermaid(module: &Module) -> String {
125    let mut out = String::from("flowchart TD\n");
126    for (id, label) in sorted_nodes(module) {
127        out.push_str(&format!("    {id}[\"{label}\"]\n"));
128    }
129    for (from, to) in sorted_edges(module) {
130        out.push_str(&format!("    {from} --> {to}\n"));
131    }
132    out
133}
134
135/// DOT digraph of the full graph (deterministic; labels escaped).
136pub fn to_dot(module: &Module) -> String {
137    fn escape(s: &str) -> String {
138        s.replace('\\', "\\\\").replace('"', "\\\"")
139    }
140    let mut out = String::from("digraph active {\n");
141    for (id, label) in sorted_nodes(module) {
142        out.push_str(&format!("    \"{id}\" [label=\"{}\"];\n", escape(&label)));
143    }
144    for (from, to) in sorted_edges(module) {
145        out.push_str(&format!("    \"{from}\" -> \"{to}\";\n"));
146    }
147    out.push_str("}\n");
148    out
149}
150
151/// JSON model of the full graph: nodes with labels, edges as
152/// id pairs (deterministic).
153pub fn to_graph_json(module: &Module) -> serde_json::Value {
154    let nodes: Vec<serde_json::Value> = sorted_nodes(module)
155        .into_iter()
156        .map(|(id, label)| serde_json::json!({"id": id, "label": label}))
157        .collect();
158    let edges: Vec<serde_json::Value> = sorted_edges(module)
159        .into_iter()
160        .map(|(from, to)| serde_json::json!({"from": from, "to": to}))
161        .collect();
162    serde_json::json!({"nodes": nodes, "edges": edges})
163}