Skip to main content

lemma/planning/
execution_plan.rs

1//! Execution plan for evaluated documents
2//!
3//! Provides a complete self-contained execution plan ready for the evaluator.
4//! The plan contains all facts, rules flattened into executable branches,
5//! and execution order - no document structure needed during evaluation.
6
7use crate::planning::graph::Graph;
8use crate::semantic::{
9    Expression, FactPath, FactReference, FactValue, LemmaType, LiteralValue, RulePath,
10};
11use crate::LemmaError;
12use crate::ResourceLimits;
13use crate::Source;
14use serde::{Deserialize, Serialize};
15use std::collections::{HashMap, HashSet};
16use std::sync::Arc;
17
18/// A complete execution plan ready for the evaluator
19///
20/// Contains the topologically sorted list of rules to execute, along with all facts.
21/// Self-contained structure - no document lookups required during evaluation.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ExecutionPlan {
24    /// Main document name
25    pub doc_name: String,
26
27    /// Resolved schema types for value-holding facts.
28    ///
29    /// This is the authoritative schema contract for adapters and validation.
30    #[serde(serialize_with = "crate::serialization::serialize_fact_type_map")]
31    #[serde(deserialize_with = "crate::serialization::deserialize_fact_type_map")]
32    pub fact_schema: HashMap<FactPath, LemmaType>,
33
34    /// Concrete literal values for facts (document-defined literals + user-provided values).
35    #[serde(serialize_with = "crate::serialization::serialize_fact_value_map")]
36    #[serde(deserialize_with = "crate::serialization::deserialize_fact_value_map")]
37    pub fact_values: HashMap<FactPath, LiteralValue>,
38
39    /// Document reference facts (path -> referenced document name).
40    #[serde(serialize_with = "crate::serialization::serialize_fact_doc_ref_map")]
41    #[serde(deserialize_with = "crate::serialization::deserialize_fact_doc_ref_map")]
42    pub doc_refs: HashMap<FactPath, String>,
43
44    /// Fact-level source information for better errors in adapters/validation.
45    #[serde(serialize_with = "crate::serialization::serialize_fact_source_map")]
46    #[serde(deserialize_with = "crate::serialization::deserialize_fact_source_map")]
47    pub fact_sources: HashMap<FactPath, Source>,
48
49    /// Rules to execute in topological order (sorted by dependencies)
50    pub rules: Vec<ExecutableRule>,
51
52    /// Source code for error messages
53    pub sources: HashMap<String, String>,
54}
55
56/// An executable rule with flattened branches
57///
58/// Contains all information needed to evaluate a rule without document lookups.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ExecutableRule {
61    /// Unique identifier for this rule
62    pub path: RulePath,
63
64    /// Rule name
65    pub name: String,
66
67    /// Branches evaluated in order (last matching wins)
68    /// First branch has condition=None (default expression)
69    /// Subsequent branches have condition=Some(...) (unless clauses)
70    /// The evaluation is done in reverse order with the earliest matching branch returning (winning) the result.
71    pub branches: Vec<Branch>,
72
73    /// All facts this rule needs (direct + inherited from rule dependencies)
74    #[serde(serialize_with = "crate::serialization::serialize_fact_path_set")]
75    #[serde(deserialize_with = "crate::serialization::deserialize_fact_path_set")]
76    pub needs_facts: HashSet<FactPath>,
77
78    /// Source location for error messages
79    pub source: Option<Source>,
80
81    /// Computed type of this rule's result
82    /// Every rule MUST have a type (Lemma is strictly typed)
83    pub rule_type: LemmaType,
84}
85
86/// A branch in an executable rule
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct Branch {
89    /// Condition expression (None for default branch)
90    pub condition: Option<Expression>,
91
92    /// Result expression
93    pub result: Expression,
94
95    /// Source location for error messages
96    pub source: Option<Source>,
97}
98
99/// Builds an execution plan from a Graph.
100/// Internal implementation detail - only called by plan()
101pub(crate) fn build_execution_plan(graph: &Graph, main_doc_name: &str) -> ExecutionPlan {
102    let execution_order = graph.execution_order();
103    let mut fact_schema: HashMap<FactPath, LemmaType> = HashMap::new();
104    let mut fact_values: HashMap<FactPath, LiteralValue> = HashMap::new();
105    let mut doc_refs: HashMap<FactPath, String> = HashMap::new();
106    let mut fact_sources: HashMap<FactPath, Source> = HashMap::new();
107
108    // Collect facts and compute an authoritative type (schema) for every fact path.
109    for (path, fact) in graph.facts().iter() {
110        if let Some(src) = fact.source_location.clone() {
111            fact_sources.insert(path.clone(), src);
112        }
113        match &fact.value {
114            FactValue::Literal(lit) => {
115                fact_values.insert(path.clone(), lit.clone());
116
117                // Check if this literal fact overrides a type-annotated fact
118                // If so, we need to resolve the original type and store it in fact_schema
119                // This happens when you have: fact x = [money] and then fact one.x = 7
120                let fact_ref = FactReference {
121                    segments: path.segments.iter().map(|s| s.fact.clone()).collect(),
122                    fact: path.fact.clone(),
123                };
124
125                // Find the original fact definition in the source documents
126                // Use the document from the first segment if available
127                let context_doc = if let Some(first_segment) = path.segments.first() {
128                    first_segment.doc.as_str()
129                } else {
130                    // Top-level fact - search for it
131                    let fact_ref_segments: Vec<String> =
132                        path.segments.iter().map(|s| s.fact.clone()).collect();
133
134                    let mut found_doc = None;
135                    for (doc_name, doc) in graph.all_docs() {
136                        for orig_fact in &doc.facts {
137                            if orig_fact.reference.segments == fact_ref_segments
138                                && orig_fact.reference.fact == path.fact
139                            {
140                                found_doc = Some(doc_name.as_str());
141                                break;
142                            }
143                        }
144                        if found_doc.is_some() {
145                            break;
146                        }
147                    }
148                    found_doc.unwrap_or(main_doc_name)
149                };
150
151                // Look for the original fact in the source document
152                // For nested facts like one.x, the original fact is x (top-level in doc "one")
153                // So we search for a fact with empty segments and the same fact name
154                if let Some(orig_doc) = graph.all_docs().get(context_doc) {
155                    for orig_fact in &orig_doc.facts {
156                        // The original fact should be top-level (empty segments) with the same name
157                        // For one.x, we're looking for fact x in doc "one"
158                        if orig_fact.reference.segments.is_empty()
159                            && orig_fact.reference.fact == fact_ref.fact
160                        {
161                            // Found the original fact - check if it has a type declaration
162                            if let FactValue::TypeDeclaration { .. } = &orig_fact.value {
163                                // Resolve the type from the original fact
164                                let orig_source = orig_fact.source_location.as_ref().unwrap_or_else(|| {
165                                    unreachable!(
166                                        "BUG: fact '{}' missing source_location during type resolution",
167                                        orig_fact.reference.fact
168                                    )
169                                });
170                                match graph.resolve_type_declaration(
171                                    &orig_fact.value,
172                                    orig_source,
173                                    context_doc,
174                                ) {
175                                    Ok(lemma_type) => {
176                                        fact_schema.insert(path.clone(), lemma_type);
177                                    }
178                                    Err(e) => {
179                                        // Type resolution failed - this should have been caught during validation
180                                        // Panic to prevent silent failures
181                                        unreachable!(
182                                            "Failed to resolve type for fact {}: {}. This indicates a bug in validation - all types should be validated before execution plan building.",
183                                            path, e
184                                        );
185                                    }
186                                }
187                            }
188                            break;
189                        }
190                    }
191                }
192
193                // If this literal does not correspond to a typed fact declaration, its schema type
194                // is inferred from the literal value itself (standard types).
195                if !fact_schema.contains_key(path) {
196                    fact_schema.insert(path.clone(), lit.get_type().clone());
197                }
198            }
199            FactValue::TypeDeclaration { .. } => {
200                // Use TypeRegistry to determine document context and resolve type
201                let fact_ref = FactReference {
202                    segments: path.segments.iter().map(|s| s.fact.clone()).collect(),
203                    fact: path.fact.clone(),
204                };
205
206                // For inline type definitions, check if they exist in resolved_types
207                // Inline type definitions are already fully resolved during type resolution, so just use them directly
208                let mut found_inline_type = false;
209                for (_doc_name, document_types) in graph.resolved_types().iter() {
210                    if let Some(resolved_type) =
211                        document_types.inline_type_definitions.get(&fact_ref)
212                    {
213                        // Inline type definition already resolved - use it directly
214                        fact_schema.insert(path.clone(), resolved_type.clone());
215                        found_inline_type = true;
216                        break;
217                    }
218                }
219                if found_inline_type {
220                    continue; // Skip the rest of the loop iteration
221                }
222
223                // Find which document this fact belongs to
224                // Use the document from the first segment (set during graph building)
225                // This is more reliable than searching, especially for nested facts
226                let context_doc = if let Some(first_segment) = path.segments.first() {
227                    first_segment.doc.as_str()
228                } else {
229                    // Top-level fact - search for it
230                    let fact_ref_segments: Vec<String> =
231                        path.segments.iter().map(|s| s.fact.clone()).collect();
232
233                    let mut found_doc = None;
234                    for (doc_name, doc) in graph.all_docs() {
235                        for fact in &doc.facts {
236                            if fact.reference.segments == fact_ref_segments
237                                && fact.reference.fact == path.fact
238                            {
239                                found_doc = Some(doc_name.as_str());
240                                break;
241                            }
242                        }
243                        if found_doc.is_some() {
244                            break;
245                        }
246                    }
247
248                    found_doc.unwrap_or_else(|| {
249                        unreachable!(
250                            "Cannot determine document context for fact '{}'. This indicates a bug in graph building.",
251                            path
252                        );
253                    })
254                };
255
256                let fact_source = fact.source_location.as_ref().unwrap_or_else(|| {
257                    unreachable!(
258                        "BUG: fact '{}' missing source_location during type resolution",
259                        fact.reference.fact
260                    )
261                });
262                match graph.resolve_type_declaration(&fact.value, fact_source, context_doc) {
263                    Ok(lemma_type) => {
264                        fact_schema.insert(path.clone(), lemma_type);
265                    }
266                    Err(e) => {
267                        unreachable!(
268                            "Failed to resolve type for fact {}: {}. This indicates a bug in validation.",
269                            path, e
270                        );
271                    }
272                }
273            }
274            FactValue::DocumentReference(doc_name) => {
275                doc_refs.insert(path.clone(), doc_name.clone());
276            }
277        }
278    }
279
280    // Apply default values for facts with TypeDeclaration that don't have literal values
281    for (path, schema_type) in &fact_schema {
282        if fact_values.contains_key(path) {
283            continue; // Fact already has a value, skip
284        }
285        if let Some(default_value) = schema_type.create_default_value() {
286            fact_values.insert(path.clone(), default_value);
287        }
288    }
289
290    // Ensure literal facts are typed consistently with their declared schema type.
291    // If a fact path has a schema type, the stored literal MUST become that type,
292    // or we reject it as incompatible.
293    //
294    // Defensive check: fact_values should only contain LiteralValue entries.
295    // If a type definition somehow slipped through validation, this will catch it.
296    for (path, value) in fact_values.iter_mut() {
297        let Some(schema_type) = fact_schema.get(path).cloned() else {
298            continue;
299        };
300
301        match coerce_literal_to_schema_type(value, &schema_type) {
302            Ok(coerced) => {
303                *value = coerced;
304            }
305            Err(msg) => {
306                unreachable!(
307                    "Fact {} literal value is incompatible with declared type {}: {}. \
308                     This should have been caught during validation. If you see a type definition here, \
309                     it indicates a bug: type definitions cannot override typed facts.",
310                    path,
311                    schema_type.name(),
312                    msg
313                );
314            }
315        }
316    }
317
318    let mut executable_rules: Vec<ExecutableRule> = Vec::new();
319
320    for rule_path in execution_order {
321        let rule_node = graph.rules().get(rule_path).expect(
322            "bug: rule from topological sort not in graph - validation should have caught this",
323        );
324
325        let mut executable_branches = Vec::new();
326        for (condition, result) in &rule_node.branches {
327            executable_branches.push(Branch {
328                condition: condition.clone(),
329                result: result.clone(),
330                source: Some(rule_node.source.clone()),
331            });
332        }
333
334        executable_rules.push(ExecutableRule {
335            path: rule_path.clone(),
336            name: rule_path.rule.clone(),
337            branches: executable_branches,
338            source: Some(rule_node.source.clone()),
339            needs_facts: HashSet::new(),
340            rule_type: rule_node.rule_type.clone(),
341        });
342    }
343
344    populate_needs_facts(&mut executable_rules, graph);
345
346    ExecutionPlan {
347        doc_name: main_doc_name.to_string(),
348        fact_schema,
349        fact_values,
350        doc_refs,
351        fact_sources,
352        rules: executable_rules,
353        sources: graph.sources().clone(),
354    }
355}
356
357fn coerce_literal_to_schema_type(
358    lit: &LiteralValue,
359    schema_type: &LemmaType,
360) -> Result<LiteralValue, String> {
361    use crate::semantic::TypeSpecification;
362    use crate::Value;
363
364    // Fast path: same specification => just retag to carry constraints/options/etc.
365    if lit.lemma_type.specifications == schema_type.specifications {
366        let mut out = lit.clone();
367        out.lemma_type = schema_type.clone();
368        return Ok(out);
369    }
370
371    match (&schema_type.specifications, &lit.value) {
372        // Same value shape; retag.
373        (TypeSpecification::Number { .. }, Value::Number(_))
374        | (TypeSpecification::Text { .. }, Value::Text(_))
375        | (TypeSpecification::Boolean { .. }, Value::Boolean(_))
376        | (TypeSpecification::Date { .. }, Value::Date(_))
377        | (TypeSpecification::Time { .. }, Value::Time(_))
378        | (TypeSpecification::Duration { .. }, Value::Duration(_, _))
379        | (TypeSpecification::Ratio { .. }, Value::Ratio(_, _))
380        | (TypeSpecification::Scale { .. }, Value::Scale(_, _)) => {
381            let mut out = lit.clone();
382            out.lemma_type = schema_type.clone();
383            Ok(out)
384        }
385
386        // Allow a bare numeric literal to satisfy a Ratio type (unitless ratio).
387        (TypeSpecification::Ratio { .. }, Value::Number(n)) => {
388            Ok(LiteralValue::ratio_with_type(*n, None, schema_type.clone()))
389        }
390
391        _ => Err(format!(
392            "value {} cannot be used as type {}",
393            lit,
394            schema_type.name()
395        )),
396    }
397}
398
399fn populate_needs_facts(rules: &mut [ExecutableRule], graph: &Graph) {
400    // Compute direct fact references per rule.
401    let mut direct: HashMap<RulePath, HashSet<FactPath>> = HashMap::new();
402    for rule in rules.iter() {
403        let mut facts = HashSet::new();
404        for branch in &rule.branches {
405            if let Some(cond) = &branch.condition {
406                cond.collect_fact_paths(&mut facts);
407            }
408            branch.result.collect_fact_paths(&mut facts);
409        }
410        direct.insert(rule.path.clone(), facts);
411    }
412
413    // Compute transitive closure over rule dependencies (order-independent).
414    fn compute_all_facts(
415        rule_path: &RulePath,
416        graph: &Graph,
417        direct: &HashMap<RulePath, HashSet<FactPath>>,
418        memo: &mut HashMap<RulePath, HashSet<FactPath>>,
419        visiting: &mut HashSet<RulePath>,
420    ) -> HashSet<FactPath> {
421        if let Some(cached) = memo.get(rule_path) {
422            return cached.clone();
423        }
424
425        // Defensive: graph is expected to be acyclic after validation.
426        if !visiting.insert(rule_path.clone()) {
427            return direct.get(rule_path).cloned().unwrap_or_default();
428        }
429
430        let mut out = direct.get(rule_path).cloned().unwrap_or_default();
431        if let Some(node) = graph.rules().get(rule_path) {
432            for dep in &node.depends_on_rules {
433                // Only include dependencies that exist in the executable set.
434                if direct.contains_key(dep) {
435                    out.extend(compute_all_facts(dep, graph, direct, memo, visiting));
436                }
437            }
438        }
439
440        visiting.remove(rule_path);
441        memo.insert(rule_path.clone(), out.clone());
442        out
443    }
444
445    let mut memo: HashMap<RulePath, HashSet<FactPath>> = HashMap::new();
446    let mut visiting: HashSet<RulePath> = HashSet::new();
447
448    for rule in rules.iter_mut() {
449        rule.needs_facts = compute_all_facts(&rule.path, graph, &direct, &mut memo, &mut visiting);
450    }
451}
452
453impl ExecutionPlan {
454    /// Look up a fact by its path string (e.g., "age" or "rules.base_price").
455    pub fn get_fact_path_by_str(&self, name: &str) -> Option<&FactPath> {
456        self.fact_schema
457            .keys()
458            .find(|path| path.to_string() == name)
459    }
460
461    /// Look up a local rule by its name (rule in the main document).
462    pub fn get_rule(&self, name: &str) -> Option<&ExecutableRule> {
463        self.rules
464            .iter()
465            .find(|r| r.name == name && r.path.segments.is_empty())
466    }
467
468    /// Look up a rule by its full path.
469    pub fn get_rule_by_path(&self, rule_path: &RulePath) -> Option<&ExecutableRule> {
470        self.rules.iter().find(|r| &r.path == rule_path)
471    }
472
473    /// Get the literal value for a fact path, if it exists and has a literal value.
474    pub fn get_fact_value(&self, path: &FactPath) -> Option<&LiteralValue> {
475        self.fact_values.get(path)
476    }
477
478    /// Provide string values for facts.
479    ///
480    /// Parses each string to its expected type, validates constraints, and applies to the plan.
481    pub fn with_values(
482        mut self,
483        values: HashMap<String, String>,
484        limits: &ResourceLimits,
485    ) -> Result<Self, LemmaError> {
486        for (name, raw_value) in values {
487            let fact_path = self.get_fact_path_by_str(&name).ok_or_else(|| {
488                let available: Vec<String> =
489                    self.fact_schema.keys().map(|p| p.to_string()).collect();
490                LemmaError::engine(
491                    format!(
492                        "Fact '{}' not found. Available facts: {}",
493                        name,
494                        available.join(", ")
495                    ),
496                    crate::parsing::ast::Span {
497                        start: 0,
498                        end: 0,
499                        line: 1,
500                        col: 0,
501                    },
502                    "<input>",
503                    std::sync::Arc::from(""),
504                    &self.doc_name,
505                    1,
506                    None::<String>,
507                )
508            })?;
509            let fact_path = fact_path.clone();
510
511            let fact_source = self
512                .fact_sources
513                .get(&fact_path)
514                .cloned()
515                .ok_or_else(|| {
516                    LemmaError::engine(
517                        format!(
518                            "Invalid execution plan: missing source location for fact '{}'. \
519                             This plan is incomplete/corrupted (missing ExecutionPlan.fact_sources entry).",
520                            name
521                        ),
522                        crate::parsing::ast::Span {
523                            start: 0,
524                            end: 0,
525                            line: 1,
526                            col: 0,
527                        },
528                        "<execution-plan>",
529                        std::sync::Arc::from(""),
530                        &self.doc_name,
531                        1,
532                        None::<String>,
533                    )
534                })?;
535            let source_text: Arc<str> = self
536                .sources
537                .get(&fact_source.attribute)
538                .map(|s| Arc::from(s.as_str()))
539                .unwrap_or_else(|| Arc::from(""));
540
541            let expected_type = self.fact_schema.get(&fact_path).cloned().ok_or_else(|| {
542                LemmaError::engine(
543                    format!("Unknown fact: {}", name),
544                    crate::parsing::ast::Span {
545                        start: 0,
546                        end: 0,
547                        line: 1,
548                        col: 0,
549                    },
550                    "<input>",
551                    std::sync::Arc::from(""),
552                    &self.doc_name,
553                    1,
554                    None::<String>,
555                )
556            })?;
557
558            // Parse string to typed value
559            let literal_value = expected_type
560                .parse_value(
561                    &raw_value,
562                    fact_source.span.clone(),
563                    &fact_source.attribute,
564                    &fact_source.doc_name,
565                )
566                .map_err(|e| {
567                    LemmaError::engine(
568                        format!(
569                            "Failed to parse fact '{}' as {}: {}",
570                            name,
571                            expected_type.name(),
572                            e
573                        ),
574                        fact_source.span.clone(),
575                        &fact_source.attribute,
576                        source_text.clone(),
577                        &fact_source.doc_name,
578                        1,
579                        None::<String>,
580                    )
581                })?;
582
583            // Check resource limits
584            let size = literal_value.byte_size();
585            if size > limits.max_fact_value_bytes {
586                return Err(LemmaError::ResourceLimitExceeded {
587                    limit_name: "max_fact_value_bytes".to_string(),
588                    limit_value: limits.max_fact_value_bytes.to_string(),
589                    actual_value: size.to_string(),
590                    suggestion: format!(
591                        "Reduce the size of fact values to {} bytes or less",
592                        limits.max_fact_value_bytes
593                    ),
594                });
595            }
596
597            // Validate constraints
598            validate_value_against_type(&expected_type, &literal_value).map_err(|msg| {
599                LemmaError::engine(
600                    format!(
601                        "Invalid value for fact {} (expected {}): {}",
602                        name,
603                        expected_type.name(),
604                        msg
605                    ),
606                    fact_source.span.clone(),
607                    &fact_source.attribute,
608                    source_text.clone(),
609                    &fact_source.doc_name,
610                    1,
611                    None::<String>,
612                )
613            })?;
614
615            self.fact_values.insert(fact_path, literal_value);
616        }
617
618        Ok(self)
619    }
620}
621
622fn validate_value_against_type(
623    expected_type: &LemmaType,
624    value: &LiteralValue,
625) -> Result<(), String> {
626    use crate::semantic::TypeSpecification;
627    use crate::Value;
628
629    let effective_decimals = |n: rust_decimal::Decimal| n.scale();
630
631    match (&expected_type.specifications, &value.value) {
632        (
633            TypeSpecification::Number {
634                minimum,
635                maximum,
636                decimals,
637                ..
638            },
639            Value::Number(n),
640        ) => {
641            if let Some(min) = minimum {
642                if n < min {
643                    return Err(format!("{} is below minimum {}", n, min));
644                }
645            }
646            if let Some(max) = maximum {
647                if n > max {
648                    return Err(format!("{} is above maximum {}", n, max));
649                }
650            }
651            if let Some(d) = decimals {
652                if effective_decimals(*n) > u32::from(*d) {
653                    return Err(format!("{} has more than {} decimals", n, d));
654                }
655            }
656            Ok(())
657        }
658        (
659            TypeSpecification::Scale {
660                minimum,
661                maximum,
662                decimals,
663                ..
664            },
665            Value::Scale(n, _unit),
666        ) => {
667            if let Some(min) = minimum {
668                if n < min {
669                    return Err(format!("{} is below minimum {}", n, min));
670                }
671            }
672            if let Some(max) = maximum {
673                if n > max {
674                    return Err(format!("{} is above maximum {}", n, max));
675                }
676            }
677            if let Some(d) = decimals {
678                if effective_decimals(*n) > u32::from(*d) {
679                    return Err(format!("{} has more than {} decimals", n, d));
680                }
681            }
682            Ok(())
683        }
684        (TypeSpecification::Text { options, .. }, Value::Text(s)) => {
685            if !options.is_empty() && !options.iter().any(|opt| opt == s) {
686                return Err(format!(
687                    "'{}' is not in allowed options: {}",
688                    s,
689                    options.join(", ")
690                ));
691            }
692            Ok(())
693        }
694        // If we get here, type mismatch should already have been rejected by the caller.
695        _ => Ok(()),
696    }
697}
698
699pub(crate) fn validate_literal_facts_against_types(plan: &ExecutionPlan) -> Vec<LemmaError> {
700    let mut errors = Vec::new();
701
702    for (fact_path, lit) in &plan.fact_values {
703        let Some(expected_type) = plan.fact_schema.get(fact_path) else {
704            continue;
705        };
706
707        if let Err(msg) = validate_value_against_type(expected_type, lit) {
708            let (span, attribute, source_text, doc_name) = plan
709                .fact_sources
710                .get(fact_path)
711                .map(|s| {
712                    let source_text: Arc<str> = plan
713                        .sources
714                        .get(&s.attribute)
715                        .map(|t| Arc::from(t.as_str()))
716                        .unwrap_or_else(|| Arc::from(""));
717                    (
718                        s.span.clone(),
719                        s.attribute.as_str(),
720                        source_text,
721                        s.doc_name.as_str(),
722                    )
723                })
724                .unwrap_or_else(|| {
725                    (
726                        crate::parsing::ast::Span {
727                            start: 0,
728                            end: 0,
729                            line: 1,
730                            col: 0,
731                        },
732                        "<input>",
733                        Arc::from(""),
734                        plan.doc_name.as_str(),
735                    )
736                });
737            errors.push(LemmaError::engine(
738                format!(
739                    "Invalid value for fact {} (expected {}): {}",
740                    fact_path,
741                    expected_type.name(),
742                    msg
743                ),
744                span,
745                attribute,
746                source_text,
747                doc_name,
748                1,
749                None::<String>,
750            ));
751        }
752    }
753
754    errors
755}
756
757#[cfg(test)]
758mod tests {
759    use super::*;
760    use crate::semantic::{BooleanValue, Expression, FactPath, LiteralValue, RulePath, Value};
761    use crate::Engine;
762    use serde_json;
763    use std::str::FromStr;
764    use std::sync::Arc;
765
766    fn default_limits() -> ResourceLimits {
767        ResourceLimits::default()
768    }
769
770    #[test]
771    fn test_with_raw_values() {
772        let mut engine = Engine::new();
773        engine
774            .add_lemma_code(
775                r#"
776                doc test
777                fact age = [number -> default 25]
778                "#,
779                "test.lemma",
780            )
781            .unwrap();
782
783        let plan = engine.get_execution_plan("test").unwrap().clone();
784        let fact_path = FactPath::local("age".to_string());
785
786        let mut values = HashMap::new();
787        values.insert("age".to_string(), "30".to_string());
788
789        let updated_plan = plan.with_values(values, &default_limits()).unwrap();
790        let updated_value = updated_plan.fact_values.get(&fact_path).unwrap();
791        match &updated_value.value {
792            Value::Number(n) => assert_eq!(n, &rust_decimal::Decimal::from(30)),
793            other => panic!("Expected number literal, got {:?}", other),
794        }
795    }
796
797    #[test]
798    fn test_with_raw_values_type_mismatch() {
799        let mut engine = Engine::new();
800        engine
801            .add_lemma_code(
802                r#"
803                doc test
804                fact age = [number]
805                "#,
806                "test.lemma",
807            )
808            .unwrap();
809
810        let plan = engine.get_execution_plan("test").unwrap().clone();
811
812        let mut values = HashMap::new();
813        values.insert("age".to_string(), "thirty".to_string());
814
815        assert!(plan.with_values(values, &default_limits()).is_err());
816    }
817
818    #[test]
819    fn test_with_raw_values_unknown_fact() {
820        let mut engine = Engine::new();
821        engine
822            .add_lemma_code(
823                r#"
824                doc test
825                fact known = [number]
826                "#,
827                "test.lemma",
828            )
829            .unwrap();
830
831        let plan = engine.get_execution_plan("test").unwrap().clone();
832
833        let mut values = HashMap::new();
834        values.insert("unknown".to_string(), "30".to_string());
835
836        assert!(plan.with_values(values, &default_limits()).is_err());
837    }
838
839    #[test]
840    fn test_with_raw_values_nested() {
841        let mut engine = Engine::new();
842        engine
843            .add_lemma_code(
844                r#"
845                doc private
846                fact base_price = [number]
847
848                doc test
849                fact rules = doc private
850                "#,
851                "test.lemma",
852            )
853            .unwrap();
854
855        let plan = engine.get_execution_plan("test").unwrap().clone();
856
857        let mut values = HashMap::new();
858        values.insert("rules.base_price".to_string(), "100".to_string());
859
860        let updated_plan = plan.with_values(values, &default_limits()).unwrap();
861        let fact_path = FactPath {
862            segments: vec![crate::semantic::PathSegment {
863                fact: "rules".to_string(),
864                doc: "private".to_string(),
865            }],
866            fact: "base_price".to_string(),
867        };
868        let updated_value = updated_plan.fact_values.get(&fact_path).unwrap();
869        match &updated_value.value {
870            Value::Number(n) => assert_eq!(n, &rust_decimal::Decimal::from(100)),
871            other => panic!("Expected number literal, got {:?}", other),
872        }
873    }
874
875    fn create_literal_expr(value: LiteralValue) -> Expression {
876        use crate::semantic::ExpressionKind;
877        Expression::new(ExpressionKind::Literal(value), None)
878    }
879
880    fn create_number_literal(n: rust_decimal::Decimal) -> LiteralValue {
881        LiteralValue::number(n)
882    }
883
884    fn create_boolean_literal(b: BooleanValue) -> LiteralValue {
885        LiteralValue::boolean(b)
886    }
887
888    fn create_text_literal(s: String) -> LiteralValue {
889        LiteralValue::text(s)
890    }
891
892    #[test]
893    fn with_values_should_enforce_number_maximum_constraint() {
894        // Higher-standard requirement: user input must be validated against type constraints.
895        // If this test fails, Lemma accepts invalid values and gives false reassurance.
896        let fact_path = FactPath::local("x".to_string());
897
898        let mut fact_schema = HashMap::new();
899        let max10 = crate::LemmaType::without_name(crate::TypeSpecification::Number {
900            minimum: None,
901            maximum: Some(rust_decimal::Decimal::from_str("10").unwrap()),
902            decimals: None,
903            precision: None,
904            help: None,
905            default: None,
906        });
907        fact_schema.insert(fact_path.clone(), max10.clone());
908        let fact_sources = HashMap::from([(
909            fact_path.clone(),
910            Source::new(
911                "<test>",
912                crate::parsing::ast::Span {
913                    start: 0,
914                    end: 0,
915                    line: 1,
916                    col: 0,
917                },
918                "test",
919            ),
920        )]);
921
922        let plan = ExecutionPlan {
923            doc_name: "test".to_string(),
924            fact_schema,
925            fact_values: HashMap::new(),
926            doc_refs: HashMap::new(),
927            fact_sources,
928            rules: Vec::new(),
929            sources: HashMap::from([("<test>".to_string(), "".to_string())]),
930        };
931
932        let mut values = HashMap::new();
933        values.insert("x".to_string(), "11".to_string());
934
935        assert!(
936            plan.with_values(values, &default_limits()).is_err(),
937            "Providing x=11 should fail due to maximum 10"
938        );
939    }
940
941    #[test]
942    fn with_values_should_enforce_text_enum_options() {
943        // Higher-standard requirement: enum options must be enforced for text types.
944        let fact_path = FactPath::local("tier".to_string());
945
946        let mut fact_schema = HashMap::new();
947        let tier = crate::LemmaType::without_name(crate::TypeSpecification::Text {
948            minimum: None,
949            maximum: None,
950            length: None,
951            options: vec!["silver".to_string(), "gold".to_string()],
952            help: None,
953            default: None,
954        });
955        fact_schema.insert(fact_path.clone(), tier.clone());
956        let fact_sources = HashMap::from([(
957            fact_path.clone(),
958            Source::new(
959                "<test>",
960                crate::parsing::ast::Span {
961                    start: 0,
962                    end: 0,
963                    line: 1,
964                    col: 0,
965                },
966                "test",
967            ),
968        )]);
969
970        let plan = ExecutionPlan {
971            doc_name: "test".to_string(),
972            fact_schema,
973            fact_values: HashMap::new(),
974            doc_refs: HashMap::new(),
975            fact_sources,
976            rules: Vec::new(),
977            sources: HashMap::from([("<test>".to_string(), "".to_string())]),
978        };
979
980        let mut values = HashMap::new();
981        values.insert("tier".to_string(), "platinum".to_string());
982
983        assert!(
984            plan.with_values(values, &default_limits()).is_err(),
985            "Invalid enum value should be rejected (tier='platinum')"
986        );
987    }
988
989    #[test]
990    fn with_values_should_enforce_scale_decimals() {
991        // Higher-standard requirement: decimals should be enforced on scale inputs,
992        // unless the language explicitly defines rounding semantics.
993        let fact_path = FactPath::local("price".to_string());
994
995        let mut fact_schema = HashMap::new();
996        let money = crate::LemmaType::without_name(crate::TypeSpecification::Scale {
997            minimum: None,
998            maximum: None,
999            decimals: Some(2),
1000            precision: None,
1001            units: vec![crate::semantic::Unit {
1002                name: "eur".to_string(),
1003                value: rust_decimal::Decimal::from_str("1.0").unwrap(),
1004            }],
1005            help: None,
1006            default: None,
1007        });
1008        fact_schema.insert(fact_path.clone(), money.clone());
1009        let fact_sources = HashMap::from([(
1010            fact_path.clone(),
1011            Source::new(
1012                "<test>",
1013                crate::parsing::ast::Span {
1014                    start: 0,
1015                    end: 0,
1016                    line: 1,
1017                    col: 0,
1018                },
1019                "test",
1020            ),
1021        )]);
1022
1023        let plan = ExecutionPlan {
1024            doc_name: "test".to_string(),
1025            fact_schema,
1026            fact_values: HashMap::new(),
1027            doc_refs: HashMap::new(),
1028            fact_sources,
1029            rules: Vec::new(),
1030            sources: HashMap::from([("<test>".to_string(), "".to_string())]),
1031        };
1032
1033        let mut values = HashMap::new();
1034        values.insert("price".to_string(), "1.234 eur".to_string());
1035
1036        assert!(
1037            plan.with_values(values, &default_limits()).is_err(),
1038            "Scale decimals=2 should reject 1.234 eur"
1039        );
1040    }
1041
1042    #[test]
1043    fn test_serialize_deserialize_execution_plan() {
1044        let fact_path = FactPath {
1045            segments: vec![],
1046            fact: "age".to_string(),
1047        };
1048        let plan = ExecutionPlan {
1049            doc_name: "test".to_string(),
1050            fact_schema: {
1051                let mut s = HashMap::new();
1052                s.insert(
1053                    fact_path.clone(),
1054                    crate::semantic::standard_number().clone(),
1055                );
1056                s
1057            },
1058            fact_values: HashMap::new(),
1059            doc_refs: HashMap::new(),
1060            fact_sources: HashMap::new(),
1061            rules: Vec::new(),
1062            sources: {
1063                let mut s = HashMap::new();
1064                s.insert("test.lemma".to_string(), "fact age: number".to_string());
1065                s
1066            },
1067        };
1068
1069        let json = serde_json::to_string(&plan).expect("Should serialize");
1070        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1071
1072        assert_eq!(deserialized.doc_name, plan.doc_name);
1073        assert_eq!(deserialized.fact_schema.len(), plan.fact_schema.len());
1074        assert_eq!(deserialized.fact_values.len(), plan.fact_values.len());
1075        assert_eq!(deserialized.doc_refs.len(), plan.doc_refs.len());
1076        assert_eq!(deserialized.fact_sources.len(), plan.fact_sources.len());
1077        assert_eq!(deserialized.rules.len(), plan.rules.len());
1078        assert_eq!(deserialized.sources.len(), plan.sources.len());
1079    }
1080
1081    #[test]
1082    fn test_serialize_deserialize_plan_with_rules() {
1083        use crate::semantic::ExpressionKind;
1084
1085        let mut plan = ExecutionPlan {
1086            doc_name: "test".to_string(),
1087            fact_schema: HashMap::new(),
1088            fact_values: HashMap::new(),
1089            doc_refs: HashMap::new(),
1090            fact_sources: HashMap::new(),
1091            rules: Vec::new(),
1092            sources: HashMap::new(),
1093        };
1094
1095        let age_path = FactPath::local("age".to_string());
1096        plan.fact_schema
1097            .insert(age_path.clone(), crate::semantic::standard_number().clone());
1098
1099        let rule = ExecutableRule {
1100            path: RulePath::local("can_drive".to_string()),
1101            name: "can_drive".to_string(),
1102            branches: vec![Branch {
1103                condition: Some(Expression::new(
1104                    ExpressionKind::Comparison(
1105                        Arc::new(Expression::new(
1106                            ExpressionKind::FactPath(age_path.clone()),
1107                            None,
1108                        )),
1109                        crate::ComparisonComputation::GreaterThanOrEqual,
1110                        Arc::new(create_literal_expr(create_number_literal(18.into()))),
1111                    ),
1112                    None,
1113                )),
1114                result: create_literal_expr(create_boolean_literal(crate::BooleanValue::True)),
1115                source: None,
1116            }],
1117            needs_facts: {
1118                let mut set = HashSet::new();
1119                set.insert(age_path);
1120                set
1121            },
1122            source: None,
1123            rule_type: crate::semantic::standard_boolean().clone(),
1124        };
1125
1126        plan.rules.push(rule);
1127
1128        let json = serde_json::to_string(&plan).expect("Should serialize");
1129        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1130
1131        assert_eq!(deserialized.doc_name, plan.doc_name);
1132        assert_eq!(deserialized.fact_schema.len(), plan.fact_schema.len());
1133        assert_eq!(deserialized.rules.len(), plan.rules.len());
1134        assert_eq!(deserialized.rules[0].name, "can_drive");
1135        assert_eq!(deserialized.rules[0].branches.len(), 1);
1136        assert_eq!(deserialized.rules[0].needs_facts.len(), 1);
1137    }
1138
1139    #[test]
1140    fn test_serialize_deserialize_plan_with_nested_fact_paths() {
1141        use crate::semantic::PathSegment;
1142        let fact_path = FactPath {
1143            segments: vec![PathSegment {
1144                fact: "employee".to_string(),
1145                doc: "private".to_string(),
1146            }],
1147            fact: "salary".to_string(),
1148        };
1149
1150        let plan = ExecutionPlan {
1151            doc_name: "test".to_string(),
1152            fact_schema: {
1153                let mut s = HashMap::new();
1154                s.insert(
1155                    fact_path.clone(),
1156                    crate::semantic::standard_number().clone(),
1157                );
1158                s
1159            },
1160            fact_values: HashMap::new(),
1161            doc_refs: HashMap::new(),
1162            fact_sources: HashMap::new(),
1163            rules: Vec::new(),
1164            sources: HashMap::new(),
1165        };
1166
1167        let json = serde_json::to_string(&plan).expect("Should serialize");
1168        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1169
1170        assert_eq!(deserialized.fact_schema.len(), 1);
1171        let (deserialized_path, _) = deserialized.fact_schema.iter().next().unwrap();
1172        assert_eq!(deserialized_path.segments.len(), 1);
1173        assert_eq!(deserialized_path.segments[0].fact, "employee");
1174        assert_eq!(deserialized_path.fact, "salary");
1175    }
1176
1177    #[test]
1178    fn test_serialize_deserialize_plan_with_multiple_fact_types() {
1179        let name_path = FactPath::local("name".to_string());
1180        let age_path = FactPath::local("age".to_string());
1181        let active_path = FactPath::local("active".to_string());
1182
1183        let mut fact_schema = HashMap::new();
1184        fact_schema.insert(name_path.clone(), crate::semantic::standard_text().clone());
1185        fact_schema.insert(age_path.clone(), crate::semantic::standard_number().clone());
1186        fact_schema.insert(
1187            active_path.clone(),
1188            crate::semantic::standard_boolean().clone(),
1189        );
1190
1191        let mut fact_values = HashMap::new();
1192        fact_values.insert(name_path.clone(), create_text_literal("Alice".to_string()));
1193        fact_values.insert(age_path.clone(), create_number_literal(30.into()));
1194        fact_values.insert(
1195            active_path.clone(),
1196            create_boolean_literal(crate::BooleanValue::True),
1197        );
1198
1199        let plan = ExecutionPlan {
1200            doc_name: "test".to_string(),
1201            fact_schema,
1202            fact_values,
1203            doc_refs: HashMap::new(),
1204            fact_sources: HashMap::new(),
1205            rules: Vec::new(),
1206            sources: HashMap::new(),
1207        };
1208
1209        let json = serde_json::to_string(&plan).expect("Should serialize");
1210        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1211
1212        assert_eq!(deserialized.fact_values.len(), 3);
1213
1214        assert_eq!(
1215            deserialized.fact_values.get(&name_path).unwrap().value,
1216            Value::Text("Alice".to_string())
1217        );
1218        assert_eq!(
1219            deserialized.fact_values.get(&age_path).unwrap().value,
1220            Value::Number(30.into())
1221        );
1222        assert_eq!(
1223            deserialized.fact_values.get(&active_path).unwrap().value,
1224            Value::Boolean(crate::BooleanValue::True)
1225        );
1226    }
1227
1228    #[test]
1229    fn test_serialize_deserialize_plan_with_multiple_branches() {
1230        use crate::semantic::ExpressionKind;
1231
1232        let mut plan = ExecutionPlan {
1233            doc_name: "test".to_string(),
1234            fact_schema: HashMap::new(),
1235            fact_values: HashMap::new(),
1236            doc_refs: HashMap::new(),
1237            fact_sources: HashMap::new(),
1238            rules: Vec::new(),
1239            sources: HashMap::new(),
1240        };
1241
1242        let points_path = FactPath::local("points".to_string());
1243        plan.fact_schema.insert(
1244            points_path.clone(),
1245            crate::semantic::standard_number().clone(),
1246        );
1247
1248        let rule = ExecutableRule {
1249            path: RulePath::local("tier".to_string()),
1250            name: "tier".to_string(),
1251            branches: vec![
1252                Branch {
1253                    condition: None,
1254                    result: create_literal_expr(create_text_literal("bronze".to_string())),
1255                    source: None,
1256                },
1257                Branch {
1258                    condition: Some(Expression::new(
1259                        ExpressionKind::Comparison(
1260                            Arc::new(Expression::new(
1261                                ExpressionKind::FactPath(points_path.clone()),
1262                                None,
1263                            )),
1264                            crate::ComparisonComputation::GreaterThanOrEqual,
1265                            Arc::new(create_literal_expr(create_number_literal(100.into()))),
1266                        ),
1267                        None,
1268                    )),
1269                    result: create_literal_expr(create_text_literal("silver".to_string())),
1270                    source: None,
1271                },
1272                Branch {
1273                    condition: Some(Expression::new(
1274                        ExpressionKind::Comparison(
1275                            Arc::new(Expression::new(
1276                                ExpressionKind::FactPath(points_path.clone()),
1277                                None,
1278                            )),
1279                            crate::ComparisonComputation::GreaterThanOrEqual,
1280                            Arc::new(create_literal_expr(create_number_literal(500.into()))),
1281                        ),
1282                        None,
1283                    )),
1284                    result: create_literal_expr(create_text_literal("gold".to_string())),
1285                    source: None,
1286                },
1287            ],
1288            needs_facts: {
1289                let mut set = HashSet::new();
1290                set.insert(points_path);
1291                set
1292            },
1293            source: None,
1294            rule_type: crate::semantic::standard_text().clone(),
1295        };
1296
1297        plan.rules.push(rule);
1298
1299        let json = serde_json::to_string(&plan).expect("Should serialize");
1300        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1301
1302        assert_eq!(deserialized.rules.len(), 1);
1303        assert_eq!(deserialized.rules[0].branches.len(), 3);
1304        assert!(deserialized.rules[0].branches[0].condition.is_none());
1305        assert!(deserialized.rules[0].branches[1].condition.is_some());
1306        assert!(deserialized.rules[0].branches[2].condition.is_some());
1307    }
1308
1309    #[test]
1310    fn test_serialize_deserialize_empty_plan() {
1311        let plan = ExecutionPlan {
1312            doc_name: "empty".to_string(),
1313            fact_schema: HashMap::new(),
1314            fact_values: HashMap::new(),
1315            doc_refs: HashMap::new(),
1316            fact_sources: HashMap::new(),
1317            rules: Vec::new(),
1318            sources: HashMap::new(),
1319        };
1320
1321        let json = serde_json::to_string(&plan).expect("Should serialize");
1322        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1323
1324        assert_eq!(deserialized.doc_name, "empty");
1325        assert_eq!(deserialized.fact_schema.len(), 0);
1326        assert_eq!(deserialized.fact_values.len(), 0);
1327        assert_eq!(deserialized.rules.len(), 0);
1328        assert_eq!(deserialized.sources.len(), 0);
1329    }
1330
1331    #[test]
1332    fn test_serialize_deserialize_plan_with_arithmetic_expressions() {
1333        use crate::semantic::ExpressionKind;
1334
1335        let mut plan = ExecutionPlan {
1336            doc_name: "test".to_string(),
1337            fact_schema: HashMap::new(),
1338            fact_values: HashMap::new(),
1339            doc_refs: HashMap::new(),
1340            fact_sources: HashMap::new(),
1341            rules: Vec::new(),
1342            sources: HashMap::new(),
1343        };
1344
1345        let x_path = FactPath::local("x".to_string());
1346        plan.fact_schema
1347            .insert(x_path.clone(), crate::semantic::standard_number().clone());
1348
1349        let rule = ExecutableRule {
1350            path: RulePath::local("doubled".to_string()),
1351            name: "doubled".to_string(),
1352            branches: vec![Branch {
1353                condition: None,
1354                result: Expression::new(
1355                    ExpressionKind::Arithmetic(
1356                        Arc::new(Expression::new(
1357                            ExpressionKind::FactPath(x_path.clone()),
1358                            None,
1359                        )),
1360                        crate::ArithmeticComputation::Multiply,
1361                        Arc::new(create_literal_expr(create_number_literal(2.into()))),
1362                    ),
1363                    None,
1364                ),
1365                source: None,
1366            }],
1367            needs_facts: {
1368                let mut set = HashSet::new();
1369                set.insert(x_path);
1370                set
1371            },
1372            source: None,
1373            rule_type: crate::semantic::standard_number().clone(),
1374        };
1375
1376        plan.rules.push(rule);
1377
1378        let json = serde_json::to_string(&plan).expect("Should serialize");
1379        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1380
1381        assert_eq!(deserialized.rules.len(), 1);
1382        match &deserialized.rules[0].branches[0].result.kind {
1383            ExpressionKind::Arithmetic(left, op, right) => {
1384                assert_eq!(*op, crate::ArithmeticComputation::Multiply);
1385                match &left.kind {
1386                    ExpressionKind::FactPath(_) => {}
1387                    _ => panic!("Expected FactPath in left operand"),
1388                }
1389                match &right.kind {
1390                    ExpressionKind::Literal(_) => {}
1391                    _ => panic!("Expected Literal in right operand"),
1392                }
1393            }
1394            _ => panic!("Expected Arithmetic expression"),
1395        }
1396    }
1397
1398    #[test]
1399    fn test_serialize_deserialize_round_trip_equality() {
1400        use crate::semantic::ExpressionKind;
1401
1402        let mut plan = ExecutionPlan {
1403            doc_name: "test".to_string(),
1404            fact_schema: HashMap::new(),
1405            fact_values: HashMap::new(),
1406            doc_refs: HashMap::new(),
1407            fact_sources: HashMap::new(),
1408            rules: Vec::new(),
1409            sources: {
1410                let mut s = HashMap::new();
1411                s.insert("test.lemma".to_string(), "fact age: number".to_string());
1412                s
1413            },
1414        };
1415
1416        let age_path = FactPath::local("age".to_string());
1417        plan.fact_schema
1418            .insert(age_path.clone(), crate::semantic::standard_number().clone());
1419
1420        let rule = ExecutableRule {
1421            path: RulePath::local("is_adult".to_string()),
1422            name: "is_adult".to_string(),
1423            branches: vec![Branch {
1424                condition: Some(Expression::new(
1425                    ExpressionKind::Comparison(
1426                        Arc::new(Expression::new(
1427                            ExpressionKind::FactPath(age_path.clone()),
1428                            None,
1429                        )),
1430                        crate::ComparisonComputation::GreaterThanOrEqual,
1431                        Arc::new(create_literal_expr(create_number_literal(18.into()))),
1432                    ),
1433                    None,
1434                )),
1435                result: create_literal_expr(create_boolean_literal(crate::BooleanValue::True)),
1436                source: None,
1437            }],
1438            needs_facts: {
1439                let mut set = HashSet::new();
1440                set.insert(age_path);
1441                set
1442            },
1443            source: None,
1444            rule_type: crate::semantic::standard_boolean().clone(),
1445        };
1446
1447        plan.rules.push(rule);
1448
1449        let json = serde_json::to_string(&plan).expect("Should serialize");
1450        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1451
1452        let json2 = serde_json::to_string(&deserialized).expect("Should serialize again");
1453        let deserialized2: ExecutionPlan =
1454            serde_json::from_str(&json2).expect("Should deserialize again");
1455
1456        assert_eq!(deserialized2.doc_name, plan.doc_name);
1457        assert_eq!(deserialized2.fact_schema.len(), plan.fact_schema.len());
1458        assert_eq!(deserialized2.rules.len(), plan.rules.len());
1459        assert_eq!(deserialized2.sources.len(), plan.sources.len());
1460        assert_eq!(deserialized2.rules[0].name, plan.rules[0].name);
1461        assert_eq!(
1462            deserialized2.rules[0].branches.len(),
1463            plan.rules[0].branches.len()
1464        );
1465    }
1466}