1use std::sync::Arc;
2
3use ahash::{HashMap, HashMapExt, HashSet, HashSetExt};
4use base64::Engine as _;
5use rust_decimal::prelude::ToPrimitive;
6use zen_expression::variable::Variable;
7use zen_expression::Isolate;
8use zen_types::decision::{
9 DecisionNode, DecisionNodeKind, DecisionTableContent, DecisionTableHitPolicy,
10 TransformExecutionMode,
11};
12
13use crate::model::GraphContent;
14use crate::nodes::decision_table::DecisionTableNodeHandler;
15use crate::workspace::db::{Db, Snapshot};
16use crate::workspace::graph::editor::{NodePaths, ReadBase};
17use crate::workspace::graph::function_source;
18use crate::workspace::types::{
19 BlockExecution, BlockTrace, ConditionTrace, DecisionTableExtras, EvaluationError, Trace,
20 WriteTrace,
21};
22use crate::DecisionGraphTrace;
23
24pub type GraphTraceMap = HashMap<Arc<str>, DecisionGraphTrace>;
25
26struct EnhanceState<'a> {
27 db: &'a Db,
28 snapshot: Arc<Snapshot>,
29 executions: Vec<BlockExecution>,
30 visiting: HashSet<Arc<str>>,
31}
32
33impl EnhanceState<'_> {
34 fn dt_environment(
35 &self,
36 content: &DecisionTableContent,
37 node_trace: &DecisionGraphTrace,
38 trace: &GraphTraceMap,
39 ) -> Option<Variable> {
40 let nodes = Variable::from_object(
41 trace
42 .values()
43 .filter(|entry| entry.order < node_trace.order)
44 .map(|entry| {
45 (
46 zen_types::symbol::Symbol::from(entry.name.as_ref()),
47 entry.output.clone(),
48 )
49 })
50 .collect(),
51 );
52 let base = node_trace.input.depth_clone(1);
53 base.dot_insert("$nodes", nodes.clone());
54 let Some(input_field) = &content.transform_attributes.input_field else {
55 return Some(base);
56 };
57 let mut isolate = Isolate::with_environment(base);
58 let calculated = isolate.run_standard(input_field.as_ref()).ok()?;
59 match &calculated {
60 Variable::Array(items) => {
61 let items = items
62 .borrow()
63 .iter()
64 .map(|item| {
65 let item = item.depth_clone(1);
66 item.dot_insert("$nodes", nodes.clone());
67 item
68 })
69 .collect();
70 Some(Variable::from_array(items))
71 }
72 _ => {
73 let calculated = calculated.depth_clone(1);
74 calculated.dot_insert("$nodes", nodes);
75 Some(calculated)
76 }
77 }
78 }
79
80 fn dt_extras(
81 &self,
82 content: &DecisionTableContent,
83 environment: Variable,
84 ) -> DecisionTableExtras {
85 let mut isolate = Isolate::with_environment(environment.depth_clone(1));
86 let bytes_per_row = content.inputs.len().div_ceil(8);
87 let mut bits = vec![0u8; bytes_per_row * content.rules.len()];
88 for (row, rule) in content.rules.iter().enumerate() {
89 for (col, input) in content.inputs.iter().enumerate() {
90 if DecisionTableNodeHandler::cell_passes(rule, input, &mut isolate) {
91 bits[row * bytes_per_row + (col >> 3)] |= 1 << (col & 7);
92 }
93 }
94 }
95 DecisionTableExtras {
96 input_pass: base64::engine::general_purpose::STANDARD.encode(&bits),
97 }
98 }
99}
100
101impl Db {
102 pub fn enhance_graph_trace(
103 &self,
104 document: &Arc<str>,
105 trace: &GraphTraceMap,
106 ) -> Result<Trace, EvaluationError> {
107 let snapshot = self.snapshot();
108 let Some(content) = snapshot
109 .graphs
110 .get(document)
111 .and_then(|content| content.as_graph())
112 .cloned()
113 else {
114 return Err(EvaluationError::PolicyNotFound(document.clone()));
115 };
116
117 let mut state = EnhanceState {
118 db: self,
119 snapshot: snapshot.clone(),
120 executions: Vec::new(),
121 visiting: HashSet::new(),
122 };
123 state.visiting.insert(document.clone());
124 walk_graph(&mut state, &content, trace, "", None);
125
126 let mut properties: HashMap<Arc<str>, Variable> = HashMap::new();
127 for execution in &state.executions {
128 for write in &execution.writes {
129 properties.insert(write.path.clone(), write.value.clone());
130 }
131 match &execution.trace {
132 BlockTrace::Expression { property, value } if !property.is_empty() => {
133 properties.insert(property.clone(), value.clone());
134 }
135 BlockTrace::DecisionTable { evaluations, .. } => {
136 for evaluation in evaluations {
137 for (path, value) in evaluation {
138 properties.insert(path.clone(), value.clone());
139 }
140 }
141 }
142 _ => {}
143 }
144 }
145
146 Ok(Trace {
147 engine_version: Arc::from(crate::ENGINE_VERSION),
148 properties,
149 executions: state.executions,
150 })
151 }
152}
153
154fn walk_graph(
155 state: &mut EnhanceState,
156 content: &GraphContent,
157 trace: &GraphTraceMap,
158 id_prefix: &str,
159 inherited_instance: Option<&Arc<str>>,
160) {
161 let mut executed: Vec<(&DecisionNode, &DecisionGraphTrace)> = content
162 .nodes
163 .iter()
164 .filter_map(|node| trace.get(node.id.as_ref()).map(|t| (node.as_ref(), t)))
165 .collect();
166 executed.sort_by_key(|(_, node_trace)| node_trace.order);
167
168 for (node, node_trace) in executed {
169 if matches!(
170 node.kind,
171 DecisionNodeKind::InputNode { .. } | DecisionNodeKind::OutputNode { .. }
172 ) {
173 continue;
174 }
175 let block_id = prefixed(id_prefix, &node.id);
176 let paths = NodePaths::new(node);
177
178 match &node.kind {
179 DecisionNodeKind::ExpressionNode { content } => {
180 let loop_mode = matches!(
181 content.transform_attributes.execution_mode,
182 TransformExecutionMode::Loop
183 );
184 let iterations = trace_entries(node_trace.trace_data.as_ref(), loop_mode);
185 for row in content.expressions.iter() {
186 if row.key.is_empty() || row.value.is_empty() {
187 continue;
188 }
189 let row_block_id: Arc<str> = Arc::from(format!("{block_id}:{}", row.id));
190 let reads = state.db.node_global_reads(
191 node,
192 &paths,
193 Some(std::slice::from_ref(&row.id)),
194 );
195 let local_reads = state
196 .db
197 .node_local_reads(node, Some(std::slice::from_ref(&row.id)));
198 let property = output_prefixed(&paths, &row.key);
199 for (index, entry) in iterations.iter().enumerate() {
200 let value = expression_trace_result(entry, row.key.as_ref())
201 .unwrap_or(Variable::Null);
202 let element = iteration_element(node_trace, &paths, loop_mode, index);
203 let dollar = expression_dollar_scope(&content.expressions, entry);
204 state.executions.push(BlockExecution {
205 block_id: row_block_id.clone(),
206 policy_path: None,
207 instance_path: instance_for(
208 node,
209 &paths,
210 loop_mode,
211 iterations.len(),
212 index,
213 inherited_instance,
214 ),
215 trace: BlockTrace::Expression {
216 property: property.clone(),
217 value,
218 },
219 operand_values: operand_values(
220 &local_reads,
221 &paths,
222 &node_trace.input,
223 element.as_ref(),
224 Some(&dollar),
225 ),
226 writes: Vec::new(),
227 reads: reads.clone(),
228 });
229 }
230 }
231 }
232 DecisionNodeKind::DecisionTableNode { content } => {
233 let loop_mode = matches!(
234 content.transform_attributes.execution_mode,
235 TransformExecutionMode::Loop
236 );
237 let collect = matches!(content.hit_policy, DecisionTableHitPolicy::Collect);
238 let column_collect =
239 !collect && content.outputs.iter().any(|output| output.write_path().1);
240 let iterations = trace_entries(node_trace.trace_data.as_ref(), loop_mode);
241 let reads = state.db.node_global_reads(node, &paths, None);
242 let environment_root = state.dt_environment(content, node_trace, trace);
243 let outputs_root = match &paths.output_path {
244 Some(path) => node_trace.output.dot(path).unwrap_or(Variable::Null),
245 None => node_trace.output.clone(),
246 };
247 for (index, entry) in iterations.iter().enumerate() {
248 let row_traces: Vec<Variable> = if collect || column_collect {
249 entry
250 .as_array()
251 .map(|rows| rows.borrow().iter().cloned().collect())
252 .unwrap_or_default()
253 } else {
254 vec![entry.clone()]
255 };
256 let matched_rows: Vec<u32> = row_traces
257 .iter()
258 .filter_map(|row| row.dot("index"))
259 .filter_map(|value| match value {
260 Variable::Number(number) => number.to_u32(),
261 _ => None,
262 })
263 .collect();
264
265 let iter_result = if loop_mode {
266 element_at(&outputs_root, index).unwrap_or(Variable::Null)
267 } else {
268 outputs_root.clone()
269 };
270 let mut evaluations: Vec<HashMap<Arc<str>, Variable>> = Vec::new();
271 if collect {
272 for (row_index, _) in row_traces.iter().enumerate() {
273 let Some(row) = element_at(&iter_result, row_index) else {
274 continue;
275 };
276 let mut evaluation: HashMap<Arc<str>, Variable> = HashMap::new();
277 for column in content.outputs.iter() {
278 let (path, _) = column.write_path();
279 if let Some(value) = row.dot(path) {
280 evaluation
281 .insert(output_prefixed(&paths, path), value.deep_clone());
282 }
283 }
284 if !evaluation.is_empty() {
285 evaluations.push(evaluation);
286 }
287 }
288 } else if column_collect {
289 let mut collect_positions: HashMap<Arc<str>, usize> = HashMap::new();
290 for (position, row_idx) in matched_rows.iter().enumerate() {
291 let rule = content.rules.get(*row_idx as usize);
292 let mut evaluation: HashMap<Arc<str>, Variable> = HashMap::new();
293 for column in content.outputs.iter() {
294 let (path, is_collect) = column.write_path();
295 if path.is_empty() {
296 continue;
297 }
298 let cell_filled = rule.is_some_and(|r| {
299 r.get(&column.id).is_some_and(|c| !c.is_empty())
300 });
301 if !cell_filled {
302 continue;
303 }
304 if is_collect {
305 let counter =
306 collect_positions.entry(column.id.clone()).or_default();
307 let value = iter_result
308 .dot(path)
309 .and_then(|values| element_at(&values, *counter));
310 *counter += 1;
311 if let Some(value) = value {
312 evaluation.insert(
313 output_prefixed(&paths, path),
314 value.deep_clone(),
315 );
316 }
317 } else if position == 0 {
318 if let Some(value) = iter_result.dot(path) {
319 evaluation.insert(
320 output_prefixed(&paths, path),
321 value.deep_clone(),
322 );
323 }
324 }
325 }
326 evaluations.push(evaluation);
327 }
328 } else {
329 let mut evaluation: HashMap<Arc<str>, Variable> = HashMap::new();
330 for column in content.outputs.iter() {
331 let (path, _) = column.write_path();
332 if let Some(value) = iter_result.dot(path) {
333 evaluation
334 .insert(output_prefixed(&paths, path), value.deep_clone());
335 }
336 }
337 evaluations.push(evaluation);
338 }
339
340 let element = iteration_element(node_trace, &paths, loop_mode, index);
341 let mut operands: HashMap<Arc<str>, Variable> = HashMap::new();
342 for row in &row_traces {
343 let Some(reference_map) =
344 row.dot("reference_map").and_then(|value| value.as_object())
345 else {
346 continue;
347 };
348 for (field, value) in reference_map.borrow().iter() {
349 operands.insert(Arc::from(field.as_ref()), value.deep_clone());
350 }
351 }
352 if operands.is_empty() {
353 let local_reads = state.db.node_local_reads(node, None);
354 operands = operand_values(
355 &local_reads,
356 &paths,
357 &node_trace.input,
358 element.as_ref(),
359 None,
360 );
361 }
362
363 let environment = if loop_mode {
364 environment_root
365 .as_ref()
366 .and_then(|env| element_at(env, index))
367 } else {
368 environment_root.clone()
369 };
370 let extras = environment.map(|env| state.dt_extras(content, env));
371 state.executions.push(BlockExecution {
372 block_id: block_id.clone(),
373 policy_path: None,
374 instance_path: instance_for(
375 node,
376 &paths,
377 loop_mode,
378 iterations.len(),
379 index,
380 inherited_instance,
381 ),
382 trace: BlockTrace::DecisionTable {
383 matched_rows,
384 evaluations,
385 extras,
386 },
387 operand_values: operands,
388 writes: Vec::new(),
389 reads: reads.clone(),
390 });
391 }
392 }
393 DecisionNodeKind::SwitchNode { content } => {
394 let taken: HashSet<Arc<str>> = node_trace
395 .trace_data
396 .as_ref()
397 .and_then(|data| data.dot("statements"))
398 .and_then(|statements| statements.as_array())
399 .map(|statements| {
400 statements
401 .borrow()
402 .iter()
403 .filter_map(|statement| statement.dot("id"))
404 .filter_map(|id| id.as_str().map(Arc::from))
405 .collect()
406 })
407 .unwrap_or_default();
408 let arms: Vec<ConditionTrace> = content
409 .statements
410 .iter()
411 .map(|statement| ConditionTrace {
412 id: statement.id.clone(),
413 result: taken.contains(&statement.id),
414 })
415 .collect();
416 let matched_arm = content
417 .statements
418 .iter()
419 .find(|statement| taken.contains(&statement.id))
420 .map(|statement| statement.id.clone());
421 let reads = state.db.node_global_reads(node, &paths, None);
422 let local_reads = state.db.node_local_reads(node, None);
423 state.executions.push(BlockExecution {
424 block_id: block_id.clone(),
425 policy_path: None,
426 instance_path: inherited_instance.cloned(),
427 trace: BlockTrace::Match {
428 matched_arm,
429 value: Variable::Null,
430 arms,
431 },
432 operand_values: operand_values(
433 &local_reads,
434 &paths,
435 &node_trace.input,
436 None,
437 None,
438 ),
439 writes: Vec::new(),
440 reads,
441 });
442 }
443 DecisionNodeKind::FunctionNode { content } => {
444 let source = function_source(content);
445 let local_reads: Vec<Arc<str>> = Db::function_input_reads(&source)
446 .into_iter()
447 .map(Arc::from)
448 .collect();
449 let reads: Vec<Arc<str>> = local_reads
450 .iter()
451 .filter_map(|read| map_read(&paths, read))
452 .collect();
453 state.executions.push(BlockExecution {
454 block_id: block_id.clone(),
455 policy_path: None,
456 instance_path: inherited_instance.cloned(),
457 trace: BlockTrace::Expression {
458 property: Arc::from(""),
459 value: node_trace.output.clone(),
460 },
461 operand_values: operand_values(
462 &local_reads,
463 &paths,
464 &node_trace.input,
465 None,
466 None,
467 ),
468 writes: shallow_writes(&node_trace.input, &node_trace.output),
469 reads,
470 });
471 }
472 DecisionNodeKind::DecisionNode { content } => {
473 let key = content.key.clone();
474 let sub_content = (!state.visiting.contains(&key))
475 .then(|| {
476 state
477 .snapshot
478 .graphs
479 .get(&key)
480 .and_then(|content| content.as_graph())
481 .cloned()
482 })
483 .flatten();
484 let sub_traces = sub_content
485 .as_ref()
486 .map(|_| sub_trace_maps(node_trace.trace_data.as_ref()))
487 .unwrap_or_default();
488
489 if let (Some(sub_content), false) = (sub_content, sub_traces.is_empty()) {
490 let group_index = state.executions.len();
491 state.executions.push(BlockExecution {
492 block_id: block_id.clone(),
493 policy_path: None,
494 instance_path: inherited_instance.cloned(),
495 trace: BlockTrace::Expression {
496 property: Arc::from(""),
497 value: node_trace.output.clone(),
498 },
499 operand_values: HashMap::new(),
500 writes: shallow_writes(&node_trace.input, &node_trace.output),
501 reads: Vec::new(),
502 });
503 let child_start = state.executions.len();
504 state.visiting.insert(key.clone());
505 let looped = sub_traces.len() > 1;
506 for (index, sub_trace) in sub_traces.iter().enumerate() {
507 let prefix = if looped {
508 format!("{block_id}[{index}]/")
509 } else {
510 format!("{block_id}/")
511 };
512 let instance: Option<Arc<str>> = if looped {
513 Some(Arc::from(format!("{}.{index}", loop_label(node, &paths))))
514 } else {
515 inherited_instance.cloned()
516 };
517 walk_graph(state, &sub_content, sub_trace, &prefix, instance.as_ref());
518 }
519 state.visiting.remove(&key);
520 let free_reads = subtree_free_reads(&state.executions[child_start..], &paths);
521 state.executions[group_index].reads = free_reads;
522 } else {
523 push_opaque(state, &block_id, node_trace, inherited_instance);
524 }
525 }
526 _ => {
527 push_opaque(state, &block_id, node_trace, inherited_instance);
528 }
529 }
530 }
531}
532
533fn push_opaque(
534 state: &mut EnhanceState,
535 block_id: &Arc<str>,
536 node_trace: &DecisionGraphTrace,
537 inherited_instance: Option<&Arc<str>>,
538) {
539 state.executions.push(BlockExecution {
540 block_id: block_id.clone(),
541 policy_path: None,
542 instance_path: inherited_instance.cloned(),
543 trace: BlockTrace::Expression {
544 property: Arc::from(""),
545 value: node_trace.output.clone(),
546 },
547 operand_values: HashMap::new(),
548 writes: shallow_writes(&node_trace.input, &node_trace.output),
549 reads: Vec::new(),
550 });
551}
552
553fn prefixed(prefix: &str, id: &str) -> Arc<str> {
554 if prefix.is_empty() {
555 Arc::from(id)
556 } else {
557 Arc::from(format!("{prefix}{id}"))
558 }
559}
560
561fn output_prefixed(paths: &NodePaths, key: &str) -> Arc<str> {
562 if paths.output_prefix.is_empty() {
563 Arc::from(key)
564 } else {
565 Arc::from(format!("{}.{key}", paths.output_prefix.join(".")))
566 }
567}
568
569fn map_read(paths: &NodePaths, path: &str) -> Option<Arc<str>> {
570 match &paths.read_base {
571 ReadBase::NodeInput => Some(Arc::from(path)),
572 ReadBase::Opaque => None,
573 ReadBase::Prefixed(prefix) => Some(Arc::from(format!("{}.{path}", prefix.join(".")))),
574 }
575}
576
577fn loop_label(node: &DecisionNode, paths: &NodePaths) -> String {
578 match &paths.read_base {
579 ReadBase::Prefixed(segments) => segments.join("."),
580 _ => node.name.to_string(),
581 }
582}
583
584fn instance_for(
585 node: &DecisionNode,
586 paths: &NodePaths,
587 loop_mode: bool,
588 total: usize,
589 index: usize,
590 inherited: Option<&Arc<str>>,
591) -> Option<Arc<str>> {
592 if loop_mode && total > 1 {
593 Some(Arc::from(format!("{}.{index}", loop_label(node, paths))))
594 } else {
595 inherited.cloned()
596 }
597}
598
599fn trace_entries(trace_data: Option<&Variable>, loop_mode: bool) -> Vec<Variable> {
600 match trace_data {
601 Some(Variable::Array(items)) if loop_mode => items.borrow().iter().cloned().collect(),
602 Some(data) => vec![data.clone()],
603 None => vec![Variable::Null],
604 }
605}
606
607fn element_at(value: &Variable, index: usize) -> Option<Variable> {
608 value
609 .as_array()
610 .and_then(|items| items.borrow().get(index).cloned())
611}
612
613fn iteration_element(
614 node_trace: &DecisionGraphTrace,
615 paths: &NodePaths,
616 loop_mode: bool,
617 index: usize,
618) -> Option<Variable> {
619 if !loop_mode {
620 return None;
621 }
622 let ReadBase::Prefixed(prefix) = &paths.read_base else {
623 return None;
624 };
625 node_trace
626 .input
627 .dot(&prefix.join("."))
628 .and_then(|collection| element_at(&collection, index))
629}
630
631fn operand_values(
632 local_reads: &[Arc<str>],
633 paths: &NodePaths,
634 node_input: &Variable,
635 element: Option<&Variable>,
636 dollar: Option<&Variable>,
637) -> HashMap<Arc<str>, Variable> {
638 let mut out: HashMap<Arc<str>, Variable> = HashMap::new();
639 for read in local_reads {
640 let value = if let Some(rest) = read.strip_prefix("$.") {
641 dollar.and_then(|scope| scope.dot(rest))
642 } else if let Some(element) = element {
643 element.dot(read)
644 } else {
645 match &paths.read_base {
646 ReadBase::NodeInput => node_input.dot(read),
647 ReadBase::Prefixed(prefix) => {
648 node_input.dot(&format!("{}.{read}", prefix.join(".")))
649 }
650 ReadBase::Opaque => None,
651 }
652 };
653 if let Some(value) = value {
654 out.insert(read.clone(), value.deep_clone());
655 }
656 }
657 out
658}
659
660fn expression_trace_result(entry: &Variable, key: &str) -> Option<Variable> {
664 let obj = entry.as_object()?;
665 let slot = obj
666 .borrow()
667 .get(&zen_types::symbol::Symbol::from(key))?
668 .shallow_clone();
669 slot.dot("result")
670}
671
672fn expression_dollar_scope(rows: &[zen_types::decision::Expression], entry: &Variable) -> Variable {
673 let scope = Variable::empty_object();
674 for row in rows {
675 if row.key.is_empty() {
676 continue;
677 }
678 let Some(value) = expression_trace_result(entry, row.key.as_ref()) else {
679 continue;
680 };
681 scope.dot_insert(row.key.as_ref(), value);
682 }
683 scope
684}
685
686fn shallow_writes(input: &Variable, output: &Variable) -> Vec<WriteTrace> {
687 let Some(entries) = output.as_object() else {
688 return Vec::new();
689 };
690 let mut writes: Vec<WriteTrace> = Vec::new();
691 for (key, value) in entries.borrow().iter() {
692 if key.starts_with('$') {
693 continue;
694 }
695 if input
696 .dot(key.as_str())
697 .is_some_and(|previous| previous == *value)
698 {
699 continue;
700 }
701 writes.push(WriteTrace {
702 path: Arc::from(key.as_ref()),
703 value: value.deep_clone(),
704 });
705 }
706 writes.sort_by(|a, b| a.path.cmp(&b.path));
707 writes
708}
709
710fn sub_trace_maps(trace_data: Option<&Variable>) -> Vec<GraphTraceMap> {
711 match trace_data {
712 Some(Variable::Array(items)) => items.borrow().iter().filter_map(as_trace_map).collect(),
713 Some(data) => as_trace_map(data).map(|map| vec![map]).unwrap_or_default(),
714 None => Vec::new(),
715 }
716}
717
718fn as_trace_map(value: &Variable) -> Option<GraphTraceMap> {
719 if !matches!(value, Variable::Object(_)) {
720 return None;
721 }
722 let json = serde_json::to_value(value).ok()?;
723 let map: GraphTraceMap = serde_json::from_value(json).ok()?;
724 (!map.is_empty()).then_some(map)
725}
726
727fn subtree_free_reads(children: &[BlockExecution], paths: &NodePaths) -> Vec<Arc<str>> {
728 let mut written: Vec<Arc<str>> = Vec::new();
729 for child in children {
730 for write in &child.writes {
731 written.push(write.path.clone());
732 }
733 match &child.trace {
734 BlockTrace::Expression { property, .. } if !property.is_empty() => {
735 written.push(property.clone());
736 }
737 BlockTrace::DecisionTable { evaluations, .. } => {
738 for evaluation in evaluations {
739 written.extend(evaluation.keys().cloned());
740 }
741 }
742 _ => {}
743 }
744 }
745 let covered = |read: &str| {
746 written
747 .iter()
748 .any(|path| read == path.as_ref() || read.starts_with(&format!("{path}.")))
749 };
750 let mut out: Vec<Arc<str>> = Vec::new();
751 for child in children {
752 for read in &child.reads {
753 if covered(read) {
754 continue;
755 }
756 if let Some(mapped) = map_read(paths, read) {
757 out.push(mapped);
758 }
759 }
760 }
761 out.sort();
762 out.dedup();
763 out
764}