Skip to main content

lemma/planning/
execution_plan.rs

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