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