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 data, rules flattened into executable branches,
5//! and execution order - no spec structure needed during evaluation.
6//!
7//! Reliability model:
8//! - `SpecSchema` is the IO contract surface for consumers (data and rule outputs).
9//!   IO compatibility is the consumer-facing guarantee.
10
11use crate::parsing::ast::{EffectiveDate, LemmaSpec, MetaValue};
12use crate::planning::graph::Graph;
13use crate::planning::graph::ResolvedSpecTypes;
14use crate::planning::semantics;
15use crate::planning::semantics::{
16    DataDefinition, DataPath, Expression, LemmaType, LiteralValue, RulePath, TypeSpecification,
17    ValueKind,
18};
19use crate::Error;
20use crate::ResourceLimits;
21use crate::Source;
22use indexmap::IndexMap;
23use serde::{Deserialize, Serialize};
24use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
25use std::sync::Arc;
26
27/// Spec sources keyed by (name, effective_from).
28pub type SpecSources = IndexMap<(String, EffectiveDate), String>;
29
30/// A complete execution plan ready for the evaluator
31///
32/// Contains the topologically sorted list of rules to execute, along with all data.
33/// Self-contained structure - no spec lookups required during evaluation.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ExecutionPlan {
36    /// Main spec name
37    pub spec_name: String,
38
39    /// Per-data data in definition order: value, type-only, or spec reference.
40    #[serde(serialize_with = "crate::serialization::serialize_resolved_data_value_map")]
41    #[serde(deserialize_with = "crate::serialization::deserialize_resolved_data_value_map")]
42    pub data: IndexMap<DataPath, DataDefinition>,
43
44    /// Rules to execute in topological order (sorted by dependencies)
45    pub rules: Vec<ExecutableRule>,
46
47    /// Order in which [`DataDefinition::Reference`] entries must be resolved
48    /// at evaluation time so that chained references (reference → reference →
49    /// data) copy values in the correct sequence. Empty when the plan has no
50    /// references.
51    #[serde(default, alias = "alias_evaluation_order")]
52    pub reference_evaluation_order: Vec<DataPath>,
53
54    /// Spec metadata
55    pub meta: HashMap<String, MetaValue>,
56
57    /// Named types defined in or imported by this spec, in deterministic order.
58    pub named_types: BTreeMap<String, LemmaType>,
59
60    pub effective: EffectiveDate,
61
62    /// Canonical source for all specs in this plan, keyed by (name, effective_from).
63    /// Reconstructed from AST — not raw file content.
64    #[serde(default)]
65    #[serde(
66        serialize_with = "serialize_sources",
67        deserialize_with = "deserialize_sources"
68    )]
69    pub sources: SpecSources,
70}
71
72/// All [`ExecutionPlan`]s for a spec name after dependency resolution.
73/// Ordered by [`ExecutionPlan::effective`]. Slice end is derived from the next plan's `effective`.
74#[derive(Debug, Clone)]
75pub struct ExecutionPlanSet {
76    pub spec_name: String,
77    pub plans: Vec<ExecutionPlan>,
78}
79
80impl ExecutionPlanSet {
81    /// Plan covering `[effective[i], effective[i+1])` (half-open).
82    #[must_use]
83    pub fn plan_at(&self, effective: &EffectiveDate) -> Option<&ExecutionPlan> {
84        for (i, plan) in self.plans.iter().enumerate() {
85            let from_ok = *effective >= plan.effective;
86            let to_ok = self
87                .plans
88                .get(i + 1)
89                .map(|next| *effective < next.effective)
90                .unwrap_or(true);
91            if from_ok && to_ok {
92                return Some(plan);
93            }
94        }
95        None
96    }
97}
98
99fn serialize_sources<S>(sources: &SpecSources, serializer: S) -> Result<S::Ok, S::Error>
100where
101    S: serde::Serializer,
102{
103    use serde::ser::SerializeSeq;
104    let mut seq = serializer.serialize_seq(Some(sources.len()))?;
105    for ((name, effective_from), source) in sources {
106        seq.serialize_element(&SpecSourceEntry {
107            name,
108            effective_from,
109            source,
110        })?;
111    }
112    seq.end()
113}
114
115fn deserialize_sources<'de, D>(deserializer: D) -> Result<SpecSources, D::Error>
116where
117    D: serde::Deserializer<'de>,
118{
119    let entries: Vec<SpecSourceEntryOwned> = Vec::deserialize(deserializer)?;
120    let mut map = IndexMap::with_capacity(entries.len());
121    for e in entries {
122        map.insert((e.name, e.effective_from), e.source);
123    }
124    Ok(map)
125}
126
127#[derive(Serialize)]
128struct SpecSourceEntry<'a> {
129    name: &'a str,
130    effective_from: &'a EffectiveDate,
131    source: &'a str,
132}
133
134#[derive(Deserialize)]
135struct SpecSourceEntryOwned {
136    name: String,
137    effective_from: EffectiveDate,
138    source: String,
139}
140
141/// An executable rule with flattened branches
142///
143/// Contains all information needed to evaluate a rule without spec lookups.
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct ExecutableRule {
146    /// Unique identifier for this rule
147    pub path: RulePath,
148
149    /// Rule name
150    pub name: String,
151
152    /// Branches evaluated in order (last matching wins)
153    /// First branch has condition=None (default expression)
154    /// Subsequent branches have condition=Some(...) (unless clauses)
155    /// The evaluation is done in reverse order with the earliest matching branch returning (winning) the result.
156    pub branches: Vec<Branch>,
157
158    /// All data this rule needs (direct + inherited from rule dependencies)
159    pub needs_data: BTreeSet<DataPath>,
160
161    /// Source location for error messages (always present for rules from parsed specs)
162    pub source: Source,
163
164    /// Computed type of this rule's result
165    /// Every rule MUST have a type (Lemma is strictly typed)
166    pub rule_type: LemmaType,
167}
168
169/// A branch in an executable rule
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct Branch {
172    /// Condition expression (None for default branch)
173    pub condition: Option<Expression>,
174
175    /// Result expression
176    pub result: Expression,
177
178    /// Source location for error messages (always present for branches from parsed specs)
179    pub source: Source,
180}
181
182/// Builds an execution plan from a Graph for one temporal slice.
183/// Internal implementation detail - only called by plan()
184pub(crate) fn build_execution_plan(
185    graph: &Graph,
186    resolved_types: &HashMap<Arc<LemmaSpec>, ResolvedSpecTypes>,
187    effective: &EffectiveDate,
188) -> ExecutionPlan {
189    let data = graph.build_data();
190    let execution_order = graph.execution_order();
191
192    let mut executable_rules: Vec<ExecutableRule> = Vec::new();
193    let mut path_to_index: HashMap<RulePath, usize> = HashMap::new();
194
195    for rule_path in execution_order {
196        let rule_node = graph.rules().get(rule_path).expect(
197            "bug: rule from topological sort not in graph - validation should have caught this",
198        );
199
200        let mut direct_data = HashSet::new();
201        for (condition, result) in &rule_node.branches {
202            if let Some(cond) = condition {
203                cond.collect_data_paths(&mut direct_data);
204            }
205            result.collect_data_paths(&mut direct_data);
206        }
207        let mut needs_data: BTreeSet<DataPath> = direct_data.into_iter().collect();
208
209        for dep in &rule_node.depends_on_rules {
210            if let Some(&dep_idx) = path_to_index.get(dep) {
211                needs_data.extend(executable_rules[dep_idx].needs_data.iter().cloned());
212            }
213        }
214
215        let mut executable_branches = Vec::new();
216        for (condition, result) in &rule_node.branches {
217            executable_branches.push(Branch {
218                condition: condition.clone(),
219                result: result.clone(),
220                source: rule_node.source.clone(),
221            });
222        }
223
224        path_to_index.insert(rule_path.clone(), executable_rules.len());
225        executable_rules.push(ExecutableRule {
226            path: rule_path.clone(),
227            name: rule_path.rule.clone(),
228            branches: executable_branches,
229            source: rule_node.source.clone(),
230            needs_data,
231            rule_type: rule_node.rule_type.clone(),
232        });
233    }
234
235    let main_spec = graph.main_spec();
236    let named_types = build_type_tables(main_spec, resolved_types);
237
238    let mut sources: SpecSources = IndexMap::new();
239    for spec in resolved_types.keys() {
240        let key = (spec.name.clone(), spec.effective_from.clone());
241        sources
242            .entry(key)
243            .or_insert_with(|| crate::formatting::format_spec(spec, crate::formatting::MAX_COLS));
244    }
245
246    ExecutionPlan {
247        spec_name: main_spec.name.clone(),
248        data,
249        rules: executable_rules,
250        reference_evaluation_order: graph.reference_evaluation_order().to_vec(),
251        meta: main_spec
252            .meta_fields
253            .iter()
254            .map(|f| (f.key.clone(), f.value.clone()))
255            .collect(),
256        named_types,
257        effective: effective.clone(),
258        sources,
259    }
260}
261
262/// Build the named types table from the main spec's resolved types.
263fn build_type_tables(
264    main_spec: &Arc<LemmaSpec>,
265    resolved_types: &HashMap<Arc<LemmaSpec>, ResolvedSpecTypes>,
266) -> BTreeMap<String, LemmaType> {
267    let mut named_types = BTreeMap::new();
268
269    let main_resolved = resolved_types
270        .iter()
271        .find(|(spec, _)| Arc::ptr_eq(spec, main_spec))
272        .map(|(_, types)| types);
273
274    if let Some(resolved) = main_resolved {
275        for (type_name, lemma_type) in &resolved.named_types {
276            named_types.insert(type_name.clone(), lemma_type.clone());
277        }
278    }
279
280    named_types
281}
282
283/// A spec's public interface: its data (inputs) and rules (outputs) with
284/// full structured type information.
285///
286/// Built from an [`ExecutionPlan`] via [`ExecutionPlan::schema`] (all data and
287/// rules) or [`ExecutionPlan::schema_for_rules`] (scoped to specific rules and
288/// only the data they need).
289///
290/// Shared by the HTTP server, the CLI, the MCP server, WASM, and any other
291/// consumer. Carries the real [`LemmaType`] and [`LiteralValue`] so consumers
292/// can work at whatever fidelity they need — structured types for input forms,
293/// or `Display` for plain text.
294///
295/// This is the IO contract consumers can rely on:
296/// - `data`: required/provided inputs with full type constraints
297/// - `rules`: produced outputs with full result types
298///
299/// For cross-spec composition, planning validates that referenced specs satisfy
300/// this contract. Plan hashes are complementary: they lock full behavior.
301/// One data input in a [`SpecSchema`].
302///
303/// A named struct instead of a `(type, default)` tuple so JSON-native consumers
304/// (TypeScript, Python, ...) get stable field names. `default` is `None` unless
305/// the spec (or a typedef it references) declared one.
306#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
307pub struct DataEntry {
308    #[serde(rename = "type")]
309    pub lemma_type: LemmaType,
310    #[serde(skip_serializing_if = "Option::is_none", default)]
311    pub default: Option<LiteralValue>,
312}
313
314#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
315pub struct SpecSchema {
316    /// Spec name
317    pub spec: String,
318    /// Data (inputs) keyed by name.
319    pub data: indexmap::IndexMap<String, DataEntry>,
320    /// Rules (outputs) keyed by name, with their computed result types
321    pub rules: indexmap::IndexMap<String, LemmaType>,
322    /// Spec metadata
323    pub meta: HashMap<String, MetaValue>,
324}
325
326impl std::fmt::Display for SpecSchema {
327    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328        write!(f, "Spec: {}", self.spec)?;
329
330        if !self.meta.is_empty() {
331            write!(f, "\n\nMeta:")?;
332            // Sort keys for deterministic output
333            let mut keys: Vec<&String> = self.meta.keys().collect();
334            keys.sort();
335            for key in keys {
336                write!(f, "\n  {}: {}", key, self.meta.get(key).unwrap())?;
337            }
338        }
339
340        if !self.data.is_empty() {
341            write!(f, "\n\nData:")?;
342            for (name, entry) in &self.data {
343                write!(f, "\n  {} ({}", name, entry.lemma_type.name())?;
344                if let Some(constraints) = format_type_constraints(&entry.lemma_type.specifications)
345                {
346                    write!(f, ", {}", constraints)?;
347                }
348                if let Some(val) = &entry.default {
349                    write!(f, ", default: {}", val)?;
350                }
351                write!(f, ")")?;
352            }
353        }
354
355        if !self.rules.is_empty() {
356            write!(f, "\n\nRules:")?;
357            for (name, rule_type) in &self.rules {
358                write!(f, "\n  {} ({})", name, rule_type.name())?;
359            }
360        }
361
362        if self.data.is_empty() && self.rules.is_empty() {
363            write!(f, "\n  (no data or rules)")?;
364        }
365
366        Ok(())
367    }
368}
369
370impl SpecSchema {
371    /// Type-structural compatibility: every data/rule present in BOTH schemas
372    /// must have the same `LemmaType`. New additions (present in one but not
373    /// the other) are allowed. Ignores literal default values on data,
374    /// spec name, and meta fields.
375    pub(crate) fn is_type_compatible(&self, other: &SpecSchema) -> bool {
376        for (name, entry) in &self.data {
377            if let Some(other_entry) = other.data.get(name) {
378                if entry.lemma_type != other_entry.lemma_type {
379                    return false;
380                }
381            }
382        }
383        for (name, lt) in &self.rules {
384            if let Some(other_lt) = other.rules.get(name) {
385                if lt != other_lt {
386                    return false;
387                }
388            }
389        }
390        true
391    }
392}
393
394/// Produce a human-readable summary of type constraints, or `None` when there
395/// are no constraints worth showing (e.g. bare `boolean`).
396fn format_type_constraints(spec: &TypeSpecification) -> Option<String> {
397    let mut parts = Vec::new();
398
399    match spec {
400        TypeSpecification::Number {
401            minimum, maximum, ..
402        } => {
403            if let Some(v) = minimum {
404                parts.push(format!("minimum: {}", v));
405            }
406            if let Some(v) = maximum {
407                parts.push(format!("maximum: {}", v));
408            }
409        }
410        TypeSpecification::Scale {
411            minimum,
412            maximum,
413            decimals,
414            units,
415            ..
416        } => {
417            let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
418            if !unit_names.is_empty() {
419                parts.push(format!("units: {}", unit_names.join(", ")));
420            }
421            if let Some(v) = minimum {
422                parts.push(format!("minimum: {}", v));
423            }
424            if let Some(v) = maximum {
425                parts.push(format!("maximum: {}", v));
426            }
427            if let Some(d) = decimals {
428                parts.push(format!("decimals: {}", d));
429            }
430        }
431        TypeSpecification::Ratio {
432            minimum, maximum, ..
433        } => {
434            if let Some(v) = minimum {
435                parts.push(format!("minimum: {}", v));
436            }
437            if let Some(v) = maximum {
438                parts.push(format!("maximum: {}", v));
439            }
440        }
441        TypeSpecification::Text { options, .. } => {
442            if !options.is_empty() {
443                let quoted: Vec<String> = options.iter().map(|o| format!("\"{}\"", o)).collect();
444                parts.push(format!("options: {}", quoted.join(", ")));
445            }
446        }
447        TypeSpecification::Date {
448            minimum, maximum, ..
449        } => {
450            if let Some(v) = minimum {
451                parts.push(format!("minimum: {}", v));
452            }
453            if let Some(v) = maximum {
454                parts.push(format!("maximum: {}", v));
455            }
456        }
457        TypeSpecification::Time {
458            minimum, maximum, ..
459        } => {
460            if let Some(v) = minimum {
461                parts.push(format!("minimum: {}", v));
462            }
463            if let Some(v) = maximum {
464                parts.push(format!("maximum: {}", v));
465            }
466        }
467        TypeSpecification::Boolean { .. }
468        | TypeSpecification::Duration { .. }
469        | TypeSpecification::Veto { .. }
470        | TypeSpecification::Undetermined => {}
471    }
472
473    if parts.is_empty() {
474        None
475    } else {
476        Some(parts.join(", "))
477    }
478}
479
480impl ExecutionPlan {
481    /// Build a [`SpecSchema`] describing this plan's public IO contract.
482    ///
483    /// Only data transitively reachable from at least one local rule (via
484    /// `needs_data`) are included. Spec-reference data (which have no schema
485    /// type) are also excluded. Only local rules (no cross-spec segments) are
486    /// included. Data and rules are sorted by source position (definition
487    /// order).
488    pub fn schema(&self) -> SpecSchema {
489        let all_local_rules: Vec<String> = self
490            .rules
491            .iter()
492            .filter(|r| r.path.segments.is_empty())
493            .map(|r| r.name.clone())
494            .collect();
495        self.schema_for_rules(&all_local_rules)
496            .expect("BUG: all_local_rules sourced from self.rules")
497    }
498
499    /// Every typed data and every local rule — the surface other specs can address.
500    pub(crate) fn interface_schema(&self) -> SpecSchema {
501        let mut data_entries: Vec<(usize, String, DataEntry)> = self
502            .data
503            .iter()
504            .filter(|(_, data)| data.schema_type().is_some())
505            .map(|(path, data)| {
506                let lemma_type = data
507                    .schema_type()
508                    .expect("BUG: filter above ensured schema_type is Some")
509                    .clone();
510                let default = data.schema_default();
511                (
512                    data.source().span.start,
513                    path.input_key(),
514                    DataEntry {
515                        lemma_type,
516                        default,
517                    },
518                )
519            })
520            .collect();
521        data_entries.sort_by_key(|(pos, _, _)| *pos);
522
523        let rule_entries: Vec<(String, LemmaType)> = self
524            .rules
525            .iter()
526            .filter(|r| r.path.segments.is_empty())
527            .map(|r| (r.name.clone(), r.rule_type.clone()))
528            .collect();
529
530        SpecSchema {
531            spec: self.spec_name.clone(),
532            data: data_entries
533                .into_iter()
534                .map(|(_, name, data)| (name, data))
535                .collect(),
536            rules: rule_entries.into_iter().collect(),
537            meta: self.meta.clone(),
538        }
539    }
540
541    /// Build a [`SpecSchema`] scoped to specific rules.
542    ///
543    /// The returned schema contains only the data **needed** by the given rules
544    /// (transitively, via `needs_data`) and only those rules. This is the
545    /// "what do I need to evaluate these rules?" view.
546    /// Data are sorted by source position (definition order).
547    ///
548    /// Returns `Err` if any rule name is not found in the plan.
549    pub fn schema_for_rules(&self, rule_names: &[String]) -> Result<SpecSchema, Error> {
550        let mut needed_data = HashSet::new();
551        let mut rule_entries: Vec<(String, LemmaType)> = Vec::new();
552
553        for rule_name in rule_names {
554            let rule = self.get_rule(rule_name).ok_or_else(|| {
555                Error::request(
556                    format!(
557                        "Rule '{}' not found in spec '{}'",
558                        rule_name, self.spec_name
559                    ),
560                    None::<String>,
561                )
562            })?;
563            needed_data.extend(rule.needs_data.iter().cloned());
564            rule_entries.push((rule.name.clone(), rule.rule_type.clone()));
565        }
566
567        let mut data_entries: Vec<(usize, String, DataEntry)> = self
568            .data
569            .iter()
570            .filter(|(path, _)| needed_data.contains(path))
571            .filter(|(_, data)| data.schema_type().is_some())
572            .map(|(path, data)| {
573                let lemma_type = data.schema_type().unwrap().clone();
574                let default = data.schema_default();
575                (
576                    data.source().span.start,
577                    path.input_key(),
578                    DataEntry {
579                        lemma_type,
580                        default,
581                    },
582                )
583            })
584            .collect();
585        data_entries.sort_by_key(|(pos, _, _)| *pos);
586        let data_entries: Vec<(String, DataEntry)> = data_entries
587            .into_iter()
588            .map(|(_, name, data)| (name, data))
589            .collect();
590
591        Ok(SpecSchema {
592            spec: self.spec_name.clone(),
593            data: data_entries.into_iter().collect(),
594            rules: rule_entries.into_iter().collect(),
595            meta: self.meta.clone(),
596        })
597    }
598
599    /// Look up a data by its input key (e.g., "age" or "rules.base_price").
600    pub fn get_data_path_by_str(&self, name: &str) -> Option<&DataPath> {
601        self.data.keys().find(|path| path.input_key() == name)
602    }
603
604    /// Look up a local rule by its name (rule in the main spec).
605    pub fn get_rule(&self, name: &str) -> Option<&ExecutableRule> {
606        self.rules
607            .iter()
608            .find(|r| r.name == name && r.path.segments.is_empty())
609    }
610
611    /// Look up a rule by its full path.
612    pub fn get_rule_by_path(&self, rule_path: &RulePath) -> Option<&ExecutableRule> {
613        self.rules.iter().find(|r| &r.path == rule_path)
614    }
615
616    /// Get the literal value for a data path, if it exists and has a literal value.
617    pub fn get_data_value(&self, path: &DataPath) -> Option<&LiteralValue> {
618        self.data.get(path).and_then(|d| d.value())
619    }
620
621    /// Provide string values for data.
622    ///
623    /// Parses each string to its expected type, validates constraints, and applies to the plan.
624    pub fn with_data_values(
625        mut self,
626        values: HashMap<String, String>,
627        limits: &ResourceLimits,
628    ) -> Result<Self, Error> {
629        for (name, raw_value) in values {
630            let data_path = self.get_data_path_by_str(&name).ok_or_else(|| {
631                let available: Vec<String> = self.data.keys().map(|p| p.input_key()).collect();
632                Error::request(
633                    format!(
634                        "Data '{}' not found. Available data: {}",
635                        name,
636                        available.join(", ")
637                    ),
638                    None::<String>,
639                )
640            })?;
641            let data_path = data_path.clone();
642
643            let data_definition = self
644                .data
645                .get(&data_path)
646                .expect("BUG: data_path was just resolved from self.data, must exist");
647
648            let data_source = data_definition.source().clone();
649            let expected_type = data_definition.schema_type().cloned().ok_or_else(|| {
650                Error::request(
651                    format!(
652                        "Data '{}' is a spec reference; cannot provide a value.",
653                        name
654                    ),
655                    None::<String>,
656                )
657            })?;
658
659            let parsed_value = crate::planning::semantics::parse_value_from_string(
660                &raw_value,
661                &expected_type.specifications,
662                &data_source,
663            )
664            .map_err(|e| e.with_related_data(&name))?;
665            let semantic_value = semantics::value_to_semantic(&parsed_value).map_err(|msg| {
666                Error::validation(msg, Some(data_source.clone()), None::<String>)
667                    .with_related_data(&name)
668            })?;
669            let literal_value = LiteralValue {
670                value: semantic_value,
671                lemma_type: expected_type.clone(),
672            };
673
674            let size = literal_value.byte_size();
675            if size > limits.max_data_value_bytes {
676                return Err(Error::resource_limit_exceeded(
677                    "max_data_value_bytes",
678                    limits.max_data_value_bytes.to_string(),
679                    size.to_string(),
680                    format!(
681                        "Reduce the size of data values to {} bytes or less",
682                        limits.max_data_value_bytes
683                    ),
684                    Some(data_source.clone()),
685                    None,
686                    None,
687                )
688                .with_related_data(&name));
689            }
690
691            validate_value_against_type(&expected_type, &literal_value).map_err(|msg| {
692                Error::validation(msg, Some(data_source.clone()), None::<String>)
693                    .with_related_data(&name)
694            })?;
695
696            self.data.insert(
697                data_path,
698                DataDefinition::Value {
699                    value: literal_value,
700                    source: data_source,
701                },
702            );
703        }
704
705        Ok(self)
706    }
707}
708
709pub(crate) fn validate_value_against_type(
710    expected_type: &LemmaType,
711    value: &LiteralValue,
712) -> Result<(), String> {
713    use crate::planning::semantics::TypeSpecification;
714
715    let effective_decimals = |n: rust_decimal::Decimal| n.scale();
716
717    match (&expected_type.specifications, &value.value) {
718        (
719            TypeSpecification::Number {
720                minimum,
721                maximum,
722                decimals,
723                ..
724            },
725            ValueKind::Number(n),
726        ) => {
727            if let Some(min) = minimum {
728                if n < min {
729                    return Err(format!("{} is below minimum {}", n, min));
730                }
731            }
732            if let Some(max) = maximum {
733                if n > max {
734                    return Err(format!("{} is above maximum {}", n, max));
735                }
736            }
737            if let Some(d) = decimals {
738                if effective_decimals(*n) > u32::from(*d) {
739                    return Err(format!("{} has more than {} decimals", n, d));
740                }
741            }
742            Ok(())
743        }
744        (
745            TypeSpecification::Scale {
746                minimum,
747                maximum,
748                decimals,
749                ..
750            },
751            ValueKind::Scale(n, _unit),
752        ) => {
753            if let Some(min) = minimum {
754                if n < min {
755                    return Err(format!("{} is below minimum {}", n, min));
756                }
757            }
758            if let Some(max) = maximum {
759                if n > max {
760                    return Err(format!("{} is above maximum {}", n, max));
761                }
762            }
763            if let Some(d) = decimals {
764                if effective_decimals(*n) > u32::from(*d) {
765                    return Err(format!("{} has more than {} decimals", n, d));
766                }
767            }
768            Ok(())
769        }
770        (
771            TypeSpecification::Text {
772                length, options, ..
773            },
774            ValueKind::Text(s),
775        ) => {
776            let len = s.chars().count();
777            if let Some(exact) = length {
778                if len != *exact {
779                    return Err(format!(
780                        "'{}' has length {} but required length is {}",
781                        s, len, exact
782                    ));
783                }
784            }
785            if !options.is_empty() && !options.iter().any(|opt| opt == s) {
786                return Err(format!(
787                    "'{}' is not in allowed options: {}",
788                    s,
789                    options.join(", ")
790                ));
791            }
792            Ok(())
793        }
794        (
795            TypeSpecification::Ratio {
796                minimum,
797                maximum,
798                decimals,
799                ..
800            },
801            ValueKind::Ratio(r, _unit),
802        ) => {
803            if let Some(min) = minimum {
804                if r < min {
805                    return Err(format!("{} is below minimum {}", r, min));
806                }
807            }
808            if let Some(max) = maximum {
809                if r > max {
810                    return Err(format!("{} is above maximum {}", r, max));
811                }
812            }
813            if let Some(d) = decimals {
814                if effective_decimals(*r) > u32::from(*d) {
815                    return Err(format!("{} has more than {} decimals", r, d));
816                }
817            }
818            Ok(())
819        }
820        (
821            TypeSpecification::Date {
822                minimum, maximum, ..
823            },
824            ValueKind::Date(dt),
825        ) => {
826            use crate::planning::semantics::{compare_semantic_dates, date_time_to_semantic};
827            use std::cmp::Ordering;
828            if let Some(min) = minimum {
829                let min_sem = date_time_to_semantic(min);
830                if compare_semantic_dates(dt, &min_sem) == Ordering::Less {
831                    return Err(format!("{} is below minimum {}", dt, min));
832                }
833            }
834            if let Some(max) = maximum {
835                let max_sem = date_time_to_semantic(max);
836                if compare_semantic_dates(dt, &max_sem) == Ordering::Greater {
837                    return Err(format!("{} is above maximum {}", dt, max));
838                }
839            }
840            Ok(())
841        }
842        (
843            TypeSpecification::Duration {
844                minimum, maximum, ..
845            },
846            ValueKind::Duration(value, unit),
847        ) => {
848            use crate::computation::units::duration_to_seconds;
849            let value_secs = duration_to_seconds(*value, unit);
850            if let Some((min_v, min_u)) = minimum {
851                let min_secs = duration_to_seconds(*min_v, min_u);
852                if value_secs < min_secs {
853                    return Err(format!(
854                        "{} {} is below minimum {} {}",
855                        value, unit, min_v, min_u
856                    ));
857                }
858            }
859            if let Some((max_v, max_u)) = maximum {
860                let max_secs = duration_to_seconds(*max_v, max_u);
861                if value_secs > max_secs {
862                    return Err(format!(
863                        "{} {} is above maximum {} {}",
864                        value, unit, max_v, max_u
865                    ));
866                }
867            }
868            Ok(())
869        }
870        (
871            TypeSpecification::Time {
872                minimum, maximum, ..
873            },
874            ValueKind::Time(t),
875        ) => {
876            use crate::planning::semantics::{compare_semantic_times, time_to_semantic};
877            use std::cmp::Ordering;
878            if let Some(min) = minimum {
879                let min_sem = time_to_semantic(min);
880                if compare_semantic_times(t, &min_sem) == Ordering::Less {
881                    return Err(format!("{} is below minimum {}", t, min));
882                }
883            }
884            if let Some(max) = maximum {
885                let max_sem = time_to_semantic(max);
886                if compare_semantic_times(t, &max_sem) == Ordering::Greater {
887                    return Err(format!("{} is above maximum {}", t, max));
888                }
889            }
890            Ok(())
891        }
892        (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
893        | (TypeSpecification::Veto { .. }, _)
894        | (TypeSpecification::Undetermined, _) => Ok(()),
895        (spec, value_kind) => unreachable!(
896            "BUG: validate_value_against_type called with mismatched type/value: \
897             spec={:?}, value={:?} — typing must be enforced before validation",
898            spec, value_kind
899        ),
900    }
901}
902
903pub(crate) fn validate_literal_data_against_types(plan: &ExecutionPlan) -> Vec<Error> {
904    let mut errors = Vec::new();
905
906    for (data_path, data_definition) in &plan.data {
907        let (expected_type, lit) = match data_definition {
908            DataDefinition::Value { value, .. } => (&value.lemma_type, value),
909            DataDefinition::TypeDeclaration { .. }
910            | DataDefinition::SpecRef { .. }
911            | DataDefinition::Reference { .. } => continue,
912        };
913
914        if let Err(msg) = validate_value_against_type(expected_type, lit) {
915            let source = data_definition.source().clone();
916            errors.push(Error::validation(
917                format!(
918                    "Invalid value for data {} (expected {}): {}",
919                    data_path,
920                    expected_type.name(),
921                    msg
922                ),
923                Some(source),
924                None::<String>,
925            ));
926        }
927    }
928
929    errors
930}
931
932#[cfg(test)]
933mod tests {
934    use super::*;
935    use crate::parsing::ast::DateTimeValue;
936    use crate::planning::semantics::{
937        primitive_boolean, primitive_text, DataPath, LiteralValue, PathSegment, RulePath,
938    };
939    use crate::Engine;
940    use serde_json;
941    use std::str::FromStr;
942    use std::sync::Arc;
943
944    fn default_limits() -> ResourceLimits {
945        ResourceLimits::default()
946    }
947
948    #[test]
949    fn test_with_raw_values() {
950        let mut engine = Engine::new();
951        engine
952            .load(
953                r#"
954                spec test
955                data age: number -> default 25
956                "#,
957                crate::SourceType::Labeled("test.lemma"),
958            )
959            .unwrap();
960
961        let now = DateTimeValue::now();
962        let plan = engine.get_plan("test", Some(&now)).unwrap().clone();
963        let data_path = DataPath::new(vec![], "age".to_string());
964
965        let mut values = HashMap::new();
966        values.insert("age".to_string(), "30".to_string());
967
968        let updated_plan = plan.with_data_values(values, &default_limits()).unwrap();
969        let updated_value = updated_plan.get_data_value(&data_path).unwrap();
970        match &updated_value.value {
971            crate::planning::semantics::ValueKind::Number(n) => {
972                assert_eq!(n, &rust_decimal::Decimal::from(30))
973            }
974            other => panic!("Expected number literal, got {:?}", other),
975        }
976    }
977
978    #[test]
979    fn test_with_raw_values_type_mismatch() {
980        let mut engine = Engine::new();
981        engine
982            .load(
983                r#"
984                spec test
985                data age: number
986                "#,
987                crate::SourceType::Labeled("test.lemma"),
988            )
989            .unwrap();
990
991        let now = DateTimeValue::now();
992        let plan = engine.get_plan("test", Some(&now)).unwrap().clone();
993
994        let mut values = HashMap::new();
995        values.insert("age".to_string(), "thirty".to_string());
996
997        assert!(plan.with_data_values(values, &default_limits()).is_err());
998    }
999
1000    #[test]
1001    fn test_with_raw_values_unknown_data() {
1002        let mut engine = Engine::new();
1003        engine
1004            .load(
1005                r#"
1006                spec test
1007                data known: number
1008                "#,
1009                crate::SourceType::Labeled("test.lemma"),
1010            )
1011            .unwrap();
1012
1013        let now = DateTimeValue::now();
1014        let plan = engine.get_plan("test", Some(&now)).unwrap().clone();
1015
1016        let mut values = HashMap::new();
1017        values.insert("unknown".to_string(), "30".to_string());
1018
1019        assert!(plan.with_data_values(values, &default_limits()).is_err());
1020    }
1021
1022    #[test]
1023    fn test_with_raw_values_nested() {
1024        let mut engine = Engine::new();
1025        engine
1026            .load(
1027                r#"
1028                spec private
1029                data base_price: number
1030
1031                spec test
1032                with rules: private
1033                "#,
1034                crate::SourceType::Labeled("test.lemma"),
1035            )
1036            .unwrap();
1037
1038        let now = DateTimeValue::now();
1039        let plan = engine.get_plan("test", Some(&now)).unwrap().clone();
1040
1041        let mut values = HashMap::new();
1042        values.insert("rules.base_price".to_string(), "100".to_string());
1043
1044        let updated_plan = plan.with_data_values(values, &default_limits()).unwrap();
1045        let data_path = DataPath {
1046            segments: vec![PathSegment {
1047                data: "rules".to_string(),
1048                spec: "private".to_string(),
1049            }],
1050            data: "base_price".to_string(),
1051        };
1052        let updated_value = updated_plan.get_data_value(&data_path).unwrap();
1053        match &updated_value.value {
1054            crate::planning::semantics::ValueKind::Number(n) => {
1055                assert_eq!(n, &rust_decimal::Decimal::from(100))
1056            }
1057            other => panic!("Expected number literal, got {:?}", other),
1058        }
1059    }
1060
1061    fn test_source() -> crate::Source {
1062        use crate::parsing::ast::Span;
1063        crate::Source::new(
1064            "<test>",
1065            Span {
1066                start: 0,
1067                end: 0,
1068                line: 1,
1069                col: 0,
1070            },
1071        )
1072    }
1073
1074    fn create_literal_expr(value: LiteralValue) -> Expression {
1075        Expression::new(
1076            crate::planning::semantics::ExpressionKind::Literal(Box::new(value)),
1077            test_source(),
1078        )
1079    }
1080
1081    fn create_data_path_expr(path: DataPath) -> Expression {
1082        Expression::new(
1083            crate::planning::semantics::ExpressionKind::DataPath(path),
1084            test_source(),
1085        )
1086    }
1087
1088    fn create_number_literal(n: rust_decimal::Decimal) -> LiteralValue {
1089        LiteralValue::number(n)
1090    }
1091
1092    fn create_boolean_literal(b: bool) -> LiteralValue {
1093        LiteralValue::from_bool(b)
1094    }
1095
1096    fn create_text_literal(s: String) -> LiteralValue {
1097        LiteralValue::text(s)
1098    }
1099
1100    #[test]
1101    fn with_values_should_enforce_number_maximum_constraint() {
1102        // Higher-standard requirement: user input must be validated against type constraints.
1103        // If this test fails, Lemma accepts invalid values and gives false reassurance.
1104        let data_path = DataPath::new(vec![], "x".to_string());
1105
1106        let max10 = crate::planning::semantics::LemmaType::primitive(
1107            crate::planning::semantics::TypeSpecification::Number {
1108                minimum: None,
1109                maximum: Some(rust_decimal::Decimal::from_str("10").unwrap()),
1110                decimals: None,
1111                precision: None,
1112                help: String::new(),
1113            },
1114        );
1115        let source = Source::new(
1116            "<test>",
1117            crate::parsing::ast::Span {
1118                start: 0,
1119                end: 0,
1120                line: 1,
1121                col: 0,
1122            },
1123        );
1124        let mut data = IndexMap::new();
1125        data.insert(
1126            data_path.clone(),
1127            crate::planning::semantics::DataDefinition::Value {
1128                value: crate::planning::semantics::LiteralValue::number_with_type(
1129                    0.into(),
1130                    max10.clone(),
1131                ),
1132                source: source.clone(),
1133            },
1134        );
1135
1136        let plan = ExecutionPlan {
1137            spec_name: "test".to_string(),
1138            data,
1139            rules: Vec::new(),
1140            reference_evaluation_order: Vec::new(),
1141            meta: HashMap::new(),
1142            named_types: BTreeMap::new(),
1143            effective: EffectiveDate::Origin,
1144            sources: IndexMap::new(),
1145        };
1146
1147        let mut values = HashMap::new();
1148        values.insert("x".to_string(), "11".to_string());
1149
1150        assert!(
1151            plan.with_data_values(values, &default_limits()).is_err(),
1152            "Providing x=11 should fail due to maximum 10"
1153        );
1154    }
1155
1156    #[test]
1157    fn with_values_should_enforce_text_enum_options() {
1158        // Higher-standard requirement: enum options must be enforced for text types.
1159        let data_path = DataPath::new(vec![], "tier".to_string());
1160
1161        let tier = crate::planning::semantics::LemmaType::primitive(
1162            crate::planning::semantics::TypeSpecification::Text {
1163                length: None,
1164                options: vec!["silver".to_string(), "gold".to_string()],
1165                help: String::new(),
1166            },
1167        );
1168        let source = Source::new(
1169            "<test>",
1170            crate::parsing::ast::Span {
1171                start: 0,
1172                end: 0,
1173                line: 1,
1174                col: 0,
1175            },
1176        );
1177        let mut data = IndexMap::new();
1178        data.insert(
1179            data_path.clone(),
1180            crate::planning::semantics::DataDefinition::Value {
1181                value: crate::planning::semantics::LiteralValue::text_with_type(
1182                    "silver".to_string(),
1183                    tier.clone(),
1184                ),
1185                source,
1186            },
1187        );
1188
1189        let plan = ExecutionPlan {
1190            spec_name: "test".to_string(),
1191            data,
1192            rules: Vec::new(),
1193            reference_evaluation_order: Vec::new(),
1194            meta: HashMap::new(),
1195            named_types: BTreeMap::new(),
1196            effective: EffectiveDate::Origin,
1197            sources: IndexMap::new(),
1198        };
1199
1200        let mut values = HashMap::new();
1201        values.insert("tier".to_string(), "platinum".to_string());
1202
1203        assert!(
1204            plan.with_data_values(values, &default_limits()).is_err(),
1205            "Invalid enum value should be rejected (tier='platinum')"
1206        );
1207    }
1208
1209    #[test]
1210    fn with_values_should_enforce_scale_decimals() {
1211        // Higher-standard requirement: decimals should be enforced on scale inputs,
1212        // unless the language explicitly defines rounding semantics.
1213        let data_path = DataPath::new(vec![], "price".to_string());
1214
1215        let money = crate::planning::semantics::LemmaType::primitive(
1216            crate::planning::semantics::TypeSpecification::Scale {
1217                minimum: None,
1218                maximum: None,
1219                decimals: Some(2),
1220                precision: None,
1221                units: crate::planning::semantics::ScaleUnits::from(vec![
1222                    crate::planning::semantics::ScaleUnit {
1223                        name: "eur".to_string(),
1224                        value: rust_decimal::Decimal::from_str("1.0").unwrap(),
1225                    },
1226                ]),
1227                help: String::new(),
1228            },
1229        );
1230        let source = Source::new(
1231            "<test>",
1232            crate::parsing::ast::Span {
1233                start: 0,
1234                end: 0,
1235                line: 1,
1236                col: 0,
1237            },
1238        );
1239        let mut data = IndexMap::new();
1240        data.insert(
1241            data_path.clone(),
1242            crate::planning::semantics::DataDefinition::Value {
1243                value: crate::planning::semantics::LiteralValue::scale_with_type(
1244                    rust_decimal::Decimal::from_str("0").unwrap(),
1245                    "eur".to_string(),
1246                    money.clone(),
1247                ),
1248                source,
1249            },
1250        );
1251
1252        let plan = ExecutionPlan {
1253            spec_name: "test".to_string(),
1254            data,
1255            rules: Vec::new(),
1256            reference_evaluation_order: Vec::new(),
1257            meta: HashMap::new(),
1258            named_types: BTreeMap::new(),
1259            effective: EffectiveDate::Origin,
1260            sources: IndexMap::new(),
1261        };
1262
1263        let mut values = HashMap::new();
1264        values.insert("price".to_string(), "1.234 eur".to_string());
1265
1266        assert!(
1267            plan.with_data_values(values, &default_limits()).is_err(),
1268            "Scale decimals=2 should reject 1.234 eur"
1269        );
1270    }
1271
1272    #[test]
1273    fn test_serialize_deserialize_execution_plan() {
1274        let data_path = DataPath {
1275            segments: vec![],
1276            data: "age".to_string(),
1277        };
1278        let mut data = IndexMap::new();
1279        data.insert(
1280            data_path.clone(),
1281            crate::planning::semantics::DataDefinition::Value {
1282                value: create_number_literal(0.into()),
1283                source: test_source(),
1284            },
1285        );
1286        let plan = ExecutionPlan {
1287            spec_name: "test".to_string(),
1288            data,
1289            rules: Vec::new(),
1290            reference_evaluation_order: Vec::new(),
1291            meta: HashMap::new(),
1292            named_types: BTreeMap::new(),
1293            effective: EffectiveDate::Origin,
1294            sources: IndexMap::new(),
1295        };
1296
1297        let json = serde_json::to_string(&plan).expect("Should serialize");
1298        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1299
1300        assert_eq!(deserialized.spec_name, plan.spec_name);
1301        assert_eq!(deserialized.data.len(), plan.data.len());
1302        assert_eq!(deserialized.rules.len(), plan.rules.len());
1303    }
1304
1305    #[test]
1306    fn test_serialize_deserialize_plan_with_imported_named_type_defining_spec() {
1307        let dep_spec = Arc::new(crate::parsing::ast::LemmaSpec::new("examples".to_string()));
1308        let imported_type = crate::planning::semantics::LemmaType::new(
1309            "salary".to_string(),
1310            TypeSpecification::scale(),
1311            crate::planning::semantics::TypeExtends::Custom {
1312                parent: "money".to_string(),
1313                family: "money".to_string(),
1314                defining_spec: crate::planning::semantics::TypeDefiningSpec::Import {
1315                    spec: Arc::clone(&dep_spec),
1316                },
1317            },
1318        );
1319
1320        let mut named_types = BTreeMap::new();
1321        named_types.insert("salary".to_string(), imported_type);
1322
1323        let plan = ExecutionPlan {
1324            spec_name: "test".to_string(),
1325            data: IndexMap::new(),
1326            rules: Vec::new(),
1327            reference_evaluation_order: Vec::new(),
1328            meta: HashMap::new(),
1329            named_types,
1330            effective: EffectiveDate::Origin,
1331            sources: IndexMap::new(),
1332        };
1333
1334        let json = serde_json::to_string(&plan).expect("Should serialize");
1335        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1336
1337        let recovered = deserialized
1338            .named_types
1339            .get("salary")
1340            .expect("salary type should be present");
1341        match &recovered.extends {
1342            crate::planning::semantics::TypeExtends::Custom {
1343                defining_spec: crate::planning::semantics::TypeDefiningSpec::Import { spec },
1344                ..
1345            } => {
1346                assert_eq!(spec.name, "examples");
1347            }
1348            other => panic!(
1349                "Expected imported defining_spec after round-trip, got {:?}",
1350                other
1351            ),
1352        }
1353    }
1354
1355    #[test]
1356    fn test_serialize_deserialize_plan_with_rules() {
1357        use crate::planning::semantics::ExpressionKind;
1358
1359        let age_path = DataPath::new(vec![], "age".to_string());
1360        let mut data = IndexMap::new();
1361        data.insert(
1362            age_path.clone(),
1363            crate::planning::semantics::DataDefinition::Value {
1364                value: create_number_literal(0.into()),
1365                source: test_source(),
1366            },
1367        );
1368        let mut plan = ExecutionPlan {
1369            spec_name: "test".to_string(),
1370            data,
1371            rules: Vec::new(),
1372            reference_evaluation_order: Vec::new(),
1373            meta: HashMap::new(),
1374            named_types: BTreeMap::new(),
1375            effective: EffectiveDate::Origin,
1376            sources: IndexMap::new(),
1377        };
1378
1379        let rule = ExecutableRule {
1380            path: RulePath::new(vec![], "can_drive".to_string()),
1381            name: "can_drive".to_string(),
1382            branches: vec![Branch {
1383                condition: Some(Expression::new(
1384                    ExpressionKind::Comparison(
1385                        Arc::new(create_data_path_expr(age_path.clone())),
1386                        crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1387                        Arc::new(create_literal_expr(create_number_literal(18.into()))),
1388                    ),
1389                    test_source(),
1390                )),
1391                result: create_literal_expr(create_boolean_literal(true)),
1392                source: test_source(),
1393            }],
1394            needs_data: BTreeSet::from([age_path]),
1395            source: test_source(),
1396            rule_type: primitive_boolean().clone(),
1397        };
1398
1399        plan.rules.push(rule);
1400
1401        let json = serde_json::to_string(&plan).expect("Should serialize");
1402        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1403
1404        assert_eq!(deserialized.spec_name, plan.spec_name);
1405        assert_eq!(deserialized.data.len(), plan.data.len());
1406        assert_eq!(deserialized.rules.len(), plan.rules.len());
1407        assert_eq!(deserialized.rules[0].name, "can_drive");
1408        assert_eq!(deserialized.rules[0].branches.len(), 1);
1409        assert_eq!(deserialized.rules[0].needs_data.len(), 1);
1410    }
1411
1412    #[test]
1413    fn test_serialize_deserialize_plan_with_nested_data_paths() {
1414        use crate::planning::semantics::PathSegment;
1415        let data_path = DataPath {
1416            segments: vec![PathSegment {
1417                data: "employee".to_string(),
1418                spec: "private".to_string(),
1419            }],
1420            data: "salary".to_string(),
1421        };
1422
1423        let mut data = IndexMap::new();
1424        data.insert(
1425            data_path.clone(),
1426            crate::planning::semantics::DataDefinition::Value {
1427                value: create_number_literal(0.into()),
1428                source: test_source(),
1429            },
1430        );
1431        let plan = ExecutionPlan {
1432            spec_name: "test".to_string(),
1433            data,
1434            rules: Vec::new(),
1435            reference_evaluation_order: Vec::new(),
1436            meta: HashMap::new(),
1437            named_types: BTreeMap::new(),
1438            effective: EffectiveDate::Origin,
1439            sources: IndexMap::new(),
1440        };
1441
1442        let json = serde_json::to_string(&plan).expect("Should serialize");
1443        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1444
1445        assert_eq!(deserialized.data.len(), 1);
1446        let (deserialized_path, _) = deserialized.data.iter().next().unwrap();
1447        assert_eq!(deserialized_path.segments.len(), 1);
1448        assert_eq!(deserialized_path.segments[0].data, "employee");
1449        assert_eq!(deserialized_path.data, "salary");
1450    }
1451
1452    #[test]
1453    fn test_serialize_deserialize_plan_with_multiple_data_types() {
1454        let name_path = DataPath::new(vec![], "name".to_string());
1455        let age_path = DataPath::new(vec![], "age".to_string());
1456        let active_path = DataPath::new(vec![], "active".to_string());
1457
1458        let mut data = IndexMap::new();
1459        data.insert(
1460            name_path.clone(),
1461            crate::planning::semantics::DataDefinition::Value {
1462                value: create_text_literal("Alice".to_string()),
1463                source: test_source(),
1464            },
1465        );
1466        data.insert(
1467            age_path.clone(),
1468            crate::planning::semantics::DataDefinition::Value {
1469                value: create_number_literal(30.into()),
1470                source: test_source(),
1471            },
1472        );
1473        data.insert(
1474            active_path.clone(),
1475            crate::planning::semantics::DataDefinition::Value {
1476                value: create_boolean_literal(true),
1477                source: test_source(),
1478            },
1479        );
1480
1481        let plan = ExecutionPlan {
1482            spec_name: "test".to_string(),
1483            data,
1484            rules: Vec::new(),
1485            reference_evaluation_order: Vec::new(),
1486            meta: HashMap::new(),
1487            named_types: BTreeMap::new(),
1488            effective: EffectiveDate::Origin,
1489            sources: IndexMap::new(),
1490        };
1491
1492        let json = serde_json::to_string(&plan).expect("Should serialize");
1493        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1494
1495        assert_eq!(deserialized.data.len(), 3);
1496
1497        assert_eq!(
1498            deserialized.get_data_value(&name_path).unwrap().value,
1499            crate::planning::semantics::ValueKind::Text("Alice".to_string())
1500        );
1501        assert_eq!(
1502            deserialized.get_data_value(&age_path).unwrap().value,
1503            crate::planning::semantics::ValueKind::Number(30.into())
1504        );
1505        assert_eq!(
1506            deserialized.get_data_value(&active_path).unwrap().value,
1507            crate::planning::semantics::ValueKind::Boolean(true)
1508        );
1509    }
1510
1511    #[test]
1512    fn test_serialize_deserialize_plan_with_multiple_branches() {
1513        use crate::planning::semantics::ExpressionKind;
1514
1515        let points_path = DataPath::new(vec![], "points".to_string());
1516        let mut data = IndexMap::new();
1517        data.insert(
1518            points_path.clone(),
1519            crate::planning::semantics::DataDefinition::Value {
1520                value: create_number_literal(0.into()),
1521                source: test_source(),
1522            },
1523        );
1524        let mut plan = ExecutionPlan {
1525            spec_name: "test".to_string(),
1526            data,
1527            rules: Vec::new(),
1528            reference_evaluation_order: Vec::new(),
1529            meta: HashMap::new(),
1530            named_types: BTreeMap::new(),
1531            effective: EffectiveDate::Origin,
1532            sources: IndexMap::new(),
1533        };
1534
1535        let rule = ExecutableRule {
1536            path: RulePath::new(vec![], "tier".to_string()),
1537            name: "tier".to_string(),
1538            branches: vec![
1539                Branch {
1540                    condition: None,
1541                    result: create_literal_expr(create_text_literal("bronze".to_string())),
1542                    source: test_source(),
1543                },
1544                Branch {
1545                    condition: Some(Expression::new(
1546                        ExpressionKind::Comparison(
1547                            Arc::new(create_data_path_expr(points_path.clone())),
1548                            crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1549                            Arc::new(create_literal_expr(create_number_literal(100.into()))),
1550                        ),
1551                        test_source(),
1552                    )),
1553                    result: create_literal_expr(create_text_literal("silver".to_string())),
1554                    source: test_source(),
1555                },
1556                Branch {
1557                    condition: Some(Expression::new(
1558                        ExpressionKind::Comparison(
1559                            Arc::new(create_data_path_expr(points_path.clone())),
1560                            crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1561                            Arc::new(create_literal_expr(create_number_literal(500.into()))),
1562                        ),
1563                        test_source(),
1564                    )),
1565                    result: create_literal_expr(create_text_literal("gold".to_string())),
1566                    source: test_source(),
1567                },
1568            ],
1569            needs_data: BTreeSet::from([points_path]),
1570            source: test_source(),
1571            rule_type: primitive_text().clone(),
1572        };
1573
1574        plan.rules.push(rule);
1575
1576        let json = serde_json::to_string(&plan).expect("Should serialize");
1577        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1578
1579        assert_eq!(deserialized.rules.len(), 1);
1580        assert_eq!(deserialized.rules[0].branches.len(), 3);
1581        assert!(deserialized.rules[0].branches[0].condition.is_none());
1582        assert!(deserialized.rules[0].branches[1].condition.is_some());
1583        assert!(deserialized.rules[0].branches[2].condition.is_some());
1584    }
1585
1586    #[test]
1587    fn test_serialize_deserialize_empty_plan() {
1588        let plan = ExecutionPlan {
1589            spec_name: "empty".to_string(),
1590            data: IndexMap::new(),
1591            rules: Vec::new(),
1592            reference_evaluation_order: Vec::new(),
1593            meta: HashMap::new(),
1594            named_types: BTreeMap::new(),
1595            effective: EffectiveDate::Origin,
1596            sources: IndexMap::new(),
1597        };
1598
1599        let json = serde_json::to_string(&plan).expect("Should serialize");
1600        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1601
1602        assert_eq!(deserialized.spec_name, "empty");
1603        assert_eq!(deserialized.data.len(), 0);
1604        assert_eq!(deserialized.rules.len(), 0);
1605    }
1606
1607    #[test]
1608    fn test_serialize_deserialize_plan_with_arithmetic_expressions() {
1609        use crate::planning::semantics::ExpressionKind;
1610
1611        let x_path = DataPath::new(vec![], "x".to_string());
1612        let mut data = IndexMap::new();
1613        data.insert(
1614            x_path.clone(),
1615            crate::planning::semantics::DataDefinition::Value {
1616                value: create_number_literal(0.into()),
1617                source: test_source(),
1618            },
1619        );
1620        let mut plan = ExecutionPlan {
1621            spec_name: "test".to_string(),
1622            data,
1623            rules: Vec::new(),
1624            reference_evaluation_order: Vec::new(),
1625            meta: HashMap::new(),
1626            named_types: BTreeMap::new(),
1627            effective: EffectiveDate::Origin,
1628            sources: IndexMap::new(),
1629        };
1630
1631        let rule = ExecutableRule {
1632            path: RulePath::new(vec![], "doubled".to_string()),
1633            name: "doubled".to_string(),
1634            branches: vec![Branch {
1635                condition: None,
1636                result: Expression::new(
1637                    ExpressionKind::Arithmetic(
1638                        Arc::new(create_data_path_expr(x_path.clone())),
1639                        crate::parsing::ast::ArithmeticComputation::Multiply,
1640                        Arc::new(create_literal_expr(create_number_literal(2.into()))),
1641                    ),
1642                    test_source(),
1643                ),
1644                source: test_source(),
1645            }],
1646            needs_data: BTreeSet::from([x_path]),
1647            source: test_source(),
1648            rule_type: crate::planning::semantics::primitive_number().clone(),
1649        };
1650
1651        plan.rules.push(rule);
1652
1653        let json = serde_json::to_string(&plan).expect("Should serialize");
1654        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1655
1656        assert_eq!(deserialized.rules.len(), 1);
1657        match &deserialized.rules[0].branches[0].result.kind {
1658            ExpressionKind::Arithmetic(left, op, right) => {
1659                assert_eq!(*op, crate::parsing::ast::ArithmeticComputation::Multiply);
1660                match &left.kind {
1661                    ExpressionKind::DataPath(_) => {}
1662                    _ => panic!("Expected DataPath in left operand"),
1663                }
1664                match &right.kind {
1665                    ExpressionKind::Literal(_) => {}
1666                    _ => panic!("Expected Literal in right operand"),
1667                }
1668            }
1669            _ => panic!("Expected Arithmetic expression"),
1670        }
1671    }
1672
1673    #[test]
1674    fn test_serialize_deserialize_round_trip_equality() {
1675        use crate::planning::semantics::ExpressionKind;
1676
1677        let age_path = DataPath::new(vec![], "age".to_string());
1678        let mut data = IndexMap::new();
1679        data.insert(
1680            age_path.clone(),
1681            crate::planning::semantics::DataDefinition::Value {
1682                value: create_number_literal(0.into()),
1683                source: test_source(),
1684            },
1685        );
1686        let mut plan = ExecutionPlan {
1687            spec_name: "test".to_string(),
1688            data,
1689            rules: Vec::new(),
1690            reference_evaluation_order: Vec::new(),
1691            meta: HashMap::new(),
1692            named_types: BTreeMap::new(),
1693            effective: EffectiveDate::Origin,
1694            sources: IndexMap::new(),
1695        };
1696
1697        let rule = ExecutableRule {
1698            path: RulePath::new(vec![], "is_adult".to_string()),
1699            name: "is_adult".to_string(),
1700            branches: vec![Branch {
1701                condition: Some(Expression::new(
1702                    ExpressionKind::Comparison(
1703                        Arc::new(create_data_path_expr(age_path.clone())),
1704                        crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1705                        Arc::new(create_literal_expr(create_number_literal(18.into()))),
1706                    ),
1707                    test_source(),
1708                )),
1709                result: create_literal_expr(create_boolean_literal(true)),
1710                source: test_source(),
1711            }],
1712            needs_data: BTreeSet::from([age_path]),
1713            source: test_source(),
1714            rule_type: primitive_boolean().clone(),
1715        };
1716
1717        plan.rules.push(rule);
1718
1719        let json = serde_json::to_string(&plan).expect("Should serialize");
1720        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1721
1722        let json2 = serde_json::to_string(&deserialized).expect("Should serialize again");
1723        let deserialized2: ExecutionPlan =
1724            serde_json::from_str(&json2).expect("Should deserialize again");
1725
1726        assert_eq!(deserialized2.spec_name, plan.spec_name);
1727        assert_eq!(deserialized2.data.len(), plan.data.len());
1728        assert_eq!(deserialized2.rules.len(), plan.rules.len());
1729        assert_eq!(deserialized2.rules[0].name, plan.rules[0].name);
1730        assert_eq!(
1731            deserialized2.rules[0].branches.len(),
1732            plan.rules[0].branches.len()
1733        );
1734    }
1735
1736    fn empty_plan(effective: crate::parsing::ast::EffectiveDate) -> ExecutionPlan {
1737        ExecutionPlan {
1738            spec_name: "s".into(),
1739            data: IndexMap::new(),
1740            rules: Vec::new(),
1741            reference_evaluation_order: Vec::new(),
1742            meta: HashMap::new(),
1743            named_types: BTreeMap::new(),
1744            effective,
1745            sources: IndexMap::new(),
1746        }
1747    }
1748
1749    #[test]
1750    fn plan_at_exact_boundary_selects_later_slice() {
1751        use crate::parsing::ast::{DateTimeValue, EffectiveDate};
1752
1753        let june = DateTimeValue {
1754            year: 2025,
1755            month: 6,
1756            day: 1,
1757            hour: 0,
1758            minute: 0,
1759            second: 0,
1760            microsecond: 0,
1761            timezone: None,
1762        };
1763        let dec = DateTimeValue {
1764            year: 2025,
1765            month: 12,
1766            day: 1,
1767            hour: 0,
1768            minute: 0,
1769            second: 0,
1770            microsecond: 0,
1771            timezone: None,
1772        };
1773
1774        let set = ExecutionPlanSet {
1775            spec_name: "s".into(),
1776            plans: vec![
1777                empty_plan(EffectiveDate::Origin),
1778                empty_plan(EffectiveDate::DateTimeValue(june.clone())),
1779                empty_plan(EffectiveDate::DateTimeValue(dec.clone())),
1780            ],
1781        };
1782
1783        assert!(std::ptr::eq(
1784            set.plan_at(&EffectiveDate::DateTimeValue(june.clone()))
1785                .expect("boundary instant"),
1786            &set.plans[1]
1787        ));
1788        assert!(std::ptr::eq(
1789            set.plan_at(&EffectiveDate::DateTimeValue(dec.clone()))
1790                .expect("dec boundary"),
1791            &set.plans[2]
1792        ));
1793    }
1794
1795    #[test]
1796    fn plan_at_day_before_boundary_stays_in_earlier_slice() {
1797        use crate::parsing::ast::{DateTimeValue, EffectiveDate};
1798
1799        let june = DateTimeValue {
1800            year: 2025,
1801            month: 6,
1802            day: 1,
1803            hour: 0,
1804            minute: 0,
1805            second: 0,
1806            microsecond: 0,
1807            timezone: None,
1808        };
1809        let may_end = DateTimeValue {
1810            year: 2025,
1811            month: 5,
1812            day: 31,
1813            hour: 23,
1814            minute: 59,
1815            second: 59,
1816            microsecond: 0,
1817            timezone: None,
1818        };
1819
1820        let set = ExecutionPlanSet {
1821            spec_name: "s".into(),
1822            plans: vec![
1823                empty_plan(EffectiveDate::Origin),
1824                empty_plan(EffectiveDate::DateTimeValue(june)),
1825            ],
1826        };
1827
1828        assert!(std::ptr::eq(
1829            set.plan_at(&EffectiveDate::DateTimeValue(may_end))
1830                .expect("may 31"),
1831            &set.plans[0]
1832        ));
1833    }
1834
1835    #[test]
1836    fn plan_at_single_plan_matches_any_instant_after_start() {
1837        use crate::parsing::ast::{DateTimeValue, EffectiveDate};
1838
1839        let t = DateTimeValue {
1840            year: 2025,
1841            month: 3,
1842            day: 1,
1843            hour: 0,
1844            minute: 0,
1845            second: 0,
1846            microsecond: 0,
1847            timezone: None,
1848        };
1849        let set = ExecutionPlanSet {
1850            spec_name: "s".into(),
1851            plans: vec![empty_plan(EffectiveDate::DateTimeValue(DateTimeValue {
1852                year: 2025,
1853                month: 1,
1854                day: 1,
1855                hour: 0,
1856                minute: 0,
1857                second: 0,
1858                microsecond: 0,
1859                timezone: None,
1860            }))],
1861        };
1862        assert!(std::ptr::eq(
1863            set.plan_at(&EffectiveDate::DateTimeValue(t))
1864                .expect("inside single slice"),
1865            &set.plans[0]
1866        ));
1867    }
1868
1869    /// The schema JSON shape is the IO contract for every non-Rust consumer
1870    /// (WASM playground, Hex, HTTP, TypeScript). Nail the exact envelope.
1871    #[test]
1872    fn schema_json_shape_contract() {
1873        let mut engine = Engine::new();
1874        engine
1875            .load(
1876                r#"
1877                spec pricing
1878                data bridge_height: scale
1879                  -> unit meter 1
1880                  -> default 100 meter
1881                data quantity: number -> minimum 0
1882                rule cost: bridge_height * quantity
1883                "#,
1884                crate::SourceType::Labeled("test.lemma"),
1885            )
1886            .unwrap();
1887        let now = DateTimeValue::now();
1888        let schema = engine.get_plan("pricing", Some(&now)).unwrap().schema();
1889
1890        let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
1891
1892        let bh = &value["data"]["bridge_height"];
1893        assert!(
1894            bh.is_object(),
1895            "data entry must be a named object, not tuple"
1896        );
1897        assert!(
1898            bh.get("type").is_some(),
1899            "data entry must expose `type` field"
1900        );
1901        assert!(
1902            bh.get("default").is_some(),
1903            "bridge_height has a promoted default"
1904        );
1905
1906        let ty = &bh["type"];
1907        assert_eq!(
1908            ty["kind"], "scale",
1909            "kind tag sits on the type object itself"
1910        );
1911        assert!(
1912            ty["units"].is_array(),
1913            "scale-only fields flatten up to top level"
1914        );
1915        assert!(
1916            ty.get("options").is_none(),
1917            "text-only fields must not leak"
1918        );
1919
1920        let qty = &value["data"]["quantity"];
1921        assert_eq!(qty["type"]["kind"], "number");
1922        assert!(
1923            qty.get("default").is_none(),
1924            "no declared default means no field"
1925        );
1926
1927        let cost = &value["rules"]["cost"];
1928        assert_eq!(cost["kind"], "scale", "rule types use the same flat shape");
1929    }
1930
1931    #[test]
1932    fn schema_json_round_trip_preserves_shape() {
1933        let mut engine = Engine::new();
1934        engine
1935            .load(
1936                r#"
1937                spec s
1938                data age: number -> minimum 0 -> default 18
1939                data grade: text -> options "A" "B" "C"
1940                rule adult: age >= 18
1941                "#,
1942                crate::SourceType::Labeled("s.lemma"),
1943            )
1944            .unwrap();
1945        let now = DateTimeValue::now();
1946        let schema = engine.get_plan("s", Some(&now)).unwrap().schema();
1947
1948        let json = serde_json::to_string(&schema).unwrap();
1949        let round_tripped: SpecSchema = serde_json::from_str(&json).unwrap();
1950        assert_eq!(schema, round_tripped);
1951    }
1952}
1953
1954// ---------------------------------------------------------------------------
1955// ExecutionPlanSet (formerly plan_set.rs)
1956// ---------------------------------------------------------------------------