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