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