Skip to main content

lemma/planning/
execution_plan.rs

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