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::{CalendarPeriodUnit, DateCalendarKind, DateRelativeKind};
13use crate::parsing::ast::{DateTimeValue, EffectiveDate, LemmaRepository, LemmaSpec, MetaValue};
14use crate::parsing::source::Source;
15use crate::planning::data_input::{parse_data_value, DataValueInput};
16use crate::planning::graph::Graph;
17use crate::planning::graph::ResolvedSpecTypes;
18use crate::planning::normalize::{build_normalized_rule_instructions, CompiledRule};
19use crate::planning::semantics::{
20    value_kind_matches_spec, ArithmeticComputation, ComparisonComputation, DataDefinition,
21    DataPath, Expression, LemmaType, LiteralValue, MathematicalComputation, ReferenceTarget,
22    RulePath, SemanticConversionTarget, TypeSpecification, ValueKind,
23};
24use crate::Error;
25use crate::ResourceLimits;
26use indexmap::IndexMap;
27use serde::{Deserialize, Deserializer, Serialize, Serializer};
28use std::collections::{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    /// Maximum register file size across all rules; sizes [`EvaluationContext::register_values`].
67    pub max_register_count: u16,
68
69    /// Order in which [`DataDefinition::Reference`] entries must be resolved
70    /// at evaluation time so that chained references (reference → reference →
71    /// data) copy values in the correct sequence. Empty when the plan has no
72    /// references.
73    pub reference_evaluation_order: Vec<DataPath>,
74
75    /// Spec metadata
76    pub meta: HashMap<String, MetaValue>,
77
78    /// Main-spec types from planning. [`ResolvedSpecTypes::unit_index`] is expression-scope
79    /// units (local types plus direct `uses` imports). Rule-result units live on each
80    /// [`ExecutableRule::rule_type`], not in this index.
81    pub resolved_types: ResolvedSpecTypes,
82
83    /// Reverse index: canonical-form unit signature `Vec<(unit_name, exponent)>` →
84    /// (unit_name, owning type). Built from expression-scope units during planning so
85    /// cross-type Multiply/Divide arithmetic can deterministically resolve a combined
86    /// signature back to a single named unit. Ambiguous signatures (the same key matched
87    /// by units in two distinct types) are rejected at planning time.
88    pub signature_index: crate::computation::arithmetic::SignatureIndex,
89
90    pub effective: EffectiveDate,
91
92    /// Canonical source for all specs in this plan (one entry per spec, includes repository).
93    /// Reconstructed from AST — not raw file content.
94    pub sources: SpecSources,
95}
96
97/// All [`ExecutionPlan`]s for a spec name after dependency resolution.
98/// Ordered by [`ExecutionPlan::effective`]. Slice end is derived from the next plan's `effective`.
99#[derive(Debug, Clone)]
100pub struct ExecutionPlanSet {
101    pub spec_name: String,
102    pub plans: Vec<ExecutionPlan>,
103}
104
105impl ExecutionPlanSet {
106    /// Plan covering `[effective[i], effective[i+1])` (half-open).
107    #[must_use]
108    pub fn plan_at(&self, effective: &EffectiveDate) -> Option<&ExecutionPlan> {
109        for (i, plan) in self.plans.iter().enumerate() {
110            let from_ok = *effective >= plan.effective;
111            let to_ok = self
112                .plans
113                .get(i + 1)
114                .map(|next| *effective < next.effective)
115                .unwrap_or(true);
116            if from_ok && to_ok {
117                return Some(plan);
118            }
119        }
120        None
121    }
122}
123
124pub const INSTRUCTIONS_VERSION: u32 = 2;
125
126/// How [`Instruction::JumpIfFalse`] treats a vetoed condition register.
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
128#[serde(rename_all = "snake_case")]
129pub enum JumpVetoSemantics {
130    /// Expression unless condition: propagate [`VetoType::MissingData`]; other vetoes are non-match.
131    #[default]
132    UnlessExpression,
133    /// Rule-reference unless condition: any veto propagates to the rule result.
134    UnlessRuleReference,
135}
136
137/// Planning-time invariant: every jump is patched and lands on a real
138/// instruction. Panics on violation — compiled output failing this check is a
139/// compiler bug.
140pub fn validate_instruction_jumps(code: &[Instruction]) {
141    if let Err(message) = check_instruction_jumps(code) {
142        panic!("BUG: {message}");
143    }
144}
145
146/// Check that every jump is patched (non-zero target) and lands on a real
147/// instruction (`target < code.len()`).
148fn check_instruction_jumps(code: &[Instruction]) -> Result<(), String> {
149    let code_len = code.len();
150    for (index, instruction) in code.iter().enumerate() {
151        match instruction {
152            Instruction::JumpIfFalse {
153                target_instruction, ..
154            } => {
155                if *target_instruction == 0 {
156                    return Err(format!("unpatched JumpIfFalse at instruction {index}"));
157                }
158                if (*target_instruction as usize) >= code_len {
159                    return Err(format!(
160                        "JumpIfFalse at instruction {index} targets {target_instruction} past the last instruction (length {code_len})"
161                    ));
162                }
163            }
164            Instruction::Jump { target_instruction } => {
165                if *target_instruction == 0 {
166                    return Err(format!("unpatched Jump at instruction {index}"));
167                }
168                if (*target_instruction as usize) >= code_len {
169                    return Err(format!(
170                        "Jump at instruction {index} targets {target_instruction} past the last instruction (length {code_len})"
171                    ));
172                }
173            }
174            _ => {}
175        }
176    }
177    Ok(())
178}
179
180/// Validate a compiled instruction stream against its operand pools: version,
181/// jump targets, register indices, constant/data/veto-message table indices,
182/// and the trailing [`Instruction::Return`].
183///
184/// Two call sites with different failure semantics:
185/// - the deserialization trust boundary ([`TryFrom<ExecutionPlanSerialized>`]),
186///   where a tampered or stale serialized plan must surface as an error;
187/// - the compiler (`CompileContext::finish`), where a violation is a compiler
188///   bug and crashes.
189pub fn validate_instructions(instructions: &Instructions) -> Result<(), String> {
190    if instructions.version != INSTRUCTIONS_VERSION {
191        return Err(format!(
192            "instructions version {} does not match supported version {}",
193            instructions.version, INSTRUCTIONS_VERSION
194        ));
195    }
196
197    check_instruction_jumps(&instructions.code)?;
198
199    let register_count = instructions.register_count;
200    let constant_count = instructions.constants.len();
201    let data_count = instructions.data_manifest.len();
202    let veto_message_count = instructions.veto_messages.len();
203
204    let check_register = |index: usize, name: &str, register: u16| -> Result<(), String> {
205        if register >= register_count {
206            return Err(format!(
207                "instruction {index} {name} register r{register} is out of bounds (register count {register_count})"
208            ));
209        }
210        Ok(())
211    };
212
213    for (index, instruction) in instructions.code.iter().enumerate() {
214        match instruction {
215            Instruction::LoadConstant {
216                destination_register,
217                constant_index,
218            } => {
219                check_register(index, "destination", *destination_register)?;
220                if (*constant_index as usize) >= constant_count {
221                    return Err(format!(
222                        "instruction {index} constant index {constant_index} is out of bounds (constant count {constant_count})"
223                    ));
224                }
225            }
226            Instruction::LoadData {
227                destination_register,
228                data_index,
229            } => {
230                check_register(index, "destination", *destination_register)?;
231                if (*data_index as usize) >= data_count {
232                    return Err(format!(
233                        "instruction {index} data index {data_index} is out of bounds (data manifest size {data_count})"
234                    ));
235                }
236            }
237            Instruction::LoadNow {
238                destination_register,
239            } => {
240                check_register(index, "destination", *destination_register)?;
241            }
242            Instruction::Arithmetic {
243                destination_register,
244                operation: _,
245                left_register,
246                right_register,
247            }
248            | Instruction::Comparison {
249                destination_register,
250                operation: _,
251                left_register,
252                right_register,
253            }
254            | Instruction::RangeLiteral {
255                destination_register,
256                left_register,
257                right_register,
258            } => {
259                check_register(index, "destination", *destination_register)?;
260                check_register(index, "left", *left_register)?;
261                check_register(index, "right", *right_register)?;
262            }
263            Instruction::UnitConversion {
264                destination_register,
265                source_register,
266                target: _,
267            }
268            | Instruction::Mathematical {
269                destination_register,
270                operation: _,
271                source_register,
272            }
273            | Instruction::DateRelative {
274                destination_register,
275                kind: _,
276                source_register,
277            }
278            | Instruction::DateCalendar {
279                destination_register,
280                kind: _,
281                unit: _,
282                source_register,
283            }
284            | Instruction::PastFutureRange {
285                destination_register,
286                kind: _,
287                source_register,
288            }
289            | Instruction::ResultIsVeto {
290                destination_register,
291                source_register,
292            }
293            | Instruction::MoveRegister {
294                destination_register,
295                source_register,
296            } => {
297                check_register(index, "destination", *destination_register)?;
298                check_register(index, "source", *source_register)?;
299            }
300            Instruction::RangeContainment {
301                destination_register,
302                value_register,
303                range_register,
304            } => {
305                check_register(index, "destination", *destination_register)?;
306                check_register(index, "value", *value_register)?;
307                check_register(index, "range", *range_register)?;
308            }
309            Instruction::UserVeto {
310                destination_register,
311                message_index,
312            } => {
313                check_register(index, "destination", *destination_register)?;
314                if (*message_index as usize) >= veto_message_count {
315                    return Err(format!(
316                        "instruction {index} veto message index {message_index} is out of bounds (veto message count {veto_message_count})"
317                    ));
318                }
319            }
320            Instruction::JumpIfFalse {
321                condition_register,
322                target_instruction: _,
323                veto_semantics: _,
324            } => {
325                check_register(index, "condition", *condition_register)?;
326            }
327            Instruction::Jump {
328                target_instruction: _,
329            } => {}
330            Instruction::Return { source_register } => {
331                check_register(index, "source", *source_register)?;
332            }
333        }
334    }
335
336    match instructions.code.last() {
337        Some(Instruction::Return { .. }) => {}
338        Some(other) => {
339            return Err(format!(
340                "instruction stream must end with Return, found {other:?}"
341            ))
342        }
343        None => return Err("instruction stream is empty".to_string()),
344    }
345
346    let code_len = instructions.code.len();
347    for tag in &instructions.arm_tags {
348        if (tag.pc as usize) >= code_len {
349            return Err(format!(
350                "arm tag pc {} is out of bounds (code length {code_len})",
351                tag.pc
352            ));
353        }
354        let tagged = &instructions.code[tag.pc as usize];
355        let valid = match tag.role {
356            ArmRole::Condition => matches!(tagged, Instruction::JumpIfFalse { .. }),
357            ArmRole::Result => matches!(tagged, Instruction::Return { .. }),
358        };
359        if !valid {
360            return Err(format!(
361                "arm tag at pc {} does not match its instruction {tagged:?}",
362                tag.pc
363            ));
364        }
365    }
366    for tag in &instructions.conversion_tags {
367        if (tag.pc as usize) >= code_len {
368            return Err(format!(
369                "conversion tag pc {} is out of bounds (code length {code_len})",
370                tag.pc
371            ));
372        }
373        if !matches!(
374            instructions.code[tag.pc as usize],
375            Instruction::UnitConversion { .. }
376        ) {
377            return Err(format!(
378                "conversion tag at pc {} does not reference a UnitConversion instruction",
379                tag.pc
380            ));
381        }
382    }
383
384    Ok(())
385}
386
387/// Compiled normalized equation for authoritative evaluation.
388#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct Instructions {
390    pub version: u32,
391    pub register_count: u16,
392    #[serde(with = "register_types_serde")]
393    pub register_types: Vec<Arc<LemmaType>>,
394    pub constants: Vec<LiteralValue>,
395    pub data_manifest: Vec<DataPath>,
396    pub veto_messages: Vec<String>,
397    pub code: Vec<Instruction>,
398    /// Piecewise arm provenance: which `JumpIfFalse`/`Return` instructions
399    /// belong to which source branch (`ExecutableRule::branches` index).
400    /// Emitted by `compile_piecewise_rule` after all rewrite passes, so arm
401    /// tags are present in both the optimized and source instruction streams.
402    #[serde(default, skip_serializing_if = "Vec::is_empty")]
403    pub arm_tags: Vec<ArmTag>,
404    /// Unit-conversion provenance: maps a `UnitConversion` instruction back
405    /// to the source location of the conversion expression it compiles.
406    /// Reliable in the source (un-optimized) stream; best-effort in the
407    /// optimized stream (rewrites may fold or merge conversion nodes).
408    #[serde(default, skip_serializing_if = "Vec::is_empty")]
409    pub conversion_tags: Vec<ConversionTag>,
410}
411
412/// Role of an arm-tagged instruction within a rule's piecewise compilation.
413#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
414#[serde(rename_all = "snake_case")]
415pub enum ArmRole {
416    /// A `JumpIfFalse` testing this arm's condition.
417    Condition,
418    /// A `Return` producing this arm's result.
419    Result,
420}
421
422/// Provenance tag linking one instruction to a piecewise arm.
423///
424/// `arm` indexes `ExecutableRule::branches`: 0 is the default branch, 1.. are
425/// the unless branches in source order.
426#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
427pub struct ArmTag {
428    pub pc: u32,
429    pub arm: u16,
430    pub role: ArmRole,
431}
432
433/// Provenance tag linking one `UnitConversion` instruction to the source
434/// location of the conversion expression it compiles.
435#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
436pub struct ConversionTag {
437    pub pc: u32,
438    pub source: Source,
439}
440
441mod register_types_serde {
442    use super::LemmaType;
443    use serde::{Deserialize, Deserializer, Serialize, Serializer};
444    use std::sync::Arc;
445
446    pub fn serialize<S>(values: &[Arc<LemmaType>], serializer: S) -> Result<S::Ok, S::Error>
447    where
448        S: Serializer,
449    {
450        let refs: Vec<&LemmaType> = values.iter().map(|v| v.as_ref()).collect();
451        refs.serialize(serializer)
452    }
453
454    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Arc<LemmaType>>, D::Error>
455    where
456        D: Deserializer<'de>,
457    {
458        let values: Vec<LemmaType> = Vec::deserialize(deserializer)?;
459        Ok(values.into_iter().map(Arc::new).collect())
460    }
461}
462
463/// One compiled operation in a rule's instruction stream.
464#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
465#[serde(rename_all = "snake_case")]
466pub enum Instruction {
467    LoadConstant {
468        destination_register: u16,
469        constant_index: u16,
470    },
471    LoadData {
472        destination_register: u16,
473        data_index: u16,
474    },
475    LoadNow {
476        destination_register: u16,
477    },
478    Arithmetic {
479        destination_register: u16,
480        operation: ArithmeticComputation,
481        left_register: u16,
482        right_register: u16,
483    },
484    Comparison {
485        destination_register: u16,
486        operation: ComparisonComputation,
487        left_register: u16,
488        right_register: u16,
489    },
490    UnitConversion {
491        destination_register: u16,
492        source_register: u16,
493        target: SemanticConversionTarget,
494    },
495    Mathematical {
496        destination_register: u16,
497        operation: MathematicalComputation,
498        source_register: u16,
499    },
500    DateRelative {
501        destination_register: u16,
502        kind: DateRelativeKind,
503        source_register: u16,
504    },
505    DateCalendar {
506        destination_register: u16,
507        kind: DateCalendarKind,
508        unit: CalendarPeriodUnit,
509        source_register: u16,
510    },
511    RangeLiteral {
512        destination_register: u16,
513        left_register: u16,
514        right_register: u16,
515    },
516    PastFutureRange {
517        destination_register: u16,
518        kind: DateRelativeKind,
519        source_register: u16,
520    },
521    RangeContainment {
522        destination_register: u16,
523        value_register: u16,
524        range_register: u16,
525    },
526    ResultIsVeto {
527        destination_register: u16,
528        source_register: u16,
529    },
530    MoveRegister {
531        destination_register: u16,
532        source_register: u16,
533    },
534    UserVeto {
535        destination_register: u16,
536        message_index: u16,
537    },
538    JumpIfFalse {
539        condition_register: u16,
540        target_instruction: u32,
541        #[serde(default)]
542        veto_semantics: JumpVetoSemantics,
543    },
544    Jump {
545        target_instruction: u32,
546    },
547    Return {
548        source_register: u16,
549    },
550}
551
552/// An executable rule with flattened branches
553///
554/// Contains all information needed to evaluate a rule without spec lookups.
555#[derive(Debug, Clone, Serialize, Deserialize)]
556pub struct ExecutableRule {
557    /// Unique identifier for this rule
558    pub path: RulePath,
559
560    /// Rule name
561    pub name: String,
562
563    /// Source branches (unless cause lines only); used for explanations
564    pub branches: Vec<Branch>,
565
566    /// Fully inlined, once-normalized compiled equation; authoritative for evaluation
567    pub instructions: Instructions,
568
569    /// Fully inlined equation compiled **without** rewrite passes: instruction
570    /// shape mirrors the source expressions. Executed (with recording) when
571    /// explanations are requested, so every runtime fact in an explanation
572    /// comes from an actual execution of source-shaped code.
573    pub source_instructions: Instructions,
574
575    /// Source location for error messages (always present for rules from parsed specs)
576    pub source: Source,
577
578    /// Computed type of this rule's result
579    /// Every rule MUST have a type (Lemma is strictly typed)
580    #[serde(with = "arc_lemma_type")]
581    pub rule_type: Arc<LemmaType>,
582}
583
584/// A branch in an executable rule (original expressions for explanation trace)
585#[derive(Debug, Clone, Serialize, Deserialize)]
586pub struct Branch {
587    /// Condition expression (None for default branch)
588    pub condition: Option<Expression>,
589
590    /// Result expression as written (`RulePath` refs preserved)
591    pub result: Expression,
592
593    /// Source location for error messages (always present for branches from parsed specs)
594    pub source: Source,
595}
596
597mod arc_lemma_type {
598    use super::LemmaType;
599    use serde::{Deserialize, Deserializer, Serialize, Serializer};
600    use std::sync::Arc;
601
602    pub fn serialize<S>(value: &Arc<LemmaType>, serializer: S) -> Result<S::Ok, S::Error>
603    where
604        S: Serializer,
605    {
606        value.as_ref().serialize(serializer)
607    }
608
609    pub fn deserialize<'de, D>(deserializer: D) -> Result<Arc<LemmaType>, D::Error>
610    where
611        D: Deserializer<'de>,
612    {
613        LemmaType::deserialize(deserializer).map(Arc::new)
614    }
615}
616
617/// Builds an execution plan from a Graph for one temporal slice.
618/// Internal implementation detail - only called by plan()
619pub(crate) fn build_execution_plan(
620    graph: &Graph,
621    resolved_types: &mut Vec<(Arc<LemmaRepository>, Arc<LemmaSpec>, ResolvedSpecTypes)>,
622    effective: &EffectiveDate,
623    limits: &crate::limits::ResourceLimits,
624) -> Result<ExecutionPlan, Vec<Error>> {
625    let execution_order = graph.execution_order();
626
627    let main_spec = graph.main_spec();
628    let main_idx = resolved_types
629        .iter()
630        .position(|(_, spec, _)| Arc::ptr_eq(spec, main_spec));
631
632    let mut sources: SpecSources = Vec::new();
633    for (repo, spec, _) in resolved_types.iter() {
634        if !sources.iter().any(|e| {
635            e.repository == repo.name
636                && e.name == spec.name
637                && e.effective_from == spec.effective_from
638        }) {
639            sources.push(SpecSource {
640                repository: repo.name.clone(),
641                name: spec.name.clone(),
642                effective_from: spec.effective_from.clone(),
643                source: crate::formatting::format_specs(&[spec.as_ref().clone()]),
644            });
645        }
646    }
647
648    let main_resolved_types = main_idx
649        .map(|idx| resolved_types.remove(idx).2)
650        .unwrap_or_default();
651    let data = graph.build_data(&main_resolved_types.resolved);
652
653    // Planning gate: every data-target reference and plain data declaration
654    // must carry a fully resolved type. Rule-target references are exempt:
655    // they deliberately ship `Undetermined` so runtime veto propagation
656    // surfaces the target rule's veto reason directly. A residual
657    // `Undetermined` anywhere else would violate the invariant evaluation
658    // and schema consumers rely on — report it instead of shipping the plan.
659    let undetermined_errors: Vec<Error> = data
660        .iter()
661        .filter_map(|(path, definition)| {
662            let (resolved_type, source) = match definition {
663                DataDefinition::TypeDeclaration {
664                    resolved_type,
665                    source,
666                    ..
667                } => (resolved_type, source),
668                DataDefinition::Reference {
669                    target: ReferenceTarget::Data(_),
670                    resolved_type,
671                    source,
672                    ..
673                } => (resolved_type, source),
674                DataDefinition::Reference {
675                    target: ReferenceTarget::Rule(_),
676                    ..
677                }
678                | DataDefinition::Value { .. }
679                | DataDefinition::Import { .. } => return None,
680            };
681            if resolved_type.is_undetermined() {
682                Some(Error::validation(
683                    format!("could not determine the type of '{path}'"),
684                    Some(source.clone()),
685                    None::<String>,
686                ))
687            } else {
688                None
689            }
690        })
691        .collect();
692    if !undetermined_errors.is_empty() {
693        return Err(undetermined_errors);
694    }
695
696    let signature_index = crate::planning::graph::build_signature_index(
697        &main_spec.name,
698        &main_resolved_types.unit_index,
699    )
700    .expect("BUG: signature_index build already validated during resolve_and_validate");
701
702    let mut executable_rules: Vec<ExecutableRule> = Vec::new();
703    let mut max_register_count: u16 = 0;
704    let plan_rule_paths: HashSet<RulePath> = graph.rules().keys().cloned().collect();
705    let mut completed_rules: HashMap<RulePath, Arc<Expression>> = HashMap::new();
706
707    for rule_path in execution_order {
708        let rule_node = graph.rules().get(rule_path).expect(
709            "bug: rule from topological sort not in graph - validation should have caught this",
710        );
711
712        let mut executable_branches = Vec::new();
713        for (condition, result) in &rule_node.branches {
714            executable_branches.push(Branch {
715                condition: condition.clone(),
716                result: result.clone(),
717                source: rule_node.source.clone(),
718            });
719        }
720
721        let unit_ctx = UnitResolutionContext::WithIndex(&main_resolved_types.unit_index);
722        let compiled = build_normalized_rule_instructions(
723            &rule_node.branches,
724            &completed_rules,
725            &plan_rule_paths,
726            &data,
727            &unit_ctx,
728            Some(rule_node.source.clone()),
729            &rule_node.rule_type,
730            limits.max_normalized_expression_nodes,
731        )
732        // Pass the error through unchanged: it is already a user-facing
733        // planning error (resource limit or constant-fold failure) carrying
734        // its own kind and source location.
735        .map_err(|error| vec![error])?;
736        let CompiledRule {
737            instructions,
738            source_instructions,
739            inlined_expression,
740        } = compiled;
741        max_register_count = max_register_count
742            .max(instructions.register_count)
743            .max(source_instructions.register_count);
744        completed_rules.insert(rule_path.clone(), inlined_expression);
745
746        executable_rules.push(ExecutableRule {
747            path: rule_path.clone(),
748            name: rule_path.rule.clone(),
749            branches: executable_branches,
750            instructions,
751            source_instructions,
752            source: rule_node.source.clone(),
753            rule_type: Arc::clone(&rule_node.rule_type),
754        });
755    }
756
757    Ok(ExecutionPlan {
758        spec_name: main_spec.name.clone(),
759        commentary: main_spec.commentary.clone(),
760        data,
761        rules: executable_rules,
762        max_register_count,
763        reference_evaluation_order: graph.reference_evaluation_order().to_vec(),
764        meta: main_spec
765            .meta_fields
766            .iter()
767            .map(|f| (f.key.clone(), f.value.clone()))
768            .collect(),
769        resolved_types: main_resolved_types,
770        signature_index,
771        effective: effective.clone(),
772        sources,
773    })
774}
775
776/// A spec's public interface: its data (inputs) and rules (outputs) with
777/// full structured type information.
778///
779/// Built from an [`ExecutionPlan`] via [`ExecutionPlan::schema`] (all data and
780/// rules) or [`ExecutionPlan::schema_for_rules`] (scoped to specific rules and
781/// only the data they need).
782///
783/// Shared by the HTTP server, the CLI, the MCP server, WASM, and any other
784/// consumer. Carries the real [`LemmaType`] and [`LiteralValue`] so consumers
785/// can work at whatever fidelity they need — structured types for input forms,
786/// or `Display` for plain text.
787///
788/// This is the IO contract consumers can rely on:
789/// - `data`: required/provided inputs with full type constraints
790/// - `rules`: produced outputs with full result types
791///
792/// For cross-spec composition, planning validates that referenced specs satisfy
793/// this contract. Plan hashes are complementary: they lock full behavior.
794/// One data input in a [`SpecSchema`].
795///
796/// A named struct instead of a `(type, bound, default)` tuple so JSON-native consumers
797/// (TypeScript, Python, ...) get stable field names. `bound_value` holds a spec or
798/// caller-fixed literal; `default` is only a `-> default ...` suggestion.
799#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
800pub struct DataEntry {
801    #[serde(rename = "type")]
802    pub lemma_type: LemmaType,
803    #[serde(skip_serializing_if = "Option::is_none", default)]
804    pub bound_value: Option<LiteralValue>,
805    #[serde(skip_serializing_if = "Option::is_none", default)]
806    pub default: Option<LiteralValue>,
807}
808
809/// User-provided data values resolved against a plan's type declarations.
810///
811/// Lightweight and cheap to construct — no plan cloning required. The
812/// [`ExecutionPlan`] stays immutable; callers pass `(&ExecutionPlan, &DataOverlay)`
813/// to evaluation and schema methods.
814#[derive(Debug, Clone, Default)]
815pub struct DataOverlay {
816    /// Successfully parsed and validated values supplied by the caller.
817    pub values: HashMap<DataPath, LiteralValue>,
818    /// Values that failed parse or constraint validation. Rules that read
819    /// these paths produce [`crate::evaluation::VetoType::Computation`] vetoes.
820    pub violated: HashMap<DataPath, String>,
821}
822
823impl DataOverlay {
824    /// Parse and validate caller-supplied values against the plan's data declarations.
825    ///
826    /// Unknown data keys and resource-limit violations return [`Err`]. Per-field
827    /// parse or constraint failures are recorded in [`Self::violated`] and the
828    /// overlay is still returned as `Ok`.
829    pub fn resolve(
830        plan: &ExecutionPlan,
831        raw_values: HashMap<String, DataValueInput>,
832        limits: &ResourceLimits,
833    ) -> Result<Self, Error> {
834        let mut overlay = Self::default();
835
836        for (name, raw_value) in raw_values {
837            let data_path = plan.get_data_path_by_str(&name).ok_or_else(|| {
838                let available: Vec<String> = plan.data.keys().map(|p| p.input_key()).collect();
839                Error::request(
840                    format!(
841                        "Data '{}' not found. Available data: {}",
842                        name,
843                        available.join(", ")
844                    ),
845                    None::<String>,
846                )
847            })?;
848            let data_path = data_path.clone();
849
850            let data_definition = plan
851                .data
852                .get(&data_path)
853                .expect("BUG: data_path was just resolved from plan.data, must exist");
854
855            let data_source = data_definition.source().clone();
856            let type_arc = match data_definition {
857                DataDefinition::TypeDeclaration { resolved_type, .. }
858                | DataDefinition::Reference { resolved_type, .. } => Arc::clone(resolved_type),
859                DataDefinition::Value { value, .. } => Arc::clone(&value.lemma_type),
860                DataDefinition::Import { .. } => {
861                    return Err(Error::request(
862                        format!(
863                            "Data '{}' is a spec reference; cannot provide a value.",
864                            name
865                        ),
866                        None::<String>,
867                    ));
868                }
869            };
870
871            let literal_value = match parse_data_value(&raw_value, &type_arc, &data_source) {
872                Ok(value) => value,
873                Err(error) => {
874                    overlay
875                        .violated
876                        .insert(data_path, error.message().to_string());
877                    continue;
878                }
879            };
880
881            let size = literal_value.byte_size();
882            if size > limits.max_data_value_bytes {
883                return Err(Error::resource_limit_exceeded(
884                    "max_data_value_bytes",
885                    limits.max_data_value_bytes.to_string(),
886                    size.to_string(),
887                    format!(
888                        "Reduce the size of data values to {} bytes or less",
889                        limits.max_data_value_bytes
890                    ),
891                    Some(data_source.clone()),
892                    None,
893                    None,
894                )
895                .with_related_data(&name));
896            }
897
898            if let Err(message) = validate_value_against_type(type_arc.as_ref(), &literal_value) {
899                overlay.violated.insert(data_path, message);
900                continue;
901            }
902
903            overlay.values.insert(data_path, literal_value);
904        }
905
906        Ok(overlay)
907    }
908
909    pub fn is_empty(&self) -> bool {
910        self.values.is_empty() && self.violated.is_empty()
911    }
912}
913
914/// Values known for partial branch pruning and schema display.
915///
916/// Priority: spec literals, then overlay values, then declared defaults.
917/// Values definitively known from the spec and caller overlay.
918///
919/// Spec literals and overlay values only. Defaults are NOT included — the
920/// evaluator applies those natively in its own context.
921pub(crate) fn build_known_values(
922    plan: &ExecutionPlan,
923    overlay: &DataOverlay,
924) -> HashMap<DataPath, LiteralValue> {
925    let mut known_values: HashMap<DataPath, LiteralValue> = plan
926        .data
927        .iter()
928        .filter_map(|(path, definition)| {
929            if overlay.violated.contains_key(path) {
930                return None;
931            }
932            definition
933                .value()
934                .map(|value| (path.clone(), value.clone()))
935        })
936        .collect();
937
938    for (path, value) in &overlay.values {
939        known_values.insert(path.clone(), value.clone());
940    }
941
942    known_values
943}
944
945fn schema_bound_value(
946    path: &DataPath,
947    data: &DataDefinition,
948    overlay: &DataOverlay,
949) -> Option<LiteralValue> {
950    if let Some(value) = overlay.values.get(path) {
951        return Some(value.clone());
952    }
953    data.bound_value().cloned()
954}
955
956#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
957pub struct SpecSchema {
958    /// Resolved spec id (logical name including path segments).
959    pub spec: String,
960    /// Optional commentary from the `"""..."""` block in the spec source.
961    #[serde(skip_serializing_if = "Option::is_none", default)]
962    pub commentary: Option<String>,
963    /// The effective date of this specific plan version. `None` for origin (unversioned) specs.
964    #[serde(skip_serializing_if = "Option::is_none", default)]
965    pub effective: Option<DateTimeValue>,
966    /// All known effective-from dates for this spec, populated by the caller when multiple
967    /// temporal versions exist. Empty for single-version specs.
968    #[serde(skip_serializing_if = "Vec::is_empty", default)]
969    pub versions: Vec<DateTimeValue>,
970    /// Data (inputs) keyed by name.
971    pub data: indexmap::IndexMap<String, DataEntry>,
972    /// Rules (outputs) keyed by name, with their computed result types
973    pub rules: indexmap::IndexMap<String, LemmaType>,
974    /// Spec metadata
975    pub meta: HashMap<String, MetaValue>,
976}
977
978impl std::fmt::Display for SpecSchema {
979    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
980        write!(f, "Spec: {}", self.spec)?;
981
982        if let Some(commentary) = &self.commentary {
983            write!(f, "\n  {}", commentary)?;
984        }
985
986        if !self.meta.is_empty() {
987            write!(f, "\n\nMeta:")?;
988            // Sort keys for deterministic output
989            let mut entries: Vec<(&String, &MetaValue)> = self.meta.iter().collect();
990            entries.sort_by_key(|(k, _)| *k);
991            for (key, value) in entries {
992                write!(f, "\n  {}: {}", key, value)?;
993            }
994        }
995
996        if !self.data.is_empty() {
997            write!(f, "\n\nData:")?;
998            for (name, entry) in &self.data {
999                write!(f, "\n  {} ({})", name, entry.lemma_type.specifications)?;
1000                for line in type_detail_lines(&entry.lemma_type.specifications) {
1001                    write!(f, "\n    {}", line)?;
1002                }
1003                let help = entry.lemma_type.specifications.help();
1004                if !help.is_empty() {
1005                    write!(f, "\n    help: {}", help)?;
1006                }
1007                if let Some(val) = &entry.bound_value {
1008                    write!(f, "\n    value: {}", val)?;
1009                }
1010                if let Some(val) = &entry.default {
1011                    write!(f, "\n    default: {}", val)?;
1012                }
1013            }
1014        }
1015
1016        if !self.rules.is_empty() {
1017            write!(f, "\n\nRules:")?;
1018            for (name, rule_type) in &self.rules {
1019                write!(f, "\n  {} ({})", name, rule_type.specifications)?;
1020            }
1021        }
1022
1023        if self.data.is_empty() && self.rules.is_empty() {
1024            write!(f, "\n  (no data or rules)")?;
1025        }
1026
1027        Ok(())
1028    }
1029}
1030
1031/// Produce a human-readable summary of type constraints, or `None` when there
1032/// are no constraints worth showing (e.g. bare `boolean`).
1033/// Returns one formatted string per constraint or property of the type specification.
1034/// Uses `rational_to_display_str` for all rational bounds so they render as decimals,
1035/// not as raw fractions.
1036pub fn type_detail_lines(spec: &TypeSpecification) -> Vec<String> {
1037    use crate::computation::rational::rational_to_display_str;
1038    let mut lines = Vec::new();
1039    match spec {
1040        TypeSpecification::Quantity {
1041            minimum,
1042            maximum,
1043            decimals,
1044            units,
1045            ..
1046        } => {
1047            let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
1048            if !unit_names.is_empty() {
1049                lines.push(format!("units: {}", unit_names.join(", ")));
1050            }
1051            if let Some(d) = decimals {
1052                lines.push(format!("decimals: {}", d));
1053            }
1054            if let Some((magnitude, unit_name)) = minimum {
1055                lines.push(format!(
1056                    "minimum: {} {}",
1057                    rational_to_display_str(magnitude),
1058                    unit_name
1059                ));
1060            }
1061            if let Some((magnitude, unit_name)) = maximum {
1062                lines.push(format!(
1063                    "maximum: {} {}",
1064                    rational_to_display_str(magnitude),
1065                    unit_name
1066                ));
1067            }
1068        }
1069        TypeSpecification::Number {
1070            minimum,
1071            maximum,
1072            decimals,
1073            ..
1074        } => {
1075            if let Some(d) = decimals {
1076                lines.push(format!("decimals: {}", d));
1077            }
1078            if let Some(v) = minimum {
1079                lines.push(format!("minimum: {}", rational_to_display_str(v)));
1080            }
1081            if let Some(v) = maximum {
1082                lines.push(format!("maximum: {}", rational_to_display_str(v)));
1083            }
1084        }
1085        TypeSpecification::Ratio {
1086            minimum,
1087            maximum,
1088            decimals,
1089            units,
1090            ..
1091        } => {
1092            let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
1093            if !unit_names.is_empty() {
1094                lines.push(format!("units: {}", unit_names.join(", ")));
1095            }
1096            if let Some(d) = decimals {
1097                lines.push(format!("decimals: {}", d));
1098            }
1099            if let Some(v) = minimum {
1100                lines.push(format!("minimum: {}", rational_to_display_str(v)));
1101            }
1102            if let Some(v) = maximum {
1103                lines.push(format!("maximum: {}", rational_to_display_str(v)));
1104            }
1105        }
1106        TypeSpecification::Text {
1107            options, length, ..
1108        } => {
1109            if let Some(l) = length {
1110                lines.push(format!("length: {}", l));
1111            }
1112            if !options.is_empty() {
1113                let quoted: Vec<String> = options.iter().map(|o| format!("\"{}\"", o)).collect();
1114                lines.push(format!("options: {}", quoted.join(", ")));
1115            }
1116        }
1117        TypeSpecification::Date {
1118            minimum, maximum, ..
1119        } => {
1120            if let Some(v) = minimum {
1121                lines.push(format!("minimum: {}", v));
1122            }
1123            if let Some(v) = maximum {
1124                lines.push(format!("maximum: {}", v));
1125            }
1126        }
1127        TypeSpecification::Time {
1128            minimum, maximum, ..
1129        } => {
1130            if let Some(v) = minimum {
1131                lines.push(format!("minimum: {}", v));
1132            }
1133            if let Some(v) = maximum {
1134                lines.push(format!("maximum: {}", v));
1135            }
1136        }
1137        TypeSpecification::QuantityRange { units, .. } => {
1138            let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
1139            if !unit_names.is_empty() {
1140                lines.push(format!("units: {}", unit_names.join(", ")));
1141            }
1142        }
1143        TypeSpecification::RatioRange { units, .. } => {
1144            let unit_names: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
1145            if !unit_names.is_empty() {
1146                lines.push(format!("units: {}", unit_names.join(", ")));
1147            }
1148        }
1149        TypeSpecification::Boolean { .. }
1150        | TypeSpecification::NumberRange { .. }
1151        | TypeSpecification::DateRange { .. }
1152        | TypeSpecification::TimeRange { .. }
1153        | TypeSpecification::Veto { .. }
1154        | TypeSpecification::Undetermined => {}
1155    }
1156    lines
1157}
1158
1159impl ExecutionPlan {
1160    /// Expression-scope unit index (local types plus direct `uses` imports).
1161    /// Rule-result units outside this scope are resolved from [`ExecutableRule::rule_type`]
1162    /// at materialization time.
1163    pub(crate) fn expression_unit_index(&self) -> &HashMap<String, Arc<LemmaType>> {
1164        &self.resolved_types.unit_index
1165    }
1166
1167    /// Build a [`SpecSchema`] describing this plan's public IO contract.
1168    ///
1169    /// Only data transitively reachable from live branches of at least one local
1170    /// rule is included. Spec-reference data (which have no schema type) are also
1171    /// excluded. Only local rules (no cross-spec segments) are included. Data are
1172    /// sorted local-first, then by source position within each depth.
1173    ///
1174    /// If the caller supplies a [`DataOverlay`], branches whose conditions are
1175    /// definitively false given those values are pruned, reducing the returned
1176    /// data set to only what is still needed.
1177    /// Names of local (main-spec) rules in plan topological order.
1178    pub fn local_rule_names(&self) -> Vec<String> {
1179        self.rules
1180            .iter()
1181            .filter(|r| r.path.segments.is_empty())
1182            .map(|r| r.name.clone())
1183            .collect()
1184    }
1185
1186    pub fn schema(&self, overlay: &DataOverlay) -> SpecSchema {
1187        let all_local_rules = self.local_rule_names();
1188        self.schema_for_rules(&all_local_rules, overlay)
1189            .expect("BUG: all_local_rules sourced from self.rules")
1190    }
1191
1192    /// Every typed data input and local rule — the full spec surface.
1193    ///
1194    /// Excludes [`DataDefinition::Reference`] (`with` bindings) and imports; those
1195    /// are not caller inputs.
1196    pub fn interface_schema(&self, overlay: &DataOverlay) -> SpecSchema {
1197        let mut data_entries: Vec<(usize, usize, String, DataEntry)> = self
1198            .data
1199            .iter()
1200            .filter(|(_, data)| {
1201                data.schema_type().is_some() && !matches!(data, DataDefinition::Reference { .. })
1202            })
1203            .map(|(path, data)| {
1204                let lemma_type = data
1205                    .schema_type()
1206                    .expect("BUG: filter above ensured schema_type is Some")
1207                    .clone();
1208                let bound_value = schema_bound_value(path, data, overlay);
1209                let default = data.default_suggestion();
1210                (
1211                    path.segments.len(),
1212                    data.source().span.start,
1213                    path.input_key(),
1214                    DataEntry {
1215                        lemma_type,
1216                        bound_value,
1217                        default,
1218                    },
1219                )
1220            })
1221            .collect();
1222        data_entries.sort_by_key(|(depth, pos, _, _)| (*depth, *pos));
1223
1224        let rule_entries: Vec<(String, LemmaType)> = self
1225            .rules
1226            .iter()
1227            .filter(|r| r.path.segments.is_empty())
1228            .map(|r| (r.name.clone(), (*r.rule_type).clone()))
1229            .collect();
1230
1231        SpecSchema {
1232            spec: self.spec_name.clone(),
1233            commentary: self.commentary.clone(),
1234            effective: self.effective.as_ref().cloned(),
1235            versions: Vec::new(),
1236            data: data_entries
1237                .into_iter()
1238                .map(|(_, _, name, data)| (name, data))
1239                .collect(),
1240            rules: rule_entries.into_iter().collect(),
1241            meta: self.meta.clone(),
1242        }
1243    }
1244
1245    /// Build a [`SpecSchema`] scoped to specific rules.
1246    ///
1247    /// The returned schema contains only the data **needed** by the given rules
1248    /// (transitively, through live arms of normalized expressions)
1249    /// and only those rules. This is the "what do I need to evaluate these rules?"
1250    /// view. [`DataDefinition::Reference`] entries (`with` bindings) are omitted —
1251    /// their values are fixed in the spec.
1252    ///
1253    /// When the caller supplies a [`DataOverlay`], branches whose conditions are
1254    /// definitively false given those values are pruned, reducing the returned
1255    /// data set to only what is still needed.
1256    ///
1257    /// Data are sorted local-first, then by source position within each depth.
1258    ///
1259    /// Returns `Err` if any rule name is not found in the plan.
1260    pub fn schema_for_rules(
1261        &self,
1262        rule_names: &[String],
1263        overlay: &DataOverlay,
1264    ) -> Result<SpecSchema, Error> {
1265        let mut rule_entries: Vec<(String, LemmaType)> = Vec::new();
1266        for rule_name in rule_names {
1267            let rule = self.get_rule(rule_name).ok_or_else(|| {
1268                Error::request(
1269                    format!(
1270                        "Rule '{}' not found in spec '{}'",
1271                        rule_name, self.spec_name
1272                    ),
1273                    None::<String>,
1274                )
1275            })?;
1276            rule_entries.push((rule.name.clone(), (*rule.rule_type).clone()));
1277        }
1278
1279        let needed_data = self.collect_needed_data_paths(rule_names, overlay)?;
1280
1281        let mut data_entries: Vec<(usize, usize, String, DataEntry)> = self
1282            .data
1283            .iter()
1284            .filter(|(path, _)| needed_data.contains(path))
1285            .filter(|(_, data)| !matches!(data, DataDefinition::Reference { .. }))
1286            .filter_map(|(path, data)| {
1287                let lemma_type = data.schema_type()?.clone();
1288                let bound_value = schema_bound_value(path, data, overlay);
1289                let default = data.default_suggestion();
1290                Some((
1291                    path.segments.len(),
1292                    data.source().span.start,
1293                    path.input_key(),
1294                    DataEntry {
1295                        lemma_type,
1296                        bound_value,
1297                        default,
1298                    },
1299                ))
1300            })
1301            .collect();
1302        data_entries.sort_by_key(|(depth, pos, _, _)| (*depth, *pos));
1303        let data_entries: Vec<(String, DataEntry)> = data_entries
1304            .into_iter()
1305            .map(|(_, _, name, data)| (name, data))
1306            .collect();
1307
1308        Ok(SpecSchema {
1309            spec: self.spec_name.clone(),
1310            commentary: self.commentary.clone(),
1311            effective: self.effective.as_ref().cloned(),
1312            versions: Vec::new(),
1313            data: data_entries.into_iter().collect(),
1314            rules: rule_entries.into_iter().collect(),
1315            meta: self.meta.clone(),
1316        })
1317    }
1318
1319    /// Look up a data by its input key (e.g., "age" or "rules.base_price").
1320    pub fn get_data_path_by_str(&self, name: &str) -> Option<&DataPath> {
1321        let canonical_name = crate::parsing::ast::ascii_lowercase_logical_name(name.to_string());
1322        self.data
1323            .keys()
1324            .find(|path| path.input_key() == canonical_name)
1325    }
1326
1327    /// Validate caller-requested rule names and return canonical local rule names.
1328    ///
1329    /// `None` means all local rules. `Some(&[])` is an error. Unknown names in `Some` slice error.
1330    pub fn validated_response_rule_names(
1331        &self,
1332        rules: Option<&[String]>,
1333    ) -> Result<std::collections::HashSet<String>, Error> {
1334        let Some(rules) = rules else {
1335            return Ok(self.local_rule_names().into_iter().collect());
1336        };
1337        if rules.is_empty() {
1338            return Err(Error::request(
1339                "at least one rule required".to_string(),
1340                None::<String>,
1341            ));
1342        }
1343        let mut names = std::collections::HashSet::new();
1344        for rule_name in rules {
1345            let rule = self.get_rule(rule_name).ok_or_else(|| {
1346                Error::request(
1347                    format!("Rule '{rule_name}' not found in spec '{}'", self.spec_name),
1348                    None::<String>,
1349                )
1350            })?;
1351            names.insert(rule.name.clone());
1352        }
1353        Ok(names)
1354    }
1355
1356    /// Look up a local rule by its name (rule in the main spec).
1357    pub fn get_rule(&self, name: &str) -> Option<&ExecutableRule> {
1358        let canonical_name = crate::parsing::ast::ascii_lowercase_logical_name(name.to_string());
1359        self.rules
1360            .iter()
1361            .find(|r| r.name == canonical_name && r.path.segments.is_empty())
1362    }
1363
1364    /// Collect the data paths statically referenced by the named local rules.
1365    ///
1366    /// Walks the live branches of each named rule transitively: rule-target
1367    /// data references extend the walk to the referenced rules. Branches whose
1368    /// conditions are definitively decided by overlay-known values are pruned,
1369    /// mirroring [`ExecutionPlan::schema_for_rules`].
1370    ///
1371    /// Returns `Err` if any rule name is not found in the plan.
1372    pub fn collect_needed_data_paths(
1373        &self,
1374        rule_names: &[String],
1375        overlay: &DataOverlay,
1376    ) -> Result<HashSet<DataPath>, Error> {
1377        let known_values = build_known_values(self, overlay);
1378
1379        let mut needed_data: HashSet<DataPath> = HashSet::new();
1380        let mut visited_rules: HashSet<RulePath> = HashSet::new();
1381        let mut rule_worklist: Vec<RulePath> = Vec::new();
1382
1383        // Validate and seed the worklist with the selected rules.
1384        for rule_name in rule_names {
1385            let rule = self.get_rule(rule_name).ok_or_else(|| {
1386                Error::request(
1387                    format!(
1388                        "Rule '{}' not found in spec '{}'",
1389                        rule_name, self.spec_name
1390                    ),
1391                    None::<String>,
1392                )
1393            })?;
1394            rule_worklist.push(rule.path.clone());
1395        }
1396
1397        // Walk the live branches of each reachable rule, collecting data paths.
1398        // Rule-target references discovered via data paths extend the worklist.
1399        while let Some(rule_path) = rule_worklist.pop() {
1400            if !visited_rules.insert(rule_path.clone()) {
1401                continue;
1402            }
1403
1404            let rule = self.get_rule_by_path(&rule_path).unwrap_or_else(|| {
1405                panic!(
1406                    "BUG: rule path '{}' placed on worklist but not found in plan '{}'",
1407                    rule_path.rule, self.spec_name
1408                )
1409            });
1410
1411            for (branch_index, branch) in rule.branches.iter().enumerate() {
1412                if branch_index == 0 {
1413                    let any_unless_definitely_true =
1414                        rule.branches[1..].iter().any(|unless_branch| {
1415                            let unless_condition = unless_branch
1416                                .condition
1417                                .as_ref()
1418                                .expect("BUG: unless branch missing condition");
1419                            crate::evaluation::partial::try_evaluate_condition(
1420                                unless_condition,
1421                                &known_values,
1422                                self,
1423                            ) == Some(true)
1424                        });
1425                    if any_unless_definitely_true {
1426                        continue;
1427                    }
1428                } else if let Some(condition) = &branch.condition {
1429                    if crate::evaluation::partial::try_evaluate_condition(
1430                        condition,
1431                        &known_values,
1432                        self,
1433                    ) == Some(false)
1434                    {
1435                        continue;
1436                    }
1437                }
1438
1439                let mut branch_data: HashSet<DataPath> = HashSet::new();
1440                if let Some(condition) = &branch.condition {
1441                    condition.collect_data_paths(&mut branch_data);
1442                }
1443                branch.result.collect_data_paths(&mut branch_data);
1444
1445                let mut branch_rules: HashSet<RulePath> = HashSet::new();
1446                if let Some(condition) = &branch.condition {
1447                    condition.collect_rule_paths(&mut branch_rules);
1448                }
1449                branch.result.collect_rule_paths(&mut branch_rules);
1450
1451                for data_path in &branch_data {
1452                    if let Some(DataDefinition::Reference {
1453                        target: ReferenceTarget::Rule(target_rule),
1454                        ..
1455                    }) = self.data.get(data_path)
1456                    {
1457                        branch_rules.insert(target_rule.clone());
1458                    }
1459                }
1460
1461                needed_data.extend(branch_data);
1462                rule_worklist.extend(branch_rules);
1463            }
1464        }
1465
1466        Ok(needed_data)
1467    }
1468
1469    /// Look up a rule by its full path.
1470    pub fn get_rule_by_path(&self, rule_path: &RulePath) -> Option<&ExecutableRule> {
1471        self.rules.iter().find(|r| &r.path == rule_path)
1472    }
1473
1474    /// Get the literal value for a data path, if it exists and has a literal value.
1475    pub fn get_data_value(&self, path: &DataPath) -> Option<&LiteralValue> {
1476        self.data.get(path).and_then(|d| d.value())
1477    }
1478}
1479
1480pub(crate) fn validate_value_against_type(
1481    expected_type: &LemmaType,
1482    value: &LiteralValue,
1483) -> Result<(), String> {
1484    use crate::computation::rational::{commit_rational_to_decimal, RationalInteger};
1485    use crate::planning::semantics::TypeSpecification;
1486
1487    fn exceeds_decimal_places(magnitude: &RationalInteger, max_decimals: u8) -> bool {
1488        match commit_rational_to_decimal(magnitude) {
1489            Ok(decimal) => decimal.scale() > u32::from(max_decimals),
1490            Err(_) => true,
1491        }
1492    }
1493
1494    fn format_rational_for_validation_message(
1495        expected_type: &crate::planning::semantics::LemmaType,
1496        magnitude: &RationalInteger,
1497    ) -> String {
1498        use crate::computation::rational::rational_to_display_str;
1499        expected_type
1500            .try_materialize_rational_as_decimal_string(magnitude)
1501            .unwrap_or_else(|_| rational_to_display_str(magnitude))
1502    }
1503
1504    match (&expected_type.specifications, &value.value) {
1505        (
1506            TypeSpecification::Number {
1507                minimum,
1508                maximum,
1509                decimals,
1510                ..
1511            },
1512            ValueKind::Number(n),
1513        ) => {
1514            if let Some(d) = decimals {
1515                if exceeds_decimal_places(n, *d) {
1516                    return Err(format!(
1517                        "{} exceeds decimals constraint {d}",
1518                        format_rational_for_validation_message(expected_type, n)
1519                    ));
1520                }
1521            }
1522            if let Some(min) = minimum {
1523                if n < min {
1524                    return Err(format!(
1525                        "{} is below minimum {}",
1526                        format_rational_for_validation_message(expected_type, n),
1527                        format_rational_for_validation_message(expected_type, min)
1528                    ));
1529                }
1530            }
1531            if let Some(max) = maximum {
1532                if n > max {
1533                    return Err(format!(
1534                        "{} is above maximum {}",
1535                        format_rational_for_validation_message(expected_type, n),
1536                        format_rational_for_validation_message(expected_type, max)
1537                    ));
1538                }
1539            }
1540            Ok(())
1541        }
1542        (
1543            TypeSpecification::Quantity {
1544                minimum,
1545                maximum,
1546                decimals,
1547                units,
1548                ..
1549            },
1550            ValueKind::Quantity(magnitude, signature),
1551        ) => {
1552            use crate::computation::rational::checked_div;
1553            use crate::planning::semantics::quantity_declared_bound_canonical;
1554            let unit = signature
1555                .first()
1556                .map(|(n, _)| n.as_str())
1557                .expect("BUG: Quantity value has empty signature in execution plan validation");
1558            let quantity_unit = units.get(unit)?;
1559            let factor = &quantity_unit.factor;
1560            let in_unit = checked_div(magnitude, factor).map_err(|failure| {
1561                format!("cannot de-canonicalize quantity for validation: {failure}")
1562            })?;
1563            if let Some(d) = decimals {
1564                if exceeds_decimal_places(&in_unit, *d) {
1565                    return Err(format!(
1566                        "{} {unit} exceeds decimals constraint {d}",
1567                        format_rational_for_validation_message(expected_type, &in_unit)
1568                    ));
1569                }
1570            }
1571            if let Some(bound) = minimum {
1572                let canonical_min = quantity_declared_bound_canonical(
1573                    bound,
1574                    units,
1575                    expected_type.name().as_str(),
1576                    "minimum",
1577                )?;
1578                if magnitude < &canonical_min {
1579                    let min_in_unit = checked_div(&canonical_min, factor).map_err(|failure| {
1580                        format!("cannot de-canonicalize minimum for validation: {failure}")
1581                    })?;
1582                    let value_display = format!(
1583                        "{} {}",
1584                        format_rational_for_validation_message(expected_type, &in_unit),
1585                        unit
1586                    );
1587                    let bound_display = format!(
1588                        "{} {}",
1589                        format_rational_for_validation_message(expected_type, &min_in_unit),
1590                        quantity_unit.name
1591                    );
1592                    return Err(format!("{value_display} is below minimum {bound_display}"));
1593                }
1594            }
1595            if let Some(bound) = maximum {
1596                let canonical_max = quantity_declared_bound_canonical(
1597                    bound,
1598                    units,
1599                    expected_type.name().as_str(),
1600                    "maximum",
1601                )?;
1602                if magnitude > &canonical_max {
1603                    let max_in_unit = checked_div(&canonical_max, factor).map_err(|failure| {
1604                        format!("cannot de-canonicalize maximum for validation: {failure}")
1605                    })?;
1606                    let value_display = format!(
1607                        "{} {}",
1608                        format_rational_for_validation_message(expected_type, &in_unit),
1609                        unit
1610                    );
1611                    let bound_display = format!(
1612                        "{} {}",
1613                        format_rational_for_validation_message(expected_type, &max_in_unit),
1614                        quantity_unit.name
1615                    );
1616                    return Err(format!("{value_display} is above maximum {bound_display}"));
1617                }
1618            }
1619            Ok(())
1620        }
1621        (
1622            TypeSpecification::Text {
1623                length, options, ..
1624            },
1625            ValueKind::Text(s),
1626        ) => {
1627            let len = s.chars().count();
1628            if let Some(exact) = length {
1629                if len != *exact {
1630                    return Err(format!(
1631                        "'{}' has length {} but required length is {}",
1632                        s, len, exact
1633                    ));
1634                }
1635            }
1636            if !options.is_empty() && !options.iter().any(|opt| opt == s) {
1637                return Err(format!(
1638                    "'{}' is not in allowed options: {}",
1639                    s,
1640                    options.join(", ")
1641                ));
1642            }
1643            Ok(())
1644        }
1645        (
1646            TypeSpecification::Ratio {
1647                minimum,
1648                maximum,
1649                decimals,
1650                units,
1651                ..
1652            },
1653            ValueKind::Ratio(r, unit_name),
1654        ) => {
1655            use crate::computation::rational::checked_mul;
1656
1657            if let Some(d) = decimals {
1658                if exceeds_decimal_places(r, *d) {
1659                    return Err(format!(
1660                        "{} exceeds decimals constraint {d}",
1661                        format_rational_for_validation_message(expected_type, r)
1662                    ));
1663                }
1664            }
1665            if let Some(type_minimum) = minimum {
1666                if r < type_minimum {
1667                    let message = match unit_name.as_deref() {
1668                        Some(unit) => {
1669                            let ratio_unit = units.get(unit)?;
1670                            let value_per_unit = checked_mul(r, &ratio_unit.value)
1671                                .map_err(|failure| failure.to_string())?;
1672                            let bound_per_unit = ratio_unit.minimum.clone().expect(
1673                                "BUG: RatioUnit.minimum missing after type minimum set by sync_ratio_units_from_canonical",
1674                            );
1675                            format!(
1676                                "{} {unit} is below minimum {} {unit}",
1677                                format_rational_for_validation_message(
1678                                    expected_type,
1679                                    &value_per_unit
1680                                ),
1681                                format_rational_for_validation_message(
1682                                    expected_type,
1683                                    &bound_per_unit.clone()
1684                                ),
1685                            )
1686                        }
1687                        None => format!(
1688                            "{} is below minimum {}",
1689                            format_rational_for_validation_message(expected_type, r),
1690                            format_rational_for_validation_message(expected_type, type_minimum),
1691                        ),
1692                    };
1693                    return Err(message);
1694                }
1695            }
1696            if let Some(type_maximum) = maximum {
1697                if r > type_maximum {
1698                    let message = match unit_name.as_deref() {
1699                        Some(unit) => {
1700                            let ratio_unit = units.get(unit)?;
1701                            let value_per_unit = checked_mul(r, &ratio_unit.value)
1702                                .map_err(|failure| failure.to_string())?;
1703                            let bound_per_unit = ratio_unit.maximum.clone().expect(
1704                                "BUG: RatioUnit.maximum missing after type maximum set by sync_ratio_units_from_canonical",
1705                            );
1706                            format!(
1707                                "{} {unit} is above maximum {} {unit}",
1708                                format_rational_for_validation_message(
1709                                    expected_type,
1710                                    &value_per_unit
1711                                ),
1712                                format_rational_for_validation_message(
1713                                    expected_type,
1714                                    &bound_per_unit.clone()
1715                                ),
1716                            )
1717                        }
1718                        None => format!(
1719                            "{} is above maximum {}",
1720                            format_rational_for_validation_message(expected_type, r),
1721                            format_rational_for_validation_message(expected_type, type_maximum),
1722                        ),
1723                    };
1724                    return Err(message);
1725                }
1726            }
1727            Ok(())
1728        }
1729        (
1730            TypeSpecification::Date {
1731                minimum, maximum, ..
1732            },
1733            ValueKind::Date(dt),
1734        ) => {
1735            use crate::planning::semantics::{compare_semantic_dates, date_time_to_semantic};
1736            use std::cmp::Ordering;
1737            if let Some(min) = minimum {
1738                let min_sem = date_time_to_semantic(min);
1739                if compare_semantic_dates(dt, &min_sem) == Ordering::Less {
1740                    return Err(format!("{} is below minimum {}", dt, min));
1741                }
1742            }
1743            if let Some(max) = maximum {
1744                let max_sem = date_time_to_semantic(max);
1745                if compare_semantic_dates(dt, &max_sem) == Ordering::Greater {
1746                    return Err(format!("{} is above maximum {}", dt, max));
1747                }
1748            }
1749            Ok(())
1750        }
1751        (
1752            TypeSpecification::Time {
1753                minimum, maximum, ..
1754            },
1755            ValueKind::Time(t),
1756        ) => {
1757            use crate::planning::semantics::{compare_semantic_times, time_to_semantic};
1758            use std::cmp::Ordering;
1759            if let Some(min) = minimum {
1760                let min_sem = time_to_semantic(min);
1761                if compare_semantic_times(t, &min_sem) == Ordering::Less {
1762                    return Err(format!("{} is below minimum {}", t, min));
1763                }
1764            }
1765            if let Some(max) = maximum {
1766                let max_sem = time_to_semantic(max);
1767                if compare_semantic_times(t, &max_sem) == Ordering::Greater {
1768                    return Err(format!("{} is above maximum {}", t, max));
1769                }
1770            }
1771            Ok(())
1772        }
1773        (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
1774        | (TypeSpecification::NumberRange { .. }, ValueKind::Range(_, _))
1775        | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
1776        | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
1777        | (TypeSpecification::QuantityRange { .. }, ValueKind::Range(_, _))
1778        | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
1779        | (TypeSpecification::Veto { .. }, _)
1780        | (TypeSpecification::Undetermined, _) => Ok(()),
1781        (spec, value_kind) if !value_kind_matches_spec(value_kind, spec) => unreachable!(
1782            "BUG: validate_value_against_type called with mismatched type/value: \
1783             spec={:?}, value={:?} — typing must be enforced before validation",
1784            spec, value_kind
1785        ),
1786        (_, _) => Ok(()),
1787    }
1788}
1789
1790pub(crate) fn validate_literal_data_against_types(plan: &ExecutionPlan) -> Vec<Error> {
1791    let mut errors = Vec::new();
1792
1793    for (data_path, data_definition) in &plan.data {
1794        let (expected_type, lit) = match data_definition {
1795            DataDefinition::Value { value, .. } => (&value.lemma_type, value),
1796            DataDefinition::TypeDeclaration { .. }
1797            | DataDefinition::Import { .. }
1798            | DataDefinition::Reference { .. } => continue,
1799        };
1800
1801        if let Err(msg) = validate_value_against_type(expected_type, lit) {
1802            let source = data_definition.source().clone();
1803            errors.push(Error::validation(
1804                format!(
1805                    "Invalid value for data {} (expected {}): {}",
1806                    data_path,
1807                    expected_type.name().as_str(),
1808                    msg
1809                ),
1810                Some(source),
1811                None::<String>,
1812            ));
1813        }
1814    }
1815
1816    errors
1817}
1818
1819pub(crate) fn validate_unit_conversion_targets(plan: &ExecutionPlan) -> Result<(), Error> {
1820    for rule in &plan.rules {
1821        for instructions in [&rule.instructions, &rule.source_instructions] {
1822            for insn in &instructions.code {
1823                let Instruction::UnitConversion { target, .. } = insn else {
1824                    continue;
1825                };
1826                let Some((unit_name, owning_type)) =
1827                    crate::computation::units::conversion_target_declares_unit(target)
1828                else {
1829                    continue;
1830                };
1831                if crate::computation::units::owning_type_declares_unit_name(
1832                    owning_type.as_ref(),
1833                    unit_name,
1834                ) {
1835                    continue;
1836                }
1837                return Err(Error::validation(
1838                    format!(
1839                        "Unit conversion target '{unit_name}' is not declared on owning type '{}'",
1840                        owning_type.name()
1841                    ),
1842                    None::<Source>,
1843                    Some(plan.spec_name.clone()),
1844                ));
1845            }
1846        }
1847    }
1848    Ok(())
1849}
1850
1851pub(crate) fn validate_unit_index_references(plan: &ExecutionPlan) -> Result<(), Error> {
1852    validate_unit_conversion_targets(plan)
1853}
1854
1855/// The serializable form of an [`ExecutionPlan`].
1856///
1857/// `ExecutionPlan` itself is not `Serialize`/`Deserialize`: it contains derived
1858/// runtime state (`signature_index`, `resolved_types.resolved`,
1859/// `resolved_types.declared_defaults`) that is either recomputed on reconstruction
1860/// or belongs to the planning phase only. This struct is the sole canonical
1861/// representation for persistence and transport.
1862///
1863/// Convert via [`From<&ExecutionPlan>`] to serialize and [`TryFrom<ExecutionPlanSerialized>`]
1864/// to reconstruct.
1865#[derive(Debug, Clone, Serialize, Deserialize)]
1866pub struct ExecutionPlanSerialized {
1867    pub spec_name: String,
1868    #[serde(skip_serializing_if = "Option::is_none", default)]
1869    pub commentary: Option<String>,
1870    #[serde(
1871        serialize_with = "serialize_resolved_data_value_map",
1872        deserialize_with = "deserialize_resolved_data_value_map"
1873    )]
1874    pub data: IndexMap<DataPath, DataDefinition>,
1875    #[serde(default)]
1876    pub rules: Vec<ExecutableRule>,
1877    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1878    pub reference_evaluation_order: Vec<DataPath>,
1879    #[serde(default)]
1880    pub meta: HashMap<String, MetaValue>,
1881    /// Only the unit index is persisted from `resolved_types`; the rest is
1882    /// ephemeral planning state that is not needed after planning.
1883    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1884    pub unit_index: HashMap<String, Arc<LemmaType>>,
1885    pub effective: EffectiveDate,
1886    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1887    pub sources: SpecSources,
1888}
1889
1890impl From<&ExecutionPlan> for ExecutionPlanSerialized {
1891    fn from(plan: &ExecutionPlan) -> Self {
1892        Self {
1893            spec_name: plan.spec_name.clone(),
1894            commentary: plan.commentary.clone(),
1895            data: plan.data.clone(),
1896            rules: plan.rules.clone(),
1897            reference_evaluation_order: plan.reference_evaluation_order.clone(),
1898            meta: plan.meta.clone(),
1899            unit_index: plan.resolved_types.unit_index.clone(),
1900            effective: plan.effective.clone(),
1901            sources: plan.sources.clone(),
1902        }
1903    }
1904}
1905
1906impl TryFrom<ExecutionPlanSerialized> for ExecutionPlan {
1907    type Error = crate::Error;
1908
1909    fn try_from(serialized: ExecutionPlanSerialized) -> Result<Self, Self::Error> {
1910        let signature_index = crate::planning::graph::build_signature_index(
1911            &serialized.spec_name,
1912            &serialized.unit_index,
1913        )?;
1914        // Serialized plans cross a trust boundary: a tampered or stale plan
1915        // must surface as an error here, never as a hang or crash in the
1916        // virtual machine.
1917        for rule in &serialized.rules {
1918            validate_instructions(&rule.instructions).map_err(|message| {
1919                crate::Error::request(
1920                    format!(
1921                        "Serialized execution plan for spec '{}' contains invalid instructions for rule '{}': {message}",
1922                        serialized.spec_name, rule.name
1923                    ),
1924                    None::<String>,
1925                )
1926            })?;
1927            validate_instructions(&rule.source_instructions).map_err(|message| {
1928                crate::Error::request(
1929                    format!(
1930                        "Serialized execution plan for spec '{}' contains invalid source instructions for rule '{}': {message}",
1931                        serialized.spec_name, rule.name
1932                    ),
1933                    None::<String>,
1934                )
1935            })?;
1936        }
1937        let max_register_count = serialized
1938            .rules
1939            .iter()
1940            .map(|rule| rule.instructions.register_count)
1941            .max()
1942            .unwrap_or(0);
1943        let plan = Self {
1944            spec_name: serialized.spec_name,
1945            commentary: serialized.commentary,
1946            data: serialized.data,
1947            rules: serialized.rules,
1948            max_register_count,
1949            reference_evaluation_order: serialized.reference_evaluation_order,
1950            meta: serialized.meta,
1951            resolved_types: ResolvedSpecTypes {
1952                unit_index: serialized.unit_index,
1953                ..ResolvedSpecTypes::default()
1954            },
1955            signature_index,
1956            effective: serialized.effective,
1957            sources: serialized.sources,
1958        };
1959        validate_unit_index_references(&plan).map_err(|error| {
1960            crate::Error::request(
1961                format!(
1962                    "Serialized execution plan for spec '{}' is invalid: {}",
1963                    plan.spec_name,
1964                    error.message()
1965                ),
1966                None::<String>,
1967            )
1968        })?;
1969        Ok(plan)
1970    }
1971}
1972
1973fn serialize_resolved_data_value_map<S>(
1974    map: &IndexMap<DataPath, DataDefinition>,
1975    serializer: S,
1976) -> Result<S::Ok, S::Error>
1977where
1978    S: Serializer,
1979{
1980    let entries: Vec<(&DataPath, &DataDefinition)> = map.iter().collect();
1981    entries.serialize(serializer)
1982}
1983
1984fn deserialize_resolved_data_value_map<'de, D>(
1985    deserializer: D,
1986) -> Result<IndexMap<DataPath, DataDefinition>, D::Error>
1987where
1988    D: Deserializer<'de>,
1989{
1990    let entries: Vec<(DataPath, DataDefinition)> = Vec::deserialize(deserializer)?;
1991    Ok(entries.into_iter().collect())
1992}
1993
1994#[cfg(test)]
1995mod tests {
1996    use super::*;
1997    use crate::computation::rational::{rational_new, rational_zero};
1998    use crate::literals::DateGranularity;
1999    use crate::parsing::ast::DateTimeValue;
2000    use crate::planning::semantics::{
2001        primitive_boolean, primitive_text, DataPath, LiteralValue, PathSegment, RulePath,
2002    };
2003    use crate::Engine;
2004    use serde_json;
2005    use std::str::FromStr;
2006    use std::sync::Arc;
2007
2008    fn default_limits() -> ResourceLimits {
2009        ResourceLimits::default()
2010    }
2011
2012    fn roundtrip_execution_plan(plan: &ExecutionPlan) -> ExecutionPlan {
2013        let serialized = ExecutionPlanSerialized::from(plan);
2014        let json = serde_json::to_string(&serialized).expect("Should serialize");
2015        let back: ExecutionPlanSerialized =
2016            serde_json::from_str(&json).expect("Should deserialize");
2017        ExecutionPlan::try_from(back).expect("Should reconstruct")
2018    }
2019
2020    fn input_data(pairs: &[(&str, &str)]) -> HashMap<String, DataValueInput> {
2021        pairs
2022            .iter()
2023            .map(|(k, v)| (k.to_string(), DataValueInput::convenience(*v)))
2024            .collect()
2025    }
2026
2027    #[test]
2028    fn test_with_raw_values() {
2029        let mut engine = Engine::new();
2030        engine
2031            .load(
2032                r#"
2033                spec test
2034                data age: number -> default 25
2035                "#,
2036                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2037                    "test.lemma",
2038                ))),
2039            )
2040            .unwrap();
2041
2042        let now = DateTimeValue::now();
2043        let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
2044        let data_path = DataPath::new(vec![], "age".to_string());
2045
2046        let values = input_data(&[("age", "30")]);
2047
2048        let overlay = DataOverlay::resolve(plan, values, &default_limits()).unwrap();
2049        let updated_value = overlay.values.get(&data_path).unwrap();
2050        match &updated_value.value {
2051            crate::planning::semantics::ValueKind::Number(n) => {
2052                assert_eq!(n, &rational_new(30, 1));
2053            }
2054            other => panic!("Expected number literal, got {:?}", other),
2055        }
2056    }
2057
2058    #[test]
2059    fn test_with_raw_values_type_mismatch() {
2060        let mut engine = Engine::new();
2061        engine
2062            .load(
2063                r#"
2064                spec test
2065                data age: number
2066                "#,
2067                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2068                    "test.lemma",
2069                ))),
2070            )
2071            .unwrap();
2072
2073        let now = DateTimeValue::now();
2074        let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
2075
2076        let values = input_data(&[("age", "thirty")]);
2077
2078        let overlay = DataOverlay::resolve(plan, values, &default_limits()).unwrap();
2079        let data_path = DataPath::new(vec![], "age".to_string());
2080        match overlay.violated.get(&data_path) {
2081            Some(reason) => {
2082                assert!(
2083                    reason.contains("number"),
2084                    "type mismatch must record violation reason, got: {reason}"
2085                );
2086            }
2087            None => panic!("expected violated data for age=thirty"),
2088        }
2089    }
2090
2091    #[test]
2092    fn test_with_raw_values_unknown_data() {
2093        let mut engine = Engine::new();
2094        engine
2095            .load(
2096                r#"
2097                spec test
2098                data known: number
2099                "#,
2100                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2101                    "test.lemma",
2102                ))),
2103            )
2104            .unwrap();
2105
2106        let now = DateTimeValue::now();
2107        let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
2108
2109        let values = input_data(&[("unknown", "30")]);
2110
2111        assert!(DataOverlay::resolve(plan, values, &default_limits()).is_err());
2112    }
2113
2114    #[test]
2115    fn test_with_raw_values_nested() {
2116        let mut engine = Engine::new();
2117        engine
2118            .load(
2119                r#"
2120                spec private
2121                data base_price: number
2122
2123                spec test
2124                uses rules: private
2125                "#,
2126                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2127                    "test.lemma",
2128                ))),
2129            )
2130            .unwrap();
2131
2132        let now = DateTimeValue::now();
2133        let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
2134
2135        let values = input_data(&[("rules.base_price", "100")]);
2136
2137        let overlay = DataOverlay::resolve(plan, values, &default_limits()).unwrap();
2138        let data_path = DataPath {
2139            segments: vec![PathSegment {
2140                data: "rules".to_string(),
2141                spec: "private".to_string(),
2142            }],
2143            data: "base_price".to_string(),
2144        };
2145        let updated_value = overlay.values.get(&data_path).unwrap();
2146        match &updated_value.value {
2147            crate::planning::semantics::ValueKind::Number(n) => {
2148                assert_eq!(n, &rational_new(100, 1));
2149            }
2150            other => panic!("Expected number literal, got {:?}", other),
2151        }
2152    }
2153
2154    fn test_source() -> Source {
2155        use crate::parsing::ast::Span;
2156        Source::new(
2157            crate::parsing::source::SourceType::Volatile,
2158            Span {
2159                start: 0,
2160                end: 0,
2161                line: 1,
2162                col: 0,
2163            },
2164        )
2165    }
2166
2167    fn create_literal_expr(value: LiteralValue) -> Expression {
2168        Expression::new(
2169            crate::planning::semantics::ExpressionKind::Literal(Box::new(value)),
2170            test_source(),
2171        )
2172    }
2173
2174    fn create_data_path_expr(path: DataPath) -> Expression {
2175        Expression::new(
2176            crate::planning::semantics::ExpressionKind::DataPath(path),
2177            test_source(),
2178        )
2179    }
2180
2181    fn constant_return_instructions(literal: LiteralValue) -> Instructions {
2182        Instructions {
2183            version: INSTRUCTIONS_VERSION,
2184            register_count: 1,
2185            register_types: vec![Arc::clone(&literal.lemma_type)],
2186            constants: vec![literal],
2187            data_manifest: Vec::new(),
2188            veto_messages: Vec::new(),
2189            arm_tags: Vec::new(),
2190            conversion_tags: Vec::new(),
2191            code: vec![
2192                Instruction::LoadConstant {
2193                    destination_register: 0,
2194                    constant_index: 0,
2195                },
2196                Instruction::Return { source_register: 0 },
2197            ],
2198        }
2199    }
2200
2201    fn create_number_literal(n: rust_decimal::Decimal) -> LiteralValue {
2202        LiteralValue::number_from_decimal(n)
2203    }
2204
2205    fn create_boolean_literal(b: bool) -> LiteralValue {
2206        LiteralValue::from_bool(b)
2207    }
2208
2209    fn create_text_literal(s: String) -> LiteralValue {
2210        LiteralValue::text(s)
2211    }
2212
2213    #[test]
2214    fn with_values_should_enforce_number_maximum_constraint() {
2215        // Higher-standard requirement: user input must be validated against type constraints.
2216        // If this test fails, Lemma accepts invalid values and gives false reassurance.
2217        let data_path = DataPath::new(vec![], "x".to_string());
2218
2219        let max10 = crate::planning::semantics::LemmaType::primitive(
2220            crate::planning::semantics::TypeSpecification::Number {
2221                minimum: None,
2222                maximum: Some(rational_new(10, 1)),
2223                decimals: None,
2224                help: String::new(),
2225            },
2226        );
2227        let source = Source::new(
2228            crate::parsing::source::SourceType::Volatile,
2229            crate::parsing::ast::Span {
2230                start: 0,
2231                end: 0,
2232                line: 1,
2233                col: 0,
2234            },
2235        );
2236        let mut data = IndexMap::new();
2237        data.insert(
2238            data_path.clone(),
2239            crate::planning::semantics::DataDefinition::Value {
2240                value: crate::planning::semantics::LiteralValue::number_with_type(
2241                    rational_new(0, 1),
2242                    Arc::new(max10.clone()),
2243                ),
2244                source: source.clone(),
2245            },
2246        );
2247
2248        let plan = ExecutionPlan {
2249            spec_name: "test".to_string(),
2250            commentary: None,
2251            data,
2252            rules: Vec::new(),
2253            max_register_count: 0,
2254            reference_evaluation_order: Vec::new(),
2255            meta: HashMap::new(),
2256            resolved_types: ResolvedSpecTypes::default(),
2257            signature_index: HashMap::new(),
2258            effective: EffectiveDate::Origin,
2259            sources: Vec::new(),
2260        };
2261
2262        let values = input_data(&[("x", "11")]);
2263
2264        let overlay = DataOverlay::resolve(&plan, values, &default_limits()).unwrap();
2265        match overlay.violated.get(&data_path) {
2266            Some(reason) => {
2267                assert!(
2268                    reason.contains("maximum") || reason.contains("10"),
2269                    "x=11 must violate maximum 10, got: {reason}"
2270                );
2271            }
2272            None => panic!("expected violated data for x=11"),
2273        }
2274    }
2275
2276    #[test]
2277    fn with_values_should_enforce_text_enum_options() {
2278        // Higher-standard requirement: enum options must be enforced for text types.
2279        let data_path = DataPath::new(vec![], "tier".to_string());
2280
2281        let tier = crate::planning::semantics::LemmaType::primitive(
2282            crate::planning::semantics::TypeSpecification::Text {
2283                length: None,
2284                options: vec!["silver".to_string(), "gold".to_string()],
2285                help: String::new(),
2286            },
2287        );
2288        let source = Source::new(
2289            crate::parsing::source::SourceType::Volatile,
2290            crate::parsing::ast::Span {
2291                start: 0,
2292                end: 0,
2293                line: 1,
2294                col: 0,
2295            },
2296        );
2297        let mut data = IndexMap::new();
2298        data.insert(
2299            data_path.clone(),
2300            crate::planning::semantics::DataDefinition::Value {
2301                value: crate::planning::semantics::LiteralValue::text_with_type(
2302                    "silver".to_string(),
2303                    Arc::new(tier.clone()),
2304                ),
2305                source,
2306            },
2307        );
2308
2309        let plan = ExecutionPlan {
2310            spec_name: "test".to_string(),
2311            commentary: None,
2312            data,
2313            rules: Vec::new(),
2314            max_register_count: 0,
2315            reference_evaluation_order: Vec::new(),
2316            meta: HashMap::new(),
2317            resolved_types: ResolvedSpecTypes::default(),
2318            signature_index: HashMap::new(),
2319            effective: EffectiveDate::Origin,
2320            sources: Vec::new(),
2321        };
2322
2323        let values = input_data(&[("tier", "platinum")]);
2324
2325        let overlay = DataOverlay::resolve(&plan, values, &default_limits()).unwrap();
2326        match overlay.violated.get(&data_path) {
2327            Some(reason) => {
2328                assert!(
2329                    reason.contains("allowed options") || reason.contains("platinum"),
2330                    "invalid enum must record violation, got: {reason}"
2331                );
2332            }
2333            None => panic!("expected violated data for tier=platinum"),
2334        }
2335    }
2336
2337    #[test]
2338    fn with_values_should_enforce_quantity_decimals() {
2339        // Higher-standard requirement: decimals should be enforced on quantity inputs,
2340        // unless the language explicitly defines rounding semantics.
2341        let data_path = DataPath::new(vec![], "price".to_string());
2342
2343        let money = crate::planning::semantics::LemmaType::primitive(
2344            crate::planning::semantics::TypeSpecification::Quantity {
2345                minimum: None,
2346                maximum: None,
2347                decimals: Some(2),
2348                units: crate::planning::semantics::QuantityUnits::from(vec![
2349                    crate::planning::semantics::QuantityUnit::from_decimal_factor(
2350                        "eur".to_string(),
2351                        rust_decimal::Decimal::from_str("1.0").unwrap(),
2352                        Vec::new(),
2353                    )
2354                    .expect("eur unit factor must be exact decimal"),
2355                ]),
2356                traits: Vec::new(),
2357                decomposition: None,
2358                help: String::new(),
2359            },
2360        );
2361        let source = Source::new(
2362            crate::parsing::source::SourceType::Volatile,
2363            crate::parsing::ast::Span {
2364                start: 0,
2365                end: 0,
2366                line: 1,
2367                col: 0,
2368            },
2369        );
2370        let mut data = IndexMap::new();
2371        data.insert(
2372            data_path.clone(),
2373            crate::planning::semantics::DataDefinition::Value {
2374                value: crate::planning::semantics::LiteralValue::quantity_with_type(
2375                    rational_zero(),
2376                    "eur".to_string(),
2377                    Arc::new(money.clone()),
2378                ),
2379                source,
2380            },
2381        );
2382
2383        let plan = ExecutionPlan {
2384            spec_name: "test".to_string(),
2385            commentary: None,
2386            data,
2387            rules: Vec::new(),
2388            max_register_count: 0,
2389            reference_evaluation_order: Vec::new(),
2390            meta: HashMap::new(),
2391            resolved_types: ResolvedSpecTypes::default(),
2392            signature_index: HashMap::new(),
2393            effective: EffectiveDate::Origin,
2394            sources: Vec::new(),
2395        };
2396
2397        let values = input_data(&[("price", "1.234 eur")]);
2398
2399        let overlay = DataOverlay::resolve(&plan, values, &default_limits()).unwrap();
2400        match overlay.violated.get(&data_path) {
2401            Some(reason) => {
2402                assert!(
2403                    reason.contains("decimals") || reason.contains("decimal"),
2404                    "1.234 eur must violate decimals=2, got: {reason}"
2405                );
2406            }
2407            None => panic!("expected violated data for price=1.234 eur"),
2408        }
2409    }
2410
2411    #[test]
2412    fn test_serialize_deserialize_execution_plan() {
2413        let data_path = DataPath {
2414            segments: vec![],
2415            data: "age".to_string(),
2416        };
2417        let mut data = IndexMap::new();
2418        data.insert(
2419            data_path.clone(),
2420            crate::planning::semantics::DataDefinition::Value {
2421                value: create_number_literal(0.into()),
2422                source: test_source(),
2423            },
2424        );
2425        let plan = ExecutionPlan {
2426            spec_name: "test".to_string(),
2427            commentary: None,
2428            data,
2429            rules: Vec::new(),
2430            max_register_count: 0,
2431            reference_evaluation_order: Vec::new(),
2432            meta: HashMap::new(),
2433            resolved_types: ResolvedSpecTypes::default(),
2434            signature_index: HashMap::new(),
2435            effective: EffectiveDate::Origin,
2436            sources: Vec::new(),
2437        };
2438
2439        let deserialized = roundtrip_execution_plan(&plan);
2440
2441        assert_eq!(deserialized.spec_name, plan.spec_name);
2442        assert_eq!(deserialized.data.len(), plan.data.len());
2443        assert_eq!(deserialized.rules.len(), plan.rules.len());
2444    }
2445
2446    #[test]
2447    fn test_serialize_deserialize_plan_with_imported_named_type_defining_spec() {
2448        let dep_spec = Arc::new(crate::parsing::ast::LemmaSpec::new("examples".to_string()));
2449        let imported_type = crate::planning::semantics::LemmaType::new(
2450            "salary".to_string(),
2451            TypeSpecification::quantity(),
2452            crate::planning::semantics::TypeExtends::Custom {
2453                parent: "money".to_string(),
2454                family: "money".to_string(),
2455                defining_spec: crate::planning::semantics::TypeDefiningSpec::Import {
2456                    spec: Arc::clone(&dep_spec),
2457                },
2458            },
2459        );
2460
2461        let salary_path = DataPath::new(vec![], "salary".to_string());
2462        let mut data = IndexMap::new();
2463        data.insert(
2464            salary_path,
2465            crate::planning::semantics::DataDefinition::TypeDeclaration {
2466                resolved_type: Arc::new(imported_type),
2467                declared_default: None,
2468                source: test_source(),
2469            },
2470        );
2471
2472        let plan = ExecutionPlan {
2473            spec_name: "test".to_string(),
2474            commentary: None,
2475            data,
2476            rules: Vec::new(),
2477            max_register_count: 0,
2478            reference_evaluation_order: Vec::new(),
2479            meta: HashMap::new(),
2480            resolved_types: ResolvedSpecTypes::default(),
2481            signature_index: HashMap::new(),
2482            effective: EffectiveDate::Origin,
2483            sources: Vec::new(),
2484        };
2485
2486        let deserialized = roundtrip_execution_plan(&plan);
2487
2488        let recovered = deserialized
2489            .data
2490            .get(&DataPath::new(vec![], "salary".to_string()))
2491            .and_then(|d| d.schema_type())
2492            .expect("salary type should be present in plan.data");
2493        match &recovered.extends {
2494            crate::planning::semantics::TypeExtends::Custom {
2495                defining_spec: crate::planning::semantics::TypeDefiningSpec::Import { spec },
2496                ..
2497            } => {
2498                assert_eq!(spec.name, "examples");
2499            }
2500            other => panic!(
2501                "Expected imported defining_spec after round-trip, got {:?}",
2502                other
2503            ),
2504        }
2505    }
2506
2507    #[test]
2508    fn test_serialize_deserialize_plan_with_rules() {
2509        use crate::planning::semantics::ExpressionKind;
2510
2511        let age_path = DataPath::new(vec![], "age".to_string());
2512        let mut data = IndexMap::new();
2513        data.insert(
2514            age_path.clone(),
2515            crate::planning::semantics::DataDefinition::Value {
2516                value: create_number_literal(0.into()),
2517                source: test_source(),
2518            },
2519        );
2520        let mut plan = ExecutionPlan {
2521            spec_name: "test".to_string(),
2522            commentary: None,
2523            data,
2524            rules: Vec::new(),
2525            max_register_count: 0,
2526            reference_evaluation_order: Vec::new(),
2527            meta: HashMap::new(),
2528            resolved_types: ResolvedSpecTypes::default(),
2529            signature_index: HashMap::new(),
2530            effective: EffectiveDate::Origin,
2531            sources: Vec::new(),
2532        };
2533
2534        let rule = ExecutableRule {
2535            path: RulePath::new(vec![], "can_drive".to_string()),
2536            name: "can_drive".to_string(),
2537            branches: vec![{
2538                let result = create_literal_expr(create_boolean_literal(true));
2539                let condition = Expression::new(
2540                    ExpressionKind::Comparison(
2541                        Arc::new(create_data_path_expr(age_path.clone())),
2542                        crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2543                        Arc::new(create_literal_expr(create_number_literal(18.into()))),
2544                    ),
2545                    test_source(),
2546                );
2547                Branch {
2548                    condition: Some(condition.clone()),
2549                    result: result.clone(),
2550                    source: test_source(),
2551                }
2552            }],
2553            instructions: constant_return_instructions(create_boolean_literal(true)),
2554            source_instructions: constant_return_instructions(create_boolean_literal(true)),
2555            source: test_source(),
2556            rule_type: Arc::new(primitive_boolean().clone()),
2557        };
2558
2559        plan.rules.push(rule);
2560        plan.max_register_count = plan.rules[0].instructions.register_count;
2561
2562        let deserialized = roundtrip_execution_plan(&plan);
2563
2564        assert_eq!(deserialized.spec_name, plan.spec_name);
2565        assert_eq!(deserialized.data.len(), plan.data.len());
2566        assert_eq!(deserialized.rules.len(), plan.rules.len());
2567        assert_eq!(deserialized.rules[0].name, "can_drive");
2568        assert_eq!(deserialized.rules[0].branches.len(), 1);
2569    }
2570
2571    #[test]
2572    fn test_serialize_deserialize_plan_with_nested_data_paths() {
2573        use crate::planning::semantics::PathSegment;
2574        let data_path = DataPath {
2575            segments: vec![PathSegment {
2576                data: "employee".to_string(),
2577                spec: "private".to_string(),
2578            }],
2579            data: "salary".to_string(),
2580        };
2581
2582        let mut data = IndexMap::new();
2583        data.insert(
2584            data_path.clone(),
2585            crate::planning::semantics::DataDefinition::Value {
2586                value: create_number_literal(0.into()),
2587                source: test_source(),
2588            },
2589        );
2590        let plan = ExecutionPlan {
2591            spec_name: "test".to_string(),
2592            commentary: None,
2593            data,
2594            rules: Vec::new(),
2595            max_register_count: 0,
2596            reference_evaluation_order: Vec::new(),
2597            meta: HashMap::new(),
2598            resolved_types: ResolvedSpecTypes::default(),
2599            signature_index: HashMap::new(),
2600            effective: EffectiveDate::Origin,
2601            sources: Vec::new(),
2602        };
2603
2604        let deserialized = roundtrip_execution_plan(&plan);
2605
2606        assert_eq!(deserialized.data.len(), 1);
2607        let (deserialized_path, _) = deserialized.data.iter().next().unwrap();
2608        assert_eq!(deserialized_path.segments.len(), 1);
2609        assert_eq!(deserialized_path.segments[0].data, "employee");
2610        assert_eq!(deserialized_path.data, "salary");
2611    }
2612
2613    #[test]
2614    fn test_serialize_deserialize_plan_with_multiple_data_types() {
2615        let name_path = DataPath::new(vec![], "name".to_string());
2616        let age_path = DataPath::new(vec![], "age".to_string());
2617        let active_path = DataPath::new(vec![], "active".to_string());
2618
2619        let mut data = IndexMap::new();
2620        data.insert(
2621            name_path.clone(),
2622            crate::planning::semantics::DataDefinition::Value {
2623                value: create_text_literal("Alice".to_string()),
2624                source: test_source(),
2625            },
2626        );
2627        data.insert(
2628            age_path.clone(),
2629            crate::planning::semantics::DataDefinition::Value {
2630                value: create_number_literal(30.into()),
2631                source: test_source(),
2632            },
2633        );
2634        data.insert(
2635            active_path.clone(),
2636            crate::planning::semantics::DataDefinition::Value {
2637                value: create_boolean_literal(true),
2638                source: test_source(),
2639            },
2640        );
2641
2642        let plan = ExecutionPlan {
2643            spec_name: "test".to_string(),
2644            commentary: None,
2645            data,
2646            rules: Vec::new(),
2647            max_register_count: 0,
2648            reference_evaluation_order: Vec::new(),
2649            meta: HashMap::new(),
2650            resolved_types: ResolvedSpecTypes::default(),
2651            signature_index: HashMap::new(),
2652            effective: EffectiveDate::Origin,
2653            sources: Vec::new(),
2654        };
2655
2656        let deserialized = roundtrip_execution_plan(&plan);
2657
2658        assert_eq!(deserialized.data.len(), 3);
2659
2660        assert_eq!(
2661            deserialized.get_data_value(&name_path).unwrap().value,
2662            crate::planning::semantics::ValueKind::Text("Alice".to_string())
2663        );
2664        assert_eq!(
2665            deserialized.get_data_value(&age_path).unwrap().value,
2666            crate::planning::semantics::ValueKind::Number(rational_new(30, 1))
2667        );
2668        assert_eq!(
2669            deserialized.get_data_value(&active_path).unwrap().value,
2670            crate::planning::semantics::ValueKind::Boolean(true)
2671        );
2672    }
2673
2674    #[test]
2675    fn test_serialize_deserialize_plan_with_multiple_branches() {
2676        use crate::planning::semantics::ExpressionKind;
2677
2678        let points_path = DataPath::new(vec![], "points".to_string());
2679        let mut data = IndexMap::new();
2680        data.insert(
2681            points_path.clone(),
2682            crate::planning::semantics::DataDefinition::Value {
2683                value: create_number_literal(0.into()),
2684                source: test_source(),
2685            },
2686        );
2687        let mut plan = ExecutionPlan {
2688            spec_name: "test".to_string(),
2689            commentary: None,
2690            data,
2691            rules: Vec::new(),
2692            max_register_count: 0,
2693            reference_evaluation_order: Vec::new(),
2694            meta: HashMap::new(),
2695            resolved_types: ResolvedSpecTypes::default(),
2696            signature_index: HashMap::new(),
2697            effective: EffectiveDate::Origin,
2698            sources: Vec::new(),
2699        };
2700
2701        let rule = ExecutableRule {
2702            path: RulePath::new(vec![], "tier".to_string()),
2703            name: "tier".to_string(),
2704            branches: vec![
2705                {
2706                    let result = create_literal_expr(create_text_literal("bronze".to_string()));
2707                    Branch {
2708                        condition: None,
2709                        result: result.clone(),
2710                        source: test_source(),
2711                    }
2712                },
2713                {
2714                    let result = create_literal_expr(create_text_literal("silver".to_string()));
2715                    Branch {
2716                        condition: Some(Expression::new(
2717                            ExpressionKind::Comparison(
2718                                Arc::new(create_data_path_expr(points_path.clone())),
2719                                crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2720                                Arc::new(create_literal_expr(create_number_literal(100.into()))),
2721                            ),
2722                            test_source(),
2723                        )),
2724                        result: result.clone(),
2725                        source: test_source(),
2726                    }
2727                },
2728                {
2729                    let result = create_literal_expr(create_text_literal("gold".to_string()));
2730                    Branch {
2731                        condition: Some(Expression::new(
2732                            ExpressionKind::Comparison(
2733                                Arc::new(create_data_path_expr(points_path.clone())),
2734                                crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2735                                Arc::new(create_literal_expr(create_number_literal(500.into()))),
2736                            ),
2737                            test_source(),
2738                        )),
2739                        result: result.clone(),
2740                        source: test_source(),
2741                    }
2742                },
2743            ],
2744            instructions: constant_return_instructions(create_text_literal("bronze".to_string())),
2745            source_instructions: constant_return_instructions(create_text_literal(
2746                "bronze".to_string(),
2747            )),
2748            source: test_source(),
2749            rule_type: Arc::new(primitive_text().clone()),
2750        };
2751
2752        plan.rules.push(rule);
2753        plan.max_register_count = plan.rules[0].instructions.register_count;
2754
2755        let deserialized = roundtrip_execution_plan(&plan);
2756
2757        assert_eq!(deserialized.rules.len(), 1);
2758        assert_eq!(deserialized.rules[0].branches.len(), 3);
2759        assert!(deserialized.rules[0].branches[0].condition.is_none());
2760        assert!(deserialized.rules[0].branches[1].condition.is_some());
2761        assert!(deserialized.rules[0].branches[2].condition.is_some());
2762    }
2763
2764    #[test]
2765    fn test_serialize_deserialize_empty_plan() {
2766        let plan = ExecutionPlan {
2767            spec_name: "empty".to_string(),
2768            commentary: None,
2769            data: IndexMap::new(),
2770            rules: Vec::new(),
2771            max_register_count: 0,
2772            reference_evaluation_order: Vec::new(),
2773            meta: HashMap::new(),
2774            resolved_types: ResolvedSpecTypes::default(),
2775            signature_index: HashMap::new(),
2776            effective: EffectiveDate::Origin,
2777            sources: Vec::new(),
2778        };
2779
2780        let deserialized = roundtrip_execution_plan(&plan);
2781
2782        assert_eq!(deserialized.spec_name, "empty");
2783        assert_eq!(deserialized.data.len(), 0);
2784        assert_eq!(deserialized.rules.len(), 0);
2785    }
2786
2787    #[test]
2788    fn test_serialize_deserialize_plan_with_arithmetic_expressions() {
2789        use crate::planning::semantics::ExpressionKind;
2790
2791        let x_path = DataPath::new(vec![], "x".to_string());
2792        let mut data = IndexMap::new();
2793        data.insert(
2794            x_path.clone(),
2795            crate::planning::semantics::DataDefinition::Value {
2796                value: create_number_literal(0.into()),
2797                source: test_source(),
2798            },
2799        );
2800        let mut plan = ExecutionPlan {
2801            spec_name: "test".to_string(),
2802            commentary: None,
2803            data,
2804            rules: Vec::new(),
2805            max_register_count: 0,
2806            reference_evaluation_order: Vec::new(),
2807            meta: HashMap::new(),
2808            resolved_types: ResolvedSpecTypes::default(),
2809            signature_index: HashMap::new(),
2810            effective: EffectiveDate::Origin,
2811            sources: Vec::new(),
2812        };
2813
2814        let rule = ExecutableRule {
2815            path: RulePath::new(vec![], "doubled".to_string()),
2816            name: "doubled".to_string(),
2817            branches: vec![{
2818                let result = Expression::new(
2819                    ExpressionKind::Arithmetic(
2820                        Arc::new(create_data_path_expr(x_path.clone())),
2821                        crate::parsing::ast::ArithmeticComputation::Multiply,
2822                        Arc::new(create_literal_expr(create_number_literal(2.into()))),
2823                    ),
2824                    test_source(),
2825                );
2826                Branch {
2827                    condition: None,
2828                    result: result.clone(),
2829                    source: test_source(),
2830                }
2831            }],
2832            instructions: constant_return_instructions(create_number_literal(0.into())),
2833            source_instructions: constant_return_instructions(create_number_literal(0.into())),
2834            source: test_source(),
2835            rule_type: Arc::new(crate::planning::semantics::primitive_number().clone()),
2836        };
2837
2838        plan.rules.push(rule);
2839        plan.max_register_count = plan.rules[0].instructions.register_count;
2840
2841        let deserialized = roundtrip_execution_plan(&plan);
2842
2843        assert_eq!(deserialized.rules.len(), 1);
2844        match &deserialized.rules[0].branches[0].result.kind {
2845            ExpressionKind::Arithmetic(left, op, right) => {
2846                assert_eq!(*op, crate::parsing::ast::ArithmeticComputation::Multiply);
2847                match &left.kind {
2848                    ExpressionKind::DataPath(_) => {}
2849                    _ => panic!("Expected DataPath in left operand"),
2850                }
2851                match &right.kind {
2852                    ExpressionKind::Literal(_) => {}
2853                    _ => panic!("Expected Literal in right operand"),
2854                }
2855            }
2856            _ => panic!("Expected Arithmetic expression"),
2857        }
2858    }
2859
2860    #[test]
2861    fn test_serialize_deserialize_round_trip_equality() {
2862        use crate::planning::semantics::ExpressionKind;
2863
2864        let age_path = DataPath::new(vec![], "age".to_string());
2865        let mut data = IndexMap::new();
2866        data.insert(
2867            age_path.clone(),
2868            crate::planning::semantics::DataDefinition::Value {
2869                value: create_number_literal(0.into()),
2870                source: test_source(),
2871            },
2872        );
2873        let mut plan = ExecutionPlan {
2874            spec_name: "test".to_string(),
2875            commentary: None,
2876            data,
2877            rules: Vec::new(),
2878            max_register_count: 0,
2879            reference_evaluation_order: Vec::new(),
2880            meta: HashMap::new(),
2881            resolved_types: ResolvedSpecTypes::default(),
2882            signature_index: HashMap::new(),
2883            effective: EffectiveDate::Origin,
2884            sources: Vec::new(),
2885        };
2886
2887        let rule = ExecutableRule {
2888            path: RulePath::new(vec![], "is_adult".to_string()),
2889            name: "is_adult".to_string(),
2890            branches: vec![{
2891                let result = create_literal_expr(create_boolean_literal(true));
2892                let condition = Expression::new(
2893                    ExpressionKind::Comparison(
2894                        Arc::new(create_data_path_expr(age_path.clone())),
2895                        crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2896                        Arc::new(create_literal_expr(create_number_literal(18.into()))),
2897                    ),
2898                    test_source(),
2899                );
2900                Branch {
2901                    condition: Some(condition.clone()),
2902                    result: result.clone(),
2903                    source: test_source(),
2904                }
2905            }],
2906            instructions: constant_return_instructions(create_boolean_literal(true)),
2907            source_instructions: constant_return_instructions(create_boolean_literal(true)),
2908            source: test_source(),
2909            rule_type: Arc::new(primitive_boolean().clone()),
2910        };
2911
2912        plan.rules.push(rule);
2913        plan.max_register_count = plan.rules[0].instructions.register_count;
2914
2915        let deserialized = roundtrip_execution_plan(&plan);
2916        let deserialized2 = roundtrip_execution_plan(&deserialized);
2917
2918        assert_eq!(deserialized2.spec_name, plan.spec_name);
2919        assert_eq!(deserialized2.data.len(), plan.data.len());
2920        assert_eq!(deserialized2.rules.len(), plan.rules.len());
2921        assert_eq!(deserialized2.rules[0].name, plan.rules[0].name);
2922        assert_eq!(
2923            deserialized2.rules[0].branches.len(),
2924            plan.rules[0].branches.len()
2925        );
2926    }
2927
2928    fn empty_plan(effective: crate::parsing::ast::EffectiveDate) -> ExecutionPlan {
2929        ExecutionPlan {
2930            spec_name: "s".into(),
2931            commentary: None,
2932            data: IndexMap::new(),
2933            rules: Vec::new(),
2934            max_register_count: 0,
2935            reference_evaluation_order: Vec::new(),
2936            meta: HashMap::new(),
2937            resolved_types: ResolvedSpecTypes::default(),
2938            signature_index: HashMap::new(),
2939            effective,
2940            sources: Vec::new(),
2941        }
2942    }
2943
2944    #[test]
2945    fn plan_at_exact_boundary_selects_later_slice() {
2946        use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2947
2948        let june = DateTimeValue {
2949            year: 2025,
2950            month: 6,
2951            day: 1,
2952            hour: 0,
2953            minute: 0,
2954            second: 0,
2955            microsecond: 0,
2956            timezone: None,
2957
2958            granularity: DateGranularity::Full,
2959        };
2960        let dec = DateTimeValue {
2961            year: 2025,
2962            month: 12,
2963            day: 1,
2964            hour: 0,
2965            minute: 0,
2966            second: 0,
2967            microsecond: 0,
2968            timezone: None,
2969
2970            granularity: DateGranularity::Full,
2971        };
2972
2973        let set = ExecutionPlanSet {
2974            spec_name: "s".into(),
2975            plans: vec![
2976                empty_plan(EffectiveDate::Origin),
2977                empty_plan(EffectiveDate::DateTimeValue(june.clone())),
2978                empty_plan(EffectiveDate::DateTimeValue(dec.clone())),
2979            ],
2980        };
2981
2982        assert!(std::ptr::eq(
2983            set.plan_at(&EffectiveDate::DateTimeValue(june.clone()))
2984                .expect("boundary instant"),
2985            &set.plans[1]
2986        ));
2987        assert!(std::ptr::eq(
2988            set.plan_at(&EffectiveDate::DateTimeValue(dec.clone()))
2989                .expect("dec boundary"),
2990            &set.plans[2]
2991        ));
2992    }
2993
2994    #[test]
2995    fn plan_at_day_before_boundary_stays_in_earlier_slice() {
2996        use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2997
2998        let june = DateTimeValue {
2999            year: 2025,
3000            month: 6,
3001            day: 1,
3002            hour: 0,
3003            minute: 0,
3004            second: 0,
3005            microsecond: 0,
3006            timezone: None,
3007
3008            granularity: DateGranularity::Full,
3009        };
3010        let may_end = DateTimeValue {
3011            year: 2025,
3012            month: 5,
3013            day: 31,
3014            hour: 23,
3015            minute: 59,
3016            second: 59,
3017            microsecond: 0,
3018            timezone: None,
3019
3020            granularity: DateGranularity::DateTime,
3021        };
3022
3023        let set = ExecutionPlanSet {
3024            spec_name: "s".into(),
3025            plans: vec![
3026                empty_plan(EffectiveDate::Origin),
3027                empty_plan(EffectiveDate::DateTimeValue(june)),
3028            ],
3029        };
3030
3031        assert!(std::ptr::eq(
3032            set.plan_at(&EffectiveDate::DateTimeValue(may_end))
3033                .expect("may 31"),
3034            &set.plans[0]
3035        ));
3036    }
3037
3038    #[test]
3039    fn plan_at_single_plan_matches_any_instant_after_start() {
3040        use crate::parsing::ast::{DateTimeValue, EffectiveDate};
3041
3042        let t = DateTimeValue {
3043            year: 2025,
3044            month: 3,
3045            day: 1,
3046            hour: 0,
3047            minute: 0,
3048            second: 0,
3049            microsecond: 0,
3050            timezone: None,
3051
3052            granularity: DateGranularity::Full,
3053        };
3054        let set = ExecutionPlanSet {
3055            spec_name: "s".into(),
3056            plans: vec![empty_plan(EffectiveDate::DateTimeValue(DateTimeValue {
3057                year: 2025,
3058                month: 1,
3059                day: 1,
3060                hour: 0,
3061                minute: 0,
3062                second: 0,
3063                microsecond: 0,
3064                timezone: None,
3065
3066                granularity: DateGranularity::Full,
3067            }))],
3068        };
3069        assert!(std::ptr::eq(
3070            set.plan_at(&EffectiveDate::DateTimeValue(t))
3071                .expect("inside single slice"),
3072            &set.plans[0]
3073        ));
3074    }
3075
3076    /// The schema JSON shape is the IO contract for every non-Rust consumer
3077    /// (WASM playground, Hex, HTTP, TypeScript). Nail the exact envelope.
3078    #[test]
3079    fn schema_json_shape_contract() {
3080        let mut engine = Engine::new();
3081        engine
3082            .load(
3083                r#"
3084                spec pricing
3085                data bridge_height: quantity
3086                  -> unit meter 1
3087                  -> default 100 meter
3088                data quantity: number -> minimum 0
3089                rule cost: bridge_height * quantity
3090                "#,
3091                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
3092                    "test.lemma",
3093                ))),
3094            )
3095            .unwrap();
3096        let now = DateTimeValue::now();
3097        let schema = engine
3098            .get_plan(None, "pricing", Some(&now))
3099            .unwrap()
3100            .schema(&DataOverlay::default());
3101
3102        let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
3103
3104        let bh = &value["data"]["bridge_height"];
3105        assert!(
3106            bh.is_object(),
3107            "data entry must be a named object, not tuple"
3108        );
3109        assert!(
3110            bh.get("type").is_some(),
3111            "data entry must expose `type` field"
3112        );
3113        assert!(
3114            bh.get("default").is_some(),
3115            "bridge_height exposes `-> default` as schema default suggestion"
3116        );
3117        assert!(
3118            bh.get("bound_value").is_none(),
3119            "bridge_height is not a spec-bound literal"
3120        );
3121
3122        let ty = &bh["type"];
3123        assert_eq!(
3124            ty["kind"], "quantity",
3125            "kind tag sits on the type object itself"
3126        );
3127        assert!(
3128            ty["units"].is_array(),
3129            "quantity-only fields flatten up to top level"
3130        );
3131        assert!(
3132            ty.get("options").is_none(),
3133            "text-only fields must not leak"
3134        );
3135
3136        let qty = &value["data"]["quantity"];
3137        assert_eq!(qty["type"]["kind"], "number");
3138        assert!(
3139            qty.get("default").is_none(),
3140            "quantity has no default suggestion"
3141        );
3142        assert!(
3143            qty.get("bound_value").is_none(),
3144            "quantity has no bound literal"
3145        );
3146
3147        let cost = &value["rules"]["cost"];
3148        assert_eq!(
3149            cost["kind"], "quantity",
3150            "rule types use the same flat shape"
3151        );
3152        assert!(
3153            cost["units"].is_array() && !cost["units"].as_array().unwrap().is_empty(),
3154            "quantity rule result types expose declared units"
3155        );
3156        assert!(
3157            cost["units"][0].get("factor").is_some(),
3158            "quantity rule units use factor field"
3159        );
3160    }
3161
3162    #[test]
3163    fn schema_rule_result_units_contract() {
3164        let mut engine = Engine::new();
3165        engine
3166            .load(
3167                r#"
3168                spec units_contract
3169                data money: quantity
3170                  -> unit eur 1
3171                  -> unit usd 0.91
3172                data rate: ratio
3173                  -> unit basis_points 10000
3174                  -> unit percent 100
3175                  -> default 500 basis_points
3176                rule total: money
3177                rule rate_out: rate
3178                "#,
3179                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
3180                    "units_contract.lemma",
3181                ))),
3182            )
3183            .unwrap();
3184        let now = DateTimeValue::now();
3185        let schema = engine
3186            .get_plan(None, "units_contract", Some(&now))
3187            .unwrap()
3188            .schema(&DataOverlay::default());
3189        let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
3190
3191        let money_units = &value["data"]["money"]["type"]["units"];
3192        assert!(money_units.is_array() && !money_units.as_array().unwrap().is_empty());
3193        assert!(money_units[0].get("name").is_some());
3194        assert!(money_units[0].get("factor").is_some());
3195        assert!(money_units[0]["factor"].get("numer").is_some());
3196        assert!(money_units[0]["factor"].get("denom").is_some());
3197
3198        let rate_units = &value["data"]["rate"]["type"]["units"];
3199        assert!(rate_units.is_array() && !rate_units.as_array().unwrap().is_empty());
3200        assert!(rate_units[0].get("name").is_some());
3201        assert!(rate_units[0].get("value").is_some());
3202        assert!(rate_units[0]["value"].get("numer").is_some());
3203        assert!(rate_units[0]["value"].get("denom").is_some());
3204
3205        let total_rule_units = &value["rules"]["total"]["units"];
3206        let money_unit_names: Vec<_> = money_units
3207            .as_array()
3208            .unwrap()
3209            .iter()
3210            .map(|u| u["name"].as_str().unwrap())
3211            .collect();
3212        let total_rule_unit_names: Vec<_> = total_rule_units
3213            .as_array()
3214            .unwrap()
3215            .iter()
3216            .map(|u| u["name"].as_str().unwrap())
3217            .collect();
3218        assert_eq!(total_rule_unit_names, money_unit_names);
3219
3220        let rate_out_rule_units = &value["rules"]["rate_out"]["units"];
3221        let rate_unit_names: Vec<_> = rate_units
3222            .as_array()
3223            .unwrap()
3224            .iter()
3225            .map(|u| u["name"].as_str().unwrap())
3226            .collect();
3227        let rate_out_rule_unit_names: Vec<_> = rate_out_rule_units
3228            .as_array()
3229            .unwrap()
3230            .iter()
3231            .map(|u| u["name"].as_str().unwrap())
3232            .collect();
3233        assert_eq!(rate_out_rule_unit_names, rate_unit_names);
3234    }
3235
3236    #[test]
3237    fn schema_json_round_trip_preserves_shape() {
3238        let mut engine = Engine::new();
3239        engine
3240            .load(
3241                r#"
3242                spec s
3243                data age: number -> minimum 0 -> default 18
3244                data grade: text -> options "A" "B" "C"
3245                rule adult: age >= 18
3246                "#,
3247                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("s.lemma"))),
3248            )
3249            .unwrap();
3250        let now = DateTimeValue::now();
3251        let schema = engine
3252            .get_plan(None, "s", Some(&now))
3253            .unwrap()
3254            .schema(&DataOverlay::default());
3255
3256        let json = serde_json::to_string(&schema).unwrap();
3257        let round_tripped: SpecSchema = serde_json::from_str(&json).unwrap();
3258        assert_eq!(schema, round_tripped);
3259    }
3260}
3261
3262// ---------------------------------------------------------------------------
3263// ExecutionPlanSet (formerly plan_set.rs)
3264// ---------------------------------------------------------------------------