Skip to main content

lemma/evaluation/
response.rs

1use crate::computation::{OperationResult, VetoType};
2use crate::evaluation::explanations::Explanation;
3
4use crate::error::Error;
5use crate::evaluation::evaluate_data::EvaluateDataValue;
6use crate::evaluation::rows::{row_result, take_row_context};
7use crate::evaluation::tree::ensure_rule_values;
8use crate::evaluation::EvaluationContext;
9use crate::parsing::ast::DateTimeValue;
10use crate::planning::execution_plan::ExecutionPlan;
11use crate::planning::semantics::{
12    DataDefinition, DataPath, InstanceKey, LemmaType, LiteralValue, ReferenceEnd, RulePath, Source,
13    SpecValue, ValueKind,
14};
15use crate::planning::unit_family::FamilyUnitCatalog;
16use crate::result_value::{
17    rule_result_value_failure_message, rule_result_value_from_literal, RowReference, RowResult,
18    RuleResultValue, RuleResultValueFailure, SpecResult, SpecRuleResult,
19};
20use indexmap::IndexMap;
21use std::collections::HashMap;
22use std::sync::Arc;
23
24/// Rule info with resolved expressions for use in evaluation response.
25/// Evaluation uses only semantics types; no parsing types.
26#[derive(Debug, Clone)]
27pub struct EvaluatedRule {
28    pub name: String,
29    pub path: RulePath,
30    pub source_location: Source,
31    pub rule_type: Arc<LemmaType>,
32}
33
34/// Response from evaluating a Lemma spec
35#[derive(Debug, Clone)]
36pub struct Response {
37    pub spec_name: String,
38    pub effective: String,
39    /// Declared temporal window `[spec_effective_from, spec_effective_to)` of the
40    /// resolved spec version. Set by [`crate::Engine::evaluate`] after evaluation;
41    /// `None` here (evaluation-internal construction) until that assignment.
42    pub spec_effective_from: Option<DateTimeValue>,
43    pub spec_effective_to: Option<DateTimeValue>,
44    pub results: IndexMap<String, RuleResult>,
45    /// Rows reached by `results`, indexed by [`RowReference::index`].
46    pub rows: Vec<RowResult>,
47}
48
49/// Result of evaluating a single rule. Struct fields match the API JSON shape.
50#[derive(Debug, Clone)]
51pub struct RuleResult {
52    pub rule: EvaluatedRule,
53    pub veto_detail: Option<VetoType>,
54
55    pub vetoed: bool,
56    pub veto_reason: Option<String>,
57    pub rule_type: String,
58
59    /// Flattened value fields, including `result` when the rule is not vetoed.
60    pub result: Option<RuleResultValue>,
61    pub explanation: Option<Explanation>,
62    /// Unbound caller data paths still live for this rule under the current run data
63    /// (`DataPath::input_key` strings; subset of `Show.data` keys with non-empty
64    /// `needed_by_rules`). Ordered by evaluation / decision-tree preorder (first key =
65    /// next fact the live tree needs); `Show.data` stays declaration order.
66    missing_data: Vec<String>,
67    /// Closed options for keys in [`Self::missing_data`]. A key with no entry is not
68    /// constrained by a set. An empty list is a closed set with nothing valid.
69    /// Each value is what the next evaluate accepts.
70    missing_data_options: IndexMap<String, Vec<EvaluateDataValue>>,
71}
72
73impl RuleResult {
74    /// Engine-rendered one-liner from the flattened [`RuleResultValue`].
75    #[must_use]
76    pub fn result(&self) -> Option<&str> {
77        self.result
78            .as_ref()
79            .and_then(|value| value.result.as_deref())
80    }
81
82    /// True when this result is a [`VetoType::MissingData`] or [`VetoType::MissingRowData`] veto.
83    ///
84    /// Value and other vetoes are settled answers; leftover live keys in
85    /// [`Self::missing_data`] must not drive prompts or human "Missing data" display.
86    #[must_use]
87    pub fn is_missing_data(&self) -> bool {
88        matches!(
89            self.veto_detail.as_ref(),
90            Some(VetoType::MissingData { .. } | VetoType::MissingRowData { .. })
91        )
92    }
93
94    /// Unbound caller data paths still live for this rule (`DataPath::input_key`),
95    /// in evaluation / decision-tree order (contrast `Show.data` declaration order).
96    #[must_use]
97    pub fn missing_data(&self) -> &[String] {
98        &self.missing_data
99    }
100
101    /// Closed sets for this rule's missing inputs. Keys are a subset of [`Self::missing_data`].
102    #[must_use]
103    pub fn missing_data_options(&self) -> &IndexMap<String, Vec<EvaluateDataValue>> {
104        &self.missing_data_options
105    }
106
107    pub(crate) fn with_missing_data_options(
108        mut self,
109        missing_data_options: IndexMap<String, Vec<EvaluateDataValue>>,
110    ) -> Self {
111        for key in missing_data_options.keys() {
112            if !self.missing_data.iter().any(|listed| listed == key) {
113                panic!(
114                    "BUG: missing_data_options key '{key}' is not in missing_data {:?}",
115                    self.missing_data
116                );
117            }
118        }
119        self.missing_data_options = missing_data_options;
120        self
121    }
122
123    /// Build a [`RuleResult`] for API output from a rule evaluation result.
124    ///
125    /// Measure and ratio payloads expand into every unit in the result type's family.
126    pub(crate) fn from_operation_result(
127        rule: EvaluatedRule,
128        operation_result: &OperationResult,
129        rule_type: &LemmaType,
130        family_units: &FamilyUnitCatalog,
131        explanation: Option<Explanation>,
132        missing_data: Vec<String>,
133    ) -> Self {
134        Self::from_evaluated(
135            rule,
136            operation_result,
137            rule_type,
138            family_units,
139            explanation,
140            missing_data,
141            None,
142        )
143    }
144
145    pub(crate) fn from_evaluated(
146        rule: EvaluatedRule,
147        operation_result: &OperationResult,
148        rule_type: &LemmaType,
149        family_units: &FamilyUnitCatalog,
150        explanation: Option<Explanation>,
151        missing_data: Vec<String>,
152        runtime_scales: Option<
153            &std::collections::HashMap<String, crate::computation::rational::RationalInteger>,
154        >,
155    ) -> Self {
156        match operation_result {
157            OperationResult::Veto(VetoType::MissingData { data, .. }) => {
158                let key = data.input_key();
159                if !missing_data.iter().any(|listed| listed == &key) {
160                    panic!("BUG: MissingData path {key} not in missing_data {missing_data:?}");
161                }
162            }
163            OperationResult::Veto(VetoType::MissingRowData { key }) => {
164                if !missing_data.contains(key) {
165                    panic!("BUG: missing row field {key} not in missing_data {missing_data:?}");
166                }
167            }
168            _ => {
169                if !missing_data.is_empty() {
170                    panic!(
171                        "BUG: missing_data must be empty when result is not MissingData: {missing_data:?}"
172                    );
173                }
174            }
175        }
176        let rule_type_name = rule_type.name().to_string();
177        match operation_result {
178            OperationResult::Veto(veto) => Self::vetoed_with(
179                rule,
180                veto.clone(),
181                rule_type_name,
182                explanation,
183                missing_data,
184                IndexMap::new(),
185            ),
186            OperationResult::Value(bound) => {
187                match rule_result_value_from_literal(
188                    &bound.to_literal(),
189                    rule_type,
190                    family_units,
191                    runtime_scales,
192                ) {
193                    Ok(value) => Self {
194                        rule,
195                        veto_detail: None,
196                        vetoed: false,
197                        veto_reason: None,
198                        rule_type: rule_type_name,
199                        result: Some(value),
200                        explanation,
201                        missing_data,
202                        missing_data_options: IndexMap::new(),
203                    },
204                    Err(failure) => vetoed_rule_result_for_rule_result_value_failure(
205                        rule,
206                        rule_type,
207                        explanation,
208                        failure,
209                        missing_data,
210                    ),
211                }
212            }
213        }
214    }
215
216    fn vetoed_with(
217        rule: EvaluatedRule,
218        veto: VetoType,
219        rule_type: String,
220        explanation: Option<Explanation>,
221        missing_data: Vec<String>,
222        missing_data_options: IndexMap<String, Vec<EvaluateDataValue>>,
223    ) -> Self {
224        Self {
225            rule,
226            veto_reason: match &veto {
227                VetoType::UserDefined { message: None } => None,
228                _ => Some(veto.to_string()),
229            },
230            veto_detail: Some(veto),
231            vetoed: true,
232            rule_type,
233            result: None,
234            explanation,
235            missing_data,
236            missing_data_options,
237        }
238    }
239
240    /// Reconstruct the evaluated [`LiteralValue`] from committed [`RuleResultValue`] fields.
241    ///
242    /// Panics if the rule is vetoed or fields cannot be reconstructed.
243    pub fn to_literal(&self) -> LiteralValue {
244        assert!(
245            !self.vetoed,
246            "BUG: to_literal called on vetoed rule '{}'",
247            self.rule.name
248        );
249        let value = self
250            .result
251            .as_ref()
252            .unwrap_or_else(|| panic!("BUG: non-vetoed rule '{}' missing result", self.rule.name));
253        value.to_literal(&self.rule.rule_type)
254    }
255}
256
257fn vetoed_rule_result_for_rule_result_value_failure(
258    rule: EvaluatedRule,
259    rule_type: &LemmaType,
260    explanation: Option<Explanation>,
261    failure: RuleResultValueFailure,
262    missing_data: Vec<String>,
263) -> RuleResult {
264    RuleResult::vetoed_with(
265        rule,
266        VetoType::computation(rule_result_value_failure_message(failure).to_string()),
267        rule_type.name().to_string(),
268        explanation,
269        missing_data,
270        IndexMap::new(),
271    )
272}
273
274impl Response {
275    /// Looks up a rule result by name.
276    ///
277    /// Returns an error if the rule is not found.
278    pub fn get(&self, rule_name: &str) -> Result<&RuleResult, Error> {
279        self.results
280            .get(rule_name)
281            .ok_or_else(|| crate::error::Error::rule_not_found(rule_name, None::<String>))
282    }
283
284    pub fn add_result(&mut self, result: RuleResult) {
285        self.results.insert(result.rule.name.clone(), result);
286    }
287}
288
289/// Distinct rows reached by one response, in first-reached order. Each row is filled
290/// once and referenced by index everywhere else, so output grows with the number of
291/// rows, not with the number of paths that reach them.
292#[derive(Default)]
293pub(crate) struct RowTable {
294    index: HashMap<InstanceKey, usize>,
295    rows: Vec<Option<RowResult>>,
296}
297
298impl RowTable {
299    pub(crate) fn into_rows(self) -> Vec<RowResult> {
300        self.rows
301            .into_iter()
302            .enumerate()
303            .map(|(index, row)| row.unwrap_or_else(|| panic!("BUG: row {index} never filled")))
304            .collect()
305    }
306}
307
308/// Fill every spec value inside `literal`'s API `value`: a planned instance (a `uses`
309/// import or a row written in a spec, at any depth) gets its data and rules inline,
310/// any other row a reference into `table`.
311pub(crate) fn fill_spec_values(
312    literal: &ValueKind,
313    value: &mut RuleResultValue,
314    plan: &ExecutionPlan,
315    ctx: &mut EvaluationContext,
316    table: &mut RowTable,
317) {
318    if matches!(
319        literal,
320        ValueKind::Number(_)
321            | ValueKind::Measure(_)
322            | ValueKind::Ratio(_)
323            | ValueKind::Text(_)
324            | ValueKind::Date(_)
325            | ValueKind::Time(_)
326            | ValueKind::Boolean(_)
327            | ValueKind::Range(_, _)
328    ) {
329        return;
330    }
331    let mut walk = Walk {
332        root: plan,
333        base: ctx,
334        table,
335        scopes: Vec::new(),
336        ancestors: Vec::new(),
337    };
338    let root = Frame::fill(literal.clone(), std::mem::take(value));
339    *value = walk.run(root).into_value();
340}
341
342/// Context of an instance whose members are being walked, over its template.
343struct Scope<'a> {
344    plan: &'a ExecutionPlan,
345    ctx: EvaluationContext,
346}
347
348/// Shared state of one fill walk. `root` is the plan being evaluated and `base` its
349/// context; `scopes` holds the instance contexts entered, innermost last, taken out of
350/// the context cache while their members are walked.
351struct Walk<'a, 'b> {
352    root: &'a ExecutionPlan,
353    base: &'b mut EvaluationContext,
354    table: &'b mut RowTable,
355    scopes: Vec<Scope<'a>>,
356    ancestors: Vec<InstanceKey>,
357}
358
359impl<'a> Walk<'a, '_> {
360    fn ctx(&mut self) -> &mut EvaluationContext {
361        match self.scopes.last_mut() {
362            Some(scope) => &mut scope.ctx,
363            None => &mut *self.base,
364        }
365    }
366
367    /// Plan and context `key`'s context is built from: its owner's scope while the walk
368    /// is inside it, else the root, which finds a root-owned row's bindings in itself
369    /// and any other owner's context in the cache.
370    fn owner_scope(&mut self, key: &InstanceKey) -> (&'a ExecutionPlan, &mut EvaluationContext) {
371        let position = key.owner().and_then(|owner| {
372            self.scopes
373                .iter()
374                .rposition(|scope| scope.ctx.owner_row.as_ref() == Some(owner))
375        });
376        match position {
377            Some(position) => {
378                let scope = &mut self.scopes[position];
379                (scope.plan, &mut scope.ctx)
380            }
381            None => (self.root, &mut *self.base),
382        }
383    }
384
385    fn run(&mut self, root: Frame<'a>) -> Output {
386        let mut frames = vec![root];
387        let mut input = None;
388        loop {
389            let frame = frames.last_mut().expect("BUG: fill walk frame stack empty");
390            match frame.resume(self, input.take()) {
391                Step::Call(child) => frames.push(child),
392                Step::Return(output) => {
393                    frames.pop();
394                    if frames.is_empty() {
395                        return output;
396                    }
397                    input = Some(output);
398                }
399            }
400        }
401    }
402}
403
404enum Frame<'a> {
405    Fill(Box<FillFrame>),
406    Instance(InstanceFrame),
407    Members(Box<MembersFrame<'a>>),
408}
409
410impl<'a> Frame<'a> {
411    fn fill(literal: ValueKind, value: RuleResultValue) -> Self {
412        Frame::Fill(Box::new(FillFrame::new(literal, value)))
413    }
414
415    fn resume(&mut self, walk: &mut Walk<'a, '_>, input: Option<Output>) -> Step<'a> {
416        match self {
417            Frame::Fill(frame) => frame.resume(walk, input),
418            Frame::Instance(frame) => frame.resume(walk, input),
419            Frame::Members(frame) => frame.resume(walk, input),
420        }
421    }
422}
423
424enum Step<'a> {
425    Call(Frame<'a>),
426    Return(Output),
427}
428
429enum Output {
430    Value(Box<RuleResultValue>),
431    Spec(Box<SpecResult>),
432    Row(RowReference),
433    Members(FilledMembers),
434}
435
436impl Output {
437    fn into_value(self) -> RuleResultValue {
438        match self {
439            Output::Value(value) => *value,
440            _ => panic!("BUG: fill walk expected a value output"),
441        }
442    }
443
444    fn into_spec(self) -> SpecResult {
445        match self {
446            Output::Spec(spec) => *spec,
447            _ => panic!("BUG: fill walk expected a spec output"),
448        }
449    }
450
451    fn into_row(self) -> RowReference {
452        match self {
453            Output::Row(row) => row,
454            _ => panic!("BUG: fill walk expected a row output"),
455        }
456    }
457
458    fn into_members(self) -> FilledMembers {
459        match self {
460            Output::Members(members) => members,
461            _ => panic!("BUG: fill walk expected a members output"),
462        }
463    }
464}
465
466fn child_output(input: Option<Output>) -> Output {
467    input.expect("BUG: fill walk frame resumed without child output")
468}
469
470enum FillState {
471    Start(ValueKind),
472    AwaitSpec,
473    /// The table row a value publishes under, labelled with the value's instance.
474    AwaitRow(Option<String>),
475    Items(
476        std::vec::IntoIter<(ValueKind, RuleResultValue)>,
477        Vec<RuleResultValue>,
478    ),
479}
480
481/// Fills one value: a planned instance gets its data and rules inline, any other row a
482/// reference into the table, a list fills each entry in order.
483struct FillFrame {
484    value: RuleResultValue,
485    state: Option<FillState>,
486}
487
488impl FillFrame {
489    fn new(literal: ValueKind, value: RuleResultValue) -> Self {
490        Self {
491            value,
492            state: Some(FillState::Start(literal)),
493        }
494    }
495
496    fn resume<'a>(&mut self, walk: &mut Walk<'a, '_>, input: Option<Output>) -> Step<'a> {
497        match self
498            .state
499            .take()
500            .expect("BUG: fill frame resumed after completion")
501        {
502            FillState::Start(literal) => {
503                assert!(
504                    input.is_none(),
505                    "BUG: fill frame started with a child output"
506                );
507                self.start(walk, literal)
508            }
509            FillState::AwaitSpec => {
510                self.value.spec = Some(Box::new(child_output(input).into_spec()));
511                self.finish()
512            }
513            FillState::AwaitRow(instance) => {
514                let mut reference = child_output(input).into_row();
515                reference.instance = instance;
516                self.value.row = Some(reference);
517                self.finish()
518            }
519            FillState::Items(items, mut filled) => {
520                filled.push(child_output(input).into_value());
521                self.next_item(items, filled)
522            }
523        }
524    }
525
526    fn start<'a>(&mut self, walk: &mut Walk<'a, '_>, literal: ValueKind) -> Step<'a> {
527        match literal {
528            ValueKind::Spec(SpecValue::Row(key)) => {
529                if key.is_planned() {
530                    assert!(
531                        self.value.spec.is_some(),
532                        "BUG: planned instance '{key}' without spec arm"
533                    );
534                    if walk.ancestors.contains(&key) {
535                        panic!("BUG: spec result walked instance '{key}' twice");
536                    }
537                    self.state = Some(FillState::AwaitSpec);
538                    return Step::Call(Frame::Instance(InstanceFrame::new(key, Publication::Spec)));
539                }
540                assert!(self.value.row.is_none(), "BUG: row '{key}' filled twice");
541                let (table, instance) = key.table_row();
542                let table = table.clone();
543                self.state = Some(FillState::AwaitRow(instance));
544                Step::Call(Frame::Instance(InstanceFrame::new(
545                    table,
546                    Publication::Table,
547                )))
548            }
549            ValueKind::List(rows) => {
550                let entries = self
551                    .value
552                    .list
553                    .take()
554                    .expect("BUG: list value without list arm");
555                assert_eq!(
556                    entries.len(),
557                    rows.len(),
558                    "BUG: list arm row count differs from the list value"
559                );
560                let filled = Vec::with_capacity(entries.len());
561                let items: Vec<(ValueKind, RuleResultValue)> = rows
562                    .iter()
563                    .map(|row| row.value.clone())
564                    .zip(entries)
565                    .collect();
566                self.next_item(items.into_iter(), filled)
567            }
568            ValueKind::Number(_)
569            | ValueKind::Measure(_)
570            | ValueKind::UnscaledMeasure { .. }
571            | ValueKind::Ratio(_)
572            | ValueKind::Text(_)
573            | ValueKind::Date(_)
574            | ValueKind::Time(_)
575            | ValueKind::Boolean(_)
576            | ValueKind::Range(_, _) => self.finish(),
577            ValueKind::Spec(SpecValue::Fixed(_)) => {
578                unreachable!("BUG: evaluation produces only row spec values")
579            }
580        }
581    }
582
583    fn next_item<'a>(
584        &mut self,
585        mut items: std::vec::IntoIter<(ValueKind, RuleResultValue)>,
586        filled: Vec<RuleResultValue>,
587    ) -> Step<'a> {
588        match items.next() {
589            Some((literal, entry)) => {
590                self.state = Some(FillState::Items(items, filled));
591                Step::Call(Frame::fill(literal, entry))
592            }
593            None => {
594                self.value.list = Some(filled);
595                self.finish()
596            }
597        }
598    }
599
600    fn finish<'a>(&mut self) -> Step<'a> {
601        Step::Return(Output::Value(Box::new(std::mem::take(&mut self.value))))
602    }
603}
604
605/// Where a reached instance publishes.
606#[derive(Clone, Copy)]
607enum Publication {
608    /// A reference into the response rows table: rows supplied by evaluation data or
609    /// appended by a rule, and anything under them.
610    Table,
611    /// Inline as the `spec` arm: `uses` imports and rows written in a spec, at any depth.
612    Spec,
613    /// Only its rules, handed to the walk of the instance that holds it as `uses` slot.
614    Rules,
615}
616
617/// Members of instance `key`, filled in its own context over its template plan. A
618/// table row is filled once and referenced by index everywhere else.
619struct InstanceFrame {
620    key: InstanceKey,
621    publication: Publication,
622    index: usize,
623}
624
625impl InstanceFrame {
626    fn new(key: InstanceKey, publication: Publication) -> Self {
627        Self {
628            key,
629            publication,
630            index: 0,
631        }
632    }
633
634    fn reference(&self, index: usize) -> RowReference {
635        RowReference {
636            repository: self.key.row.identity.repository.clone(),
637            spec: self.key.row.identity.spec.clone(),
638            effective: self.key.row.identity.effective.to_string(),
639            instance: self.key.path(),
640            index,
641        }
642    }
643
644    fn resume<'a>(&mut self, walk: &mut Walk<'a, '_>, input: Option<Output>) -> Step<'a> {
645        match input {
646            None => {
647                match self.publication {
648                    Publication::Table => {
649                        if let Some(index) = walk.table.index.get(&self.key).copied() {
650                            return Step::Return(Output::Row(self.reference(index)));
651                        }
652                        let index = walk.table.rows.len();
653                        walk.table.index.insert(self.key.clone(), index);
654                        walk.table.rows.push(None);
655                        self.index = index;
656                    }
657                    Publication::Spec => walk.ancestors.push(self.key.clone()),
658                    Publication::Rules => {}
659                }
660                let template = walk.root.row_template(&self.key.row.identity);
661                let (owner_plan, owner_ctx) = walk.owner_scope(&self.key);
662                let ctx = take_row_context(&self.key, owner_plan, template, owner_ctx);
663                walk.scopes.push(Scope {
664                    plan: template,
665                    ctx,
666                });
667                Step::Call(Frame::Members(Box::new(MembersFrame::new(
668                    template,
669                    self.key.clone(),
670                    matches!(self.publication, Publication::Rules),
671                ))))
672            }
673            Some(output) => {
674                let (data, rules) = output.into_members();
675                let scope = walk
676                    .scopes
677                    .pop()
678                    .expect("BUG: instance scope ended before it was entered");
679                walk.ctx()
680                    .caches
681                    .row_contexts
682                    .borrow_mut()
683                    .insert(self.key.clone(), scope.ctx);
684                match self.publication {
685                    Publication::Table => {
686                        walk.table.rows[self.index] = Some(RowResult {
687                            repository: self.key.row.identity.repository.clone(),
688                            spec: self.key.row.identity.spec.clone(),
689                            effective: self.key.row.identity.effective.to_string(),
690                            data,
691                            rules,
692                        });
693                        Step::Return(Output::Row(self.reference(self.index)))
694                    }
695                    Publication::Spec => {
696                        let popped = walk.ancestors.pop();
697                        assert!(
698                            popped.as_ref() == Some(&self.key),
699                            "BUG: spec result of '{}' ended on another instance",
700                            self.key
701                        );
702                        let path = self.key.segments().unwrap_or_else(|| {
703                            panic!("BUG: planned instance '{}' has no hops", self.key)
704                        });
705                        Step::Return(Output::Spec(Box::new(SpecResult {
706                            repository: self.key.row.identity.repository.clone(),
707                            spec: self.key.row.identity.spec.clone(),
708                            effective: self.key.row.identity.effective.to_string(),
709                            instance: self.key.to_string(),
710                            path: path.iter().map(crate::api::PathSegment::from).collect(),
711                            data,
712                            rules,
713                        })))
714                    }
715                    Publication::Rules => Step::Return(Output::Members((data, rules))),
716                }
717            }
718        }
719    }
720}
721
722/// An entry whose value is being filled, with the key it is inserted under once filled,
723/// or the `uses` slot whose rules are being filled.
724enum Awaiting {
725    Data(String, SpecRuleResult),
726    Rule(String, SpecRuleResult),
727    Import(String),
728}
729
730/// Data then rules of instance `row` over its template `plan`, then the rules of every
731/// `uses` instance under it, keyed by their alias hops (`sub.total`). Missing data is
732/// reported under the instance's path.
733struct MembersFrame<'a> {
734    plan: &'a ExecutionPlan,
735    row: InstanceKey,
736    data_keys: std::vec::IntoIter<(&'a DataPath, &'a DataDefinition)>,
737    rule_paths: std::vec::IntoIter<&'a RulePath>,
738    imports: std::vec::IntoIter<&'a DataPath>,
739    data: IndexMap<String, SpecRuleResult>,
740    rules: IndexMap<String, SpecRuleResult>,
741    awaiting: Option<Awaiting>,
742}
743
744impl<'a> MembersFrame<'a> {
745    fn new(plan: &'a ExecutionPlan, row: InstanceKey, rules_only: bool) -> Self {
746        let data_keys: Vec<(&'a DataPath, &'a DataDefinition)> = if rules_only {
747            Vec::new()
748        } else {
749            plan.input_slots().collect()
750        };
751        let imports: Vec<&'a DataPath> = plan
752            .data
753            .iter()
754            .filter(|(_, definition)| matches!(definition, DataDefinition::Import { .. }))
755            .map(|(path, _)| path)
756            .collect();
757        Self {
758            plan,
759            row,
760            data_keys: data_keys.into_iter(),
761            rule_paths: plan.rules.keys().collect::<Vec<_>>().into_iter(),
762            imports: imports.into_iter(),
763            data: IndexMap::new(),
764            rules: IndexMap::new(),
765            awaiting: None,
766        }
767    }
768
769    fn resume(&mut self, walk: &mut Walk<'a, '_>, input: Option<Output>) -> Step<'a> {
770        match (self.awaiting.take(), input) {
771            (None, None) => {}
772            (Some(Awaiting::Data(key, mut entry)), Some(output)) => {
773                entry.result = Some(output.into_value());
774                self.data.insert(key, entry);
775            }
776            (Some(Awaiting::Rule(key, mut entry)), Some(output)) => {
777                entry.result = Some(output.into_value());
778                self.rules.insert(key, entry);
779            }
780            (Some(Awaiting::Import(alias)), Some(output)) => {
781                let (data, rules) = output.into_members();
782                assert!(
783                    data.is_empty(),
784                    "BUG: rules of `uses {alias}` were filled with data"
785                );
786                for (key, entry) in rules {
787                    self.rules.insert(format!("{alias}.{key}"), entry);
788                }
789            }
790            _ => panic!("BUG: members frame resumed with an unexpected child output"),
791        }
792        while let Some((path, definition)) = self.data_keys.next() {
793            if let Some(step) = self.data_item(walk, path, definition) {
794                return step;
795            }
796        }
797        while let Some(path) = self.rule_paths.next() {
798            if let Some(step) = self.rule_item(walk, path) {
799                return step;
800            }
801        }
802        if let Some(slot) = self.imports.next() {
803            let nested = InstanceKey::new(
804                Arc::clone(self.plan.import_row(Some(&self.row.row), slot, None)),
805                Some(self.row.clone()),
806            );
807            self.awaiting = Some(Awaiting::Import(slot.input_key()));
808            return Step::Call(Frame::Instance(InstanceFrame::new(
809                nested,
810                Publication::Rules,
811            )));
812        }
813        Step::Return(Output::Members((
814            std::mem::take(&mut self.data),
815            std::mem::take(&mut self.rules),
816        )))
817    }
818
819    fn data_item(
820        &mut self,
821        walk: &mut Walk<'a, '_>,
822        path: &'a DataPath,
823        definition: &'a DataDefinition,
824    ) -> Option<Step<'a>> {
825        let plan = self.plan;
826        definition.schema_type()?;
827        let key = path.input_key();
828        let slot = match walk.ctx().data_slot(plan, path).cloned() {
829            Some(slot) => slot,
830            None => match plan.reference_ends.get(path) {
831                Some(ReferenceEnd::Rule(rule)) => {
832                    ensure_rule_values(plan.rule_index(rule), plan, walk.ctx());
833                    walk.ctx().rule_value(plan, rule).clone()
834                }
835                Some(ReferenceEnd::Promptable(_)) | None => return None,
836            },
837        };
838        if matches!(slot, OperationResult::Veto(VetoType::MissingData { .. })) {
839            return None;
840        }
841        let display_type =
842            crate::evaluation::EvaluationContext::data_display_type(plan, path, &slot);
843        let mut entry = spec_entry(
844            &slot,
845            key.clone(),
846            RulePath {
847                segments: path.segments.clone(),
848                rule: path.data.clone(),
849            },
850            definition.source().clone(),
851            display_type.as_ref(),
852            &plan.family_units,
853            Vec::new(),
854        );
855        let OperationResult::Value(bound) = slot else {
856            self.data.insert(key, entry);
857            return None;
858        };
859        let Some(value) = entry.result.take() else {
860            self.data.insert(key, entry);
861            return None;
862        };
863        self.awaiting = Some(Awaiting::Data(key, entry));
864        Some(Step::Call(Frame::fill(bound.value, value)))
865    }
866
867    fn rule_item(&mut self, walk: &mut Walk<'a, '_>, path: &'a RulePath) -> Option<Step<'a>> {
868        let plan = self.plan;
869        let exec = plan
870            .rules
871            .get(path)
872            .expect("BUG: rule path missing from plan");
873        ensure_rule_values(plan.rule_index(path), plan, walk.ctx());
874        let local = walk.ctx().rule_value(plan, path).clone();
875        let missing_data = if local.is_missing_data() {
876            walk.ctx()
877                .missing_data_for_rule(plan, exec.normal_form, Some(&self.row))
878        } else {
879            Vec::new()
880        };
881        let operation = row_result(local, &self.row, plan);
882        let key = path.input_key();
883        let mut entry = spec_entry(
884            &operation,
885            key.clone(),
886            path.clone(),
887            exec.source.clone(),
888            exec.rule_type.as_ref(),
889            &plan.family_units,
890            missing_data,
891        );
892        match operation {
893            OperationResult::Value(bound) => match entry.result.take() {
894                Some(value) => {
895                    self.awaiting = Some(Awaiting::Rule(key, entry));
896                    Some(Step::Call(Frame::fill(bound.value, value)))
897                }
898                None => {
899                    self.rules.insert(key, entry);
900                    None
901                }
902            },
903            OperationResult::Veto(_) => {
904                self.rules.insert(key, entry);
905                None
906            }
907        }
908    }
909}
910
911type FilledMembers = (
912    IndexMap<String, SpecRuleResult>,
913    IndexMap<String, SpecRuleResult>,
914);
915
916fn spec_entry(
917    operation: &OperationResult,
918    name: String,
919    path: RulePath,
920    source: Source,
921    display_type: &LemmaType,
922    family_units: &FamilyUnitCatalog,
923    missing_data: Vec<String>,
924) -> SpecRuleResult {
925    let built = RuleResult::from_operation_result(
926        EvaluatedRule {
927            name,
928            path,
929            source_location: source,
930            rule_type: Arc::new(display_type.clone()),
931        },
932        operation,
933        display_type,
934        family_units,
935        None,
936        missing_data,
937    );
938    SpecRuleResult {
939        vetoed: built.vetoed,
940        veto_reason: built.veto_reason.clone(),
941        rule_type: built.rule_type.clone(),
942        result: built.result.clone(),
943    }
944}
945
946#[cfg(test)]
947mod tests {
948    use super::*;
949    use crate::literals::DateGranularity;
950    use crate::parsing::ast::Span;
951    use crate::planning::semantics::{
952        primitive_number_arc, BaseMeasureVector, DataPath, LemmaType, LiteralValue, MeasureUnit,
953        MeasureUnits, RatioUnit, RatioUnits, RulePath, TypeExtends, TypeSpecification, ValueKind,
954    };
955    use crate::planning::unit_family::FamilyUnitCatalog;
956    use crate::planning::unit_index::{UnitIndex, UnitOwner};
957    use rust_decimal::Decimal;
958    use std::str::FromStr;
959    use std::sync::Arc;
960
961    fn dec(s: &str) -> Decimal {
962        Decimal::from_str(s).expect("BUG: test decimal literal")
963    }
964
965    fn empty_family_catalog() -> FamilyUnitCatalog {
966        FamilyUnitCatalog::default()
967    }
968
969    fn family_catalog_for_index(index: &UnitIndex) -> FamilyUnitCatalog {
970        FamilyUnitCatalog::build(index)
971    }
972
973    fn family_catalog_for_money(money: &LemmaType) -> FamilyUnitCatalog {
974        family_catalog_for_index(&unit_index_for_money(money))
975    }
976
977    fn family_catalog_for_ratio(ratio_type: &LemmaType) -> FamilyUnitCatalog {
978        family_catalog_for_index(&unit_index_for_ratio(ratio_type))
979    }
980
981    fn unit_index_for_money(money: &LemmaType) -> UnitIndex {
982        let mut index = UnitIndex::new();
983        let arc = Arc::new(money.clone());
984        for unit_name in money.measure_unit_names().into_iter().flatten() {
985            index.insert_owner(
986                unit_name.to_string(),
987                UnitOwner::Named {
988                    import_path: Vec::new(),
989                    type_name: "money".into(),
990                    lemma_type: Arc::clone(&arc),
991                },
992            );
993        }
994        index
995    }
996
997    fn unit_index_for_ratio(ratio_type: &LemmaType) -> UnitIndex {
998        let mut index = UnitIndex::new();
999        let arc = Arc::new(ratio_type.clone());
1000        let type_name = ratio_type.name();
1001        for unit_name in ratio_type.ratio_unit_names().into_iter().flatten() {
1002            index.insert_owner(
1003                unit_name.to_string(),
1004                UnitOwner::Named {
1005                    import_path: Vec::new(),
1006                    type_name: type_name.clone(),
1007                    lemma_type: Arc::clone(&arc),
1008                },
1009            );
1010        }
1011        index
1012    }
1013
1014    fn dummy_source() -> Source {
1015        Source::new(
1016            crate::parsing::source::SourceType::Volatile,
1017            Span {
1018                start: 0,
1019                end: 0,
1020                line: 1,
1021                col: 1,
1022            },
1023        )
1024    }
1025
1026    fn dummy_evaluated_rule(name: &str, rule_type: &LemmaType) -> EvaluatedRule {
1027        EvaluatedRule {
1028            name: name.to_string(),
1029            path: RulePath::new(vec![], name.to_string()),
1030            source_location: dummy_source(),
1031            rule_type: Arc::new(rule_type.clone()),
1032        }
1033    }
1034
1035    #[test]
1036    fn test_response_serialization() {
1037        let mut results = IndexMap::new();
1038        results.insert(
1039            "test_rule".to_string(),
1040            RuleResult::from_operation_result(
1041                dummy_evaluated_rule("test_rule", primitive_number_arc().as_ref()),
1042                &OperationResult::from_literal(LiteralValue::number_from_decimal(Decimal::from(
1043                    42,
1044                ))),
1045                primitive_number_arc().as_ref(),
1046                &empty_family_catalog(),
1047                None,
1048                Vec::new(),
1049            ),
1050        );
1051        let response = Response {
1052            spec_name: "test_spec".to_string(),
1053            effective: "2026-01-01".to_string(),
1054            spec_effective_from: None,
1055            spec_effective_to: None,
1056            results,
1057            rows: Vec::new(),
1058        };
1059
1060        let json = serde_json::to_string(&crate::api::Response::from(&response)).unwrap();
1061        assert!(json.contains("test_spec"));
1062        assert!(json.contains("test_rule"));
1063        assert!(json.contains("\"number\":\"42\""));
1064        assert!(!json.contains("lemma_type"));
1065    }
1066
1067    #[test]
1068    fn response_number_json_never_uses_fraction_notation() {
1069        use crate::computation::rational::decimal_to_rational;
1070
1071        let rational = decimal_to_rational(Decimal::new(1, 1) / Decimal::new(3, 1)).unwrap();
1072        let decimal_string = rational.try_to_decimal().unwrap().to_string();
1073        let mut results = IndexMap::new();
1074        results.insert(
1075            "third".to_string(),
1076            RuleResult::from_operation_result(
1077                dummy_evaluated_rule("third", primitive_number_arc().as_ref()),
1078                &OperationResult::from_literal(LiteralValue::number_from_decimal(
1079                    rational.try_to_decimal().unwrap(),
1080                )),
1081                primitive_number_arc().as_ref(),
1082                &empty_family_catalog(),
1083                None,
1084                Vec::new(),
1085            ),
1086        );
1087        // Override committed decimal number field to match serialization path under test
1088        if let Some(rule) = results.get_mut("third") {
1089            let decimal = rational.try_to_decimal().unwrap();
1090            rule.result = Some(crate::result_value::RuleResultValue {
1091                result: Some(decimal_string.clone()),
1092                number: Some(decimal),
1093                ..Default::default()
1094            });
1095        }
1096
1097        let response = Response {
1098            spec_name: "test".to_string(),
1099            effective: "test".to_string(),
1100            spec_effective_from: None,
1101            spec_effective_to: None,
1102            results,
1103            rows: Vec::new(),
1104        };
1105
1106        let json: serde_json::Value = serde_json::from_str(
1107            &serde_json::to_string(&crate::api::Response::from(&response)).unwrap(),
1108        )
1109        .unwrap();
1110        let number = json["results"]["third"]["number"]
1111            .as_str()
1112            .expect("number must be a JSON string");
1113        assert!(
1114            !number.contains('/'),
1115            "API decimal string must not use fraction notation, got {number}"
1116        );
1117    }
1118
1119    #[test]
1120    fn test_rule_result_veto() {
1121        let missing = RuleResult::from_operation_result(
1122            dummy_evaluated_rule("rule3", &LemmaType::veto_type()),
1123            &OperationResult::Veto(VetoType::missing_data(
1124                DataPath::new(vec![], "data1".to_string()),
1125                None,
1126            )),
1127            &LemmaType::veto_type(),
1128            &empty_family_catalog(),
1129            None,
1130            vec!["data1".to_string()],
1131        );
1132        assert!(missing.vetoed);
1133        assert!(missing.veto_reason.as_ref().unwrap().contains("data1"));
1134
1135        let veto = RuleResult::from_operation_result(
1136            dummy_evaluated_rule("rule4", &LemmaType::veto_type()),
1137            &OperationResult::Veto(VetoType::UserDefined {
1138                message: Some("Vetoed".to_string()),
1139            }),
1140            &LemmaType::veto_type(),
1141            &empty_family_catalog(),
1142            None,
1143            Vec::new(),
1144        );
1145        assert_eq!(veto.veto_reason.as_deref(), Some("Vetoed"));
1146    }
1147
1148    /// Attach hole: MissingData + empty missing_data is a BUG, not a silent stall.
1149    #[test]
1150    #[should_panic(expected = "BUG: MissingData")]
1151    fn missing_data_veto_must_not_attach_empty_missing_data_list() {
1152        RuleResult::from_operation_result(
1153            dummy_evaluated_rule("rule3", &LemmaType::veto_type()),
1154            &OperationResult::Veto(VetoType::missing_data(
1155                DataPath::new(vec![], "data1".to_string()),
1156                None,
1157            )),
1158            &LemmaType::veto_type(),
1159            &empty_family_catalog(),
1160            None,
1161            Vec::new(),
1162        );
1163    }
1164
1165    #[test]
1166    #[should_panic(expected = "BUG: MissingData")]
1167    fn missing_data_veto_must_include_veto_path_in_list() {
1168        RuleResult::from_operation_result(
1169            dummy_evaluated_rule("rule3", &LemmaType::veto_type()),
1170            &OperationResult::Veto(VetoType::missing_data(
1171                DataPath::new(vec![], "data1".to_string()),
1172                None,
1173            )),
1174            &LemmaType::veto_type(),
1175            &empty_family_catalog(),
1176            None,
1177            vec!["other".to_string()],
1178        );
1179    }
1180
1181    #[test]
1182    #[should_panic(expected = "BUG: missing_data must be empty")]
1183    fn non_missing_data_result_must_not_attach_leftover_missing_data() {
1184        RuleResult::from_operation_result(
1185            dummy_evaluated_rule("rule4", &LemmaType::veto_type()),
1186            &OperationResult::Veto(VetoType::UserDefined {
1187                message: Some("Vetoed".to_string()),
1188            }),
1189            &LemmaType::veto_type(),
1190            &empty_family_catalog(),
1191            None,
1192            vec!["leftover".to_string()],
1193        );
1194    }
1195
1196    #[test]
1197    fn rule_result_value_out_of_memory_is_not_decimal_limit_veto() {
1198        let result = vetoed_rule_result_for_rule_result_value_failure(
1199            dummy_evaluated_rule("rule", primitive_number_arc().as_ref()),
1200            primitive_number_arc().as_ref(),
1201            None,
1202            RuleResultValueFailure::OutOfMemory,
1203            Vec::new(),
1204        );
1205        assert_eq!(
1206            result.veto_reason.as_deref(),
1207            Some("The calculation ran out of memory.")
1208        );
1209        assert_ne!(
1210            result.veto_reason.as_deref(),
1211            Some("The calculated result exceeds the decimal value limit.")
1212        );
1213    }
1214
1215    #[test]
1216    fn rule_result_value_decimal_limit_uses_commit_message() {
1217        let result = vetoed_rule_result_for_rule_result_value_failure(
1218            dummy_evaluated_rule("rule", primitive_number_arc().as_ref()),
1219            primitive_number_arc().as_ref(),
1220            None,
1221            RuleResultValueFailure::DecimalLimit,
1222            Vec::new(),
1223        );
1224        assert_eq!(
1225            result.veto_reason.as_deref(),
1226            Some("The calculated result exceeds the decimal value limit.")
1227        );
1228    }
1229
1230    fn test_money_type() -> LemmaType {
1231        LemmaType::new(
1232            "money".to_string(),
1233            TypeSpecification::Measure {
1234                minimum: None,
1235                maximum: None,
1236                units: MeasureUnits::from(vec![
1237                    MeasureUnit {
1238                        name: "eur".to_string(),
1239                        scale: crate::literals::UnitScale::Literal(
1240                            crate::computation::rational::rational_one(),
1241                        ),
1242                        derived_measure_factors: Vec::new(),
1243                        decomposition: BaseMeasureVector::new(),
1244                        minimum: None,
1245                        maximum: None,
1246                        suggestion_magnitude: None,
1247                    },
1248                    MeasureUnit {
1249                        name: "usd".to_string(),
1250                        scale: crate::literals::UnitScale::Literal(
1251                            crate::computation::rational::decimal_to_rational(Decimal::new(91, 2))
1252                                .expect("factor"),
1253                        ),
1254                        derived_measure_factors: Vec::new(),
1255                        decomposition: BaseMeasureVector::new(),
1256                        minimum: None,
1257                        maximum: None,
1258                        suggestion_magnitude: None,
1259                    },
1260                ]),
1261                traits: Vec::new(),
1262                decomposition: Some(BaseMeasureVector::new()),
1263                unit: None,
1264                help: String::new(),
1265            },
1266            TypeExtends::Primitive,
1267        )
1268    }
1269
1270    #[test]
1271    fn measure_rule_result_value_uses_rule_type_when_expression_index_empty() {
1272        let money = test_money_type();
1273        let family_units = family_catalog_for_money(&money);
1274        let ten_usd = LiteralValue {
1275            value: ValueKind::Measure(
1276                crate::computation::rational::checked_mul(
1277                    &crate::computation::rational::decimal_to_rational(Decimal::from(10))
1278                        .expect("ten"),
1279                    &crate::computation::rational::decimal_to_rational(Decimal::new(91, 2))
1280                        .expect("usd factor"),
1281                )
1282                .expect("canonical usd"),
1283            ),
1284        };
1285        let result = RuleResult::from_operation_result(
1286            dummy_evaluated_rule("total", &money),
1287            &OperationResult::from_literal(ten_usd),
1288            &money,
1289            &family_units,
1290            None,
1291            Vec::new(),
1292        );
1293        let measure = result
1294            .result
1295            .as_ref()
1296            .expect("value")
1297            .measure
1298            .clone()
1299            .expect("measure map");
1300        assert_eq!(measure.get("usd"), Some(&dec("10.00")));
1301        assert!(measure.contains_key("eur"));
1302    }
1303
1304    #[test]
1305    fn test_measure_rule_result_value_multi_unit() {
1306        let money = test_money_type();
1307        let family_units = family_catalog_for_money(&money);
1308        let ten_eur = LiteralValue {
1309            value: ValueKind::Measure(
1310                crate::computation::rational::decimal_to_rational(Decimal::from(10)).expect("ten"),
1311            ),
1312        };
1313        let result = RuleResult::from_operation_result(
1314            dummy_evaluated_rule("total", &money),
1315            &OperationResult::from_literal(ten_eur),
1316            &money,
1317            &family_units,
1318            None,
1319            Vec::new(),
1320        );
1321        let measure = result
1322            .result
1323            .as_ref()
1324            .expect("value")
1325            .measure
1326            .clone()
1327            .expect("measure map");
1328        assert_eq!(measure.get("eur"), Some(&dec("10.00")));
1329        assert_eq!(
1330            measure.get("usd"),
1331            Some(&dec("10.98901098901098901098901098"))
1332        );
1333    }
1334
1335    #[test]
1336    fn measure_rule_result_value_keeps_unit_conversion_unrounded() {
1337        let money = LemmaType::new(
1338            "money".to_string(),
1339            TypeSpecification::Measure {
1340                minimum: None,
1341                maximum: None,
1342                units: MeasureUnits::from(vec![
1343                    MeasureUnit {
1344                        name: "eur".to_string(),
1345                        scale: crate::literals::UnitScale::Literal(
1346                            crate::computation::rational::rational_one(),
1347                        ),
1348                        derived_measure_factors: Vec::new(),
1349                        decomposition: BaseMeasureVector::new(),
1350                        minimum: None,
1351                        maximum: None,
1352                        suggestion_magnitude: None,
1353                    },
1354                    MeasureUnit {
1355                        name: "usd".to_string(),
1356                        scale: crate::literals::UnitScale::Literal(
1357                            crate::computation::rational::decimal_to_rational(Decimal::new(84, 2))
1358                                .expect("usd factor"),
1359                        ),
1360                        derived_measure_factors: Vec::new(),
1361                        decomposition: BaseMeasureVector::new(),
1362                        minimum: None,
1363                        maximum: None,
1364                        suggestion_magnitude: None,
1365                    },
1366                ]),
1367                traits: Vec::new(),
1368                decomposition: Some(BaseMeasureVector::new()),
1369                unit: None,
1370                help: String::new(),
1371            },
1372            TypeExtends::Primitive,
1373        );
1374        let family_units = family_catalog_for_money(&money);
1375        let three_twelve_eur = LiteralValue {
1376            value: ValueKind::Measure(
1377                crate::computation::rational::decimal_to_rational(Decimal::new(312, 2))
1378                    .expect("3.12 eur canonical"),
1379            ),
1380        };
1381        let result = RuleResult::from_operation_result(
1382            dummy_evaluated_rule("delivery_cost", &money),
1383            &OperationResult::from_literal(three_twelve_eur),
1384            &money,
1385            &family_units,
1386            None,
1387            Vec::new(),
1388        );
1389        let measure = result
1390            .result
1391            .as_ref()
1392            .expect("value")
1393            .measure
1394            .clone()
1395            .expect("measure map");
1396        assert_eq!(measure.get("eur"), Some(&dec("3.12")));
1397        assert_eq!(
1398            measure.get("usd"),
1399            Some(&dec("3.714285714285714285714285714"))
1400        );
1401    }
1402
1403    #[test]
1404    fn test_ratio_rule_result_value_multi_unit() {
1405        let ratio_type = LemmaType::new(
1406            "rate".to_string(),
1407            TypeSpecification::Ratio {
1408                minimum: None,
1409                maximum: None,
1410                units: RatioUnits::from(vec![
1411                    RatioUnit {
1412                        name: "percent".to_string(),
1413                        value: crate::computation::rational::decimal_to_rational(Decimal::from(
1414                            100,
1415                        ))
1416                        .expect("percent"),
1417                        minimum: None,
1418                        maximum: None,
1419                        suggestion_magnitude: None,
1420                    },
1421                    RatioUnit {
1422                        name: "basis_points".to_string(),
1423                        value: crate::computation::rational::decimal_to_rational(Decimal::from(
1424                            10_000,
1425                        ))
1426                        .expect("bp"),
1427                        minimum: None,
1428                        maximum: None,
1429                        suggestion_magnitude: None,
1430                    },
1431                ]),
1432                unit: None,
1433                help: String::new(),
1434            },
1435            TypeExtends::Primitive,
1436        );
1437        let family_units = family_catalog_for_ratio(&ratio_type);
1438        let half = crate::computation::rational::rational_new(1, 2);
1439        let lit = LiteralValue {
1440            value: ValueKind::Ratio(half),
1441        };
1442        let result = RuleResult::from_operation_result(
1443            dummy_evaluated_rule("rate_out", &ratio_type),
1444            &OperationResult::from_literal(lit),
1445            &ratio_type,
1446            &family_units,
1447            None,
1448            Vec::new(),
1449        );
1450        let ratio = result
1451            .result
1452            .as_ref()
1453            .expect("value")
1454            .ratio
1455            .clone()
1456            .expect("ratio map");
1457        assert_eq!(ratio.get("percent"), Some(&dec("50")));
1458        assert_eq!(ratio.get("basis_points"), Some(&dec("5000")));
1459    }
1460
1461    #[test]
1462    fn test_measure_rule_result_value_cross_spec_import() {
1463        use crate::parsing::source::SourceType;
1464        use crate::Engine;
1465
1466        let mut engine = Engine::new();
1467        engine
1468            .load([(
1469                SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("t.lemma"))),
1470                r#"
1471spec consumer 2025-01-01
1472uses d: dep 2025-10-01
1473rule out: d.doubled
1474
1475spec dep 2025-01-01
1476uses c: child 2025-06-01
1477data money: c.money
1478data p: 5 usd
1479rule doubled: p * 2
1480
1481spec child 2025-01-01
1482data money: measure
1483 -> unit eur: 1.00
1484
1485spec child 2025-06-01
1486data money: measure
1487 -> unit eur: 1.00
1488 -> unit usd: 0.91
1489"#
1490                .to_string(),
1491            )])
1492            .expect("load");
1493        let effective = crate::literals::DateTimeValue {
1494            year: 2025,
1495            month: 3,
1496            day: 1,
1497            hour: 0,
1498            minute: 0,
1499            second: 0,
1500            microsecond: 0,
1501            timezone: None,
1502
1503            granularity: DateGranularity::Full,
1504        };
1505        let response = engine
1506            .evaluate(
1507                None,
1508                "consumer",
1509                Some(&effective),
1510                IndexMap::new(),
1511                None,
1512                false,
1513            )
1514            .expect("evaluate");
1515        let out = response.results.get("out").expect("out rule");
1516        assert!(!out.vetoed);
1517        let measure = out
1518            .result
1519            .as_ref()
1520            .expect("value")
1521            .measure
1522            .as_ref()
1523            .expect("measure map");
1524        assert!(measure.contains_key("usd"));
1525        assert!(measure.contains_key("eur"));
1526    }
1527
1528    #[test]
1529    fn to_literal_roundtrips_number() {
1530        let literal = LiteralValue::number_from_decimal(Decimal::from(42));
1531        let rule_result = RuleResult::from_operation_result(
1532            dummy_evaluated_rule("answer", primitive_number_arc().as_ref()),
1533            &OperationResult::from_literal(literal.clone()),
1534            primitive_number_arc().as_ref(),
1535            &empty_family_catalog(),
1536            None,
1537            Vec::new(),
1538        );
1539        assert_eq!(rule_result.to_literal(), literal);
1540    }
1541
1542    #[test]
1543    fn to_literal_roundtrips_measure() {
1544        let money = test_money_type();
1545        let family_units = family_catalog_for_money(&money);
1546        let literal = LiteralValue::measure(crate::computation::rational::rational_new(60, 1));
1547        let rule_result = RuleResult::from_operation_result(
1548            dummy_evaluated_rule("pay", &money),
1549            &OperationResult::from_literal(literal.clone()),
1550            &money,
1551            &family_units,
1552            None,
1553            Vec::new(),
1554        );
1555        assert_eq!(rule_result.to_literal(), literal);
1556    }
1557}