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