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::computation::UnitResolutionContext;
12use crate::parsing::ast::{DateTimeValue, EffectiveDate, LemmaRepository, LemmaSpec, MetaValue};
13use crate::parsing::source::Source;
14use crate::planning::data_input::{parse_data_value, DataValueInput};
15use crate::planning::graph::Graph;
16use crate::planning::graph::ResolvedSpecTypes;
17use crate::planning::normalize::{
18    build_decision_table, is_literal_bool_expression, normalize_expression,
19};
20use crate::planning::semantics::{
21    value_kind_matches_spec, DataDefinition, DataPath, Expression, LemmaType, LiteralValue,
22    RulePath, TypeSpecification, ValueKind,
23};
24use crate::Error;
25use crate::ResourceLimits;
26use indexmap::IndexMap;
27use serde::{Deserialize, Deserializer, Serialize, Serializer};
28use std::collections::{BTreeSet, HashMap, HashSet};
29use std::sync::Arc;
30
31/// One spec's contribution to an [`ExecutionPlan`], together with its
32/// formatted AST source.
33///
34/// `repository` is `None` for workspace (root) specs. Including the
35/// repository name means two specs with the same base name from different
36/// repos are always distinct entries.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct SpecSource {
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub repository: Option<String>,
41    pub name: String,
42    pub effective_from: EffectiveDate,
43    pub source: String,
44}
45
46pub type SpecSources = Vec<SpecSource>;
47
48/// A complete execution plan ready for the evaluator
49///
50/// Contains the topologically sorted list of rules to execute, along with all data.
51/// Self-contained structure - no spec lookups required during evaluation.
52#[derive(Debug, Clone)]
53pub struct ExecutionPlan {
54    /// Main spec name
55    pub spec_name: String,
56
57    /// Optional commentary from the `"""..."""` block in the spec source.
58    pub commentary: Option<String>,
59
60    /// Per-data data in definition order: value, type-only, or spec reference.
61    pub data: IndexMap<DataPath, DataDefinition>,
62
63    /// Rules to execute in topological order (sorted by dependencies)
64    pub rules: Vec<ExecutableRule>,
65
66    /// Order in which [`DataDefinition::Reference`] entries must be resolved
67    /// at evaluation time so that chained references (reference → reference →
68    /// data) copy values in the correct sequence. Empty when the plan has no
69    /// references.
70    pub reference_evaluation_order: Vec<DataPath>,
71
72    /// Spec metadata
73    pub meta: HashMap<String, MetaValue>,
74
75    /// Main-spec types from planning. [`ResolvedSpecTypes::unit_index`] is expression-scope
76    /// units (local types plus direct `uses` imports). Rule-result units live on each
77    /// [`ExecutableRule::rule_type`], not in this index.
78    pub resolved_types: ResolvedSpecTypes,
79
80    /// Reverse index: canonical-form unit signature `Vec<(unit_name, exponent)>` →
81    /// (unit_name, owning type). Built from expression-scope units during planning so
82    /// cross-type Multiply/Divide arithmetic can deterministically resolve a combined
83    /// signature back to a single named unit. Ambiguous signatures (the same key matched
84    /// by units in two distinct types) are rejected at planning time.
85    pub signature_index: crate::computation::arithmetic::SignatureIndex,
86
87    pub effective: EffectiveDate,
88
89    /// Canonical source for all specs in this plan (one entry per spec, includes repository).
90    /// Reconstructed from AST — not raw file content.
91    pub sources: SpecSources,
92}
93
94/// All [`ExecutionPlan`]s for a spec name after dependency resolution.
95/// Ordered by [`ExecutionPlan::effective`]. Slice end is derived from the next plan's `effective`.
96#[derive(Debug, Clone)]
97pub struct ExecutionPlanSet {
98    pub spec_name: String,
99    pub plans: Vec<ExecutionPlan>,
100}
101
102impl ExecutionPlanSet {
103    /// Plan covering `[effective[i], effective[i+1])` (half-open).
104    #[must_use]
105    pub fn plan_at(&self, effective: &EffectiveDate) -> Option<&ExecutionPlan> {
106        for (i, plan) in self.plans.iter().enumerate() {
107            let from_ok = *effective >= plan.effective;
108            let to_ok = self
109                .plans
110                .get(i + 1)
111                .map(|next| *effective < next.effective)
112                .unwrap_or(true);
113            if from_ok && to_ok {
114                return Some(plan);
115            }
116        }
117        None
118    }
119}
120
121/// An executable rule with flattened branches
122///
123/// Contains all information needed to evaluate a rule without spec lookups.
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct ExecutableRule {
126    /// Unique identifier for this rule
127    pub path: RulePath,
128
129    /// Rule name
130    pub name: String,
131
132    /// Branches as written in source (evaluated only when `explain` is enabled)
133    /// First branch has condition=None (default expression)
134    /// Subsequent branches have condition=Some(...) (unless clauses)
135    pub branches: Vec<Branch>,
136
137    /// Normalized decision table (self-contained conditions); authoritative for evaluation
138    pub normalized_branches: Vec<NormalizedBranch>,
139
140    /// All data this rule needs (direct + inherited from rule dependencies)
141    pub needs_data: BTreeSet<DataPath>,
142
143    /// Source location for error messages (always present for rules from parsed specs)
144    pub source: Source,
145
146    /// Computed type of this rule's result
147    /// Every rule MUST have a type (Lemma is strictly typed)
148    #[serde(with = "arc_lemma_type")]
149    pub rule_type: Arc<LemmaType>,
150}
151
152/// A branch in an executable rule (original expressions for explanation trace)
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct Branch {
155    /// Condition expression (None for default branch)
156    pub condition: Option<Expression>,
157
158    /// Result expression as written (`RulePath` refs preserved)
159    pub result: Expression,
160
161    /// Source location for error messages (always present for branches from parsed specs)
162    pub source: Source,
163}
164
165/// One entry in the normalized decision table
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct NormalizedBranch {
168    /// Self-contained condition (always present)
169    pub condition: Expression,
170
171    /// Normalized result expression
172    pub result: Expression,
173}
174
175mod arc_lemma_type {
176    use super::LemmaType;
177    use serde::{Deserialize, Deserializer, Serialize, Serializer};
178    use std::sync::Arc;
179
180    pub fn serialize<S>(value: &Arc<LemmaType>, serializer: S) -> Result<S::Ok, S::Error>
181    where
182        S: Serializer,
183    {
184        value.as_ref().serialize(serializer)
185    }
186
187    pub fn deserialize<'de, D>(deserializer: D) -> Result<Arc<LemmaType>, D::Error>
188    where
189        D: Deserializer<'de>,
190    {
191        LemmaType::deserialize(deserializer).map(Arc::new)
192    }
193}
194
195/// Builds an execution plan from a Graph for one temporal slice.
196/// Internal implementation detail - only called by plan()
197pub(crate) fn build_execution_plan(
198    graph: &Graph,
199    resolved_types: &mut Vec<(Arc<LemmaRepository>, Arc<LemmaSpec>, ResolvedSpecTypes)>,
200    effective: &EffectiveDate,
201) -> Result<ExecutionPlan, Vec<Error>> {
202    let execution_order = graph.execution_order();
203
204    let main_spec = graph.main_spec();
205    let main_idx = resolved_types
206        .iter()
207        .position(|(_, spec, _)| Arc::ptr_eq(spec, main_spec));
208
209    let mut sources: SpecSources = Vec::new();
210    for (repo, spec, _) in resolved_types.iter() {
211        if !sources.iter().any(|e| {
212            e.repository == repo.name
213                && e.name == spec.name
214                && e.effective_from == spec.effective_from
215        }) {
216            sources.push(SpecSource {
217                repository: repo.name.clone(),
218                name: spec.name.clone(),
219                effective_from: spec.effective_from.clone(),
220                source: crate::formatting::format_specs(&[spec.as_ref().clone()]),
221            });
222        }
223    }
224
225    let main_resolved_types = main_idx
226        .map(|idx| resolved_types.remove(idx).2)
227        .unwrap_or_default();
228    let data = graph.build_data(&main_resolved_types.resolved);
229
230    let signature_index = crate::planning::graph::build_signature_index(
231        &main_spec.name,
232        &main_resolved_types.unit_index,
233    )
234    .expect("BUG: signature_index build already validated during resolve_and_validate");
235
236    let mut executable_rules: Vec<ExecutableRule> = Vec::new();
237    let mut path_to_index: HashMap<RulePath, usize> = HashMap::new();
238
239    for rule_path in execution_order {
240        let rule_node = graph.rules().get(rule_path).expect(
241            "bug: rule from topological sort not in graph - validation should have caught this",
242        );
243
244        let mut executable_branches = Vec::new();
245        for (condition, result) in &rule_node.branches {
246            executable_branches.push(Branch {
247                condition: condition.clone(),
248                result: result.clone(),
249                source: rule_node.source.clone(),
250            });
251        }
252
253        let unit_ctx = UnitResolutionContext::WithIndex(&main_resolved_types.unit_index);
254        let decision_table = build_decision_table(&rule_node.branches);
255        let mut normalized_branches = Vec::new();
256        let mut direct_data = HashSet::new();
257        for (condition, result) in decision_table {
258            let normalized_condition =
259                normalize_expression(&condition, Some(&unit_ctx)).map_err(|error| {
260                    vec![Error::validation(
261                        format!("failed to normalize decision table condition: {error}"),
262                        Some(rule_node.source.clone()),
263                        None::<String>,
264                    )]
265                })?;
266            if is_literal_bool_expression(&normalized_condition, false) {
267                continue;
268            }
269            let normalized_result =
270                normalize_expression(&result, Some(&unit_ctx)).map_err(|error| {
271                    vec![Error::validation(
272                        format!("failed to normalize decision table result: {error}"),
273                        Some(rule_node.source.clone()),
274                        None::<String>,
275                    )]
276                })?;
277            normalized_condition.collect_data_paths(&mut direct_data);
278            normalized_result.collect_data_paths(&mut direct_data);
279            normalized_branches.push(NormalizedBranch {
280                condition: normalized_condition,
281                result: normalized_result,
282            });
283        }
284
285        let mut needs_data: BTreeSet<DataPath> = direct_data.into_iter().collect();
286
287        for dep in &rule_node.depends_on_rules {
288            if let Some(&dep_idx) = path_to_index.get(dep) {
289                needs_data.extend(executable_rules[dep_idx].needs_data.iter().cloned());
290            }
291        }
292
293        path_to_index.insert(rule_path.clone(), executable_rules.len());
294        executable_rules.push(ExecutableRule {
295            path: rule_path.clone(),
296            name: rule_path.rule.clone(),
297            branches: executable_branches,
298            normalized_branches,
299            source: rule_node.source.clone(),
300            needs_data,
301            rule_type: Arc::clone(&rule_node.rule_type),
302        });
303    }
304
305    Ok(ExecutionPlan {
306        spec_name: main_spec.name.clone(),
307        commentary: main_spec.commentary.clone(),
308        data,
309        rules: executable_rules,
310        reference_evaluation_order: graph.reference_evaluation_order().to_vec(),
311        meta: main_spec
312            .meta_fields
313            .iter()
314            .map(|f| (f.key.clone(), f.value.clone()))
315            .collect(),
316        resolved_types: main_resolved_types,
317        signature_index,
318        effective: effective.clone(),
319        sources,
320    })
321}
322
323/// A spec's public interface: its data (inputs) and rules (outputs) with
324/// full structured type information.
325///
326/// Built from an [`ExecutionPlan`] via [`ExecutionPlan::schema`] (all data and
327/// rules) or [`ExecutionPlan::schema_for_rules`] (scoped to specific rules and
328/// only the data they need).
329///
330/// Shared by the HTTP server, the CLI, the MCP server, WASM, and any other
331/// consumer. Carries the real [`LemmaType`] and [`LiteralValue`] so consumers
332/// can work at whatever fidelity they need — structured types for input forms,
333/// or `Display` for plain text.
334///
335/// This is the IO contract consumers can rely on:
336/// - `data`: required/provided inputs with full type constraints
337/// - `rules`: produced outputs with full result types
338///
339/// For cross-spec composition, planning validates that referenced specs satisfy
340/// this contract. Plan hashes are complementary: they lock full behavior.
341/// One data input in a [`SpecSchema`].
342///
343/// A named struct instead of a `(type, bound, default)` tuple so JSON-native consumers
344/// (TypeScript, Python, ...) get stable field names. `bound_value` holds a spec or
345/// caller-fixed literal; `default` is only a `-> default ...` suggestion.
346#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
347pub struct DataEntry {
348    #[serde(rename = "type")]
349    pub lemma_type: LemmaType,
350    #[serde(skip_serializing_if = "Option::is_none", default)]
351    pub bound_value: Option<LiteralValue>,
352    #[serde(skip_serializing_if = "Option::is_none", default)]
353    pub default: Option<LiteralValue>,
354}
355
356#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
357pub struct SpecSchema {
358    /// Resolved spec id (logical name including path segments).
359    pub spec: String,
360    /// Optional commentary from the `"""..."""` block in the spec source.
361    #[serde(skip_serializing_if = "Option::is_none", default)]
362    pub commentary: Option<String>,
363    /// The effective date of this specific plan version. `None` for origin (unversioned) specs.
364    #[serde(skip_serializing_if = "Option::is_none", default)]
365    pub effective: Option<DateTimeValue>,
366    /// All known effective-from dates for this spec, populated by the caller when multiple
367    /// temporal versions exist. Empty for single-version specs.
368    #[serde(skip_serializing_if = "Vec::is_empty", default)]
369    pub versions: Vec<DateTimeValue>,
370    /// Data (inputs) keyed by name.
371    pub data: indexmap::IndexMap<String, DataEntry>,
372    /// Rules (outputs) keyed by name, with their computed result types
373    pub rules: indexmap::IndexMap<String, LemmaType>,
374    /// Spec metadata
375    pub meta: HashMap<String, MetaValue>,
376}
377
378impl std::fmt::Display for SpecSchema {
379    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
380        write!(f, "Spec: {}", self.spec)?;
381
382        if let Some(commentary) = &self.commentary {
383            write!(f, "\n  {}", commentary)?;
384        }
385
386        if !self.meta.is_empty() {
387            write!(f, "\n\nMeta:")?;
388            // Sort keys for deterministic output
389            let mut entries: Vec<(&String, &MetaValue)> = self.meta.iter().collect();
390            entries.sort_by_key(|(k, _)| *k);
391            for (key, value) in entries {
392                write!(f, "\n  {}: {}", key, value)?;
393            }
394        }
395
396        if !self.data.is_empty() {
397            write!(f, "\n\nData:")?;
398            for (name, entry) in &self.data {
399                write!(f, "\n  {} ({})", name, entry.lemma_type.specifications)?;
400                for line in type_detail_lines(&entry.lemma_type.specifications) {
401                    write!(f, "\n    {}", line)?;
402                }
403                let help = entry.lemma_type.specifications.help();
404                if !help.is_empty() {
405                    write!(f, "\n    help: {}", help)?;
406                }
407                if let Some(val) = &entry.bound_value {
408                    write!(f, "\n    value: {}", val)?;
409                }
410                if let Some(val) = &entry.default {
411                    write!(f, "\n    default: {}", val)?;
412                }
413            }
414        }
415
416        if !self.rules.is_empty() {
417            write!(f, "\n\nRules:")?;
418            for (name, rule_type) in &self.rules {
419                write!(f, "\n  {} ({})", name, rule_type.specifications)?;
420            }
421        }
422
423        if self.data.is_empty() && self.rules.is_empty() {
424            write!(f, "\n  (no data or rules)")?;
425        }
426
427        Ok(())
428    }
429}
430
431impl SpecSchema {
432    /// Type-structural compatibility: every data/rule present in BOTH schemas
433    /// must have the same `LemmaType`. New additions (present in one but not
434    /// the other) are allowed. Ignores literal default values on data,
435    /// spec name, and meta fields.
436    pub(crate) fn is_type_compatible(&self, other: &SpecSchema) -> bool {
437        for (name, entry) in &self.data {
438            if let Some(other_entry) = other.data.get(name) {
439                if entry.lemma_type != other_entry.lemma_type {
440                    return false;
441                }
442            }
443        }
444        for (name, lt) in &self.rules {
445            if let Some(other_lt) = other.rules.get(name) {
446                if lt != other_lt {
447                    return false;
448                }
449            }
450        }
451        true
452    }
453}
454
455/// Produce a human-readable summary of type constraints, or `None` when there
456/// are no constraints worth showing (e.g. bare `boolean`).
457/// Returns one formatted string per constraint or property of the type specification.
458/// Uses `rational_to_display_str` for all rational bounds so they render as decimals,
459/// not as raw fractions.
460pub fn type_detail_lines(spec: &TypeSpecification) -> Vec<String> {
461    use crate::computation::rational::rational_to_display_str;
462    let mut lines = Vec::new();
463    match spec {
464        TypeSpecification::Quantity {
465            minimum,
466            maximum,
467            decimals,
468            units,
469            ..
470        } => {
471            let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
472            if !unit_names.is_empty() {
473                lines.push(format!("units: {}", unit_names.join(", ")));
474            }
475            if let Some(d) = decimals {
476                lines.push(format!("decimals: {}", d));
477            }
478            if let Some((magnitude, unit_name)) = minimum {
479                lines.push(format!(
480                    "minimum: {} {}",
481                    rational_to_display_str(magnitude),
482                    unit_name
483                ));
484            }
485            if let Some((magnitude, unit_name)) = maximum {
486                lines.push(format!(
487                    "maximum: {} {}",
488                    rational_to_display_str(magnitude),
489                    unit_name
490                ));
491            }
492        }
493        TypeSpecification::Number {
494            minimum,
495            maximum,
496            decimals,
497            ..
498        } => {
499            if let Some(d) = decimals {
500                lines.push(format!("decimals: {}", d));
501            }
502            if let Some(v) = minimum {
503                lines.push(format!("minimum: {}", rational_to_display_str(v)));
504            }
505            if let Some(v) = maximum {
506                lines.push(format!("maximum: {}", rational_to_display_str(v)));
507            }
508        }
509        TypeSpecification::Ratio {
510            minimum,
511            maximum,
512            decimals,
513            units,
514            ..
515        } => {
516            let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
517            if !unit_names.is_empty() {
518                lines.push(format!("units: {}", unit_names.join(", ")));
519            }
520            if let Some(d) = decimals {
521                lines.push(format!("decimals: {}", d));
522            }
523            if let Some(v) = minimum {
524                lines.push(format!("minimum: {}", rational_to_display_str(v)));
525            }
526            if let Some(v) = maximum {
527                lines.push(format!("maximum: {}", rational_to_display_str(v)));
528            }
529        }
530        TypeSpecification::Text {
531            options, length, ..
532        } => {
533            if let Some(l) = length {
534                lines.push(format!("length: {}", l));
535            }
536            if !options.is_empty() {
537                let quoted: Vec<String> = options.iter().map(|o| format!("\"{}\"", o)).collect();
538                lines.push(format!("options: {}", quoted.join(", ")));
539            }
540        }
541        TypeSpecification::Date {
542            minimum, maximum, ..
543        } => {
544            if let Some(v) = minimum {
545                lines.push(format!("minimum: {}", v));
546            }
547            if let Some(v) = maximum {
548                lines.push(format!("maximum: {}", v));
549            }
550        }
551        TypeSpecification::Time {
552            minimum, maximum, ..
553        } => {
554            if let Some(v) = minimum {
555                lines.push(format!("minimum: {}", v));
556            }
557            if let Some(v) = maximum {
558                lines.push(format!("maximum: {}", v));
559            }
560        }
561        TypeSpecification::QuantityRange { units, .. } => {
562            let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
563            if !unit_names.is_empty() {
564                lines.push(format!("units: {}", unit_names.join(", ")));
565            }
566        }
567        TypeSpecification::RatioRange { units, .. } => {
568            let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
569            if !unit_names.is_empty() {
570                lines.push(format!("units: {}", unit_names.join(", ")));
571            }
572        }
573        TypeSpecification::Boolean { .. }
574        | TypeSpecification::NumberRange { .. }
575        | TypeSpecification::DateRange { .. }
576        | TypeSpecification::TimeRange { .. }
577        | TypeSpecification::Veto { .. }
578        | TypeSpecification::Undetermined => {}
579    }
580    lines
581}
582
583impl ExecutionPlan {
584    /// Expression-scope unit index (local types plus direct `uses` imports).
585    /// Rule-result units outside this scope are resolved from [`ExecutableRule::rule_type`]
586    /// at materialization time.
587    pub(crate) fn expression_unit_index(&self) -> &HashMap<String, Arc<LemmaType>> {
588        &self.resolved_types.unit_index
589    }
590
591    /// Build a [`SpecSchema`] describing this plan's public IO contract.
592    ///
593    /// Only data transitively reachable from at least one local rule (via
594    /// `needs_data`) are included. Spec-reference data (which have no schema
595    /// type) are also excluded. Only local rules (no cross-spec segments) are
596    /// included. Data and rules are sorted by source position (definition
597    /// order).
598    pub fn schema(&self) -> SpecSchema {
599        let all_local_rules: Vec<String> = self
600            .rules
601            .iter()
602            .filter(|r| r.path.segments.is_empty())
603            .map(|r| r.name.clone())
604            .collect();
605        self.schema_for_rules(&all_local_rules)
606            .expect("BUG: all_local_rules sourced from self.rules")
607    }
608
609    /// Every typed data input and local rule — the full spec surface.
610    pub fn interface_schema(&self) -> SpecSchema {
611        let mut data_entries: Vec<(usize, String, DataEntry)> = self
612            .data
613            .iter()
614            .filter(|(_, data)| data.schema_type().is_some())
615            .map(|(path, data)| {
616                let lemma_type = data
617                    .schema_type()
618                    .expect("BUG: filter above ensured schema_type is Some")
619                    .clone();
620                let bound_value = data.bound_value().cloned();
621                let default = data.default_suggestion();
622                (
623                    data.source().span.start,
624                    path.input_key(),
625                    DataEntry {
626                        lemma_type,
627                        bound_value,
628                        default,
629                    },
630                )
631            })
632            .collect();
633        data_entries.sort_by_key(|(pos, _, _)| *pos);
634
635        let rule_entries: Vec<(String, LemmaType)> = self
636            .rules
637            .iter()
638            .filter(|r| r.path.segments.is_empty())
639            .map(|r| (r.name.clone(), (*r.rule_type).clone()))
640            .collect();
641
642        SpecSchema {
643            spec: self.spec_name.clone(),
644            commentary: self.commentary.clone(),
645            effective: self.effective.as_ref().cloned(),
646            versions: Vec::new(),
647            data: data_entries
648                .into_iter()
649                .map(|(_, name, data)| (name, data))
650                .collect(),
651            rules: rule_entries.into_iter().collect(),
652            meta: self.meta.clone(),
653        }
654    }
655
656    /// Build a [`SpecSchema`] scoped to specific rules.
657    ///
658    /// The returned schema contains only the data **needed** by the given rules
659    /// (transitively, via `needs_data`) and only those rules. This is the
660    /// "what do I need to evaluate these rules?" view.
661    /// Data are sorted by source position (definition order).
662    ///
663    /// Returns `Err` if any rule name is not found in the plan.
664    pub fn schema_for_rules(&self, rule_names: &[String]) -> Result<SpecSchema, Error> {
665        let mut needed_data = HashSet::new();
666        let mut rule_entries: Vec<(String, LemmaType)> = Vec::new();
667
668        for rule_name in rule_names {
669            let rule = self.get_rule(rule_name).ok_or_else(|| {
670                Error::request(
671                    format!(
672                        "Rule '{}' not found in spec '{}'",
673                        rule_name, self.spec_name
674                    ),
675                    None::<String>,
676                )
677            })?;
678            needed_data.extend(rule.needs_data.iter().cloned());
679            rule_entries.push((rule.name.clone(), (*rule.rule_type).clone()));
680        }
681
682        let mut data_entries: Vec<(usize, String, DataEntry)> = self
683            .data
684            .iter()
685            .filter(|(path, _)| needed_data.contains(path))
686            .filter_map(|(path, data)| {
687                let lemma_type = data.schema_type()?.clone();
688                let bound_value = data.bound_value().cloned();
689                let default = data.default_suggestion();
690                Some((
691                    data.source().span.start,
692                    path.input_key(),
693                    DataEntry {
694                        lemma_type,
695                        bound_value,
696                        default,
697                    },
698                ))
699            })
700            .collect();
701        data_entries.sort_by_key(|(pos, _, _)| *pos);
702        let data_entries: Vec<(String, DataEntry)> = data_entries
703            .into_iter()
704            .map(|(_, name, data)| (name, data))
705            .collect();
706
707        Ok(SpecSchema {
708            spec: self.spec_name.clone(),
709            commentary: self.commentary.clone(),
710            effective: self.effective.as_ref().cloned(),
711            versions: Vec::new(),
712            data: data_entries.into_iter().collect(),
713            rules: rule_entries.into_iter().collect(),
714            meta: self.meta.clone(),
715        })
716    }
717
718    /// Look up a data by its input key (e.g., "age" or "rules.base_price").
719    pub fn get_data_path_by_str(&self, name: &str) -> Option<&DataPath> {
720        let canonical_name = crate::parsing::ast::ascii_lowercase_logical_name(name.to_string());
721        self.data
722            .keys()
723            .find(|path| path.input_key() == canonical_name)
724    }
725
726    /// Look up a local rule by its name (rule in the main spec).
727    pub fn get_rule(&self, name: &str) -> Option<&ExecutableRule> {
728        let canonical_name = crate::parsing::ast::ascii_lowercase_logical_name(name.to_string());
729        self.rules
730            .iter()
731            .find(|r| r.name == canonical_name && r.path.segments.is_empty())
732    }
733
734    /// Look up a rule by its full path.
735    pub fn get_rule_by_path(&self, rule_path: &RulePath) -> Option<&ExecutableRule> {
736        self.rules.iter().find(|r| &r.path == rule_path)
737    }
738
739    /// Get the literal value for a data path, if it exists and has a literal value.
740    pub fn get_data_value(&self, path: &DataPath) -> Option<&LiteralValue> {
741        self.data.get(path).and_then(|d| d.value())
742    }
743
744    /// Provide typed data values from a client.
745    ///
746    /// Parses each value to its expected type, validates constraints, and applies to the plan.
747    pub fn set_data_values(
748        mut self,
749        values: std::collections::HashMap<String, DataValueInput>,
750        limits: &ResourceLimits,
751    ) -> Result<Self, Error> {
752        for (name, raw_value) in values {
753            let data_path = self.get_data_path_by_str(&name).ok_or_else(|| {
754                let available: Vec<String> = self.data.keys().map(|p| p.input_key()).collect();
755                Error::request(
756                    format!(
757                        "Data '{}' not found. Available data: {}",
758                        name,
759                        available.join(", ")
760                    ),
761                    None::<String>,
762                )
763            })?;
764            let data_path = data_path.clone();
765
766            let data_definition = self
767                .data
768                .get(&data_path)
769                .expect("BUG: data_path was just resolved from self.data, must exist");
770
771            let data_source = data_definition.source().clone();
772            let type_arc = match data_definition {
773                DataDefinition::TypeDeclaration { resolved_type, .. }
774                | DataDefinition::Reference { resolved_type, .. } => Arc::clone(resolved_type),
775                DataDefinition::Value { value, .. } => Arc::clone(&value.lemma_type),
776                DataDefinition::Import { .. } => {
777                    return Err(Error::request(
778                        format!(
779                            "Data '{}' is a spec reference; cannot provide a value.",
780                            name
781                        ),
782                        None::<String>,
783                    ));
784                }
785                DataDefinition::Violated { .. } => {
786                    unreachable!(
787                        "BUG: Violated data '{}' cannot appear before set_data_values on a fresh plan clone",
788                        name
789                    );
790                }
791            };
792
793            let literal_value = match parse_data_value(&raw_value, &type_arc, &data_source) {
794                Ok(value) => value,
795                Err(error) => {
796                    self.data.insert(
797                        data_path,
798                        DataDefinition::Violated {
799                            reason: error.message().to_string(),
800                            source: data_source,
801                        },
802                    );
803                    continue;
804                }
805            };
806
807            let size = literal_value.byte_size();
808            if size > limits.max_data_value_bytes {
809                return Err(Error::resource_limit_exceeded(
810                    "max_data_value_bytes",
811                    limits.max_data_value_bytes.to_string(),
812                    size.to_string(),
813                    format!(
814                        "Reduce the size of data values to {} bytes or less",
815                        limits.max_data_value_bytes
816                    ),
817                    Some(data_source.clone()),
818                    None,
819                    None,
820                )
821                .with_related_data(&name));
822            }
823
824            if let Err(msg) = validate_value_against_type(type_arc.as_ref(), &literal_value) {
825                self.data.insert(
826                    data_path,
827                    DataDefinition::Violated {
828                        reason: msg,
829                        source: data_source,
830                    },
831                );
832                continue;
833            }
834
835            self.data.insert(
836                data_path,
837                DataDefinition::Value {
838                    value: literal_value,
839                    source: data_source,
840                },
841            );
842        }
843
844        Ok(self)
845    }
846
847    /// Promote declared defaults on type declarations into concrete [`DataDefinition::Value`] entries.
848    /// Call BEFORE [`Self::set_data_values`] so user-provided values override defaults.
849    /// Reference resolution is handled by the evaluator at runtime.
850    #[must_use]
851    pub fn with_defaults(mut self) -> Self {
852        let promotions: Vec<(DataPath, DataDefinition)> = self
853            .data
854            .iter()
855            .filter_map(|(path, def)| {
856                if let DataDefinition::TypeDeclaration {
857                    declared_default: Some(dv),
858                    resolved_type,
859                    source,
860                } = def
861                {
862                    Some((
863                        path.clone(),
864                        DataDefinition::Value {
865                            value: LiteralValue {
866                                value: dv.clone(),
867                                lemma_type: Arc::clone(resolved_type),
868                            },
869                            source: source.clone(),
870                        },
871                    ))
872                } else {
873                    None
874                }
875            })
876            .collect();
877
878        for (path, def) in promotions {
879            self.data.insert(path, def);
880        }
881        self
882    }
883}
884
885pub(crate) fn validate_value_against_type(
886    expected_type: &LemmaType,
887    value: &LiteralValue,
888) -> Result<(), String> {
889    use crate::computation::rational::{commit_rational_to_decimal, RationalInteger};
890    use crate::planning::semantics::TypeSpecification;
891
892    fn exceeds_decimal_places(magnitude: &RationalInteger, max_decimals: u8) -> bool {
893        match commit_rational_to_decimal(magnitude) {
894            Ok(decimal) => decimal.scale() > u32::from(max_decimals),
895            Err(_) => true,
896        }
897    }
898
899    fn format_rational(r: &RationalInteger, decimals: Option<u8>) -> String {
900        use crate::computation::rational::rational_to_display_str;
901        match commit_rational_to_decimal(r) {
902            Ok(decimal) => match decimals {
903                Some(dp) => {
904                    let rounded = decimal.round_dp(u32::from(dp));
905                    format!("{:.prec$}", rounded, prec = dp as usize)
906                }
907                None => decimal.normalize().to_string(),
908            },
909            Err(_) => rational_to_display_str(r),
910        }
911    }
912
913    match (&expected_type.specifications, &value.value) {
914        (
915            TypeSpecification::Number {
916                minimum,
917                maximum,
918                decimals,
919                ..
920            },
921            ValueKind::Number(n),
922        ) => {
923            if let Some(d) = decimals {
924                if exceeds_decimal_places(n, *d) {
925                    return Err(format!(
926                        "{} exceeds decimals constraint {d}",
927                        format_rational(n, *decimals)
928                    ));
929                }
930            }
931            if let Some(min) = minimum {
932                if n < min {
933                    return Err(format!(
934                        "{} is below minimum {}",
935                        format_rational(n, *decimals),
936                        format_rational(min, *decimals)
937                    ));
938                }
939            }
940            if let Some(max) = maximum {
941                if n > max {
942                    return Err(format!(
943                        "{} is above maximum {}",
944                        format_rational(n, *decimals),
945                        format_rational(max, *decimals)
946                    ));
947                }
948            }
949            Ok(())
950        }
951        (
952            TypeSpecification::Quantity {
953                minimum,
954                maximum,
955                decimals,
956                units,
957                ..
958            },
959            ValueKind::Quantity(magnitude, signature),
960        ) => {
961            use crate::computation::rational::checked_div;
962            use crate::planning::semantics::quantity_declared_bound_canonical;
963            let unit = signature
964                .first()
965                .map(|(n, _)| n.as_str())
966                .expect("BUG: Quantity value has empty signature in execution plan validation");
967            let quantity_unit = units.get(unit)?;
968            let factor = &quantity_unit.factor;
969            let in_unit = checked_div(magnitude, factor).map_err(|failure| {
970                format!("cannot de-canonicalize quantity for validation: {failure}")
971            })?;
972            if let Some(d) = decimals {
973                if exceeds_decimal_places(&in_unit, *d) {
974                    return Err(format!(
975                        "{} {unit} exceeds decimals constraint {d}",
976                        format_rational(&in_unit, *decimals)
977                    ));
978                }
979            }
980            if let Some(bound) = minimum {
981                let canonical_min = quantity_declared_bound_canonical(
982                    bound,
983                    units,
984                    expected_type.name().as_str(),
985                    "minimum",
986                )?;
987                if magnitude < &canonical_min {
988                    let min_in_unit = checked_div(&canonical_min, factor).map_err(|failure| {
989                        format!("cannot de-canonicalize minimum for validation: {failure}")
990                    })?;
991                    let value_display =
992                        format!("{} {}", format_rational(&in_unit, *decimals), unit);
993                    let bound_display = format!(
994                        "{} {}",
995                        format_rational(&min_in_unit, *decimals),
996                        quantity_unit.name
997                    );
998                    return Err(format!("{value_display} is below minimum {bound_display}"));
999                }
1000            }
1001            if let Some(bound) = maximum {
1002                let canonical_max = quantity_declared_bound_canonical(
1003                    bound,
1004                    units,
1005                    expected_type.name().as_str(),
1006                    "maximum",
1007                )?;
1008                if magnitude > &canonical_max {
1009                    let max_in_unit = checked_div(&canonical_max, factor).map_err(|failure| {
1010                        format!("cannot de-canonicalize maximum for validation: {failure}")
1011                    })?;
1012                    let value_display =
1013                        format!("{} {}", format_rational(&in_unit, *decimals), unit);
1014                    let bound_display = format!(
1015                        "{} {}",
1016                        format_rational(&max_in_unit, *decimals),
1017                        quantity_unit.name
1018                    );
1019                    return Err(format!("{value_display} is above maximum {bound_display}"));
1020                }
1021            }
1022            Ok(())
1023        }
1024        (
1025            TypeSpecification::Text {
1026                length, options, ..
1027            },
1028            ValueKind::Text(s),
1029        ) => {
1030            let len = s.chars().count();
1031            if let Some(exact) = length {
1032                if len != *exact {
1033                    return Err(format!(
1034                        "'{}' has length {} but required length is {}",
1035                        s, len, exact
1036                    ));
1037                }
1038            }
1039            if !options.is_empty() && !options.iter().any(|opt| opt == s) {
1040                return Err(format!(
1041                    "'{}' is not in allowed options: {}",
1042                    s,
1043                    options.join(", ")
1044                ));
1045            }
1046            Ok(())
1047        }
1048        (
1049            TypeSpecification::Ratio {
1050                minimum,
1051                maximum,
1052                decimals,
1053                units,
1054                ..
1055            },
1056            ValueKind::Ratio(r, unit_name),
1057        ) => {
1058            use crate::computation::rational::checked_mul;
1059
1060            if let Some(d) = decimals {
1061                if exceeds_decimal_places(r, *d) {
1062                    return Err(format!(
1063                        "{} exceeds decimals constraint {d}",
1064                        format_rational(r, *decimals)
1065                    ));
1066                }
1067            }
1068            if let Some(type_minimum) = minimum {
1069                if r < type_minimum {
1070                    let message = match unit_name.as_deref() {
1071                        Some(unit) => {
1072                            let ratio_unit = units.get(unit)?;
1073                            let value_per_unit = checked_mul(r, &ratio_unit.value)
1074                                .map_err(|failure| failure.to_string())?;
1075                            let bound_per_unit = ratio_unit.minimum.expect(
1076                                "BUG: RatioUnit.minimum missing after type minimum set by sync_ratio_units_from_canonical",
1077                            );
1078                            format!(
1079                                "{} {unit} is below minimum {} {unit}",
1080                                format_rational(&value_per_unit, *decimals),
1081                                format_rational(&bound_per_unit, *decimals),
1082                            )
1083                        }
1084                        None => format!(
1085                            "{} is below minimum {}",
1086                            format_rational(r, *decimals),
1087                            format_rational(type_minimum, *decimals),
1088                        ),
1089                    };
1090                    return Err(message);
1091                }
1092            }
1093            if let Some(type_maximum) = maximum {
1094                if r > type_maximum {
1095                    let message = match unit_name.as_deref() {
1096                        Some(unit) => {
1097                            let ratio_unit = units.get(unit)?;
1098                            let value_per_unit = checked_mul(r, &ratio_unit.value)
1099                                .map_err(|failure| failure.to_string())?;
1100                            let bound_per_unit = ratio_unit.maximum.expect(
1101                                "BUG: RatioUnit.maximum missing after type maximum set by sync_ratio_units_from_canonical",
1102                            );
1103                            format!(
1104                                "{} {unit} is above maximum {} {unit}",
1105                                format_rational(&value_per_unit, *decimals),
1106                                format_rational(&bound_per_unit, *decimals),
1107                            )
1108                        }
1109                        None => format!(
1110                            "{} is above maximum {}",
1111                            format_rational(r, *decimals),
1112                            format_rational(type_maximum, *decimals),
1113                        ),
1114                    };
1115                    return Err(message);
1116                }
1117            }
1118            Ok(())
1119        }
1120        (
1121            TypeSpecification::Date {
1122                minimum, maximum, ..
1123            },
1124            ValueKind::Date(dt),
1125        ) => {
1126            use crate::planning::semantics::{compare_semantic_dates, date_time_to_semantic};
1127            use std::cmp::Ordering;
1128            if let Some(min) = minimum {
1129                let min_sem = date_time_to_semantic(min);
1130                if compare_semantic_dates(dt, &min_sem) == Ordering::Less {
1131                    return Err(format!("{} is below minimum {}", dt, min));
1132                }
1133            }
1134            if let Some(max) = maximum {
1135                let max_sem = date_time_to_semantic(max);
1136                if compare_semantic_dates(dt, &max_sem) == Ordering::Greater {
1137                    return Err(format!("{} is above maximum {}", dt, max));
1138                }
1139            }
1140            Ok(())
1141        }
1142        (
1143            TypeSpecification::Time {
1144                minimum, maximum, ..
1145            },
1146            ValueKind::Time(t),
1147        ) => {
1148            use crate::planning::semantics::{compare_semantic_times, time_to_semantic};
1149            use std::cmp::Ordering;
1150            if let Some(min) = minimum {
1151                let min_sem = time_to_semantic(min);
1152                if compare_semantic_times(t, &min_sem) == Ordering::Less {
1153                    return Err(format!("{} is below minimum {}", t, min));
1154                }
1155            }
1156            if let Some(max) = maximum {
1157                let max_sem = time_to_semantic(max);
1158                if compare_semantic_times(t, &max_sem) == Ordering::Greater {
1159                    return Err(format!("{} is above maximum {}", t, max));
1160                }
1161            }
1162            Ok(())
1163        }
1164        (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
1165        | (TypeSpecification::NumberRange { .. }, ValueKind::Range(_, _))
1166        | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
1167        | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
1168        | (TypeSpecification::QuantityRange { .. }, ValueKind::Range(_, _))
1169        | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
1170        | (TypeSpecification::Veto { .. }, _)
1171        | (TypeSpecification::Undetermined, _) => Ok(()),
1172        (spec, value_kind) if !value_kind_matches_spec(value_kind, spec) => unreachable!(
1173            "BUG: validate_value_against_type called with mismatched type/value: \
1174             spec={:?}, value={:?} — typing must be enforced before validation",
1175            spec, value_kind
1176        ),
1177        (_, _) => Ok(()),
1178    }
1179}
1180
1181pub(crate) fn validate_literal_data_against_types(plan: &ExecutionPlan) -> Vec<Error> {
1182    let mut errors = Vec::new();
1183
1184    for (data_path, data_definition) in &plan.data {
1185        let (expected_type, lit) = match data_definition {
1186            DataDefinition::Value { value, .. } => (&value.lemma_type, value),
1187            DataDefinition::TypeDeclaration { .. }
1188            | DataDefinition::Import { .. }
1189            | DataDefinition::Reference { .. }
1190            | DataDefinition::Violated { .. } => continue,
1191        };
1192
1193        if let Err(msg) = validate_value_against_type(expected_type, lit) {
1194            let source = data_definition.source().clone();
1195            errors.push(Error::validation(
1196                format!(
1197                    "Invalid value for data {} (expected {}): {}",
1198                    data_path,
1199                    expected_type.name().as_str(),
1200                    msg
1201                ),
1202                Some(source),
1203                None::<String>,
1204            ));
1205        }
1206    }
1207
1208    errors
1209}
1210
1211fn collect_unit_conversion_targets(expression: &Expression, units: &mut BTreeSet<String>) {
1212    use crate::planning::semantics::{ExpressionKind, SemanticConversionTarget};
1213    match &expression.kind {
1214        ExpressionKind::UnitConversion(inner, SemanticConversionTarget::Unit { unit_name }) => {
1215            units.insert(unit_name.clone());
1216            collect_unit_conversion_targets(inner, units);
1217        }
1218        ExpressionKind::UnitConversion(inner, SemanticConversionTarget::Type(_))
1219        | ExpressionKind::LogicalNegation(inner, _)
1220        | ExpressionKind::MathematicalComputation(_, inner)
1221        | ExpressionKind::PastFutureRange(_, inner) => {
1222            collect_unit_conversion_targets(inner, units);
1223        }
1224        ExpressionKind::LogicalAnd(left, right) | ExpressionKind::LogicalOr(left, right) => {
1225            collect_unit_conversion_targets(left, units);
1226            collect_unit_conversion_targets(right, units);
1227        }
1228        ExpressionKind::Arithmetic(left, _, right)
1229        | ExpressionKind::Comparison(left, _, right)
1230        | ExpressionKind::RangeLiteral(left, right)
1231        | ExpressionKind::RangeContainment(left, right) => {
1232            collect_unit_conversion_targets(left, units);
1233            collect_unit_conversion_targets(right, units);
1234        }
1235        ExpressionKind::DateRelative(_, date_expr) => {
1236            collect_unit_conversion_targets(date_expr, units);
1237        }
1238        ExpressionKind::DateCalendar(_, _, date_expr) => {
1239            collect_unit_conversion_targets(date_expr, units);
1240        }
1241        ExpressionKind::ResultIsVeto(operand) => {
1242            collect_unit_conversion_targets(operand, units);
1243        }
1244        ExpressionKind::Literal(_)
1245        | ExpressionKind::DataPath(_)
1246        | ExpressionKind::RulePath(_)
1247        | ExpressionKind::Veto(_)
1248        | ExpressionKind::Now => {}
1249    }
1250}
1251
1252pub(crate) fn validate_unit_index_references(plan: &ExecutionPlan) -> Result<(), Error> {
1253    let mut required_units = BTreeSet::new();
1254    for rule in &plan.rules {
1255        for branch in &rule.normalized_branches {
1256            collect_unit_conversion_targets(&branch.result, &mut required_units);
1257            collect_unit_conversion_targets(&branch.condition, &mut required_units);
1258        }
1259    }
1260    for unit_name in required_units {
1261        if plan.resolved_types.unit_index.contains_key(&unit_name) {
1262            continue;
1263        }
1264        return Err(Error::validation(
1265            format!("Unknown unit '{unit_name}' in execution plan unit index."),
1266            None::<Source>,
1267            Some(plan.spec_name.clone()),
1268        ));
1269    }
1270    Ok(())
1271}
1272
1273/// The serializable form of an [`ExecutionPlan`].
1274///
1275/// `ExecutionPlan` itself is not `Serialize`/`Deserialize`: it contains derived
1276/// runtime state (`signature_index`, `resolved_types.resolved`,
1277/// `resolved_types.declared_defaults`) that is either recomputed on reconstruction
1278/// or belongs to the planning phase only. This struct is the sole canonical
1279/// representation for persistence and transport.
1280///
1281/// Convert via [`From<&ExecutionPlan>`] to serialize and [`TryFrom<ExecutionPlanSerialized>`]
1282/// to reconstruct.
1283#[derive(Debug, Clone, Serialize, Deserialize)]
1284pub struct ExecutionPlanSerialized {
1285    pub spec_name: String,
1286    #[serde(skip_serializing_if = "Option::is_none", default)]
1287    pub commentary: Option<String>,
1288    #[serde(
1289        serialize_with = "serialize_resolved_data_value_map",
1290        deserialize_with = "deserialize_resolved_data_value_map"
1291    )]
1292    pub data: IndexMap<DataPath, DataDefinition>,
1293    #[serde(default)]
1294    pub rules: Vec<ExecutableRule>,
1295    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1296    pub reference_evaluation_order: Vec<DataPath>,
1297    #[serde(default)]
1298    pub meta: HashMap<String, MetaValue>,
1299    /// Only the unit index is persisted from `resolved_types`; the rest is
1300    /// ephemeral planning state that is not needed after planning.
1301    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1302    pub unit_index: HashMap<String, Arc<LemmaType>>,
1303    pub effective: EffectiveDate,
1304    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1305    pub sources: SpecSources,
1306}
1307
1308impl From<&ExecutionPlan> for ExecutionPlanSerialized {
1309    fn from(plan: &ExecutionPlan) -> Self {
1310        Self {
1311            spec_name: plan.spec_name.clone(),
1312            commentary: plan.commentary.clone(),
1313            data: plan.data.clone(),
1314            rules: plan.rules.clone(),
1315            reference_evaluation_order: plan.reference_evaluation_order.clone(),
1316            meta: plan.meta.clone(),
1317            unit_index: plan.resolved_types.unit_index.clone(),
1318            effective: plan.effective.clone(),
1319            sources: plan.sources.clone(),
1320        }
1321    }
1322}
1323
1324impl TryFrom<ExecutionPlanSerialized> for ExecutionPlan {
1325    type Error = crate::Error;
1326
1327    fn try_from(serialized: ExecutionPlanSerialized) -> Result<Self, Self::Error> {
1328        let signature_index = crate::planning::graph::build_signature_index(
1329            &serialized.spec_name,
1330            &serialized.unit_index,
1331        )?;
1332        Ok(Self {
1333            spec_name: serialized.spec_name,
1334            commentary: serialized.commentary,
1335            data: serialized.data,
1336            rules: serialized.rules,
1337            reference_evaluation_order: serialized.reference_evaluation_order,
1338            meta: serialized.meta,
1339            resolved_types: ResolvedSpecTypes {
1340                unit_index: serialized.unit_index,
1341                ..ResolvedSpecTypes::default()
1342            },
1343            signature_index,
1344            effective: serialized.effective,
1345            sources: serialized.sources,
1346        })
1347    }
1348}
1349
1350fn serialize_resolved_data_value_map<S>(
1351    map: &IndexMap<DataPath, DataDefinition>,
1352    serializer: S,
1353) -> Result<S::Ok, S::Error>
1354where
1355    S: Serializer,
1356{
1357    let entries: Vec<(&DataPath, &DataDefinition)> = map.iter().collect();
1358    entries.serialize(serializer)
1359}
1360
1361fn deserialize_resolved_data_value_map<'de, D>(
1362    deserializer: D,
1363) -> Result<IndexMap<DataPath, DataDefinition>, D::Error>
1364where
1365    D: Deserializer<'de>,
1366{
1367    let entries: Vec<(DataPath, DataDefinition)> = Vec::deserialize(deserializer)?;
1368    Ok(entries.into_iter().collect())
1369}
1370
1371#[cfg(test)]
1372mod tests {
1373    use super::*;
1374    use crate::computation::rational::{rational_zero, RationalInteger};
1375    use crate::parsing::ast::DateTimeValue;
1376    use crate::planning::semantics::{
1377        primitive_boolean, primitive_text, DataPath, LiteralValue, PathSegment, RulePath,
1378    };
1379    use crate::Engine;
1380    use serde_json;
1381    use std::str::FromStr;
1382    use std::sync::Arc;
1383
1384    fn default_limits() -> ResourceLimits {
1385        ResourceLimits::default()
1386    }
1387
1388    fn roundtrip_execution_plan(plan: &ExecutionPlan) -> ExecutionPlan {
1389        let serialized = ExecutionPlanSerialized::from(plan);
1390        let json = serde_json::to_string(&serialized).expect("Should serialize");
1391        let back: ExecutionPlanSerialized =
1392            serde_json::from_str(&json).expect("Should deserialize");
1393        ExecutionPlan::try_from(back).expect("Should reconstruct")
1394    }
1395
1396    fn input_data(pairs: &[(&str, &str)]) -> HashMap<String, DataValueInput> {
1397        pairs
1398            .iter()
1399            .map(|(k, v)| (k.to_string(), DataValueInput::convenience(*v)))
1400            .collect()
1401    }
1402
1403    #[test]
1404    fn test_with_raw_values() {
1405        let mut engine = Engine::new();
1406        engine
1407            .load(
1408                r#"
1409                spec test
1410                data age: number -> default 25
1411                "#,
1412                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1413                    "test.lemma",
1414                ))),
1415            )
1416            .unwrap();
1417
1418        let now = DateTimeValue::now();
1419        let plan = engine.get_plan(None, "test", Some(&now)).unwrap().clone();
1420        let data_path = DataPath::new(vec![], "age".to_string());
1421
1422        let values = input_data(&[("age", "30")]);
1423
1424        let updated_plan = plan.set_data_values(values, &default_limits()).unwrap();
1425        let updated_value = updated_plan.get_data_value(&data_path).unwrap();
1426        match &updated_value.value {
1427            crate::planning::semantics::ValueKind::Number(n) => {
1428                assert_eq!(*n, RationalInteger::new(30, 1));
1429            }
1430            other => panic!("Expected number literal, got {:?}", other),
1431        }
1432    }
1433
1434    #[test]
1435    fn test_with_raw_values_type_mismatch() {
1436        let mut engine = Engine::new();
1437        engine
1438            .load(
1439                r#"
1440                spec test
1441                data age: number
1442                "#,
1443                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1444                    "test.lemma",
1445                ))),
1446            )
1447            .unwrap();
1448
1449        let now = DateTimeValue::now();
1450        let plan = engine.get_plan(None, "test", Some(&now)).unwrap().clone();
1451
1452        let values = input_data(&[("age", "thirty")]);
1453
1454        let updated = plan.set_data_values(values, &default_limits()).unwrap();
1455        let data_path = DataPath::new(vec![], "age".to_string());
1456        match updated.data.get(&data_path) {
1457            Some(crate::planning::semantics::DataDefinition::Violated { reason, .. }) => {
1458                assert!(
1459                    reason.contains("number"),
1460                    "type mismatch must record violation reason, got: {reason}"
1461                );
1462            }
1463            other => panic!("expected Violated data for age=thirty, got: {other:?}"),
1464        }
1465    }
1466
1467    #[test]
1468    fn test_with_raw_values_unknown_data() {
1469        let mut engine = Engine::new();
1470        engine
1471            .load(
1472                r#"
1473                spec test
1474                data known: number
1475                "#,
1476                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1477                    "test.lemma",
1478                ))),
1479            )
1480            .unwrap();
1481
1482        let now = DateTimeValue::now();
1483        let plan = engine.get_plan(None, "test", Some(&now)).unwrap().clone();
1484
1485        let values = input_data(&[("unknown", "30")]);
1486
1487        assert!(plan.set_data_values(values, &default_limits()).is_err());
1488    }
1489
1490    #[test]
1491    fn test_with_raw_values_nested() {
1492        let mut engine = Engine::new();
1493        engine
1494            .load(
1495                r#"
1496                spec private
1497                data base_price: number
1498
1499                spec test
1500                uses rules: private
1501                "#,
1502                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1503                    "test.lemma",
1504                ))),
1505            )
1506            .unwrap();
1507
1508        let now = DateTimeValue::now();
1509        let plan = engine.get_plan(None, "test", Some(&now)).unwrap().clone();
1510
1511        let values = input_data(&[("rules.base_price", "100")]);
1512
1513        let updated_plan = plan.set_data_values(values, &default_limits()).unwrap();
1514        let data_path = DataPath {
1515            segments: vec![PathSegment {
1516                data: "rules".to_string(),
1517                spec: "private".to_string(),
1518            }],
1519            data: "base_price".to_string(),
1520        };
1521        let updated_value = updated_plan.get_data_value(&data_path).unwrap();
1522        match &updated_value.value {
1523            crate::planning::semantics::ValueKind::Number(n) => {
1524                assert_eq!(*n, RationalInteger::new(100, 1));
1525            }
1526            other => panic!("Expected number literal, got {:?}", other),
1527        }
1528    }
1529
1530    fn test_source() -> Source {
1531        use crate::parsing::ast::Span;
1532        Source::new(
1533            crate::parsing::source::SourceType::Volatile,
1534            Span {
1535                start: 0,
1536                end: 0,
1537                line: 1,
1538                col: 0,
1539            },
1540        )
1541    }
1542
1543    fn create_literal_expr(value: LiteralValue) -> Expression {
1544        Expression::new(
1545            crate::planning::semantics::ExpressionKind::Literal(Box::new(value)),
1546            test_source(),
1547        )
1548    }
1549
1550    fn create_data_path_expr(path: DataPath) -> Expression {
1551        Expression::new(
1552            crate::planning::semantics::ExpressionKind::DataPath(path),
1553            test_source(),
1554        )
1555    }
1556
1557    fn create_number_literal(n: rust_decimal::Decimal) -> LiteralValue {
1558        LiteralValue::number_from_decimal(n)
1559    }
1560
1561    fn create_boolean_literal(b: bool) -> LiteralValue {
1562        LiteralValue::from_bool(b)
1563    }
1564
1565    fn create_text_literal(s: String) -> LiteralValue {
1566        LiteralValue::text(s)
1567    }
1568
1569    #[test]
1570    fn with_values_should_enforce_number_maximum_constraint() {
1571        // Higher-standard requirement: user input must be validated against type constraints.
1572        // If this test fails, Lemma accepts invalid values and gives false reassurance.
1573        let data_path = DataPath::new(vec![], "x".to_string());
1574
1575        let max10 = crate::planning::semantics::LemmaType::primitive(
1576            crate::planning::semantics::TypeSpecification::Number {
1577                minimum: None,
1578                maximum: Some(RationalInteger::new(10, 1)),
1579                decimals: None,
1580                help: String::new(),
1581            },
1582        );
1583        let source = Source::new(
1584            crate::parsing::source::SourceType::Volatile,
1585            crate::parsing::ast::Span {
1586                start: 0,
1587                end: 0,
1588                line: 1,
1589                col: 0,
1590            },
1591        );
1592        let mut data = IndexMap::new();
1593        data.insert(
1594            data_path.clone(),
1595            crate::planning::semantics::DataDefinition::Value {
1596                value: crate::planning::semantics::LiteralValue::number_with_type(
1597                    0.into(),
1598                    Arc::new(max10.clone()),
1599                ),
1600                source: source.clone(),
1601            },
1602        );
1603
1604        let plan = ExecutionPlan {
1605            spec_name: "test".to_string(),
1606            commentary: None,
1607            data,
1608            rules: Vec::new(),
1609            reference_evaluation_order: Vec::new(),
1610            meta: HashMap::new(),
1611            resolved_types: ResolvedSpecTypes::default(),
1612            signature_index: HashMap::new(),
1613            effective: EffectiveDate::Origin,
1614            sources: Vec::new(),
1615        };
1616
1617        let values = input_data(&[("x", "11")]);
1618
1619        let updated = plan.set_data_values(values, &default_limits()).unwrap();
1620        match updated.data.get(&data_path) {
1621            Some(crate::planning::semantics::DataDefinition::Violated { reason, .. }) => {
1622                assert!(
1623                    reason.contains("maximum") || reason.contains("10"),
1624                    "x=11 must violate maximum 10, got: {reason}"
1625                );
1626            }
1627            other => panic!("expected Violated data for x=11, got: {other:?}"),
1628        }
1629    }
1630
1631    #[test]
1632    fn with_values_should_enforce_text_enum_options() {
1633        // Higher-standard requirement: enum options must be enforced for text types.
1634        let data_path = DataPath::new(vec![], "tier".to_string());
1635
1636        let tier = crate::planning::semantics::LemmaType::primitive(
1637            crate::planning::semantics::TypeSpecification::Text {
1638                length: None,
1639                options: vec!["silver".to_string(), "gold".to_string()],
1640                help: String::new(),
1641            },
1642        );
1643        let source = Source::new(
1644            crate::parsing::source::SourceType::Volatile,
1645            crate::parsing::ast::Span {
1646                start: 0,
1647                end: 0,
1648                line: 1,
1649                col: 0,
1650            },
1651        );
1652        let mut data = IndexMap::new();
1653        data.insert(
1654            data_path.clone(),
1655            crate::planning::semantics::DataDefinition::Value {
1656                value: crate::planning::semantics::LiteralValue::text_with_type(
1657                    "silver".to_string(),
1658                    Arc::new(tier.clone()),
1659                ),
1660                source,
1661            },
1662        );
1663
1664        let plan = ExecutionPlan {
1665            spec_name: "test".to_string(),
1666            commentary: None,
1667            data,
1668            rules: Vec::new(),
1669            reference_evaluation_order: Vec::new(),
1670            meta: HashMap::new(),
1671            resolved_types: ResolvedSpecTypes::default(),
1672            signature_index: HashMap::new(),
1673            effective: EffectiveDate::Origin,
1674            sources: Vec::new(),
1675        };
1676
1677        let values = input_data(&[("tier", "platinum")]);
1678
1679        let updated = plan.set_data_values(values, &default_limits()).unwrap();
1680        match updated.data.get(&data_path) {
1681            Some(crate::planning::semantics::DataDefinition::Violated { reason, .. }) => {
1682                assert!(
1683                    reason.contains("allowed options") || reason.contains("platinum"),
1684                    "invalid enum must record violation, got: {reason}"
1685                );
1686            }
1687            other => panic!("expected Violated data for tier=platinum, got: {other:?}"),
1688        }
1689    }
1690
1691    #[test]
1692    fn with_values_should_enforce_quantity_decimals() {
1693        // Higher-standard requirement: decimals should be enforced on quantity inputs,
1694        // unless the language explicitly defines rounding semantics.
1695        let data_path = DataPath::new(vec![], "price".to_string());
1696
1697        let money = crate::planning::semantics::LemmaType::primitive(
1698            crate::planning::semantics::TypeSpecification::Quantity {
1699                minimum: None,
1700                maximum: None,
1701                decimals: Some(2),
1702                units: crate::planning::semantics::QuantityUnits::from(vec![
1703                    crate::planning::semantics::QuantityUnit::from_decimal_factor(
1704                        "eur".to_string(),
1705                        rust_decimal::Decimal::from_str("1.0").unwrap(),
1706                        Vec::new(),
1707                    )
1708                    .expect("eur unit factor must be exact decimal"),
1709                ]),
1710                traits: Vec::new(),
1711                decomposition: None,
1712                help: String::new(),
1713            },
1714        );
1715        let source = Source::new(
1716            crate::parsing::source::SourceType::Volatile,
1717            crate::parsing::ast::Span {
1718                start: 0,
1719                end: 0,
1720                line: 1,
1721                col: 0,
1722            },
1723        );
1724        let mut data = IndexMap::new();
1725        data.insert(
1726            data_path.clone(),
1727            crate::planning::semantics::DataDefinition::Value {
1728                value: crate::planning::semantics::LiteralValue::quantity_with_type(
1729                    rational_zero(),
1730                    "eur".to_string(),
1731                    Arc::new(money.clone()),
1732                ),
1733                source,
1734            },
1735        );
1736
1737        let plan = ExecutionPlan {
1738            spec_name: "test".to_string(),
1739            commentary: None,
1740            data,
1741            rules: Vec::new(),
1742            reference_evaluation_order: Vec::new(),
1743            meta: HashMap::new(),
1744            resolved_types: ResolvedSpecTypes::default(),
1745            signature_index: HashMap::new(),
1746            effective: EffectiveDate::Origin,
1747            sources: Vec::new(),
1748        };
1749
1750        let values = input_data(&[("price", "1.234 eur")]);
1751
1752        let updated = plan.set_data_values(values, &default_limits()).unwrap();
1753        match updated.data.get(&data_path) {
1754            Some(crate::planning::semantics::DataDefinition::Violated { reason, .. }) => {
1755                assert!(
1756                    reason.contains("decimals") || reason.contains("decimal"),
1757                    "1.234 eur must violate decimals=2, got: {reason}"
1758                );
1759            }
1760            other => panic!("expected Violated data for price=1.234 eur, got: {other:?}"),
1761        }
1762    }
1763
1764    #[test]
1765    fn test_serialize_deserialize_execution_plan() {
1766        let data_path = DataPath {
1767            segments: vec![],
1768            data: "age".to_string(),
1769        };
1770        let mut data = IndexMap::new();
1771        data.insert(
1772            data_path.clone(),
1773            crate::planning::semantics::DataDefinition::Value {
1774                value: create_number_literal(0.into()),
1775                source: test_source(),
1776            },
1777        );
1778        let plan = ExecutionPlan {
1779            spec_name: "test".to_string(),
1780            commentary: None,
1781            data,
1782            rules: Vec::new(),
1783            reference_evaluation_order: Vec::new(),
1784            meta: HashMap::new(),
1785            resolved_types: ResolvedSpecTypes::default(),
1786            signature_index: HashMap::new(),
1787            effective: EffectiveDate::Origin,
1788            sources: Vec::new(),
1789        };
1790
1791        let deserialized = roundtrip_execution_plan(&plan);
1792
1793        assert_eq!(deserialized.spec_name, plan.spec_name);
1794        assert_eq!(deserialized.data.len(), plan.data.len());
1795        assert_eq!(deserialized.rules.len(), plan.rules.len());
1796    }
1797
1798    #[test]
1799    fn test_serialize_deserialize_plan_with_imported_named_type_defining_spec() {
1800        let dep_spec = Arc::new(crate::parsing::ast::LemmaSpec::new("examples".to_string()));
1801        let imported_type = crate::planning::semantics::LemmaType::new(
1802            "salary".to_string(),
1803            TypeSpecification::quantity(),
1804            crate::planning::semantics::TypeExtends::Custom {
1805                parent: "money".to_string(),
1806                family: "money".to_string(),
1807                defining_spec: crate::planning::semantics::TypeDefiningSpec::Import {
1808                    spec: Arc::clone(&dep_spec),
1809                },
1810            },
1811        );
1812
1813        let salary_path = DataPath::new(vec![], "salary".to_string());
1814        let mut data = IndexMap::new();
1815        data.insert(
1816            salary_path,
1817            crate::planning::semantics::DataDefinition::TypeDeclaration {
1818                resolved_type: Arc::new(imported_type),
1819                declared_default: None,
1820                source: test_source(),
1821            },
1822        );
1823
1824        let plan = ExecutionPlan {
1825            spec_name: "test".to_string(),
1826            commentary: None,
1827            data,
1828            rules: Vec::new(),
1829            reference_evaluation_order: Vec::new(),
1830            meta: HashMap::new(),
1831            resolved_types: ResolvedSpecTypes::default(),
1832            signature_index: HashMap::new(),
1833            effective: EffectiveDate::Origin,
1834            sources: Vec::new(),
1835        };
1836
1837        let deserialized = roundtrip_execution_plan(&plan);
1838
1839        let recovered = deserialized
1840            .data
1841            .get(&DataPath::new(vec![], "salary".to_string()))
1842            .and_then(|d| d.schema_type())
1843            .expect("salary type should be present in plan.data");
1844        match &recovered.extends {
1845            crate::planning::semantics::TypeExtends::Custom {
1846                defining_spec: crate::planning::semantics::TypeDefiningSpec::Import { spec },
1847                ..
1848            } => {
1849                assert_eq!(spec.name, "examples");
1850            }
1851            other => panic!(
1852                "Expected imported defining_spec after round-trip, got {:?}",
1853                other
1854            ),
1855        }
1856    }
1857
1858    #[test]
1859    fn test_serialize_deserialize_plan_with_rules() {
1860        use crate::planning::semantics::ExpressionKind;
1861
1862        let age_path = DataPath::new(vec![], "age".to_string());
1863        let mut data = IndexMap::new();
1864        data.insert(
1865            age_path.clone(),
1866            crate::planning::semantics::DataDefinition::Value {
1867                value: create_number_literal(0.into()),
1868                source: test_source(),
1869            },
1870        );
1871        let mut plan = ExecutionPlan {
1872            spec_name: "test".to_string(),
1873            commentary: None,
1874            data,
1875            rules: Vec::new(),
1876            reference_evaluation_order: Vec::new(),
1877            meta: HashMap::new(),
1878            resolved_types: ResolvedSpecTypes::default(),
1879            signature_index: HashMap::new(),
1880            effective: EffectiveDate::Origin,
1881            sources: Vec::new(),
1882        };
1883
1884        let rule = ExecutableRule {
1885            path: RulePath::new(vec![], "can_drive".to_string()),
1886            name: "can_drive".to_string(),
1887            branches: vec![{
1888                let result = create_literal_expr(create_boolean_literal(true));
1889                let condition = Expression::new(
1890                    ExpressionKind::Comparison(
1891                        Arc::new(create_data_path_expr(age_path.clone())),
1892                        crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1893                        Arc::new(create_literal_expr(create_number_literal(18.into()))),
1894                    ),
1895                    test_source(),
1896                );
1897                Branch {
1898                    condition: Some(condition.clone()),
1899                    result: result.clone(),
1900                    source: test_source(),
1901                }
1902            }],
1903            normalized_branches: vec![{
1904                let result = create_literal_expr(create_boolean_literal(true));
1905                let condition = Expression::new(
1906                    ExpressionKind::Comparison(
1907                        Arc::new(create_data_path_expr(age_path.clone())),
1908                        crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1909                        Arc::new(create_literal_expr(create_number_literal(18.into()))),
1910                    ),
1911                    test_source(),
1912                );
1913                NormalizedBranch { condition, result }
1914            }],
1915            needs_data: BTreeSet::from([age_path]),
1916            source: test_source(),
1917            rule_type: Arc::new(primitive_boolean().clone()),
1918        };
1919
1920        plan.rules.push(rule);
1921
1922        let deserialized = roundtrip_execution_plan(&plan);
1923
1924        assert_eq!(deserialized.spec_name, plan.spec_name);
1925        assert_eq!(deserialized.data.len(), plan.data.len());
1926        assert_eq!(deserialized.rules.len(), plan.rules.len());
1927        assert_eq!(deserialized.rules[0].name, "can_drive");
1928        assert_eq!(deserialized.rules[0].branches.len(), 1);
1929        assert_eq!(deserialized.rules[0].needs_data.len(), 1);
1930    }
1931
1932    #[test]
1933    fn test_serialize_deserialize_plan_with_nested_data_paths() {
1934        use crate::planning::semantics::PathSegment;
1935        let data_path = DataPath {
1936            segments: vec![PathSegment {
1937                data: "employee".to_string(),
1938                spec: "private".to_string(),
1939            }],
1940            data: "salary".to_string(),
1941        };
1942
1943        let mut data = IndexMap::new();
1944        data.insert(
1945            data_path.clone(),
1946            crate::planning::semantics::DataDefinition::Value {
1947                value: create_number_literal(0.into()),
1948                source: test_source(),
1949            },
1950        );
1951        let plan = ExecutionPlan {
1952            spec_name: "test".to_string(),
1953            commentary: None,
1954            data,
1955            rules: Vec::new(),
1956            reference_evaluation_order: Vec::new(),
1957            meta: HashMap::new(),
1958            resolved_types: ResolvedSpecTypes::default(),
1959            signature_index: HashMap::new(),
1960            effective: EffectiveDate::Origin,
1961            sources: Vec::new(),
1962        };
1963
1964        let deserialized = roundtrip_execution_plan(&plan);
1965
1966        assert_eq!(deserialized.data.len(), 1);
1967        let (deserialized_path, _) = deserialized.data.iter().next().unwrap();
1968        assert_eq!(deserialized_path.segments.len(), 1);
1969        assert_eq!(deserialized_path.segments[0].data, "employee");
1970        assert_eq!(deserialized_path.data, "salary");
1971    }
1972
1973    #[test]
1974    fn test_serialize_deserialize_plan_with_multiple_data_types() {
1975        let name_path = DataPath::new(vec![], "name".to_string());
1976        let age_path = DataPath::new(vec![], "age".to_string());
1977        let active_path = DataPath::new(vec![], "active".to_string());
1978
1979        let mut data = IndexMap::new();
1980        data.insert(
1981            name_path.clone(),
1982            crate::planning::semantics::DataDefinition::Value {
1983                value: create_text_literal("Alice".to_string()),
1984                source: test_source(),
1985            },
1986        );
1987        data.insert(
1988            age_path.clone(),
1989            crate::planning::semantics::DataDefinition::Value {
1990                value: create_number_literal(30.into()),
1991                source: test_source(),
1992            },
1993        );
1994        data.insert(
1995            active_path.clone(),
1996            crate::planning::semantics::DataDefinition::Value {
1997                value: create_boolean_literal(true),
1998                source: test_source(),
1999            },
2000        );
2001
2002        let plan = ExecutionPlan {
2003            spec_name: "test".to_string(),
2004            commentary: None,
2005            data,
2006            rules: Vec::new(),
2007            reference_evaluation_order: Vec::new(),
2008            meta: HashMap::new(),
2009            resolved_types: ResolvedSpecTypes::default(),
2010            signature_index: HashMap::new(),
2011            effective: EffectiveDate::Origin,
2012            sources: Vec::new(),
2013        };
2014
2015        let deserialized = roundtrip_execution_plan(&plan);
2016
2017        assert_eq!(deserialized.data.len(), 3);
2018
2019        assert_eq!(
2020            deserialized.get_data_value(&name_path).unwrap().value,
2021            crate::planning::semantics::ValueKind::Text("Alice".to_string())
2022        );
2023        assert_eq!(
2024            deserialized.get_data_value(&age_path).unwrap().value,
2025            crate::planning::semantics::ValueKind::Number(30.into())
2026        );
2027        assert_eq!(
2028            deserialized.get_data_value(&active_path).unwrap().value,
2029            crate::planning::semantics::ValueKind::Boolean(true)
2030        );
2031    }
2032
2033    #[test]
2034    fn test_serialize_deserialize_plan_with_multiple_branches() {
2035        use crate::planning::semantics::ExpressionKind;
2036
2037        let points_path = DataPath::new(vec![], "points".to_string());
2038        let mut data = IndexMap::new();
2039        data.insert(
2040            points_path.clone(),
2041            crate::planning::semantics::DataDefinition::Value {
2042                value: create_number_literal(0.into()),
2043                source: test_source(),
2044            },
2045        );
2046        let mut plan = ExecutionPlan {
2047            spec_name: "test".to_string(),
2048            commentary: None,
2049            data,
2050            rules: Vec::new(),
2051            reference_evaluation_order: Vec::new(),
2052            meta: HashMap::new(),
2053            resolved_types: ResolvedSpecTypes::default(),
2054            signature_index: HashMap::new(),
2055            effective: EffectiveDate::Origin,
2056            sources: Vec::new(),
2057        };
2058
2059        let rule = ExecutableRule {
2060            path: RulePath::new(vec![], "tier".to_string()),
2061            name: "tier".to_string(),
2062            branches: vec![
2063                {
2064                    let result = create_literal_expr(create_text_literal("bronze".to_string()));
2065                    Branch {
2066                        condition: None,
2067                        result: result.clone(),
2068                        source: test_source(),
2069                    }
2070                },
2071                {
2072                    let result = create_literal_expr(create_text_literal("silver".to_string()));
2073                    Branch {
2074                        condition: Some(Expression::new(
2075                            ExpressionKind::Comparison(
2076                                Arc::new(create_data_path_expr(points_path.clone())),
2077                                crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2078                                Arc::new(create_literal_expr(create_number_literal(100.into()))),
2079                            ),
2080                            test_source(),
2081                        )),
2082                        result: result.clone(),
2083                        source: test_source(),
2084                    }
2085                },
2086                {
2087                    let result = create_literal_expr(create_text_literal("gold".to_string()));
2088                    Branch {
2089                        condition: Some(Expression::new(
2090                            ExpressionKind::Comparison(
2091                                Arc::new(create_data_path_expr(points_path.clone())),
2092                                crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2093                                Arc::new(create_literal_expr(create_number_literal(500.into()))),
2094                            ),
2095                            test_source(),
2096                        )),
2097                        result: result.clone(),
2098                        source: test_source(),
2099                    }
2100                },
2101            ],
2102            normalized_branches: vec![
2103                NormalizedBranch {
2104                    condition: create_literal_expr(create_boolean_literal(true)),
2105                    result: create_literal_expr(create_text_literal("bronze".to_string())),
2106                },
2107                NormalizedBranch {
2108                    condition: create_literal_expr(create_boolean_literal(true)),
2109                    result: create_literal_expr(create_text_literal("silver".to_string())),
2110                },
2111                NormalizedBranch {
2112                    condition: create_literal_expr(create_boolean_literal(true)),
2113                    result: create_literal_expr(create_text_literal("gold".to_string())),
2114                },
2115            ],
2116            needs_data: BTreeSet::from([points_path]),
2117            source: test_source(),
2118            rule_type: Arc::new(primitive_text().clone()),
2119        };
2120
2121        plan.rules.push(rule);
2122
2123        let deserialized = roundtrip_execution_plan(&plan);
2124
2125        assert_eq!(deserialized.rules.len(), 1);
2126        assert_eq!(deserialized.rules[0].branches.len(), 3);
2127        assert!(deserialized.rules[0].branches[0].condition.is_none());
2128        assert!(deserialized.rules[0].branches[1].condition.is_some());
2129        assert!(deserialized.rules[0].branches[2].condition.is_some());
2130    }
2131
2132    #[test]
2133    fn test_serialize_deserialize_empty_plan() {
2134        let plan = ExecutionPlan {
2135            spec_name: "empty".to_string(),
2136            commentary: None,
2137            data: IndexMap::new(),
2138            rules: Vec::new(),
2139            reference_evaluation_order: Vec::new(),
2140            meta: HashMap::new(),
2141            resolved_types: ResolvedSpecTypes::default(),
2142            signature_index: HashMap::new(),
2143            effective: EffectiveDate::Origin,
2144            sources: Vec::new(),
2145        };
2146
2147        let deserialized = roundtrip_execution_plan(&plan);
2148
2149        assert_eq!(deserialized.spec_name, "empty");
2150        assert_eq!(deserialized.data.len(), 0);
2151        assert_eq!(deserialized.rules.len(), 0);
2152    }
2153
2154    #[test]
2155    fn test_serialize_deserialize_plan_with_arithmetic_expressions() {
2156        use crate::planning::semantics::ExpressionKind;
2157
2158        let x_path = DataPath::new(vec![], "x".to_string());
2159        let mut data = IndexMap::new();
2160        data.insert(
2161            x_path.clone(),
2162            crate::planning::semantics::DataDefinition::Value {
2163                value: create_number_literal(0.into()),
2164                source: test_source(),
2165            },
2166        );
2167        let mut plan = ExecutionPlan {
2168            spec_name: "test".to_string(),
2169            commentary: None,
2170            data,
2171            rules: Vec::new(),
2172            reference_evaluation_order: Vec::new(),
2173            meta: HashMap::new(),
2174            resolved_types: ResolvedSpecTypes::default(),
2175            signature_index: HashMap::new(),
2176            effective: EffectiveDate::Origin,
2177            sources: Vec::new(),
2178        };
2179
2180        let rule = ExecutableRule {
2181            path: RulePath::new(vec![], "doubled".to_string()),
2182            name: "doubled".to_string(),
2183            branches: vec![{
2184                let result = Expression::new(
2185                    ExpressionKind::Arithmetic(
2186                        Arc::new(create_data_path_expr(x_path.clone())),
2187                        crate::parsing::ast::ArithmeticComputation::Multiply,
2188                        Arc::new(create_literal_expr(create_number_literal(2.into()))),
2189                    ),
2190                    test_source(),
2191                );
2192                Branch {
2193                    condition: None,
2194                    result: result.clone(),
2195                    source: test_source(),
2196                }
2197            }],
2198            normalized_branches: vec![{
2199                let result = Expression::new(
2200                    ExpressionKind::Arithmetic(
2201                        Arc::new(create_data_path_expr(x_path.clone())),
2202                        crate::parsing::ast::ArithmeticComputation::Multiply,
2203                        Arc::new(create_literal_expr(create_number_literal(2.into()))),
2204                    ),
2205                    test_source(),
2206                );
2207                NormalizedBranch {
2208                    condition: create_literal_expr(create_boolean_literal(true)),
2209                    result,
2210                }
2211            }],
2212            needs_data: BTreeSet::from([x_path]),
2213            source: test_source(),
2214            rule_type: Arc::new(crate::planning::semantics::primitive_number().clone()),
2215        };
2216
2217        plan.rules.push(rule);
2218
2219        let deserialized = roundtrip_execution_plan(&plan);
2220
2221        assert_eq!(deserialized.rules.len(), 1);
2222        match &deserialized.rules[0].branches[0].result.kind {
2223            ExpressionKind::Arithmetic(left, op, right) => {
2224                assert_eq!(*op, crate::parsing::ast::ArithmeticComputation::Multiply);
2225                match &left.kind {
2226                    ExpressionKind::DataPath(_) => {}
2227                    _ => panic!("Expected DataPath in left operand"),
2228                }
2229                match &right.kind {
2230                    ExpressionKind::Literal(_) => {}
2231                    _ => panic!("Expected Literal in right operand"),
2232                }
2233            }
2234            _ => panic!("Expected Arithmetic expression"),
2235        }
2236    }
2237
2238    #[test]
2239    fn test_serialize_deserialize_round_trip_equality() {
2240        use crate::planning::semantics::ExpressionKind;
2241
2242        let age_path = DataPath::new(vec![], "age".to_string());
2243        let mut data = IndexMap::new();
2244        data.insert(
2245            age_path.clone(),
2246            crate::planning::semantics::DataDefinition::Value {
2247                value: create_number_literal(0.into()),
2248                source: test_source(),
2249            },
2250        );
2251        let mut plan = ExecutionPlan {
2252            spec_name: "test".to_string(),
2253            commentary: None,
2254            data,
2255            rules: Vec::new(),
2256            reference_evaluation_order: Vec::new(),
2257            meta: HashMap::new(),
2258            resolved_types: ResolvedSpecTypes::default(),
2259            signature_index: HashMap::new(),
2260            effective: EffectiveDate::Origin,
2261            sources: Vec::new(),
2262        };
2263
2264        let rule = ExecutableRule {
2265            path: RulePath::new(vec![], "is_adult".to_string()),
2266            name: "is_adult".to_string(),
2267            branches: vec![{
2268                let result = create_literal_expr(create_boolean_literal(true));
2269                let condition = Expression::new(
2270                    ExpressionKind::Comparison(
2271                        Arc::new(create_data_path_expr(age_path.clone())),
2272                        crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2273                        Arc::new(create_literal_expr(create_number_literal(18.into()))),
2274                    ),
2275                    test_source(),
2276                );
2277                Branch {
2278                    condition: Some(condition.clone()),
2279                    result: result.clone(),
2280                    source: test_source(),
2281                }
2282            }],
2283            normalized_branches: vec![{
2284                let result = create_literal_expr(create_boolean_literal(true));
2285                let condition = Expression::new(
2286                    ExpressionKind::Comparison(
2287                        Arc::new(create_data_path_expr(age_path.clone())),
2288                        crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2289                        Arc::new(create_literal_expr(create_number_literal(18.into()))),
2290                    ),
2291                    test_source(),
2292                );
2293                NormalizedBranch { condition, result }
2294            }],
2295            needs_data: BTreeSet::from([age_path]),
2296            source: test_source(),
2297            rule_type: Arc::new(primitive_boolean().clone()),
2298        };
2299
2300        plan.rules.push(rule);
2301
2302        let deserialized = roundtrip_execution_plan(&plan);
2303        let deserialized2 = roundtrip_execution_plan(&deserialized);
2304
2305        assert_eq!(deserialized2.spec_name, plan.spec_name);
2306        assert_eq!(deserialized2.data.len(), plan.data.len());
2307        assert_eq!(deserialized2.rules.len(), plan.rules.len());
2308        assert_eq!(deserialized2.rules[0].name, plan.rules[0].name);
2309        assert_eq!(
2310            deserialized2.rules[0].branches.len(),
2311            plan.rules[0].branches.len()
2312        );
2313    }
2314
2315    fn empty_plan(effective: crate::parsing::ast::EffectiveDate) -> ExecutionPlan {
2316        ExecutionPlan {
2317            spec_name: "s".into(),
2318            commentary: None,
2319            data: IndexMap::new(),
2320            rules: Vec::new(),
2321            reference_evaluation_order: Vec::new(),
2322            meta: HashMap::new(),
2323            resolved_types: ResolvedSpecTypes::default(),
2324            signature_index: HashMap::new(),
2325            effective,
2326            sources: Vec::new(),
2327        }
2328    }
2329
2330    #[test]
2331    fn plan_at_exact_boundary_selects_later_slice() {
2332        use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2333
2334        let june = DateTimeValue {
2335            year: 2025,
2336            month: 6,
2337            day: 1,
2338            hour: 0,
2339            minute: 0,
2340            second: 0,
2341            microsecond: 0,
2342            timezone: None,
2343        };
2344        let dec = DateTimeValue {
2345            year: 2025,
2346            month: 12,
2347            day: 1,
2348            hour: 0,
2349            minute: 0,
2350            second: 0,
2351            microsecond: 0,
2352            timezone: None,
2353        };
2354
2355        let set = ExecutionPlanSet {
2356            spec_name: "s".into(),
2357            plans: vec![
2358                empty_plan(EffectiveDate::Origin),
2359                empty_plan(EffectiveDate::DateTimeValue(june.clone())),
2360                empty_plan(EffectiveDate::DateTimeValue(dec.clone())),
2361            ],
2362        };
2363
2364        assert!(std::ptr::eq(
2365            set.plan_at(&EffectiveDate::DateTimeValue(june.clone()))
2366                .expect("boundary instant"),
2367            &set.plans[1]
2368        ));
2369        assert!(std::ptr::eq(
2370            set.plan_at(&EffectiveDate::DateTimeValue(dec.clone()))
2371                .expect("dec boundary"),
2372            &set.plans[2]
2373        ));
2374    }
2375
2376    #[test]
2377    fn plan_at_day_before_boundary_stays_in_earlier_slice() {
2378        use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2379
2380        let june = DateTimeValue {
2381            year: 2025,
2382            month: 6,
2383            day: 1,
2384            hour: 0,
2385            minute: 0,
2386            second: 0,
2387            microsecond: 0,
2388            timezone: None,
2389        };
2390        let may_end = DateTimeValue {
2391            year: 2025,
2392            month: 5,
2393            day: 31,
2394            hour: 23,
2395            minute: 59,
2396            second: 59,
2397            microsecond: 0,
2398            timezone: None,
2399        };
2400
2401        let set = ExecutionPlanSet {
2402            spec_name: "s".into(),
2403            plans: vec![
2404                empty_plan(EffectiveDate::Origin),
2405                empty_plan(EffectiveDate::DateTimeValue(june)),
2406            ],
2407        };
2408
2409        assert!(std::ptr::eq(
2410            set.plan_at(&EffectiveDate::DateTimeValue(may_end))
2411                .expect("may 31"),
2412            &set.plans[0]
2413        ));
2414    }
2415
2416    #[test]
2417    fn plan_at_single_plan_matches_any_instant_after_start() {
2418        use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2419
2420        let t = DateTimeValue {
2421            year: 2025,
2422            month: 3,
2423            day: 1,
2424            hour: 0,
2425            minute: 0,
2426            second: 0,
2427            microsecond: 0,
2428            timezone: None,
2429        };
2430        let set = ExecutionPlanSet {
2431            spec_name: "s".into(),
2432            plans: vec![empty_plan(EffectiveDate::DateTimeValue(DateTimeValue {
2433                year: 2025,
2434                month: 1,
2435                day: 1,
2436                hour: 0,
2437                minute: 0,
2438                second: 0,
2439                microsecond: 0,
2440                timezone: None,
2441            }))],
2442        };
2443        assert!(std::ptr::eq(
2444            set.plan_at(&EffectiveDate::DateTimeValue(t))
2445                .expect("inside single slice"),
2446            &set.plans[0]
2447        ));
2448    }
2449
2450    /// The schema JSON shape is the IO contract for every non-Rust consumer
2451    /// (WASM playground, Hex, HTTP, TypeScript). Nail the exact envelope.
2452    #[test]
2453    fn schema_json_shape_contract() {
2454        let mut engine = Engine::new();
2455        engine
2456            .load(
2457                r#"
2458                spec pricing
2459                data bridge_height: quantity
2460                  -> unit meter 1
2461                  -> default 100 meter
2462                data quantity: number -> minimum 0
2463                rule cost: bridge_height * quantity
2464                "#,
2465                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2466                    "test.lemma",
2467                ))),
2468            )
2469            .unwrap();
2470        let now = DateTimeValue::now();
2471        let schema = engine
2472            .get_plan(None, "pricing", Some(&now))
2473            .unwrap()
2474            .schema();
2475
2476        let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
2477
2478        let bh = &value["data"]["bridge_height"];
2479        assert!(
2480            bh.is_object(),
2481            "data entry must be a named object, not tuple"
2482        );
2483        assert!(
2484            bh.get("type").is_some(),
2485            "data entry must expose `type` field"
2486        );
2487        assert!(
2488            bh.get("default").is_some(),
2489            "bridge_height exposes `-> default` as schema default suggestion"
2490        );
2491        assert!(
2492            bh.get("bound_value").is_none(),
2493            "bridge_height is not a spec-bound literal"
2494        );
2495
2496        let ty = &bh["type"];
2497        assert_eq!(
2498            ty["kind"], "quantity",
2499            "kind tag sits on the type object itself"
2500        );
2501        assert!(
2502            ty["units"].is_array(),
2503            "quantity-only fields flatten up to top level"
2504        );
2505        assert!(
2506            ty.get("options").is_none(),
2507            "text-only fields must not leak"
2508        );
2509
2510        let qty = &value["data"]["quantity"];
2511        assert_eq!(qty["type"]["kind"], "number");
2512        assert!(
2513            qty.get("default").is_none(),
2514            "quantity has no default suggestion"
2515        );
2516        assert!(
2517            qty.get("bound_value").is_none(),
2518            "quantity has no bound literal"
2519        );
2520
2521        let cost = &value["rules"]["cost"];
2522        assert_eq!(
2523            cost["kind"], "quantity",
2524            "rule types use the same flat shape"
2525        );
2526        assert!(
2527            cost["units"].is_array() && !cost["units"].as_array().unwrap().is_empty(),
2528            "quantity rule result types expose declared units"
2529        );
2530        assert!(
2531            cost["units"][0].get("factor").is_some(),
2532            "quantity rule units use factor field"
2533        );
2534    }
2535
2536    #[test]
2537    fn schema_rule_result_units_contract() {
2538        let mut engine = Engine::new();
2539        engine
2540            .load(
2541                r#"
2542                spec units_contract
2543                data money: quantity
2544                  -> unit eur 1
2545                  -> unit usd 0.91
2546                data rate: ratio
2547                  -> unit basis_points 10000
2548                  -> unit percent 100
2549                  -> default 500 basis_points
2550                rule total: money
2551                rule rate_out: rate
2552                "#,
2553                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2554                    "units_contract.lemma",
2555                ))),
2556            )
2557            .unwrap();
2558        let now = DateTimeValue::now();
2559        let schema = engine
2560            .get_plan(None, "units_contract", Some(&now))
2561            .unwrap()
2562            .schema();
2563        let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
2564
2565        let money_units = &value["data"]["money"]["type"]["units"];
2566        assert!(money_units.is_array() && !money_units.as_array().unwrap().is_empty());
2567        assert!(money_units[0].get("name").is_some());
2568        assert!(money_units[0].get("factor").is_some());
2569        assert!(money_units[0]["factor"].get("numer").is_some());
2570        assert!(money_units[0]["factor"].get("denom").is_some());
2571
2572        let rate_units = &value["data"]["rate"]["type"]["units"];
2573        assert!(rate_units.is_array() && !rate_units.as_array().unwrap().is_empty());
2574        assert!(rate_units[0].get("name").is_some());
2575        assert!(rate_units[0].get("value").is_some());
2576        assert!(rate_units[0]["value"].get("numer").is_some());
2577        assert!(rate_units[0]["value"].get("denom").is_some());
2578
2579        let total_rule_units = &value["rules"]["total"]["units"];
2580        let money_unit_names: Vec<_> = money_units
2581            .as_array()
2582            .unwrap()
2583            .iter()
2584            .map(|u| u["name"].as_str().unwrap())
2585            .collect();
2586        let total_rule_unit_names: Vec<_> = total_rule_units
2587            .as_array()
2588            .unwrap()
2589            .iter()
2590            .map(|u| u["name"].as_str().unwrap())
2591            .collect();
2592        assert_eq!(total_rule_unit_names, money_unit_names);
2593
2594        let rate_out_rule_units = &value["rules"]["rate_out"]["units"];
2595        let rate_unit_names: Vec<_> = rate_units
2596            .as_array()
2597            .unwrap()
2598            .iter()
2599            .map(|u| u["name"].as_str().unwrap())
2600            .collect();
2601        let rate_out_rule_unit_names: Vec<_> = rate_out_rule_units
2602            .as_array()
2603            .unwrap()
2604            .iter()
2605            .map(|u| u["name"].as_str().unwrap())
2606            .collect();
2607        assert_eq!(rate_out_rule_unit_names, rate_unit_names);
2608    }
2609
2610    #[test]
2611    fn schema_json_round_trip_preserves_shape() {
2612        let mut engine = Engine::new();
2613        engine
2614            .load(
2615                r#"
2616                spec s
2617                data age: number -> minimum 0 -> default 18
2618                data grade: text -> options "A" "B" "C"
2619                rule adult: age >= 18
2620                "#,
2621                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("s.lemma"))),
2622            )
2623            .unwrap();
2624        let now = DateTimeValue::now();
2625        let schema = engine.get_plan(None, "s", Some(&now)).unwrap().schema();
2626
2627        let json = serde_json::to_string(&schema).unwrap();
2628        let round_tripped: SpecSchema = serde_json::from_str(&json).unwrap();
2629        assert_eq!(schema, round_tripped);
2630    }
2631}
2632
2633// ---------------------------------------------------------------------------
2634// ExecutionPlanSet (formerly plan_set.rs)
2635// ---------------------------------------------------------------------------