Skip to main content

json_eval_rs/parse_schema/
common.rs

1use crate::jsoneval::path_utils;
2use crate::jsoneval::table_metadata::{
3    ColumnMetadata, RepeatBoundMetadata, RowMetadata, TableMetadata,
4};
5use crate::{LogicId, RLogic};
6/// Shared utilities for schema parsing (used by both legacy and parsed implementations)
7use indexmap::{IndexMap, IndexSet};
8use serde_json::Map;
9use serde_json::Value;
10use std::sync::Arc;
11
12/// Collect schema field pointers referenced by `$layout` elements.
13///
14/// This runs once during schema parsing. Runtime schema-value extraction then uses
15/// an O(1) set membership check instead of re-walking a potentially large schema.
16pub fn collect_layout_field_refs(value: &Value, refs: &mut IndexSet<String>) {
17    fn collect_elements(elements: &Value, refs: &mut IndexSet<String>) {
18        let Some(elements) = elements.as_array() else {
19            return;
20        };
21        for element in elements {
22            let Some(map) = element.as_object() else {
23                continue;
24            };
25            if let Some(reference) = map.get("$ref").and_then(Value::as_str) {
26                let pointer = path_utils::normalize_to_json_pointer(reference);
27                refs.insert(pointer.trim_start_matches('#').to_string());
28            }
29            if let Some(children) = map.get("elements") {
30                collect_elements(children, refs);
31            }
32        }
33    }
34
35    match value {
36        Value::Object(map) => {
37            if let Some(elements) = map
38                .get("$layout")
39                .and_then(Value::as_object)
40                .and_then(|layout| layout.get("elements"))
41            {
42                collect_elements(elements, refs);
43            }
44            for child in map.values() {
45                collect_layout_field_refs(child, refs);
46            }
47        }
48        Value::Array(values) => {
49            for child in values {
50                collect_layout_field_refs(child, refs);
51            }
52        }
53        _ => {}
54    }
55}
56
57/// Collect $ref dependencies from a JSON value recursively
58pub fn collect_refs(value: &Value, refs: &mut IndexSet<String>) {
59    match value {
60        Value::Object(map) => {
61            if let Some(path) = map.get("$ref").and_then(Value::as_str) {
62                refs.insert(path_utils::normalize_to_json_pointer(path).into_owned());
63            }
64            if let Some(path) = map.get("ref").and_then(Value::as_str) {
65                refs.insert(path_utils::normalize_to_json_pointer(path).into_owned());
66            }
67            if let Some(var_val) = map.get("var") {
68                match var_val {
69                    Value::String(s) => {
70                        refs.insert(s.clone());
71                    }
72                    Value::Array(arr) => {
73                        if let Some(path) = arr.get(0).and_then(Value::as_str) {
74                            refs.insert(path.to_string());
75                        }
76                    }
77                    _ => {}
78                }
79            }
80            for val in map.values() {
81                collect_refs(val, refs);
82            }
83        }
84        Value::Array(arr) => {
85            for val in arr {
86                collect_refs(val, refs);
87            }
88        }
89        _ => {}
90    }
91}
92
93/// Check if a value contains any actionable schema keys recursively (with depth limit for arrays)
94/// used to skip large pure-data arrays during schema walking
95#[inline]
96pub fn has_actionable_keys(value: &Value) -> bool {
97    match value {
98        Value::Object(map) => {
99            if map.contains_key("$evaluation")
100                || map.contains_key("$table")
101                || map.contains_key("dependents")
102                || map.contains_key("$layout")
103            {
104                return true;
105            }
106
107            // Check for conditional hidden/disabled fields
108            if let Some(Value::Object(condition)) = map.get("condition") {
109                if condition.contains_key("hidden") || condition.contains_key("disabled") {
110                    return true;
111                }
112            }
113
114            // Check for rules object
115            if map.contains_key("rules") {
116                return true;
117            }
118
119            // Check for type="array" with items (subforms)
120            if let Some(Value::String(type_str)) = map.get("type") {
121                if type_str == "array" && map.contains_key("items") {
122                    return true;
123                }
124            }
125
126            // Check for options with URL templates
127            if let Some(Value::String(url)) = map.get("url") {
128                if url.contains('{') && url.contains('}') {
129                    return true;
130                }
131            }
132
133            map.values().any(has_actionable_keys)
134        }
135        Value::Array(arr) => arr.iter().take(5).any(has_actionable_keys),
136        _ => false,
137    }
138}
139
140/// Compute forward/normal column partitions with transitive closure
141///
142/// This function identifies which columns have forward references (dependencies on later columns)
143/// and separates them from normal columns for proper evaluation order.
144pub fn compute_column_partitions(columns: &[ColumnMetadata]) -> (Vec<usize>, Vec<usize>) {
145    use std::collections::HashSet;
146
147    // Build set of all forward-referencing column names (direct + transitive)
148    let mut fwd_cols = HashSet::new();
149    for col in columns {
150        if col.has_forward_ref {
151            fwd_cols.insert(col.name.as_ref());
152        }
153    }
154
155    // Transitive closure: any column that depends on forward columns is also forward
156    loop {
157        let mut changed = false;
158        for col in columns {
159            if !fwd_cols.contains(col.name.as_ref()) {
160                // Check if this column depends on any forward column
161                for dep in col.dependencies.iter() {
162                    // Strip $ prefix from dependency name for comparison
163                    let dep_name = dep.trim_start_matches('$');
164                    if fwd_cols.contains(dep_name) {
165                        fwd_cols.insert(col.name.as_ref());
166                        changed = true;
167                        break;
168                    }
169                }
170            }
171        }
172        // Stop when no more changes
173        if !changed {
174            break;
175        }
176    }
177
178    // Separate into forward and normal indices
179    let mut forward_indices = Vec::new();
180    let mut normal_indices = Vec::new();
181
182    for (idx, col) in columns.iter().enumerate() {
183        if fwd_cols.contains(col.name.as_ref()) {
184            forward_indices.push(idx);
185        } else {
186            normal_indices.push(idx);
187        }
188    }
189
190    let forward_sorted = toposort_column_indices(columns, forward_indices);
191    let normal_sorted = toposort_column_indices(columns, normal_indices);
192
193    (forward_sorted, normal_sorted)
194}
195
196/// Topologically sort column indices within a partition based on intra-row dependencies.
197/// Uses Kahn's algorithm with cycle fallback to guarantee acyclic dependencies evaluate in causal order.
198fn toposort_column_indices(columns: &[ColumnMetadata], indices: Vec<usize>) -> Vec<usize> {
199    if indices.len() <= 1 {
200        return indices;
201    }
202
203    let n = indices.len();
204    let mut name_to_sub_idx = std::collections::HashMap::with_capacity(n);
205    for (sub_idx, &col_idx) in indices.iter().enumerate() {
206        name_to_sub_idx.insert(columns[col_idx].name.as_ref(), sub_idx);
207    }
208
209    let mut in_degree = vec![0usize; n];
210    let mut adj = vec![Vec::new(); n];
211
212    for (u, &col_idx) in indices.iter().enumerate() {
213        for dep in columns[col_idx].dependencies.iter() {
214            if dep.starts_with('$') {
215                let dep_name = dep.trim_start_matches('$');
216                if let Some(&v) = name_to_sub_idx.get(dep_name) {
217                    if v != u {
218                        // v must evaluate before u: edge v -> u
219                        adj[v].push(u);
220                        in_degree[u] += 1;
221                    }
222                }
223            }
224        }
225    }
226
227    let mut queue = std::collections::VecDeque::new();
228    for (i, &deg) in in_degree.iter().enumerate() {
229        if deg == 0 {
230            queue.push_back(i);
231        }
232    }
233
234    let mut sorted = Vec::with_capacity(n);
235    let mut visited = vec![false; n];
236
237    while let Some(v) = queue.pop_front() {
238        visited[v] = true;
239        sorted.push(indices[v]);
240        for &u in &adj[v] {
241            in_degree[u] -= 1;
242            if in_degree[u] == 0 {
243                queue.push_back(u);
244            }
245        }
246    }
247
248    // Cycle fallback: append any unvisited columns in original order
249    if sorted.len() < n {
250        for (i, &was_visited) in visited.iter().enumerate() {
251            if !was_visited {
252                sorted.push(indices[i]);
253            }
254        }
255    }
256
257    sorted
258}
259
260pub fn walk_schema(
261    value: &Value,
262    path: &str,
263    engine: &mut RLogic,
264    evaluations: &mut IndexMap<String, LogicId>,
265    tables: &mut IndexMap<String, Value>,
266    deps: &mut IndexMap<String, IndexSet<String>>,
267    value_fields: &mut Vec<String>,
268    layout_paths: &mut Vec<String>,
269    dependents: &mut IndexMap<String, Vec<crate::DependentItem>>,
270    options_templates: &mut Vec<(String, String, String)>,
271    subforms: &mut Vec<(String, serde_json::Map<String, Value>, Value)>,
272    fields_with_rules: &mut Vec<String>,
273    conditional_hidden_fields: &mut Vec<String>,
274    conditional_readonly_fields: &mut Vec<String>,
275) -> Result<(), String> {
276    match value {
277        Value::Object(map) => {
278            // Check for $evaluation
279            if let Some(evaluation) = map.get("$evaluation") {
280                let key = path.to_string();
281                let logic_value = evaluation.get("logic").unwrap_or(evaluation);
282                let logic_id = engine
283                    .compile(logic_value)
284                    .map_err(|e| format!("failed to compile evaluation at {key}: {e}"))?;
285                evaluations.insert(key.clone(), logic_id);
286
287                // Collect dependencies with smart table inheritance
288                let mut refs: IndexSet<String> = engine
289                    .get_referenced_vars(&logic_id)
290                    .unwrap_or_default()
291                    .into_iter()
292                    .map(|dep| path_utils::canonicalize_schema_path(&dep).into_owned())
293                    .filter(|dep| {
294                        // Filter out simple column references (e.g., "/INSAGE_YEAR", "/PREM_PP")
295                        // These are FINDINDEX/MATCH column names, not actual data dependencies
296                        // Real dependencies have multiple path segments (e.g., "/illustration/properties/...")
297                        // Update: allow top-level fields only if they are system paths or deeper paths
298                        dep.matches('/').count() > 1 || dep.starts_with("/$")
299                    })
300                    .collect();
301                let mut extra_refs = IndexSet::new();
302                collect_refs(logic_value, &mut extra_refs);
303                if !extra_refs.is_empty() {
304                    refs.extend(extra_refs.into_iter());
305                }
306
307                // For table dependencies, inherit parent table path instead of individual rows
308                let refs: IndexSet<String> = refs
309                    .into_iter()
310                    .filter_map(|dep| {
311                        // If dependency is a table row (contains /$table/), inherit table path
312                        if let Some(table_idx) = dep.find("/$table/") {
313                            let table_path = &dep[..table_idx];
314                            Some(table_path.to_string())
315                        } else {
316                            Some(dep.to_string())
317                        }
318                    })
319                    .collect();
320
321                if !refs.is_empty() {
322                    deps.insert(key.clone(), refs);
323                }
324            }
325
326            // Check for $table
327            if let Some(table) = map.get("$table") {
328                let key = path.to_string();
329
330                let rows = table.clone();
331                let datas = map
332                    .get("$datas")
333                    .cloned()
334                    .unwrap_or_else(|| Value::Array(vec![]));
335                let skip = map.get("$skip").cloned().unwrap_or(Value::Bool(false));
336                let clear = map.get("$clear").cloned().unwrap_or(Value::Bool(false));
337
338                let mut table_entry = Map::new();
339                table_entry.insert("rows".to_string(), rows);
340                table_entry.insert("datas".to_string(), datas);
341                table_entry.insert("skip".to_string(), skip);
342                table_entry.insert("clear".to_string(), clear);
343
344                tables.insert(key, Value::Object(table_entry));
345            }
346
347            // Check for $layout with elements
348            if let Some(layout_obj) = map.get("$layout") {
349                if let Some(Value::Array(_)) = layout_obj.get("elements") {
350                    let layout_elements_path = format!("{}/$layout/elements", path);
351                    layout_paths.push(layout_elements_path);
352                }
353            }
354
355            // Check for rules object - collect field path for efficient validation
356            if map.contains_key("rules") && !path.is_empty() && !path.starts_with("#/$") {
357                // Convert JSON pointer path to dotted notation for validation
358                // E.g., "#/properties/form/properties/name" -> "form.name"
359                let field_path = path
360                    .trim_start_matches('#')
361                    .replace("/properties/", ".")
362                    .trim_start_matches('/')
363                    .trim_start_matches('.')
364                    .to_string();
365
366                if !field_path.is_empty() && !field_path.starts_with("$") {
367                    fields_with_rules.push(field_path);
368                }
369            }
370
371            // Check for options with URL templates
372            if let Some(Value::String(url)) = map.get("url") {
373                // Check if URL contains template pattern {variable}
374                if url.contains('{') && url.contains('}') {
375                    // Convert to JSON pointer format for evaluated_schema access
376                    let url_path = path_utils::normalize_to_json_pointer(&format!("{}/url", path))
377                        .into_owned();
378                    let params_path =
379                        path_utils::normalize_to_json_pointer(&format!("{}/params", path))
380                            .into_owned();
381                    options_templates.push((url_path, url.clone(), params_path));
382                }
383            }
384
385            // Check for array fields with items (subforms)
386            if let Some(Value::String(type_str)) = map.get("type") {
387                if type_str == "array" {
388                    if let Some(items) = map.get("items") {
389                        // Store subform info for later creation (after walk completes)
390                        subforms.push((path.to_string(), map.clone(), items.clone()));
391                    }
392                }
393            }
394
395            // Check for conditional hidden/disabled fields
396            if let Some(Value::Object(condition)) = map.get("condition") {
397                // Hidden
398                if condition.contains_key("hidden") {
399                    conditional_hidden_fields.push(path.to_string());
400                }
401                // Disabled (Read Only) - only relevant if it has a value enforce
402                if condition.contains_key("disabled") && map.contains_key("value") {
403                    conditional_readonly_fields.push(path.to_string());
404                }
405            }
406
407            // Check for dependents array
408            if let Some(Value::Array(dependents_arr)) = map.get("dependents") {
409                let mut dependent_items = Vec::new();
410
411                for (dep_idx, dep_item) in dependents_arr.iter().enumerate() {
412                    if let Value::Object(dep_obj) = dep_item {
413                        if let Some(Value::String(ref_path)) = dep_obj.get("$ref") {
414                            // Process clear - compile if it's an $evaluation
415                            let clear_val = if let Some(clear) = dep_obj.get("clear") {
416                                if let Value::Object(clear_obj) = clear {
417                                    if clear_obj.contains_key("$evaluation") {
418                                        // Compile and store the evaluation
419                                        let clear_eval = clear_obj.get("$evaluation").unwrap();
420                                        let clear_key =
421                                            format!("{}/dependents/{}/clear", path, dep_idx);
422                                        let logic_id = engine.compile(clear_eval).map_err(|e| {
423                                            format!(
424                                                "Failed to compile dependent clear at {}: {}",
425                                                clear_key, e
426                                            )
427                                        })?;
428                                        evaluations.insert(clear_key.clone(), logic_id);
429                                        // Replace with eval key reference
430                                        Some(Value::String(clear_key))
431                                    } else {
432                                        Some(clear.clone())
433                                    }
434                                } else {
435                                    Some(clear.clone())
436                                }
437                            } else {
438                                None
439                            };
440
441                            // Process value - compile if it's an $evaluation
442                            let value_val = if let Some(value) = dep_obj.get("value") {
443                                if let Value::Object(value_obj) = value {
444                                    if value_obj.contains_key("$evaluation") {
445                                        // Compile and store the evaluation
446                                        let value_eval = value_obj.get("$evaluation").unwrap();
447                                        let value_key =
448                                            format!("{}/dependents/{}/value", path, dep_idx);
449                                        let logic_id = engine.compile(value_eval).map_err(|e| {
450                                            format!(
451                                                "Failed to compile dependent value at {}: {}",
452                                                value_key, e
453                                            )
454                                        })?;
455                                        evaluations.insert(value_key.clone(), logic_id);
456                                        // Replace with eval key reference
457                                        Some(Value::String(value_key))
458                                    } else {
459                                        Some(value.clone())
460                                    }
461                                } else {
462                                    Some(value.clone())
463                                }
464                            } else {
465                                None
466                            };
467
468                            dependent_items.push(crate::DependentItem {
469                                ref_path: ref_path.clone(),
470                                clear: clear_val,
471                                value: value_val,
472                            });
473                        }
474                    }
475                }
476
477                if !dependent_items.is_empty() {
478                    dependents.insert(path.to_string(), dependent_items);
479                }
480            }
481
482            // Recurse into children
483            Ok(for (key, val) in map {
484                // Skip special evaluation and dependents keys from recursion (already processed above)
485                if key == "$evaluation"
486                    || key == "dependents"
487                    || (key == "items" && map.get("type").and_then(Value::as_str) == Some("array"))
488                {
489                    continue;
490                }
491
492                let next_path = if path == "#" {
493                    format!("#/{key}")
494                } else {
495                    format!("{path}/{key}")
496                };
497
498                // Check if this is a "value" field
499                // Allow $params but exclude other special $ paths like $layout, $items, etc.
500                let is_excluded_special_path = next_path.contains("/$layout/")
501                    || next_path.contains("/$items/")
502                    || next_path.contains("/$options/")
503                    || next_path.contains("/$dependents/")
504                    || next_path.contains("/$rules/");
505
506                if key == "value" && !is_excluded_special_path {
507                    value_fields.push(next_path.clone());
508                }
509
510                // Recurse into all children (including $ keys like $table, $datas, etc.)
511                walk_schema(
512                    val,
513                    &next_path,
514                    engine,
515                    evaluations,
516                    tables,
517                    deps,
518                    value_fields,
519                    layout_paths,
520                    dependents,
521                    options_templates,
522                    subforms,
523                    fields_with_rules,
524                    conditional_hidden_fields,
525                    conditional_readonly_fields,
526                )?;
527            })
528        }
529        Value::Array(arr) => {
530            // Skip large arrays that contain no actionable schema keys.
531            // This avoids recursively walking pure-data arrays (e.g., table rows in $params).
532            // We specifically avoid skipping layout elements which can be large.
533            let is_layout_array = path.contains("$layout") || path.contains("elements");
534            if !is_layout_array && arr.len() > 10 && !has_actionable_keys(value) {
535                return Ok(());
536            }
537            Ok(for (index, item) in arr.iter().enumerate() {
538                let next_path = if path == "#" {
539                    format!("#/{index}")
540                } else {
541                    format!("{path}/{index}")
542                };
543                walk_schema(
544                    item,
545                    &next_path,
546                    engine,
547                    evaluations,
548                    tables,
549                    deps,
550                    value_fields,
551                    layout_paths,
552                    dependents,
553                    options_templates,
554                    subforms,
555                    fields_with_rules,
556                    conditional_hidden_fields,
557                    conditional_readonly_fields,
558                )?;
559            })
560        }
561        _ => Ok(()),
562    }
563}
564pub fn collect_table_dependencies(
565    tables: &IndexMap<String, Value>,
566    dependencies: &mut IndexMap<String, IndexSet<String>>,
567) {
568    for (table_key, _) in tables.iter() {
569        let mut table_deps = IndexSet::new();
570
571        let table_data_prefix = path_utils::normalize_to_json_pointer(table_key)
572            .replace("/properties/", "/")
573            .trim_start_matches('#')
574            .to_string();
575        let table_data_prefix_slash = format!("{}/", table_data_prefix);
576
577        for (eval_key, deps) in dependencies.iter() {
578            let is_child = eval_key.len() > table_key.len()
579                && eval_key.starts_with(table_key.as_str())
580                && eval_key.as_bytes().get(table_key.len()) == Some(&b'/');
581
582            if is_child {
583                for dep in deps {
584                    let dep_data_path = path_utils::normalize_to_json_pointer(dep)
585                        .replace("/properties/", "/")
586                        .trim_start_matches('#')
587                        .to_string();
588
589                    if dep_data_path == table_data_prefix
590                        || dep_data_path.starts_with(&table_data_prefix_slash)
591                    {
592                        continue;
593                    }
594
595                    let is_params_dep = dep.contains("$params");
596                    let is_inline_system = !is_params_dep
597                        && !dep.contains("$context")
598                        && (dep.starts_with("/$") || dep.starts_with('$'));
599                    if is_inline_system {
600                        continue;
601                    }
602                    table_deps.insert(dep.clone());
603                }
604            }
605        }
606
607        if !table_deps.is_empty() {
608            dependencies.insert(table_key.clone(), table_deps);
609        }
610    }
611}
612
613pub fn categorize_evaluations(
614    sorted_evaluations: &[Vec<String>],
615    evaluations: &IndexMap<String, crate::LogicId>,
616    tables: &IndexMap<String, Value>,
617) -> (Vec<String>, Vec<String>) {
618    let batched_keys: IndexSet<String> = sorted_evaluations.iter().flatten().cloned().collect();
619
620    let mut rules_evaluations = Vec::new();
621    let mut others_evaluations = Vec::new();
622
623    for eval_key in evaluations.keys() {
624        if batched_keys.contains(eval_key) {
625            continue;
626        }
627
628        if tables.iter().any(|(key, _)| eval_key.starts_with(key)) {
629            continue;
630        }
631
632        if eval_key.contains("/$params/") {
633            continue;
634        }
635
636        if eval_key.contains("/rules/") {
637            rules_evaluations.push(eval_key.clone());
638        } else if !eval_key.contains("/dependents/") {
639            others_evaluations.push(eval_key.clone());
640        }
641    }
642
643    (rules_evaluations, others_evaluations)
644}
645
646pub fn process_value_fields(
647    value_fields: Vec<String>,
648    tables: &IndexMap<String, Value>,
649) -> Vec<String> {
650    let mut value_evaluations = Vec::new();
651
652    for path in value_fields {
653        if value_evaluations.contains(&path) {
654            continue;
655        }
656
657        if path.contains("/$params/") || tables.iter().any(|(key, _)| path.starts_with(key)) {
658            continue;
659        }
660
661        value_evaluations.push(path);
662    }
663
664    value_evaluations
665}
666
667pub fn compile_table_metadata(
668    evaluations: &IndexMap<String, crate::LogicId>,
669    engine: &crate::RLogic,
670    eval_key: &str,
671    table: &Value,
672) -> Result<TableMetadata, String> {
673    let rows = table
674        .get("rows")
675        .and_then(|v| v.as_array())
676        .ok_or("table missing rows")?;
677    let empty_datas = Vec::new();
678    let datas = table
679        .get("datas")
680        .and_then(|v| v.as_array())
681        .unwrap_or(&empty_datas);
682
683    // Pre-compile data plans with Arc sharing
684    let mut data_plans = Vec::with_capacity(datas.len());
685    for (idx, entry) in datas.iter().enumerate() {
686        let Some(name) = entry.get("name").and_then(|v| v.as_str()) else {
687            continue;
688        };
689        let logic_path = format!("{eval_key}/$datas/{idx}/data");
690        let logic = evaluations.get(&logic_path).copied();
691        let literal = entry.get("data").map(|v| Arc::new(v.clone()));
692        data_plans.push((Arc::from(name), logic, literal));
693    }
694
695    // Pre-compile row plans with dependency analysis
696    let mut row_plans = Vec::with_capacity(rows.len());
697    for (row_idx, row_val) in rows.iter().enumerate() {
698        let Some(row_obj) = row_val.as_object() else {
699            continue;
700        };
701
702        if let Some(repeat_arr) = row_obj.get("$repeat").and_then(|v| v.as_array()) {
703            if repeat_arr.len() == 3 {
704                let start_logic_path = format!("{eval_key}/$table/{row_idx}/$repeat/0");
705                let end_logic_path = format!("{eval_key}/$table/{row_idx}/$repeat/1");
706                let start_logic = evaluations.get(&start_logic_path).copied();
707                let end_logic = evaluations.get(&end_logic_path).copied();
708
709                let start_literal = Arc::new(repeat_arr.get(0).cloned().unwrap_or(Value::Null));
710                let end_literal = Arc::new(repeat_arr.get(1).cloned().unwrap_or(Value::Null));
711
712                if let Some(template) = repeat_arr.get(2).and_then(|v| v.as_object()) {
713                    let mut columns = Vec::with_capacity(template.len());
714                    for (col_name, col_val) in template {
715                        let col_eval_path =
716                            format!("{eval_key}/$table/{row_idx}/$repeat/2/{col_name}");
717                        let logic = evaluations.get(&col_eval_path).copied();
718                        let literal = if logic.is_none() {
719                            Some(col_val.clone())
720                        } else {
721                            None
722                        };
723
724                        // Extract dependencies ONCE at parse time (not during evaluation)
725                        let (dependencies, has_forward_ref) = if let Some(logic_id) = logic {
726                            let deps = engine
727                                .get_referenced_vars(&logic_id)
728                                .unwrap_or_default()
729                                .into_iter()
730                                .filter(|v| {
731                                    v.starts_with('$') && v != "$iteration" && v != "$threshold"
732                                })
733                                .collect();
734                            let has_fwd = engine.has_forward_reference(&logic_id);
735                            (deps, has_fwd)
736                        } else {
737                            (Vec::new(), false)
738                        };
739
740                        columns.push(ColumnMetadata::new(
741                            col_name,
742                            logic,
743                            literal,
744                            dependencies,
745                            has_forward_ref,
746                        ));
747                    }
748
749                    // Pre-compute forward column propagation (transitive closure)
750                    let (forward_cols, normal_cols) = compute_column_partitions(&columns);
751
752                    row_plans.push(RowMetadata::Repeat {
753                        start: RepeatBoundMetadata {
754                            logic: start_logic,
755                            literal: start_literal,
756                        },
757                        end: RepeatBoundMetadata {
758                            logic: end_logic,
759                            literal: end_literal,
760                        },
761                        columns: columns.into(),
762                        forward_cols: forward_cols.into(),
763                        normal_cols: normal_cols.into(),
764                    });
765                    continue;
766                }
767            }
768        }
769
770        // Static row
771        let mut columns = Vec::with_capacity(row_obj.len());
772        for (col_name, col_val) in row_obj {
773            if col_name == "$repeat" {
774                continue;
775            }
776            let col_eval_path = format!("{eval_key}/$table/{row_idx}/{col_name}");
777            let logic = evaluations.get(&col_eval_path).copied();
778            let literal = if logic.is_none() {
779                Some(col_val.clone())
780            } else {
781                None
782            };
783
784            // Extract dependencies ONCE at parse time
785            let (dependencies, has_forward_ref) = if let Some(logic_id) = logic {
786                let deps = engine
787                    .get_referenced_vars(&logic_id)
788                    .unwrap_or_default()
789                    .into_iter()
790                    .filter(|v| v.starts_with('$') && v != "$iteration" && v != "$threshold")
791                    .collect();
792                let has_fwd = engine.has_forward_reference(&logic_id);
793                (deps, has_fwd)
794            } else {
795                (Vec::new(), false)
796            };
797
798            columns.push(ColumnMetadata::new(
799                col_name,
800                logic,
801                literal,
802                dependencies,
803                has_forward_ref,
804            ));
805        }
806        row_plans.push(RowMetadata::Static {
807            columns: columns.into(),
808        });
809    }
810
811    // Pre-compile skip/clear logic
812    let skip_logic = evaluations.get(&format!("{eval_key}/$skip")).copied();
813    let skip_literal = table.get("skip").and_then(Value::as_bool).unwrap_or(false);
814    let clear_logic = evaluations.get(&format!("{eval_key}/$clear")).copied();
815    let clear_literal = table.get("clear").and_then(Value::as_bool).unwrap_or(false);
816
817    Ok(TableMetadata {
818        data_plans: data_plans.into(),
819        row_plans: row_plans.into(),
820        skip_logic,
821        skip_literal,
822        clear_logic,
823        clear_literal,
824    })
825}
826pub fn build_reffed_by(
827    dependencies: &IndexMap<String, IndexSet<String>>,
828) -> IndexMap<String, Vec<String>> {
829    let mut reffed_by: IndexMap<String, Vec<String>> = IndexMap::new();
830
831    for (eval_path, deps) in dependencies.iter() {
832        if eval_path.ends_with("/condition/hidden") {
833            let subject_path = eval_path[..eval_path.len() - 17].to_string();
834
835            for dep in deps {
836                let normalized_dep = path_utils::normalize_to_json_pointer(dep)
837                    .replace("/properties/", "/")
838                    .trim_start_matches('#')
839                    .to_string();
840
841                let dep_key = if normalized_dep.starts_with('/') {
842                    normalized_dep
843                } else {
844                    format!("/{}", normalized_dep)
845                };
846
847                reffed_by
848                    .entry(dep_key)
849                    .or_insert_with(Vec::new)
850                    .push(subject_path.clone());
851            }
852        }
853    }
854
855    reffed_by
856}
857
858pub fn build_dep_formula_triggers(
859    dependents_evaluations: &IndexMap<String, Vec<crate::DependentItem>>,
860    evaluations: &IndexMap<String, crate::LogicId>,
861    engine: &crate::RLogic,
862) -> IndexMap<String, Vec<(String, usize)>> {
863    let mut triggers: IndexMap<String, Vec<(String, usize)>> = IndexMap::new();
864
865    for (source_path, dep_items) in dependents_evaluations.iter() {
866        for (dep_idx, dep_item) in dep_items.iter().enumerate() {
867            let formula_keys: Vec<String> = [
868                dep_item
869                    .value
870                    .as_ref()
871                    .and_then(|v| v.as_str())
872                    .map(|s| s.to_string()),
873                dep_item
874                    .clear
875                    .as_ref()
876                    .and_then(|v| v.as_str())
877                    .map(|s| s.to_string()),
878            ]
879            .into_iter()
880            .flatten()
881            .filter(|k| k.contains("/dependents/"))
882            .collect();
883
884            for formula_key in formula_keys {
885                let logic_id = match evaluations.get(&formula_key).copied() {
886                    Some(id) => id,
887                    None => continue,
888                };
889
890                let refs = engine.get_referenced_vars(&logic_id).unwrap_or_default();
891
892                for dep_ref in refs {
893                    let normalized = path_utils::normalize_to_json_pointer(&dep_ref)
894                        .replace("/properties/", "/")
895                        .trim_start_matches('#')
896                        .to_string();
897                    let dep_key = if normalized.starts_with('/') {
898                        normalized
899                    } else {
900                        format!("/{}", normalized)
901                    };
902
903                    if dep_key.starts_with("/$") {
904                        continue;
905                    }
906
907                    if dep_key.matches('/').count() <= 1 {
908                        continue;
909                    }
910
911                    let source_data = path_utils::normalize_to_json_pointer(source_path)
912                        .replace("/properties/", "/")
913                        .trim_start_matches('#')
914                        .to_string();
915                    let source_data_key = if source_data.starts_with('/') {
916                        source_data
917                    } else {
918                        format!("/{}", source_data)
919                    };
920                    if dep_key == source_data_key {
921                        continue;
922                    }
923
924                    let target_data = path_utils::normalize_to_json_pointer(&dep_item.ref_path)
925                        .replace("/properties/", "/")
926                        .trim_start_matches('#')
927                        .to_string();
928                    let target_data_key = if target_data.starts_with('/') {
929                        target_data
930                    } else {
931                        format!("/{}", target_data)
932                    };
933                    if dep_key == target_data_key {
934                        continue;
935                    }
936
937                    let pair = (source_path.clone(), dep_idx);
938                    let entry = triggers.entry(dep_key).or_insert_with(Vec::new);
939                    if !entry.contains(&pair) {
940                        entry.push(pair);
941                    }
942                }
943            }
944        }
945    }
946
947    for sources in triggers.values_mut() {
948        sources.sort();
949        // Since we check contains above and we sort, it's pretty clean.
950    }
951
952    triggers
953}