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        self.data.keys().find(|path| path.input_key() == name)
631    }
632
633    /// Look up a local rule by its name (rule in the main spec).
634    pub fn get_rule(&self, name: &str) -> Option<&ExecutableRule> {
635        self.rules
636            .iter()
637            .find(|r| r.name == name && r.path.segments.is_empty())
638    }
639
640    /// Look up a rule by its full path.
641    pub fn get_rule_by_path(&self, rule_path: &RulePath) -> Option<&ExecutableRule> {
642        self.rules.iter().find(|r| &r.path == rule_path)
643    }
644
645    /// Get the literal value for a data path, if it exists and has a literal value.
646    pub fn get_data_value(&self, path: &DataPath) -> Option<&LiteralValue> {
647        self.data.get(path).and_then(|d| d.value())
648    }
649
650    /// Provide data values as JSON (convenience strings or serialized objects).
651    ///
652    /// Parses each value to its expected type, validates constraints, and applies to the plan.
653    pub fn set_data_values(
654        mut self,
655        values: std::collections::HashMap<String, serde_json::Value>,
656        limits: &ResourceLimits,
657    ) -> Result<Self, Error> {
658        for (name, raw_value) in values {
659            let data_path = self.get_data_path_by_str(&name).ok_or_else(|| {
660                let available: Vec<String> = self.data.keys().map(|p| p.input_key()).collect();
661                Error::request(
662                    format!(
663                        "Data '{}' not found. Available data: {}",
664                        name,
665                        available.join(", ")
666                    ),
667                    None::<String>,
668                )
669            })?;
670            let data_path = data_path.clone();
671
672            let data_definition = self
673                .data
674                .get(&data_path)
675                .expect("BUG: data_path was just resolved from self.data, must exist");
676
677            let data_source = data_definition.source().clone();
678            let expected_type = data_definition.schema_type().cloned().ok_or_else(|| {
679                Error::request(
680                    format!(
681                        "Data '{}' is a spec reference; cannot provide a value.",
682                        name
683                    ),
684                    None::<String>,
685                )
686            })?;
687
688            let literal_value = crate::planning::semantics::parse_data_value_from_json(
689                &raw_value,
690                &expected_type.specifications,
691                &expected_type,
692                &data_source,
693            )
694            .map_err(|e| e.with_related_data(&name))?;
695
696            let size = literal_value.byte_size();
697            if size > limits.max_data_value_bytes {
698                return Err(Error::resource_limit_exceeded(
699                    "max_data_value_bytes",
700                    limits.max_data_value_bytes.to_string(),
701                    size.to_string(),
702                    format!(
703                        "Reduce the size of data values to {} bytes or less",
704                        limits.max_data_value_bytes
705                    ),
706                    Some(data_source.clone()),
707                    None,
708                    None,
709                )
710                .with_related_data(&name));
711            }
712
713            validate_value_against_type(&expected_type, &literal_value).map_err(|msg| {
714                Error::validation(msg, Some(data_source.clone()), None::<String>)
715                    .with_related_data(&name)
716            })?;
717
718            self.data.insert(
719                data_path,
720                DataDefinition::Value {
721                    value: literal_value,
722                    source: data_source,
723                },
724            );
725        }
726
727        Ok(self)
728    }
729
730    /// Promote declared defaults on type declarations into concrete [`DataDefinition::Value`] entries.
731    /// Call BEFORE [`Self::set_data_values`] so user-provided values override defaults.
732    /// Reference resolution is handled by the evaluator at runtime.
733    #[must_use]
734    pub fn with_defaults(mut self) -> Self {
735        let promotions: Vec<(DataPath, DataDefinition)> = self
736            .data
737            .iter()
738            .filter_map(|(path, def)| {
739                if let DataDefinition::TypeDeclaration {
740                    declared_default: Some(dv),
741                    resolved_type,
742                    source,
743                } = def
744                {
745                    Some((
746                        path.clone(),
747                        DataDefinition::Value {
748                            value: LiteralValue {
749                                value: dv.clone(),
750                                lemma_type: resolved_type.clone(),
751                            },
752                            source: source.clone(),
753                        },
754                    ))
755                } else {
756                    None
757                }
758            })
759            .collect();
760
761        for (path, def) in promotions {
762            self.data.insert(path, def);
763        }
764        self
765    }
766}
767
768pub(crate) fn validate_value_against_type(
769    expected_type: &LemmaType,
770    value: &LiteralValue,
771) -> Result<(), String> {
772    use crate::computation::rational::{commit_rational_to_decimal, RationalInteger};
773    use crate::planning::semantics::TypeSpecification;
774
775    fn exceeds_decimal_places(magnitude: &RationalInteger, max_decimals: u8) -> bool {
776        match commit_rational_to_decimal(magnitude) {
777            Ok(decimal) => decimal.scale() > u32::from(max_decimals),
778            Err(_) => true,
779        }
780    }
781
782    fn format_rational(r: &RationalInteger, decimals: Option<u8>) -> String {
783        use crate::computation::rational::rational_to_display_str;
784        match commit_rational_to_decimal(r) {
785            Ok(decimal) => match decimals {
786                Some(dp) => {
787                    let rounded = decimal.round_dp(u32::from(dp));
788                    format!("{:.prec$}", rounded, prec = dp as usize)
789                }
790                None => decimal.normalize().to_string(),
791            },
792            Err(_) => rational_to_display_str(r),
793        }
794    }
795
796    match (&expected_type.specifications, &value.value) {
797        (
798            TypeSpecification::Number {
799                minimum,
800                maximum,
801                decimals,
802                ..
803            },
804            ValueKind::Number(n),
805        ) => {
806            if let Some(d) = decimals {
807                if exceeds_decimal_places(n, *d) {
808                    return Err(format!(
809                        "{} exceeds decimals constraint {d}",
810                        format_rational(n, *decimals)
811                    ));
812                }
813            }
814            if let Some(min) = minimum {
815                if n < min {
816                    return Err(format!(
817                        "{} is below minimum {}",
818                        format_rational(n, *decimals),
819                        format_rational(min, *decimals)
820                    ));
821                }
822            }
823            if let Some(max) = maximum {
824                if n > max {
825                    return Err(format!(
826                        "{} is above maximum {}",
827                        format_rational(n, *decimals),
828                        format_rational(max, *decimals)
829                    ));
830                }
831            }
832            Ok(())
833        }
834        (
835            TypeSpecification::Quantity {
836                minimum,
837                maximum,
838                decimals,
839                units,
840                ..
841            },
842            ValueKind::Quantity(magnitude, unit, _),
843        ) => {
844            if let Some(d) = decimals {
845                if exceeds_decimal_places(magnitude, *d) {
846                    return Err(format!(
847                        "{} {unit} exceeds decimals constraint {d}",
848                        format_rational(magnitude, *decimals)
849                    ));
850                }
851            }
852            let quantity_unit = units.get(unit)?;
853            if minimum.is_some() {
854                let unit_minimum = quantity_unit.minimum.expect(
855                    "BUG: QuantityUnit.minimum missing after type minimum set by sync_quantity_units_from_canonical",
856                );
857                if magnitude < &unit_minimum {
858                    let value_display =
859                        format!("{} {}", format_rational(magnitude, *decimals), unit);
860                    let bound_display = format!(
861                        "{} {}",
862                        format_rational(&unit_minimum, *decimals),
863                        quantity_unit.name
864                    );
865                    return Err(format!("{value_display} is below minimum {bound_display}"));
866                }
867            }
868            if maximum.is_some() {
869                let unit_maximum = quantity_unit.maximum.expect(
870                    "BUG: QuantityUnit.maximum missing after type maximum set by sync_quantity_units_from_canonical",
871                );
872                if magnitude > &unit_maximum {
873                    let value_display =
874                        format!("{} {}", format_rational(magnitude, *decimals), unit);
875                    let bound_display = format!(
876                        "{} {}",
877                        format_rational(&unit_maximum, *decimals),
878                        quantity_unit.name
879                    );
880                    return Err(format!("{value_display} is above maximum {bound_display}"));
881                }
882            }
883            Ok(())
884        }
885        (
886            TypeSpecification::Text {
887                length, options, ..
888            },
889            ValueKind::Text(s),
890        ) => {
891            let len = s.chars().count();
892            if let Some(exact) = length {
893                if len != *exact {
894                    return Err(format!(
895                        "'{}' has length {} but required length is {}",
896                        s, len, exact
897                    ));
898                }
899            }
900            if !options.is_empty() && !options.iter().any(|opt| opt == s) {
901                return Err(format!(
902                    "'{}' is not in allowed options: {}",
903                    s,
904                    options.join(", ")
905                ));
906            }
907            Ok(())
908        }
909        (
910            TypeSpecification::Ratio {
911                minimum,
912                maximum,
913                decimals,
914                units,
915                ..
916            },
917            ValueKind::Ratio(r, unit_name),
918        ) => {
919            use crate::computation::rational::checked_mul;
920
921            if let Some(d) = decimals {
922                if exceeds_decimal_places(r, *d) {
923                    return Err(format!(
924                        "{} exceeds decimals constraint {d}",
925                        format_rational(r, *decimals)
926                    ));
927                }
928            }
929            if let Some(type_minimum) = minimum {
930                if r < type_minimum {
931                    let message = match unit_name.as_deref() {
932                        Some(unit) => {
933                            let ratio_unit = units.get(unit)?;
934                            let value_per_unit = checked_mul(r, &ratio_unit.value)
935                                .map_err(|failure| failure.to_string())?;
936                            let bound_per_unit = ratio_unit.minimum.expect(
937                                "BUG: RatioUnit.minimum missing after type minimum set by sync_ratio_units_from_canonical",
938                            );
939                            format!(
940                                "{} {unit} is below minimum {} {unit}",
941                                format_rational(&value_per_unit, *decimals),
942                                format_rational(&bound_per_unit, *decimals),
943                            )
944                        }
945                        None => format!(
946                            "{} is below minimum {}",
947                            format_rational(r, *decimals),
948                            format_rational(type_minimum, *decimals),
949                        ),
950                    };
951                    return Err(message);
952                }
953            }
954            if let Some(type_maximum) = maximum {
955                if r > type_maximum {
956                    let message = match unit_name.as_deref() {
957                        Some(unit) => {
958                            let ratio_unit = units.get(unit)?;
959                            let value_per_unit = checked_mul(r, &ratio_unit.value)
960                                .map_err(|failure| failure.to_string())?;
961                            let bound_per_unit = ratio_unit.maximum.expect(
962                                "BUG: RatioUnit.maximum missing after type maximum set by sync_ratio_units_from_canonical",
963                            );
964                            format!(
965                                "{} {unit} is above maximum {} {unit}",
966                                format_rational(&value_per_unit, *decimals),
967                                format_rational(&bound_per_unit, *decimals),
968                            )
969                        }
970                        None => format!(
971                            "{} is above maximum {}",
972                            format_rational(r, *decimals),
973                            format_rational(type_maximum, *decimals),
974                        ),
975                    };
976                    return Err(message);
977                }
978            }
979            Ok(())
980        }
981        (
982            TypeSpecification::Date {
983                minimum, maximum, ..
984            },
985            ValueKind::Date(dt),
986        ) => {
987            use crate::planning::semantics::{compare_semantic_dates, date_time_to_semantic};
988            use std::cmp::Ordering;
989            if let Some(min) = minimum {
990                let min_sem = date_time_to_semantic(min);
991                if compare_semantic_dates(dt, &min_sem) == Ordering::Less {
992                    return Err(format!("{} is below minimum {}", dt, min));
993                }
994            }
995            if let Some(max) = maximum {
996                let max_sem = date_time_to_semantic(max);
997                if compare_semantic_dates(dt, &max_sem) == Ordering::Greater {
998                    return Err(format!("{} is above maximum {}", dt, max));
999                }
1000            }
1001            Ok(())
1002        }
1003        (
1004            TypeSpecification::Calendar {
1005                minimum, maximum, ..
1006            },
1007            ValueKind::Calendar(value, unit),
1008        ) => {
1009            let value_months = crate::computation::units::convert_calendar_magnitude(
1010                *value,
1011                unit,
1012                &SemanticCalendarUnit::Month,
1013            );
1014            if let Some((min_val, min_unit)) = minimum {
1015                let min_months = crate::computation::units::convert_calendar_magnitude(
1016                    *min_val,
1017                    min_unit,
1018                    &SemanticCalendarUnit::Month,
1019                );
1020                if value_months < min_months {
1021                    return Err(format!(
1022                        "{value} {unit} is below minimum {min_val} {min_unit}"
1023                    ));
1024                }
1025            }
1026            if let Some((max_val, max_unit)) = maximum {
1027                let max_months = crate::computation::units::convert_calendar_magnitude(
1028                    *max_val,
1029                    max_unit,
1030                    &SemanticCalendarUnit::Month,
1031                );
1032                if value_months > max_months {
1033                    return Err(format!(
1034                        "{value} {unit} is above maximum {max_val} {max_unit}"
1035                    ));
1036                }
1037            }
1038            Ok(())
1039        }
1040        (
1041            TypeSpecification::Time {
1042                minimum, maximum, ..
1043            },
1044            ValueKind::Time(t),
1045        ) => {
1046            use crate::planning::semantics::{compare_semantic_times, time_to_semantic};
1047            use std::cmp::Ordering;
1048            if let Some(min) = minimum {
1049                let min_sem = time_to_semantic(min);
1050                if compare_semantic_times(t, &min_sem) == Ordering::Less {
1051                    return Err(format!("{} is below minimum {}", t, min));
1052                }
1053            }
1054            if let Some(max) = maximum {
1055                let max_sem = time_to_semantic(max);
1056                if compare_semantic_times(t, &max_sem) == Ordering::Greater {
1057                    return Err(format!("{} is above maximum {}", t, max));
1058                }
1059            }
1060            Ok(())
1061        }
1062        (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
1063        | (TypeSpecification::NumberRange { .. }, ValueKind::Range(_, _))
1064        | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
1065        | (TypeSpecification::QuantityRange { .. }, ValueKind::Range(_, _))
1066        | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
1067        | (TypeSpecification::CalendarRange { .. }, ValueKind::Range(_, _))
1068        | (TypeSpecification::Veto { .. }, _)
1069        | (TypeSpecification::Undetermined, _) => Ok(()),
1070        (spec, value_kind) => unreachable!(
1071            "BUG: validate_value_against_type called with mismatched type/value: \
1072             spec={:?}, value={:?} — typing must be enforced before validation",
1073            spec, value_kind
1074        ),
1075    }
1076}
1077
1078pub(crate) fn validate_literal_data_against_types(plan: &ExecutionPlan) -> Vec<Error> {
1079    let mut errors = Vec::new();
1080
1081    for (data_path, data_definition) in &plan.data {
1082        let (expected_type, lit) = match data_definition {
1083            DataDefinition::Value { value, .. } => (&value.lemma_type, value),
1084            DataDefinition::TypeDeclaration { .. }
1085            | DataDefinition::Import { .. }
1086            | DataDefinition::Reference { .. } => continue,
1087        };
1088
1089        if let Err(msg) = validate_value_against_type(expected_type, lit) {
1090            let source = data_definition.source().clone();
1091            errors.push(Error::validation(
1092                format!(
1093                    "Invalid value for data {} (expected {}): {}",
1094                    data_path,
1095                    expected_type.name(),
1096                    msg
1097                ),
1098                Some(source),
1099                None::<String>,
1100            ));
1101        }
1102    }
1103
1104    errors
1105}
1106
1107#[cfg(test)]
1108mod tests {
1109    use super::*;
1110    use crate::computation::rational::{rational_zero, RationalInteger};
1111    use crate::parsing::ast::DateTimeValue;
1112    use crate::planning::semantics::{
1113        primitive_boolean, primitive_text, DataPath, LiteralValue, PathSegment, RulePath,
1114    };
1115    use crate::Engine;
1116    use serde_json;
1117    use std::str::FromStr;
1118    use std::sync::Arc;
1119
1120    fn default_limits() -> ResourceLimits {
1121        ResourceLimits::default()
1122    }
1123
1124    fn json_data(pairs: &[(&str, &str)]) -> HashMap<String, serde_json::Value> {
1125        pairs
1126            .iter()
1127            .map(|(k, v)| (k.to_string(), serde_json::Value::String((*v).to_string())))
1128            .collect()
1129    }
1130
1131    #[test]
1132    fn test_with_raw_values() {
1133        let mut engine = Engine::new();
1134        engine
1135            .load(
1136                r#"
1137                spec test
1138                data age: number -> default 25
1139                "#,
1140                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1141                    "test.lemma",
1142                ))),
1143            )
1144            .unwrap();
1145
1146        let now = DateTimeValue::now();
1147        let plan = engine.get_plan(None, "test", Some(&now)).unwrap().clone();
1148        let data_path = DataPath::new(vec![], "age".to_string());
1149
1150        let values = json_data(&[("age", "30")]);
1151
1152        let updated_plan = plan.set_data_values(values, &default_limits()).unwrap();
1153        let updated_value = updated_plan.get_data_value(&data_path).unwrap();
1154        match &updated_value.value {
1155            crate::planning::semantics::ValueKind::Number(n) => {
1156                assert_eq!(*n, RationalInteger::new(30, 1));
1157            }
1158            other => panic!("Expected number literal, got {:?}", other),
1159        }
1160    }
1161
1162    #[test]
1163    fn test_with_raw_values_type_mismatch() {
1164        let mut engine = Engine::new();
1165        engine
1166            .load(
1167                r#"
1168                spec test
1169                data age: number
1170                "#,
1171                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1172                    "test.lemma",
1173                ))),
1174            )
1175            .unwrap();
1176
1177        let now = DateTimeValue::now();
1178        let plan = engine.get_plan(None, "test", Some(&now)).unwrap().clone();
1179
1180        let values = json_data(&[("age", "thirty")]);
1181
1182        assert!(plan.set_data_values(values, &default_limits()).is_err());
1183    }
1184
1185    #[test]
1186    fn test_with_raw_values_unknown_data() {
1187        let mut engine = Engine::new();
1188        engine
1189            .load(
1190                r#"
1191                spec test
1192                data known: number
1193                "#,
1194                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1195                    "test.lemma",
1196                ))),
1197            )
1198            .unwrap();
1199
1200        let now = DateTimeValue::now();
1201        let plan = engine.get_plan(None, "test", Some(&now)).unwrap().clone();
1202
1203        let values = json_data(&[("unknown", "30")]);
1204
1205        assert!(plan.set_data_values(values, &default_limits()).is_err());
1206    }
1207
1208    #[test]
1209    fn test_with_raw_values_nested() {
1210        let mut engine = Engine::new();
1211        engine
1212            .load(
1213                r#"
1214                spec private
1215                data base_price: number
1216
1217                spec test
1218                uses rules: private
1219                "#,
1220                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
1221                    "test.lemma",
1222                ))),
1223            )
1224            .unwrap();
1225
1226        let now = DateTimeValue::now();
1227        let plan = engine.get_plan(None, "test", Some(&now)).unwrap().clone();
1228
1229        let values = json_data(&[("rules.base_price", "100")]);
1230
1231        let updated_plan = plan.set_data_values(values, &default_limits()).unwrap();
1232        let data_path = DataPath {
1233            segments: vec![PathSegment {
1234                data: "rules".to_string(),
1235                spec: "private".to_string(),
1236            }],
1237            data: "base_price".to_string(),
1238        };
1239        let updated_value = updated_plan.get_data_value(&data_path).unwrap();
1240        match &updated_value.value {
1241            crate::planning::semantics::ValueKind::Number(n) => {
1242                assert_eq!(*n, RationalInteger::new(100, 1));
1243            }
1244            other => panic!("Expected number literal, got {:?}", other),
1245        }
1246    }
1247
1248    fn test_source() -> Source {
1249        use crate::parsing::ast::Span;
1250        Source::new(
1251            crate::parsing::source::SourceType::Volatile,
1252            Span {
1253                start: 0,
1254                end: 0,
1255                line: 1,
1256                col: 0,
1257            },
1258        )
1259    }
1260
1261    fn create_literal_expr(value: LiteralValue) -> Expression {
1262        Expression::new(
1263            crate::planning::semantics::ExpressionKind::Literal(Box::new(value)),
1264            test_source(),
1265        )
1266    }
1267
1268    fn create_data_path_expr(path: DataPath) -> Expression {
1269        Expression::new(
1270            crate::planning::semantics::ExpressionKind::DataPath(path),
1271            test_source(),
1272        )
1273    }
1274
1275    fn create_number_literal(n: rust_decimal::Decimal) -> LiteralValue {
1276        LiteralValue::number_from_decimal(n)
1277    }
1278
1279    fn create_boolean_literal(b: bool) -> LiteralValue {
1280        LiteralValue::from_bool(b)
1281    }
1282
1283    fn create_text_literal(s: String) -> LiteralValue {
1284        LiteralValue::text(s)
1285    }
1286
1287    #[test]
1288    fn with_values_should_enforce_number_maximum_constraint() {
1289        // Higher-standard requirement: user input must be validated against type constraints.
1290        // If this test fails, Lemma accepts invalid values and gives false reassurance.
1291        let data_path = DataPath::new(vec![], "x".to_string());
1292
1293        let max10 = crate::planning::semantics::LemmaType::primitive(
1294            crate::planning::semantics::TypeSpecification::Number {
1295                minimum: None,
1296                maximum: Some(RationalInteger::new(10, 1)),
1297                decimals: None,
1298                help: String::new(),
1299            },
1300        );
1301        let source = Source::new(
1302            crate::parsing::source::SourceType::Volatile,
1303            crate::parsing::ast::Span {
1304                start: 0,
1305                end: 0,
1306                line: 1,
1307                col: 0,
1308            },
1309        );
1310        let mut data = IndexMap::new();
1311        data.insert(
1312            data_path.clone(),
1313            crate::planning::semantics::DataDefinition::Value {
1314                value: crate::planning::semantics::LiteralValue::number_with_type(
1315                    0.into(),
1316                    max10.clone(),
1317                ),
1318                source: source.clone(),
1319            },
1320        );
1321
1322        let plan = ExecutionPlan {
1323            spec_name: "test".to_string(),
1324            data,
1325            rules: Vec::new(),
1326            reference_evaluation_order: Vec::new(),
1327            meta: HashMap::new(),
1328            unit_index: HashMap::new(),
1329            effective: EffectiveDate::Origin,
1330            sources: Vec::new(),
1331        };
1332
1333        let values = json_data(&[("x", "11")]);
1334
1335        assert!(
1336            plan.set_data_values(values, &default_limits()).is_err(),
1337            "Providing x=11 should fail due to maximum 10"
1338        );
1339    }
1340
1341    #[test]
1342    fn with_values_should_enforce_text_enum_options() {
1343        // Higher-standard requirement: enum options must be enforced for text types.
1344        let data_path = DataPath::new(vec![], "tier".to_string());
1345
1346        let tier = crate::planning::semantics::LemmaType::primitive(
1347            crate::planning::semantics::TypeSpecification::Text {
1348                length: None,
1349                options: vec!["silver".to_string(), "gold".to_string()],
1350                help: String::new(),
1351            },
1352        );
1353        let source = Source::new(
1354            crate::parsing::source::SourceType::Volatile,
1355            crate::parsing::ast::Span {
1356                start: 0,
1357                end: 0,
1358                line: 1,
1359                col: 0,
1360            },
1361        );
1362        let mut data = IndexMap::new();
1363        data.insert(
1364            data_path.clone(),
1365            crate::planning::semantics::DataDefinition::Value {
1366                value: crate::planning::semantics::LiteralValue::text_with_type(
1367                    "silver".to_string(),
1368                    tier.clone(),
1369                ),
1370                source,
1371            },
1372        );
1373
1374        let plan = ExecutionPlan {
1375            spec_name: "test".to_string(),
1376            data,
1377            rules: Vec::new(),
1378            reference_evaluation_order: Vec::new(),
1379            meta: HashMap::new(),
1380            unit_index: HashMap::new(),
1381            effective: EffectiveDate::Origin,
1382            sources: Vec::new(),
1383        };
1384
1385        let values = json_data(&[("tier", "platinum")]);
1386
1387        assert!(
1388            plan.set_data_values(values, &default_limits()).is_err(),
1389            "Invalid enum value should be rejected (tier='platinum')"
1390        );
1391    }
1392
1393    #[test]
1394    fn with_values_should_enforce_quantity_decimals() {
1395        // Higher-standard requirement: decimals should be enforced on quantity inputs,
1396        // unless the language explicitly defines rounding semantics.
1397        let data_path = DataPath::new(vec![], "price".to_string());
1398
1399        let money = crate::planning::semantics::LemmaType::primitive(
1400            crate::planning::semantics::TypeSpecification::Quantity {
1401                minimum: None,
1402                maximum: None,
1403                decimals: Some(2),
1404                units: crate::planning::semantics::QuantityUnits::from(vec![
1405                    crate::planning::semantics::QuantityUnit::from_decimal_factor(
1406                        "eur".to_string(),
1407                        rust_decimal::Decimal::from_str("1.0").unwrap(),
1408                        Vec::new(),
1409                    )
1410                    .expect("eur unit factor must be exact decimal"),
1411                ]),
1412                traits: Vec::new(),
1413                decomposition: crate::literals::BaseQuantityVector::new(),
1414                canonical_unit: "eur".to_string(),
1415                help: String::new(),
1416            },
1417        );
1418        let source = Source::new(
1419            crate::parsing::source::SourceType::Volatile,
1420            crate::parsing::ast::Span {
1421                start: 0,
1422                end: 0,
1423                line: 1,
1424                col: 0,
1425            },
1426        );
1427        let mut data = IndexMap::new();
1428        data.insert(
1429            data_path.clone(),
1430            crate::planning::semantics::DataDefinition::Value {
1431                value: crate::planning::semantics::LiteralValue::quantity_with_type(
1432                    rational_zero(),
1433                    "eur".to_string(),
1434                    money.clone(),
1435                ),
1436                source,
1437            },
1438        );
1439
1440        let plan = ExecutionPlan {
1441            spec_name: "test".to_string(),
1442            data,
1443            rules: Vec::new(),
1444            reference_evaluation_order: Vec::new(),
1445            meta: HashMap::new(),
1446            unit_index: HashMap::new(),
1447            effective: EffectiveDate::Origin,
1448            sources: Vec::new(),
1449        };
1450
1451        let values = json_data(&[("price", "1.234 eur")]);
1452
1453        assert!(
1454            plan.set_data_values(values, &default_limits()).is_err(),
1455            "Quantity decimals=2 should reject 1.234 eur"
1456        );
1457    }
1458
1459    #[test]
1460    fn test_serialize_deserialize_execution_plan() {
1461        let data_path = DataPath {
1462            segments: vec![],
1463            data: "age".to_string(),
1464        };
1465        let mut data = IndexMap::new();
1466        data.insert(
1467            data_path.clone(),
1468            crate::planning::semantics::DataDefinition::Value {
1469                value: create_number_literal(0.into()),
1470                source: test_source(),
1471            },
1472        );
1473        let plan = ExecutionPlan {
1474            spec_name: "test".to_string(),
1475            data,
1476            rules: Vec::new(),
1477            reference_evaluation_order: Vec::new(),
1478            meta: HashMap::new(),
1479            unit_index: HashMap::new(),
1480            effective: EffectiveDate::Origin,
1481            sources: Vec::new(),
1482        };
1483
1484        let json = serde_json::to_string(&plan).expect("Should serialize");
1485        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1486
1487        assert_eq!(deserialized.spec_name, plan.spec_name);
1488        assert_eq!(deserialized.data.len(), plan.data.len());
1489        assert_eq!(deserialized.rules.len(), plan.rules.len());
1490    }
1491
1492    #[test]
1493    fn test_serialize_deserialize_plan_with_imported_named_type_defining_spec() {
1494        let dep_spec = Arc::new(crate::parsing::ast::LemmaSpec::new("examples".to_string()));
1495        let imported_type = crate::planning::semantics::LemmaType::new(
1496            "salary".to_string(),
1497            TypeSpecification::quantity(),
1498            crate::planning::semantics::TypeExtends::Custom {
1499                parent: "money".to_string(),
1500                family: "money".to_string(),
1501                defining_spec: crate::planning::semantics::TypeDefiningSpec::Import {
1502                    spec: Arc::clone(&dep_spec),
1503                },
1504            },
1505        );
1506
1507        let salary_path = DataPath::new(vec![], "salary".to_string());
1508        let mut data = IndexMap::new();
1509        data.insert(
1510            salary_path,
1511            crate::planning::semantics::DataDefinition::TypeDeclaration {
1512                resolved_type: imported_type,
1513                declared_default: None,
1514                source: test_source(),
1515            },
1516        );
1517
1518        let plan = ExecutionPlan {
1519            spec_name: "test".to_string(),
1520            data,
1521            rules: Vec::new(),
1522            reference_evaluation_order: Vec::new(),
1523            meta: HashMap::new(),
1524            unit_index: HashMap::new(),
1525            effective: EffectiveDate::Origin,
1526            sources: Vec::new(),
1527        };
1528
1529        let json = serde_json::to_string(&plan).expect("Should serialize");
1530        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1531
1532        let recovered = deserialized
1533            .data
1534            .get(&DataPath::new(vec![], "salary".to_string()))
1535            .and_then(|d| d.schema_type())
1536            .expect("salary type should be present in plan.data");
1537        match &recovered.extends {
1538            crate::planning::semantics::TypeExtends::Custom {
1539                defining_spec: crate::planning::semantics::TypeDefiningSpec::Import { spec },
1540                ..
1541            } => {
1542                assert_eq!(spec.name, "examples");
1543            }
1544            other => panic!(
1545                "Expected imported defining_spec after round-trip, got {:?}",
1546                other
1547            ),
1548        }
1549    }
1550
1551    #[test]
1552    fn test_serialize_deserialize_plan_with_rules() {
1553        use crate::planning::semantics::ExpressionKind;
1554
1555        let age_path = DataPath::new(vec![], "age".to_string());
1556        let mut data = IndexMap::new();
1557        data.insert(
1558            age_path.clone(),
1559            crate::planning::semantics::DataDefinition::Value {
1560                value: create_number_literal(0.into()),
1561                source: test_source(),
1562            },
1563        );
1564        let mut plan = ExecutionPlan {
1565            spec_name: "test".to_string(),
1566            data,
1567            rules: Vec::new(),
1568            reference_evaluation_order: Vec::new(),
1569            meta: HashMap::new(),
1570            unit_index: HashMap::new(),
1571            effective: EffectiveDate::Origin,
1572            sources: Vec::new(),
1573        };
1574
1575        let rule = ExecutableRule {
1576            path: RulePath::new(vec![], "can_drive".to_string()),
1577            name: "can_drive".to_string(),
1578            branches: vec![{
1579                let result = create_literal_expr(create_boolean_literal(true));
1580                Branch {
1581                    condition: Some(Expression::new(
1582                        ExpressionKind::Comparison(
1583                            Arc::new(create_data_path_expr(age_path.clone())),
1584                            crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1585                            Arc::new(create_literal_expr(create_number_literal(18.into()))),
1586                        ),
1587                        test_source(),
1588                    )),
1589                    normalized_condition: None,
1590                    result: result.clone(),
1591                    normalized_result: result,
1592                    source: test_source(),
1593                }
1594            }],
1595            needs_data: BTreeSet::from([age_path]),
1596            source: test_source(),
1597            rule_type: primitive_boolean().clone(),
1598        };
1599
1600        plan.rules.push(rule);
1601
1602        let json = serde_json::to_string(&plan).expect("Should serialize");
1603        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1604
1605        assert_eq!(deserialized.spec_name, plan.spec_name);
1606        assert_eq!(deserialized.data.len(), plan.data.len());
1607        assert_eq!(deserialized.rules.len(), plan.rules.len());
1608        assert_eq!(deserialized.rules[0].name, "can_drive");
1609        assert_eq!(deserialized.rules[0].branches.len(), 1);
1610        assert_eq!(deserialized.rules[0].needs_data.len(), 1);
1611    }
1612
1613    #[test]
1614    fn test_serialize_deserialize_plan_with_nested_data_paths() {
1615        use crate::planning::semantics::PathSegment;
1616        let data_path = DataPath {
1617            segments: vec![PathSegment {
1618                data: "employee".to_string(),
1619                spec: "private".to_string(),
1620            }],
1621            data: "salary".to_string(),
1622        };
1623
1624        let mut data = IndexMap::new();
1625        data.insert(
1626            data_path.clone(),
1627            crate::planning::semantics::DataDefinition::Value {
1628                value: create_number_literal(0.into()),
1629                source: test_source(),
1630            },
1631        );
1632        let plan = ExecutionPlan {
1633            spec_name: "test".to_string(),
1634            data,
1635            rules: Vec::new(),
1636            reference_evaluation_order: Vec::new(),
1637            meta: HashMap::new(),
1638            unit_index: HashMap::new(),
1639            effective: EffectiveDate::Origin,
1640            sources: Vec::new(),
1641        };
1642
1643        let json = serde_json::to_string(&plan).expect("Should serialize");
1644        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1645
1646        assert_eq!(deserialized.data.len(), 1);
1647        let (deserialized_path, _) = deserialized.data.iter().next().unwrap();
1648        assert_eq!(deserialized_path.segments.len(), 1);
1649        assert_eq!(deserialized_path.segments[0].data, "employee");
1650        assert_eq!(deserialized_path.data, "salary");
1651    }
1652
1653    #[test]
1654    fn test_serialize_deserialize_plan_with_multiple_data_types() {
1655        let name_path = DataPath::new(vec![], "name".to_string());
1656        let age_path = DataPath::new(vec![], "age".to_string());
1657        let active_path = DataPath::new(vec![], "active".to_string());
1658
1659        let mut data = IndexMap::new();
1660        data.insert(
1661            name_path.clone(),
1662            crate::planning::semantics::DataDefinition::Value {
1663                value: create_text_literal("Alice".to_string()),
1664                source: test_source(),
1665            },
1666        );
1667        data.insert(
1668            age_path.clone(),
1669            crate::planning::semantics::DataDefinition::Value {
1670                value: create_number_literal(30.into()),
1671                source: test_source(),
1672            },
1673        );
1674        data.insert(
1675            active_path.clone(),
1676            crate::planning::semantics::DataDefinition::Value {
1677                value: create_boolean_literal(true),
1678                source: test_source(),
1679            },
1680        );
1681
1682        let plan = ExecutionPlan {
1683            spec_name: "test".to_string(),
1684            data,
1685            rules: Vec::new(),
1686            reference_evaluation_order: Vec::new(),
1687            meta: HashMap::new(),
1688            unit_index: HashMap::new(),
1689            effective: EffectiveDate::Origin,
1690            sources: Vec::new(),
1691        };
1692
1693        let json = serde_json::to_string(&plan).expect("Should serialize");
1694        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1695
1696        assert_eq!(deserialized.data.len(), 3);
1697
1698        assert_eq!(
1699            deserialized.get_data_value(&name_path).unwrap().value,
1700            crate::planning::semantics::ValueKind::Text("Alice".to_string())
1701        );
1702        assert_eq!(
1703            deserialized.get_data_value(&age_path).unwrap().value,
1704            crate::planning::semantics::ValueKind::Number(30.into())
1705        );
1706        assert_eq!(
1707            deserialized.get_data_value(&active_path).unwrap().value,
1708            crate::planning::semantics::ValueKind::Boolean(true)
1709        );
1710    }
1711
1712    #[test]
1713    fn test_serialize_deserialize_plan_with_multiple_branches() {
1714        use crate::planning::semantics::ExpressionKind;
1715
1716        let points_path = DataPath::new(vec![], "points".to_string());
1717        let mut data = IndexMap::new();
1718        data.insert(
1719            points_path.clone(),
1720            crate::planning::semantics::DataDefinition::Value {
1721                value: create_number_literal(0.into()),
1722                source: test_source(),
1723            },
1724        );
1725        let mut plan = ExecutionPlan {
1726            spec_name: "test".to_string(),
1727            data,
1728            rules: Vec::new(),
1729            reference_evaluation_order: Vec::new(),
1730            meta: HashMap::new(),
1731            unit_index: HashMap::new(),
1732            effective: EffectiveDate::Origin,
1733            sources: Vec::new(),
1734        };
1735
1736        let rule = ExecutableRule {
1737            path: RulePath::new(vec![], "tier".to_string()),
1738            name: "tier".to_string(),
1739            branches: vec![
1740                {
1741                    let result = create_literal_expr(create_text_literal("bronze".to_string()));
1742                    Branch {
1743                        condition: None,
1744                        normalized_condition: None,
1745                        result: result.clone(),
1746                        normalized_result: result,
1747                        source: test_source(),
1748                    }
1749                },
1750                {
1751                    let result = create_literal_expr(create_text_literal("silver".to_string()));
1752                    Branch {
1753                        condition: Some(Expression::new(
1754                            ExpressionKind::Comparison(
1755                                Arc::new(create_data_path_expr(points_path.clone())),
1756                                crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1757                                Arc::new(create_literal_expr(create_number_literal(100.into()))),
1758                            ),
1759                            test_source(),
1760                        )),
1761                        normalized_condition: None,
1762                        result: result.clone(),
1763                        normalized_result: result,
1764                        source: test_source(),
1765                    }
1766                },
1767                {
1768                    let result = create_literal_expr(create_text_literal("gold".to_string()));
1769                    Branch {
1770                        condition: Some(Expression::new(
1771                            ExpressionKind::Comparison(
1772                                Arc::new(create_data_path_expr(points_path.clone())),
1773                                crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1774                                Arc::new(create_literal_expr(create_number_literal(500.into()))),
1775                            ),
1776                            test_source(),
1777                        )),
1778                        normalized_condition: None,
1779                        result: result.clone(),
1780                        normalized_result: result,
1781                        source: test_source(),
1782                    }
1783                },
1784            ],
1785            needs_data: BTreeSet::from([points_path]),
1786            source: test_source(),
1787            rule_type: primitive_text().clone(),
1788        };
1789
1790        plan.rules.push(rule);
1791
1792        let json = serde_json::to_string(&plan).expect("Should serialize");
1793        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1794
1795        assert_eq!(deserialized.rules.len(), 1);
1796        assert_eq!(deserialized.rules[0].branches.len(), 3);
1797        assert!(deserialized.rules[0].branches[0].condition.is_none());
1798        assert!(deserialized.rules[0].branches[1].condition.is_some());
1799        assert!(deserialized.rules[0].branches[2].condition.is_some());
1800    }
1801
1802    #[test]
1803    fn test_serialize_deserialize_empty_plan() {
1804        let plan = ExecutionPlan {
1805            spec_name: "empty".to_string(),
1806            data: IndexMap::new(),
1807            rules: Vec::new(),
1808            reference_evaluation_order: Vec::new(),
1809            meta: HashMap::new(),
1810            unit_index: HashMap::new(),
1811            effective: EffectiveDate::Origin,
1812            sources: Vec::new(),
1813        };
1814
1815        let json = serde_json::to_string(&plan).expect("Should serialize");
1816        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1817
1818        assert_eq!(deserialized.spec_name, "empty");
1819        assert_eq!(deserialized.data.len(), 0);
1820        assert_eq!(deserialized.rules.len(), 0);
1821    }
1822
1823    #[test]
1824    fn test_serialize_deserialize_plan_with_arithmetic_expressions() {
1825        use crate::planning::semantics::ExpressionKind;
1826
1827        let x_path = DataPath::new(vec![], "x".to_string());
1828        let mut data = IndexMap::new();
1829        data.insert(
1830            x_path.clone(),
1831            crate::planning::semantics::DataDefinition::Value {
1832                value: create_number_literal(0.into()),
1833                source: test_source(),
1834            },
1835        );
1836        let mut plan = ExecutionPlan {
1837            spec_name: "test".to_string(),
1838            data,
1839            rules: Vec::new(),
1840            reference_evaluation_order: Vec::new(),
1841            meta: HashMap::new(),
1842            unit_index: HashMap::new(),
1843            effective: EffectiveDate::Origin,
1844            sources: Vec::new(),
1845        };
1846
1847        let rule = ExecutableRule {
1848            path: RulePath::new(vec![], "doubled".to_string()),
1849            name: "doubled".to_string(),
1850            branches: vec![{
1851                let result = Expression::new(
1852                    ExpressionKind::Arithmetic(
1853                        Arc::new(create_data_path_expr(x_path.clone())),
1854                        crate::parsing::ast::ArithmeticComputation::Multiply,
1855                        Arc::new(create_literal_expr(create_number_literal(2.into()))),
1856                    ),
1857                    test_source(),
1858                );
1859                Branch {
1860                    condition: None,
1861                    normalized_condition: None,
1862                    result: result.clone(),
1863                    normalized_result: result,
1864                    source: test_source(),
1865                }
1866            }],
1867            needs_data: BTreeSet::from([x_path]),
1868            source: test_source(),
1869            rule_type: crate::planning::semantics::primitive_number().clone(),
1870        };
1871
1872        plan.rules.push(rule);
1873
1874        let json = serde_json::to_string(&plan).expect("Should serialize");
1875        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1876
1877        assert_eq!(deserialized.rules.len(), 1);
1878        match &deserialized.rules[0].branches[0].result.kind {
1879            ExpressionKind::Arithmetic(left, op, right) => {
1880                assert_eq!(*op, crate::parsing::ast::ArithmeticComputation::Multiply);
1881                match &left.kind {
1882                    ExpressionKind::DataPath(_) => {}
1883                    _ => panic!("Expected DataPath in left operand"),
1884                }
1885                match &right.kind {
1886                    ExpressionKind::Literal(_) => {}
1887                    _ => panic!("Expected Literal in right operand"),
1888                }
1889            }
1890            _ => panic!("Expected Arithmetic expression"),
1891        }
1892    }
1893
1894    #[test]
1895    fn test_serialize_deserialize_round_trip_equality() {
1896        use crate::planning::semantics::ExpressionKind;
1897
1898        let age_path = DataPath::new(vec![], "age".to_string());
1899        let mut data = IndexMap::new();
1900        data.insert(
1901            age_path.clone(),
1902            crate::planning::semantics::DataDefinition::Value {
1903                value: create_number_literal(0.into()),
1904                source: test_source(),
1905            },
1906        );
1907        let mut plan = ExecutionPlan {
1908            spec_name: "test".to_string(),
1909            data,
1910            rules: Vec::new(),
1911            reference_evaluation_order: Vec::new(),
1912            meta: HashMap::new(),
1913            unit_index: HashMap::new(),
1914            effective: EffectiveDate::Origin,
1915            sources: Vec::new(),
1916        };
1917
1918        let rule = ExecutableRule {
1919            path: RulePath::new(vec![], "is_adult".to_string()),
1920            name: "is_adult".to_string(),
1921            branches: vec![{
1922                let result = create_literal_expr(create_boolean_literal(true));
1923                Branch {
1924                    condition: Some(Expression::new(
1925                        ExpressionKind::Comparison(
1926                            Arc::new(create_data_path_expr(age_path.clone())),
1927                            crate::parsing::ast::ComparisonComputation::GreaterThanOrEqual,
1928                            Arc::new(create_literal_expr(create_number_literal(18.into()))),
1929                        ),
1930                        test_source(),
1931                    )),
1932                    normalized_condition: None,
1933                    result: result.clone(),
1934                    normalized_result: result,
1935                    source: test_source(),
1936                }
1937            }],
1938            needs_data: BTreeSet::from([age_path]),
1939            source: test_source(),
1940            rule_type: primitive_boolean().clone(),
1941        };
1942
1943        plan.rules.push(rule);
1944
1945        let json = serde_json::to_string(&plan).expect("Should serialize");
1946        let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1947
1948        let json2 = serde_json::to_string(&deserialized).expect("Should serialize again");
1949        let deserialized2: ExecutionPlan =
1950            serde_json::from_str(&json2).expect("Should deserialize again");
1951
1952        assert_eq!(deserialized2.spec_name, plan.spec_name);
1953        assert_eq!(deserialized2.data.len(), plan.data.len());
1954        assert_eq!(deserialized2.rules.len(), plan.rules.len());
1955        assert_eq!(deserialized2.rules[0].name, plan.rules[0].name);
1956        assert_eq!(
1957            deserialized2.rules[0].branches.len(),
1958            plan.rules[0].branches.len()
1959        );
1960    }
1961
1962    fn empty_plan(effective: crate::parsing::ast::EffectiveDate) -> ExecutionPlan {
1963        ExecutionPlan {
1964            spec_name: "s".into(),
1965            data: IndexMap::new(),
1966            rules: Vec::new(),
1967            reference_evaluation_order: Vec::new(),
1968            meta: HashMap::new(),
1969            unit_index: HashMap::new(),
1970            effective,
1971            sources: Vec::new(),
1972        }
1973    }
1974
1975    #[test]
1976    fn plan_at_exact_boundary_selects_later_slice() {
1977        use crate::parsing::ast::{DateTimeValue, EffectiveDate};
1978
1979        let june = DateTimeValue {
1980            year: 2025,
1981            month: 6,
1982            day: 1,
1983            hour: 0,
1984            minute: 0,
1985            second: 0,
1986            microsecond: 0,
1987            timezone: None,
1988        };
1989        let dec = DateTimeValue {
1990            year: 2025,
1991            month: 12,
1992            day: 1,
1993            hour: 0,
1994            minute: 0,
1995            second: 0,
1996            microsecond: 0,
1997            timezone: None,
1998        };
1999
2000        let set = ExecutionPlanSet {
2001            spec_name: "s".into(),
2002            plans: vec![
2003                empty_plan(EffectiveDate::Origin),
2004                empty_plan(EffectiveDate::DateTimeValue(june.clone())),
2005                empty_plan(EffectiveDate::DateTimeValue(dec.clone())),
2006            ],
2007        };
2008
2009        assert!(std::ptr::eq(
2010            set.plan_at(&EffectiveDate::DateTimeValue(june.clone()))
2011                .expect("boundary instant"),
2012            &set.plans[1]
2013        ));
2014        assert!(std::ptr::eq(
2015            set.plan_at(&EffectiveDate::DateTimeValue(dec.clone()))
2016                .expect("dec boundary"),
2017            &set.plans[2]
2018        ));
2019    }
2020
2021    #[test]
2022    fn plan_at_day_before_boundary_stays_in_earlier_slice() {
2023        use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2024
2025        let june = DateTimeValue {
2026            year: 2025,
2027            month: 6,
2028            day: 1,
2029            hour: 0,
2030            minute: 0,
2031            second: 0,
2032            microsecond: 0,
2033            timezone: None,
2034        };
2035        let may_end = DateTimeValue {
2036            year: 2025,
2037            month: 5,
2038            day: 31,
2039            hour: 23,
2040            minute: 59,
2041            second: 59,
2042            microsecond: 0,
2043            timezone: None,
2044        };
2045
2046        let set = ExecutionPlanSet {
2047            spec_name: "s".into(),
2048            plans: vec![
2049                empty_plan(EffectiveDate::Origin),
2050                empty_plan(EffectiveDate::DateTimeValue(june)),
2051            ],
2052        };
2053
2054        assert!(std::ptr::eq(
2055            set.plan_at(&EffectiveDate::DateTimeValue(may_end))
2056                .expect("may 31"),
2057            &set.plans[0]
2058        ));
2059    }
2060
2061    #[test]
2062    fn plan_at_single_plan_matches_any_instant_after_start() {
2063        use crate::parsing::ast::{DateTimeValue, EffectiveDate};
2064
2065        let t = DateTimeValue {
2066            year: 2025,
2067            month: 3,
2068            day: 1,
2069            hour: 0,
2070            minute: 0,
2071            second: 0,
2072            microsecond: 0,
2073            timezone: None,
2074        };
2075        let set = ExecutionPlanSet {
2076            spec_name: "s".into(),
2077            plans: vec![empty_plan(EffectiveDate::DateTimeValue(DateTimeValue {
2078                year: 2025,
2079                month: 1,
2080                day: 1,
2081                hour: 0,
2082                minute: 0,
2083                second: 0,
2084                microsecond: 0,
2085                timezone: None,
2086            }))],
2087        };
2088        assert!(std::ptr::eq(
2089            set.plan_at(&EffectiveDate::DateTimeValue(t))
2090                .expect("inside single slice"),
2091            &set.plans[0]
2092        ));
2093    }
2094
2095    /// The schema JSON shape is the IO contract for every non-Rust consumer
2096    /// (WASM playground, Hex, HTTP, TypeScript). Nail the exact envelope.
2097    #[test]
2098    fn schema_json_shape_contract() {
2099        let mut engine = Engine::new();
2100        engine
2101            .load(
2102                r#"
2103                spec pricing
2104                data bridge_height: quantity
2105                  -> unit meter 1
2106                  -> default 100 meter
2107                data quantity: number -> minimum 0
2108                rule cost: bridge_height * quantity
2109                "#,
2110                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2111                    "test.lemma",
2112                ))),
2113            )
2114            .unwrap();
2115        let now = DateTimeValue::now();
2116        let schema = engine
2117            .get_plan(None, "pricing", Some(&now))
2118            .unwrap()
2119            .schema();
2120
2121        let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
2122
2123        let bh = &value["data"]["bridge_height"];
2124        assert!(
2125            bh.is_object(),
2126            "data entry must be a named object, not tuple"
2127        );
2128        assert!(
2129            bh.get("type").is_some(),
2130            "data entry must expose `type` field"
2131        );
2132        assert!(
2133            bh.get("default").is_some(),
2134            "bridge_height exposes `-> default` as schema default suggestion"
2135        );
2136        assert!(
2137            bh.get("bound_value").is_none(),
2138            "bridge_height is not a spec-bound literal"
2139        );
2140
2141        let ty = &bh["type"];
2142        assert_eq!(
2143            ty["kind"], "quantity",
2144            "kind tag sits on the type object itself"
2145        );
2146        assert!(
2147            ty["units"].is_array(),
2148            "quantity-only fields flatten up to top level"
2149        );
2150        assert!(
2151            ty.get("options").is_none(),
2152            "text-only fields must not leak"
2153        );
2154
2155        let qty = &value["data"]["quantity"];
2156        assert_eq!(qty["type"]["kind"], "number");
2157        assert!(
2158            qty.get("default").is_none(),
2159            "quantity has no default suggestion"
2160        );
2161        assert!(
2162            qty.get("bound_value").is_none(),
2163            "quantity has no bound literal"
2164        );
2165
2166        let cost = &value["rules"]["cost"];
2167        assert_eq!(
2168            cost["kind"], "quantity",
2169            "rule types use the same flat shape"
2170        );
2171        assert!(
2172            cost["units"].is_array() && !cost["units"].as_array().unwrap().is_empty(),
2173            "quantity rule result types expose declared units"
2174        );
2175        assert!(
2176            cost["units"][0].get("factor").is_some(),
2177            "quantity rule units use factor field"
2178        );
2179    }
2180
2181    #[test]
2182    fn schema_rule_result_units_contract() {
2183        let mut engine = Engine::new();
2184        engine
2185            .load(
2186                r#"
2187                spec units_contract
2188                data money: quantity
2189                  -> unit eur 1
2190                  -> unit usd 0.91
2191                data rate: ratio
2192                  -> unit basis_points 10000
2193                  -> unit percent 100
2194                  -> default 500 basis_points
2195                rule total: money
2196                rule rate_out: rate
2197                "#,
2198                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
2199                    "units_contract.lemma",
2200                ))),
2201            )
2202            .unwrap();
2203        let now = DateTimeValue::now();
2204        let schema = engine
2205            .get_plan(None, "units_contract", Some(&now))
2206            .unwrap()
2207            .schema();
2208        let value: serde_json::Value = serde_json::to_value(&schema).unwrap();
2209
2210        let money_units = &value["data"]["money"]["type"]["units"];
2211        assert!(money_units.is_array() && !money_units.as_array().unwrap().is_empty());
2212        assert!(money_units[0].get("name").is_some());
2213        assert!(money_units[0].get("factor").is_some());
2214        assert!(money_units[0]["factor"].get("numer").is_some());
2215        assert!(money_units[0]["factor"].get("denom").is_some());
2216
2217        let rate_units = &value["data"]["rate"]["type"]["units"];
2218        assert!(rate_units.is_array() && !rate_units.as_array().unwrap().is_empty());
2219        assert!(rate_units[0].get("name").is_some());
2220        assert!(rate_units[0].get("value").is_some());
2221        assert!(rate_units[0]["value"].get("numer").is_some());
2222        assert!(rate_units[0]["value"].get("denom").is_some());
2223
2224        let total_rule_units = &value["rules"]["total"]["units"];
2225        let money_unit_names: Vec<_> = money_units
2226            .as_array()
2227            .unwrap()
2228            .iter()
2229            .map(|u| u["name"].as_str().unwrap())
2230            .collect();
2231        let total_rule_unit_names: Vec<_> = total_rule_units
2232            .as_array()
2233            .unwrap()
2234            .iter()
2235            .map(|u| u["name"].as_str().unwrap())
2236            .collect();
2237        assert_eq!(total_rule_unit_names, money_unit_names);
2238
2239        let rate_out_rule_units = &value["rules"]["rate_out"]["units"];
2240        let rate_unit_names: Vec<_> = rate_units
2241            .as_array()
2242            .unwrap()
2243            .iter()
2244            .map(|u| u["name"].as_str().unwrap())
2245            .collect();
2246        let rate_out_rule_unit_names: Vec<_> = rate_out_rule_units
2247            .as_array()
2248            .unwrap()
2249            .iter()
2250            .map(|u| u["name"].as_str().unwrap())
2251            .collect();
2252        assert_eq!(rate_out_rule_unit_names, rate_unit_names);
2253    }
2254
2255    #[test]
2256    fn schema_json_round_trip_preserves_shape() {
2257        let mut engine = Engine::new();
2258        engine
2259            .load(
2260                r#"
2261                spec s
2262                data age: number -> minimum 0 -> default 18
2263                data grade: text -> options "A" "B" "C"
2264                rule adult: age >= 18
2265                "#,
2266                crate::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("s.lemma"))),
2267            )
2268            .unwrap();
2269        let now = DateTimeValue::now();
2270        let schema = engine.get_plan(None, "s", Some(&now)).unwrap().schema();
2271
2272        let json = serde_json::to_string(&schema).unwrap();
2273        let round_tripped: SpecSchema = serde_json::from_str(&json).unwrap();
2274        assert_eq!(schema, round_tripped);
2275    }
2276}
2277
2278// ---------------------------------------------------------------------------
2279// ExecutionPlanSet (formerly plan_set.rs)
2280// ---------------------------------------------------------------------------