Skip to main content

lemma/planning/
execution_plan.rs

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