Skip to main content

lemma/planning/
execution_plan.rs

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