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