1use crate::{
2 AuditEvent, AuditExplanationLevel, Graph, GraphError, GraphResult, NodeChangeExplanation,
3 NodeHandle, NodeId, OutputFrameExplanation, OutputFrameKindTrace, OutputKey,
4 ResourceCommandCause, ResourceCommandExplanation, ResourceCommandKind, ResourceKey, ScopeId,
5 ScopeResourceInventory, TransactionResult,
6};
7use std::collections::{BTreeMap, BTreeSet, VecDeque};
8
9impl<C> Graph<C> {
10 pub fn why_changed<H: NodeHandle>(&self, node: H) -> Option<&NodeChangeExplanation> {
12 self.why_changed_by_id(node.id())
13 }
14
15 pub fn why_changed_by_id(&self, node: NodeId) -> Option<&NodeChangeExplanation> {
17 self.audit.node_changes.get(&node)
18 }
19
20 pub fn why_resource_command(&self, key: &ResourceKey) -> Option<&ResourceCommandExplanation> {
22 self.audit.resource_commands.get(key)
23 }
24
25 pub fn why_output_frame(&self, key: OutputKey) -> Option<&OutputFrameExplanation> {
27 self.audit.output_frames.get(&key)
28 }
29
30 pub fn dependency_path(&self, from: NodeId, to: NodeId) -> Option<Vec<NodeId>> {
32 if !self.nodes.contains_key(&from) || !self.nodes.contains_key(&to) {
33 return None;
34 }
35 let downstream = self.reverse_dependency_index();
36 shortest_dependency_path(&downstream, from, to)
37 }
38
39 pub fn scope_resource_inventory(&self, scope: ScopeId) -> GraphResult<ScopeResourceInventory> {
41 self.scope_meta(scope)
42 .ok_or(GraphError::UnknownScope(scope))?;
43 let resources = self
44 .resource_owners
45 .iter()
46 .filter(|(_, owners)| owners.contains(&scope))
47 .map(|(key, _)| key.clone())
48 .collect();
49 Ok(ScopeResourceInventory { scope, resources })
50 }
51
52 pub(crate) fn record_transaction_audit(
53 &mut self,
54 result: &TransactionResult<C>,
55 level: AuditExplanationLevel,
56 ) {
57 let causes = std::mem::take(&mut self.audit.pending_resource_causes);
58 if level == AuditExplanationLevel::Disabled {
59 self.audit.clear_explanations();
60 return;
61 }
62
63 let changed_inputs = result.changed_inputs.clone();
64 let changed_nodes = changed_nodes(result);
65 let downstream = (level == AuditExplanationLevel::DependencyPaths)
66 .then(|| self.reverse_dependency_index());
67 for entry in &result.audit_log {
68 if let Some(node) = event_node(entry.event) {
69 let dependency_paths =
70 paths_from_inputs_to_targets(downstream.as_ref(), &changed_inputs, &[node]);
71 let input_causes = input_causes_from_paths(&dependency_paths);
72 self.audit.node_changes.insert(
73 node,
74 NodeChangeExplanation {
75 node,
76 transaction_id: entry.transaction_id,
77 revision: entry.revision,
78 event: entry.event,
79 input_causes,
80 dependency_paths,
81 },
82 );
83 }
84 }
85
86 for (index, command) in result.resource_plan.commands().iter().enumerate() {
87 let cause = causes
88 .get(index)
89 .copied()
90 .expect("resource command cause recorded during reconciliation");
91 let collection_diffs = cause.collection().into_iter().collect::<Vec<_>>();
92 let dependency_paths = paths_from_inputs_to_targets(
93 downstream.as_ref(),
94 &changed_inputs,
95 &collection_diffs,
96 );
97 let input_causes = input_causes_from_paths(&dependency_paths);
98 self.audit.resource_commands.insert(
99 command.key().clone(),
100 ResourceCommandExplanation {
101 key: command.key().clone(),
102 scope: command.scope(),
103 transaction_id: result.transaction_id,
104 revision: result.revision,
105 kind: ResourceCommandKind::from_command(command),
106 cause,
107 collection_diffs,
108 changed_nodes: changed_nodes.clone(),
109 input_causes,
110 dependency_paths,
111 },
112 );
113 }
114
115 for frame in &result.output_frames {
116 let dependencies = self
117 .output_meta(frame.output_key)
118 .map(|meta| meta.dependencies().as_slice().to_vec())
119 .unwrap_or_default();
120 let changed_dependencies = dependencies
121 .iter()
122 .copied()
123 .filter(|node| changed_nodes.contains(node))
124 .collect::<Vec<_>>();
125 let dependency_paths = paths_from_inputs_to_targets(
126 downstream.as_ref(),
127 &changed_inputs,
128 &changed_dependencies,
129 );
130 let input_causes = input_causes_from_paths(&dependency_paths);
131 self.audit.output_frames.insert(
132 frame.output_key,
133 OutputFrameExplanation {
134 output_key: frame.output_key,
135 scope: frame.scope,
136 transaction_id: frame.transaction_id,
137 revision: frame.revision,
138 kind: OutputFrameKindTrace::from_kind(&frame.kind),
139 dependencies,
140 changed_dependencies,
141 input_causes,
142 dependency_paths,
143 },
144 );
145 }
146 }
147
148 fn reverse_dependency_index(&self) -> BTreeMap<NodeId, Vec<NodeId>> {
149 let mut downstream: BTreeMap<NodeId, Vec<NodeId>> = BTreeMap::new();
150 for meta in self.nodes.values() {
151 for dependency in meta.dependencies().as_slice() {
152 downstream.entry(*dependency).or_default().push(meta.id());
153 }
154 }
155 for nodes in downstream.values_mut() {
156 nodes.sort();
157 }
158 downstream
159 }
160}
161
162fn paths_from_inputs_to_targets(
163 downstream: Option<&BTreeMap<NodeId, Vec<NodeId>>>,
164 inputs: &[NodeId],
165 targets: &[NodeId],
166) -> Vec<Vec<NodeId>> {
167 let Some(downstream) = downstream else {
168 return Vec::new();
169 };
170
171 inputs
172 .iter()
173 .flat_map(|input| {
174 targets
175 .iter()
176 .filter_map(|target| shortest_dependency_path(downstream, *input, *target))
177 })
178 .collect()
179}
180
181fn shortest_dependency_path(
182 downstream: &BTreeMap<NodeId, Vec<NodeId>>,
183 from: NodeId,
184 to: NodeId,
185) -> Option<Vec<NodeId>> {
186 if from == to {
187 return Some(vec![from]);
188 }
189
190 let mut queue = VecDeque::from([from]);
191 let mut visited = BTreeSet::from([from]);
192 let mut previous = BTreeMap::new();
193
194 while let Some(current) = queue.pop_front() {
195 for next in downstream.get(¤t).into_iter().flatten().copied() {
196 if !visited.insert(next) {
197 continue;
198 }
199 previous.insert(next, current);
200 if next == to {
201 return Some(reconstruct_path(from, to, &previous));
202 }
203 queue.push_back(next);
204 }
205 }
206
207 None
208}
209
210fn reconstruct_path(from: NodeId, to: NodeId, previous: &BTreeMap<NodeId, NodeId>) -> Vec<NodeId> {
211 let mut path = vec![to];
212 let mut current = to;
213 while current != from {
214 current = previous[¤t];
215 path.push(current);
216 }
217 path.reverse();
218 path
219}
220
221impl ResourceCommandCause {
222 fn collection(self) -> Option<NodeId> {
223 match self {
224 Self::Planner { collection } => Some(collection),
225 Self::ScopeClosed { .. } => None,
226 }
227 }
228}
229
230fn input_causes_from_paths(paths: &[Vec<NodeId>]) -> Vec<NodeId> {
231 let mut causes = Vec::new();
232 for path in paths {
233 if let Some(input) = path.first()
234 && !causes.contains(input)
235 {
236 causes.push(*input);
237 }
238 }
239 causes
240}
241
242fn changed_nodes<C>(result: &TransactionResult<C>) -> Vec<NodeId> {
243 let mut nodes = result.changed_inputs.clone();
244 nodes.extend(result.changed_derived_nodes.iter().copied());
245 nodes.extend(result.changed_collection_nodes.iter().copied());
246 nodes
247}
248
249fn event_node(event: AuditEvent) -> Option<NodeId> {
250 match event {
251 AuditEvent::InputChanged(node)
252 | AuditEvent::DerivedChanged(node)
253 | AuditEvent::CollectionChanged(node)
254 | AuditEvent::NodeCreated(node) => Some(node),
255 AuditEvent::NodeAttached { node, .. } => Some(node),
256 AuditEvent::InputUnchanged(_)
257 | AuditEvent::ScopeCreated(_)
258 | AuditEvent::ScopeClosed(_) => None,
259 }
260}