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