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::{BTreeSet, HashMap, HashSet};
29use std::sync::Arc;
30
31/// One spec's contribution to an [`ExecutionPlan`], together with its
32/// formatted AST source.
33///
34/// `repository` is `None` for workspace (root) specs. Including the
35/// repository name means two specs with the same base name from different
36/// repos are always distinct entries.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct SpecSource {
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub repository: Option<String>,
41    pub name: String,
42    pub effective_from: EffectiveDate,
43    pub source: String,
44}
45
46pub type SpecSources = Vec<SpecSource>;
47
48/// A complete execution plan ready for the evaluator
49///
50/// Contains the topologically sorted list of rules to execute, along with all data.
51/// Self-contained structure - no spec lookups required during evaluation.
52#[derive(Debug, Clone)]
53pub struct ExecutionPlan {
54    /// Main spec name
55    pub spec_name: String,
56
57    /// Optional commentary from the `"""..."""` block in the spec source.
58    pub commentary: Option<String>,
59
60    /// Per-data data in definition order: value, type-only, or spec reference.
61    pub data: IndexMap<DataPath, DataDefinition>,
62
63    /// Rules to execute in topological order (sorted by dependencies)
64    pub rules: Vec<ExecutableRule>,
65
66    /// 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(r: &RationalInteger, decimals: Option<u8>) -> String {
1495        use crate::computation::rational::rational_to_display_str;
1496        match commit_rational_to_decimal(r) {
1497            Ok(decimal) => match decimals {
1498                Some(dp) => {
1499                    let rounded = decimal.round_dp(u32::from(dp));
1500                    format!("{:.prec$}", rounded, prec = dp as usize)
1501                }
1502                None => decimal.normalize().to_string(),
1503            },
1504            Err(_) => rational_to_display_str(r),
1505        }
1506    }
1507
1508    match (&expected_type.specifications, &value.value) {
1509        (
1510            TypeSpecification::Number {
1511                minimum,
1512                maximum,
1513                decimals,
1514                ..
1515            },
1516            ValueKind::Number(n),
1517        ) => {
1518            if let Some(d) = decimals {
1519                if exceeds_decimal_places(n, *d) {
1520                    return Err(format!(
1521                        "{} exceeds decimals constraint {d}",
1522                        format_rational(n, *decimals)
1523                    ));
1524                }
1525            }
1526            if let Some(min) = minimum {
1527                if n < min {
1528                    return Err(format!(
1529                        "{} is below minimum {}",
1530                        format_rational(n, *decimals),
1531                        format_rational(min, *decimals)
1532                    ));
1533                }
1534            }
1535            if let Some(max) = maximum {
1536                if n > max {
1537                    return Err(format!(
1538                        "{} is above maximum {}",
1539                        format_rational(n, *decimals),
1540                        format_rational(max, *decimals)
1541                    ));
1542                }
1543            }
1544            Ok(())
1545        }
1546        (
1547            TypeSpecification::Quantity {
1548                minimum,
1549                maximum,
1550                decimals,
1551                units,
1552                ..
1553            },
1554            ValueKind::Quantity(magnitude, signature),
1555        ) => {
1556            use crate::computation::rational::checked_div;
1557            use crate::planning::semantics::quantity_declared_bound_canonical;
1558            let unit = signature
1559                .first()
1560                .map(|(n, _)| n.as_str())
1561                .expect("BUG: Quantity value has empty signature in execution plan validation");
1562            let quantity_unit = units.get(unit)?;
1563            let factor = &quantity_unit.factor;
1564            let in_unit = checked_div(magnitude, factor).map_err(|failure| {
1565                format!("cannot de-canonicalize quantity for validation: {failure}")
1566            })?;
1567            if let Some(d) = decimals {
1568                if exceeds_decimal_places(&in_unit, *d) {
1569                    return Err(format!(
1570                        "{} {unit} exceeds decimals constraint {d}",
1571                        format_rational(&in_unit, *decimals)
1572                    ));
1573                }
1574            }
1575            if let Some(bound) = minimum {
1576                let canonical_min = quantity_declared_bound_canonical(
1577                    bound,
1578                    units,
1579                    expected_type.name().as_str(),
1580                    "minimum",
1581                )?;
1582                if magnitude < &canonical_min {
1583                    let min_in_unit = checked_div(&canonical_min, factor).map_err(|failure| {
1584                        format!("cannot de-canonicalize minimum for validation: {failure}")
1585                    })?;
1586                    let value_display =
1587                        format!("{} {}", format_rational(&in_unit, *decimals), unit);
1588                    let bound_display = format!(
1589                        "{} {}",
1590                        format_rational(&min_in_unit, *decimals),
1591                        quantity_unit.name
1592                    );
1593                    return Err(format!("{value_display} is below minimum {bound_display}"));
1594                }
1595            }
1596            if let Some(bound) = maximum {
1597                let canonical_max = quantity_declared_bound_canonical(
1598                    bound,
1599                    units,
1600                    expected_type.name().as_str(),
1601                    "maximum",
1602                )?;
1603                if magnitude > &canonical_max {
1604                    let max_in_unit = checked_div(&canonical_max, factor).map_err(|failure| {
1605                        format!("cannot de-canonicalize maximum for validation: {failure}")
1606                    })?;
1607                    let value_display =
1608                        format!("{} {}", format_rational(&in_unit, *decimals), unit);
1609                    let bound_display = format!(
1610                        "{} {}",
1611                        format_rational(&max_in_unit, *decimals),
1612                        quantity_unit.name
1613                    );
1614                    return Err(format!("{value_display} is above maximum {bound_display}"));
1615                }
1616            }
1617            Ok(())
1618        }
1619        (
1620            TypeSpecification::Text {
1621                length, options, ..
1622            },
1623            ValueKind::Text(s),
1624        ) => {
1625            let len = s.chars().count();
1626            if let Some(exact) = length {
1627                if len != *exact {
1628                    return Err(format!(
1629                        "'{}' has length {} but required length is {}",
1630                        s, len, exact
1631                    ));
1632                }
1633            }
1634            if !options.is_empty() && !options.iter().any(|opt| opt == s) {
1635                return Err(format!(
1636                    "'{}' is not in allowed options: {}",
1637                    s,
1638                    options.join(", ")
1639                ));
1640            }
1641            Ok(())
1642        }
1643        (
1644            TypeSpecification::Ratio {
1645                minimum,
1646                maximum,
1647                decimals,
1648                units,
1649                ..
1650            },
1651            ValueKind::Ratio(r, unit_name),
1652        ) => {
1653            use crate::computation::rational::checked_mul;
1654
1655            if let Some(d) = decimals {
1656                if exceeds_decimal_places(r, *d) {
1657                    return Err(format!(
1658                        "{} exceeds decimals constraint {d}",
1659                        format_rational(r, *decimals)
1660                    ));
1661                }
1662            }
1663            if let Some(type_minimum) = minimum {
1664                if r < type_minimum {
1665                    let message = match unit_name.as_deref() {
1666                        Some(unit) => {
1667                            let ratio_unit = units.get(unit)?;
1668                            let value_per_unit = checked_mul(r, &ratio_unit.value)
1669                                .map_err(|failure| failure.to_string())?;
1670                            let bound_per_unit = ratio_unit.minimum.clone().expect(
1671                                "BUG: RatioUnit.minimum missing after type minimum set by sync_ratio_units_from_canonical",
1672                            );
1673                            format!(
1674                                "{} {unit} is below minimum {} {unit}",
1675                                format_rational(&value_per_unit, *decimals),
1676                                format_rational(&bound_per_unit.clone(), *decimals),
1677                            )
1678                        }
1679                        None => format!(
1680                            "{} is below minimum {}",
1681                            format_rational(r, *decimals),
1682                            format_rational(type_minimum, *decimals),
1683                        ),
1684                    };
1685                    return Err(message);
1686                }
1687            }
1688            if let Some(type_maximum) = maximum {
1689                if r > type_maximum {
1690                    let message = match unit_name.as_deref() {
1691                        Some(unit) => {
1692                            let ratio_unit = units.get(unit)?;
1693                            let value_per_unit = checked_mul(r, &ratio_unit.value)
1694                                .map_err(|failure| failure.to_string())?;
1695                            let bound_per_unit = ratio_unit.maximum.clone().expect(
1696                                "BUG: RatioUnit.maximum missing after type maximum set by sync_ratio_units_from_canonical",
1697                            );
1698                            format!(
1699                                "{} {unit} is above maximum {} {unit}",
1700                                format_rational(&value_per_unit, *decimals),
1701                                format_rational(&bound_per_unit.clone(), *decimals),
1702                            )
1703                        }
1704                        None => format!(
1705                            "{} is above maximum {}",
1706                            format_rational(r, *decimals),
1707                            format_rational(type_maximum, *decimals),
1708                        ),
1709                    };
1710                    return Err(message);
1711                }
1712            }
1713            Ok(())
1714        }
1715        (
1716            TypeSpecification::Date {
1717                minimum, maximum, ..
1718            },
1719            ValueKind::Date(dt),
1720        ) => {
1721            use crate::planning::semantics::{compare_semantic_dates, date_time_to_semantic};
1722            use std::cmp::Ordering;
1723            if let Some(min) = minimum {
1724                let min_sem = date_time_to_semantic(min);
1725                if compare_semantic_dates(dt, &min_sem) == Ordering::Less {
1726                    return Err(format!("{} is below minimum {}", dt, min));
1727                }
1728            }
1729            if let Some(max) = maximum {
1730                let max_sem = date_time_to_semantic(max);
1731                if compare_semantic_dates(dt, &max_sem) == Ordering::Greater {
1732                    return Err(format!("{} is above maximum {}", dt, max));
1733                }
1734            }
1735            Ok(())
1736        }
1737        (
1738            TypeSpecification::Time {
1739                minimum, maximum, ..
1740            },
1741            ValueKind::Time(t),
1742        ) => {
1743            use crate::planning::semantics::{compare_semantic_times, time_to_semantic};
1744            use std::cmp::Ordering;
1745            if let Some(min) = minimum {
1746                let min_sem = time_to_semantic(min);
1747                if compare_semantic_times(t, &min_sem) == Ordering::Less {
1748                    return Err(format!("{} is below minimum {}", t, min));
1749                }
1750            }
1751            if let Some(max) = maximum {
1752                let max_sem = time_to_semantic(max);
1753                if compare_semantic_times(t, &max_sem) == Ordering::Greater {
1754                    return Err(format!("{} is above maximum {}", t, max));
1755                }
1756            }
1757            Ok(())
1758        }
1759        (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
1760        | (TypeSpecification::NumberRange { .. }, ValueKind::Range(_, _))
1761        | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
1762        | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
1763        | (TypeSpecification::QuantityRange { .. }, ValueKind::Range(_, _))
1764        | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
1765        | (TypeSpecification::Veto { .. }, _)
1766        | (TypeSpecification::Undetermined, _) => Ok(()),
1767        (spec, value_kind) if !value_kind_matches_spec(value_kind, spec) => unreachable!(
1768            "BUG: validate_value_against_type called with mismatched type/value: \
1769             spec={:?}, value={:?} — typing must be enforced before validation",
1770            spec, value_kind
1771        ),
1772        (_, _) => Ok(()),
1773    }
1774}
1775
1776pub(crate) fn validate_literal_data_against_types(plan: &ExecutionPlan) -> Vec<Error> {
1777    let mut errors = Vec::new();
1778
1779    for (data_path, data_definition) in &plan.data {
1780        let (expected_type, lit) = match data_definition {
1781            DataDefinition::Value { value, .. } => (&value.lemma_type, value),
1782            DataDefinition::TypeDeclaration { .. }
1783            | DataDefinition::Import { .. }
1784            | DataDefinition::Reference { .. } => continue,
1785        };
1786
1787        if let Err(msg) = validate_value_against_type(expected_type, lit) {
1788            let source = data_definition.source().clone();
1789            errors.push(Error::validation(
1790                format!(
1791                    "Invalid value for data {} (expected {}): {}",
1792                    data_path,
1793                    expected_type.name().as_str(),
1794                    msg
1795                ),
1796                Some(source),
1797                None::<String>,
1798            ));
1799        }
1800    }
1801
1802    errors
1803}
1804
1805fn collect_unit_conversion_targets_from_instructions(
1806    instructions: &Instructions,
1807    units: &mut BTreeSet<String>,
1808) {
1809    for insn in &instructions.code {
1810        if let Instruction::UnitConversion {
1811            target: SemanticConversionTarget::Unit { unit_name },
1812            ..
1813        } = insn
1814        {
1815            units.insert(unit_name.clone());
1816        }
1817    }
1818}
1819
1820pub(crate) fn validate_unit_index_references(plan: &ExecutionPlan) -> Result<(), Error> {
1821    let mut required_units = BTreeSet::new();
1822    for rule in &plan.rules {
1823        collect_unit_conversion_targets_from_instructions(&rule.instructions, &mut required_units);
1824    }
1825    for unit_name in required_units {
1826        if plan.resolved_types.unit_index.contains_key(&unit_name) {
1827            continue;
1828        }
1829        return Err(Error::validation(
1830            format!("Unknown unit '{unit_name}' in execution plan unit index."),
1831            None::<Source>,
1832            Some(plan.spec_name.clone()),
1833        ));
1834    }
1835    Ok(())
1836}
1837
1838/// The serializable form of an [`ExecutionPlan`].
1839///
1840/// `ExecutionPlan` itself is not `Serialize`/`Deserialize`: it contains derived
1841/// runtime state (`signature_index`, `resolved_types.resolved`,
1842/// `resolved_types.declared_defaults`) that is either recomputed on reconstruction
1843/// or belongs to the planning phase only. This struct is the sole canonical
1844/// representation for persistence and transport.
1845///
1846/// Convert via [`From<&ExecutionPlan>`] to serialize and [`TryFrom<ExecutionPlanSerialized>`]
1847/// to reconstruct.
1848#[derive(Debug, Clone, Serialize, Deserialize)]
1849pub struct ExecutionPlanSerialized {
1850    pub spec_name: String,
1851    #[serde(skip_serializing_if = "Option::is_none", default)]
1852    pub commentary: Option<String>,
1853    #[serde(
1854        serialize_with = "serialize_resolved_data_value_map",
1855        deserialize_with = "deserialize_resolved_data_value_map"
1856    )]
1857    pub data: IndexMap<DataPath, DataDefinition>,
1858    #[serde(default)]
1859    pub rules: Vec<ExecutableRule>,
1860    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1861    pub reference_evaluation_order: Vec<DataPath>,
1862    #[serde(default)]
1863    pub meta: HashMap<String, MetaValue>,
1864    /// Only the unit index is persisted from `resolved_types`; the rest is
1865    /// ephemeral planning state that is not needed after planning.
1866    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1867    pub unit_index: HashMap<String, Arc<LemmaType>>,
1868    pub effective: EffectiveDate,
1869    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1870    pub sources: SpecSources,
1871}
1872
1873impl From<&ExecutionPlan> for ExecutionPlanSerialized {
1874    fn from(plan: &ExecutionPlan) -> Self {
1875        Self {
1876            spec_name: plan.spec_name.clone(),
1877            commentary: plan.commentary.clone(),
1878            data: plan.data.clone(),
1879            rules: plan.rules.clone(),
1880            reference_evaluation_order: plan.reference_evaluation_order.clone(),
1881            meta: plan.meta.clone(),
1882            unit_index: plan.resolved_types.unit_index.clone(),
1883            effective: plan.effective.clone(),
1884            sources: plan.sources.clone(),
1885        }
1886    }
1887}
1888
1889impl TryFrom<ExecutionPlanSerialized> for ExecutionPlan {
1890    type Error = crate::Error;
1891
1892    fn try_from(serialized: ExecutionPlanSerialized) -> Result<Self, Self::Error> {
1893        let signature_index = crate::planning::graph::build_signature_index(
1894            &serialized.spec_name,
1895            &serialized.unit_index,
1896        )?;
1897        // Serialized plans cross a trust boundary: a tampered or stale plan
1898        // must surface as an error here, never as a hang or crash in the
1899        // virtual machine.
1900        for rule in &serialized.rules {
1901            validate_instructions(&rule.instructions).map_err(|message| {
1902                crate::Error::request(
1903                    format!(
1904                        "Serialized execution plan for spec '{}' contains invalid instructions for rule '{}': {message}",
1905                        serialized.spec_name, rule.name
1906                    ),
1907                    None::<String>,
1908                )
1909            })?;
1910            validate_instructions(&rule.source_instructions).map_err(|message| {
1911                crate::Error::request(
1912                    format!(
1913                        "Serialized execution plan for spec '{}' contains invalid source instructions for rule '{}': {message}",
1914                        serialized.spec_name, rule.name
1915                    ),
1916                    None::<String>,
1917                )
1918            })?;
1919        }
1920        let max_register_count = serialized
1921            .rules
1922            .iter()
1923            .map(|rule| rule.instructions.register_count)
1924            .max()
1925            .unwrap_or(0);
1926        Ok(Self {
1927            spec_name: serialized.spec_name,
1928            commentary: serialized.commentary,
1929            data: serialized.data,
1930            rules: serialized.rules,
1931            max_register_count,
1932            reference_evaluation_order: serialized.reference_evaluation_order,
1933            meta: serialized.meta,
1934            resolved_types: ResolvedSpecTypes {
1935                unit_index: serialized.unit_index,
1936                ..ResolvedSpecTypes::default()
1937            },
1938            signature_index,
1939            effective: serialized.effective,
1940            sources: serialized.sources,
1941        })
1942    }
1943}
1944
1945fn serialize_resolved_data_value_map<S>(
1946    map: &IndexMap<DataPath, DataDefinition>,
1947    serializer: S,
1948) -> Result<S::Ok, S::Error>
1949where
1950    S: Serializer,
1951{
1952    let entries: Vec<(&DataPath, &DataDefinition)> = map.iter().collect();
1953    entries.serialize(serializer)
1954}
1955
1956fn deserialize_resolved_data_value_map<'de, D>(
1957    deserializer: D,
1958) -> Result<IndexMap<DataPath, DataDefinition>, D::Error>
1959where
1960    D: Deserializer<'de>,
1961{
1962    let entries: Vec<(DataPath, DataDefinition)> = Vec::deserialize(deserializer)?;
1963    Ok(entries.into_iter().collect())
1964}
1965
1966#[cfg(test)]
1967mod tests {
1968    use super::*;
1969    use crate::computation::rational::{rational_new, rational_zero};
1970    use crate::literals::DateGranularity;
1971    use crate::parsing::ast::DateTimeValue;
1972    use crate::planning::semantics::{
1973        primitive_boolean, primitive_text, DataPath, LiteralValue, PathSegment, RulePath,
1974    };
1975    use crate::Engine;
1976    use serde_json;
1977    use std::str::FromStr;
1978    use std::sync::Arc;
1979
1980    fn default_limits() -> ResourceLimits {
1981        ResourceLimits::default()
1982    }
1983
1984    fn roundtrip_execution_plan(plan: &ExecutionPlan) -> ExecutionPlan {
1985        let serialized = ExecutionPlanSerialized::from(plan);
1986        let json = serde_json::to_string(&serialized).expect("Should serialize");
1987        let back: ExecutionPlanSerialized =
1988            serde_json::from_str(&json).expect("Should deserialize");
1989        ExecutionPlan::try_from(back).expect("Should reconstruct")
1990    }
1991
1992    fn input_data(pairs: &[(&str, &str)]) -> HashMap<String, DataValueInput> {
1993        pairs
1994            .iter()
1995            .map(|(k, v)| (k.to_string(), DataValueInput::convenience(*v)))
1996            .collect()
1997    }
1998
1999    #[test]
2000    fn test_with_raw_values() {
2001        let mut engine = Engine::new();
2002        engine
2003            .load(
2004                r#"
2005                spec test
2006                data age: number -> default 25
2007                "#,
2008                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2009                    "test.lemma",
2010                ))),
2011            )
2012            .unwrap();
2013
2014        let now = DateTimeValue::now();
2015        let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
2016        let data_path = DataPath::new(vec![], "age".to_string());
2017
2018        let values = input_data(&[("age", "30")]);
2019
2020        let overlay = DataOverlay::resolve(plan, values, &default_limits()).unwrap();
2021        let updated_value = overlay.values.get(&data_path).unwrap();
2022        match &updated_value.value {
2023            crate::planning::semantics::ValueKind::Number(n) => {
2024                assert_eq!(n, &rational_new(30, 1));
2025            }
2026            other => panic!("Expected number literal, got {:?}", other),
2027        }
2028    }
2029
2030    #[test]
2031    fn test_with_raw_values_type_mismatch() {
2032        let mut engine = Engine::new();
2033        engine
2034            .load(
2035                r#"
2036                spec test
2037                data age: number
2038                "#,
2039                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2040                    "test.lemma",
2041                ))),
2042            )
2043            .unwrap();
2044
2045        let now = DateTimeValue::now();
2046        let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
2047
2048        let values = input_data(&[("age", "thirty")]);
2049
2050        let overlay = DataOverlay::resolve(plan, values, &default_limits()).unwrap();
2051        let data_path = DataPath::new(vec![], "age".to_string());
2052        match overlay.violated.get(&data_path) {
2053            Some(reason) => {
2054                assert!(
2055                    reason.contains("number"),
2056                    "type mismatch must record violation reason, got: {reason}"
2057                );
2058            }
2059            None => panic!("expected violated data for age=thirty"),
2060        }
2061    }
2062
2063    #[test]
2064    fn test_with_raw_values_unknown_data() {
2065        let mut engine = Engine::new();
2066        engine
2067            .load(
2068                r#"
2069                spec test
2070                data known: number
2071                "#,
2072                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2073                    "test.lemma",
2074                ))),
2075            )
2076            .unwrap();
2077
2078        let now = DateTimeValue::now();
2079        let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
2080
2081        let values = input_data(&[("unknown", "30")]);
2082
2083        assert!(DataOverlay::resolve(plan, values, &default_limits()).is_err());
2084    }
2085
2086    #[test]
2087    fn test_with_raw_values_nested() {
2088        let mut engine = Engine::new();
2089        engine
2090            .load(
2091                r#"
2092                spec private
2093                data base_price: number
2094
2095                spec test
2096                uses rules: private
2097                "#,
2098                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2099                    "test.lemma",
2100                ))),
2101            )
2102            .unwrap();
2103
2104        let now = DateTimeValue::now();
2105        let plan = engine.get_plan(None, "test", Some(&now)).unwrap();
2106
2107        let values = input_data(&[("rules.base_price", "100")]);
2108
2109        let overlay = DataOverlay::resolve(plan, values, &default_limits()).unwrap();
2110        let data_path = DataPath {
2111            segments: vec![PathSegment {
2112                data: "rules".to_string(),
2113                spec: "private".to_string(),
2114            }],
2115            data: "base_price".to_string(),
2116        };
2117        let updated_value = overlay.values.get(&data_path).unwrap();
2118        match &updated_value.value {
2119            crate::planning::semantics::ValueKind::Number(n) => {
2120                assert_eq!(n, &rational_new(100, 1));
2121            }
2122            other => panic!("Expected number literal, got {:?}", other),
2123        }
2124    }
2125
2126    fn test_source() -> Source {
2127        use crate::parsing::ast::Span;
2128        Source::new(
2129            crate::parsing::source::SourceType::Volatile,
2130            Span {
2131                start: 0,
2132                end: 0,
2133                line: 1,
2134                col: 0,
2135            },
2136        )
2137    }
2138
2139    fn create_literal_expr(value: LiteralValue) -> Expression {
2140        Expression::new(
2141            crate::planning::semantics::ExpressionKind::Literal(Box::new(value)),
2142            test_source(),
2143        )
2144    }
2145
2146    fn create_data_path_expr(path: DataPath) -> Expression {
2147        Expression::new(
2148            crate::planning::semantics::ExpressionKind::DataPath(path),
2149            test_source(),
2150        )
2151    }
2152
2153    fn constant_return_instructions(literal: LiteralValue) -> Instructions {
2154        Instructions {
2155            version: INSTRUCTIONS_VERSION,
2156            register_count: 1,
2157            register_types: vec![Arc::clone(&literal.lemma_type)],
2158            constants: vec![literal],
2159            data_manifest: Vec::new(),
2160            veto_messages: Vec::new(),
2161            arm_tags: Vec::new(),
2162            conversion_tags: Vec::new(),
2163            code: vec![
2164                Instruction::LoadConstant {
2165                    destination_register: 0,
2166                    constant_index: 0,
2167                },
2168                Instruction::Return { source_register: 0 },
2169            ],
2170        }
2171    }
2172
2173    fn create_number_literal(n: rust_decimal::Decimal) -> LiteralValue {
2174        LiteralValue::number_from_decimal(n)
2175    }
2176
2177    fn create_boolean_literal(b: bool) -> LiteralValue {
2178        LiteralValue::from_bool(b)
2179    }
2180
2181    fn create_text_literal(s: String) -> LiteralValue {
2182        LiteralValue::text(s)
2183    }
2184
2185    #[test]
2186    fn with_values_should_enforce_number_maximum_constraint() {
2187        // Higher-standard requirement: user input must be validated against type constraints.
2188        // If this test fails, Lemma accepts invalid values and gives false reassurance.
2189        let data_path = DataPath::new(vec![], "x".to_string());
2190
2191        let max10 = crate::planning::semantics::LemmaType::primitive(
2192            crate::planning::semantics::TypeSpecification::Number {
2193                minimum: None,
2194                maximum: Some(rational_new(10, 1)),
2195                decimals: None,
2196                help: String::new(),
2197            },
2198        );
2199        let source = Source::new(
2200            crate::parsing::source::SourceType::Volatile,
2201            crate::parsing::ast::Span {
2202                start: 0,
2203                end: 0,
2204                line: 1,
2205                col: 0,
2206            },
2207        );
2208        let mut data = IndexMap::new();
2209        data.insert(
2210            data_path.clone(),
2211            crate::planning::semantics::DataDefinition::Value {
2212                value: crate::planning::semantics::LiteralValue::number_with_type(
2213                    rational_new(0, 1),
2214                    Arc::new(max10.clone()),
2215                ),
2216                source: source.clone(),
2217            },
2218        );
2219
2220        let plan = ExecutionPlan {
2221            spec_name: "test".to_string(),
2222            commentary: None,
2223            data,
2224            rules: Vec::new(),
2225            max_register_count: 0,
2226            reference_evaluation_order: Vec::new(),
2227            meta: HashMap::new(),
2228            resolved_types: ResolvedSpecTypes::default(),
2229            signature_index: HashMap::new(),
2230            effective: EffectiveDate::Origin,
2231            sources: Vec::new(),
2232        };
2233
2234        let values = input_data(&[("x", "11")]);
2235
2236        let overlay = DataOverlay::resolve(&plan, values, &default_limits()).unwrap();
2237        match overlay.violated.get(&data_path) {
2238            Some(reason) => {
2239                assert!(
2240                    reason.contains("maximum") || reason.contains("10"),
2241                    "x=11 must violate maximum 10, got: {reason}"
2242                );
2243            }
2244            None => panic!("expected violated data for x=11"),
2245        }
2246    }
2247
2248    #[test]
2249    fn with_values_should_enforce_text_enum_options() {
2250        // Higher-standard requirement: enum options must be enforced for text types.
2251        let data_path = DataPath::new(vec![], "tier".to_string());
2252
2253        let tier = crate::planning::semantics::LemmaType::primitive(
2254            crate::planning::semantics::TypeSpecification::Text {
2255                length: None,
2256                options: vec!["silver".to_string(), "gold".to_string()],
2257                help: String::new(),
2258            },
2259        );
2260        let source = Source::new(
2261            crate::parsing::source::SourceType::Volatile,
2262            crate::parsing::ast::Span {
2263                start: 0,
2264                end: 0,
2265                line: 1,
2266                col: 0,
2267            },
2268        );
2269        let mut data = IndexMap::new();
2270        data.insert(
2271            data_path.clone(),
2272            crate::planning::semantics::DataDefinition::Value {
2273                value: crate::planning::semantics::LiteralValue::text_with_type(
2274                    "silver".to_string(),
2275                    Arc::new(tier.clone()),
2276                ),
2277                source,
2278            },
2279        );
2280
2281        let plan = ExecutionPlan {
2282            spec_name: "test".to_string(),
2283            commentary: None,
2284            data,
2285            rules: Vec::new(),
2286            max_register_count: 0,
2287            reference_evaluation_order: Vec::new(),
2288            meta: HashMap::new(),
2289            resolved_types: ResolvedSpecTypes::default(),
2290            signature_index: HashMap::new(),
2291            effective: EffectiveDate::Origin,
2292            sources: Vec::new(),
2293        };
2294
2295        let values = input_data(&[("tier", "platinum")]);
2296
2297        let overlay = DataOverlay::resolve(&plan, values, &default_limits()).unwrap();
2298        match overlay.violated.get(&data_path) {
2299            Some(reason) => {
2300                assert!(
2301                    reason.contains("allowed options") || reason.contains("platinum"),
2302                    "invalid enum must record violation, got: {reason}"
2303                );
2304            }
2305            None => panic!("expected violated data for tier=platinum"),
2306        }
2307    }
2308
2309    #[test]
2310    fn with_values_should_enforce_quantity_decimals() {
2311        // Higher-standard requirement: decimals should be enforced on quantity inputs,
2312        // unless the language explicitly defines rounding semantics.
2313        let data_path = DataPath::new(vec![], "price".to_string());
2314
2315        let money = crate::planning::semantics::LemmaType::primitive(
2316            crate::planning::semantics::TypeSpecification::Quantity {
2317                minimum: None,
2318                maximum: None,
2319                decimals: Some(2),
2320                units: crate::planning::semantics::QuantityUnits::from(vec![
2321                    crate::planning::semantics::QuantityUnit::from_decimal_factor(
2322                        "eur".to_string(),
2323                        rust_decimal::Decimal::from_str("1.0").unwrap(),
2324                        Vec::new(),
2325                    )
2326                    .expect("eur unit factor must be exact decimal"),
2327                ]),
2328                traits: Vec::new(),
2329                decomposition: None,
2330                help: String::new(),
2331            },
2332        );
2333        let source = Source::new(
2334            crate::parsing::source::SourceType::Volatile,
2335            crate::parsing::ast::Span {
2336                start: 0,
2337                end: 0,
2338                line: 1,
2339                col: 0,
2340            },
2341        );
2342        let mut data = IndexMap::new();
2343        data.insert(
2344            data_path.clone(),
2345            crate::planning::semantics::DataDefinition::Value {
2346                value: crate::planning::semantics::LiteralValue::quantity_with_type(
2347                    rational_zero(),
2348                    "eur".to_string(),
2349                    Arc::new(money.clone()),
2350                ),
2351                source,
2352            },
2353        );
2354
2355        let plan = ExecutionPlan {
2356            spec_name: "test".to_string(),
2357            commentary: None,
2358            data,
2359            rules: Vec::new(),
2360            max_register_count: 0,
2361            reference_evaluation_order: Vec::new(),
2362            meta: HashMap::new(),
2363            resolved_types: ResolvedSpecTypes::default(),
2364            signature_index: HashMap::new(),
2365            effective: EffectiveDate::Origin,
2366            sources: Vec::new(),
2367        };
2368
2369        let values = input_data(&[("price", "1.234 eur")]);
2370
2371        let overlay = DataOverlay::resolve(&plan, values, &default_limits()).unwrap();
2372        match overlay.violated.get(&data_path) {
2373            Some(reason) => {
2374                assert!(
2375                    reason.contains("decimals") || reason.contains("decimal"),
2376                    "1.234 eur must violate decimals=2, got: {reason}"
2377                );
2378            }
2379            None => panic!("expected violated data for price=1.234 eur"),
2380        }
2381    }
2382
2383    #[test]
2384    fn test_serialize_deserialize_execution_plan() {
2385        let data_path = DataPath {
2386            segments: vec![],
2387            data: "age".to_string(),
2388        };
2389        let mut data = IndexMap::new();
2390        data.insert(
2391            data_path.clone(),
2392            crate::planning::semantics::DataDefinition::Value {
2393                value: create_number_literal(0.into()),
2394                source: test_source(),
2395            },
2396        );
2397        let plan = ExecutionPlan {
2398            spec_name: "test".to_string(),
2399            commentary: None,
2400            data,
2401            rules: Vec::new(),
2402            max_register_count: 0,
2403            reference_evaluation_order: Vec::new(),
2404            meta: HashMap::new(),
2405            resolved_types: ResolvedSpecTypes::default(),
2406            signature_index: HashMap::new(),
2407            effective: EffectiveDate::Origin,
2408            sources: Vec::new(),
2409        };
2410
2411        let deserialized = roundtrip_execution_plan(&plan);
2412
2413        assert_eq!(deserialized.spec_name, plan.spec_name);
2414        assert_eq!(deserialized.data.len(), plan.data.len());
2415        assert_eq!(deserialized.rules.len(), plan.rules.len());
2416    }
2417
2418    #[test]
2419    fn test_serialize_deserialize_plan_with_imported_named_type_defining_spec() {
2420        let dep_spec = Arc::new(crate::parsing::ast::LemmaSpec::new("examples".to_string()));
2421        let imported_type = crate::planning::semantics::LemmaType::new(
2422            "salary".to_string(),
2423            TypeSpecification::quantity(),
2424            crate::planning::semantics::TypeExtends::Custom {
2425                parent: "money".to_string(),
2426                family: "money".to_string(),
2427                defining_spec: crate::planning::semantics::TypeDefiningSpec::Import {
2428                    spec: Arc::clone(&dep_spec),
2429                },
2430            },
2431        );
2432
2433        let salary_path = DataPath::new(vec![], "salary".to_string());
2434        let mut data = IndexMap::new();
2435        data.insert(
2436            salary_path,
2437            crate::planning::semantics::DataDefinition::TypeDeclaration {
2438                resolved_type: Arc::new(imported_type),
2439                declared_default: None,
2440                source: test_source(),
2441            },
2442        );
2443
2444        let plan = ExecutionPlan {
2445            spec_name: "test".to_string(),
2446            commentary: None,
2447            data,
2448            rules: Vec::new(),
2449            max_register_count: 0,
2450            reference_evaluation_order: Vec::new(),
2451            meta: HashMap::new(),
2452            resolved_types: ResolvedSpecTypes::default(),
2453            signature_index: HashMap::new(),
2454            effective: EffectiveDate::Origin,
2455            sources: Vec::new(),
2456        };
2457
2458        let deserialized = roundtrip_execution_plan(&plan);
2459
2460        let recovered = deserialized
2461            .data
2462            .get(&DataPath::new(vec![], "salary".to_string()))
2463            .and_then(|d| d.schema_type())
2464            .expect("salary type should be present in plan.data");
2465        match &recovered.extends {
2466            crate::planning::semantics::TypeExtends::Custom {
2467                defining_spec: crate::planning::semantics::TypeDefiningSpec::Import { spec },
2468                ..
2469            } => {
2470                assert_eq!(spec.name, "examples");
2471            }
2472            other => panic!(
2473                "Expected imported defining_spec after round-trip, got {:?}",
2474                other
2475            ),
2476        }
2477    }
2478
2479    #[test]
2480    fn test_serialize_deserialize_plan_with_rules() {
2481        use crate::planning::semantics::ExpressionKind;
2482
2483        let age_path = DataPath::new(vec![], "age".to_string());
2484        let mut data = IndexMap::new();
2485        data.insert(
2486            age_path.clone(),
2487            crate::planning::semantics::DataDefinition::Value {
2488                value: create_number_literal(0.into()),
2489                source: test_source(),
2490            },
2491        );
2492        let mut plan = ExecutionPlan {
2493            spec_name: "test".to_string(),
2494            commentary: None,
2495            data,
2496            rules: Vec::new(),
2497            max_register_count: 0,
2498            reference_evaluation_order: Vec::new(),
2499            meta: HashMap::new(),
2500            resolved_types: ResolvedSpecTypes::default(),
2501            signature_index: HashMap::new(),
2502            effective: EffectiveDate::Origin,
2503            sources: Vec::new(),
2504        };
2505
2506        let rule = ExecutableRule {
2507            path: RulePath::new(vec![], "can_drive".to_string()),
2508            name: "can_drive".to_string(),
2509            branches: vec![{
2510                let result = create_literal_expr(create_boolean_literal(true));
2511                let condition = Expression::new(
2512                    ExpressionKind::Comparison(
2513                        Arc::new(create_data_path_expr(age_path.clone())),
2514                        crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2515                        Arc::new(create_literal_expr(create_number_literal(18.into()))),
2516                    ),
2517                    test_source(),
2518                );
2519                Branch {
2520                    condition: Some(condition.clone()),
2521                    result: result.clone(),
2522                    source: test_source(),
2523                }
2524            }],
2525            instructions: constant_return_instructions(create_boolean_literal(true)),
2526            source_instructions: constant_return_instructions(create_boolean_literal(true)),
2527            source: test_source(),
2528            rule_type: Arc::new(primitive_boolean().clone()),
2529        };
2530
2531        plan.rules.push(rule);
2532        plan.max_register_count = plan.rules[0].instructions.register_count;
2533
2534        let deserialized = roundtrip_execution_plan(&plan);
2535
2536        assert_eq!(deserialized.spec_name, plan.spec_name);
2537        assert_eq!(deserialized.data.len(), plan.data.len());
2538        assert_eq!(deserialized.rules.len(), plan.rules.len());
2539        assert_eq!(deserialized.rules[0].name, "can_drive");
2540        assert_eq!(deserialized.rules[0].branches.len(), 1);
2541    }
2542
2543    #[test]
2544    fn test_serialize_deserialize_plan_with_nested_data_paths() {
2545        use crate::planning::semantics::PathSegment;
2546        let data_path = DataPath {
2547            segments: vec![PathSegment {
2548                data: "employee".to_string(),
2549                spec: "private".to_string(),
2550            }],
2551            data: "salary".to_string(),
2552        };
2553
2554        let mut data = IndexMap::new();
2555        data.insert(
2556            data_path.clone(),
2557            crate::planning::semantics::DataDefinition::Value {
2558                value: create_number_literal(0.into()),
2559                source: test_source(),
2560            },
2561        );
2562        let plan = ExecutionPlan {
2563            spec_name: "test".to_string(),
2564            commentary: None,
2565            data,
2566            rules: Vec::new(),
2567            max_register_count: 0,
2568            reference_evaluation_order: Vec::new(),
2569            meta: HashMap::new(),
2570            resolved_types: ResolvedSpecTypes::default(),
2571            signature_index: HashMap::new(),
2572            effective: EffectiveDate::Origin,
2573            sources: Vec::new(),
2574        };
2575
2576        let deserialized = roundtrip_execution_plan(&plan);
2577
2578        assert_eq!(deserialized.data.len(), 1);
2579        let (deserialized_path, _) = deserialized.data.iter().next().unwrap();
2580        assert_eq!(deserialized_path.segments.len(), 1);
2581        assert_eq!(deserialized_path.segments[0].data, "employee");
2582        assert_eq!(deserialized_path.data, "salary");
2583    }
2584
2585    #[test]
2586    fn test_serialize_deserialize_plan_with_multiple_data_types() {
2587        let name_path = DataPath::new(vec![], "name".to_string());
2588        let age_path = DataPath::new(vec![], "age".to_string());
2589        let active_path = DataPath::new(vec![], "active".to_string());
2590
2591        let mut data = IndexMap::new();
2592        data.insert(
2593            name_path.clone(),
2594            crate::planning::semantics::DataDefinition::Value {
2595                value: create_text_literal("Alice".to_string()),
2596                source: test_source(),
2597            },
2598        );
2599        data.insert(
2600            age_path.clone(),
2601            crate::planning::semantics::DataDefinition::Value {
2602                value: create_number_literal(30.into()),
2603                source: test_source(),
2604            },
2605        );
2606        data.insert(
2607            active_path.clone(),
2608            crate::planning::semantics::DataDefinition::Value {
2609                value: create_boolean_literal(true),
2610                source: test_source(),
2611            },
2612        );
2613
2614        let plan = ExecutionPlan {
2615            spec_name: "test".to_string(),
2616            commentary: None,
2617            data,
2618            rules: Vec::new(),
2619            max_register_count: 0,
2620            reference_evaluation_order: Vec::new(),
2621            meta: HashMap::new(),
2622            resolved_types: ResolvedSpecTypes::default(),
2623            signature_index: HashMap::new(),
2624            effective: EffectiveDate::Origin,
2625            sources: Vec::new(),
2626        };
2627
2628        let deserialized = roundtrip_execution_plan(&plan);
2629
2630        assert_eq!(deserialized.data.len(), 3);
2631
2632        assert_eq!(
2633            deserialized.get_data_value(&name_path).unwrap().value,
2634            crate::planning::semantics::ValueKind::Text("Alice".to_string())
2635        );
2636        assert_eq!(
2637            deserialized.get_data_value(&age_path).unwrap().value,
2638            crate::planning::semantics::ValueKind::Number(rational_new(30, 1))
2639        );
2640        assert_eq!(
2641            deserialized.get_data_value(&active_path).unwrap().value,
2642            crate::planning::semantics::ValueKind::Boolean(true)
2643        );
2644    }
2645
2646    #[test]
2647    fn test_serialize_deserialize_plan_with_multiple_branches() {
2648        use crate::planning::semantics::ExpressionKind;
2649
2650        let points_path = DataPath::new(vec![], "points".to_string());
2651        let mut data = IndexMap::new();
2652        data.insert(
2653            points_path.clone(),
2654            crate::planning::semantics::DataDefinition::Value {
2655                value: create_number_literal(0.into()),
2656                source: test_source(),
2657            },
2658        );
2659        let mut plan = ExecutionPlan {
2660            spec_name: "test".to_string(),
2661            commentary: None,
2662            data,
2663            rules: Vec::new(),
2664            max_register_count: 0,
2665            reference_evaluation_order: Vec::new(),
2666            meta: HashMap::new(),
2667            resolved_types: ResolvedSpecTypes::default(),
2668            signature_index: HashMap::new(),
2669            effective: EffectiveDate::Origin,
2670            sources: Vec::new(),
2671        };
2672
2673        let rule = ExecutableRule {
2674            path: RulePath::new(vec![], "tier".to_string()),
2675            name: "tier".to_string(),
2676            branches: vec![
2677                {
2678                    let result = create_literal_expr(create_text_literal("bronze".to_string()));
2679                    Branch {
2680                        condition: None,
2681                        result: result.clone(),
2682                        source: test_source(),
2683                    }
2684                },
2685                {
2686                    let result = create_literal_expr(create_text_literal("silver".to_string()));
2687                    Branch {
2688                        condition: Some(Expression::new(
2689                            ExpressionKind::Comparison(
2690                                Arc::new(create_data_path_expr(points_path.clone())),
2691                                crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2692                                Arc::new(create_literal_expr(create_number_literal(100.into()))),
2693                            ),
2694                            test_source(),
2695                        )),
2696                        result: result.clone(),
2697                        source: test_source(),
2698                    }
2699                },
2700                {
2701                    let result = create_literal_expr(create_text_literal("gold".to_string()));
2702                    Branch {
2703                        condition: Some(Expression::new(
2704                            ExpressionKind::Comparison(
2705                                Arc::new(create_data_path_expr(points_path.clone())),
2706                                crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2707                                Arc::new(create_literal_expr(create_number_literal(500.into()))),
2708                            ),
2709                            test_source(),
2710                        )),
2711                        result: result.clone(),
2712                        source: test_source(),
2713                    }
2714                },
2715            ],
2716            instructions: constant_return_instructions(create_text_literal("bronze".to_string())),
2717            source_instructions: constant_return_instructions(create_text_literal(
2718                "bronze".to_string(),
2719            )),
2720            source: test_source(),
2721            rule_type: Arc::new(primitive_text().clone()),
2722        };
2723
2724        plan.rules.push(rule);
2725        plan.max_register_count = plan.rules[0].instructions.register_count;
2726
2727        let deserialized = roundtrip_execution_plan(&plan);
2728
2729        assert_eq!(deserialized.rules.len(), 1);
2730        assert_eq!(deserialized.rules[0].branches.len(), 3);
2731        assert!(deserialized.rules[0].branches[0].condition.is_none());
2732        assert!(deserialized.rules[0].branches[1].condition.is_some());
2733        assert!(deserialized.rules[0].branches[2].condition.is_some());
2734    }
2735
2736    #[test]
2737    fn test_serialize_deserialize_empty_plan() {
2738        let plan = ExecutionPlan {
2739            spec_name: "empty".to_string(),
2740            commentary: None,
2741            data: IndexMap::new(),
2742            rules: Vec::new(),
2743            max_register_count: 0,
2744            reference_evaluation_order: Vec::new(),
2745            meta: HashMap::new(),
2746            resolved_types: ResolvedSpecTypes::default(),
2747            signature_index: HashMap::new(),
2748            effective: EffectiveDate::Origin,
2749            sources: Vec::new(),
2750        };
2751
2752        let deserialized = roundtrip_execution_plan(&plan);
2753
2754        assert_eq!(deserialized.spec_name, "empty");
2755        assert_eq!(deserialized.data.len(), 0);
2756        assert_eq!(deserialized.rules.len(), 0);
2757    }
2758
2759    #[test]
2760    fn test_serialize_deserialize_plan_with_arithmetic_expressions() {
2761        use crate::planning::semantics::ExpressionKind;
2762
2763        let x_path = DataPath::new(vec![], "x".to_string());
2764        let mut data = IndexMap::new();
2765        data.insert(
2766            x_path.clone(),
2767            crate::planning::semantics::DataDefinition::Value {
2768                value: create_number_literal(0.into()),
2769                source: test_source(),
2770            },
2771        );
2772        let mut plan = ExecutionPlan {
2773            spec_name: "test".to_string(),
2774            commentary: None,
2775            data,
2776            rules: Vec::new(),
2777            max_register_count: 0,
2778            reference_evaluation_order: Vec::new(),
2779            meta: HashMap::new(),
2780            resolved_types: ResolvedSpecTypes::default(),
2781            signature_index: HashMap::new(),
2782            effective: EffectiveDate::Origin,
2783            sources: Vec::new(),
2784        };
2785
2786        let rule = ExecutableRule {
2787            path: RulePath::new(vec![], "doubled".to_string()),
2788            name: "doubled".to_string(),
2789            branches: vec![{
2790                let result = Expression::new(
2791                    ExpressionKind::Arithmetic(
2792                        Arc::new(create_data_path_expr(x_path.clone())),
2793                        crate::parsing::ast::ArithmeticComputation::Multiply,
2794                        Arc::new(create_literal_expr(create_number_literal(2.into()))),
2795                    ),
2796                    test_source(),
2797                );
2798                Branch {
2799                    condition: None,
2800                    result: result.clone(),
2801                    source: test_source(),
2802                }
2803            }],
2804            instructions: constant_return_instructions(create_number_literal(0.into())),
2805            source_instructions: constant_return_instructions(create_number_literal(0.into())),
2806            source: test_source(),
2807            rule_type: Arc::new(crate::planning::semantics::primitive_number().clone()),
2808        };
2809
2810        plan.rules.push(rule);
2811        plan.max_register_count = plan.rules[0].instructions.register_count;
2812
2813        let deserialized = roundtrip_execution_plan(&plan);
2814
2815        assert_eq!(deserialized.rules.len(), 1);
2816        match &deserialized.rules[0].branches[0].result.kind {
2817            ExpressionKind::Arithmetic(left, op, right) => {
2818                assert_eq!(*op, crate::parsing::ast::ArithmeticComputation::Multiply);
2819                match &left.kind {
2820                    ExpressionKind::DataPath(_) => {}
2821                    _ => panic!("Expected DataPath in left operand"),
2822                }
2823                match &right.kind {
2824                    ExpressionKind::Literal(_) => {}
2825                    _ => panic!("Expected Literal in right operand"),
2826                }
2827            }
2828            _ => panic!("Expected Arithmetic expression"),
2829        }
2830    }
2831
2832    #[test]
2833    fn test_serialize_deserialize_round_trip_equality() {
2834        use crate::planning::semantics::ExpressionKind;
2835
2836        let age_path = DataPath::new(vec![], "age".to_string());
2837        let mut data = IndexMap::new();
2838        data.insert(
2839            age_path.clone(),
2840            crate::planning::semantics::DataDefinition::Value {
2841                value: create_number_literal(0.into()),
2842                source: test_source(),
2843            },
2844        );
2845        let mut plan = ExecutionPlan {
2846            spec_name: "test".to_string(),
2847            commentary: None,
2848            data,
2849            rules: Vec::new(),
2850            max_register_count: 0,
2851            reference_evaluation_order: Vec::new(),
2852            meta: HashMap::new(),
2853            resolved_types: ResolvedSpecTypes::default(),
2854            signature_index: HashMap::new(),
2855            effective: EffectiveDate::Origin,
2856            sources: Vec::new(),
2857        };
2858
2859        let rule = ExecutableRule {
2860            path: RulePath::new(vec![], "is_adult".to_string()),
2861            name: "is_adult".to_string(),
2862            branches: vec![{
2863                let result = create_literal_expr(create_boolean_literal(true));
2864                let condition = Expression::new(
2865                    ExpressionKind::Comparison(
2866                        Arc::new(create_data_path_expr(age_path.clone())),
2867                        crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
2868                        Arc::new(create_literal_expr(create_number_literal(18.into()))),
2869                    ),
2870                    test_source(),
2871                );
2872                Branch {
2873                    condition: Some(condition.clone()),
2874                    result: result.clone(),
2875                    source: test_source(),
2876                }
2877            }],
2878            instructions: constant_return_instructions(create_boolean_literal(true)),
2879            source_instructions: constant_return_instructions(create_boolean_literal(true)),
2880            source: test_source(),
2881            rule_type: Arc::new(primitive_boolean().clone()),
2882        };
2883
2884        plan.rules.push(rule);
2885        plan.max_register_count = plan.rules[0].instructions.register_count;
2886
2887        let deserialized = roundtrip_execution_plan(&plan);
2888        let deserialized2 = roundtrip_execution_plan(&deserialized);
2889
2890        assert_eq!(deserialized2.spec_name, plan.spec_name);
2891        assert_eq!(deserialized2.data.len(), plan.data.len());
2892        assert_eq!(deserialized2.rules.len(), plan.rules.len());
2893        assert_eq!(deserialized2.rules[0].name, plan.rules[0].name);
2894        assert_eq!(
2895            deserialized2.rules[0].branches.len(),
2896            plan.rules[0].branches.len()
2897        );
2898    }
2899
2900    fn empty_plan(effective: crate::parsing::ast::EffectiveDate) -> ExecutionPlan {
2901        ExecutionPlan {
2902            spec_name: "s".into(),
2903            commentary: None,
2904            data: IndexMap::new(),
2905            rules: Vec::new(),
2906            max_register_count: 0,
2907            reference_evaluation_order: Vec::new(),
2908            meta: HashMap::new(),
2909            resolved_types: ResolvedSpecTypes::default(),
2910            signature_index: HashMap::new(),
2911            effective,
2912            sources: Vec::new(),
2913        }
2914    }
2915
2916    #[test]
2917    fn plan_at_exact_boundary_selects_later_slice() {
2918        use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2919
2920        let june = DateTimeValue {
2921            year: 2025,
2922            month: 6,
2923            day: 1,
2924            hour: 0,
2925            minute: 0,
2926            second: 0,
2927            microsecond: 0,
2928            timezone: None,
2929
2930            granularity: DateGranularity::Full,
2931        };
2932        let dec = DateTimeValue {
2933            year: 2025,
2934            month: 12,
2935            day: 1,
2936            hour: 0,
2937            minute: 0,
2938            second: 0,
2939            microsecond: 0,
2940            timezone: None,
2941
2942            granularity: DateGranularity::Full,
2943        };
2944
2945        let set = ExecutionPlanSet {
2946            spec_name: "s".into(),
2947            plans: vec![
2948                empty_plan(EffectiveDate::Origin),
2949                empty_plan(EffectiveDate::DateTimeValue(june.clone())),
2950                empty_plan(EffectiveDate::DateTimeValue(dec.clone())),
2951            ],
2952        };
2953
2954        assert!(std::ptr::eq(
2955            set.plan_at(&EffectiveDate::DateTimeValue(june.clone()))
2956                .expect("boundary instant"),
2957            &set.plans[1]
2958        ));
2959        assert!(std::ptr::eq(
2960            set.plan_at(&EffectiveDate::DateTimeValue(dec.clone()))
2961                .expect("dec boundary"),
2962            &set.plans[2]
2963        ));
2964    }
2965
2966    #[test]
2967    fn plan_at_day_before_boundary_stays_in_earlier_slice() {
2968        use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2969
2970        let june = DateTimeValue {
2971            year: 2025,
2972            month: 6,
2973            day: 1,
2974            hour: 0,
2975            minute: 0,
2976            second: 0,
2977            microsecond: 0,
2978            timezone: None,
2979
2980            granularity: DateGranularity::Full,
2981        };
2982        let may_end = DateTimeValue {
2983            year: 2025,
2984            month: 5,
2985            day: 31,
2986            hour: 23,
2987            minute: 59,
2988            second: 59,
2989            microsecond: 0,
2990            timezone: None,
2991
2992            granularity: DateGranularity::DateTime,
2993        };
2994
2995        let set = ExecutionPlanSet {
2996            spec_name: "s".into(),
2997            plans: vec![
2998                empty_plan(EffectiveDate::Origin),
2999                empty_plan(EffectiveDate::DateTimeValue(june)),
3000            ],
3001        };
3002
3003        assert!(std::ptr::eq(
3004            set.plan_at(&EffectiveDate::DateTimeValue(may_end))
3005                .expect("may 31"),
3006            &set.plans[0]
3007        ));
3008    }
3009
3010    #[test]
3011    fn plan_at_single_plan_matches_any_instant_after_start() {
3012        use crate::parsing::ast::{DateTimeValue, EffectiveDate};
3013
3014        let t = DateTimeValue {
3015            year: 2025,
3016            month: 3,
3017            day: 1,
3018            hour: 0,
3019            minute: 0,
3020            second: 0,
3021            microsecond: 0,
3022            timezone: None,
3023
3024            granularity: DateGranularity::Full,
3025        };
3026        let set = ExecutionPlanSet {
3027            spec_name: "s".into(),
3028            plans: vec![empty_plan(EffectiveDate::DateTimeValue(DateTimeValue {
3029                year: 2025,
3030                month: 1,
3031                day: 1,
3032                hour: 0,
3033                minute: 0,
3034                second: 0,
3035                microsecond: 0,
3036                timezone: None,
3037
3038                granularity: DateGranularity::Full,
3039            }))],
3040        };
3041        assert!(std::ptr::eq(
3042            set.plan_at(&EffectiveDate::DateTimeValue(t))
3043                .expect("inside single slice"),
3044            &set.plans[0]
3045        ));
3046    }
3047
3048    /// The schema JSON shape is the IO contract for every non-Rust consumer
3049    /// (WASM playground, Hex, HTTP, TypeScript). Nail the exact envelope.
3050    #[test]
3051    fn schema_json_shape_contract() {
3052        let mut engine = Engine::new();
3053        engine
3054            .load(
3055                r#"
3056                spec pricing
3057                data bridge_height: quantity
3058                  -> unit meter 1
3059                  -> default 100 meter
3060                data quantity: number -> minimum 0
3061                rule cost: bridge_height * quantity
3062                "#,
3063                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
3064                    "test.lemma",
3065                ))),
3066            )
3067            .unwrap();
3068        let now = DateTimeValue::now();
3069        let schema = engine
3070            .get_plan(None, "pricing", Some(&now))
3071            .unwrap()
3072            .schema(&DataOverlay::default());
3073
3074        let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
3075
3076        let bh = &value["data"]["bridge_height"];
3077        assert!(
3078            bh.is_object(),
3079            "data entry must be a named object, not tuple"
3080        );
3081        assert!(
3082            bh.get("type").is_some(),
3083            "data entry must expose `type` field"
3084        );
3085        assert!(
3086            bh.get("default").is_some(),
3087            "bridge_height exposes `-> default` as schema default suggestion"
3088        );
3089        assert!(
3090            bh.get("bound_value").is_none(),
3091            "bridge_height is not a spec-bound literal"
3092        );
3093
3094        let ty = &bh["type"];
3095        assert_eq!(
3096            ty["kind"], "quantity",
3097            "kind tag sits on the type object itself"
3098        );
3099        assert!(
3100            ty["units"].is_array(),
3101            "quantity-only fields flatten up to top level"
3102        );
3103        assert!(
3104            ty.get("options").is_none(),
3105            "text-only fields must not leak"
3106        );
3107
3108        let qty = &value["data"]["quantity"];
3109        assert_eq!(qty["type"]["kind"], "number");
3110        assert!(
3111            qty.get("default").is_none(),
3112            "quantity has no default suggestion"
3113        );
3114        assert!(
3115            qty.get("bound_value").is_none(),
3116            "quantity has no bound literal"
3117        );
3118
3119        let cost = &value["rules"]["cost"];
3120        assert_eq!(
3121            cost["kind"], "quantity",
3122            "rule types use the same flat shape"
3123        );
3124        assert!(
3125            cost["units"].is_array() && !cost["units"].as_array().unwrap().is_empty(),
3126            "quantity rule result types expose declared units"
3127        );
3128        assert!(
3129            cost["units"][0].get("factor").is_some(),
3130            "quantity rule units use factor field"
3131        );
3132    }
3133
3134    #[test]
3135    fn schema_rule_result_units_contract() {
3136        let mut engine = Engine::new();
3137        engine
3138            .load(
3139                r#"
3140                spec units_contract
3141                data money: quantity
3142                  -> unit eur 1
3143                  -> unit usd 0.91
3144                data rate: ratio
3145                  -> unit basis_points 10000
3146                  -> unit percent 100
3147                  -> default 500 basis_points
3148                rule total: money
3149                rule rate_out: rate
3150                "#,
3151                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
3152                    "units_contract.lemma",
3153                ))),
3154            )
3155            .unwrap();
3156        let now = DateTimeValue::now();
3157        let schema = engine
3158            .get_plan(None, "units_contract", Some(&now))
3159            .unwrap()
3160            .schema(&DataOverlay::default());
3161        let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
3162
3163        let money_units = &value["data"]["money"]["type"]["units"];
3164        assert!(money_units.is_array() && !money_units.as_array().unwrap().is_empty());
3165        assert!(money_units[0].get("name").is_some());
3166        assert!(money_units[0].get("factor").is_some());
3167        assert!(money_units[0]["factor"].get("numer").is_some());
3168        assert!(money_units[0]["factor"].get("denom").is_some());
3169
3170        let rate_units = &value["data"]["rate"]["type"]["units"];
3171        assert!(rate_units.is_array() && !rate_units.as_array().unwrap().is_empty());
3172        assert!(rate_units[0].get("name").is_some());
3173        assert!(rate_units[0].get("value").is_some());
3174        assert!(rate_units[0]["value"].get("numer").is_some());
3175        assert!(rate_units[0]["value"].get("denom").is_some());
3176
3177        let total_rule_units = &value["rules"]["total"]["units"];
3178        let money_unit_names: Vec<_> = money_units
3179            .as_array()
3180            .unwrap()
3181            .iter()
3182            .map(|u| u["name"].as_str().unwrap())
3183            .collect();
3184        let total_rule_unit_names: Vec<_> = total_rule_units
3185            .as_array()
3186            .unwrap()
3187            .iter()
3188            .map(|u| u["name"].as_str().unwrap())
3189            .collect();
3190        assert_eq!(total_rule_unit_names, money_unit_names);
3191
3192        let rate_out_rule_units = &value["rules"]["rate_out"]["units"];
3193        let rate_unit_names: Vec<_> = rate_units
3194            .as_array()
3195            .unwrap()
3196            .iter()
3197            .map(|u| u["name"].as_str().unwrap())
3198            .collect();
3199        let rate_out_rule_unit_names: Vec<_> = rate_out_rule_units
3200            .as_array()
3201            .unwrap()
3202            .iter()
3203            .map(|u| u["name"].as_str().unwrap())
3204            .collect();
3205        assert_eq!(rate_out_rule_unit_names, rate_unit_names);
3206    }
3207
3208    #[test]
3209    fn schema_json_round_trip_preserves_shape() {
3210        let mut engine = Engine::new();
3211        engine
3212            .load(
3213                r#"
3214                spec s
3215                data age: number -> minimum 0 -> default 18
3216                data grade: text -> options "A" "B" "C"
3217                rule adult: age >= 18
3218                "#,
3219                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("s.lemma"))),
3220            )
3221            .unwrap();
3222        let now = DateTimeValue::now();
3223        let schema = engine
3224            .get_plan(None, "s", Some(&now))
3225            .unwrap()
3226            .schema(&DataOverlay::default());
3227
3228        let json = serde_json::to_string(&schema).unwrap();
3229        let round_tripped: SpecSchema = serde_json::from_str(&json).unwrap();
3230        assert_eq!(schema, round_tripped);
3231    }
3232}
3233
3234// ---------------------------------------------------------------------------
3235// ExecutionPlanSet (formerly plan_set.rs)
3236// ---------------------------------------------------------------------------