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