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