Skip to main content

json_eval_rs/jsoneval/
dependents.rs

1use super::JSONEval;
2use crate::jsoneval::json_parser;
3use crate::jsoneval::path_utils;
4use crate::rlogic::{LogicId, RLogic};
5use crate::jsoneval::types::DependentItem;
6use crate::jsoneval::cancellation::CancellationToken;
7use crate::utils::clean_float_noise_scalar;
8use crate::EvalData;
9
10use indexmap::{IndexMap, IndexSet};
11use serde_json::Value;
12
13
14impl JSONEval {
15    /// Evaluate fields that depend on a changed path
16    /// This processes all dependent fields transitively when a source field changes
17    pub fn evaluate_dependents(
18        &mut self,
19        changed_paths: &[String],
20        data: Option<&str>,
21        context: Option<&str>,
22        re_evaluate: bool,
23        token: Option<&CancellationToken>,
24        mut canceled_paths: Option<&mut Vec<String>>,
25    ) -> Result<Value, String> {
26        // Check cancellation
27        if let Some(t) = token {
28            if t.is_cancelled() {
29                return Err("Cancelled".to_string());
30            }
31        }
32        // Acquire lock for synchronous execution
33        let _lock = self.eval_lock.lock().unwrap();
34
35        // Update data if provided
36        if let Some(data_str) = data {
37            // Save old data for comparison
38            let old_data = self.eval_data.snapshot_data();
39
40            let data_value = json_parser::parse_json_str(data_str)?;
41            let context_value = if let Some(ctx) = context {
42                json_parser::parse_json_str(ctx)?
43            } else {
44                Value::Object(serde_json::Map::new())
45            };
46            self.eval_data
47                .replace_data_and_context(data_value.clone(), context_value);
48
49            // Selectively purge cache entries that depend on changed data
50            // Only purge if values actually changed
51            self.purge_cache_for_changed_data_with_comparison(&old_data, &data_value);
52        }
53
54        let mut result = Vec::new();
55        let mut processed = IndexSet::new();
56
57        // Normalize all changed paths and add to processing queue
58        // Converts: "illustration.insured.name" -> "#/illustration/properties/insured/properties/name"
59        let mut to_process: Vec<(String, bool)> = changed_paths
60            .iter()
61            .map(|path| (path_utils::dot_notation_to_schema_pointer(path), false))
62            .collect(); // (path, is_transitive)
63
64        // Process dependents recursively (always nested/transitive)
65        Self::process_dependents_queue(
66            &self.engine,
67            &self.evaluations,
68            &mut self.eval_data,
69            &self.dependents_evaluations,
70            &self.evaluated_schema,
71            &mut to_process,
72            &mut processed,
73            &mut result,
74            token,
75            canceled_paths.as_mut().map(|v| &mut **v)
76        )?;
77
78        // If re_evaluate is true, perform full evaluation with the mutated eval_data
79        // Then perform post-evaluation checks (ReadOnly, Hidden)
80        if re_evaluate {
81            // Drop lock for evaluate_internal
82            drop(_lock); 
83
84            // Clear the entire eval cache before re-evaluation.
85            // The dependents graph processing above may have modified eval_data for
86            // many fields beyond the user's changed_paths, but those changes were not
87            // tracked for cache purging. Clearing ensures evaluate_internal doesn't
88            // return stale cached results for fields whose dependencies were updated.
89            self.eval_cache.clear();
90
91            self.evaluate_internal(None, token)?;
92            
93            // Re-acquire lock for ReadOnly/Hidden processing
94            let _lock = self.eval_lock.lock().unwrap();
95
96            // 1. Read-Only Pass
97            // Collect read-only fields - include ALL readonly values in the result
98            let mut readonly_changes = Vec::new();
99            let mut readonly_values = Vec::new();  // Track all readonly values (including unchanged)
100            
101            // OPTIMIZATION: Use conditional_readonly_fields cache instead of recursing whole schema
102            // self.collect_readonly_fixes(&self.evaluated_schema, "#", &mut readonly_changes);
103            for path in self.conditional_readonly_fields.iter() {
104                let normalized = path_utils::normalize_to_json_pointer(path);
105                if let Some(schema_element) = self.evaluated_schema.pointer(&normalized) {
106                    self.check_readonly_for_dependents(schema_element, path, &mut readonly_changes, &mut readonly_values);
107                }
108            }
109
110            // Apply fixes for changed values and add to queue
111            for (path, schema_value) in readonly_changes {
112                // Set data to match schema value
113                let data_path = path_utils::normalize_to_json_pointer(&path)
114                    .replace("/properties/", "/")
115                    .trim_start_matches('#')
116                    .to_string();
117                
118                self.eval_data.set(&data_path, schema_value.clone());
119                
120                // Add to process queue for changed values
121                to_process.push((path, true));
122            }
123            
124            // Add ALL readonly values to result (both changed and unchanged)
125            for (path, schema_value) in readonly_values {
126                let data_path = path_utils::normalize_to_json_pointer(&path)
127                    .replace("/properties/", "/")
128                    .trim_start_matches('#')
129                    .to_string();
130                
131                let mut change_obj = serde_json::Map::new();
132                change_obj.insert("$ref".to_string(), Value::String(path_utils::pointer_to_dot_notation(&data_path)));
133                change_obj.insert("$readonly".to_string(), Value::Bool(true));
134                change_obj.insert("value".to_string(), schema_value);
135                
136                result.push(Value::Object(change_obj));
137            }
138            
139            // Refund process queue for ReadOnly effects
140            if !to_process.is_empty() {
141                 Self::process_dependents_queue(
142                    &self.engine,
143                    &self.evaluations,
144                    &mut self.eval_data,
145                    &self.dependents_evaluations,
146                    &self.evaluated_schema,
147                    &mut to_process,
148                    &mut processed,
149                    &mut result,
150                    token,
151                    canceled_paths.as_mut().map(|v| &mut **v)
152                )?;
153            }
154
155            // 2. Recursive Hide Pass
156            // Collect hidden fields that have values
157            let mut hidden_fields = Vec::new();
158            // OPTIMIZATION: Use conditional_hidden_fields cache instead of recursing whole schema
159            // self.collect_hidden_fields(&self.evaluated_schema, "#", &mut hidden_fields);
160            for path in self.conditional_hidden_fields.iter() {
161                let normalized = path_utils::normalize_to_json_pointer(path);
162                 if let Some(schema_element) = self.evaluated_schema.pointer(&normalized) {
163                    self.check_hidden_field(schema_element, path, &mut hidden_fields);
164                }
165            }
166            
167            // Logic for recursive hiding (using reffed_by)
168            if !hidden_fields.is_empty() {
169                Self::recursive_hide_effect(
170                    &self.engine,
171                    &self.evaluations,
172                    &self.reffed_by,
173                    &mut self.eval_data,
174                    hidden_fields, 
175                    &mut to_process, 
176                    &mut result
177                );
178            }
179            
180            // Process queue for Hidden effects
181             if !to_process.is_empty() {
182                 Self::process_dependents_queue(
183                    &self.engine,
184                    &self.evaluations,
185                    &mut self.eval_data,
186                    &self.dependents_evaluations,
187                    &self.evaluated_schema,
188                    &mut to_process,
189                    &mut processed,
190                    &mut result,
191                    token,
192                    canceled_paths.as_mut().map(|v| &mut **v)
193                )?;
194            }
195        }
196
197        Ok(Value::Array(result))
198    }
199
200    /// Helper to evaluate a dependent value - uses pre-compiled eval keys for fast lookup
201    pub(crate) fn evaluate_dependent_value_static(
202        engine: &RLogic,
203        evaluations: &IndexMap<String, LogicId>,
204        eval_data: &EvalData,
205        value: &Value,
206        changed_field_value: &Value,
207        changed_field_ref_value: &Value,
208    ) -> Result<Value, String> {
209        match value {
210            // If it's a String, check if it's an eval key reference
211            Value::String(eval_key) => {
212                if let Some(logic_id) = evaluations.get(eval_key) {
213                    // It's a pre-compiled evaluation - run it with scoped context
214                    // Create internal context with $value and $refValue
215                    let mut internal_context = serde_json::Map::new();
216                    internal_context.insert("$value".to_string(), changed_field_value.clone());
217                    internal_context.insert("$refValue".to_string(), changed_field_ref_value.clone());
218                    let context_value = Value::Object(internal_context);
219
220                    let result = engine.run_with_context(logic_id, eval_data.data(), &context_value)
221                        .map_err(|e| format!("Failed to evaluate dependent logic '{}': {}", eval_key, e))?;
222                    Ok(result)
223                } else {
224                    // It's a regular string value
225                    Ok(value.clone())
226                }
227            }
228            // For backwards compatibility: compile $evaluation on-the-fly
229            // This shouldn't happen with properly parsed schemas
230            Value::Object(map) if map.contains_key("$evaluation") => {
231                Err("Dependent evaluation contains unparsed $evaluation - schema was not properly parsed".to_string())
232            }
233            // Primitive value - return as-is
234            _ => Ok(value.clone()),
235        }
236    }
237
238    /// Check if a single field is readonly and populate vectors for both changes and all values
239    pub(crate) fn check_readonly_for_dependents(
240        &self,
241        schema_element: &Value,
242        path: &str,
243        changes: &mut Vec<(String, Value)>,
244        all_values: &mut Vec<(String, Value)>,
245    ) {
246        match schema_element {
247            Value::Object(map) => {
248                // Check if field is disabled (ReadOnly)
249                let mut is_disabled = false;
250                if let Some(Value::Object(condition)) = map.get("condition") {
251                    if let Some(Value::Bool(d)) = condition.get("disabled") {
252                        is_disabled = *d;
253                    }
254                }
255
256                // Check skipReadOnlyValue config
257                 let mut skip_readonly = false;
258                if let Some(Value::Object(config)) = map.get("config") {
259                    if let Some(Value::Object(all)) = config.get("all") {
260                         if let Some(Value::Bool(skip)) = all.get("skipReadOnlyValue") {
261                             skip_readonly = *skip;
262                         }
263                    }
264                }
265
266                if is_disabled && !skip_readonly {
267                    if let Some(schema_value) = map.get("value") {
268                         let data_path = path_utils::normalize_to_json_pointer(path)
269                            .replace("/properties/", "/")
270                            .trim_start_matches('#')
271                            .to_string();
272                         
273                         let current_data = self.eval_data.data().pointer(&data_path).unwrap_or(&Value::Null);
274                         
275                         // Add to all_values (include in dependents result regardless of change)
276                         all_values.push((path.to_string(), schema_value.clone()));
277                         
278                         // Only add to changes if value doesn't match
279                         if current_data != schema_value {
280                             changes.push((path.to_string(), schema_value.clone()));
281                         }
282                    }
283                }
284            }
285            _ => {}
286        }
287    }
288    
289    /// Recursively collect read-only fields that need updates (Legacy/Full-Scan)
290    #[allow(dead_code)]
291    pub(crate) fn collect_readonly_fixes(
292        &self,
293        schema_element: &Value,
294        path: &str,
295        changes: &mut Vec<(String, Value)>,
296    ) {
297        match schema_element {
298            Value::Object(map) => {
299                // Check if field is disabled (ReadOnly)
300                let mut is_disabled = false;
301                if let Some(Value::Object(condition)) = map.get("condition") {
302                    if let Some(Value::Bool(d)) = condition.get("disabled") {
303                        is_disabled = *d;
304                    }
305                }
306
307                // Check skipReadOnlyValue config
308                 let mut skip_readonly = false;
309                if let Some(Value::Object(config)) = map.get("config") {
310                    if let Some(Value::Object(all)) = config.get("all") {
311                         if let Some(Value::Bool(skip)) = all.get("skipReadOnlyValue") {
312                             skip_readonly = *skip;
313                         }
314                    }
315                }
316
317                if is_disabled && !skip_readonly {
318                    // Check if it's a value field (has "value" property or implicit via path?)
319                    // In JS: "const readOnlyValues = this.getSchemaValues();"
320                    // We only care if data != schema value
321                    if let Some(schema_value) = map.get("value") {
322                         let data_path = path_utils::normalize_to_json_pointer(path)
323                            .replace("/properties/", "/")
324                            .trim_start_matches('#')
325                            .to_string();
326                         
327                         let current_data = self.eval_data.data().pointer(&data_path).unwrap_or(&Value::Null);
328                         
329                         if current_data != schema_value {
330                             changes.push((path.to_string(), schema_value.clone()));
331                         }
332                    }
333                }
334
335                // Recurse into properties
336                 if let Some(Value::Object(props)) = map.get("properties") {
337                    for (key, val) in props {
338                        let next_path = if path == "#" {
339                            format!("#/properties/{}", key)
340                        } else {
341                            format!("{}/properties/{}", path, key)
342                        };
343                        self.collect_readonly_fixes(val, &next_path, changes);
344                    }
345                }
346            }
347            _ => {}
348        }
349    }
350
351    /// Check if a single field is hidden and needs clearing (Optimized non-recursive)
352    pub(crate) fn check_hidden_field(
353        &self,
354        schema_element: &Value,
355        path: &str,
356        hidden_fields: &mut Vec<String>,
357    ) {
358         match schema_element {
359            Value::Object(map) => {
360                 // Check if field is hidden
361                let mut is_hidden = false;
362                if let Some(Value::Object(condition)) = map.get("condition") {
363                    if let Some(Value::Bool(h)) = condition.get("hidden") {
364                        is_hidden = *h;
365                    }
366                }
367
368                 // Check keepHiddenValue config
369                 let mut keep_hidden = false;
370                if let Some(Value::Object(config)) = map.get("config") {
371                    if let Some(Value::Object(all)) = config.get("all") {
372                         if let Some(Value::Bool(keep)) = all.get("keepHiddenValue") {
373                             keep_hidden = *keep;
374                         }
375                    }
376                }
377
378                if is_hidden && !keep_hidden {
379                     let data_path = path_utils::normalize_to_json_pointer(path)
380                        .replace("/properties/", "/")
381                        .trim_start_matches('#')
382                        .to_string();
383
384                     let current_data = self.eval_data.data().pointer(&data_path).unwrap_or(&Value::Null);
385                     
386                     // If hidden and has non-empty value, add to list
387                     if current_data != &Value::Null && current_data != "" {
388                         hidden_fields.push(path.to_string());
389                     }
390                }
391            }
392             _ => {}
393         }
394    }
395
396    /// Recursively collect hidden fields that have values (candidates for clearing) (Legacy/Full-Scan)
397    #[allow(dead_code)]
398    pub(crate) fn collect_hidden_fields(
399        &self,
400        schema_element: &Value,
401        path: &str,
402        hidden_fields: &mut Vec<String>,
403    ) {
404         match schema_element {
405            Value::Object(map) => {
406                 // Check if field is hidden
407                let mut is_hidden = false;
408                if let Some(Value::Object(condition)) = map.get("condition") {
409                    if let Some(Value::Bool(h)) = condition.get("hidden") {
410                        is_hidden = *h;
411                    }
412                }
413
414                 // Check keepHiddenValue config
415                 let mut keep_hidden = false;
416                if let Some(Value::Object(config)) = map.get("config") {
417                    if let Some(Value::Object(all)) = config.get("all") {
418                         if let Some(Value::Bool(keep)) = all.get("keepHiddenValue") {
419                             keep_hidden = *keep;
420                         }
421                    }
422                }
423                
424                if is_hidden && !keep_hidden {
425                     let data_path = path_utils::normalize_to_json_pointer(path)
426                        .replace("/properties/", "/")
427                        .trim_start_matches('#')
428                        .to_string();
429
430                     let current_data = self.eval_data.data().pointer(&data_path).unwrap_or(&Value::Null);
431                     
432                     // If hidden and has non-empty value, add to list
433                     if current_data != &Value::Null && current_data != "" {
434                         hidden_fields.push(path.to_string());
435                     }
436                }
437
438                // Recurse into children
439                for (key, val) in map {
440                    if key == "properties" {
441                        if let Value::Object(props) = val {
442                            for (p_key, p_val) in props {
443                                let next_path = if path == "#" {
444                                    format!("#/properties/{}", p_key)
445                                } else {
446                                    format!("{}/properties/{}", path, p_key)
447                                };
448                                self.collect_hidden_fields(p_val, &next_path, hidden_fields);
449                            }
450                        }
451                    } else if let Value::Object(_) = val {
452                        // Skip known metadata keys and explicitly handled keys
453                        if key == "condition" 
454                            || key == "config" 
455                            || key == "rules" 
456                            || key == "dependents" 
457                            || key == "hideLayout" 
458                            || key == "$layout" 
459                            || key == "$params" 
460                            || key == "definitions"
461                            || key == "$defs"
462                            || key.starts_with('$') 
463                        {
464                            continue;
465                        }
466                        
467                         let next_path = if path == "#" {
468                            format!("#/{}", key)
469                        } else {
470                            format!("{}/{}", path, key)
471                        };
472                        self.collect_hidden_fields(val, &next_path, hidden_fields);
473                    }
474                }
475            }
476            _ => {}
477        }
478    }
479
480    /// Perform recursive hiding effect using reffed_by graph
481    pub(crate) fn recursive_hide_effect(
482        engine: &RLogic,
483        evaluations: &IndexMap<String, LogicId>,
484        reffed_by: &IndexMap<String, Vec<String>>,
485        eval_data: &mut EvalData,
486        mut hidden_fields: Vec<String>,
487        queue: &mut Vec<(String, bool)>, 
488        result: &mut Vec<Value>
489    ) {
490        while let Some(hf) = hidden_fields.pop() {
491            let data_path = path_utils::normalize_to_json_pointer(&hf)
492                .replace("/properties/", "/")
493                .trim_start_matches('#')
494                .to_string();
495            
496            // clear data
497            eval_data.set(&data_path, Value::Null);
498            
499             // Create dependent object for result
500            let mut change_obj = serde_json::Map::new();
501            change_obj.insert("$ref".to_string(), Value::String(path_utils::pointer_to_dot_notation(&data_path)));
502            change_obj.insert("$hidden".to_string(), Value::Bool(true));
503            change_obj.insert("clear".to_string(), Value::Bool(true));
504            result.push(Value::Object(change_obj));
505            
506            // Add to queue for standard dependent processing
507            queue.push((hf.clone(), true));
508
509            // Check reffed_by to find other fields that might become hidden
510            if let Some(referencing_fields) = reffed_by.get(&data_path) {
511                for rb in referencing_fields {
512                    // Evaluate condition.hidden for rb
513                    // We need a way to run specific evaluation?
514                    // We can check if rb has a hidden evaluation in self.evaluations
515                    let hidden_eval_key = format!("{}/condition/hidden", rb);
516                    
517                    if let Some(logic_id) = evaluations.get(&hidden_eval_key) {
518                        // Run evaluation
519                        // Context: $value = current field (rb) value? No, $value usually refers to changed field in deps.
520                        // But here we are just re-evaluating the rule.
521                        // In JS logic: "const result = hiddenFn(runnerCtx);"
522                        // runnerCtx has the updated data (we just set hf to null).
523                        
524                         let rb_data_path = path_utils::normalize_to_json_pointer(rb)
525                                .replace("/properties/", "/")
526                                .trim_start_matches('#')
527                                .to_string();
528                         let rb_value = eval_data.data().pointer(&rb_data_path).cloned().unwrap_or(Value::Null);
529                         
530                         // We can use engine.run w/ eval_data
531                         if let Ok(Value::Bool(is_hidden)) = engine.run(
532                             logic_id, 
533                             eval_data.data()
534                         ) {
535                             if is_hidden {
536                                 // Check if rb is not already in hidden_fields and has value
537                                 // rb is &String, hidden_fields is Vec<String>
538                                 if !hidden_fields.contains(rb) {
539                                     let has_value = rb_value != Value::Null && rb_value != "";
540                                     if has_value {
541                                          hidden_fields.push(rb.clone());
542                                     }
543                                 }
544                             }
545                         }
546                    }
547                }
548            }
549        }
550    }
551
552    /// Process the dependents queue
553    /// This handles the transitive propagation of changes based on the dependents graph
554    pub(crate) fn process_dependents_queue(
555        engine: &RLogic,
556        evaluations: &IndexMap<String, LogicId>,
557        eval_data: &mut EvalData,
558        dependents_evaluations: &IndexMap<String, Vec<DependentItem>>,
559        evaluated_schema: &Value,
560        queue: &mut Vec<(String, bool)>,
561        processed: &mut IndexSet<String>,
562        result: &mut Vec<Value>,
563        token: Option<&CancellationToken>,
564        canceled_paths: Option<&mut Vec<String>>,
565    ) -> Result<(), String> {
566        while let Some((current_path, is_transitive)) = queue.pop() {
567            if let Some(t) = token {
568                if t.is_cancelled() {
569                    // Accumulate canceled paths if buffer provided
570                    if let Some(cp) = canceled_paths {
571                        cp.push(current_path.clone());
572                        // Also push remaining items in queue?
573                        // The user request says "accumulate canceled path if provided", usually implies what was actively cancelled 
574                        // or what was pending. Since we pop one by one, we can just dump the queue back or just push pending.
575                        // But since we just popped `current_path`, it is the one being cancelled on.
576                        // Let's also drain the queue.
577                        for (path, _) in queue.iter() {
578                             cp.push(path.clone());
579                        }
580                    }
581                    return Err("Cancelled".to_string());
582                }
583            }
584            if processed.contains(&current_path) {
585                continue;
586            }
587            processed.insert(current_path.clone());
588
589            // Get the value of the changed field for $value context
590            let current_data_path = path_utils::normalize_to_json_pointer(&current_path)
591                .replace("/properties/", "/")
592                .trim_start_matches('#')
593                .to_string();
594            let mut current_value = eval_data
595                .data()
596                .pointer(&current_data_path)
597                .cloned()
598                .unwrap_or(Value::Null);
599
600            // Find dependents for this path
601            if let Some(dependent_items) = dependents_evaluations.get(&current_path) {
602                for dep_item in dependent_items {
603                    let ref_path = &dep_item.ref_path;
604                    let pointer_path = path_utils::normalize_to_json_pointer(ref_path);
605                    // Data paths don't include /properties/, strip it for data access
606                    let data_path = pointer_path.replace("/properties/", "/");
607
608                    let current_ref_value = eval_data
609                        .data()
610                        .pointer(&data_path)
611                        .cloned()
612                        .unwrap_or(Value::Null);
613
614                    // Get field and parent field from schema
615                    let field = evaluated_schema.pointer(&pointer_path).cloned();
616
617                    // Get parent field - skip /properties/ to get actual parent object
618                    let parent_path = if let Some(last_slash) = pointer_path.rfind("/properties") {
619                        &pointer_path[..last_slash]
620                    } else {
621                        "/"
622                    };
623                    let mut parent_field = if parent_path.is_empty() || parent_path == "/" {
624                        evaluated_schema.clone()
625                    } else {
626                        evaluated_schema
627                            .pointer(parent_path)
628                            .cloned()
629                            .unwrap_or_else(|| Value::Object(serde_json::Map::new()))
630                    };
631
632                    // omit properties to minimize size of parent field
633                    if let Value::Object(ref mut map) = parent_field {
634                        map.remove("properties");
635                        map.remove("$layout");
636                    }
637
638                    let mut change_obj = serde_json::Map::new();
639                    change_obj.insert(
640                        "$ref".to_string(),
641                        Value::String(path_utils::pointer_to_dot_notation(&data_path)),
642                    );
643                    if let Some(f) = field {
644                        change_obj.insert("$field".to_string(), f);
645                    }
646                    change_obj.insert("$parentField".to_string(), parent_field);
647                    change_obj.insert("transitive".to_string(), Value::Bool(is_transitive));
648
649                    let mut add_transitive = false;
650                    let mut add_deps = false;
651                    // Process clear
652                    if let Some(clear_val) = &dep_item.clear {
653                        let should_clear = Self::evaluate_dependent_value_static(
654                            engine,
655                            evaluations,
656                            eval_data,
657                            clear_val,
658                            &current_value,
659                            &current_ref_value,
660                        )?;
661                        let clear_bool = match should_clear {
662                            Value::Bool(b) => b,
663                            _ => false,
664                        };
665
666                        if clear_bool {
667                            // Clear the field
668                            if data_path == current_data_path {
669                                current_value = Value::Null;
670                            }
671                            eval_data.set(&data_path, Value::Null);
672                            change_obj.insert("clear".to_string(), Value::Bool(true));
673                            add_transitive = true;
674                            add_deps = true;
675                        }
676                    }
677
678                    // Process value
679                    if let Some(value_val) = &dep_item.value {
680                        let computed_value = Self::evaluate_dependent_value_static(
681                            engine,
682                            evaluations,
683                            eval_data,
684                            value_val,
685                            &current_value,
686                            &current_ref_value,
687                        )?;
688                        let cleaned_val = clean_float_noise_scalar(computed_value);
689
690                        if cleaned_val != current_ref_value && cleaned_val != Value::Null {
691                            // Set the value
692                            if data_path == current_data_path {
693                                current_value = cleaned_val.clone();
694                            }
695                            eval_data.set(&data_path, cleaned_val.clone());
696                            change_obj.insert("value".to_string(), cleaned_val);
697                            add_transitive = true;
698                            add_deps = true;
699                        }
700                    }
701
702                    // add only when has clear / value
703                    if add_deps {
704                        result.push(Value::Object(change_obj));
705                    }
706
707                    // Add this dependent to queue for transitive processing
708                    if add_transitive {
709                        queue.push((ref_path.clone(), true));
710                    }
711                }
712            }
713        }
714        Ok(())
715    }
716}