Skip to main content

lemma/planning/
graph.rs

1use crate::engine::Context;
2use crate::parsing::ast::{
3    self as ast, Constraint, EffectiveDate, LemmaData, LemmaRule, LemmaSpec, MetaValue, ParentType,
4    Value,
5};
6use crate::parsing::source::Source;
7use crate::planning::discovery;
8use crate::planning::semantics::{
9    self, conversion_target_to_semantic, primitive_boolean, primitive_date, primitive_duration,
10    primitive_number, primitive_ratio, primitive_text, primitive_time, value_to_semantic,
11    ArithmeticComputation, ComparisonComputation, DataDefinition, DataPath, Expression,
12    ExpressionKind, LemmaType, LiteralValue, PathSegment, ReferenceTarget, RulePath,
13    SemanticConversionTarget, TypeDefiningSpec, TypeExtends, TypeSpecification, ValueKind,
14};
15use crate::Error;
16use ast::DataValue as ParsedDataValue;
17use indexmap::IndexMap;
18use rust_decimal::Decimal;
19use std::cmp::Ordering;
20use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
21use std::fmt;
22use std::sync::Arc;
23
24/// Data bindings map: maps a target data name path to the binding's value and source.
25///
26/// The key is the full path of **data names** from the root spec to the target data.
27/// Spec names are intentionally excluded from the key because spec ref bindings may change
28/// which spec a segment points to — matching by data names only ensures bindings
29/// are applied correctly regardless of spec ref bindings.
30///
31/// Example: `data employee.salary: 7500` in the root spec produces key `["employee", "salary"]`.
32type DataBindings = HashMap<Vec<String>, (BindingValue, Source)>;
33
34/// Binding value stored in [`DataBindings`]. Only two forms are valid for a
35/// cross-spec binding: a literal value, or a reference to another data or rule.
36///
37/// References on the binding's right-hand side (e.g. `data license.other: law.other`)
38/// are resolved at binding collection time against the spec in which the binding
39/// itself was written (not the nested target spec). The resolved [`ReferenceTarget`]
40/// is carried through so the nested spec's planning does not need the outer
41/// spec's scope to interpret the reference.
42#[derive(Debug, Clone)]
43pub(crate) enum BindingValue {
44    /// Literal RHS (parsed as a `Value`). Applied as a plain value to the bound data.
45    Literal(ast::Value),
46    /// Reference RHS pre-resolved to a concrete reference target.
47    Reference {
48        target: ReferenceTarget,
49        constraints: Option<Vec<Constraint>>,
50    },
51}
52
53#[derive(Debug)]
54pub(crate) struct Graph {
55    /// Root spec being planned (for error spec_context).
56    main_spec: Arc<LemmaSpec>,
57    data: IndexMap<DataPath, DataDefinition>,
58    rules: BTreeMap<RulePath, RuleNode>,
59    execution_order: Vec<RulePath>,
60    /// Order in which references must be resolved so each reference's target
61    /// (when it too is a reference) is already computed. References targeting
62    /// non-reference data have no ordering constraints amongst themselves and
63    /// appear in the order they are discovered.
64    reference_evaluation_order: Vec<DataPath>,
65}
66
67impl Graph {
68    pub(crate) fn data(&self) -> &IndexMap<DataPath, DataDefinition> {
69        &self.data
70    }
71
72    pub(crate) fn rules(&self) -> &BTreeMap<RulePath, RuleNode> {
73        &self.rules
74    }
75
76    pub(crate) fn rules_mut(&mut self) -> &mut BTreeMap<RulePath, RuleNode> {
77        &mut self.rules
78    }
79
80    pub(crate) fn execution_order(&self) -> &[RulePath] {
81        &self.execution_order
82    }
83
84    pub(crate) fn reference_evaluation_order(&self) -> &[DataPath] {
85        &self.reference_evaluation_order
86    }
87
88    pub(crate) fn main_spec(&self) -> &Arc<LemmaSpec> {
89        &self.main_spec
90    }
91
92    /// Build the data map: one entry per data (Value or SpecRef), with defaults and coercion applied.
93    /// Preserves definition order from the source spec.
94    pub(crate) fn build_data(&self) -> IndexMap<DataPath, DataDefinition> {
95        struct PendingReference {
96            target: ReferenceTarget,
97            resolved_type: LemmaType,
98            local_constraints: Option<Vec<Constraint>>,
99            local_default: Option<ValueKind>,
100        }
101
102        let mut schema: HashMap<DataPath, LemmaType> = HashMap::new();
103        let mut declared_defaults: HashMap<DataPath, ValueKind> = HashMap::new();
104        let mut values: HashMap<DataPath, LiteralValue> = HashMap::new();
105        let mut spec_arcs: HashMap<DataPath, Arc<LemmaSpec>> = HashMap::new();
106        let mut references: HashMap<DataPath, PendingReference> = HashMap::new();
107
108        for (path, rfv) in self.data.iter() {
109            match rfv {
110                DataDefinition::Value { value, .. } => {
111                    values.insert(path.clone(), value.clone());
112                    schema.insert(path.clone(), value.lemma_type.clone());
113                }
114                DataDefinition::TypeDeclaration {
115                    resolved_type,
116                    declared_default,
117                    ..
118                } => {
119                    schema.insert(path.clone(), resolved_type.clone());
120                    if let Some(dv) = declared_default {
121                        declared_defaults.insert(path.clone(), dv.clone());
122                    }
123                }
124                DataDefinition::SpecRef { spec: spec_arc, .. } => {
125                    spec_arcs.insert(path.clone(), Arc::clone(spec_arc));
126                }
127                DataDefinition::Reference {
128                    target,
129                    resolved_type,
130                    local_constraints,
131                    local_default,
132                    ..
133                } => {
134                    schema.insert(path.clone(), resolved_type.clone());
135                    references.insert(
136                        path.clone(),
137                        PendingReference {
138                            target: target.clone(),
139                            resolved_type: resolved_type.clone(),
140                            local_constraints: local_constraints.clone(),
141                            local_default: local_default.clone(),
142                        },
143                    );
144                }
145            }
146        }
147
148        for (path, schema_type) in &schema {
149            if values.contains_key(path) {
150                continue;
151            }
152            if references.contains_key(path) {
153                continue;
154            }
155            if let Some(declared) = declared_defaults.get(path) {
156                values.insert(
157                    path.clone(),
158                    LiteralValue {
159                        value: declared.clone(),
160                        lemma_type: schema_type.clone(),
161                    },
162                );
163            }
164        }
165
166        for (path, value) in values.iter_mut() {
167            let Some(schema_type) = schema.get(path).cloned() else {
168                continue;
169            };
170            match Self::coerce_literal_to_schema_type(value, &schema_type) {
171                Ok(coerced) => *value = coerced,
172                Err(msg) => unreachable!("Data {} incompatible: {}", path, msg),
173            }
174        }
175
176        let mut data = IndexMap::new();
177        for (path, rfv) in &self.data {
178            let source = rfv.source().clone();
179            if let Some(spec_arc) = spec_arcs.remove(path) {
180                data.insert(
181                    path.clone(),
182                    DataDefinition::SpecRef {
183                        spec: spec_arc,
184                        source,
185                    },
186                );
187            } else if let Some(pending) = references.remove(path) {
188                data.insert(
189                    path.clone(),
190                    DataDefinition::Reference {
191                        target: pending.target,
192                        resolved_type: pending.resolved_type,
193                        local_constraints: pending.local_constraints,
194                        local_default: pending.local_default,
195                        source,
196                    },
197                );
198            } else if let Some(value) = values.remove(path) {
199                data.insert(path.clone(), DataDefinition::Value { value, source });
200            } else {
201                let resolved_type = schema
202                    .get(path)
203                    .cloned()
204                    .expect("non-spec-ref data has schema (value, reference, or type-only)");
205                let declared_default = declared_defaults.remove(path);
206                data.insert(
207                    path.clone(),
208                    DataDefinition::TypeDeclaration {
209                        resolved_type,
210                        declared_default,
211                        source,
212                    },
213                );
214            }
215        }
216        data
217    }
218
219    fn coerce_literal_to_schema_type(
220        lit: &LiteralValue,
221        schema_type: &LemmaType,
222    ) -> Result<LiteralValue, String> {
223        if lit.lemma_type.specifications == schema_type.specifications {
224            let mut out = lit.clone();
225            out.lemma_type = schema_type.clone();
226            return Ok(out);
227        }
228        match (&schema_type.specifications, &lit.value) {
229            (TypeSpecification::Number { .. }, ValueKind::Number(_))
230            | (TypeSpecification::Text { .. }, ValueKind::Text(_))
231            | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
232            | (TypeSpecification::Date { .. }, ValueKind::Date(_))
233            | (TypeSpecification::Time { .. }, ValueKind::Time(_))
234            | (TypeSpecification::Duration { .. }, ValueKind::Duration(_, _))
235            | (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_, _))
236            | (TypeSpecification::Scale { .. }, ValueKind::Scale(_, _)) => {
237                let mut out = lit.clone();
238                out.lemma_type = schema_type.clone();
239                Ok(out)
240            }
241            (TypeSpecification::Ratio { .. }, ValueKind::Number(n)) => {
242                Ok(LiteralValue::ratio_with_type(*n, None, schema_type.clone()))
243            }
244            _ => Err(format!(
245                "value {} cannot be used as type {}",
246                lit,
247                schema_type.name()
248            )),
249        }
250    }
251
252    /// Resolve each data-target [`DataDefinition::Reference`]'s provisional
253    /// `resolved_type` into its final merged form by combining:
254    ///   1. the target data's declared schema type,
255    ///   2. any local `-> ...` constraints attached to the reference itself,
256    ///   3. the LHS-declared type of the referencing data (when present; only
257    ///      possible in a binding whose bound data has its own type
258    ///      declaration in the nested spec).
259    ///
260    /// Rule-target references are skipped here — they are resolved later in
261    /// [`Self::resolve_rule_reference_types`] using the inferred rule
262    /// type, which is only available after [`infer_rule_types`] has run.
263    fn resolve_data_reference_types(&mut self) -> Result<(), Vec<Error>> {
264        let mut errors: Vec<Error> = Vec::new();
265        let mut updates: Vec<(DataPath, LemmaType, Option<ValueKind>)> = Vec::new();
266
267        for (reference_path, entry) in &self.data {
268            let DataDefinition::Reference {
269                target,
270                resolved_type: provisional,
271                local_constraints,
272                source,
273                ..
274            } = entry
275            else {
276                continue;
277            };
278
279            let target_data_path = match target {
280                ReferenceTarget::Data(path) => path,
281                ReferenceTarget::Rule(_) => continue,
282            };
283
284            let Some(target_entry) = self.data.get(target_data_path) else {
285                errors.push(reference_error(
286                    &self.main_spec,
287                    source,
288                    format!(
289                        "Data reference '{}' target '{}' does not exist",
290                        reference_path, target_data_path
291                    ),
292                ));
293                continue;
294            };
295
296            let Some(target_type) = target_entry.schema_type().cloned() else {
297                errors.push(reference_error(
298                    &self.main_spec,
299                    source,
300                    format!(
301                        "Data reference '{}' target '{}' is a spec reference and cannot carry a value",
302                        reference_path, target_data_path
303                    ),
304                ));
305                continue;
306            };
307
308            let lhs_declared_type: Option<&LemmaType> = if provisional.is_undetermined() {
309                None
310            } else {
311                Some(provisional)
312            };
313
314            if let Some(lhs) = lhs_declared_type {
315                if let Some(msg) = reference_kind_mismatch_message(
316                    lhs,
317                    &target_type,
318                    reference_path,
319                    target_data_path,
320                    "target",
321                ) {
322                    errors.push(reference_error(&self.main_spec, source, msg));
323                    continue;
324                }
325            }
326
327            // Merge: prefer LHS-declared spec when present so child-declared
328            // constraints (e.g. `maximum 5` from a binding's parent type
329            // chain) are enforced on the copied value at run time. Without
330            // a LHS-declared type, fall back to the target's spec.
331            let mut merged = match lhs_declared_type {
332                Some(lhs) => lhs.clone(),
333                None => target_type.clone(),
334            };
335            let mut captured_default: Option<ValueKind> = None;
336            if let Some(constraints) = local_constraints {
337                match apply_constraints_to_spec(
338                    &self.main_spec,
339                    merged.specifications.clone(),
340                    constraints,
341                    source,
342                    &mut captured_default,
343                ) {
344                    Ok(specs) => merged.specifications = specs,
345                    Err(errs) => {
346                        errors.extend(errs);
347                        continue;
348                    }
349                }
350            }
351
352            updates.push((reference_path.clone(), merged, captured_default));
353        }
354
355        for (path, new_type, new_default) in updates {
356            if let Some(DataDefinition::Reference {
357                resolved_type,
358                local_default,
359                ..
360            }) = self.data.get_mut(&path)
361            {
362                *resolved_type = new_type;
363                if new_default.is_some() {
364                    *local_default = new_default;
365                }
366            } else {
367                unreachable!("BUG: reference path disappeared between collect and update phases");
368            }
369        }
370
371        if errors.is_empty() {
372            Ok(())
373        } else {
374            Err(errors)
375        }
376    }
377
378    /// Resolve each rule-target [`DataDefinition::Reference`]'s `resolved_type`
379    /// from the inferred type of the target rule. Applies the same LHS-vs-target
380    /// kind compatibility check and local `-> ...` constraint merge that
381    /// [`Self::resolve_data_reference_types`] applies to data-target references.
382    ///
383    /// Must run AFTER [`infer_rule_types`] so each target rule's inferred type
384    /// is available, and BEFORE [`check_rule_types`] so consumers see the
385    /// merged reference type during validation.
386    fn resolve_rule_reference_types(
387        &mut self,
388        computed_rule_types: &HashMap<RulePath, LemmaType>,
389    ) -> Result<(), Vec<Error>> {
390        let mut errors: Vec<Error> = Vec::new();
391        let mut updates: Vec<(DataPath, LemmaType, Option<ValueKind>)> = Vec::new();
392
393        for (reference_path, entry) in &self.data {
394            let DataDefinition::Reference {
395                target,
396                resolved_type: provisional,
397                local_constraints,
398                source,
399                ..
400            } = entry
401            else {
402                continue;
403            };
404
405            let target_rule_path = match target {
406                ReferenceTarget::Rule(path) => path,
407                ReferenceTarget::Data(_) => continue,
408            };
409
410            let Some(target_type) = computed_rule_types.get(target_rule_path) else {
411                errors.push(reference_error(
412                    &self.main_spec,
413                    source,
414                    format!(
415                        "Data reference '{}' target rule '{}' does not exist",
416                        reference_path, target_rule_path
417                    ),
418                ));
419                continue;
420            };
421
422            // A target rule whose inferred type is `veto` carries no concrete
423            // schema kind, so a LHS declared type cannot be checked against
424            // it at planning time. The runtime veto propagation in the
425            // evaluator will surface the rule's veto reason directly.
426            if target_type.vetoed() || target_type.is_undetermined() {
427                let mut merged = target_type.clone();
428                let mut captured_default: Option<ValueKind> = None;
429                if let Some(constraints) = local_constraints {
430                    match apply_constraints_to_spec(
431                        &self.main_spec,
432                        merged.specifications.clone(),
433                        constraints,
434                        source,
435                        &mut captured_default,
436                    ) {
437                        Ok(specs) => merged.specifications = specs,
438                        Err(errs) => {
439                            errors.extend(errs);
440                            continue;
441                        }
442                    }
443                }
444                updates.push((reference_path.clone(), merged, captured_default));
445                continue;
446            }
447
448            let lhs_declared_type: Option<&LemmaType> = if provisional.is_undetermined() {
449                None
450            } else {
451                Some(provisional)
452            };
453
454            if let Some(lhs) = lhs_declared_type {
455                if let Some(msg) = reference_kind_mismatch_message(
456                    lhs,
457                    target_type,
458                    reference_path,
459                    target_rule_path,
460                    "target rule",
461                ) {
462                    errors.push(reference_error(&self.main_spec, source, msg));
463                    continue;
464                }
465            }
466
467            // Prefer LHS-declared spec when present (see data-target merge
468            // for rationale).
469            let mut merged = match lhs_declared_type {
470                Some(lhs) => lhs.clone(),
471                None => target_type.clone(),
472            };
473            let mut captured_default: Option<ValueKind> = None;
474            if let Some(constraints) = local_constraints {
475                match apply_constraints_to_spec(
476                    &self.main_spec,
477                    merged.specifications.clone(),
478                    constraints,
479                    source,
480                    &mut captured_default,
481                ) {
482                    Ok(specs) => merged.specifications = specs,
483                    Err(errs) => {
484                        errors.extend(errs);
485                        continue;
486                    }
487                }
488            }
489
490            updates.push((reference_path.clone(), merged, captured_default));
491        }
492
493        for (path, new_type, new_default) in updates {
494            if let Some(DataDefinition::Reference {
495                resolved_type,
496                local_default,
497                ..
498            }) = self.data.get_mut(&path)
499            {
500                *resolved_type = new_type;
501                if new_default.is_some() {
502                    *local_default = new_default;
503                }
504            } else {
505                unreachable!(
506                    "BUG: rule-target reference path disappeared between collect and update phases"
507                );
508            }
509        }
510
511        if errors.is_empty() {
512            Ok(())
513        } else {
514            Err(errors)
515        }
516    }
517
518    /// Add a `depends_on_rules` edge from every rule that reads a rule-target
519    /// reference data path to the reference's target rule. This ensures the
520    /// target rule is evaluated before the consumer (so the lazy reference
521    /// resolver in the evaluator finds the result), and lets the topological
522    /// sort detect cycles that flow through reference paths.
523    ///
524    /// Walks data-target reference chains so that a path `y: m.x` where
525    /// `m.x: r` is a rule-target reference, still adds a dep edge from any
526    /// consumer of `y` to `r`.
527    fn add_rule_reference_dependency_edges(&mut self) {
528        let reference_to_rule: HashMap<DataPath, RulePath> =
529            self.transitive_reference_to_rule_map();
530
531        if reference_to_rule.is_empty() {
532            return;
533        }
534
535        let mut updates: Vec<(RulePath, RulePath)> = Vec::new();
536        for (rule_path, rule_node) in &self.rules {
537            let mut found: BTreeSet<RulePath> = BTreeSet::new();
538            for (cond, result) in &rule_node.branches {
539                if let Some(c) = cond {
540                    collect_rule_reference_dependencies(c, &reference_to_rule, &mut found);
541                }
542                collect_rule_reference_dependencies(result, &reference_to_rule, &mut found);
543            }
544            for target in found {
545                updates.push((rule_path.clone(), target));
546            }
547        }
548
549        for (rule_path, target) in updates {
550            if let Some(node) = self.rules.get_mut(&rule_path) {
551                node.depends_on_rules.insert(target);
552            }
553        }
554    }
555
556    /// For each [`DataDefinition::Reference`] in `self.data`, follow the
557    /// `Reference::Data` chain and record the eventual `Reference::Rule`
558    /// target (if any). Includes direct rule-target references. Cycles
559    /// among data-target references are not possible here because
560    /// `compute_reference_evaluation_order` already rejected them; we still
561    /// guard with a visited set as defense-in-depth.
562    fn transitive_reference_to_rule_map(&self) -> HashMap<DataPath, RulePath> {
563        let mut out: HashMap<DataPath, RulePath> = HashMap::new();
564        for (path, def) in &self.data {
565            if !matches!(def, DataDefinition::Reference { .. }) {
566                continue;
567            }
568            let mut visited: HashSet<DataPath> = HashSet::new();
569            let mut cursor: DataPath = path.clone();
570            loop {
571                if !visited.insert(cursor.clone()) {
572                    break;
573                }
574                let Some(DataDefinition::Reference { target, .. }) = self.data.get(&cursor) else {
575                    break;
576                };
577                match target {
578                    ReferenceTarget::Data(next) => cursor = next.clone(),
579                    ReferenceTarget::Rule(rule_path) => {
580                        out.insert(path.clone(), rule_path.clone());
581                        break;
582                    }
583                }
584            }
585        }
586        out
587    }
588
589    /// Compute an order in which data-target references can be evaluated at
590    /// runtime so each reference's target (when itself a reference) has been
591    /// evaluated first. Rule-target references are intentionally excluded —
592    /// they are resolved lazily on first read in the evaluator from the
593    /// already-evaluated target rule's result. Cycles among data-target
594    /// references are reported as planning errors.
595    fn compute_reference_evaluation_order(&self) -> Result<Vec<DataPath>, Vec<Error>> {
596        let reference_paths: Vec<DataPath> = self
597            .data
598            .iter()
599            .filter_map(|(p, d)| match d {
600                DataDefinition::Reference {
601                    target: ReferenceTarget::Data(_),
602                    ..
603                } => Some(p.clone()),
604                _ => None,
605            })
606            .collect();
607
608        if reference_paths.is_empty() {
609            return Ok(Vec::new());
610        }
611
612        let reference_set: BTreeSet<DataPath> = reference_paths.iter().cloned().collect();
613        let mut in_degree: BTreeMap<DataPath, usize> = BTreeMap::new();
614        let mut dependents: BTreeMap<DataPath, Vec<DataPath>> = BTreeMap::new();
615        for p in &reference_paths {
616            in_degree.insert(p.clone(), 0);
617            dependents.insert(p.clone(), Vec::new());
618        }
619
620        for p in &reference_paths {
621            let Some(DataDefinition::Reference { target, .. }) = self.data.get(p) else {
622                unreachable!("BUG: reference entry lost between collect and walk");
623            };
624            if let ReferenceTarget::Data(target_path) = target {
625                if reference_set.contains(target_path) {
626                    *in_degree
627                        .get_mut(p)
628                        .expect("BUG: reference missing in_degree") += 1;
629                    dependents
630                        .get_mut(target_path)
631                        .expect("BUG: reference missing dependents list")
632                        .push(p.clone());
633                }
634            }
635        }
636
637        let mut queue: VecDeque<DataPath> = in_degree
638            .iter()
639            .filter(|(_, d)| **d == 0)
640            .map(|(p, _)| p.clone())
641            .collect();
642
643        let mut result: Vec<DataPath> = Vec::new();
644        while let Some(path) = queue.pop_front() {
645            result.push(path.clone());
646            if let Some(deps) = dependents.get(&path) {
647                for dependent in deps.clone() {
648                    let degree = in_degree
649                        .get_mut(&dependent)
650                        .expect("BUG: reference dependent missing in_degree");
651                    *degree -= 1;
652                    if *degree == 0 {
653                        queue.push_back(dependent);
654                    }
655                }
656            }
657        }
658
659        if result.len() != reference_paths.len() {
660            let cycle_members: Vec<DataPath> = reference_paths
661                .iter()
662                .filter(|p| !result.contains(p))
663                .cloned()
664                .collect();
665            let cycle_display: String = cycle_members
666                .iter()
667                .map(|p| p.to_string())
668                .collect::<Vec<_>>()
669                .join(", ");
670            let errors: Vec<Error> = cycle_members
671                .iter()
672                .filter_map(|p| {
673                    self.data.get(p).map(|entry| {
674                        reference_error(
675                            &self.main_spec,
676                            entry.source(),
677                            format!("Circular data reference ({})", cycle_display),
678                        )
679                    })
680                })
681                .collect();
682            return Err(errors);
683        }
684
685        Ok(result)
686    }
687
688    fn topological_sort(&self) -> Result<Vec<RulePath>, Vec<Error>> {
689        let mut in_degree: BTreeMap<RulePath, usize> = BTreeMap::new();
690        let mut dependents: BTreeMap<RulePath, Vec<RulePath>> = BTreeMap::new();
691        let mut queue = VecDeque::new();
692        let mut result = Vec::new();
693
694        for rule_path in self.rules.keys() {
695            in_degree.insert(rule_path.clone(), 0);
696            dependents.insert(rule_path.clone(), Vec::new());
697        }
698
699        for (rule_path, rule_node) in &self.rules {
700            for dependency in &rule_node.depends_on_rules {
701                if self.rules.contains_key(dependency) {
702                    if let Some(degree) = in_degree.get_mut(rule_path) {
703                        *degree += 1;
704                    }
705                    if let Some(deps) = dependents.get_mut(dependency) {
706                        deps.push(rule_path.clone());
707                    }
708                }
709            }
710        }
711
712        for (rule_path, degree) in &in_degree {
713            if *degree == 0 {
714                queue.push_back(rule_path.clone());
715            }
716        }
717
718        while let Some(rule_path) = queue.pop_front() {
719            result.push(rule_path.clone());
720
721            if let Some(dependent_rules) = dependents.get(&rule_path) {
722                for dependent in dependent_rules {
723                    if let Some(degree) = in_degree.get_mut(dependent) {
724                        *degree -= 1;
725                        if *degree == 0 {
726                            queue.push_back(dependent.clone());
727                        }
728                    }
729                }
730            }
731        }
732
733        if result.len() != self.rules.len() {
734            let missing: Vec<RulePath> = self
735                .rules
736                .keys()
737                .filter(|rule| !result.contains(rule))
738                .cloned()
739                .collect();
740            let cycle: Vec<Source> = missing
741                .iter()
742                .filter_map(|rule| self.rules.get(rule).map(|n| n.source.clone()))
743                .collect();
744
745            if cycle.is_empty() {
746                unreachable!(
747                    "BUG: circular dependency detected but no sources could be collected ({} missing rules)",
748                    missing.len()
749                );
750            }
751            let rules_involved: String = missing
752                .iter()
753                .map(|rp| rp.rule.as_str())
754                .collect::<Vec<_>>()
755                .join(", ");
756            let message = format!("Circular dependency (rules: {})", rules_involved);
757            let errors: Vec<Error> = cycle
758                .into_iter()
759                .map(|source| {
760                    Error::validation_with_context(
761                        message.clone(),
762                        Some(source),
763                        None::<String>,
764                        Some(Arc::clone(&self.main_spec)),
765                        None,
766                    )
767                })
768                .collect();
769            return Err(errors);
770        }
771
772        Ok(result)
773    }
774}
775
776#[derive(Debug)]
777pub(crate) struct RuleNode {
778    /// First branch has condition=None (default expression), subsequent branches are unless clauses.
779    /// Resolved expressions (Reference -> DataPath or RulePath).
780    pub branches: Vec<(Option<Expression>, Expression)>,
781    pub source: Source,
782
783    pub depends_on_rules: BTreeSet<RulePath>,
784
785    /// Computed type of this rule's result (populated during validation)
786    /// Every rule MUST have a type (Lemma is strictly typed)
787    pub rule_type: LemmaType,
788
789    /// Name of the spec this rule belongs to (for type resolution during validation)
790    pub spec_name: String,
791}
792
793type ResolvedTypesMap = HashMap<Arc<LemmaSpec>, ResolvedSpecTypes>;
794
795struct GraphBuilder<'a> {
796    data: IndexMap<DataPath, DataDefinition>,
797    rules: BTreeMap<RulePath, RuleNode>,
798    context: &'a Context,
799    local_types: ResolvedTypesMap,
800    errors: Vec<Error>,
801    main_spec: Arc<LemmaSpec>,
802}
803
804fn reference_error(main_spec: &Arc<LemmaSpec>, source: &Source, message: String) -> Error {
805    Error::validation_with_context(
806        message,
807        Some(source.clone()),
808        None::<String>,
809        Some(Arc::clone(main_spec)),
810        None,
811    )
812}
813
814/// Decide whether an LHS-declared reference type and the resolved target type
815/// share a compatible kind. Returns `None` when they do; returns `Some(msg)`
816/// describing the mismatch otherwise.
817///
818/// "Same kind" requires:
819/// 1. matching base type spec (number / scale / text / ratio / …) — see
820///    [`LemmaType::has_same_base_type`]; and
821/// 2. for scale types, matching scale family — see
822///    [`LemmaType::same_scale_family`]. Two scales in different families
823///    (e.g. `eur` vs `celsius`) share the `Scale` discriminant but are not
824///    interchangeable values; copying one into the other would silently
825///    propagate a wrong-domain quantity.
826///
827/// `target_kind_label` distinguishes the two callers ("target" for data
828/// references, "target rule" for rule references) so the message reads
829/// naturally.
830fn reference_kind_mismatch_message<P: fmt::Display>(
831    lhs: &LemmaType,
832    target_type: &LemmaType,
833    reference_path: &DataPath,
834    target_path: &P,
835    target_kind_label: &str,
836) -> Option<String> {
837    if !lhs.has_same_base_type(target_type) {
838        return Some(format!(
839            "Data reference '{}' type mismatch: declared as '{}' but {} '{}' is '{}'",
840            reference_path,
841            lhs.name(),
842            target_kind_label,
843            target_path,
844            target_type.name(),
845        ));
846    }
847    if lhs.is_scale() && !lhs.same_scale_family(target_type) {
848        let lhs_family = lhs.scale_family_name().expect(
849            "BUG: declared scale data must carry a family name; \
850             anonymous scale types only arise from runtime synthesis \
851             and never appear as a reference's LHS-declared type",
852        );
853        let target_family = target_type.scale_family_name().expect(
854            "BUG: declared scale data must carry a family name; \
855             anonymous scale types only arise from runtime synthesis \
856             and never appear as a reference target's schema type",
857        );
858        return Some(format!(
859            "Data reference '{}' scale family mismatch: declared as '{}' (family '{}') but {} '{}' is '{}' (family '{}')",
860            reference_path,
861            lhs.name(),
862            lhs_family,
863            target_kind_label,
864            target_path,
865            target_type.name(),
866            target_family,
867        ));
868    }
869    None
870}
871
872/// Fold a list of typedef-style constraints into a [`TypeSpecification`].
873/// Used for both the GraphBuilder's regular TypeDeclaration path and the
874/// post-build reference type-merging pass, so the underlying constraint
875/// application logic stays in one place.
876fn apply_constraints_to_spec(
877    spec: &Arc<LemmaSpec>,
878    mut specs: TypeSpecification,
879    constraints: &[Constraint],
880    source: &crate::Source,
881    declared_default: &mut Option<ValueKind>,
882) -> Result<TypeSpecification, Vec<Error>> {
883    let mut errors = Vec::new();
884    for (command, args) in constraints {
885        let specs_clone = specs.clone();
886        let mut default_before = declared_default.clone();
887        match specs.apply_constraint(*command, args, &mut default_before) {
888            Ok(updated_specs) => {
889                specs = updated_specs;
890                *declared_default = default_before;
891            }
892            Err(e) => {
893                errors.push(Error::validation_with_context(
894                    format!("Failed to apply constraint '{}': {}", command, e),
895                    Some(source.clone()),
896                    None::<String>,
897                    Some(Arc::clone(spec)),
898                    None,
899                ));
900                specs = specs_clone;
901            }
902        }
903    }
904    if !errors.is_empty() {
905        return Err(errors);
906    }
907    Ok(specs)
908}
909
910impl Graph {
911    /// Build the dependency graph for a single spec within a pre-resolved DAG slice.
912    pub(crate) fn build(
913        context: &Context,
914        main_spec: &Arc<LemmaSpec>,
915        dag: &[Arc<LemmaSpec>],
916        effective: &EffectiveDate,
917    ) -> Result<(Graph, ResolvedTypesMap), Vec<Error>> {
918        let mut type_resolver = TypeResolver::new(context, dag);
919
920        let mut type_errors: Vec<Error> = Vec::new();
921        for spec in dag {
922            type_errors.extend(type_resolver.register_all(spec));
923        }
924
925        let (data, rules, graph_errors, local_types) = {
926            let mut builder = GraphBuilder {
927                data: IndexMap::new(),
928                rules: BTreeMap::new(),
929                context,
930                local_types: HashMap::new(),
931                errors: Vec::new(),
932                main_spec: Arc::clone(main_spec),
933            };
934
935            builder.build_spec(
936                main_spec,
937                Vec::new(),
938                HashMap::new(),
939                effective,
940                &mut type_resolver,
941            )?;
942
943            (
944                builder.data,
945                builder.rules,
946                builder.errors,
947                builder.local_types,
948            )
949        };
950
951        let mut graph = Graph {
952            data,
953            rules,
954            execution_order: Vec::new(),
955            reference_evaluation_order: Vec::new(),
956            main_spec: Arc::clone(main_spec),
957        };
958
959        let validation_errors = match graph.validate(&local_types) {
960            Ok(()) => Vec::new(),
961            Err(errors) => errors,
962        };
963
964        let mut all_errors = type_errors;
965        all_errors.extend(graph_errors);
966        all_errors.extend(validation_errors);
967
968        if all_errors.is_empty() {
969            Ok((graph, local_types))
970        } else {
971            Err(all_errors)
972        }
973    }
974
975    fn validate(&mut self, resolved_types: &ResolvedTypesMap) -> Result<(), Vec<Error>> {
976        let mut errors = Vec::new();
977
978        // Structural checks (no type info needed)
979        if let Err(structural_errors) = check_all_rule_references_exist(self) {
980            errors.extend(structural_errors);
981        }
982        if let Err(collision_errors) = check_data_and_rule_name_collisions(self) {
983            errors.extend(collision_errors);
984        }
985
986        // Phase 1: Resolve data-target reference types now that all data
987        // definitions (across all specs) are populated. Rule-target references
988        // are resolved in Phase 4 once the target rule's type is inferred.
989        if let Err(reference_errors) = self.resolve_data_reference_types() {
990            errors.extend(reference_errors);
991        }
992
993        // Compute the data-target reference evaluation (copy) order. Rule-target
994        // references are resolved lazily at evaluation time — they do not
995        // participate in the prepop copy loop.
996        let reference_order = match self.compute_reference_evaluation_order() {
997            Ok(order) => order,
998            Err(circular_errors) => {
999                errors.extend(circular_errors);
1000                return Err(errors);
1001            }
1002        };
1003
1004        // Phase 2: Inject rule-rule dependency edges for rule-target references.
1005        // A rule R that reads a data path D where D is `Reference(target: rule T)`
1006        // must be evaluated AFTER T so the lazy resolver can read T's result.
1007        // This must happen before topological_sort so cycles through reference
1008        // paths are detected.
1009        self.add_rule_reference_dependency_edges();
1010
1011        let execution_order = match self.topological_sort() {
1012            Ok(order) => order,
1013            Err(circular_errors) => {
1014                errors.extend(circular_errors);
1015                return Err(errors);
1016            }
1017        };
1018
1019        // Continue to type inference and type checking even when structural
1020        // checks found errors.  This lets us report structural errors (e.g.
1021        // missing rule reference) alongside type errors (e.g. branch type
1022        // mismatch) in a single pass.
1023
1024        // Phase 3: Infer types (pure, no errors). Looks through rule-target
1025        // references by consulting `computed_rule_types` for the target rule.
1026        let inferred_types = infer_rule_types(self, &execution_order, resolved_types);
1027
1028        // Phase 4: Now that target rule types are known, materialize each
1029        // rule-target reference's `resolved_type` (LHS check + target type +
1030        // local constraints), so check_rule_types and downstream consumers
1031        // see a real type on the reference path.
1032        if let Err(rule_reference_errors) = self.resolve_rule_reference_types(&inferred_types) {
1033            errors.extend(rule_reference_errors);
1034        }
1035
1036        // Phase 5: Check types (pure, returns Result)
1037        if let Err(type_errors) =
1038            check_rule_types(self, &execution_order, &inferred_types, resolved_types)
1039        {
1040            errors.extend(type_errors);
1041        }
1042
1043        if !errors.is_empty() {
1044            return Err(errors);
1045        }
1046
1047        // Phase 6: Apply (only on full success)
1048        apply_inferred_types(self, inferred_types);
1049        self.execution_order = execution_order;
1050        self.reference_evaluation_order = reference_order;
1051        Ok(())
1052    }
1053}
1054
1055impl<'a> GraphBuilder<'a> {
1056    fn engine_error(&self, message: impl Into<String>, source: &Source) -> Error {
1057        Error::validation_with_context(
1058            message.into(),
1059            Some(source.clone()),
1060            None::<String>,
1061            Some(Arc::clone(&self.main_spec)),
1062            None,
1063        )
1064    }
1065
1066    fn process_meta_fields(&mut self, spec: &LemmaSpec) {
1067        let mut seen = HashSet::new();
1068        for field in &spec.meta_fields {
1069            // Validate built-in keys
1070            if field.key == "title" && !matches!(field.value, MetaValue::Literal(Value::Text(_))) {
1071                self.errors.push(self.engine_error(
1072                    "Meta 'title' must be a text literal",
1073                    &field.source_location,
1074                ));
1075            }
1076
1077            if !seen.insert(field.key.clone()) {
1078                self.errors.push(self.engine_error(
1079                    format!("Duplicate meta key '{}'", field.key),
1080                    &field.source_location,
1081                ));
1082            }
1083        }
1084    }
1085
1086    fn resolve_spec_ref(
1087        &self,
1088        spec_ref: &ast::SpecRef,
1089        effective: &EffectiveDate,
1090    ) -> Result<Arc<LemmaSpec>, Error> {
1091        discovery::resolve_spec_ref(
1092            self.context,
1093            spec_ref,
1094            effective,
1095            &self.main_spec.name,
1096            None,
1097            Some(Arc::clone(&self.main_spec)),
1098        )
1099    }
1100
1101    /// Validate a data binding path by walking through spec references, and
1102    /// convert the binding's right-hand side into a [`BindingValue`] that the
1103    /// nested spec can interpret without access to the outer spec.
1104    ///
1105    /// The binding key (full path as data names from root) uses data names only
1106    /// (no spec names) so that spec ref bindings don't cause mismatches.
1107    fn resolve_data_binding(
1108        &mut self,
1109        data: &LemmaData,
1110        current_segment_names: &[String],
1111        parent_spec: &Arc<LemmaSpec>,
1112        effective: &EffectiveDate,
1113    ) -> Option<(Vec<String>, BindingValue, Source)> {
1114        let binding_path_display = format!(
1115            "{}.{}",
1116            data.reference.segments.join("."),
1117            data.reference.name
1118        );
1119
1120        let mut walk_spec = Arc::clone(parent_spec);
1121
1122        for segment in &data.reference.segments {
1123            let Some(seg_data) = walk_spec
1124                .data
1125                .iter()
1126                .find(|f| f.reference.segments.is_empty() && f.reference.name == *segment)
1127            else {
1128                self.errors.push(self.engine_error(
1129                    format!(
1130                        "Data binding path '{}': data '{}' not found in spec '{}'",
1131                        binding_path_display, segment, walk_spec.name
1132                    ),
1133                    &data.source_location,
1134                ));
1135                return None;
1136            };
1137
1138            let spec_ref = match &seg_data.value {
1139                ParsedDataValue::SpecReference(sr) => sr,
1140                _ => {
1141                    self.errors.push(self.engine_error(
1142                        format!(
1143                            "Data binding path '{}': '{}' in spec '{}' is not a spec reference",
1144                            binding_path_display, segment, walk_spec.name
1145                        ),
1146                        &data.source_location,
1147                    ));
1148                    return None;
1149                }
1150            };
1151
1152            walk_spec = match self.resolve_spec_ref(spec_ref, effective) {
1153                Ok(arc) => arc,
1154                Err(e) => {
1155                    self.errors.push(e);
1156                    return None;
1157                }
1158            };
1159        }
1160
1161        // Build the binding key: current_segment_names ++ data.reference.segments ++ [data.reference.name]
1162        let mut binding_key: Vec<String> = current_segment_names.to_vec();
1163        binding_key.extend(data.reference.segments.iter().cloned());
1164        binding_key.push(data.reference.name.clone());
1165
1166        let binding_value = match &data.value {
1167            ParsedDataValue::Literal(v) => BindingValue::Literal(v.clone()),
1168            ParsedDataValue::Reference {
1169                target,
1170                constraints,
1171            } => {
1172                let resolved_target = self.resolve_reference_target_in_spec(
1173                    target,
1174                    &data.source_location,
1175                    parent_spec,
1176                    current_segment_names,
1177                    effective,
1178                )?;
1179                BindingValue::Reference {
1180                    target: resolved_target,
1181                    constraints: constraints.clone(),
1182                }
1183            }
1184            ParsedDataValue::TypeDeclaration { .. } | ParsedDataValue::SpecReference(_) => {
1185                unreachable!(
1186                    "BUG: build_data_bindings must reject TypeDeclaration/SpecReference bindings before calling resolve_data_binding"
1187                );
1188            }
1189        };
1190
1191        Some((binding_key, binding_value, data.source_location.clone()))
1192    }
1193
1194    /// Resolve a parsed [`ast::Reference`] appearing on the RHS of a `data x: ref`
1195    /// assignment against the scope of `containing_spec_arc`. Returns an
1196    /// [`ReferenceTarget`] pointing at a data path or rule path. Errors push into
1197    /// `self.errors`; this function returns `None` on failure (and does not
1198    /// return a proper `Result` because it mirrors `resolve_path_segments`'s
1199    /// side-effecting convention so the two can compose cleanly).
1200    fn resolve_reference_target_in_spec(
1201        &mut self,
1202        reference: &ast::Reference,
1203        reference_source: &Source,
1204        containing_spec_arc: &Arc<LemmaSpec>,
1205        containing_segments_names: &[String],
1206        effective: &EffectiveDate,
1207    ) -> Option<ReferenceTarget> {
1208        let containing_data_map: HashMap<String, LemmaData> = containing_spec_arc
1209            .data
1210            .iter()
1211            .filter(|d| d.reference.is_local())
1212            .map(|d| (d.reference.name.clone(), d.clone()))
1213            .collect();
1214
1215        let containing_rule_names: HashSet<&str> = containing_spec_arc
1216            .rules
1217            .iter()
1218            .map(|r| r.name.as_str())
1219            .collect();
1220
1221        let containing_segments: Vec<PathSegment> = containing_segments_names
1222            .iter()
1223            .map(|name| PathSegment {
1224                data: name.clone(),
1225                spec: containing_spec_arc.name.clone(),
1226            })
1227            .collect();
1228
1229        if reference.segments.is_empty() {
1230            let is_data = containing_data_map.contains_key(&reference.name);
1231            let is_rule = containing_rule_names.contains(reference.name.as_str());
1232            if is_data && is_rule {
1233                self.errors.push(self.engine_error(
1234                    format!(
1235                        "Reference target '{}' is ambiguous: both a data and a rule in spec '{}'",
1236                        reference.name, containing_spec_arc.name
1237                    ),
1238                    reference_source,
1239                ));
1240                return None;
1241            }
1242            if is_data {
1243                return Some(ReferenceTarget::Data(DataPath {
1244                    segments: containing_segments,
1245                    data: reference.name.clone(),
1246                }));
1247            }
1248            if is_rule {
1249                return Some(ReferenceTarget::Rule(RulePath {
1250                    segments: containing_segments,
1251                    rule: reference.name.clone(),
1252                }));
1253            }
1254            self.errors.push(self.engine_error(
1255                format!(
1256                    "Reference target '{}' not found in spec '{}'",
1257                    reference.name, containing_spec_arc.name
1258                ),
1259                reference_source,
1260            ));
1261            return None;
1262        }
1263
1264        let (resolved_segments, target_spec_arc) = self.resolve_path_segments(
1265            &reference.segments,
1266            reference_source,
1267            containing_data_map,
1268            containing_segments,
1269            effective,
1270        )?;
1271
1272        let target_data_names: HashSet<&str> = target_spec_arc
1273            .data
1274            .iter()
1275            .filter(|d| d.reference.is_local())
1276            .map(|d| d.reference.name.as_str())
1277            .collect();
1278        let target_rule_names: HashSet<&str> = target_spec_arc
1279            .rules
1280            .iter()
1281            .map(|r| r.name.as_str())
1282            .collect();
1283        let is_data = target_data_names.contains(reference.name.as_str());
1284        let is_rule = target_rule_names.contains(reference.name.as_str());
1285
1286        if is_data && is_rule {
1287            self.errors.push(self.engine_error(
1288                format!(
1289                    "Reference target '{}' is ambiguous: both a data and a rule in spec '{}'",
1290                    reference.name, target_spec_arc.name
1291                ),
1292                reference_source,
1293            ));
1294            return None;
1295        }
1296        if is_data {
1297            return Some(ReferenceTarget::Data(DataPath {
1298                segments: resolved_segments,
1299                data: reference.name.clone(),
1300            }));
1301        }
1302        if is_rule {
1303            return Some(ReferenceTarget::Rule(RulePath {
1304                segments: resolved_segments,
1305                rule: reference.name.clone(),
1306            }));
1307        }
1308
1309        self.errors.push(self.engine_error(
1310            format!(
1311                "Reference target '{}' not found in spec '{}'",
1312                reference.name, target_spec_arc.name
1313            ),
1314            reference_source,
1315        ));
1316        None
1317    }
1318
1319    /// Build the data bindings declared in a spec.
1320    ///
1321    /// For each cross-spec data (reference.segments is non-empty), validate the path
1322    /// and collect into a DataBindings map. Rejects TypeDeclaration binding values and
1323    /// duplicate bindings targeting the same path.
1324    fn build_data_bindings(
1325        &mut self,
1326        spec: &LemmaSpec,
1327        current_segment_names: &[String],
1328        spec_arc: &Arc<LemmaSpec>,
1329        effective: &EffectiveDate,
1330    ) -> Result<DataBindings, Vec<Error>> {
1331        let mut bindings: DataBindings = HashMap::new();
1332        let mut errors: Vec<Error> = Vec::new();
1333
1334        for data in &spec.data {
1335            if data.reference.segments.is_empty() {
1336                continue; // Local data are not bindings
1337            }
1338
1339            let binding_path_display = format!(
1340                "{}.{}",
1341                data.reference.segments.join("."),
1342                data.reference.name
1343            );
1344
1345            // Reject spec reference as binding value — spec injection is not supported
1346            if matches!(&data.value, ParsedDataValue::SpecReference { .. }) {
1347                errors.push(self.engine_error(
1348                    format!(
1349                        "Data binding '{}' cannot override a spec reference — only literal values can be bound to nested data",
1350                        binding_path_display
1351                    ),
1352                    &data.source_location,
1353                ));
1354                continue;
1355            }
1356
1357            // Reject TypeDeclaration as binding value
1358            if matches!(&data.value, ParsedDataValue::TypeDeclaration { .. }) {
1359                errors.push(self.engine_error(
1360                    format!(
1361                        "Data binding '{}' must provide a literal value, not a type declaration",
1362                        binding_path_display
1363                    ),
1364                    &data.source_location,
1365                ));
1366                continue;
1367            }
1368
1369            if let Some((binding_key, binding_value, source)) =
1370                self.resolve_data_binding(data, current_segment_names, spec_arc, effective)
1371            {
1372                if let Some((_, existing_source)) = bindings.get(&binding_key) {
1373                    errors.push(self.engine_error(
1374                        format!(
1375                            "Duplicate data binding for '{}' (previously bound at {}:{})",
1376                            binding_key.join("."),
1377                            existing_source.attribute,
1378                            existing_source.span.line
1379                        ),
1380                        &data.source_location,
1381                    ));
1382                } else {
1383                    bindings.insert(binding_key, (binding_value, source));
1384                }
1385            }
1386            // resolve_data_binding failures are pushed into self.errors already.
1387        }
1388
1389        if !errors.is_empty() {
1390            return Err(errors);
1391        }
1392
1393        Ok(bindings)
1394    }
1395
1396    /// Add a single local data to the graph.
1397    ///
1398    /// Determines the effective value by checking `data_bindings` for an entry at
1399    /// the data's path. If a binding exists, uses the bound value; otherwise uses
1400    /// the data's own value. Reports an error on duplicate data.
1401    #[allow(clippy::too_many_arguments)]
1402    fn add_data(
1403        &mut self,
1404        data: &LemmaData,
1405        current_segments: &[PathSegment],
1406        data_bindings: &DataBindings,
1407        current_spec_arc: &Arc<LemmaSpec>,
1408        used_binding_keys: &mut HashSet<Vec<String>>,
1409        effective: &EffectiveDate,
1410    ) {
1411        let data_path = DataPath {
1412            segments: current_segments.to_vec(),
1413            data: data.reference.name.clone(),
1414        };
1415
1416        // Check for duplicates
1417        if self.data.contains_key(&data_path) {
1418            self.errors.push(self.engine_error(
1419                format!("Duplicate data '{}'", data_path.data),
1420                &data.source_location,
1421            ));
1422            return;
1423        }
1424
1425        // Build the binding key for this data: segment data names + data name
1426        let binding_key: Vec<String> = current_segments
1427            .iter()
1428            .map(|s| s.data.clone())
1429            .chain(std::iter::once(data.reference.name.clone()))
1430            .collect();
1431
1432        // A binding (if any) overrides the data's own RHS. We track the binding
1433        // separately from the data's own value because `BindingValue` (resolved)
1434        // and `ParsedDataValue` (raw AST) are different types.
1435        let binding_override: Option<(BindingValue, Source)> =
1436            data_bindings.get(&binding_key).map(|(v, s)| {
1437                used_binding_keys.insert(binding_key.clone());
1438                (v.clone(), s.clone())
1439            });
1440
1441        let (original_schema_type, original_declared_default) =
1442            if matches!(&data.value, ParsedDataValue::TypeDeclaration { .. }) {
1443                let resolved = self
1444                    .local_types
1445                    .get(current_spec_arc)
1446                    .expect("BUG: no resolved types for spec during add_local_data");
1447                let lemma_type = resolved
1448                    .named_types
1449                    .get(&data.reference.name)
1450                    .expect("BUG: type not in named_types — TypeResolver should have registered it")
1451                    .clone();
1452                let declared = resolved
1453                    .declared_defaults
1454                    .get(&data.reference.name)
1455                    .cloned();
1456                (Some(lemma_type), declared)
1457            } else {
1458                (None, None)
1459            };
1460
1461        if let Some((binding_value, binding_source)) = binding_override {
1462            self.add_data_from_binding(
1463                data_path,
1464                binding_value,
1465                binding_source,
1466                original_schema_type,
1467                current_spec_arc,
1468            );
1469            return;
1470        }
1471
1472        let effective_source = data.source_location.clone();
1473
1474        match &data.value {
1475            ParsedDataValue::Literal(value) => {
1476                self.insert_literal_data(
1477                    data_path,
1478                    value,
1479                    original_schema_type,
1480                    effective_source,
1481                    current_spec_arc,
1482                );
1483            }
1484            ParsedDataValue::TypeDeclaration { .. } => {
1485                let resolved_type = original_schema_type.unwrap_or_else(|| {
1486                    unreachable!(
1487                        "BUG: TypeDeclaration effective value without original_schema_type"
1488                    )
1489                });
1490
1491                self.data.insert(
1492                    data_path,
1493                    DataDefinition::TypeDeclaration {
1494                        resolved_type,
1495                        declared_default: original_declared_default,
1496                        source: effective_source,
1497                    },
1498                );
1499            }
1500            ParsedDataValue::SpecReference(spec_ref) => {
1501                let effective_spec_arc = match self.resolve_spec_ref(spec_ref, effective) {
1502                    Ok(arc) => arc,
1503                    Err(e) => {
1504                        self.errors.push(e);
1505                        return;
1506                    }
1507                };
1508
1509                self.data.insert(
1510                    data_path,
1511                    DataDefinition::SpecRef {
1512                        spec: Arc::clone(&effective_spec_arc),
1513                        source: effective_source,
1514                    },
1515                );
1516            }
1517            ParsedDataValue::Reference {
1518                target,
1519                constraints,
1520            } => {
1521                let current_segment_names: Vec<String> =
1522                    current_segments.iter().map(|s| s.data.clone()).collect();
1523                let Some(resolved_target) = self.resolve_reference_target_in_spec(
1524                    target,
1525                    &effective_source,
1526                    current_spec_arc,
1527                    &current_segment_names,
1528                    effective,
1529                ) else {
1530                    return;
1531                };
1532                // Reference type is resolved in a later pass once all data+rule
1533                // types are known. Use LHS declared type if present, otherwise
1534                // a placeholder that must be filled before validation.
1535                let provisional_type = original_schema_type
1536                    .clone()
1537                    .unwrap_or_else(LemmaType::undetermined_type);
1538                self.data.insert(
1539                    data_path,
1540                    DataDefinition::Reference {
1541                        target: resolved_target,
1542                        resolved_type: provisional_type,
1543                        local_constraints: constraints.clone(),
1544                        local_default: None,
1545                        source: effective_source,
1546                    },
1547                );
1548            }
1549        }
1550    }
1551
1552    /// Inserts a literal-value data definition using the given literal.
1553    /// Shared between the literal path of `add_data` and the literal path of
1554    /// a binding-provided value (bindings can only be literals or references).
1555    fn insert_literal_data(
1556        &mut self,
1557        data_path: DataPath,
1558        value: &ast::Value,
1559        declared_schema_type: Option<LemmaType>,
1560        effective_source: Source,
1561        current_spec_arc: &Arc<LemmaSpec>,
1562    ) {
1563        let semantic_value = match value_to_semantic(value) {
1564            Ok(s) => s,
1565            Err(e) => {
1566                self.errors.push(self.engine_error(e, &effective_source));
1567                return;
1568            }
1569        };
1570        let inferred_type = match value {
1571            Value::Text(_) => primitive_text().clone(),
1572            Value::Number(_) => primitive_number().clone(),
1573            Value::Scale(_, unit) => {
1574                match self
1575                    .local_types
1576                    .get(current_spec_arc)
1577                    .and_then(|dt| dt.unit_index.get(unit))
1578                {
1579                    Some(lt) => lt.clone(),
1580                    None => {
1581                        self.errors.push(self.engine_error(
1582                            format!("Scale literal uses unknown unit '{}' for this spec", unit),
1583                            &effective_source,
1584                        ));
1585                        return;
1586                    }
1587                }
1588            }
1589            Value::Boolean(_) => primitive_boolean().clone(),
1590            Value::Date(_) => primitive_date().clone(),
1591            Value::Time(_) => primitive_time().clone(),
1592            Value::Duration(_, _) => primitive_duration().clone(),
1593            Value::Ratio(_, _) => primitive_ratio().clone(),
1594        };
1595        let schema_type = declared_schema_type.unwrap_or(inferred_type);
1596        let literal_value = LiteralValue {
1597            value: semantic_value,
1598            lemma_type: schema_type,
1599        };
1600        self.data.insert(
1601            data_path,
1602            DataDefinition::Value {
1603                value: literal_value,
1604                source: effective_source,
1605            },
1606        );
1607    }
1608
1609    /// Apply a binding override to insert the bound data's definition.
1610    /// Bindings are pre-resolved — literal values or reference targets.
1611    fn add_data_from_binding(
1612        &mut self,
1613        data_path: DataPath,
1614        binding_value: BindingValue,
1615        binding_source: Source,
1616        declared_schema_type: Option<LemmaType>,
1617        current_spec_arc: &Arc<LemmaSpec>,
1618    ) {
1619        match binding_value {
1620            BindingValue::Literal(value) => {
1621                self.insert_literal_data(
1622                    data_path,
1623                    &value,
1624                    declared_schema_type,
1625                    binding_source,
1626                    current_spec_arc,
1627                );
1628            }
1629            BindingValue::Reference {
1630                target,
1631                constraints,
1632            } => {
1633                let provisional_type =
1634                    declared_schema_type.unwrap_or_else(LemmaType::undetermined_type);
1635                self.data.insert(
1636                    data_path,
1637                    DataDefinition::Reference {
1638                        target,
1639                        resolved_type: provisional_type,
1640                        local_constraints: constraints,
1641                        local_default: None,
1642                        source: binding_source,
1643                    },
1644                );
1645            }
1646        }
1647    }
1648
1649    /// Returns (path_segments, last_resolved_spec_arc) on success.
1650    fn resolve_path_segments(
1651        &mut self,
1652        segments: &[String],
1653        reference_source: &Source,
1654        mut current_data_map: HashMap<String, LemmaData>,
1655        mut path_segments: Vec<PathSegment>,
1656        effective: &EffectiveDate,
1657    ) -> Option<(Vec<PathSegment>, Arc<LemmaSpec>)> {
1658        let mut last_arc: Option<Arc<LemmaSpec>> = None;
1659
1660        for segment in segments.iter() {
1661            let data_ref =
1662                match current_data_map.get(segment) {
1663                    Some(f) => f,
1664                    None => {
1665                        self.errors.push(self.engine_error(
1666                            format!("Data '{}' not found", segment),
1667                            reference_source,
1668                        ));
1669                        return None;
1670                    }
1671                };
1672
1673            if let ParsedDataValue::SpecReference(original_spec_ref) = &data_ref.value {
1674                let arc = match self.resolve_spec_ref(original_spec_ref, effective) {
1675                    Ok(a) => a,
1676                    Err(e) => {
1677                        self.errors.push(e);
1678                        return None;
1679                    }
1680                };
1681
1682                path_segments.push(PathSegment {
1683                    data: segment.clone(),
1684                    spec: arc.name.clone(),
1685                });
1686                current_data_map = arc
1687                    .data
1688                    .iter()
1689                    .map(|f| (f.reference.name.clone(), f.clone()))
1690                    .collect();
1691                last_arc = Some(arc);
1692            } else {
1693                self.errors.push(self.engine_error(
1694                    format!("Data '{}' is not a spec reference", segment),
1695                    reference_source,
1696                ));
1697                return None;
1698            }
1699        }
1700
1701        let final_arc = last_arc.unwrap_or_else(|| {
1702            unreachable!(
1703                "BUG: resolve_path_segments called with empty segments should not reach here"
1704            )
1705        });
1706        Some((path_segments, final_arc))
1707    }
1708
1709    fn build_spec(
1710        &mut self,
1711        spec_arc: &Arc<LemmaSpec>,
1712        current_segments: Vec<PathSegment>,
1713        data_bindings: DataBindings,
1714        effective: &EffectiveDate,
1715        type_resolver: &mut TypeResolver<'a>,
1716    ) -> Result<(), Vec<Error>> {
1717        let spec = spec_arc.as_ref();
1718
1719        if current_segments.is_empty() {
1720            self.process_meta_fields(spec);
1721        }
1722
1723        // Step 0: Cross-version self-reference check.
1724        // A spec must not reference any version of itself (same base name).
1725        for data in spec.data.iter() {
1726            if let ParsedDataValue::SpecReference(spec_ref) = &data.value {
1727                if spec_ref.name == spec.name {
1728                    self.errors.push(self.engine_error(
1729                        format!(
1730                            "spec '{}' cannot reference '{}' (same base name)",
1731                            spec.name, spec_ref
1732                        ),
1733                        &data.source_location,
1734                    ));
1735                }
1736            }
1737        }
1738        let current_segment_names: Vec<String> =
1739            current_segments.iter().map(|s| s.data.clone()).collect();
1740
1741        // Step 2: Build data bindings declared in this spec (for passing to referenced specs)
1742        let this_spec_bindings =
1743            match self.build_data_bindings(spec, &current_segment_names, spec_arc, effective) {
1744                Ok(bindings) => bindings,
1745                Err(errors) => {
1746                    self.errors.extend(errors);
1747                    HashMap::new()
1748                }
1749            };
1750
1751        // Build data_map for rule resolution and other lookups
1752        let data_map: HashMap<String, &LemmaData> = spec
1753            .data
1754            .iter()
1755            .map(|data| (data.reference.name.clone(), data))
1756            .collect();
1757
1758        if !self.local_types.contains_key(spec_arc) {
1759            match type_resolver.resolve_and_validate(spec_arc, effective) {
1760                Ok(resolved_types) => {
1761                    self.local_types
1762                        .insert(Arc::clone(spec_arc), resolved_types);
1763                }
1764                Err(es) => {
1765                    self.errors.extend(es);
1766                    return Ok(());
1767                }
1768            }
1769        }
1770
1771        for data in &spec.data {
1772            if let ParsedDataValue::TypeDeclaration {
1773                from: Some(from_ref),
1774                ..
1775            } = &data.value
1776            {
1777                match self.resolve_spec_ref(from_ref, effective) {
1778                    Ok(source_arc) => {
1779                        if let std::collections::hash_map::Entry::Vacant(e) =
1780                            self.local_types.entry(source_arc)
1781                        {
1782                            match type_resolver.resolve_and_validate(e.key(), effective) {
1783                                Ok(resolved_types) => {
1784                                    e.insert(resolved_types);
1785                                }
1786                                Err(es) => self.errors.extend(es),
1787                            }
1788                        }
1789                    }
1790                    Err(e) => self.errors.push(e),
1791                }
1792            }
1793        }
1794
1795        // Step 4: Add local data using caller's data_bindings
1796        let mut used_binding_keys: HashSet<Vec<String>> = HashSet::new();
1797        for data in &spec.data {
1798            if !data.reference.segments.is_empty() {
1799                continue; // Skip binding data (processed in step 2)
1800            }
1801            if let ParsedDataValue::SpecReference(spec_ref) = &data.value {
1802                if spec_ref.name == spec.name {
1803                    continue; // Self-reference — error already reported in step 0
1804                }
1805            }
1806            self.add_data(
1807                data,
1808                &current_segments,
1809                &data_bindings,
1810                spec_arc,
1811                &mut used_binding_keys,
1812                effective,
1813            );
1814        }
1815
1816        for data in &spec.data {
1817            if !data.reference.segments.is_empty() {
1818                continue;
1819            }
1820            if let ParsedDataValue::SpecReference(spec_ref) = &data.value {
1821                if spec_ref.name == spec.name {
1822                    continue; // Self-reference — error already reported in step 0
1823                }
1824                let nested_effective = spec_ref.at(effective);
1825                let nested_arc = match self.resolve_spec_ref(spec_ref, effective) {
1826                    Ok(arc) => arc,
1827                    Err(e) => {
1828                        self.errors.push(e);
1829                        continue;
1830                    }
1831                };
1832                let mut nested_segments = current_segments.clone();
1833                nested_segments.push(PathSegment {
1834                    data: data.reference.name.clone(),
1835                    spec: nested_arc.name.clone(),
1836                });
1837
1838                let nested_segment_names: Vec<String> =
1839                    nested_segments.iter().map(|s| s.data.clone()).collect();
1840                let mut combined_bindings = this_spec_bindings.clone();
1841                for (key, value_and_source) in &data_bindings {
1842                    if key.len() > nested_segment_names.len()
1843                        && key[..nested_segment_names.len()] == nested_segment_names[..]
1844                        && !combined_bindings.contains_key(key)
1845                    {
1846                        combined_bindings.insert(key.clone(), value_and_source.clone());
1847                    }
1848                }
1849
1850                if let Err(errs) = self.build_spec(
1851                    &nested_arc,
1852                    nested_segments,
1853                    combined_bindings,
1854                    &nested_effective,
1855                    type_resolver,
1856                ) {
1857                    self.errors.extend(errs);
1858                }
1859            }
1860        }
1861
1862        // Check for unused data bindings that targeted this spec's data
1863        // Only check bindings at exactly this depth (deeper bindings are passed through)
1864        let expected_key_len = current_segments.len() + 1;
1865        for (binding_key, (_, binding_source)) in &data_bindings {
1866            if binding_key.len() == expected_key_len
1867                && binding_key[..current_segments.len()]
1868                    .iter()
1869                    .zip(current_segments.iter())
1870                    .all(|(a, b)| a == &b.data)
1871                && !used_binding_keys.contains(binding_key)
1872            {
1873                self.errors.push(self.engine_error(
1874                    format!(
1875                        "Data binding targets a data that does not exist in the referenced spec: '{}'",
1876                        binding_key.join(".")
1877                    ),
1878                    binding_source,
1879                ));
1880            }
1881        }
1882
1883        let rule_names: HashSet<&str> = spec.rules.iter().map(|r| r.name.as_str()).collect();
1884        for rule in &spec.rules {
1885            self.add_rule(
1886                rule,
1887                spec_arc,
1888                &data_map,
1889                &current_segments,
1890                &rule_names,
1891                effective,
1892            );
1893        }
1894
1895        Ok(())
1896    }
1897
1898    fn add_rule(
1899        &mut self,
1900        rule: &LemmaRule,
1901        current_spec_arc: &Arc<LemmaSpec>,
1902        data_map: &HashMap<String, &LemmaData>,
1903        current_segments: &[PathSegment],
1904        rule_names: &HashSet<&str>,
1905        effective: &EffectiveDate,
1906    ) {
1907        let rule_path = RulePath {
1908            segments: current_segments.to_vec(),
1909            rule: rule.name.clone(),
1910        };
1911
1912        if self.rules.contains_key(&rule_path) {
1913            let rule_source = &rule.source_location;
1914            self.errors.push(
1915                self.engine_error(format!("Duplicate rule '{}'", rule_path.rule), rule_source),
1916            );
1917            return;
1918        }
1919
1920        let mut branches = Vec::new();
1921        let mut depends_on_rules = BTreeSet::new();
1922
1923        let converted_expression = match self.convert_expression_and_extract_dependencies(
1924            &rule.expression,
1925            current_spec_arc,
1926            data_map,
1927            current_segments,
1928            &mut depends_on_rules,
1929            rule_names,
1930            effective,
1931        ) {
1932            Some(expr) => expr,
1933            None => return,
1934        };
1935        branches.push((None, converted_expression));
1936
1937        for unless_clause in &rule.unless_clauses {
1938            let converted_condition = match self.convert_expression_and_extract_dependencies(
1939                &unless_clause.condition,
1940                current_spec_arc,
1941                data_map,
1942                current_segments,
1943                &mut depends_on_rules,
1944                rule_names,
1945                effective,
1946            ) {
1947                Some(expr) => expr,
1948                None => return,
1949            };
1950            let converted_result = match self.convert_expression_and_extract_dependencies(
1951                &unless_clause.result,
1952                current_spec_arc,
1953                data_map,
1954                current_segments,
1955                &mut depends_on_rules,
1956                rule_names,
1957                effective,
1958            ) {
1959                Some(expr) => expr,
1960                None => return,
1961            };
1962            branches.push((Some(converted_condition), converted_result));
1963        }
1964
1965        let rule_node = RuleNode {
1966            branches,
1967            source: rule.source_location.clone(),
1968            depends_on_rules,
1969            rule_type: LemmaType::veto_type(),
1970            spec_name: current_spec_arc.name.clone(),
1971        };
1972
1973        self.rules.insert(rule_path, rule_node);
1974    }
1975
1976    /// Converts left and right expressions and accumulates rule dependencies.
1977    #[allow(clippy::too_many_arguments)]
1978    fn convert_binary_operands(
1979        &mut self,
1980        left: &ast::Expression,
1981        right: &ast::Expression,
1982        current_spec_arc: &Arc<LemmaSpec>,
1983        data_map: &HashMap<String, &LemmaData>,
1984        current_segments: &[PathSegment],
1985        depends_on_rules: &mut BTreeSet<RulePath>,
1986        rule_names: &HashSet<&str>,
1987        effective: &EffectiveDate,
1988    ) -> Option<(Expression, Expression)> {
1989        let converted_left = self.convert_expression_and_extract_dependencies(
1990            left,
1991            current_spec_arc,
1992            data_map,
1993            current_segments,
1994            depends_on_rules,
1995            rule_names,
1996            effective,
1997        )?;
1998        let converted_right = self.convert_expression_and_extract_dependencies(
1999            right,
2000            current_spec_arc,
2001            data_map,
2002            current_segments,
2003            depends_on_rules,
2004            rule_names,
2005            effective,
2006        )?;
2007        Some((converted_left, converted_right))
2008    }
2009
2010    /// Converts an AST expression into a resolved expression and records any rule references.
2011    #[allow(clippy::too_many_arguments)]
2012    fn convert_expression_and_extract_dependencies(
2013        &mut self,
2014        expr: &ast::Expression,
2015        current_spec_arc: &Arc<LemmaSpec>,
2016        data_map: &HashMap<String, &LemmaData>,
2017        current_segments: &[PathSegment],
2018        depends_on_rules: &mut BTreeSet<RulePath>,
2019        rule_names: &HashSet<&str>,
2020        effective: &EffectiveDate,
2021    ) -> Option<Expression> {
2022        let expr_src = expr
2023            .source_location
2024            .as_ref()
2025            .expect("BUG: AST expression missing source location");
2026        match &expr.kind {
2027            ast::ExpressionKind::Reference(r) => {
2028                let expr_source = expr_src;
2029                let (segments, target_arc_opt) = if r.segments.is_empty() {
2030                    (current_segments.to_vec(), None)
2031                } else {
2032                    let data_map_owned: HashMap<String, LemmaData> = data_map
2033                        .iter()
2034                        .map(|(k, v)| (k.clone(), (*v).clone()))
2035                        .collect();
2036                    let (segs, arc) = self.resolve_path_segments(
2037                        &r.segments,
2038                        expr_source,
2039                        data_map_owned,
2040                        current_segments.to_vec(),
2041                        effective,
2042                    )?;
2043                    (segs, Some(arc))
2044                };
2045
2046                let (is_data, is_rule, target_spec_name_opt) = match &target_arc_opt {
2047                    None => {
2048                        let is_data = data_map.contains_key(&r.name);
2049                        let is_rule = rule_names.contains(r.name.as_str());
2050                        (is_data, is_rule, None)
2051                    }
2052                    Some(target_arc) => {
2053                        let target_spec = target_arc.as_ref();
2054                        let target_data_names: HashSet<&str> = target_spec
2055                            .data
2056                            .iter()
2057                            .filter(|f| f.reference.is_local())
2058                            .map(|f| f.reference.name.as_str())
2059                            .collect();
2060                        let target_rule_names: HashSet<&str> =
2061                            target_spec.rules.iter().map(|r| r.name.as_str()).collect();
2062                        let is_data = target_data_names.contains(r.name.as_str());
2063                        let is_rule = target_rule_names.contains(r.name.as_str());
2064                        (is_data, is_rule, Some(target_spec.name.as_str()))
2065                    }
2066                };
2067
2068                if is_data && is_rule {
2069                    self.errors.push(self.engine_error(
2070                        format!("'{}' is both a data and a rule", r.name),
2071                        expr_source,
2072                    ));
2073                    return None;
2074                }
2075                if is_data {
2076                    let data_path = DataPath {
2077                        segments,
2078                        data: r.name.clone(),
2079                    };
2080                    return Some(Expression {
2081                        kind: ExpressionKind::DataPath(data_path),
2082                        source_location: expr.source_location.clone(),
2083                    });
2084                }
2085                if is_rule {
2086                    let rule_path = RulePath {
2087                        segments,
2088                        rule: r.name.clone(),
2089                    };
2090                    depends_on_rules.insert(rule_path.clone());
2091                    return Some(Expression {
2092                        kind: ExpressionKind::RulePath(rule_path),
2093                        source_location: expr.source_location.clone(),
2094                    });
2095                }
2096                let msg = match target_spec_name_opt {
2097                    Some(s) => format!("Reference '{}' not found in spec '{}'", r.name, s),
2098                    None => format!("Reference '{}' not found", r.name),
2099                };
2100                self.errors.push(self.engine_error(msg, expr_source));
2101                None
2102            }
2103
2104            ast::ExpressionKind::LogicalAnd(left, right) => {
2105                let (l, r) = self.convert_binary_operands(
2106                    left,
2107                    right,
2108                    current_spec_arc,
2109                    data_map,
2110                    current_segments,
2111                    depends_on_rules,
2112                    rule_names,
2113                    effective,
2114                )?;
2115                Some(Expression {
2116                    kind: ExpressionKind::LogicalAnd(Arc::new(l), Arc::new(r)),
2117                    source_location: expr.source_location.clone(),
2118                })
2119            }
2120
2121            ast::ExpressionKind::Arithmetic(left, op, right) => {
2122                let (l, r) = self.convert_binary_operands(
2123                    left,
2124                    right,
2125                    current_spec_arc,
2126                    data_map,
2127                    current_segments,
2128                    depends_on_rules,
2129                    rule_names,
2130                    effective,
2131                )?;
2132                Some(Expression {
2133                    kind: ExpressionKind::Arithmetic(Arc::new(l), op.clone(), Arc::new(r)),
2134                    source_location: expr.source_location.clone(),
2135                })
2136            }
2137
2138            ast::ExpressionKind::Comparison(left, op, right) => {
2139                let (l, r) = self.convert_binary_operands(
2140                    left,
2141                    right,
2142                    current_spec_arc,
2143                    data_map,
2144                    current_segments,
2145                    depends_on_rules,
2146                    rule_names,
2147                    effective,
2148                )?;
2149                Some(Expression {
2150                    kind: ExpressionKind::Comparison(Arc::new(l), op.clone(), Arc::new(r)),
2151                    source_location: expr.source_location.clone(),
2152                })
2153            }
2154
2155            ast::ExpressionKind::UnitConversion(value, target) => {
2156                let converted_value = self.convert_expression_and_extract_dependencies(
2157                    value,
2158                    current_spec_arc,
2159                    data_map,
2160                    current_segments,
2161                    depends_on_rules,
2162                    rule_names,
2163                    effective,
2164                )?;
2165
2166                let resolved_spec_types = self.local_types.get(current_spec_arc);
2167                let unit_index = resolved_spec_types.map(|dt| &dt.unit_index);
2168                let semantic_target = match conversion_target_to_semantic(target, unit_index) {
2169                    Ok(t) => t,
2170                    Err(msg) => {
2171                        // When there is no unit index (e.g. primitive context), surface the
2172                        // conversion error without a "valid units" list.
2173                        let full_msg = unit_index
2174                            .map(|idx| {
2175                                let valid: Vec<&str> = idx.keys().map(String::as_str).collect();
2176                                format!("{} Valid units: {}", msg, valid.join(", "))
2177                            })
2178                            .unwrap_or(msg);
2179                        self.errors.push(Error::validation_with_context(
2180                            full_msg,
2181                            expr.source_location.clone(),
2182                            None::<String>,
2183                            Some(Arc::clone(&self.main_spec)),
2184                            None,
2185                        ));
2186                        return None;
2187                    }
2188                };
2189
2190                Some(Expression {
2191                    kind: ExpressionKind::UnitConversion(
2192                        Arc::new(converted_value),
2193                        semantic_target,
2194                    ),
2195                    source_location: expr.source_location.clone(),
2196                })
2197            }
2198
2199            ast::ExpressionKind::LogicalNegation(operand, neg_type) => {
2200                let converted_operand = self.convert_expression_and_extract_dependencies(
2201                    operand,
2202                    current_spec_arc,
2203                    data_map,
2204                    current_segments,
2205                    depends_on_rules,
2206                    rule_names,
2207                    effective,
2208                )?;
2209                Some(Expression {
2210                    kind: ExpressionKind::LogicalNegation(
2211                        Arc::new(converted_operand),
2212                        neg_type.clone(),
2213                    ),
2214                    source_location: expr.source_location.clone(),
2215                })
2216            }
2217
2218            ast::ExpressionKind::MathematicalComputation(op, operand) => {
2219                let converted_operand = self.convert_expression_and_extract_dependencies(
2220                    operand,
2221                    current_spec_arc,
2222                    data_map,
2223                    current_segments,
2224                    depends_on_rules,
2225                    rule_names,
2226                    effective,
2227                )?;
2228                Some(Expression {
2229                    kind: ExpressionKind::MathematicalComputation(
2230                        op.clone(),
2231                        Arc::new(converted_operand),
2232                    ),
2233                    source_location: expr.source_location.clone(),
2234                })
2235            }
2236
2237            ast::ExpressionKind::Literal(value) => {
2238                // Convert AST Value to semantic ValueKind
2239                let semantic_value = match value_to_semantic(value) {
2240                    Ok(v) => v,
2241                    Err(e) => {
2242                        self.errors.push(self.engine_error(e, expr_src));
2243                        return None;
2244                    }
2245                };
2246                // Create LiteralValue with inferred type from the Value
2247                let lemma_type = match value {
2248                    Value::Text(_) => primitive_text().clone(),
2249                    Value::Number(_) => primitive_number().clone(),
2250                    Value::Scale(_, unit) => {
2251                        match self
2252                            .local_types
2253                            .get(current_spec_arc)
2254                            .and_then(|dt| dt.unit_index.get(unit))
2255                        {
2256                            Some(lt) => lt.clone(),
2257                            None => {
2258                                self.errors.push(self.engine_error(
2259                                    format!(
2260                                        "Scale literal uses unknown unit '{}' for this spec",
2261                                        unit
2262                                    ),
2263                                    expr_src,
2264                                ));
2265                                return None;
2266                            }
2267                        }
2268                    }
2269                    Value::Boolean(_) => primitive_boolean().clone(),
2270                    Value::Date(_) => primitive_date().clone(),
2271                    Value::Time(_) => primitive_time().clone(),
2272                    Value::Duration(_, _) => primitive_duration().clone(),
2273                    Value::Ratio(_, _) => primitive_ratio().clone(),
2274                };
2275                let literal_value = LiteralValue {
2276                    value: semantic_value,
2277                    lemma_type,
2278                };
2279                Some(Expression {
2280                    kind: ExpressionKind::Literal(Box::new(literal_value)),
2281                    source_location: expr.source_location.clone(),
2282                })
2283            }
2284
2285            ast::ExpressionKind::Veto(veto_expression) => Some(Expression {
2286                kind: ExpressionKind::Veto(veto_expression.clone()),
2287                source_location: expr.source_location.clone(),
2288            }),
2289
2290            ast::ExpressionKind::UnresolvedUnitLiteral(value, unit) => {
2291                if let Some(lt) = self
2292                    .local_types
2293                    .get(current_spec_arc)
2294                    .and_then(|dt| dt.unit_index.get(unit))
2295                {
2296                    let semantic_value = ValueKind::Scale(*value, unit.clone());
2297                    let literal_value = LiteralValue {
2298                        value: semantic_value,
2299                        lemma_type: lt.clone(),
2300                    };
2301                    Some(Expression {
2302                        kind: ExpressionKind::Literal(Box::new(literal_value)),
2303                        source_location: expr.source_location.clone(),
2304                    })
2305                } else {
2306                    self.errors
2307                        .push(self.engine_error(format!("Unknown unit '{}'", unit), expr_src));
2308                    None
2309                }
2310            }
2311
2312            ast::ExpressionKind::Now => Some(Expression {
2313                kind: ExpressionKind::Now,
2314                source_location: expr.source_location.clone(),
2315            }),
2316
2317            ast::ExpressionKind::DateRelative(kind, date_expr, tolerance) => {
2318                let converted_date = self.convert_expression_and_extract_dependencies(
2319                    date_expr,
2320                    current_spec_arc,
2321                    data_map,
2322                    current_segments,
2323                    depends_on_rules,
2324                    rule_names,
2325                    effective,
2326                )?;
2327                let converted_tolerance = match tolerance {
2328                    Some(tol) => Some(Arc::new(self.convert_expression_and_extract_dependencies(
2329                        tol,
2330                        current_spec_arc,
2331                        data_map,
2332                        current_segments,
2333                        depends_on_rules,
2334                        rule_names,
2335                        effective,
2336                    )?)),
2337                    None => None,
2338                };
2339                Some(Expression {
2340                    kind: ExpressionKind::DateRelative(
2341                        *kind,
2342                        Arc::new(converted_date),
2343                        converted_tolerance,
2344                    ),
2345                    source_location: expr.source_location.clone(),
2346                })
2347            }
2348
2349            ast::ExpressionKind::DateCalendar(kind, unit, date_expr) => {
2350                let converted_date = self.convert_expression_and_extract_dependencies(
2351                    date_expr,
2352                    current_spec_arc,
2353                    data_map,
2354                    current_segments,
2355                    depends_on_rules,
2356                    rule_names,
2357                    effective,
2358                )?;
2359                Some(Expression {
2360                    kind: ExpressionKind::DateCalendar(*kind, *unit, Arc::new(converted_date)),
2361                    source_location: expr.source_location.clone(),
2362                })
2363            }
2364        }
2365    }
2366}
2367
2368/// Find resolved types for a spec by name. Since per-slice resolution registers
2369/// at most one version per spec name, this is a simple name match.
2370fn find_types_by_name<'b>(
2371    types: &'b ResolvedTypesMap,
2372    name: &str,
2373) -> Option<&'b ResolvedSpecTypes> {
2374    types
2375        .iter()
2376        .find(|(spec, _)| spec.name == name)
2377        .map(|(_, t)| t)
2378}
2379
2380fn compute_arithmetic_result_type(left_type: LemmaType, right_type: LemmaType) -> LemmaType {
2381    compute_arithmetic_result_type_recursive(left_type, right_type, false)
2382}
2383
2384fn compute_arithmetic_result_type_recursive(
2385    left_type: LemmaType,
2386    right_type: LemmaType,
2387    swapped: bool,
2388) -> LemmaType {
2389    match (&left_type.specifications, &right_type.specifications) {
2390        (TypeSpecification::Veto { .. }, _) | (_, TypeSpecification::Veto { .. }) => {
2391            LemmaType::veto_type()
2392        }
2393        (TypeSpecification::Undetermined, _) => LemmaType::undetermined_type(),
2394
2395        (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => {
2396            primitive_duration().clone()
2397        }
2398        (TypeSpecification::Date { .. }, TypeSpecification::Time { .. }) => {
2399            primitive_duration().clone()
2400        }
2401        (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => {
2402            primitive_duration().clone()
2403        }
2404
2405        _ if left_type == right_type => left_type,
2406
2407        (TypeSpecification::Date { .. }, TypeSpecification::Duration { .. }) => left_type,
2408        (TypeSpecification::Time { .. }, TypeSpecification::Duration { .. }) => left_type,
2409
2410        (TypeSpecification::Scale { .. }, TypeSpecification::Ratio { .. }) => left_type,
2411        (TypeSpecification::Scale { .. }, TypeSpecification::Number { .. }) => left_type,
2412        (TypeSpecification::Scale { .. }, TypeSpecification::Duration { .. }) => {
2413            primitive_number().clone()
2414        }
2415        (TypeSpecification::Scale { .. }, TypeSpecification::Scale { .. }) => left_type,
2416
2417        (TypeSpecification::Duration { .. }, TypeSpecification::Number { .. }) => left_type,
2418        (TypeSpecification::Duration { .. }, TypeSpecification::Ratio { .. }) => left_type,
2419        (TypeSpecification::Duration { .. }, TypeSpecification::Duration { .. }) => {
2420            primitive_duration().clone()
2421        }
2422
2423        (TypeSpecification::Number { .. }, TypeSpecification::Ratio { .. }) => {
2424            primitive_number().clone()
2425        }
2426        (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => {
2427            primitive_number().clone()
2428        }
2429
2430        (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => left_type,
2431
2432        _ => {
2433            if swapped {
2434                LemmaType::undetermined_type()
2435            } else {
2436                compute_arithmetic_result_type_recursive(right_type, left_type, true)
2437            }
2438        }
2439    }
2440}
2441
2442// =============================================================================
2443// Phase 1: Pure type inference (no validation, no error collection)
2444// =============================================================================
2445
2446/// Infer the type of an expression without performing any validation.
2447/// Returns `LemmaType::undetermined_type()` when a type cannot be determined (e.g. unknown data).
2448fn infer_expression_type(
2449    expression: &Expression,
2450    graph: &Graph,
2451    computed_rule_types: &HashMap<RulePath, LemmaType>,
2452    resolved_types: &ResolvedTypesMap,
2453    spec_name: &str,
2454) -> LemmaType {
2455    match &expression.kind {
2456        ExpressionKind::Literal(literal_value) => literal_value.as_ref().get_type().clone(),
2457
2458        ExpressionKind::DataPath(data_path) => {
2459            infer_data_type(data_path, graph, computed_rule_types)
2460        }
2461
2462        ExpressionKind::RulePath(rule_path) => computed_rule_types
2463            .get(rule_path)
2464            .cloned()
2465            .unwrap_or_else(LemmaType::undetermined_type),
2466
2467        ExpressionKind::LogicalAnd(left, right) => {
2468            let left_type =
2469                infer_expression_type(left, graph, computed_rule_types, resolved_types, spec_name);
2470            let right_type =
2471                infer_expression_type(right, graph, computed_rule_types, resolved_types, spec_name);
2472            if left_type.vetoed() || right_type.vetoed() {
2473                return LemmaType::veto_type();
2474            }
2475            if left_type.is_undetermined() || right_type.is_undetermined() {
2476                return LemmaType::undetermined_type();
2477            }
2478            primitive_boolean().clone()
2479        }
2480
2481        ExpressionKind::LogicalNegation(operand, _) => {
2482            let operand_type = infer_expression_type(
2483                operand,
2484                graph,
2485                computed_rule_types,
2486                resolved_types,
2487                spec_name,
2488            );
2489            if operand_type.vetoed() {
2490                return LemmaType::veto_type();
2491            }
2492            if operand_type.is_undetermined() {
2493                return LemmaType::undetermined_type();
2494            }
2495            primitive_boolean().clone()
2496        }
2497
2498        ExpressionKind::Comparison(left, _op, right) => {
2499            let left_type =
2500                infer_expression_type(left, graph, computed_rule_types, resolved_types, spec_name);
2501            let right_type =
2502                infer_expression_type(right, graph, computed_rule_types, resolved_types, spec_name);
2503            if left_type.vetoed() || right_type.vetoed() {
2504                return LemmaType::veto_type();
2505            }
2506            if left_type.is_undetermined() || right_type.is_undetermined() {
2507                return LemmaType::undetermined_type();
2508            }
2509            primitive_boolean().clone()
2510        }
2511
2512        ExpressionKind::Arithmetic(left, _operator, right) => {
2513            let left_type =
2514                infer_expression_type(left, graph, computed_rule_types, resolved_types, spec_name);
2515            let right_type =
2516                infer_expression_type(right, graph, computed_rule_types, resolved_types, spec_name);
2517            compute_arithmetic_result_type(left_type, right_type)
2518        }
2519
2520        ExpressionKind::UnitConversion(source_expression, target) => {
2521            let source_type = infer_expression_type(
2522                source_expression,
2523                graph,
2524                computed_rule_types,
2525                resolved_types,
2526                spec_name,
2527            );
2528            if source_type.vetoed() {
2529                return LemmaType::veto_type();
2530            }
2531            if source_type.is_undetermined() {
2532                return LemmaType::undetermined_type();
2533            }
2534            match target {
2535                SemanticConversionTarget::Duration(_) => primitive_duration().clone(),
2536                SemanticConversionTarget::ScaleUnit(unit_name) => {
2537                    if source_type.is_number() {
2538                        find_types_by_name(resolved_types, spec_name)
2539                            .and_then(|dt| dt.unit_index.get(unit_name))
2540                            .cloned()
2541                            .unwrap_or_else(LemmaType::undetermined_type)
2542                    } else {
2543                        source_type
2544                    }
2545                }
2546                SemanticConversionTarget::RatioUnit(unit_name) => {
2547                    if source_type.is_number() {
2548                        find_types_by_name(resolved_types, spec_name)
2549                            .and_then(|dt| dt.unit_index.get(unit_name))
2550                            .cloned()
2551                            .unwrap_or_else(LemmaType::undetermined_type)
2552                    } else {
2553                        source_type
2554                    }
2555                }
2556            }
2557        }
2558
2559        ExpressionKind::MathematicalComputation(_, operand) => {
2560            let operand_type = infer_expression_type(
2561                operand,
2562                graph,
2563                computed_rule_types,
2564                resolved_types,
2565                spec_name,
2566            );
2567            if operand_type.vetoed() {
2568                return LemmaType::veto_type();
2569            }
2570            if operand_type.is_undetermined() {
2571                return LemmaType::undetermined_type();
2572            }
2573            primitive_number().clone()
2574        }
2575
2576        ExpressionKind::Veto(_) => LemmaType::veto_type(),
2577
2578        ExpressionKind::Now => primitive_date().clone(),
2579
2580        ExpressionKind::DateRelative(..) | ExpressionKind::DateCalendar(..) => {
2581            primitive_boolean().clone()
2582        }
2583    }
2584}
2585
2586/// Infer the type of a data reference without producing errors.
2587/// Returns `LemmaType::undetermined_type()` when the data cannot be found or is a spec reference.
2588///
2589/// For rule-target references the reference's stored `resolved_type` is still
2590/// the LHS-only placeholder (or fully `undetermined`) at the time
2591/// [`infer_rule_types`] runs — that field is filled by
2592/// [`Graph::resolve_rule_reference_types`] AFTER this pass. We therefore
2593/// look the target rule's inferred type up in `computed_rule_types`.
2594fn infer_data_type(
2595    data_path: &DataPath,
2596    graph: &Graph,
2597    computed_rule_types: &HashMap<RulePath, LemmaType>,
2598) -> LemmaType {
2599    let entry = match graph.data().get(data_path) {
2600        Some(e) => e,
2601        None => return LemmaType::undetermined_type(),
2602    };
2603    match entry {
2604        DataDefinition::Value { value, .. } => value.lemma_type.clone(),
2605        DataDefinition::TypeDeclaration { resolved_type, .. } => resolved_type.clone(),
2606        DataDefinition::Reference {
2607            target: ReferenceTarget::Rule(target_rule),
2608            resolved_type,
2609            ..
2610        } => {
2611            if !resolved_type.is_undetermined() {
2612                resolved_type.clone()
2613            } else {
2614                computed_rule_types
2615                    .get(target_rule)
2616                    .cloned()
2617                    .unwrap_or_else(LemmaType::undetermined_type)
2618            }
2619        }
2620        DataDefinition::Reference { resolved_type, .. } => resolved_type.clone(),
2621        DataDefinition::SpecRef { .. } => LemmaType::undetermined_type(),
2622    }
2623}
2624
2625/// Walk an expression tree, find every `DataPath` that resolves to a
2626/// rule-target reference in `reference_to_rule`, and accumulate the reference's
2627/// target rule into `out`. Used by
2628/// [`Graph::add_rule_reference_dependency_edges`] to inject rule-rule
2629/// dependency edges so `topological_sort` orders the target rule before any
2630/// consumer of the reference data path.
2631fn collect_rule_reference_dependencies(
2632    expression: &Expression,
2633    reference_to_rule: &HashMap<DataPath, RulePath>,
2634    out: &mut BTreeSet<RulePath>,
2635) {
2636    let mut paths: HashSet<DataPath> = HashSet::new();
2637    expression.kind.collect_data_paths(&mut paths);
2638    for path in paths {
2639        if let Some(target_rule) = reference_to_rule.get(&path) {
2640            out.insert(target_rule.clone());
2641        }
2642    }
2643}
2644
2645// =============================================================================
2646// Phase 2: Pure type checking (validation only, no mutation, returns Result)
2647// =============================================================================
2648
2649fn engine_error_at_graph(graph: &Graph, source: &Source, message: impl Into<String>) -> Error {
2650    Error::validation_with_context(
2651        message.into(),
2652        Some(source.clone()),
2653        None::<String>,
2654        Some(Arc::clone(&graph.main_spec)),
2655        None,
2656    )
2657}
2658
2659fn check_logical_operands(
2660    graph: &Graph,
2661    left_type: &LemmaType,
2662    right_type: &LemmaType,
2663    source: &Source,
2664) -> Result<(), Vec<Error>> {
2665    if left_type.vetoed() || right_type.vetoed() {
2666        return Ok(());
2667    }
2668    let mut errors = Vec::new();
2669    if !left_type.is_boolean() {
2670        errors.push(engine_error_at_graph(
2671            graph,
2672            source,
2673            format!(
2674                "Logical operation requires boolean operands, got {:?} for left operand",
2675                left_type
2676            ),
2677        ));
2678    }
2679    if !right_type.is_boolean() {
2680        errors.push(engine_error_at_graph(
2681            graph,
2682            source,
2683            format!(
2684                "Logical operation requires boolean operands, got {:?} for right operand",
2685                right_type
2686            ),
2687        ));
2688    }
2689    if errors.is_empty() {
2690        Ok(())
2691    } else {
2692        Err(errors)
2693    }
2694}
2695
2696fn check_logical_operand(
2697    graph: &Graph,
2698    operand_type: &LemmaType,
2699    source: &Source,
2700) -> Result<(), Vec<Error>> {
2701    if operand_type.vetoed() {
2702        return Ok(());
2703    }
2704    if !operand_type.is_boolean() {
2705        Err(vec![engine_error_at_graph(
2706            graph,
2707            source,
2708            format!(
2709                "Logical negation requires boolean operand, got {:?}",
2710                operand_type
2711            ),
2712        )])
2713    } else {
2714        Ok(())
2715    }
2716}
2717
2718fn check_comparison_types(
2719    graph: &Graph,
2720    left_type: &LemmaType,
2721    op: &ComparisonComputation,
2722    right_type: &LemmaType,
2723    source: &Source,
2724) -> Result<(), Vec<Error>> {
2725    if left_type.vetoed() || right_type.vetoed() {
2726        return Ok(());
2727    }
2728    let is_equality_only = matches!(op, ComparisonComputation::Is | ComparisonComputation::IsNot);
2729
2730    if left_type.is_boolean() && right_type.is_boolean() {
2731        if !is_equality_only {
2732            return Err(vec![engine_error_at_graph(
2733                graph,
2734                source,
2735                format!("Can only use 'is' and 'is not' with booleans (got {})", op),
2736            )]);
2737        }
2738        return Ok(());
2739    }
2740
2741    if left_type.is_text() && right_type.is_text() {
2742        if !is_equality_only {
2743            return Err(vec![engine_error_at_graph(
2744                graph,
2745                source,
2746                format!("Can only use 'is' and 'is not' with text (got {})", op),
2747            )]);
2748        }
2749        return Ok(());
2750    }
2751
2752    if left_type.is_number() && right_type.is_number() {
2753        return Ok(());
2754    }
2755
2756    if left_type.is_ratio() && right_type.is_ratio() {
2757        return Ok(());
2758    }
2759
2760    if left_type.is_date() && right_type.is_date() {
2761        return Ok(());
2762    }
2763
2764    if left_type.is_time() && right_type.is_time() {
2765        return Ok(());
2766    }
2767
2768    if left_type.is_scale() && right_type.is_scale() {
2769        if !left_type.same_scale_family(right_type) {
2770            return Err(vec![engine_error_at_graph(
2771                graph,
2772                source,
2773                format!(
2774                    "Cannot compare different scale types: {} and {}",
2775                    left_type.name(),
2776                    right_type.name()
2777                ),
2778            )]);
2779        }
2780        return Ok(());
2781    }
2782
2783    if left_type.is_duration() && right_type.is_duration() {
2784        return Ok(());
2785    }
2786    if left_type.is_duration() && right_type.is_number() {
2787        return Ok(());
2788    }
2789    if left_type.is_number() && right_type.is_duration() {
2790        return Ok(());
2791    }
2792
2793    Err(vec![engine_error_at_graph(
2794        graph,
2795        source,
2796        format!("Cannot compare {:?} with {:?}", left_type, right_type),
2797    )])
2798}
2799
2800fn check_arithmetic_types(
2801    graph: &Graph,
2802    left_type: &LemmaType,
2803    right_type: &LemmaType,
2804    operator: &ArithmeticComputation,
2805    source: &Source,
2806) -> Result<(), Vec<Error>> {
2807    if left_type.vetoed() || right_type.vetoed() {
2808        return Ok(());
2809    }
2810    // Date/Time: only Add and Subtract with Duration (or Date/Time - Date/Time)
2811    if left_type.is_date() || left_type.is_time() || right_type.is_date() || right_type.is_time() {
2812        let both_temporal = (left_type.is_date() || left_type.is_time())
2813            && (right_type.is_date() || right_type.is_time());
2814        let one_is_duration = left_type.is_duration() || right_type.is_duration();
2815        let valid = matches!(
2816            operator,
2817            ArithmeticComputation::Add | ArithmeticComputation::Subtract
2818        ) && (both_temporal || one_is_duration);
2819        if !valid {
2820            return Err(vec![engine_error_at_graph(
2821                graph,
2822                source,
2823                format!(
2824                    "Cannot apply '{}' to {} and {}.",
2825                    operator,
2826                    left_type.name(),
2827                    right_type.name()
2828                ),
2829            )]);
2830        }
2831        return Ok(());
2832    }
2833
2834    // Different scale families: reject all operators
2835    if left_type.is_scale() && right_type.is_scale() && !left_type.same_scale_family(right_type) {
2836        return Err(vec![engine_error_at_graph(
2837            graph,
2838            source,
2839            format!(
2840                "Cannot {} different scale types: {} and {}. Operations between different scale types produce ambiguous result units.",
2841                match operator {
2842                    ArithmeticComputation::Add => "add",
2843                    ArithmeticComputation::Subtract => "subtract",
2844                    ArithmeticComputation::Multiply => "multiply",
2845                    ArithmeticComputation::Divide => "divide",
2846                    ArithmeticComputation::Modulo => "modulo",
2847                    ArithmeticComputation::Power => "power",
2848                },
2849                left_type.name(),
2850                right_type.name()
2851            ),
2852        )]);
2853    }
2854
2855    // Only Scale, Number, Ratio, and Duration can participate in arithmetic
2856    let left_valid = left_type.is_scale()
2857        || left_type.is_number()
2858        || left_type.is_duration()
2859        || left_type.is_ratio();
2860    let right_valid = right_type.is_scale()
2861        || right_type.is_number()
2862        || right_type.is_duration()
2863        || right_type.is_ratio();
2864
2865    if !left_valid || !right_valid {
2866        return Err(vec![engine_error_at_graph(
2867            graph,
2868            source,
2869            format!(
2870                "Cannot apply '{}' to {} and {}.",
2871                operator,
2872                left_type.name(),
2873                right_type.name()
2874            ),
2875        )]);
2876    }
2877
2878    // Operator-specific constraints (same base type is always allowed)
2879    if left_type.has_same_base_type(right_type) {
2880        return Ok(());
2881    }
2882
2883    let pair = |a: fn(&LemmaType) -> bool, b: fn(&LemmaType) -> bool| {
2884        (a(left_type) && b(right_type)) || (b(left_type) && a(right_type))
2885    };
2886
2887    let allowed = match operator {
2888        ArithmeticComputation::Multiply => {
2889            pair(LemmaType::is_scale, LemmaType::is_number)
2890                || pair(LemmaType::is_scale, LemmaType::is_ratio)
2891                || pair(LemmaType::is_scale, LemmaType::is_duration)
2892                || pair(LemmaType::is_duration, LemmaType::is_number)
2893                || pair(LemmaType::is_duration, LemmaType::is_ratio)
2894                || pair(LemmaType::is_number, LemmaType::is_ratio)
2895        }
2896        ArithmeticComputation::Divide => {
2897            pair(LemmaType::is_scale, LemmaType::is_number)
2898                || pair(LemmaType::is_scale, LemmaType::is_ratio)
2899                || pair(LemmaType::is_scale, LemmaType::is_duration)
2900                || (left_type.is_duration() && right_type.is_number())
2901                || (left_type.is_duration() && right_type.is_ratio())
2902                || pair(LemmaType::is_number, LemmaType::is_ratio)
2903        }
2904        ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
2905            pair(LemmaType::is_scale, LemmaType::is_number)
2906                || pair(LemmaType::is_scale, LemmaType::is_ratio)
2907                || pair(LemmaType::is_duration, LemmaType::is_number)
2908                || pair(LemmaType::is_duration, LemmaType::is_ratio)
2909                || pair(LemmaType::is_number, LemmaType::is_ratio)
2910        }
2911        ArithmeticComputation::Power => {
2912            (left_type.is_number()
2913                || left_type.is_scale()
2914                || left_type.is_ratio()
2915                || left_type.is_duration())
2916                && (right_type.is_number() || right_type.is_ratio())
2917        }
2918        ArithmeticComputation::Modulo => right_type.is_number() || right_type.is_ratio(),
2919    };
2920
2921    if !allowed {
2922        return Err(vec![engine_error_at_graph(
2923            graph,
2924            source,
2925            format!(
2926                "Cannot apply '{}' to {} and {}.",
2927                operator,
2928                left_type.name(),
2929                right_type.name(),
2930            ),
2931        )]);
2932    }
2933
2934    Ok(())
2935}
2936
2937fn check_unit_conversion_types(
2938    graph: &Graph,
2939    source_type: &LemmaType,
2940    target: &SemanticConversionTarget,
2941    resolved_types: &ResolvedTypesMap,
2942    source: &Source,
2943    spec_name: &str,
2944) -> Result<(), Vec<Error>> {
2945    if source_type.vetoed() {
2946        return Ok(());
2947    }
2948    match target {
2949        SemanticConversionTarget::ScaleUnit(unit_name)
2950        | SemanticConversionTarget::RatioUnit(unit_name) => {
2951            let unit_check: Option<(bool, Vec<&str>)> = match (&source_type.specifications, target)
2952            {
2953                (
2954                    TypeSpecification::Scale { units, .. },
2955                    SemanticConversionTarget::ScaleUnit(_),
2956                ) => {
2957                    let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
2958                    let found = units.iter().any(|u| u.name.eq_ignore_ascii_case(unit_name));
2959                    Some((found, valid))
2960                }
2961                (
2962                    TypeSpecification::Ratio { units, .. },
2963                    SemanticConversionTarget::RatioUnit(_),
2964                ) => {
2965                    let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
2966                    let found = units.iter().any(|u| u.name.eq_ignore_ascii_case(unit_name));
2967                    Some((found, valid))
2968                }
2969                _ => None,
2970            };
2971
2972            match unit_check {
2973                Some((true, _)) => Ok(()),
2974                Some((false, valid)) => Err(vec![engine_error_at_graph(
2975                    graph,
2976                    source,
2977                    format!(
2978                        "Unknown unit '{}' for type {}. Valid units: {}",
2979                        unit_name,
2980                        source_type.name(),
2981                        valid.join(", ")
2982                    ),
2983                )]),
2984                None if source_type.is_number() => {
2985                    if find_types_by_name(resolved_types, spec_name)
2986                        .and_then(|dt| dt.unit_index.get(unit_name))
2987                        .is_none()
2988                    {
2989                        Err(vec![engine_error_at_graph(
2990                            graph,
2991                            source,
2992                            format!("Unknown unit '{}' in spec '{}'.", unit_name, spec_name),
2993                        )])
2994                    } else {
2995                        Ok(())
2996                    }
2997                }
2998                None => Err(vec![engine_error_at_graph(
2999                    graph,
3000                    source,
3001                    format!(
3002                        "Cannot convert {} to unit '{}'.",
3003                        source_type.name(),
3004                        unit_name
3005                    ),
3006                )]),
3007            }
3008        }
3009        SemanticConversionTarget::Duration(_) => {
3010            if !source_type.is_duration() && !source_type.is_numeric() {
3011                Err(vec![engine_error_at_graph(
3012                    graph,
3013                    source,
3014                    format!("Cannot convert {} to duration.", source_type.name()),
3015                )])
3016            } else {
3017                Ok(())
3018            }
3019        }
3020    }
3021}
3022
3023fn check_mathematical_operand(
3024    graph: &Graph,
3025    operand_type: &LemmaType,
3026    source: &Source,
3027) -> Result<(), Vec<Error>> {
3028    if operand_type.vetoed() {
3029        return Ok(());
3030    }
3031    if !operand_type.is_number() {
3032        Err(vec![engine_error_at_graph(
3033            graph,
3034            source,
3035            format!(
3036                "Mathematical function requires number operand, got {:?}",
3037                operand_type
3038            ),
3039        )])
3040    } else {
3041        Ok(())
3042    }
3043}
3044
3045/// Check that all rule references in the graph point to existing rules.
3046fn check_all_rule_references_exist(graph: &Graph) -> Result<(), Vec<Error>> {
3047    let mut errors = Vec::new();
3048    let existing_rules: HashSet<&RulePath> = graph.rules().keys().collect();
3049    for (rule_path, rule_node) in graph.rules() {
3050        for dependency in &rule_node.depends_on_rules {
3051            if !existing_rules.contains(dependency) {
3052                errors.push(engine_error_at_graph(
3053                    graph,
3054                    &rule_node.source,
3055                    format!(
3056                        "Rule '{}' references non-existent rule '{}'",
3057                        rule_path.rule, dependency.rule
3058                    ),
3059                ));
3060            }
3061        }
3062    }
3063    if errors.is_empty() {
3064        Ok(())
3065    } else {
3066        Err(errors)
3067    }
3068}
3069
3070/// Check that no data and rule share the same name in the same spec.
3071fn check_data_and_rule_name_collisions(graph: &Graph) -> Result<(), Vec<Error>> {
3072    let mut errors = Vec::new();
3073    for rule_path in graph.rules().keys() {
3074        let data_path = DataPath::new(rule_path.segments.clone(), rule_path.rule.clone());
3075        if graph.data().contains_key(&data_path) {
3076            let rule_node = graph.rules().get(rule_path).unwrap_or_else(|| {
3077                unreachable!(
3078                    "BUG: rule '{}' missing from graph while validating name collisions",
3079                    rule_path.rule
3080                )
3081            });
3082            errors.push(engine_error_at_graph(
3083                graph,
3084                &rule_node.source,
3085                format!(
3086                    "Name collision: '{}' is defined as both a data and a rule",
3087                    data_path
3088                ),
3089            ));
3090        }
3091    }
3092    if errors.is_empty() {
3093        Ok(())
3094    } else {
3095        Err(errors)
3096    }
3097}
3098
3099/// Check that a data reference is valid (exists and is not a bare spec reference).
3100fn check_data_reference(
3101    data_path: &DataPath,
3102    graph: &Graph,
3103    data_source: &Source,
3104) -> Result<(), Vec<Error>> {
3105    let entry = match graph.data().get(data_path) {
3106        Some(e) => e,
3107        None => {
3108            return Err(vec![engine_error_at_graph(
3109                graph,
3110                data_source,
3111                format!("Unknown data reference '{}'", data_path),
3112            )]);
3113        }
3114    };
3115    match entry {
3116        DataDefinition::Value { .. }
3117        | DataDefinition::TypeDeclaration { .. }
3118        | DataDefinition::Reference { .. } => Ok(()),
3119        DataDefinition::SpecRef { .. } => Err(vec![engine_error_at_graph(
3120            graph,
3121            entry.source(),
3122            format!(
3123                "Cannot compute type for spec reference data '{}'",
3124                data_path
3125            ),
3126        )]),
3127    }
3128}
3129
3130/// Check a single expression for type errors, given precomputed inferred types.
3131/// Recursively checks sub-expressions. Skips validation when either operand is `Error`
3132/// (the root cause is reported by `check_data_reference` or similar).
3133fn check_expression(
3134    expression: &Expression,
3135    graph: &Graph,
3136    inferred_types: &HashMap<RulePath, LemmaType>,
3137    resolved_types: &ResolvedTypesMap,
3138    spec_name: &str,
3139) -> Result<(), Vec<Error>> {
3140    let mut errors = Vec::new();
3141
3142    let collect = |result: Result<(), Vec<Error>>, errors: &mut Vec<Error>| {
3143        if let Err(errs) = result {
3144            errors.extend(errs);
3145        }
3146    };
3147
3148    match &expression.kind {
3149        ExpressionKind::Literal(_) => {}
3150
3151        ExpressionKind::DataPath(data_path) => {
3152            let data_source = expression
3153                .source_location
3154                .as_ref()
3155                .expect("BUG: expression missing source in check_expression");
3156            collect(
3157                check_data_reference(data_path, graph, data_source),
3158                &mut errors,
3159            );
3160        }
3161
3162        ExpressionKind::RulePath(_) => {}
3163
3164        ExpressionKind::LogicalAnd(left, right) => {
3165            collect(
3166                check_expression(left, graph, inferred_types, resolved_types, spec_name),
3167                &mut errors,
3168            );
3169            collect(
3170                check_expression(right, graph, inferred_types, resolved_types, spec_name),
3171                &mut errors,
3172            );
3173
3174            let left_type =
3175                infer_expression_type(left, graph, inferred_types, resolved_types, spec_name);
3176            let right_type =
3177                infer_expression_type(right, graph, inferred_types, resolved_types, spec_name);
3178            let expr_source = expression
3179                .source_location
3180                .as_ref()
3181                .expect("BUG: expression missing source in check_expression");
3182            collect(
3183                check_logical_operands(graph, &left_type, &right_type, expr_source),
3184                &mut errors,
3185            );
3186        }
3187
3188        ExpressionKind::LogicalNegation(operand, _) => {
3189            collect(
3190                check_expression(operand, graph, inferred_types, resolved_types, spec_name),
3191                &mut errors,
3192            );
3193
3194            let operand_type =
3195                infer_expression_type(operand, graph, inferred_types, resolved_types, spec_name);
3196            let expr_source = expression
3197                .source_location
3198                .as_ref()
3199                .expect("BUG: expression missing source in check_expression");
3200            collect(
3201                check_logical_operand(graph, &operand_type, expr_source),
3202                &mut errors,
3203            );
3204        }
3205
3206        ExpressionKind::Comparison(left, op, right) => {
3207            collect(
3208                check_expression(left, graph, inferred_types, resolved_types, spec_name),
3209                &mut errors,
3210            );
3211            collect(
3212                check_expression(right, graph, inferred_types, resolved_types, spec_name),
3213                &mut errors,
3214            );
3215
3216            let left_type =
3217                infer_expression_type(left, graph, inferred_types, resolved_types, spec_name);
3218            let right_type =
3219                infer_expression_type(right, graph, inferred_types, resolved_types, spec_name);
3220            let expr_source = expression
3221                .source_location
3222                .as_ref()
3223                .expect("BUG: expression missing source in check_expression");
3224            collect(
3225                check_comparison_types(graph, &left_type, op, &right_type, expr_source),
3226                &mut errors,
3227            );
3228        }
3229
3230        ExpressionKind::Arithmetic(left, operator, right) => {
3231            collect(
3232                check_expression(left, graph, inferred_types, resolved_types, spec_name),
3233                &mut errors,
3234            );
3235            collect(
3236                check_expression(right, graph, inferred_types, resolved_types, spec_name),
3237                &mut errors,
3238            );
3239
3240            let left_type =
3241                infer_expression_type(left, graph, inferred_types, resolved_types, spec_name);
3242            let right_type =
3243                infer_expression_type(right, graph, inferred_types, resolved_types, spec_name);
3244            let expr_source = expression
3245                .source_location
3246                .as_ref()
3247                .expect("BUG: expression missing source in check_expression");
3248            collect(
3249                check_arithmetic_types(graph, &left_type, &right_type, operator, expr_source),
3250                &mut errors,
3251            );
3252        }
3253
3254        ExpressionKind::UnitConversion(source_expression, target) => {
3255            collect(
3256                check_expression(
3257                    source_expression,
3258                    graph,
3259                    inferred_types,
3260                    resolved_types,
3261                    spec_name,
3262                ),
3263                &mut errors,
3264            );
3265
3266            let source_type = infer_expression_type(
3267                source_expression,
3268                graph,
3269                inferred_types,
3270                resolved_types,
3271                spec_name,
3272            );
3273            let expr_source = expression
3274                .source_location
3275                .as_ref()
3276                .expect("BUG: expression missing source in check_expression");
3277            collect(
3278                check_unit_conversion_types(
3279                    graph,
3280                    &source_type,
3281                    target,
3282                    resolved_types,
3283                    expr_source,
3284                    spec_name,
3285                ),
3286                &mut errors,
3287            );
3288
3289            if source_type.is_number() {
3290                match target {
3291                    SemanticConversionTarget::ScaleUnit(unit_name)
3292                    | SemanticConversionTarget::RatioUnit(unit_name) => {
3293                        if find_types_by_name(resolved_types, spec_name)
3294                            .and_then(|dt| dt.unit_index.get(unit_name))
3295                            .is_none()
3296                        {
3297                            errors.push(engine_error_at_graph(
3298                                graph,
3299                                expr_source,
3300                                format!(
3301                                    "Cannot resolve unit '{}' for spec '{}' (types may not have been resolved)",
3302                                    unit_name,
3303                                    spec_name
3304                                ),
3305                            ));
3306                        }
3307                    }
3308                    SemanticConversionTarget::Duration(_) => {}
3309                }
3310            }
3311        }
3312
3313        ExpressionKind::MathematicalComputation(_, operand) => {
3314            collect(
3315                check_expression(operand, graph, inferred_types, resolved_types, spec_name),
3316                &mut errors,
3317            );
3318
3319            let operand_type =
3320                infer_expression_type(operand, graph, inferred_types, resolved_types, spec_name);
3321            let expr_source = expression
3322                .source_location
3323                .as_ref()
3324                .expect("BUG: expression missing source in check_expression");
3325            collect(
3326                check_mathematical_operand(graph, &operand_type, expr_source),
3327                &mut errors,
3328            );
3329        }
3330
3331        ExpressionKind::Veto(_) => {}
3332
3333        ExpressionKind::Now => {}
3334
3335        ExpressionKind::DateRelative(_, date_expr, tolerance) => {
3336            collect(
3337                check_expression(date_expr, graph, inferred_types, resolved_types, spec_name),
3338                &mut errors,
3339            );
3340
3341            let date_type =
3342                infer_expression_type(date_expr, graph, inferred_types, resolved_types, spec_name);
3343            if !date_type.is_date() {
3344                let expr_source = expression
3345                    .source_location
3346                    .as_ref()
3347                    .expect("BUG: expression missing source in check_expression");
3348                errors.push(engine_error_at_graph(
3349                    graph,
3350                    expr_source,
3351                    format!(
3352                        "Date sugar 'in past/future' requires a date expression, got type '{}'",
3353                        date_type
3354                    ),
3355                ));
3356            }
3357
3358            if let Some(tol) = tolerance {
3359                collect(
3360                    check_expression(tol, graph, inferred_types, resolved_types, spec_name),
3361                    &mut errors,
3362                );
3363
3364                let tol_type =
3365                    infer_expression_type(tol, graph, inferred_types, resolved_types, spec_name);
3366                if !tol_type.is_duration() {
3367                    let expr_source = expression
3368                        .source_location
3369                        .as_ref()
3370                        .expect("BUG: expression missing source in check_expression");
3371                    errors.push(engine_error_at_graph(
3372                        graph,
3373                        expr_source,
3374                        format!(
3375                            "Tolerance in date sugar must be a duration, got type '{}'",
3376                            tol_type
3377                        ),
3378                    ));
3379                }
3380            }
3381        }
3382
3383        ExpressionKind::DateCalendar(_, _, date_expr) => {
3384            collect(
3385                check_expression(date_expr, graph, inferred_types, resolved_types, spec_name),
3386                &mut errors,
3387            );
3388
3389            let date_type =
3390                infer_expression_type(date_expr, graph, inferred_types, resolved_types, spec_name);
3391            if !date_type.is_date() {
3392                let expr_source = expression
3393                    .source_location
3394                    .as_ref()
3395                    .expect("BUG: expression missing source in check_expression");
3396                errors.push(engine_error_at_graph(
3397                    graph,
3398                    expr_source,
3399                    format!(
3400                        "Calendar sugar requires a date expression, got type '{}'",
3401                        date_type
3402                    ),
3403                ));
3404            }
3405        }
3406    }
3407
3408    if errors.is_empty() {
3409        Ok(())
3410    } else {
3411        Err(errors)
3412    }
3413}
3414
3415/// Check all rule types in topological order, given precomputed inferred types.
3416/// Validates:
3417/// - Branch type consistency (all non-Veto branches must return the same primitive type)
3418/// - Condition types (unless clause conditions must be boolean)
3419/// - All sub-expressions via `check_expression`
3420fn check_rule_types(
3421    graph: &Graph,
3422    execution_order: &[RulePath],
3423    inferred_types: &HashMap<RulePath, LemmaType>,
3424    resolved_types: &ResolvedTypesMap,
3425) -> Result<(), Vec<Error>> {
3426    let mut errors = Vec::new();
3427
3428    let collect = |result: Result<(), Vec<Error>>, errors: &mut Vec<Error>| {
3429        if let Err(errs) = result {
3430            errors.extend(errs);
3431        }
3432    };
3433
3434    for rule_path in execution_order {
3435        let rule_node = match graph.rules().get(rule_path) {
3436            Some(node) => node,
3437            None => continue,
3438        };
3439        let branches = &rule_node.branches;
3440        let spec_name = rule_node.spec_name.as_str();
3441
3442        if branches.is_empty() {
3443            continue;
3444        }
3445
3446        let (_, default_result) = &branches[0];
3447        collect(
3448            check_expression(
3449                default_result,
3450                graph,
3451                inferred_types,
3452                resolved_types,
3453                spec_name,
3454            ),
3455            &mut errors,
3456        );
3457        let default_type = infer_expression_type(
3458            default_result,
3459            graph,
3460            inferred_types,
3461            resolved_types,
3462            spec_name,
3463        );
3464
3465        let mut non_veto_type: Option<LemmaType> = None;
3466        if !default_type.vetoed() && !default_type.is_undetermined() {
3467            non_veto_type = Some(default_type.clone());
3468        }
3469
3470        for (branch_index, (condition, result)) in branches.iter().enumerate().skip(1) {
3471            if let Some(condition_expression) = condition {
3472                collect(
3473                    check_expression(
3474                        condition_expression,
3475                        graph,
3476                        inferred_types,
3477                        resolved_types,
3478                        spec_name,
3479                    ),
3480                    &mut errors,
3481                );
3482                let condition_type = infer_expression_type(
3483                    condition_expression,
3484                    graph,
3485                    inferred_types,
3486                    resolved_types,
3487                    spec_name,
3488                );
3489                if !condition_type.is_boolean() && !condition_type.is_undetermined() {
3490                    let condition_source = condition_expression
3491                        .source_location
3492                        .as_ref()
3493                        .expect("BUG: condition expression missing source in check_rule_types");
3494                    errors.push(engine_error_at_graph(
3495                        graph,
3496                        condition_source,
3497                        format!(
3498                            "Unless clause condition in rule '{}' must be boolean, got {:?}",
3499                            rule_path.rule, condition_type
3500                        ),
3501                    ));
3502                }
3503            }
3504
3505            collect(
3506                check_expression(result, graph, inferred_types, resolved_types, spec_name),
3507                &mut errors,
3508            );
3509            let result_type =
3510                infer_expression_type(result, graph, inferred_types, resolved_types, spec_name);
3511
3512            if !result_type.vetoed() && !result_type.is_undetermined() {
3513                if non_veto_type.is_none() {
3514                    non_veto_type = Some(result_type.clone());
3515                } else if let Some(ref existing_type) = non_veto_type {
3516                    if !existing_type.has_same_base_type(&result_type) {
3517                        let Some(rule_node) = graph.rules().get(rule_path) else {
3518                            unreachable!(
3519                                "BUG: rule type validation referenced missing rule '{}'",
3520                                rule_path.rule
3521                            );
3522                        };
3523                        let rule_source = &rule_node.source;
3524                        let default_expr = &branches[0].1;
3525
3526                        let mut location_parts = vec![format!(
3527                            "{}:{}:{}",
3528                            rule_source.attribute, rule_source.span.line, rule_source.span.col
3529                        )];
3530
3531                        if let Some(loc) = &default_expr.source_location {
3532                            location_parts.push(format!(
3533                                "default branch at {}:{}:{}",
3534                                loc.attribute, loc.span.line, loc.span.col
3535                            ));
3536                        }
3537                        if let Some(loc) = &result.source_location {
3538                            location_parts.push(format!(
3539                                "unless clause {} at {}:{}:{}",
3540                                branch_index, loc.attribute, loc.span.line, loc.span.col
3541                            ));
3542                        }
3543
3544                        errors.push(Error::validation_with_context(
3545                            format!("Type mismatch in rule '{}' in spec '{}' ({}): default branch returns {}, but unless clause {} returns {}. All branches must return the same primitive type.",
3546                            rule_path.rule,
3547                            spec_name,
3548                            location_parts.join(", "),
3549                            existing_type.name(),
3550                            branch_index,
3551                            result_type.name()),
3552                            Some(rule_source.clone()),
3553                            None::<String>,
3554                            Some(Arc::clone(&graph.main_spec)),
3555                            None,
3556                        ));
3557                    }
3558                }
3559            }
3560        }
3561    }
3562
3563    if errors.is_empty() {
3564        Ok(())
3565    } else {
3566        Err(errors)
3567    }
3568}
3569
3570// =============================================================================
3571// Phase 3: Apply inferred types to the graph (the only mutation point)
3572// =============================================================================
3573
3574/// Write inferred types into the graph's rule nodes.
3575/// This is the only function that mutates the graph during the validation pipeline.
3576/// It must only be called after all checks pass (no errors).
3577fn apply_inferred_types(graph: &mut Graph, inferred_types: HashMap<RulePath, LemmaType>) {
3578    for (rule_path, rule_type) in inferred_types {
3579        if let Some(rule_node) = graph.rules_mut().get_mut(&rule_path) {
3580            rule_node.rule_type = rule_type;
3581        }
3582    }
3583}
3584
3585/// Infer the types of all rules in topological order without performing any validation.
3586/// Returns a map from rule path to its inferred type.
3587/// This function is pure: it takes `&Graph` and returns data with no side effects.
3588fn infer_rule_types(
3589    graph: &Graph,
3590    execution_order: &[RulePath],
3591    resolved_types: &ResolvedTypesMap,
3592) -> HashMap<RulePath, LemmaType> {
3593    let mut computed_types: HashMap<RulePath, LemmaType> = HashMap::new();
3594
3595    for rule_path in execution_order {
3596        let rule_node = match graph.rules().get(rule_path) {
3597            Some(node) => node,
3598            None => continue,
3599        };
3600        let branches = &rule_node.branches;
3601        let spec_name = rule_node.spec_name.as_str();
3602
3603        if branches.is_empty() {
3604            continue;
3605        }
3606
3607        let (_, default_result) = &branches[0];
3608        let default_type = infer_expression_type(
3609            default_result,
3610            graph,
3611            &computed_types,
3612            resolved_types,
3613            spec_name,
3614        );
3615
3616        let mut non_veto_type: Option<LemmaType> = None;
3617        if !default_type.vetoed() && !default_type.is_undetermined() {
3618            non_veto_type = Some(default_type.clone());
3619        }
3620
3621        for (_branch_index, (condition, result)) in branches.iter().enumerate().skip(1) {
3622            if let Some(condition_expression) = condition {
3623                let _condition_type = infer_expression_type(
3624                    condition_expression,
3625                    graph,
3626                    &computed_types,
3627                    resolved_types,
3628                    spec_name,
3629                );
3630            }
3631
3632            let result_type =
3633                infer_expression_type(result, graph, &computed_types, resolved_types, spec_name);
3634            if !result_type.vetoed() && !result_type.is_undetermined() && non_veto_type.is_none() {
3635                non_veto_type = Some(result_type.clone());
3636            }
3637        }
3638
3639        let rule_type = non_veto_type.unwrap_or_else(LemmaType::veto_type);
3640        computed_types.insert(rule_path.clone(), rule_type);
3641    }
3642
3643    computed_types
3644}
3645
3646#[cfg(test)]
3647mod tests {
3648    use super::*;
3649
3650    use crate::parsing::ast::{BooleanValue, Reference, Span, Value};
3651
3652    fn test_source() -> Source {
3653        Source::new(
3654            "test.lemma",
3655            Span {
3656                start: 0,
3657                end: 0,
3658                line: 1,
3659                col: 0,
3660            },
3661        )
3662    }
3663
3664    fn build_graph(main_spec: &LemmaSpec, all_specs: &[LemmaSpec]) -> Result<Graph, Vec<Error>> {
3665        use crate::engine::Context;
3666        use crate::planning::discovery;
3667
3668        let mut ctx = Context::new();
3669        for s in all_specs {
3670            if let Err(e) = ctx.insert_spec(Arc::new(s.clone()), s.from_registry) {
3671                return Err(vec![e]);
3672            }
3673        }
3674        let effective = EffectiveDate::from_option(main_spec.effective_from().cloned());
3675        let main_spec_arc = ctx
3676            .spec_sets()
3677            .get(main_spec.name.as_str())
3678            .and_then(|ss| ss.get_exact(main_spec.effective_from()).cloned())
3679            .expect("main_spec must be in all_specs");
3680        let dag =
3681            discovery::build_dag_for_spec(&ctx, &main_spec_arc, &effective).map_err(
3682                |e| match e {
3683                    discovery::DagError::Cycle(es) | discovery::DagError::Other(es) => es,
3684                },
3685            )?;
3686        match Graph::build(&ctx, &main_spec_arc, &dag, &effective) {
3687            Ok((graph, _types)) => Ok(graph),
3688            Err(errors) => Err(errors),
3689        }
3690    }
3691
3692    fn create_test_spec(name: &str) -> LemmaSpec {
3693        LemmaSpec::new(name.to_string())
3694    }
3695
3696    fn create_literal_data(name: &str, value: Value) -> LemmaData {
3697        LemmaData {
3698            reference: Reference {
3699                segments: Vec::new(),
3700                name: name.to_string(),
3701            },
3702            value: ParsedDataValue::Literal(value),
3703            source_location: test_source(),
3704        }
3705    }
3706
3707    fn create_literal_expr(value: Value) -> ast::Expression {
3708        ast::Expression {
3709            kind: ast::ExpressionKind::Literal(value),
3710            source_location: Some(test_source()),
3711        }
3712    }
3713
3714    #[test]
3715    fn should_reject_data_binding_into_non_spec_data() {
3716        // Higher-standard language rule:
3717        // if `x` is a literal (not a spec reference), `x.y = ...` must be rejected.
3718        //
3719        // This is currently expected to FAIL until graph building enforces it consistently.
3720        let mut spec = create_test_spec("test");
3721        spec = spec.add_data(create_literal_data("x", Value::Number(1.into())));
3722
3723        // Bind x.y, but x is not a spec reference.
3724        spec = spec.add_data(LemmaData {
3725            reference: Reference::from_path(vec!["x".to_string(), "y".to_string()]),
3726            value: ParsedDataValue::Literal(Value::Number(2.into())),
3727            source_location: test_source(),
3728        });
3729
3730        let result = build_graph(&spec, &[spec.clone()]);
3731        assert!(
3732            result.is_err(),
3733            "Overriding x.y must fail when x is not a spec reference"
3734        );
3735    }
3736
3737    #[test]
3738    fn should_reject_data_and_rule_name_collision() {
3739        // Higher-standard language rule: data and rule names should not collide.
3740        // It's ambiguous for humans and leads to confusing error messages.
3741        //
3742        // This is currently expected to FAIL until the language enforces it.
3743        let mut spec = create_test_spec("test");
3744        spec = spec.add_data(create_literal_data("x", Value::Number(1.into())));
3745        spec = spec.add_rule(LemmaRule {
3746            name: "x".to_string(),
3747            expression: create_literal_expr(Value::Number(2.into())),
3748            unless_clauses: Vec::new(),
3749            source_location: test_source(),
3750        });
3751
3752        let result = build_graph(&spec, &[spec.clone()]);
3753        assert!(
3754            result.is_err(),
3755            "Data and rule name collisions should be rejected"
3756        );
3757    }
3758
3759    #[test]
3760    fn test_duplicate_data() {
3761        let mut spec = create_test_spec("test");
3762        spec = spec.add_data(create_literal_data(
3763            "age",
3764            Value::Number(rust_decimal::Decimal::from(25)),
3765        ));
3766        spec = spec.add_data(create_literal_data(
3767            "age",
3768            Value::Number(rust_decimal::Decimal::from(30)),
3769        ));
3770
3771        let result = build_graph(&spec, &[spec.clone()]);
3772        assert!(result.is_err(), "Should detect duplicate data");
3773
3774        let errors = result.unwrap_err();
3775        assert!(errors
3776            .iter()
3777            .any(|e| e.to_string().contains("Duplicate data") && e.to_string().contains("age")));
3778    }
3779
3780    #[test]
3781    fn test_duplicate_rule() {
3782        let mut spec = create_test_spec("test");
3783
3784        let rule1 = LemmaRule {
3785            name: "test_rule".to_string(),
3786            expression: create_literal_expr(Value::Boolean(BooleanValue::True)),
3787            unless_clauses: Vec::new(),
3788            source_location: test_source(),
3789        };
3790        let rule2 = LemmaRule {
3791            name: "test_rule".to_string(),
3792            expression: create_literal_expr(Value::Boolean(BooleanValue::False)),
3793            unless_clauses: Vec::new(),
3794            source_location: test_source(),
3795        };
3796
3797        spec = spec.add_rule(rule1);
3798        spec = spec.add_rule(rule2);
3799
3800        let result = build_graph(&spec, &[spec.clone()]);
3801        assert!(result.is_err(), "Should detect duplicate rule");
3802
3803        let errors = result.unwrap_err();
3804        assert!(errors.iter().any(
3805            |e| e.to_string().contains("Duplicate rule") && e.to_string().contains("test_rule")
3806        ));
3807    }
3808
3809    #[test]
3810    fn test_missing_data_reference() {
3811        let mut spec = create_test_spec("test");
3812
3813        let missing_data_expr = ast::Expression {
3814            kind: ast::ExpressionKind::Reference(Reference {
3815                segments: Vec::new(),
3816                name: "nonexistent".to_string(),
3817            }),
3818            source_location: Some(test_source()),
3819        };
3820
3821        let rule = LemmaRule {
3822            name: "test_rule".to_string(),
3823            expression: missing_data_expr,
3824            unless_clauses: Vec::new(),
3825            source_location: test_source(),
3826        };
3827        spec = spec.add_rule(rule);
3828
3829        let result = build_graph(&spec, &[spec.clone()]);
3830        assert!(result.is_err(), "Should detect missing data");
3831
3832        let errors = result.unwrap_err();
3833        assert!(errors
3834            .iter()
3835            .any(|e| e.to_string().contains("Reference 'nonexistent' not found")));
3836    }
3837
3838    #[test]
3839    fn test_missing_spec_reference() {
3840        let mut spec = create_test_spec("test");
3841
3842        let data = LemmaData {
3843            reference: Reference {
3844                segments: Vec::new(),
3845                name: "contract".to_string(),
3846            },
3847            value: ParsedDataValue::SpecReference(crate::parsing::ast::SpecRef::local(
3848                "nonexistent",
3849            )),
3850            source_location: test_source(),
3851        };
3852        spec = spec.add_data(data);
3853
3854        let result = build_graph(&spec, &[spec.clone()]);
3855        assert!(result.is_err(), "Should detect missing spec");
3856
3857        let errors = result.unwrap_err();
3858        assert!(
3859            errors.iter().any(|e| e.to_string().contains("nonexistent")),
3860            "Error should mention nonexistent spec: {:?}",
3861            errors.iter().map(|e| e.to_string()).collect::<Vec<_>>()
3862        );
3863    }
3864
3865    #[test]
3866    fn test_data_reference_conversion() {
3867        let mut spec = create_test_spec("test");
3868        spec = spec.add_data(create_literal_data(
3869            "age",
3870            Value::Number(rust_decimal::Decimal::from(25)),
3871        ));
3872
3873        let age_expr = ast::Expression {
3874            kind: ast::ExpressionKind::Reference(Reference {
3875                segments: Vec::new(),
3876                name: "age".to_string(),
3877            }),
3878            source_location: Some(test_source()),
3879        };
3880
3881        let rule = LemmaRule {
3882            name: "test_rule".to_string(),
3883            expression: age_expr,
3884            unless_clauses: Vec::new(),
3885            source_location: test_source(),
3886        };
3887        spec = spec.add_rule(rule);
3888
3889        let result = build_graph(&spec, &[spec.clone()]);
3890        assert!(result.is_ok(), "Should build graph successfully");
3891
3892        let graph = result.unwrap();
3893        let rule_node = graph.rules().values().next().unwrap();
3894
3895        assert!(matches!(
3896            rule_node.branches[0].1.kind,
3897            ExpressionKind::DataPath(_)
3898        ));
3899    }
3900
3901    #[test]
3902    fn test_rule_reference_conversion() {
3903        let mut spec = create_test_spec("test");
3904
3905        let rule1_expr = ast::Expression {
3906            kind: ast::ExpressionKind::Reference(Reference {
3907                segments: Vec::new(),
3908                name: "age".to_string(),
3909            }),
3910            source_location: Some(test_source()),
3911        };
3912
3913        let rule1 = LemmaRule {
3914            name: "rule1".to_string(),
3915            expression: rule1_expr,
3916            unless_clauses: Vec::new(),
3917            source_location: test_source(),
3918        };
3919        spec = spec.add_rule(rule1);
3920
3921        let rule2_expr = ast::Expression {
3922            kind: ast::ExpressionKind::Reference(Reference {
3923                segments: Vec::new(),
3924                name: "rule1".to_string(),
3925            }),
3926            source_location: Some(test_source()),
3927        };
3928
3929        let rule2 = LemmaRule {
3930            name: "rule2".to_string(),
3931            expression: rule2_expr,
3932            unless_clauses: Vec::new(),
3933            source_location: test_source(),
3934        };
3935        spec = spec.add_rule(rule2);
3936
3937        spec = spec.add_data(create_literal_data(
3938            "age",
3939            Value::Number(rust_decimal::Decimal::from(25)),
3940        ));
3941
3942        let result = build_graph(&spec, &[spec.clone()]);
3943        assert!(result.is_ok(), "Should build graph successfully");
3944
3945        let graph = result.unwrap();
3946        let rule2_node = graph
3947            .rules()
3948            .get(&RulePath {
3949                segments: Vec::new(),
3950                rule: "rule2".to_string(),
3951            })
3952            .unwrap();
3953
3954        assert_eq!(rule2_node.depends_on_rules.len(), 1);
3955        assert!(matches!(
3956            rule2_node.branches[0].1.kind,
3957            ExpressionKind::RulePath(_)
3958        ));
3959    }
3960
3961    #[test]
3962    fn test_collect_multiple_errors() {
3963        let mut spec = create_test_spec("test");
3964        spec = spec.add_data(create_literal_data(
3965            "age",
3966            Value::Number(rust_decimal::Decimal::from(25)),
3967        ));
3968        spec = spec.add_data(create_literal_data(
3969            "age",
3970            Value::Number(rust_decimal::Decimal::from(30)),
3971        ));
3972
3973        let missing_data_expr = ast::Expression {
3974            kind: ast::ExpressionKind::Reference(Reference {
3975                segments: Vec::new(),
3976                name: "nonexistent".to_string(),
3977            }),
3978            source_location: Some(test_source()),
3979        };
3980
3981        let rule = LemmaRule {
3982            name: "test_rule".to_string(),
3983            expression: missing_data_expr,
3984            unless_clauses: Vec::new(),
3985            source_location: test_source(),
3986        };
3987        spec = spec.add_rule(rule);
3988
3989        let result = build_graph(&spec, &[spec.clone()]);
3990        assert!(result.is_err(), "Should collect multiple errors");
3991
3992        let errors = result.unwrap_err();
3993        assert!(errors.len() >= 2, "Should have at least 2 errors");
3994        assert!(errors
3995            .iter()
3996            .any(|e| e.to_string().contains("Duplicate data")));
3997        assert!(errors
3998            .iter()
3999            .any(|e| e.to_string().contains("Reference 'nonexistent' not found")));
4000    }
4001
4002    #[test]
4003    fn test_type_registration_collects_multiple_errors() {
4004        use crate::parsing::ast::{DataValue, ParentType, PrimitiveKind, SpecRef};
4005
4006        let type_source = Source::new(
4007            "a.lemma",
4008            Span {
4009                start: 0,
4010                end: 0,
4011                line: 1,
4012                col: 0,
4013            },
4014        );
4015        let spec_a = create_test_spec("spec_a")
4016            .with_attribute("a.lemma".to_string())
4017            .add_data(LemmaData {
4018                reference: Reference::local("dep".to_string()),
4019                value: DataValue::SpecReference(SpecRef::local("spec_b")),
4020                source_location: type_source.clone(),
4021            })
4022            .add_data(LemmaData {
4023                reference: Reference::local("money".to_string()),
4024                value: DataValue::TypeDeclaration {
4025                    base: ParentType::Primitive {
4026                        primitive: PrimitiveKind::Number,
4027                    },
4028                    constraints: None,
4029                    from: None,
4030                },
4031                source_location: type_source.clone(),
4032            })
4033            .add_data(LemmaData {
4034                reference: Reference::local("money".to_string()),
4035                value: DataValue::TypeDeclaration {
4036                    base: ParentType::Primitive {
4037                        primitive: PrimitiveKind::Number,
4038                    },
4039                    constraints: None,
4040                    from: None,
4041                },
4042                source_location: type_source,
4043            });
4044
4045        let type_source_b = Source::new(
4046            "b.lemma",
4047            Span {
4048                start: 0,
4049                end: 0,
4050                line: 1,
4051                col: 0,
4052            },
4053        );
4054        let spec_b = create_test_spec("spec_b")
4055            .with_attribute("b.lemma".to_string())
4056            .add_data(LemmaData {
4057                reference: Reference::local("length".to_string()),
4058                value: DataValue::TypeDeclaration {
4059                    base: ParentType::Primitive {
4060                        primitive: PrimitiveKind::Number,
4061                    },
4062                    constraints: None,
4063                    from: None,
4064                },
4065                source_location: type_source_b.clone(),
4066            })
4067            .add_data(LemmaData {
4068                reference: Reference::local("length".to_string()),
4069                value: DataValue::TypeDeclaration {
4070                    base: ParentType::Primitive {
4071                        primitive: PrimitiveKind::Number,
4072                    },
4073                    constraints: None,
4074                    from: None,
4075                },
4076                source_location: type_source_b,
4077            });
4078
4079        let mut sources = HashMap::new();
4080        sources.insert(
4081            "a.lemma".to_string(),
4082            "spec spec_a\nwith dep: spec_b\ndata money: number\ndata money: number".to_string(),
4083        );
4084        sources.insert(
4085            "b.lemma".to_string(),
4086            "spec spec_b\ndata length: number\ndata length: number".to_string(),
4087        );
4088
4089        let result = build_graph(&spec_a, &[spec_a.clone(), spec_b.clone()]);
4090        assert!(
4091            result.is_err(),
4092            "Should fail with duplicate type/data errors"
4093        );
4094    }
4095
4096    // =================================================================
4097    // Versioned spec identifiers: latest-resolution (section 6.3)
4098    // =================================================================
4099
4100    #[test]
4101    fn spec_ref_resolves_to_single_spec_by_name() {
4102        let code = r#"spec myspec
4103data x: 10
4104
4105spec consumer
4106with m: myspec
4107rule result: m.x"#;
4108        let specs = crate::parse(code, "test.lemma", &crate::ResourceLimits::default())
4109            .unwrap()
4110            .specs;
4111        let consumer = specs.iter().find(|d| d.name == "consumer").unwrap();
4112
4113        let graph = build_graph(consumer, &specs).unwrap();
4114        let data_path = DataPath {
4115            segments: vec![PathSegment {
4116                data: "m".to_string(),
4117                spec: "myspec".to_string(),
4118            }],
4119            data: "x".to_string(),
4120        };
4121        assert!(
4122            graph.data.contains_key(&data_path),
4123            "Ref should resolve to myspec. Data: {:?}",
4124            graph.data.keys().collect::<Vec<_>>()
4125        );
4126    }
4127
4128    #[test]
4129    fn spec_ref_to_nonexistent_spec_is_error() {
4130        let code = r#"spec myspec
4131data x: 10
4132
4133spec consumer
4134with m: nonexistent
4135rule result: m.x"#;
4136        let specs = crate::parse(code, "test.lemma", &crate::ResourceLimits::default())
4137            .unwrap()
4138            .specs;
4139        let consumer = specs.iter().find(|d| d.name == "consumer").unwrap();
4140        let result = build_graph(consumer, &specs);
4141        assert!(result.is_err(), "Should fail for non-existent spec");
4142    }
4143
4144    // =================================================================
4145    // Versioned spec identifiers: self-reference check (section 6.4)
4146    // =================================================================
4147
4148    #[test]
4149    fn self_reference_is_error() {
4150        let code = "spec myspec\nwith m: myspec";
4151        let specs = crate::parse(code, "test.lemma", &crate::ResourceLimits::default())
4152            .unwrap()
4153            .specs;
4154        let result = build_graph(&specs[0], &specs);
4155        assert!(result.is_err(), "Self-reference should be an error");
4156        let errors = result.unwrap_err();
4157        assert!(
4158            errors.iter().any(|e| {
4159                let s = e.to_string();
4160                s.contains("cycle") || s.contains("myspec")
4161            }),
4162            "Error should mention cycle or self-referencing spec: {:?}",
4163            errors.iter().map(|e| e.to_string()).collect::<Vec<_>>()
4164        );
4165    }
4166}
4167
4168// ============================================================================
4169// Type resolution (formerly types.rs)
4170// ============================================================================
4171
4172/// Fully resolved types for a single spec.
4173/// After resolution, all imports are inlined — specs are independent.
4174#[derive(Debug, Clone)]
4175pub struct ResolvedSpecTypes {
4176    /// Named types: type_name -> fully resolved type
4177    pub named_types: HashMap<String, LemmaType>,
4178
4179    /// Declared default per named type (e.g. `type rate: ratio -> default 0.5`).
4180    /// Only present for types that declared a `-> default ...` constraint anywhere
4181    /// in their extension chain; the inner-most `-> default` wins. Defaults live
4182    /// outside [`TypeSpecification`] so the type itself stays free of binding data.
4183    pub declared_defaults: HashMap<String, ValueKind>,
4184
4185    /// Unit index: unit_name -> resolved type.
4186    /// Built during resolution — if unit appears in multiple types, resolution fails.
4187    pub unit_index: HashMap<String, LemmaType>,
4188}
4189
4190/// Intermediate type definition extracted from `DataValue::TypeDeclaration` data.
4191/// Replaces the deleted `TypeDef` AST enum for type resolution purposes.
4192#[derive(Debug, Clone, PartialEq)]
4193pub(crate) struct DataTypeDef {
4194    pub parent: ParentType,
4195    pub constraints: Option<Vec<Constraint>>,
4196    pub from: Option<ast::SpecRef>,
4197    pub source: crate::Source,
4198    pub name: String,
4199}
4200
4201/// Resolved spec for a parent type reference (same-spec or cross-spec import).
4202#[derive(Debug, Clone)]
4203pub(crate) struct ResolvedParentSpec {
4204    pub spec: Arc<LemmaSpec>,
4205}
4206
4207/// Per-slice type resolver. Constructed for each `Graph::build` call.
4208///
4209/// Types are extracted from `DataValue::TypeDeclaration` data and keyed by `Arc<LemmaSpec>`.
4210/// The resolver handles cycle detection and accumulates constraints through the inheritance chain.
4211#[derive(Debug, Clone)]
4212pub(crate) struct TypeResolver<'a> {
4213    data_types: HashMap<Arc<LemmaSpec>, HashMap<String, DataTypeDef>>,
4214    context: &'a Context,
4215    all_registered_specs: Vec<Arc<LemmaSpec>>,
4216}
4217
4218impl<'a> TypeResolver<'a> {
4219    pub fn new(context: &'a Context, _dag: &'a [Arc<LemmaSpec>]) -> Self {
4220        TypeResolver {
4221            data_types: HashMap::new(),
4222            context,
4223            all_registered_specs: Vec::new(),
4224        }
4225    }
4226
4227    /// Register all type-declaring data from a spec.
4228    pub fn register_all(&mut self, spec: &Arc<LemmaSpec>) -> Vec<Error> {
4229        if !self
4230            .all_registered_specs
4231            .iter()
4232            .any(|s| Arc::ptr_eq(s, spec))
4233        {
4234            self.all_registered_specs.push(Arc::clone(spec));
4235        }
4236
4237        let mut errors = Vec::new();
4238        for data in &spec.data {
4239            if let ParsedDataValue::TypeDeclaration {
4240                base,
4241                constraints,
4242                from,
4243            } = &data.value
4244            {
4245                let name = &data.reference.name;
4246                let ftd = DataTypeDef {
4247                    parent: base.clone(),
4248                    constraints: constraints.clone(),
4249                    from: from.clone(),
4250                    source: data.source_location.clone(),
4251                    name: name.clone(),
4252                };
4253                if let Err(e) = self.register_type(spec, ftd) {
4254                    errors.push(e);
4255                }
4256            }
4257        }
4258        errors
4259    }
4260
4261    /// Register a type from a data declaration.
4262    pub fn register_type(&mut self, spec: &Arc<LemmaSpec>, def: DataTypeDef) -> Result<(), Error> {
4263        if !self
4264            .all_registered_specs
4265            .iter()
4266            .any(|s| Arc::ptr_eq(s, spec))
4267        {
4268            self.all_registered_specs.push(Arc::clone(spec));
4269        }
4270
4271        let spec_types = self.data_types.entry(Arc::clone(spec)).or_default();
4272        if spec_types.contains_key(&def.name) {
4273            return Err(Error::validation_with_context(
4274                format!(
4275                    "Type '{}' is already defined in spec '{}'",
4276                    def.name, spec.name
4277                ),
4278                Some(def.source.clone()),
4279                None::<String>,
4280                Some(Arc::clone(spec)),
4281                None,
4282            ));
4283        }
4284        spec_types.insert(def.name.clone(), def);
4285        Ok(())
4286    }
4287
4288    /// Resolve types for a single spec and validate their specifications.
4289    /// `at` is the planning instant for this spec (nested qualified refs use their pin).
4290    pub fn resolve_and_validate(
4291        &self,
4292        spec: &Arc<LemmaSpec>,
4293        at: &EffectiveDate,
4294    ) -> Result<ResolvedSpecTypes, Vec<Error>> {
4295        let resolved_types = self.resolve_types_internal(spec, at)?;
4296        let mut errors = Vec::new();
4297
4298        for (type_name, lemma_type) in &resolved_types.named_types {
4299            let source = self
4300                .data_types
4301                .get(spec)
4302                .and_then(|defs| defs.get(type_name))
4303                .map(|ftd| ftd.source.clone())
4304                .unwrap_or_else(|| {
4305                    unreachable!(
4306                        "BUG: resolved type '{}' has no corresponding DataTypeDef in spec '{}'",
4307                        type_name, spec.name
4308                    )
4309                });
4310            let mut spec_errors = validate_type_specifications(
4311                &lemma_type.specifications,
4312                resolved_types.declared_defaults.get(type_name),
4313                type_name,
4314                &source,
4315                Some(Arc::clone(spec)),
4316            );
4317            errors.append(&mut spec_errors);
4318        }
4319
4320        if errors.is_empty() {
4321            Ok(resolved_types)
4322        } else {
4323            Err(errors)
4324        }
4325    }
4326
4327    // =========================================================================
4328    // Private resolution methods
4329    // =========================================================================
4330
4331    fn resolve_types_internal(
4332        &self,
4333        spec: &Arc<LemmaSpec>,
4334        at: &EffectiveDate,
4335    ) -> Result<ResolvedSpecTypes, Vec<Error>> {
4336        let mut named_types = HashMap::new();
4337        let mut declared_defaults: HashMap<String, ValueKind> = HashMap::new();
4338        let mut visited = HashSet::new();
4339
4340        if let Some(spec_types) = self.data_types.get(spec) {
4341            for type_name in spec_types.keys() {
4342                match self.resolve_type_internal(spec, type_name, &mut visited, at) {
4343                    Ok(Some((resolved_type, declared_default))) => {
4344                        named_types.insert(type_name.clone(), resolved_type);
4345                        if let Some(dv) = declared_default {
4346                            declared_defaults.insert(type_name.clone(), dv);
4347                        }
4348                    }
4349                    Ok(None) => {
4350                        unreachable!(
4351                            "BUG: registered type '{}' could not be resolved (spec='{}')",
4352                            type_name, spec.name
4353                        );
4354                    }
4355                    Err(es) => return Err(es),
4356                }
4357                visited.clear();
4358            }
4359        }
4360
4361        // Build unit_index with DataTypeDef for conflict detection, then strip to LemmaType.
4362        let mut unit_index_tmp: HashMap<String, (LemmaType, Option<DataTypeDef>)> = HashMap::new();
4363        let mut errors = Vec::new();
4364
4365        let prim_ratio = semantics::primitive_ratio();
4366        for unit in Self::extract_units_from_type(&prim_ratio.specifications) {
4367            unit_index_tmp.insert(unit, (prim_ratio.clone(), None));
4368        }
4369
4370        for (type_name, resolved_type) in &named_types {
4371            let data_type_def = self
4372                .data_types
4373                .get(spec)
4374                .and_then(|defs| defs.get(type_name.as_str()))
4375                .expect("BUG: type was resolved but not in registry");
4376            let e: Result<(), Error> = if resolved_type.is_scale() {
4377                Self::add_scale_units_to_index(
4378                    spec,
4379                    &mut unit_index_tmp,
4380                    resolved_type,
4381                    data_type_def,
4382                )
4383            } else if resolved_type.is_ratio() {
4384                Self::add_ratio_units_to_index(
4385                    spec,
4386                    &mut unit_index_tmp,
4387                    resolved_type,
4388                    data_type_def,
4389                )
4390            } else {
4391                Ok(())
4392            };
4393            if let Err(e) = e {
4394                errors.push(e);
4395            }
4396        }
4397
4398        if !errors.is_empty() {
4399            return Err(errors);
4400        }
4401
4402        let unit_index = unit_index_tmp
4403            .into_iter()
4404            .map(|(k, (lt, _))| (k, lt))
4405            .collect();
4406
4407        Ok(ResolvedSpecTypes {
4408            named_types,
4409            declared_defaults,
4410            unit_index,
4411        })
4412    }
4413
4414    fn resolve_type_internal(
4415        &self,
4416        spec: &Arc<LemmaSpec>,
4417        name: &str,
4418        visited: &mut HashSet<String>,
4419        at: &EffectiveDate,
4420    ) -> Result<Option<(LemmaType, Option<ValueKind>)>, Vec<Error>> {
4421        let key = format!("{}::{}", spec.name, name);
4422        if visited.contains(&key) {
4423            let source_location = self
4424                .data_types
4425                .get(spec)
4426                .and_then(|dt| dt.get(name))
4427                .map(|ftd| ftd.source.clone())
4428                .unwrap_or_else(|| {
4429                    unreachable!(
4430                        "BUG: circular dependency detected for type '{}::{}' but type definition not found in registry",
4431                        spec.name, name
4432                    )
4433                });
4434            return Err(vec![Error::validation_with_context(
4435                format!("Circular dependency detected in type resolution: {}", key),
4436                Some(source_location),
4437                None::<String>,
4438                Some(Arc::clone(spec)),
4439                None,
4440            )]);
4441        }
4442        visited.insert(key.clone());
4443
4444        let ftd = match self.data_types.get(spec).and_then(|dt| dt.get(name)) {
4445            Some(def) => def.clone(),
4446            None => {
4447                visited.remove(&key);
4448                return Ok(None);
4449            }
4450        };
4451
4452        let parent = ftd.parent.clone();
4453        let from = ftd.from.clone();
4454        let constraints = ftd.constraints.clone();
4455
4456        let (parent_specs, parent_declared_default) = match self.resolve_parent(
4457            spec,
4458            &parent,
4459            &from,
4460            visited,
4461            &ftd.source,
4462            at,
4463        ) {
4464            Ok(Some(pair)) => pair,
4465            Ok(None) => {
4466                visited.remove(&key);
4467                return Err(vec![Error::validation_with_context(
4468                        format!("Unknown type: '{}'. Type must be defined before use. Valid primitive types are: boolean, scale, number, ratio, text, date, time, duration, percent", parent),
4469                        Some(ftd.source.clone()),
4470                        None::<String>,
4471                        Some(Arc::clone(spec)),
4472                        None,
4473                    )]);
4474            }
4475            Err(es) => {
4476                visited.remove(&key);
4477                return Err(es);
4478            }
4479        };
4480
4481        let mut declared_default = parent_declared_default;
4482        let final_specs = if let Some(constraints) = &constraints {
4483            match apply_constraints_to_spec(
4484                spec,
4485                parent_specs,
4486                constraints,
4487                &ftd.source,
4488                &mut declared_default,
4489            ) {
4490                Ok(specs) => specs,
4491                Err(errors) => {
4492                    visited.remove(&key);
4493                    return Err(errors);
4494                }
4495            }
4496        } else {
4497            parent_specs
4498        };
4499
4500        visited.remove(&key);
4501
4502        let extends = {
4503            let parent_name = parent.to_string();
4504            let parent_spec = match self.get_spec_arc_for_parent(spec, &from, &ftd.source, at) {
4505                Ok(x) => x,
4506                Err(e) => return Err(vec![e]),
4507            };
4508            let family = match &parent_spec {
4509                Some(r) => match self.resolve_type_internal(&r.spec, &parent_name, visited, at) {
4510                    Ok(Some((parent_type, _))) => parent_type
4511                        .scale_family_name()
4512                        .map(String::from)
4513                        .unwrap_or_else(|| name.to_string()),
4514                    Ok(None) => name.to_string(),
4515                    Err(es) => return Err(es),
4516                },
4517                None => name.to_string(),
4518            };
4519            let defining_spec = if from.is_some() {
4520                match &parent_spec {
4521                    Some(r) => TypeDefiningSpec::Import {
4522                        spec: Arc::clone(&r.spec),
4523                    },
4524                    None => unreachable!(
4525                        "BUG: from.is_some() but get_spec_arc_for_parent returned Ok(None)"
4526                    ),
4527                }
4528            } else {
4529                TypeDefiningSpec::Local
4530            };
4531            TypeExtends::Custom {
4532                parent: parent_name,
4533                family,
4534                defining_spec,
4535            }
4536        };
4537
4538        Ok(Some((
4539            LemmaType {
4540                name: Some(parent.to_string()),
4541                specifications: final_specs,
4542                extends,
4543            },
4544            declared_default,
4545        )))
4546    }
4547
4548    fn resolve_parent(
4549        &self,
4550        spec: &Arc<LemmaSpec>,
4551        parent: &ParentType,
4552        from: &Option<crate::parsing::ast::SpecRef>,
4553        visited: &mut HashSet<String>,
4554        source: &crate::Source,
4555        at: &EffectiveDate,
4556    ) -> Result<Option<(TypeSpecification, Option<ValueKind>)>, Vec<Error>> {
4557        if let ParentType::Primitive { primitive: kind } = parent {
4558            return Ok(Some((semantics::type_spec_for_primitive(*kind), None)));
4559        }
4560
4561        let parent_name = match parent {
4562            ParentType::Custom { name } => name.as_str(),
4563            ParentType::Primitive { .. } => unreachable!("already returned above"),
4564        };
4565
4566        let parent_spec = match self.get_spec_arc_for_parent(spec, from, source, at) {
4567            Ok(x) => x,
4568            Err(e) => return Err(vec![e]),
4569        };
4570        let result = match &parent_spec {
4571            Some(r) => self.resolve_type_internal(&r.spec, parent_name, visited, at),
4572            None => Ok(None),
4573        };
4574        match result {
4575            Ok(Some((t, declared_default))) => Ok(Some((t.specifications, declared_default))),
4576            Ok(None) => {
4577                let type_exists = parent_spec
4578                    .as_ref()
4579                    .and_then(|r| self.data_types.get(&r.spec))
4580                    .map(|spec_types| spec_types.contains_key(parent_name))
4581                    .unwrap_or(false);
4582
4583                if !type_exists {
4584                    if from.is_none()
4585                        && spec.data.iter().any(|d| {
4586                            d.reference.is_local()
4587                                && d.reference.name == parent_name
4588                                && matches!(&d.value, ParsedDataValue::SpecReference(_))
4589                        })
4590                    {
4591                        return Err(vec![Error::validation_with_context(
4592                            format!(
4593                                "'{}' is a spec reference and cannot carry a value: a spec reference is not a type and cannot be referenced from a data declaration",
4594                                parent_name
4595                            ),
4596                            Some(source.clone()),
4597                            Some(format!(
4598                                "To reference data inside the spec, use a dotted path like '{}.<data_name>'",
4599                                parent_name
4600                            )),
4601                            Some(Arc::clone(spec)),
4602                            None,
4603                        )]);
4604                    }
4605                    let suggestion = from.as_ref().filter(|r| r.from_registry).map(|r| {
4606                        format!(
4607                            "Run `lemma get` or `lemma get {}` to fetch this dependency.",
4608                            r.name
4609                        )
4610                    });
4611                    Err(vec![Error::validation_with_context(
4612                        format!("Unknown type: '{}'. Type must be defined before use. Valid primitive types are: boolean, scale, number, ratio, text, date, time, duration, percent", parent),
4613                        Some(source.clone()),
4614                        suggestion,
4615                        Some(Arc::clone(spec)),
4616                        None,
4617                    )])
4618                } else {
4619                    Ok(None)
4620                }
4621            }
4622            Err(es) => Err(es),
4623        }
4624    }
4625
4626    fn get_spec_arc_for_parent(
4627        &self,
4628        spec: &Arc<LemmaSpec>,
4629        from: &Option<crate::parsing::ast::SpecRef>,
4630        import_site: &crate::Source,
4631        at: &EffectiveDate,
4632    ) -> Result<Option<ResolvedParentSpec>, Error> {
4633        match from {
4634            Some(from_ref) => self
4635                .resolve_spec_for_import(spec, from_ref, import_site, at)
4636                .map(|arc| Some(ResolvedParentSpec { spec: arc })),
4637            None => Ok(Some(ResolvedParentSpec {
4638                spec: Arc::clone(spec),
4639            })),
4640        }
4641    }
4642
4643    fn resolve_spec_for_import(
4644        &self,
4645        spec: &Arc<LemmaSpec>,
4646        from: &crate::parsing::ast::SpecRef,
4647        import_site: &crate::Source,
4648        at: &EffectiveDate,
4649    ) -> Result<Arc<LemmaSpec>, Error> {
4650        discovery::resolve_spec_ref(
4651            self.context,
4652            from,
4653            at,
4654            &spec.name,
4655            Some(import_site.clone()),
4656            Some(Arc::clone(spec)),
4657        )
4658    }
4659
4660    // =========================================================================
4661    // Static helpers (no &self)
4662    // =========================================================================
4663
4664    fn add_scale_units_to_index(
4665        spec: &Arc<LemmaSpec>,
4666        unit_index: &mut HashMap<String, (LemmaType, Option<DataTypeDef>)>,
4667        resolved_type: &LemmaType,
4668        defined_by: &DataTypeDef,
4669    ) -> Result<(), Error> {
4670        let units = Self::extract_units_from_type(&resolved_type.specifications);
4671        for unit in units {
4672            if let Some((existing_type, existing_def)) = unit_index.get(&unit) {
4673                let same_type = existing_def.as_ref() == Some(defined_by);
4674
4675                if same_type {
4676                    return Err(Error::validation_with_context(
4677                        format!(
4678                            "Unit '{}' is defined more than once in type '{}'",
4679                            unit, defined_by.name
4680                        ),
4681                        Some(defined_by.source.clone()),
4682                        None::<String>,
4683                        Some(Arc::clone(spec)),
4684                        None,
4685                    ));
4686                }
4687
4688                let existing_name: String = existing_def
4689                    .as_ref()
4690                    .map(|d| d.name.clone())
4691                    .unwrap_or_else(|| existing_type.name());
4692                let current_extends_existing = resolved_type
4693                    .extends
4694                    .parent_name()
4695                    .map(|p| p == existing_name.as_str())
4696                    .unwrap_or(false);
4697                let existing_extends_current = existing_type
4698                    .extends
4699                    .parent_name()
4700                    .map(|p| p == defined_by.name.as_str())
4701                    .unwrap_or(false);
4702
4703                if existing_type.is_scale()
4704                    && (current_extends_existing || existing_extends_current)
4705                {
4706                    if current_extends_existing {
4707                        unit_index.insert(unit, (resolved_type.clone(), Some(defined_by.clone())));
4708                    }
4709                    continue;
4710                }
4711
4712                if existing_type.same_scale_family(resolved_type) {
4713                    continue;
4714                }
4715
4716                return Err(Error::validation_with_context(
4717                    format!(
4718                        "Ambiguous unit '{}'. Defined in multiple types: '{}' and '{}'",
4719                        unit, existing_name, defined_by.name
4720                    ),
4721                    Some(defined_by.source.clone()),
4722                    None::<String>,
4723                    Some(Arc::clone(spec)),
4724                    None,
4725                ));
4726            }
4727            unit_index.insert(unit, (resolved_type.clone(), Some(defined_by.clone())));
4728        }
4729        Ok(())
4730    }
4731
4732    fn add_ratio_units_to_index(
4733        spec: &Arc<LemmaSpec>,
4734        unit_index: &mut HashMap<String, (LemmaType, Option<DataTypeDef>)>,
4735        resolved_type: &LemmaType,
4736        defined_by: &DataTypeDef,
4737    ) -> Result<(), Error> {
4738        let units = Self::extract_units_from_type(&resolved_type.specifications);
4739        for unit in units {
4740            if let Some((existing_type, existing_def)) = unit_index.get(&unit) {
4741                if existing_type.is_ratio() {
4742                    continue;
4743                }
4744                let existing_name: String = existing_def
4745                    .as_ref()
4746                    .map(|d| d.name.clone())
4747                    .unwrap_or_else(|| existing_type.name());
4748                return Err(Error::validation_with_context(
4749                    format!(
4750                        "Ambiguous unit '{}'. Defined in multiple types: '{}' and '{}'",
4751                        unit, existing_name, defined_by.name
4752                    ),
4753                    Some(defined_by.source.clone()),
4754                    None::<String>,
4755                    Some(Arc::clone(spec)),
4756                    None,
4757                ));
4758            }
4759            unit_index.insert(unit, (resolved_type.clone(), Some(defined_by.clone())));
4760        }
4761        Ok(())
4762    }
4763
4764    fn extract_units_from_type(specs: &TypeSpecification) -> Vec<String> {
4765        match specs {
4766            TypeSpecification::Scale { units, .. } => {
4767                units.iter().map(|unit| unit.name.clone()).collect()
4768            }
4769            TypeSpecification::Ratio { units, .. } => {
4770                units.iter().map(|unit| unit.name.clone()).collect()
4771            }
4772            _ => Vec::new(),
4773        }
4774    }
4775}
4776
4777#[cfg(test)]
4778mod type_resolution_tests {
4779    use super::*;
4780    use crate::parse;
4781    use crate::parsing::ast::{
4782        CommandArg, LemmaSpec, ParentType, PrimitiveKind, TypeConstraintCommand,
4783    };
4784    use crate::ResourceLimits;
4785    use rust_decimal::Decimal;
4786    use std::sync::Arc;
4787
4788    fn test_context_and_effective(
4789        specs: &[Arc<LemmaSpec>],
4790    ) -> (&'static Context, &'static EffectiveDate) {
4791        use crate::engine::Context;
4792        let mut ctx = Context::new();
4793        for s in specs {
4794            ctx.insert_spec(Arc::clone(s), s.from_registry).unwrap();
4795        }
4796        let ctx = Box::leak(Box::new(ctx));
4797        let eff = Box::leak(Box::new(EffectiveDate::Origin));
4798        (ctx, eff)
4799    }
4800
4801    fn dag_and_spec() -> (Vec<Arc<LemmaSpec>>, Arc<LemmaSpec>) {
4802        let spec = LemmaSpec::new("test_spec".to_string());
4803        let arc = Arc::new(spec);
4804        let dag = vec![Arc::clone(&arc)];
4805        (dag, arc)
4806    }
4807
4808    fn resolver_for_code(code: &str) -> (TypeResolver<'static>, Vec<Arc<LemmaSpec>>) {
4809        let specs = parse(code, "test.lemma", &ResourceLimits::default())
4810            .unwrap()
4811            .specs;
4812        let spec_arcs: Vec<Arc<LemmaSpec>> = specs.iter().map(|s| Arc::new(s.clone())).collect();
4813        let dag: Vec<Arc<LemmaSpec>> = spec_arcs.iter().map(Arc::clone).collect();
4814        let dag = Box::leak(Box::new(dag));
4815        let (ctx, _) = test_context_and_effective(&spec_arcs);
4816        let mut resolver = TypeResolver::new(ctx, dag);
4817        for spec_arc in &spec_arcs {
4818            resolver.register_all(spec_arc);
4819        }
4820        (resolver, spec_arcs)
4821    }
4822
4823    fn resolver_single_spec(code: &str) -> (TypeResolver<'static>, Arc<LemmaSpec>) {
4824        let (resolver, spec_arcs) = resolver_for_code(code);
4825        let spec_arc = spec_arcs.into_iter().next().expect("at least one spec");
4826        (resolver, spec_arc)
4827    }
4828
4829    #[test]
4830    fn test_type_spec_for_primitive_covers_all_variants() {
4831        use crate::parsing::ast::PrimitiveKind;
4832        use crate::planning::semantics::type_spec_for_primitive;
4833
4834        for kind in [
4835            PrimitiveKind::Boolean,
4836            PrimitiveKind::Scale,
4837            PrimitiveKind::Number,
4838            PrimitiveKind::Percent,
4839            PrimitiveKind::Ratio,
4840            PrimitiveKind::Text,
4841            PrimitiveKind::Date,
4842            PrimitiveKind::Time,
4843            PrimitiveKind::Duration,
4844        ] {
4845            let spec = type_spec_for_primitive(kind);
4846            assert!(
4847                !matches!(
4848                    spec,
4849                    crate::planning::semantics::TypeSpecification::Undetermined
4850                ),
4851                "type_spec_for_primitive({:?}) returned Undetermined",
4852                kind
4853            );
4854        }
4855    }
4856
4857    #[test]
4858    fn test_register_data_type_def() {
4859        let (dag, spec_arc) = dag_and_spec();
4860        let (ctx, _) = test_context_and_effective(&dag);
4861        let mut resolver = TypeResolver::new(ctx, &dag);
4862        let ftd = DataTypeDef {
4863            parent: ParentType::Primitive {
4864                primitive: PrimitiveKind::Number,
4865            },
4866            constraints: Some(vec![
4867                (
4868                    TypeConstraintCommand::Minimum,
4869                    vec![CommandArg::Literal(crate::literals::Value::Number(
4870                        Decimal::ZERO,
4871                    ))],
4872                ),
4873                (
4874                    TypeConstraintCommand::Maximum,
4875                    vec![CommandArg::Literal(crate::literals::Value::Number(
4876                        Decimal::from(150),
4877                    ))],
4878                ),
4879            ]),
4880            from: None,
4881            source: crate::Source::new(
4882                "<test>",
4883                crate::parsing::ast::Span {
4884                    start: 0,
4885                    end: 0,
4886                    line: 1,
4887                    col: 0,
4888                },
4889            ),
4890            name: "age".to_string(),
4891        };
4892
4893        let result = resolver.register_type(&spec_arc, ftd);
4894        assert!(result.is_ok());
4895        let resolved = resolver
4896            .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
4897            .unwrap();
4898        assert!(resolved.named_types.contains_key("age"));
4899    }
4900
4901    #[test]
4902    fn test_register_duplicate_type_fails() {
4903        let (dag, spec_arc) = dag_and_spec();
4904        let (ctx, _) = test_context_and_effective(&dag);
4905        let mut resolver = TypeResolver::new(ctx, &dag);
4906        let ftd = DataTypeDef {
4907            parent: ParentType::Primitive {
4908                primitive: PrimitiveKind::Number,
4909            },
4910            constraints: None,
4911            from: None,
4912            source: crate::Source::new(
4913                "<test>",
4914                crate::parsing::ast::Span {
4915                    start: 0,
4916                    end: 0,
4917                    line: 1,
4918                    col: 0,
4919                },
4920            ),
4921            name: "money".to_string(),
4922        };
4923
4924        resolver.register_type(&spec_arc, ftd.clone()).unwrap();
4925        let result = resolver.register_type(&spec_arc, ftd);
4926        assert!(result.is_err());
4927    }
4928
4929    #[test]
4930    fn test_resolve_custom_type_from_primitive() {
4931        let (dag, spec_arc) = dag_and_spec();
4932        let (ctx, _) = test_context_and_effective(&dag);
4933        let mut resolver = TypeResolver::new(ctx, &dag);
4934        let ftd = DataTypeDef {
4935            parent: ParentType::Primitive {
4936                primitive: PrimitiveKind::Number,
4937            },
4938            constraints: None,
4939            from: None,
4940            source: crate::Source::new(
4941                "<test>",
4942                crate::parsing::ast::Span {
4943                    start: 0,
4944                    end: 0,
4945                    line: 1,
4946                    col: 0,
4947                },
4948            ),
4949            name: "money".to_string(),
4950        };
4951
4952        resolver.register_type(&spec_arc, ftd).unwrap();
4953        let resolved = resolver
4954            .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
4955            .unwrap();
4956
4957        assert!(resolved.named_types.contains_key("money"));
4958        let money_type = resolved.named_types.get("money").unwrap();
4959        assert_eq!(money_type.name, Some("number".to_string()));
4960    }
4961
4962    #[test]
4963    fn test_type_definition_resolution() {
4964        let (resolver, spec_arc) = resolver_single_spec(
4965            r#"spec test
4966data dice: number -> minimum 0 -> maximum 6"#,
4967        );
4968
4969        let resolved_types = resolver
4970            .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
4971            .unwrap();
4972        let dice_type = resolved_types.named_types.get("dice").unwrap();
4973
4974        match &dice_type.specifications {
4975            TypeSpecification::Number {
4976                minimum, maximum, ..
4977            } => {
4978                assert_eq!(*minimum, Some(Decimal::from(0)));
4979                assert_eq!(*maximum, Some(Decimal::from(6)));
4980            }
4981            _ => panic!("Expected Number type specifications"),
4982        }
4983    }
4984
4985    #[test]
4986    fn test_type_definition_with_multiple_commands() {
4987        let (resolver, spec_arc) = resolver_single_spec(
4988            r#"spec test
4989data money: scale -> decimals 2 -> unit eur 1.0 -> unit usd 1.18"#,
4990        );
4991
4992        let resolved_types = resolver
4993            .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
4994            .unwrap();
4995        let money_type = resolved_types.named_types.get("money").unwrap();
4996
4997        match &money_type.specifications {
4998            TypeSpecification::Scale {
4999                decimals, units, ..
5000            } => {
5001                assert_eq!(*decimals, Some(2));
5002                assert_eq!(units.len(), 2);
5003                assert!(units.iter().any(|u| u.name == "eur"));
5004                assert!(units.iter().any(|u| u.name == "usd"));
5005            }
5006            _ => panic!("Expected Scale type specifications"),
5007        }
5008    }
5009
5010    #[test]
5011    fn test_number_type_with_decimals() {
5012        let (resolver, spec_arc) = resolver_single_spec(
5013            r#"spec test
5014data price: number -> decimals 2 -> minimum 0"#,
5015        );
5016
5017        let resolved_types = resolver
5018            .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
5019            .unwrap();
5020        let price_type = resolved_types.named_types.get("price").unwrap();
5021
5022        match &price_type.specifications {
5023            TypeSpecification::Number {
5024                decimals, minimum, ..
5025            } => {
5026                assert_eq!(*decimals, Some(2));
5027                assert_eq!(*minimum, Some(Decimal::from(0)));
5028            }
5029            _ => panic!("Expected Number type specifications with decimals"),
5030        }
5031    }
5032
5033    #[test]
5034    fn test_number_type_decimals_only() {
5035        let (resolver, spec_arc) = resolver_single_spec(
5036            r#"spec test
5037data precise_number: number -> decimals 4"#,
5038        );
5039
5040        let resolved_types = resolver
5041            .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
5042            .unwrap();
5043        let precise_type = resolved_types.named_types.get("precise_number").unwrap();
5044
5045        match &precise_type.specifications {
5046            TypeSpecification::Number { decimals, .. } => {
5047                assert_eq!(*decimals, Some(4));
5048            }
5049            _ => panic!("Expected Number type with decimals 4"),
5050        }
5051    }
5052
5053    #[test]
5054    fn test_scale_type_decimals_only() {
5055        let (resolver, spec_arc) = resolver_single_spec(
5056            r#"spec test
5057data weight: scale -> unit kg 1 -> decimals 3"#,
5058        );
5059
5060        let resolved_types = resolver
5061            .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
5062            .unwrap();
5063        let weight_type = resolved_types.named_types.get("weight").unwrap();
5064
5065        match &weight_type.specifications {
5066            TypeSpecification::Scale { decimals, .. } => {
5067                assert_eq!(*decimals, Some(3));
5068            }
5069            _ => panic!("Expected Scale type with decimals 3"),
5070        }
5071    }
5072
5073    #[test]
5074    fn test_ratio_type_accepts_optional_decimals_command() {
5075        let (resolver, spec_arc) = resolver_single_spec(
5076            r#"spec test
5077data ratio_type: ratio -> decimals 2"#,
5078        );
5079
5080        let resolved_types = resolver
5081            .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
5082            .unwrap();
5083        let ratio_type = resolved_types.named_types.get("ratio_type").unwrap();
5084
5085        match &ratio_type.specifications {
5086            TypeSpecification::Ratio { decimals, .. } => {
5087                assert_eq!(
5088                    *decimals,
5089                    Some(2),
5090                    "ratio type should accept decimals command"
5091                );
5092            }
5093            _ => panic!("Expected Ratio type with decimals 2"),
5094        }
5095    }
5096
5097    #[test]
5098    fn test_ratio_type_with_default_command() {
5099        let (resolver, spec_arc) = resolver_single_spec(
5100            r#"spec test
5101data percentage: ratio -> minimum 0 -> maximum 1 -> default 0.5"#,
5102        );
5103
5104        let resolved_types = resolver
5105            .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
5106            .unwrap();
5107        let percentage_type = resolved_types.named_types.get("percentage").unwrap();
5108
5109        match &percentage_type.specifications {
5110            TypeSpecification::Ratio {
5111                minimum, maximum, ..
5112            } => {
5113                assert_eq!(
5114                    *minimum,
5115                    Some(Decimal::from(0)),
5116                    "ratio type should have minimum 0"
5117                );
5118                assert_eq!(
5119                    *maximum,
5120                    Some(Decimal::from(1)),
5121                    "ratio type should have maximum 1"
5122                );
5123            }
5124            _ => panic!("Expected Ratio type with minimum and maximum"),
5125        }
5126
5127        let declared = resolved_types
5128            .declared_defaults
5129            .get("percentage")
5130            .expect("declared default must be tracked for percentage");
5131        match declared {
5132            ValueKind::Ratio(v, _) => assert_eq!(*v, Decimal::from_i128_with_scale(5, 1)),
5133            other => panic!("expected Ratio declared default, got {:?}", other),
5134        }
5135    }
5136
5137    #[test]
5138    fn test_scale_extension_chain_same_family_units_allowed() {
5139        let (resolver, spec_arc) = resolver_single_spec(
5140            r#"spec test
5141data money: scale -> unit eur 1
5142data money2: money -> unit usd 1.24"#,
5143        );
5144
5145        let result = resolver.resolve_types_internal(&spec_arc, &EffectiveDate::Origin);
5146        assert!(
5147            result.is_ok(),
5148            "Scale extension chain should resolve: {:?}",
5149            result.err()
5150        );
5151
5152        let resolved = result.unwrap();
5153        assert!(
5154            resolved.unit_index.contains_key("eur"),
5155            "eur should be in unit_index"
5156        );
5157        assert!(
5158            resolved.unit_index.contains_key("usd"),
5159            "usd should be in unit_index"
5160        );
5161        let eur_type = resolved.unit_index.get("eur").unwrap();
5162        let usd_type = resolved.unit_index.get("usd").unwrap();
5163        assert_eq!(
5164            eur_type.name.as_deref(),
5165            Some("money"),
5166            "more derived type (money2) should own eur; its parent name is 'money'"
5167        );
5168        assert_eq!(
5169            usd_type.name.as_deref(),
5170            Some("money"),
5171            "usd defined on money2 whose parent is 'money'"
5172        );
5173    }
5174
5175    #[test]
5176    fn test_invalid_parent_type_in_named_type_should_error() {
5177        let (resolver, spec_arc) = resolver_single_spec(
5178            r#"spec test
5179data invalid: nonexistent_type -> minimum 0"#,
5180        );
5181
5182        let result = resolver.resolve_types_internal(&spec_arc, &EffectiveDate::Origin);
5183        assert!(result.is_err(), "Should reject invalid parent type");
5184
5185        let errs = result.unwrap_err();
5186        assert!(!errs.is_empty(), "expected at least one error");
5187        let error_msg = errs[0].to_string();
5188        assert!(
5189            error_msg.contains("Unknown type") && error_msg.contains("nonexistent_type"),
5190            "Error should mention unknown type. Got: {}",
5191            error_msg
5192        );
5193    }
5194
5195    #[test]
5196    fn test_invalid_primitive_type_name_should_error() {
5197        let (resolver, spec_arc) = resolver_single_spec(
5198            r#"spec test
5199data invalid: choice -> option "a""#,
5200        );
5201
5202        let result = resolver.resolve_types_internal(&spec_arc, &EffectiveDate::Origin);
5203        assert!(result.is_err(), "Should reject invalid type base 'choice'");
5204
5205        let errs = result.unwrap_err();
5206        assert!(!errs.is_empty(), "expected at least one error");
5207        let error_msg = errs[0].to_string();
5208        assert!(
5209            error_msg.contains("Unknown type") && error_msg.contains("choice"),
5210            "Error should mention unknown type 'choice'. Got: {}",
5211            error_msg
5212        );
5213    }
5214
5215    #[test]
5216    fn test_unit_constraint_validation_errors_are_reported() {
5217        let (resolver, spec_arc) = resolver_single_spec(
5218            r#"spec test
5219data money: scale
5220  -> unit eur 1.00
5221  -> unit usd 1.19
5222
5223data money2: money
5224  -> unit eur 1.20
5225  -> unit usd 1.21
5226  -> unit gbp 1.30"#,
5227        );
5228
5229        let result = resolver.resolve_types_internal(&spec_arc, &EffectiveDate::Origin);
5230        assert!(
5231            result.is_err(),
5232            "Expected unit constraint conflicts to error"
5233        );
5234
5235        let errs = result.unwrap_err();
5236        assert!(!errs.is_empty(), "expected at least one error");
5237        let error_msg = errs
5238            .iter()
5239            .map(ToString::to_string)
5240            .collect::<Vec<_>>()
5241            .join("; ");
5242        assert!(
5243            error_msg.contains("eur") || error_msg.contains("usd"),
5244            "Error should mention the conflicting units. Got: {}",
5245            error_msg
5246        );
5247    }
5248
5249    #[test]
5250    fn test_spec_level_unit_ambiguity_errors_are_reported() {
5251        let (resolver, spec_arc) = resolver_single_spec(
5252            r#"spec test
5253data money_a: scale
5254  -> unit eur 1.00
5255  -> unit usd 1.19
5256
5257data money_b: scale
5258  -> unit eur 1.00
5259  -> unit usd 1.20
5260
5261data length_a: scale
5262  -> unit meter 1.0
5263
5264data length_b: scale
5265  -> unit meter 1.0"#,
5266        );
5267
5268        let result = resolver.resolve_types_internal(&spec_arc, &EffectiveDate::Origin);
5269        assert!(
5270            result.is_err(),
5271            "Expected ambiguous unit definitions to error"
5272        );
5273
5274        let errs = result.unwrap_err();
5275        assert!(!errs.is_empty(), "expected at least one error");
5276        let error_msg = errs
5277            .iter()
5278            .map(ToString::to_string)
5279            .collect::<Vec<_>>()
5280            .join("; ");
5281        assert!(
5282            error_msg.contains("eur") || error_msg.contains("usd") || error_msg.contains("meter"),
5283            "Error should mention at least one ambiguous unit. Got: {}",
5284            error_msg
5285        );
5286    }
5287
5288    #[test]
5289    fn test_number_type_cannot_have_units() {
5290        let (resolver, spec_arc) = resolver_single_spec(
5291            r#"spec test
5292data price: number
5293  -> unit eur 1.00"#,
5294        );
5295
5296        let result = resolver.resolve_types_internal(&spec_arc, &EffectiveDate::Origin);
5297        assert!(result.is_err(), "Number types must reject unit commands");
5298
5299        let errs = result.unwrap_err();
5300        assert!(!errs.is_empty(), "expected at least one error");
5301        let error_msg = errs[0].to_string();
5302        assert!(
5303            error_msg.contains("unit") && error_msg.contains("number"),
5304            "Error should mention units are invalid on number. Got: {}",
5305            error_msg
5306        );
5307    }
5308
5309    #[test]
5310    fn test_extending_type_inherits_units() {
5311        let (resolver, spec_arc) = resolver_single_spec(
5312            r#"spec test
5313data money: scale
5314  -> unit eur 1.00
5315  -> unit usd 1.19
5316
5317data my_money: money
5318  -> unit gbp 1.30"#,
5319        );
5320
5321        let resolved = resolver
5322            .resolve_types_internal(&spec_arc, &EffectiveDate::Origin)
5323            .unwrap();
5324        let my_money_type = resolved.named_types.get("my_money").unwrap();
5325
5326        match &my_money_type.specifications {
5327            TypeSpecification::Scale { units, .. } => {
5328                assert_eq!(units.len(), 3);
5329                assert!(units.iter().any(|u| u.name == "eur"));
5330                assert!(units.iter().any(|u| u.name == "usd"));
5331                assert!(units.iter().any(|u| u.name == "gbp"));
5332            }
5333            other => panic!("Expected Scale type specifications, got {:?}", other),
5334        }
5335    }
5336
5337    #[test]
5338    fn test_duplicate_unit_in_same_type_is_rejected() {
5339        let (resolver, spec_arc) = resolver_single_spec(
5340            r#"spec test
5341data money: scale
5342  -> unit eur 1.00
5343  -> unit eur 1.19"#,
5344        );
5345
5346        let result = resolver.resolve_types_internal(&spec_arc, &EffectiveDate::Origin);
5347        assert!(
5348            result.is_err(),
5349            "Duplicate units within a type should error"
5350        );
5351
5352        let errs = result.unwrap_err();
5353        assert!(!errs.is_empty(), "expected at least one error");
5354        let error_msg = errs[0].to_string();
5355        assert!(
5356            error_msg.contains("Duplicate unit")
5357                || error_msg.contains("duplicate")
5358                || error_msg.contains("already exists")
5359                || error_msg.contains("eur"),
5360            "Error should mention duplicate unit issue. Got: {}",
5361            error_msg
5362        );
5363    }
5364}
5365
5366// ============================================================================
5367// Validation (formerly validation.rs)
5368// ============================================================================
5369
5370/// Validate that TypeSpecification constraints are internally consistent.
5371///
5372/// Checks range, decimals/precision, length, unit, and option constraints, and
5373/// validates the `declared_default` (when present) against those constraints.
5374/// The default lives outside the type specification (on the data binding or
5375/// typedef entry); callers thread it in explicitly so this function can verify
5376/// consistency without owning the value.
5377///
5378/// Returns a vector of errors (empty if valid).
5379pub fn validate_type_specifications(
5380    specs: &TypeSpecification,
5381    declared_default: Option<&ValueKind>,
5382    type_name: &str,
5383    source: &Source,
5384    spec_context: Option<Arc<LemmaSpec>>,
5385) -> Vec<Error> {
5386    let mut errors = Vec::new();
5387
5388    match specs {
5389        TypeSpecification::Scale {
5390            minimum,
5391            maximum,
5392            decimals,
5393            precision,
5394            units,
5395            ..
5396        } => {
5397            // Validate range consistency
5398            if let (Some(min), Some(max)) = (minimum, maximum) {
5399                if min > max {
5400                    errors.push(Error::validation_with_context(
5401                        format!(
5402                            "Type '{}' has invalid range: minimum {} is greater than maximum {}",
5403                            type_name, min, max
5404                        ),
5405                        Some(source.clone()),
5406                        None::<String>,
5407                        spec_context.clone(),
5408                        None,
5409                    ));
5410                }
5411            }
5412
5413            // Validate decimals range (0-28 is rust_decimal limit)
5414            if let Some(d) = decimals {
5415                if *d > 28 {
5416                    errors.push(Error::validation_with_context(
5417                        format!(
5418                            "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
5419                            type_name, d
5420                        ),
5421                        Some(source.clone()),
5422                        None::<String>,
5423                        spec_context.clone(),
5424                        None,
5425                    ));
5426                }
5427            }
5428
5429            // Validate precision is positive if set
5430            if let Some(prec) = precision {
5431                if *prec <= Decimal::ZERO {
5432                    errors.push(Error::validation_with_context(
5433                        format!(
5434                            "Type '{}' has invalid precision: {}. Must be positive",
5435                            type_name, prec
5436                        ),
5437                        Some(source.clone()),
5438                        None::<String>,
5439                        spec_context.clone(),
5440                        None,
5441                    ));
5442                }
5443            }
5444
5445            if let Some(ValueKind::Scale(def_value, def_unit)) = declared_default {
5446                if !units.iter().any(|u| u.name == *def_unit) {
5447                    errors.push(Error::validation_with_context(
5448                        format!(
5449                            "Type '{}' default unit '{}' is not a valid unit. Valid units: {}",
5450                            type_name,
5451                            def_unit,
5452                            units
5453                                .iter()
5454                                .map(|u| u.name.clone())
5455                                .collect::<Vec<_>>()
5456                                .join(", ")
5457                        ),
5458                        Some(source.clone()),
5459                        None::<String>,
5460                        spec_context.clone(),
5461                        None,
5462                    ));
5463                }
5464                if let Some(min) = minimum {
5465                    if *def_value < *min {
5466                        errors.push(Error::validation_with_context(
5467                            format!(
5468                                "Type '{}' default value {} {} is less than minimum {}",
5469                                type_name, def_value, def_unit, min
5470                            ),
5471                            Some(source.clone()),
5472                            None::<String>,
5473                            spec_context.clone(),
5474                            None,
5475                        ));
5476                    }
5477                }
5478                if let Some(max) = maximum {
5479                    if *def_value > *max {
5480                        errors.push(Error::validation_with_context(
5481                            format!(
5482                                "Type '{}' default value {} {} is greater than maximum {}",
5483                                type_name, def_value, def_unit, max
5484                            ),
5485                            Some(source.clone()),
5486                            None::<String>,
5487                            spec_context.clone(),
5488                            None,
5489                        ));
5490                    }
5491                }
5492            }
5493
5494            // Scale types must have at least one unit (required for parsing and conversion)
5495            if units.is_empty() {
5496                errors.push(Error::validation_with_context(
5497                    format!(
5498                        "Type '{}' is a scale type but has no units. Scale types must define at least one unit (e.g. -> unit eur 1).",
5499                        type_name
5500                    ),
5501                    Some(source.clone()),
5502                    None::<String>,
5503                    spec_context.clone(),
5504                    None,
5505                ));
5506            }
5507
5508            // Validate units (if present)
5509            if !units.is_empty() {
5510                let mut seen_names: Vec<String> = Vec::new();
5511                for unit in units.iter() {
5512                    // Validate unit name is not empty
5513                    if unit.name.trim().is_empty() {
5514                        errors.push(Error::validation_with_context(
5515                            format!(
5516                                "Type '{}' has a unit with empty name. Unit names cannot be empty.",
5517                                type_name
5518                            ),
5519                            Some(source.clone()),
5520                            None::<String>,
5521                            spec_context.clone(),
5522                            None,
5523                        ));
5524                    }
5525
5526                    // Validate unit names are unique within the type (case-insensitive)
5527                    let lower_name = unit.name.to_lowercase();
5528                    if seen_names
5529                        .iter()
5530                        .any(|seen| seen.to_lowercase() == lower_name)
5531                    {
5532                        errors.push(Error::validation_with_context(
5533                            format!("Type '{}' has duplicate unit name '{}' (case-insensitive). Unit names must be unique within a type.", type_name, unit.name),
5534                            Some(source.clone()),
5535                            None::<String>,
5536                            spec_context.clone(),
5537                            None,
5538                        ));
5539                    } else {
5540                        seen_names.push(unit.name.clone());
5541                    }
5542
5543                    // Validate unit values are positive (conversion factors relative to type base of 1)
5544                    if unit.value <= Decimal::ZERO {
5545                        errors.push(Error::validation_with_context(
5546                            format!("Type '{}' has unit '{}' with invalid value {}. Unit values must be positive (conversion factor relative to type base).", type_name, unit.name, unit.value),
5547                            Some(source.clone()),
5548                            None::<String>,
5549                            spec_context.clone(),
5550                            None,
5551                        ));
5552                    }
5553                }
5554            }
5555        }
5556        TypeSpecification::Number {
5557            minimum,
5558            maximum,
5559            decimals,
5560            precision,
5561            ..
5562        } => {
5563            // Validate range consistency
5564            if let (Some(min), Some(max)) = (minimum, maximum) {
5565                if min > max {
5566                    errors.push(Error::validation_with_context(
5567                        format!(
5568                            "Type '{}' has invalid range: minimum {} is greater than maximum {}",
5569                            type_name, min, max
5570                        ),
5571                        Some(source.clone()),
5572                        None::<String>,
5573                        spec_context.clone(),
5574                        None,
5575                    ));
5576                }
5577            }
5578
5579            // Validate decimals range (0-28 is rust_decimal limit)
5580            if let Some(d) = decimals {
5581                if *d > 28 {
5582                    errors.push(Error::validation_with_context(
5583                        format!(
5584                            "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
5585                            type_name, d
5586                        ),
5587                        Some(source.clone()),
5588                        None::<String>,
5589                        spec_context.clone(),
5590                        None,
5591                    ));
5592                }
5593            }
5594
5595            // Validate precision is positive if set
5596            if let Some(prec) = precision {
5597                if *prec <= Decimal::ZERO {
5598                    errors.push(Error::validation_with_context(
5599                        format!(
5600                            "Type '{}' has invalid precision: {}. Must be positive",
5601                            type_name, prec
5602                        ),
5603                        Some(source.clone()),
5604                        None::<String>,
5605                        spec_context.clone(),
5606                        None,
5607                    ));
5608                }
5609            }
5610
5611            if let Some(ValueKind::Number(def)) = declared_default {
5612                if let Some(min) = minimum {
5613                    if *def < *min {
5614                        errors.push(Error::validation_with_context(
5615                            format!(
5616                                "Type '{}' default value {} is less than minimum {}",
5617                                type_name, def, min
5618                            ),
5619                            Some(source.clone()),
5620                            None::<String>,
5621                            spec_context.clone(),
5622                            None,
5623                        ));
5624                    }
5625                }
5626                if let Some(max) = maximum {
5627                    if *def > *max {
5628                        errors.push(Error::validation_with_context(
5629                            format!(
5630                                "Type '{}' default value {} is greater than maximum {}",
5631                                type_name, def, max
5632                            ),
5633                            Some(source.clone()),
5634                            None::<String>,
5635                            spec_context.clone(),
5636                            None,
5637                        ));
5638                    }
5639                }
5640            }
5641            // Note: Number types are dimensionless and cannot have units (validated in apply_constraint)
5642        }
5643
5644        TypeSpecification::Ratio {
5645            minimum,
5646            maximum,
5647            decimals,
5648            units,
5649            ..
5650        } => {
5651            // Validate decimals range (0-28 is rust_decimal limit)
5652            if let Some(d) = decimals {
5653                if *d > 28 {
5654                    errors.push(Error::validation_with_context(
5655                        format!(
5656                            "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
5657                            type_name, d
5658                        ),
5659                        Some(source.clone()),
5660                        None::<String>,
5661                        spec_context.clone(),
5662                        None,
5663                    ));
5664                }
5665            }
5666
5667            // Validate range consistency
5668            if let (Some(min), Some(max)) = (minimum, maximum) {
5669                if min > max {
5670                    errors.push(Error::validation_with_context(
5671                        format!(
5672                            "Type '{}' has invalid range: minimum {} is greater than maximum {}",
5673                            type_name, min, max
5674                        ),
5675                        Some(source.clone()),
5676                        None::<String>,
5677                        spec_context.clone(),
5678                        None,
5679                    ));
5680                }
5681            }
5682
5683            if let Some(ValueKind::Ratio(def, _)) = declared_default {
5684                if let Some(min) = minimum {
5685                    if *def < *min {
5686                        errors.push(Error::validation_with_context(
5687                            format!(
5688                                "Type '{}' default value {} is less than minimum {}",
5689                                type_name, def, min
5690                            ),
5691                            Some(source.clone()),
5692                            None::<String>,
5693                            spec_context.clone(),
5694                            None,
5695                        ));
5696                    }
5697                }
5698                if let Some(max) = maximum {
5699                    if *def > *max {
5700                        errors.push(Error::validation_with_context(
5701                            format!(
5702                                "Type '{}' default value {} is greater than maximum {}",
5703                                type_name, def, max
5704                            ),
5705                            Some(source.clone()),
5706                            None::<String>,
5707                            spec_context.clone(),
5708                            None,
5709                        ));
5710                    }
5711                }
5712            }
5713
5714            // Validate units (if present)
5715            // Types can have zero units (e.g., type ratio: number -> ratio) - this is valid
5716            // Only validate if units are defined
5717            if !units.is_empty() {
5718                let mut seen_names: Vec<String> = Vec::new();
5719                for unit in units.iter() {
5720                    // Validate unit name is not empty
5721                    if unit.name.trim().is_empty() {
5722                        errors.push(Error::validation_with_context(
5723                            format!(
5724                                "Type '{}' has a unit with empty name. Unit names cannot be empty.",
5725                                type_name
5726                            ),
5727                            Some(source.clone()),
5728                            None::<String>,
5729                            spec_context.clone(),
5730                            None,
5731                        ));
5732                    }
5733
5734                    // Validate unit names are unique within the type (case-insensitive)
5735                    let lower_name = unit.name.to_lowercase();
5736                    if seen_names
5737                        .iter()
5738                        .any(|seen| seen.to_lowercase() == lower_name)
5739                    {
5740                        errors.push(Error::validation_with_context(
5741                            format!("Type '{}' has duplicate unit name '{}' (case-insensitive). Unit names must be unique within a type.", type_name, unit.name),
5742                            Some(source.clone()),
5743                            None::<String>,
5744                            spec_context.clone(),
5745                            None,
5746                        ));
5747                    } else {
5748                        seen_names.push(unit.name.clone());
5749                    }
5750
5751                    // Validate unit values are positive (conversion factors relative to type base of 1)
5752                    if unit.value <= Decimal::ZERO {
5753                        errors.push(Error::validation_with_context(
5754                            format!("Type '{}' has unit '{}' with invalid value {}. Unit values must be positive (conversion factor relative to type base).", type_name, unit.name, unit.value),
5755                            Some(source.clone()),
5756                            None::<String>,
5757                            spec_context.clone(),
5758                            None,
5759                        ));
5760                    }
5761                }
5762            }
5763        }
5764
5765        TypeSpecification::Text {
5766            length, options, ..
5767        } => {
5768            if let Some(ValueKind::Text(def)) = declared_default {
5769                let def_len = def.len();
5770
5771                if let Some(len) = length {
5772                    if def_len != *len {
5773                        errors.push(Error::validation_with_context(
5774                            format!("Type '{}' default value length {} does not match required length {}", type_name, def_len, len),
5775                            Some(source.clone()),
5776                            None::<String>,
5777                            spec_context.clone(),
5778                            None,
5779                        ));
5780                    }
5781                }
5782                if !options.is_empty() && !options.contains(def) {
5783                    errors.push(Error::validation_with_context(
5784                        format!(
5785                            "Type '{}' default value '{}' is not in allowed options: {:?}",
5786                            type_name, def, options
5787                        ),
5788                        Some(source.clone()),
5789                        None::<String>,
5790                        spec_context.clone(),
5791                        None,
5792                    ));
5793                }
5794            }
5795        }
5796
5797        TypeSpecification::Date {
5798            minimum,
5799            maximum,
5800            ..
5801        } => {
5802            // Validate range consistency
5803            if let (Some(min), Some(max)) = (minimum, maximum) {
5804                let min_sem = semantics::date_time_to_semantic(min);
5805                let max_sem = semantics::date_time_to_semantic(max);
5806                if semantics::compare_semantic_dates(&min_sem, &max_sem) == Ordering::Greater {
5807                    errors.push(Error::validation_with_context(
5808                        format!(
5809                            "Type '{}' has invalid date range: minimum {} is after maximum {}",
5810                            type_name, min, max
5811                        ),
5812                        Some(source.clone()),
5813                        None::<String>,
5814                        spec_context.clone(),
5815                        None,
5816                    ));
5817                }
5818            }
5819
5820            if let Some(ValueKind::Date(def)) = declared_default {
5821                if let Some(min) = minimum {
5822                    let min_sem = semantics::date_time_to_semantic(min);
5823                    if semantics::compare_semantic_dates(def, &min_sem) == Ordering::Less {
5824                        errors.push(Error::validation_with_context(
5825                            format!(
5826                                "Type '{}' default date {} is before minimum {}",
5827                                type_name, def, min
5828                            ),
5829                            Some(source.clone()),
5830                            None::<String>,
5831                            spec_context.clone(),
5832                            None,
5833                        ));
5834                    }
5835                }
5836                if let Some(max) = maximum {
5837                    let max_sem = semantics::date_time_to_semantic(max);
5838                    if semantics::compare_semantic_dates(def, &max_sem) == Ordering::Greater {
5839                        errors.push(Error::validation_with_context(
5840                            format!(
5841                                "Type '{}' default date {} is after maximum {}",
5842                                type_name, def, max
5843                            ),
5844                            Some(source.clone()),
5845                            None::<String>,
5846                            spec_context.clone(),
5847                            None,
5848                        ));
5849                    }
5850                }
5851            }
5852        }
5853
5854        TypeSpecification::Time {
5855            minimum,
5856            maximum,
5857            ..
5858        } => {
5859            // Validate range consistency
5860            if let (Some(min), Some(max)) = (minimum, maximum) {
5861                let min_sem = semantics::time_to_semantic(min);
5862                let max_sem = semantics::time_to_semantic(max);
5863                if semantics::compare_semantic_times(&min_sem, &max_sem) == Ordering::Greater {
5864                    errors.push(Error::validation_with_context(
5865                        format!(
5866                            "Type '{}' has invalid time range: minimum {} is after maximum {}",
5867                            type_name, min, max
5868                        ),
5869                        Some(source.clone()),
5870                        None::<String>,
5871                        spec_context.clone(),
5872                        None,
5873                    ));
5874                }
5875            }
5876
5877            if let Some(ValueKind::Time(def)) = declared_default {
5878                if let Some(min) = minimum {
5879                    let min_sem = semantics::time_to_semantic(min);
5880                    if semantics::compare_semantic_times(def, &min_sem) == Ordering::Less {
5881                        errors.push(Error::validation_with_context(
5882                            format!(
5883                                "Type '{}' default time {} is before minimum {}",
5884                                type_name, def, min
5885                            ),
5886                            Some(source.clone()),
5887                            None::<String>,
5888                            spec_context.clone(),
5889                            None,
5890                        ));
5891                    }
5892                }
5893                if let Some(max) = maximum {
5894                    let max_sem = semantics::time_to_semantic(max);
5895                    if semantics::compare_semantic_times(def, &max_sem) == Ordering::Greater {
5896                        errors.push(Error::validation_with_context(
5897                            format!(
5898                                "Type '{}' default time {} is after maximum {}",
5899                                type_name, def, max
5900                            ),
5901                            Some(source.clone()),
5902                            None::<String>,
5903                            spec_context.clone(),
5904                            None,
5905                        ));
5906                    }
5907                }
5908            }
5909        }
5910
5911        TypeSpecification::Boolean { .. } | TypeSpecification::Duration { .. } => {
5912            // No constraint validation needed for these types
5913        }
5914        TypeSpecification::Veto { .. } => {
5915            // Veto is not a user-declarable type, so validation should not be called on it
5916            // But if it is, there's nothing to validate
5917        }
5918        TypeSpecification::Undetermined => unreachable!(
5919            "BUG: validate_type_specification_constraints called with Undetermined sentinel type; this type exists only during type inference"
5920        ),
5921    }
5922
5923    errors
5924}
5925
5926/// Validate that a registry spec (`from_registry == true`) does not contain
5927/// bare (non-`@`) references. The registry is responsible for rewriting all
5928/// spec references to use `@`-prefixed names before serving the bundle.
5929///
5930/// Returns a list of bare reference names found, empty if valid.
5931pub fn collect_bare_registry_refs(spec: &LemmaSpec) -> Vec<String> {
5932    if !spec.from_registry {
5933        return Vec::new();
5934    }
5935    let mut bare: Vec<String> = Vec::new();
5936    for data in &spec.data {
5937        match &data.value {
5938            ParsedDataValue::SpecReference(r) if !r.from_registry => {
5939                bare.push(r.name.clone());
5940            }
5941            ParsedDataValue::TypeDeclaration { from: Some(r), .. } if !r.from_registry => {
5942                bare.push(r.name.clone());
5943            }
5944            _ => {}
5945        }
5946    }
5947    bare
5948}
5949
5950#[cfg(test)]
5951mod validation_tests {
5952    use super::*;
5953    use crate::parsing::ast::{CommandArg, TypeConstraintCommand};
5954    use crate::planning::semantics::TypeSpecification;
5955    use rust_decimal::Decimal;
5956
5957    fn test_source() -> Source {
5958        Source::new(
5959            "<test>",
5960            crate::parsing::ast::Span {
5961                start: 0,
5962                end: 0,
5963                line: 1,
5964                col: 0,
5965            },
5966        )
5967    }
5968
5969    fn apply(
5970        specs: TypeSpecification,
5971        command: TypeConstraintCommand,
5972        args: &[CommandArg],
5973    ) -> TypeSpecification {
5974        let mut default = None;
5975        specs.apply_constraint(command, args, &mut default).unwrap()
5976    }
5977
5978    fn number_arg(n: i64) -> CommandArg {
5979        CommandArg::Literal(crate::literals::Value::Number(Decimal::from(n)))
5980    }
5981
5982    fn date_arg(s: &str) -> CommandArg {
5983        let dt = s.parse::<crate::literals::DateTimeValue>().expect("date");
5984        CommandArg::Literal(crate::literals::Value::Date(dt))
5985    }
5986
5987    fn time_arg(s: &str) -> CommandArg {
5988        let t = s.parse::<crate::literals::TimeValue>().expect("time");
5989        CommandArg::Literal(crate::literals::Value::Time(t))
5990    }
5991
5992    #[test]
5993    fn validate_number_minimum_greater_than_maximum() {
5994        let mut specs = TypeSpecification::number();
5995        specs = apply(specs, TypeConstraintCommand::Minimum, &[number_arg(100)]);
5996        specs = apply(specs, TypeConstraintCommand::Maximum, &[number_arg(50)]);
5997
5998        let src = test_source();
5999        let errors = validate_type_specifications(&specs, None, "test", &src, None);
6000        assert_eq!(errors.len(), 1);
6001        assert!(errors[0]
6002            .to_string()
6003            .contains("minimum 100 is greater than maximum 50"));
6004    }
6005
6006    #[test]
6007    fn validate_number_default_below_minimum() {
6008        let specs = TypeSpecification::Number {
6009            minimum: Some(Decimal::from(10)),
6010            maximum: None,
6011            decimals: None,
6012            precision: None,
6013            help: String::new(),
6014        };
6015        let default = ValueKind::Number(Decimal::from(5));
6016
6017        let src = test_source();
6018        let errors = validate_type_specifications(&specs, Some(&default), "test", &src, None);
6019        assert_eq!(errors.len(), 1);
6020        assert!(errors[0]
6021            .to_string()
6022            .contains("default value 5 is less than minimum 10"));
6023    }
6024
6025    #[test]
6026    fn validate_number_default_above_maximum() {
6027        let specs = TypeSpecification::Number {
6028            minimum: None,
6029            maximum: Some(Decimal::from(100)),
6030            decimals: None,
6031            precision: None,
6032            help: String::new(),
6033        };
6034        let default = ValueKind::Number(Decimal::from(150));
6035
6036        let src = test_source();
6037        let errors = validate_type_specifications(&specs, Some(&default), "test", &src, None);
6038        assert_eq!(errors.len(), 1);
6039        assert!(errors[0]
6040            .to_string()
6041            .contains("default value 150 is greater than maximum 100"));
6042    }
6043
6044    #[test]
6045    fn validate_number_default_valid() {
6046        let specs = TypeSpecification::Number {
6047            minimum: Some(Decimal::from(0)),
6048            maximum: Some(Decimal::from(100)),
6049            decimals: None,
6050            precision: None,
6051            help: String::new(),
6052        };
6053        let default = ValueKind::Number(Decimal::from(50));
6054
6055        let src = test_source();
6056        let errors = validate_type_specifications(&specs, Some(&default), "test", &src, None);
6057        assert!(errors.is_empty());
6058    }
6059
6060    #[test]
6061    fn text_minimum_command_is_rejected() {
6062        let specs = TypeSpecification::text();
6063        let res =
6064            specs.apply_constraint(TypeConstraintCommand::Minimum, &[number_arg(5)], &mut None);
6065        assert!(res.is_err());
6066        assert!(res
6067            .unwrap_err()
6068            .contains("Invalid command 'minimum' for text type"));
6069    }
6070
6071    #[test]
6072    fn text_maximum_command_is_rejected() {
6073        let specs = TypeSpecification::text();
6074        let res =
6075            specs.apply_constraint(TypeConstraintCommand::Maximum, &[number_arg(5)], &mut None);
6076        assert!(res.is_err());
6077        assert!(res
6078            .unwrap_err()
6079            .contains("Invalid command 'maximum' for text type"));
6080    }
6081
6082    #[test]
6083    fn validate_text_default_not_in_options() {
6084        let specs = TypeSpecification::Text {
6085            length: None,
6086            options: vec!["red".to_string(), "blue".to_string()],
6087            help: String::new(),
6088        };
6089        let default = ValueKind::Text("green".to_string());
6090
6091        let src = test_source();
6092        let errors = validate_type_specifications(&specs, Some(&default), "test", &src, None);
6093        assert_eq!(errors.len(), 1);
6094        assert!(errors[0]
6095            .to_string()
6096            .contains("default value 'green' is not in allowed options"));
6097    }
6098
6099    #[test]
6100    fn validate_ratio_minimum_greater_than_maximum() {
6101        let specs = TypeSpecification::Ratio {
6102            minimum: Some(Decimal::from(2)),
6103            maximum: Some(Decimal::from(1)),
6104            decimals: None,
6105            units: crate::planning::semantics::RatioUnits::new(),
6106            help: String::new(),
6107        };
6108
6109        let src = test_source();
6110        let errors = validate_type_specifications(&specs, None, "test", &src, None);
6111        assert_eq!(errors.len(), 1);
6112        assert!(errors[0]
6113            .to_string()
6114            .contains("minimum 2 is greater than maximum 1"));
6115    }
6116
6117    #[test]
6118    fn validate_date_minimum_after_maximum() {
6119        let mut specs = TypeSpecification::date();
6120        specs = apply(
6121            specs,
6122            TypeConstraintCommand::Minimum,
6123            &[date_arg("2024-12-31")],
6124        );
6125        specs = apply(
6126            specs,
6127            TypeConstraintCommand::Maximum,
6128            &[date_arg("2024-01-01")],
6129        );
6130
6131        let src = test_source();
6132        let errors = validate_type_specifications(&specs, None, "test", &src, None);
6133        assert_eq!(errors.len(), 1);
6134        assert!(
6135            errors[0].to_string().contains("minimum")
6136                && errors[0].to_string().contains("is after maximum")
6137        );
6138    }
6139
6140    #[test]
6141    fn validate_date_valid_range() {
6142        let mut specs = TypeSpecification::date();
6143        specs = apply(
6144            specs,
6145            TypeConstraintCommand::Minimum,
6146            &[date_arg("2024-01-01")],
6147        );
6148        specs = apply(
6149            specs,
6150            TypeConstraintCommand::Maximum,
6151            &[date_arg("2024-12-31")],
6152        );
6153
6154        let src = test_source();
6155        let errors = validate_type_specifications(&specs, None, "test", &src, None);
6156        assert!(errors.is_empty());
6157    }
6158
6159    #[test]
6160    fn validate_time_minimum_after_maximum() {
6161        let mut specs = TypeSpecification::time();
6162        specs = apply(
6163            specs,
6164            TypeConstraintCommand::Minimum,
6165            &[time_arg("23:00:00")],
6166        );
6167        specs = apply(
6168            specs,
6169            TypeConstraintCommand::Maximum,
6170            &[time_arg("10:00:00")],
6171        );
6172
6173        let src = test_source();
6174        let errors = validate_type_specifications(&specs, None, "test", &src, None);
6175        assert_eq!(errors.len(), 1);
6176        assert!(
6177            errors[0].to_string().contains("minimum")
6178                && errors[0].to_string().contains("is after maximum")
6179        );
6180    }
6181}