Skip to main content

lemma/planning/
graph.rs

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