Skip to main content

lemma/planning/
validation.rs

1//! Semantic validation for Lemma specs
2//!
3//! Validates spec structure and type declarations
4//! to catch errors early with clear messages.
5
6use crate::parsing::ast::{
7    ComparisonComputation, DateTimeValue, FactValue, LemmaSpec, TimeValue, TypeDef,
8};
9use crate::planning::semantics::{
10    Expression, ExpressionKind, FactData, FactPath, LemmaType, RulePath, SemanticConversionTarget,
11    TypeSpecification, ValueKind,
12};
13use crate::Error;
14use crate::Source;
15use indexmap::IndexMap;
16use rust_decimal::Decimal;
17use std::cmp::Ordering;
18use std::collections::{HashMap, HashSet};
19use std::sync::Arc;
20
21/// Validate that TypeSpecification constraints are internally consistent
22///
23/// This checks:
24/// - minimum <= maximum (for types that support ranges)
25/// - default values satisfy all constraints
26/// - length constraints are consistent (for Text)
27/// - precision/decimals are within valid ranges
28///
29/// Returns a vector of errors (empty if valid)
30pub fn validate_type_specifications(
31    specs: &TypeSpecification,
32    type_name: &str,
33    source: &Source,
34    spec_context: Option<Arc<LemmaSpec>>,
35) -> Vec<Error> {
36    let mut errors = Vec::new();
37
38    match specs {
39        TypeSpecification::Scale {
40            minimum,
41            maximum,
42            decimals,
43            precision,
44            default,
45            units,
46            ..
47        } => {
48            // Validate range consistency
49            if let (Some(min), Some(max)) = (minimum, maximum) {
50                if min > max {
51                    errors.push(Error::validation_with_context(
52                        format!(
53                            "Type '{}' has invalid range: minimum {} is greater than maximum {}",
54                            type_name, min, max
55                        ),
56                        Some(source.clone()),
57                        None::<String>,
58                        spec_context.clone(),
59                        None,
60                    ));
61                }
62            }
63
64            // Validate decimals range (0-28 is rust_decimal limit)
65            if let Some(d) = decimals {
66                if *d > 28 {
67                    errors.push(Error::validation_with_context(
68                        format!(
69                            "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
70                            type_name, d
71                        ),
72                        Some(source.clone()),
73                        None::<String>,
74                        spec_context.clone(),
75                        None,
76                    ));
77                }
78            }
79
80            // Validate precision is positive if set
81            if let Some(prec) = precision {
82                if *prec <= Decimal::ZERO {
83                    errors.push(Error::validation_with_context(
84                        format!(
85                            "Type '{}' has invalid precision: {}. Must be positive",
86                            type_name, prec
87                        ),
88                        Some(source.clone()),
89                        None::<String>,
90                        spec_context.clone(),
91                        None,
92                    ));
93                }
94            }
95
96            // Validate default value constraints
97            if let Some((def_value, def_unit)) = default {
98                // Validate that the default unit exists
99                if !units.iter().any(|u| u.name == *def_unit) {
100                    errors.push(Error::validation_with_context(
101                        format!(
102                            "Type '{}' default unit '{}' is not a valid unit. Valid units: {}",
103                            type_name,
104                            def_unit,
105                            units
106                                .iter()
107                                .map(|u| u.name.clone())
108                                .collect::<Vec<_>>()
109                                .join(", ")
110                        ),
111                        Some(source.clone()),
112                        None::<String>,
113                        spec_context.clone(),
114                        None,
115                    ));
116                }
117                if let Some(min) = minimum {
118                    if *def_value < *min {
119                        errors.push(Error::validation_with_context(
120                            format!(
121                                "Type '{}' default value {} {} is less than minimum {}",
122                                type_name, def_value, def_unit, min
123                            ),
124                            Some(source.clone()),
125                            None::<String>,
126                            spec_context.clone(),
127                            None,
128                        ));
129                    }
130                }
131                if let Some(max) = maximum {
132                    if *def_value > *max {
133                        errors.push(Error::validation_with_context(
134                            format!(
135                                "Type '{}' default value {} {} is greater than maximum {}",
136                                type_name, def_value, def_unit, max
137                            ),
138                            Some(source.clone()),
139                            None::<String>,
140                            spec_context.clone(),
141                            None,
142                        ));
143                    }
144                }
145            }
146
147            // Scale types must have at least one unit (required for parsing and conversion)
148            if units.is_empty() {
149                errors.push(Error::validation_with_context(
150                    format!(
151                        "Type '{}' is a scale type but has no units. Scale types must define at least one unit (e.g. -> unit eur 1).",
152                        type_name
153                    ),
154                    Some(source.clone()),
155                    None::<String>,
156                    spec_context.clone(),
157                    None,
158                ));
159            }
160
161            // Validate units (if present)
162            if !units.is_empty() {
163                let mut seen_names: Vec<String> = Vec::new();
164                for unit in units.iter() {
165                    // Validate unit name is not empty
166                    if unit.name.trim().is_empty() {
167                        errors.push(Error::validation_with_context(
168                            format!(
169                                "Type '{}' has a unit with empty name. Unit names cannot be empty.",
170                                type_name
171                            ),
172                            Some(source.clone()),
173                            None::<String>,
174                            spec_context.clone(),
175                            None,
176                        ));
177                    }
178
179                    // Validate unit names are unique within the type (case-insensitive)
180                    let lower_name = unit.name.to_lowercase();
181                    if seen_names
182                        .iter()
183                        .any(|seen| seen.to_lowercase() == lower_name)
184                    {
185                        errors.push(Error::validation_with_context(
186                            format!("Type '{}' has duplicate unit name '{}' (case-insensitive). Unit names must be unique within a type.", type_name, unit.name),
187                            Some(source.clone()),
188                            None::<String>,
189                            spec_context.clone(),
190                            None,
191                        ));
192                    } else {
193                        seen_names.push(unit.name.clone());
194                    }
195
196                    // Validate unit values are positive (conversion factors relative to type base of 1)
197                    if unit.value <= Decimal::ZERO {
198                        errors.push(Error::validation_with_context(
199                            format!("Type '{}' has unit '{}' with invalid value {}. Unit values must be positive (conversion factor relative to type base).", type_name, unit.name, unit.value),
200                            Some(source.clone()),
201                            None::<String>,
202                            spec_context.clone(),
203                            None,
204                        ));
205                    }
206                }
207            }
208        }
209        TypeSpecification::Number {
210            minimum,
211            maximum,
212            decimals,
213            precision,
214            default,
215            ..
216        } => {
217            // Validate range consistency
218            if let (Some(min), Some(max)) = (minimum, maximum) {
219                if min > max {
220                    errors.push(Error::validation_with_context(
221                        format!(
222                            "Type '{}' has invalid range: minimum {} is greater than maximum {}",
223                            type_name, min, max
224                        ),
225                        Some(source.clone()),
226                        None::<String>,
227                        spec_context.clone(),
228                        None,
229                    ));
230                }
231            }
232
233            // Validate decimals range (0-28 is rust_decimal limit)
234            if let Some(d) = decimals {
235                if *d > 28 {
236                    errors.push(Error::validation_with_context(
237                        format!(
238                            "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
239                            type_name, d
240                        ),
241                        Some(source.clone()),
242                        None::<String>,
243                        spec_context.clone(),
244                        None,
245                    ));
246                }
247            }
248
249            // Validate precision is positive if set
250            if let Some(prec) = precision {
251                if *prec <= Decimal::ZERO {
252                    errors.push(Error::validation_with_context(
253                        format!(
254                            "Type '{}' has invalid precision: {}. Must be positive",
255                            type_name, prec
256                        ),
257                        Some(source.clone()),
258                        None::<String>,
259                        spec_context.clone(),
260                        None,
261                    ));
262                }
263            }
264
265            // Validate default value constraints
266            if let Some(def) = default {
267                if let Some(min) = minimum {
268                    if *def < *min {
269                        errors.push(Error::validation_with_context(
270                            format!(
271                                "Type '{}' default value {} is less than minimum {}",
272                                type_name, def, min
273                            ),
274                            Some(source.clone()),
275                            None::<String>,
276                            spec_context.clone(),
277                            None,
278                        ));
279                    }
280                }
281                if let Some(max) = maximum {
282                    if *def > *max {
283                        errors.push(Error::validation_with_context(
284                            format!(
285                                "Type '{}' default value {} is greater than maximum {}",
286                                type_name, def, max
287                            ),
288                            Some(source.clone()),
289                            None::<String>,
290                            spec_context.clone(),
291                            None,
292                        ));
293                    }
294                }
295            }
296            // Note: Number types are dimensionless and cannot have units (validated in apply_constraint)
297        }
298
299        TypeSpecification::Ratio {
300            minimum,
301            maximum,
302            decimals,
303            default,
304            units,
305            ..
306        } => {
307            // Validate decimals range (0-28 is rust_decimal limit)
308            if let Some(d) = decimals {
309                if *d > 28 {
310                    errors.push(Error::validation_with_context(
311                        format!(
312                            "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
313                            type_name, d
314                        ),
315                        Some(source.clone()),
316                        None::<String>,
317                        spec_context.clone(),
318                        None,
319                    ));
320                }
321            }
322
323            // Validate range consistency
324            if let (Some(min), Some(max)) = (minimum, maximum) {
325                if min > max {
326                    errors.push(Error::validation_with_context(
327                        format!(
328                            "Type '{}' has invalid range: minimum {} is greater than maximum {}",
329                            type_name, min, max
330                        ),
331                        Some(source.clone()),
332                        None::<String>,
333                        spec_context.clone(),
334                        None,
335                    ));
336                }
337            }
338
339            // Validate default value constraints
340            if let Some(def) = default {
341                if let Some(min) = minimum {
342                    if *def < *min {
343                        errors.push(Error::validation_with_context(
344                            format!(
345                                "Type '{}' default value {} is less than minimum {}",
346                                type_name, def, min
347                            ),
348                            Some(source.clone()),
349                            None::<String>,
350                            spec_context.clone(),
351                            None,
352                        ));
353                    }
354                }
355                if let Some(max) = maximum {
356                    if *def > *max {
357                        errors.push(Error::validation_with_context(
358                            format!(
359                                "Type '{}' default value {} is greater than maximum {}",
360                                type_name, def, max
361                            ),
362                            Some(source.clone()),
363                            None::<String>,
364                            spec_context.clone(),
365                            None,
366                        ));
367                    }
368                }
369            }
370
371            // Validate units (if present)
372            // Types can have zero units (e.g., type ratio: number -> ratio) - this is valid
373            // Only validate if units are defined
374            if !units.is_empty() {
375                let mut seen_names: Vec<String> = Vec::new();
376                for unit in units.iter() {
377                    // Validate unit name is not empty
378                    if unit.name.trim().is_empty() {
379                        errors.push(Error::validation_with_context(
380                            format!(
381                                "Type '{}' has a unit with empty name. Unit names cannot be empty.",
382                                type_name
383                            ),
384                            Some(source.clone()),
385                            None::<String>,
386                            spec_context.clone(),
387                            None,
388                        ));
389                    }
390
391                    // Validate unit names are unique within the type (case-insensitive)
392                    let lower_name = unit.name.to_lowercase();
393                    if seen_names
394                        .iter()
395                        .any(|seen| seen.to_lowercase() == lower_name)
396                    {
397                        errors.push(Error::validation_with_context(
398                            format!("Type '{}' has duplicate unit name '{}' (case-insensitive). Unit names must be unique within a type.", type_name, unit.name),
399                            Some(source.clone()),
400                            None::<String>,
401                            spec_context.clone(),
402                            None,
403                        ));
404                    } else {
405                        seen_names.push(unit.name.clone());
406                    }
407
408                    // Validate unit values are positive (conversion factors relative to type base of 1)
409                    if unit.value <= Decimal::ZERO {
410                        errors.push(Error::validation_with_context(
411                            format!("Type '{}' has unit '{}' with invalid value {}. Unit values must be positive (conversion factor relative to type base).", type_name, unit.name, unit.value),
412                            Some(source.clone()),
413                            None::<String>,
414                            spec_context.clone(),
415                            None,
416                        ));
417                    }
418                }
419            }
420        }
421
422        TypeSpecification::Text {
423            minimum,
424            maximum,
425            length,
426            options,
427            default,
428            ..
429        } => {
430            // Validate range consistency
431            if let (Some(min), Some(max)) = (minimum, maximum) {
432                if min > max {
433                    errors.push(Error::validation_with_context(
434                        format!("Type '{}' has invalid range: minimum length {} is greater than maximum length {}", type_name, min, max),
435                        Some(source.clone()),
436                        None::<String>,
437                        spec_context.clone(),
438                        None,
439                    ));
440                }
441            }
442
443            // Validate length consistency
444            if let Some(len) = length {
445                if let Some(min) = minimum {
446                    if *len < *min {
447                        errors.push(Error::validation_with_context(
448                            format!("Type '{}' has inconsistent length constraint: length {} is less than minimum {}", type_name, len, min),
449                            Some(source.clone()),
450                            None::<String>,
451                            spec_context.clone(),
452                            None,
453                        ));
454                    }
455                }
456                if let Some(max) = maximum {
457                    if *len > *max {
458                        errors.push(Error::validation_with_context(
459                            format!("Type '{}' has inconsistent length constraint: length {} is greater than maximum {}", type_name, len, max),
460                            Some(source.clone()),
461                            None::<String>,
462                            spec_context.clone(),
463                            None,
464                        ));
465                    }
466                }
467            }
468
469            // Validate default value constraints
470            if let Some(def) = default {
471                let def_len = def.len();
472
473                if let Some(min) = minimum {
474                    if def_len < *min {
475                        errors.push(Error::validation_with_context(
476                            format!(
477                                "Type '{}' default value length {} is less than minimum {}",
478                                type_name, def_len, min
479                            ),
480                            Some(source.clone()),
481                            None::<String>,
482                            spec_context.clone(),
483                            None,
484                        ));
485                    }
486                }
487                if let Some(max) = maximum {
488                    if def_len > *max {
489                        errors.push(Error::validation_with_context(
490                            format!(
491                                "Type '{}' default value length {} is greater than maximum {}",
492                                type_name, def_len, max
493                            ),
494                            Some(source.clone()),
495                            None::<String>,
496                            spec_context.clone(),
497                            None,
498                        ));
499                    }
500                }
501                if let Some(len) = length {
502                    if def_len != *len {
503                        errors.push(Error::validation_with_context(
504                            format!("Type '{}' default value length {} does not match required length {}", type_name, def_len, len),
505                            Some(source.clone()),
506                            None::<String>,
507                            spec_context.clone(),
508                            None,
509                        ));
510                    }
511                }
512                if !options.is_empty() && !options.contains(def) {
513                    errors.push(Error::validation_with_context(
514                        format!(
515                            "Type '{}' default value '{}' is not in allowed options: {:?}",
516                            type_name, def, options
517                        ),
518                        Some(source.clone()),
519                        None::<String>,
520                        spec_context.clone(),
521                        None,
522                    ));
523                }
524            }
525        }
526
527        TypeSpecification::Date {
528            minimum,
529            maximum,
530            default,
531            ..
532        } => {
533            // Validate range consistency
534            if let (Some(min), Some(max)) = (minimum, maximum) {
535                if compare_date_values(min, max) == Ordering::Greater {
536                    errors.push(Error::validation_with_context(
537                        format!(
538                            "Type '{}' has invalid date range: minimum {} is after maximum {}",
539                            type_name, min, max
540                        ),
541                        Some(source.clone()),
542                        None::<String>,
543                        spec_context.clone(),
544                        None,
545                    ));
546                }
547            }
548
549            // Validate default value constraints
550            if let Some(def) = default {
551                if let Some(min) = minimum {
552                    if compare_date_values(def, min) == Ordering::Less {
553                        errors.push(Error::validation_with_context(
554                            format!(
555                                "Type '{}' default date {} is before minimum {}",
556                                type_name, def, min
557                            ),
558                            Some(source.clone()),
559                            None::<String>,
560                            spec_context.clone(),
561                            None,
562                        ));
563                    }
564                }
565                if let Some(max) = maximum {
566                    if compare_date_values(def, max) == Ordering::Greater {
567                        errors.push(Error::validation_with_context(
568                            format!(
569                                "Type '{}' default date {} is after maximum {}",
570                                type_name, def, max
571                            ),
572                            Some(source.clone()),
573                            None::<String>,
574                            spec_context.clone(),
575                            None,
576                        ));
577                    }
578                }
579            }
580        }
581
582        TypeSpecification::Time {
583            minimum,
584            maximum,
585            default,
586            ..
587        } => {
588            // Validate range consistency
589            if let (Some(min), Some(max)) = (minimum, maximum) {
590                if compare_time_values(min, max) == Ordering::Greater {
591                    errors.push(Error::validation_with_context(
592                        format!(
593                            "Type '{}' has invalid time range: minimum {} is after maximum {}",
594                            type_name, min, max
595                        ),
596                        Some(source.clone()),
597                        None::<String>,
598                        spec_context.clone(),
599                        None,
600                    ));
601                }
602            }
603
604            // Validate default value constraints
605            if let Some(def) = default {
606                if let Some(min) = minimum {
607                    if compare_time_values(def, min) == Ordering::Less {
608                        errors.push(Error::validation_with_context(
609                            format!(
610                                "Type '{}' default time {} is before minimum {}",
611                                type_name, def, min
612                            ),
613                            Some(source.clone()),
614                            None::<String>,
615                            spec_context.clone(),
616                            None,
617                        ));
618                    }
619                }
620                if let Some(max) = maximum {
621                    if compare_time_values(def, max) == Ordering::Greater {
622                        errors.push(Error::validation_with_context(
623                            format!(
624                                "Type '{}' default time {} is after maximum {}",
625                                type_name, def, max
626                            ),
627                            Some(source.clone()),
628                            None::<String>,
629                            spec_context.clone(),
630                            None,
631                        ));
632                    }
633                }
634            }
635        }
636
637        TypeSpecification::Boolean { .. } | TypeSpecification::Duration { .. } => {
638            // No constraint validation needed for these types
639        }
640        TypeSpecification::Veto { .. } => {
641            // Veto is not a user-declarable type, so validation should not be called on it
642            // But if it is, there's nothing to validate
643        }
644        TypeSpecification::Undetermined => unreachable!(
645            "BUG: validate_type_specification_constraints called with Undetermined sentinel type; this type exists only during type inference"
646        ),
647    }
648
649    errors
650}
651
652/// Compare two DateTimeValue instances for ordering
653fn compare_date_values(left: &DateTimeValue, right: &DateTimeValue) -> Ordering {
654    // Compare by year, month, day, hour, minute, second
655    left.year
656        .cmp(&right.year)
657        .then_with(|| left.month.cmp(&right.month))
658        .then_with(|| left.day.cmp(&right.day))
659        .then_with(|| left.hour.cmp(&right.hour))
660        .then_with(|| left.minute.cmp(&right.minute))
661        .then_with(|| left.second.cmp(&right.second))
662}
663
664/// Compare two TimeValue instances for ordering
665fn compare_time_values(left: &TimeValue, right: &TimeValue) -> Ordering {
666    // Compare by hour, minute, second
667    left.hour
668        .cmp(&right.hour)
669        .then_with(|| left.minute.cmp(&right.minute))
670        .then_with(|| left.second.cmp(&right.second))
671}
672
673// -----------------------------------------------------------------------------
674// Spec interface validation (required rule names + rule result types)
675// -----------------------------------------------------------------------------
676
677/// Rule data needed to validate spec interfaces (inference snapshot before apply).
678pub struct RuleEntryForBindingCheck {
679    pub rule_type: LemmaType,
680    pub depends_on_rules: std::collections::BTreeSet<RulePath>,
681    pub branches: Vec<(Option<Expression>, Expression)>,
682}
683
684#[derive(Clone, Copy, Debug)]
685enum BaseTypeRequirement {
686    Any,
687    Boolean,
688    Number,
689    Duration,
690    Ratio,
691    Scale,
692    Text,
693    Date,
694    Time,
695    Comparable,
696    Numeric,
697}
698
699#[derive(Clone, Debug)]
700struct NumericLiteralConstraint {
701    op: ComparisonComputation,
702    literal: Decimal,
703    reference_on_left: bool,
704}
705
706#[derive(Clone, Debug)]
707enum RuleRefRequirement {
708    Base(BaseTypeRequirement),
709    ScaleMustContainUnit(String),
710    RatioMustContainUnit(String),
711    SameBaseAs(LemmaType),
712    SameScaleFamilyAs(LemmaType),
713    ArithmeticCompatibleWithNumber,
714    ArithmeticCompatibleWithRatio,
715    ArithmeticCompatibleWithScale(LemmaType),
716    ArithmeticCompatibleWithDuration,
717    NumericLiteral(NumericLiteralConstraint),
718}
719
720impl RuleRefRequirement {
721    fn describe(&self) -> String {
722        match self {
723            RuleRefRequirement::Base(kind) => match kind {
724                BaseTypeRequirement::Any => "any".to_string(),
725                BaseTypeRequirement::Boolean => "boolean".to_string(),
726                BaseTypeRequirement::Number => "number".to_string(),
727                BaseTypeRequirement::Duration => "duration".to_string(),
728                BaseTypeRequirement::Ratio => "ratio".to_string(),
729                BaseTypeRequirement::Scale => "scale".to_string(),
730                BaseTypeRequirement::Text => "text".to_string(),
731                BaseTypeRequirement::Date => "date".to_string(),
732                BaseTypeRequirement::Time => "time".to_string(),
733                BaseTypeRequirement::Comparable => "comparable".to_string(),
734                BaseTypeRequirement::Numeric => "numeric (number, scale, or ratio)".to_string(),
735            },
736            RuleRefRequirement::ScaleMustContainUnit(unit) => {
737                format!("scale type containing unit '{}'", unit)
738            }
739            RuleRefRequirement::RatioMustContainUnit(unit) => {
740                format!("ratio type containing unit '{}'", unit)
741            }
742            RuleRefRequirement::SameBaseAs(other) => {
743                format!("same base type as {}", other.name())
744            }
745            RuleRefRequirement::SameScaleFamilyAs(other) => {
746                format!("same scale family as {}", other.name())
747            }
748            RuleRefRequirement::ArithmeticCompatibleWithNumber => {
749                "arithmetic-compatible with number (number or ratio)".to_string()
750            }
751            RuleRefRequirement::ArithmeticCompatibleWithRatio => {
752                "arithmetic-compatible with ratio".to_string()
753            }
754            RuleRefRequirement::ArithmeticCompatibleWithScale(other) => {
755                format!("arithmetic-compatible with scale family {}", other.name())
756            }
757            RuleRefRequirement::ArithmeticCompatibleWithDuration => {
758                "arithmetic-compatible with duration".to_string()
759            }
760            RuleRefRequirement::NumericLiteral(rule) => {
761                let side = if rule.reference_on_left {
762                    "left"
763                } else {
764                    "right"
765                };
766                format!(
767                    "numeric range compatible with comparison (rule-ref {} side, op {:?}, literal {})",
768                    side, rule.op, rule.literal
769                )
770            }
771        }
772    }
773}
774
775fn lemma_type_to_base_requirement(lemma_type: &LemmaType) -> BaseTypeRequirement {
776    if lemma_type.is_boolean() {
777        return BaseTypeRequirement::Boolean;
778    }
779    if lemma_type.is_number() {
780        return BaseTypeRequirement::Number;
781    }
782    if lemma_type.is_scale() {
783        return BaseTypeRequirement::Scale;
784    }
785    if lemma_type.is_duration() {
786        return BaseTypeRequirement::Duration;
787    }
788    if lemma_type.is_ratio() {
789        return BaseTypeRequirement::Ratio;
790    }
791    if lemma_type.is_text() {
792        return BaseTypeRequirement::Text;
793    }
794    if lemma_type.is_date() {
795        return BaseTypeRequirement::Date;
796    }
797    if lemma_type.is_time() {
798        return BaseTypeRequirement::Time;
799    }
800    BaseTypeRequirement::Any
801}
802
803fn base_requirement_satisfied(lemma_type: &LemmaType, constraint: BaseTypeRequirement) -> bool {
804    match constraint {
805        BaseTypeRequirement::Any => true,
806        BaseTypeRequirement::Boolean => lemma_type.is_boolean(),
807        BaseTypeRequirement::Number => lemma_type.is_number(),
808        BaseTypeRequirement::Duration => lemma_type.is_duration(),
809        BaseTypeRequirement::Ratio => lemma_type.is_ratio(),
810        BaseTypeRequirement::Scale => lemma_type.is_scale(),
811        BaseTypeRequirement::Text => lemma_type.is_text(),
812        BaseTypeRequirement::Date => lemma_type.is_date(),
813        BaseTypeRequirement::Time => lemma_type.is_time(),
814        BaseTypeRequirement::Numeric => {
815            lemma_type.is_number() || lemma_type.is_scale() || lemma_type.is_ratio()
816        }
817        BaseTypeRequirement::Comparable => {
818            lemma_type.is_boolean()
819                || lemma_type.is_text()
820                || lemma_type.is_number()
821                || lemma_type.is_ratio()
822                || lemma_type.is_date()
823                || lemma_type.is_time()
824                || lemma_type.is_scale()
825                || lemma_type.is_duration()
826        }
827    }
828}
829
830fn has_scale_unit(lemma_type: &LemmaType, unit: &str) -> bool {
831    match &lemma_type.specifications {
832        TypeSpecification::Scale { units, .. } => {
833            units.iter().any(|u| u.name.eq_ignore_ascii_case(unit))
834        }
835        _ => false,
836    }
837}
838
839fn has_ratio_unit(lemma_type: &LemmaType, unit: &str) -> bool {
840    match &lemma_type.specifications {
841        TypeSpecification::Ratio { units, .. } => {
842            units.iter().any(|u| u.name.eq_ignore_ascii_case(unit))
843        }
844        _ => false,
845    }
846}
847
848fn numeric_bounds(lemma_type: &LemmaType) -> Option<(Option<Decimal>, Option<Decimal>)> {
849    match &lemma_type.specifications {
850        TypeSpecification::Number {
851            minimum, maximum, ..
852        }
853        | TypeSpecification::Scale {
854            minimum, maximum, ..
855        }
856        | TypeSpecification::Ratio {
857            minimum, maximum, ..
858        } => Some((*minimum, *maximum)),
859        _ => None,
860    }
861}
862
863fn normalize_literal_constraint(rule: NumericLiteralConstraint) -> NumericLiteralConstraint {
864    if rule.reference_on_left {
865        return rule;
866    }
867    let op = match rule.op {
868        ComparisonComputation::GreaterThan => ComparisonComputation::LessThan,
869        ComparisonComputation::LessThan => ComparisonComputation::GreaterThan,
870        ComparisonComputation::GreaterThanOrEqual => ComparisonComputation::LessThanOrEqual,
871        ComparisonComputation::LessThanOrEqual => ComparisonComputation::GreaterThanOrEqual,
872        ComparisonComputation::Equal => ComparisonComputation::Equal,
873        ComparisonComputation::NotEqual => ComparisonComputation::NotEqual,
874        ComparisonComputation::Is => ComparisonComputation::Is,
875        ComparisonComputation::IsNot => ComparisonComputation::IsNot,
876    };
877    NumericLiteralConstraint {
878        op,
879        literal: rule.literal,
880        reference_on_left: true,
881    }
882}
883
884fn numeric_literal_constraint_satisfied(
885    lemma_type: &LemmaType,
886    rule: NumericLiteralConstraint,
887) -> bool {
888    let Some((minimum, maximum)) = numeric_bounds(lemma_type) else {
889        return false;
890    };
891    let normalized = normalize_literal_constraint(rule);
892    match normalized.op {
893        ComparisonComputation::GreaterThan => maximum.is_none_or(|max| max > normalized.literal),
894        ComparisonComputation::GreaterThanOrEqual => {
895            maximum.is_none_or(|max| max >= normalized.literal)
896        }
897        ComparisonComputation::LessThan => minimum.is_none_or(|min| min < normalized.literal),
898        ComparisonComputation::LessThanOrEqual => {
899            minimum.is_none_or(|min| min <= normalized.literal)
900        }
901        ComparisonComputation::Equal | ComparisonComputation::Is => {
902            minimum.is_none_or(|min| min <= normalized.literal)
903                && maximum.is_none_or(|max| max >= normalized.literal)
904        }
905        ComparisonComputation::NotEqual | ComparisonComputation::IsNot => {
906            !(minimum == Some(normalized.literal) && maximum == Some(normalized.literal))
907        }
908    }
909}
910
911fn rule_type_satisfies_requirement(
912    lemma_type: &LemmaType,
913    requirement: &RuleRefRequirement,
914) -> bool {
915    if lemma_type.is_undetermined() {
916        unreachable!("BUG: rule_type_satisfies_requirement called with undetermined type; this type exists only during type inference")
917    }
918    // veto is control flow, not a type incompatibility -- it propagates at runtime
919    if lemma_type.vetoed() {
920        return true;
921    }
922    match requirement {
923        RuleRefRequirement::Base(kind) => base_requirement_satisfied(lemma_type, *kind),
924        RuleRefRequirement::ScaleMustContainUnit(unit) => {
925            lemma_type.is_scale() && has_scale_unit(lemma_type, unit)
926        }
927        RuleRefRequirement::RatioMustContainUnit(unit) => {
928            lemma_type.is_ratio() && has_ratio_unit(lemma_type, unit)
929        }
930        RuleRefRequirement::SameBaseAs(other) => lemma_type.has_same_base_type(other),
931        RuleRefRequirement::SameScaleFamilyAs(other) => {
932            lemma_type.is_scale() && other.is_scale() && lemma_type.same_scale_family(other)
933        }
934        RuleRefRequirement::ArithmeticCompatibleWithNumber => {
935            lemma_type.is_number() || lemma_type.is_ratio()
936        }
937        RuleRefRequirement::ArithmeticCompatibleWithRatio => {
938            lemma_type.is_number()
939                || lemma_type.is_ratio()
940                || lemma_type.is_scale()
941                || lemma_type.is_duration()
942        }
943        RuleRefRequirement::ArithmeticCompatibleWithScale(other) => {
944            lemma_type.is_number()
945                || lemma_type.is_ratio()
946                || (lemma_type.is_scale()
947                    && other.is_scale()
948                    && lemma_type.same_scale_family(other))
949        }
950        RuleRefRequirement::ArithmeticCompatibleWithDuration => {
951            lemma_type.is_number() || lemma_type.is_ratio() || lemma_type.is_duration()
952        }
953        RuleRefRequirement::NumericLiteral(rule) => {
954            numeric_literal_constraint_satisfied(lemma_type, rule.clone())
955        }
956    }
957}
958
959fn infer_interface_expression_type(
960    expr: &Expression,
961    rule_entries: &IndexMap<RulePath, RuleEntryForBindingCheck>,
962    facts: &IndexMap<FactPath, FactData>,
963) -> Option<LemmaType> {
964    match &expr.kind {
965        ExpressionKind::Literal(lv) => Some(lv.lemma_type.clone()),
966        ExpressionKind::FactPath(fp) => facts.get(fp).and_then(|f| f.schema_type().cloned()),
967        ExpressionKind::RulePath(rp) => rule_entries.get(rp).map(|r| r.rule_type.clone()),
968        _ => None,
969    }
970}
971
972fn numeric_literal_from_expression(expr: &Expression) -> Option<Decimal> {
973    let ExpressionKind::Literal(lv) = &expr.kind else {
974        return None;
975    };
976    match &lv.value {
977        ValueKind::Number(n) => Some(*n),
978        _ => None,
979    }
980}
981
982fn collect_expected_requirements_for_rule_ref(
983    expr: &Expression,
984    rule_path: &RulePath,
985    expected: RuleRefRequirement,
986    rule_entries: &IndexMap<RulePath, RuleEntryForBindingCheck>,
987    facts: &IndexMap<FactPath, FactData>,
988) -> Vec<(Option<Source>, RuleRefRequirement)> {
989    let mut out = Vec::new();
990    match &expr.kind {
991        ExpressionKind::RulePath(rp) => {
992            if rp == rule_path {
993                out.push((expr.source_location.clone(), expected));
994            }
995        }
996        ExpressionKind::LogicalAnd(left, right) => {
997            out.extend(collect_expected_requirements_for_rule_ref(
998                left,
999                rule_path,
1000                RuleRefRequirement::Base(BaseTypeRequirement::Boolean),
1001                rule_entries,
1002                facts,
1003            ));
1004            out.extend(collect_expected_requirements_for_rule_ref(
1005                right,
1006                rule_path,
1007                RuleRefRequirement::Base(BaseTypeRequirement::Boolean),
1008                rule_entries,
1009                facts,
1010            ));
1011        }
1012        ExpressionKind::LogicalNegation(operand, _) => {
1013            out.extend(collect_expected_requirements_for_rule_ref(
1014                operand,
1015                rule_path,
1016                RuleRefRequirement::Base(BaseTypeRequirement::Boolean),
1017                rule_entries,
1018                facts,
1019            ));
1020        }
1021        ExpressionKind::Comparison(left, op, right) => {
1022            out.extend(collect_expected_requirements_for_rule_ref(
1023                left,
1024                rule_path,
1025                RuleRefRequirement::Base(BaseTypeRequirement::Comparable),
1026                rule_entries,
1027                facts,
1028            ));
1029            out.extend(collect_expected_requirements_for_rule_ref(
1030                right,
1031                rule_path,
1032                RuleRefRequirement::Base(BaseTypeRequirement::Comparable),
1033                rule_entries,
1034                facts,
1035            ));
1036
1037            if let ExpressionKind::RulePath(rp) = &left.kind {
1038                if rp == rule_path {
1039                    if let Some(other_type) =
1040                        infer_interface_expression_type(right, rule_entries, facts)
1041                    {
1042                        out.push((
1043                            expr.source_location.clone(),
1044                            RuleRefRequirement::SameBaseAs(other_type.clone()),
1045                        ));
1046                        if other_type.is_scale() {
1047                            out.push((
1048                                expr.source_location.clone(),
1049                                RuleRefRequirement::SameScaleFamilyAs(other_type),
1050                            ));
1051                        }
1052                    }
1053                    if let Some(lit) = numeric_literal_from_expression(right) {
1054                        out.push((
1055                            expr.source_location.clone(),
1056                            RuleRefRequirement::NumericLiteral(NumericLiteralConstraint {
1057                                op: op.clone(),
1058                                literal: lit,
1059                                reference_on_left: true,
1060                            }),
1061                        ));
1062                    }
1063                }
1064            }
1065            if let ExpressionKind::RulePath(rp) = &right.kind {
1066                if rp == rule_path {
1067                    if let Some(other_type) =
1068                        infer_interface_expression_type(left, rule_entries, facts)
1069                    {
1070                        out.push((
1071                            expr.source_location.clone(),
1072                            RuleRefRequirement::SameBaseAs(other_type.clone()),
1073                        ));
1074                        if other_type.is_scale() {
1075                            out.push((
1076                                expr.source_location.clone(),
1077                                RuleRefRequirement::SameScaleFamilyAs(other_type),
1078                            ));
1079                        }
1080                    }
1081                    if let Some(lit) = numeric_literal_from_expression(left) {
1082                        out.push((
1083                            expr.source_location.clone(),
1084                            RuleRefRequirement::NumericLiteral(NumericLiteralConstraint {
1085                                op: op.clone(),
1086                                literal: lit,
1087                                reference_on_left: false,
1088                            }),
1089                        ));
1090                    }
1091                }
1092            }
1093        }
1094        ExpressionKind::Arithmetic(left, _, right) => {
1095            out.extend(collect_expected_requirements_for_rule_ref(
1096                left,
1097                rule_path,
1098                RuleRefRequirement::Base(BaseTypeRequirement::Numeric),
1099                rule_entries,
1100                facts,
1101            ));
1102            out.extend(collect_expected_requirements_for_rule_ref(
1103                right,
1104                rule_path,
1105                RuleRefRequirement::Base(BaseTypeRequirement::Numeric),
1106                rule_entries,
1107                facts,
1108            ));
1109
1110            if let ExpressionKind::RulePath(rp) = &left.kind {
1111                if rp == rule_path {
1112                    if let Some(other_type) =
1113                        infer_interface_expression_type(right, rule_entries, facts)
1114                    {
1115                        if other_type.is_scale() {
1116                            out.push((
1117                                expr.source_location.clone(),
1118                                RuleRefRequirement::ArithmeticCompatibleWithScale(other_type),
1119                            ));
1120                        } else if other_type.is_number() || other_type.is_ratio() {
1121                            out.push((
1122                                expr.source_location.clone(),
1123                                if other_type.is_number() {
1124                                    RuleRefRequirement::ArithmeticCompatibleWithNumber
1125                                } else {
1126                                    RuleRefRequirement::ArithmeticCompatibleWithRatio
1127                                },
1128                            ));
1129                        } else if other_type.is_duration() {
1130                            out.push((
1131                                expr.source_location.clone(),
1132                                RuleRefRequirement::ArithmeticCompatibleWithDuration,
1133                            ));
1134                        }
1135                    }
1136                }
1137            }
1138            if let ExpressionKind::RulePath(rp) = &right.kind {
1139                if rp == rule_path {
1140                    if let Some(other_type) =
1141                        infer_interface_expression_type(left, rule_entries, facts)
1142                    {
1143                        if other_type.is_scale() {
1144                            out.push((
1145                                expr.source_location.clone(),
1146                                RuleRefRequirement::ArithmeticCompatibleWithScale(other_type),
1147                            ));
1148                        } else if other_type.is_number() || other_type.is_ratio() {
1149                            out.push((
1150                                expr.source_location.clone(),
1151                                if other_type.is_number() {
1152                                    RuleRefRequirement::ArithmeticCompatibleWithNumber
1153                                } else {
1154                                    RuleRefRequirement::ArithmeticCompatibleWithRatio
1155                                },
1156                            ));
1157                        } else if other_type.is_duration() {
1158                            out.push((
1159                                expr.source_location.clone(),
1160                                RuleRefRequirement::ArithmeticCompatibleWithDuration,
1161                            ));
1162                        }
1163                    }
1164                }
1165            }
1166        }
1167        ExpressionKind::UnitConversion(source, target) => {
1168            let constraint = match target {
1169                SemanticConversionTarget::Duration(_) => {
1170                    RuleRefRequirement::Base(BaseTypeRequirement::Duration)
1171                }
1172                SemanticConversionTarget::ScaleUnit(unit) => {
1173                    RuleRefRequirement::ScaleMustContainUnit(unit.clone())
1174                }
1175                SemanticConversionTarget::RatioUnit(unit) => {
1176                    RuleRefRequirement::RatioMustContainUnit(unit.clone())
1177                }
1178            };
1179            out.extend(collect_expected_requirements_for_rule_ref(
1180                source,
1181                rule_path,
1182                constraint,
1183                rule_entries,
1184                facts,
1185            ));
1186        }
1187        ExpressionKind::MathematicalComputation(_, operand) => {
1188            out.extend(collect_expected_requirements_for_rule_ref(
1189                operand,
1190                rule_path,
1191                RuleRefRequirement::Base(BaseTypeRequirement::Number),
1192                rule_entries,
1193                facts,
1194            ));
1195        }
1196        ExpressionKind::DateRelative(_, date_expr, tolerance) => {
1197            out.extend(collect_expected_requirements_for_rule_ref(
1198                date_expr,
1199                rule_path,
1200                RuleRefRequirement::Base(BaseTypeRequirement::Date),
1201                rule_entries,
1202                facts,
1203            ));
1204            if let Some(tol) = tolerance {
1205                out.extend(collect_expected_requirements_for_rule_ref(
1206                    tol,
1207                    rule_path,
1208                    RuleRefRequirement::Base(BaseTypeRequirement::Duration),
1209                    rule_entries,
1210                    facts,
1211                ));
1212            }
1213        }
1214        ExpressionKind::DateCalendar(_, _, date_expr) => {
1215            out.extend(collect_expected_requirements_for_rule_ref(
1216                date_expr,
1217                rule_path,
1218                RuleRefRequirement::Base(BaseTypeRequirement::Date),
1219                rule_entries,
1220                facts,
1221            ));
1222        }
1223        ExpressionKind::Literal(_)
1224        | ExpressionKind::FactPath(_)
1225        | ExpressionKind::Veto(_)
1226        | ExpressionKind::Now => {}
1227    }
1228    out
1229}
1230
1231fn spec_interface_error(
1232    source: &Source,
1233    message: impl Into<String>,
1234    spec_context: Option<Arc<LemmaSpec>>,
1235    related_spec: Option<Arc<LemmaSpec>>,
1236) -> Error {
1237    Error::validation_with_context(
1238        message.into(),
1239        Some(source.clone()),
1240        None::<String>,
1241        spec_context,
1242        related_spec,
1243    )
1244}
1245
1246/// Validate cross-spec IO contracts for spec-reference bindings.
1247///
1248/// Enforces:
1249/// - required referenced rule names exist on the provider spec
1250/// - referenced rule result types satisfy structural requirements implied by
1251///   the consumer expression context (base kind, units, scale family, bounds)
1252///
1253/// This runs at planning time and reports binding-site errors when a provider
1254/// interface is incompatible with consumer expectations.
1255pub fn validate_spec_interfaces(
1256    referenced_rules: &HashMap<Vec<String>, HashSet<String>>,
1257    spec_ref_facts: &[(FactPath, Arc<LemmaSpec>, Source)],
1258    facts: &IndexMap<FactPath, FactData>,
1259    rule_entries: &IndexMap<RulePath, RuleEntryForBindingCheck>,
1260    main_spec: &Arc<LemmaSpec>,
1261) -> Result<(), Vec<Error>> {
1262    let mut errors = Vec::new();
1263
1264    for (fact_path, spec_arc, fact_source) in spec_ref_facts {
1265        let mut full_path: Vec<String> =
1266            fact_path.segments.iter().map(|s| s.fact.clone()).collect();
1267        full_path.push(fact_path.fact.clone());
1268
1269        let Some(required_rules) = referenced_rules.get(&full_path) else {
1270            continue;
1271        };
1272
1273        let spec = spec_arc.as_ref();
1274        let spec_rule_names: HashSet<&str> = spec.rules.iter().map(|r| r.name.as_str()).collect();
1275
1276        for required_rule in required_rules {
1277            if !spec_rule_names.contains(required_rule.as_str()) {
1278                errors.push(spec_interface_error(
1279                    fact_source,
1280                    format!(
1281                        "Spec '{}' referenced by '{}' is missing required rule '{}'",
1282                        spec.name, fact_path, required_rule
1283                    ),
1284                    Some(Arc::clone(main_spec)),
1285                    Some(Arc::clone(spec_arc)),
1286                ));
1287                continue;
1288            }
1289
1290            let mut ref_segments = fact_path.segments.clone();
1291            ref_segments.push(crate::planning::semantics::PathSegment {
1292                fact: fact_path.fact.clone(),
1293                spec: spec.name.clone(),
1294            });
1295            let ref_rule_path = RulePath::new(ref_segments, required_rule.clone());
1296            let Some(ref_entry) = rule_entries.get(&ref_rule_path) else {
1297                let binding_path_str = fact_path
1298                    .segments
1299                    .iter()
1300                    .map(|s| s.fact.as_str())
1301                    .collect::<Vec<_>>()
1302                    .join(".");
1303                let binding_path_str = if binding_path_str.is_empty() {
1304                    fact_path.fact.clone()
1305                } else {
1306                    format!("{}.{}", binding_path_str, fact_path.fact)
1307                };
1308                errors.push(spec_interface_error(
1309                    fact_source,
1310                    format!(
1311                        "Fact binding '{}' sets spec reference to '{}', but interface validation could not resolve rule path '{}.{}' for contract checking",
1312                        binding_path_str, spec.name, fact_path.fact, required_rule
1313                    ),
1314                    Some(Arc::clone(main_spec)),
1315                    Some(Arc::clone(spec_arc)),
1316                ));
1317                continue;
1318            };
1319            let ref_rule_type = &ref_entry.rule_type;
1320
1321            for (_referencing_path, entry) in rule_entries {
1322                if !entry.depends_on_rules.contains(&ref_rule_path) {
1323                    continue;
1324                }
1325                let expected =
1326                    RuleRefRequirement::Base(lemma_type_to_base_requirement(&entry.rule_type));
1327                for (_condition, result_expr) in &entry.branches {
1328                    let requirements = collect_expected_requirements_for_rule_ref(
1329                        result_expr,
1330                        &ref_rule_path,
1331                        expected.clone(),
1332                        rule_entries,
1333                        facts,
1334                    );
1335                    for (_source, requirement) in requirements {
1336                        if !rule_type_satisfies_requirement(ref_rule_type, &requirement) {
1337                            let report_source = fact_source;
1338
1339                            let binding_path_str = fact_path
1340                                .segments
1341                                .iter()
1342                                .map(|s| s.fact.as_str())
1343                                .collect::<Vec<_>>()
1344                                .join(".");
1345                            let binding_path_str = if binding_path_str.is_empty() {
1346                                fact_path.fact.clone()
1347                            } else {
1348                                format!("{}.{}", binding_path_str, fact_path.fact)
1349                            };
1350
1351                            errors.push(spec_interface_error(
1352                                report_source,
1353                                format!(
1354                                    "Fact binding '{}' sets spec reference to '{}', but that spec's rule '{}' has result type {}; the referencing expression expects a {} value",
1355                                    binding_path_str,
1356                                    spec.name,
1357                                    required_rule,
1358                                    ref_rule_type.name(),
1359                                    requirement.describe(),
1360                                ),
1361                                Some(Arc::clone(main_spec)),
1362                                Some(Arc::clone(spec_arc)),
1363                            ));
1364                        }
1365                    }
1366                }
1367            }
1368        }
1369    }
1370
1371    if errors.is_empty() {
1372        Ok(())
1373    } else {
1374        Err(errors)
1375    }
1376}
1377
1378/// Validate that a registry spec (`from_registry == true`) does not contain
1379/// bare (non-`@`) references. The registry is responsible for rewriting all
1380/// spec references to use `@`-prefixed names before serving the bundle.
1381///
1382/// Returns a list of bare reference names found, empty if valid.
1383pub fn collect_bare_registry_refs(spec: &LemmaSpec) -> Vec<String> {
1384    if !spec.from_registry {
1385        return Vec::new();
1386    }
1387    let mut bare: Vec<String> = Vec::new();
1388    for fact in &spec.facts {
1389        match &fact.value {
1390            FactValue::SpecReference(r) if !r.from_registry => {
1391                bare.push(r.name.clone());
1392            }
1393            FactValue::TypeDeclaration { from: Some(r), .. } if !r.from_registry => {
1394                bare.push(r.name.clone());
1395            }
1396            _ => {}
1397        }
1398    }
1399    for type_def in &spec.types {
1400        match type_def {
1401            TypeDef::Import { from, .. } if !from.from_registry => {
1402                bare.push(from.name.clone());
1403            }
1404            TypeDef::Inline { from: Some(r), .. } if !r.from_registry => {
1405                bare.push(r.name.clone());
1406            }
1407            _ => {}
1408        }
1409    }
1410    bare
1411}
1412
1413#[cfg(test)]
1414mod tests {
1415    use super::*;
1416    use crate::parsing::ast::{CommandArg, TypeConstraintCommand};
1417    use crate::planning::semantics::{
1418        LemmaType, RatioUnit, RatioUnits, ScaleUnit, ScaleUnits, TypeSpecification,
1419    };
1420    use rust_decimal::Decimal;
1421
1422    fn test_source() -> Source {
1423        Source::new(
1424            "<test>",
1425            crate::parsing::ast::Span {
1426                start: 0,
1427                end: 0,
1428                line: 1,
1429                col: 0,
1430            },
1431        )
1432    }
1433
1434    #[test]
1435    fn validate_number_minimum_greater_than_maximum() {
1436        let mut specs = TypeSpecification::number();
1437        specs = specs
1438            .apply_constraint(
1439                TypeConstraintCommand::Minimum,
1440                &[CommandArg::Number("100".to_string())],
1441            )
1442            .unwrap();
1443        specs = specs
1444            .apply_constraint(
1445                TypeConstraintCommand::Maximum,
1446                &[CommandArg::Number("50".to_string())],
1447            )
1448            .unwrap();
1449
1450        let src = test_source();
1451        let errors = validate_type_specifications(&specs, "test", &src, None);
1452        assert_eq!(errors.len(), 1);
1453        assert!(errors[0]
1454            .to_string()
1455            .contains("minimum 100 is greater than maximum 50"));
1456    }
1457
1458    #[test]
1459    fn validate_number_valid_range() {
1460        let mut specs = TypeSpecification::number();
1461        specs = specs
1462            .apply_constraint(
1463                TypeConstraintCommand::Minimum,
1464                &[CommandArg::Number("0".to_string())],
1465            )
1466            .unwrap();
1467        specs = specs
1468            .apply_constraint(
1469                TypeConstraintCommand::Maximum,
1470                &[CommandArg::Number("100".to_string())],
1471            )
1472            .unwrap();
1473
1474        let src = test_source();
1475        let errors = validate_type_specifications(&specs, "test", &src, None);
1476        assert!(errors.is_empty());
1477    }
1478
1479    #[test]
1480    fn validate_number_default_below_minimum() {
1481        let specs = TypeSpecification::Number {
1482            minimum: Some(Decimal::from(10)),
1483            maximum: None,
1484            decimals: None,
1485            precision: None,
1486            help: String::new(),
1487            default: Some(Decimal::from(5)),
1488        };
1489
1490        let src = test_source();
1491        let errors = validate_type_specifications(&specs, "test", &src, None);
1492        assert_eq!(errors.len(), 1);
1493        assert!(errors[0]
1494            .to_string()
1495            .contains("default value 5 is less than minimum 10"));
1496    }
1497
1498    #[test]
1499    fn validate_number_default_above_maximum() {
1500        let specs = TypeSpecification::Number {
1501            minimum: None,
1502            maximum: Some(Decimal::from(100)),
1503            decimals: None,
1504            precision: None,
1505            help: String::new(),
1506            default: Some(Decimal::from(150)),
1507        };
1508
1509        let src = test_source();
1510        let errors = validate_type_specifications(&specs, "test", &src, None);
1511        assert_eq!(errors.len(), 1);
1512        assert!(errors[0]
1513            .to_string()
1514            .contains("default value 150 is greater than maximum 100"));
1515    }
1516
1517    #[test]
1518    fn validate_number_default_valid() {
1519        let specs = TypeSpecification::Number {
1520            minimum: Some(Decimal::from(0)),
1521            maximum: Some(Decimal::from(100)),
1522            decimals: None,
1523            precision: None,
1524            help: String::new(),
1525            default: Some(Decimal::from(50)),
1526        };
1527
1528        let src = test_source();
1529        let errors = validate_type_specifications(&specs, "test", &src, None);
1530        assert!(errors.is_empty());
1531    }
1532
1533    #[test]
1534    fn validate_text_minimum_greater_than_maximum() {
1535        let mut specs = TypeSpecification::text();
1536        specs = specs
1537            .apply_constraint(
1538                TypeConstraintCommand::Minimum,
1539                &[CommandArg::Number("100".to_string())],
1540            )
1541            .unwrap();
1542        specs = specs
1543            .apply_constraint(
1544                TypeConstraintCommand::Maximum,
1545                &[CommandArg::Number("50".to_string())],
1546            )
1547            .unwrap();
1548
1549        let src = test_source();
1550        let errors = validate_type_specifications(&specs, "test", &src, None);
1551        assert_eq!(errors.len(), 1);
1552        assert!(errors[0]
1553            .to_string()
1554            .contains("minimum length 100 is greater than maximum length 50"));
1555    }
1556
1557    #[test]
1558    fn validate_text_length_inconsistent_with_minimum() {
1559        let mut specs = TypeSpecification::text();
1560        specs = specs
1561            .apply_constraint(
1562                TypeConstraintCommand::Minimum,
1563                &[CommandArg::Number("10".to_string())],
1564            )
1565            .unwrap();
1566        specs = specs
1567            .apply_constraint(
1568                TypeConstraintCommand::Length,
1569                &[CommandArg::Number("5".to_string())],
1570            )
1571            .unwrap();
1572
1573        let src = test_source();
1574        let errors = validate_type_specifications(&specs, "test", &src, None);
1575        assert_eq!(errors.len(), 1);
1576        assert!(errors[0]
1577            .to_string()
1578            .contains("length 5 is less than minimum 10"));
1579    }
1580
1581    #[test]
1582    fn validate_text_default_not_in_options() {
1583        let specs = TypeSpecification::Text {
1584            minimum: None,
1585            maximum: None,
1586            length: None,
1587            options: vec!["red".to_string(), "blue".to_string()],
1588            help: String::new(),
1589            default: Some("green".to_string()),
1590        };
1591
1592        let src = test_source();
1593        let errors = validate_type_specifications(&specs, "test", &src, None);
1594        assert_eq!(errors.len(), 1);
1595        assert!(errors[0]
1596            .to_string()
1597            .contains("default value 'green' is not in allowed options"));
1598    }
1599
1600    #[test]
1601    fn validate_text_default_valid_in_options() {
1602        let specs = TypeSpecification::Text {
1603            minimum: None,
1604            maximum: None,
1605            length: None,
1606            options: vec!["red".to_string(), "blue".to_string()],
1607            help: String::new(),
1608            default: Some("red".to_string()),
1609        };
1610
1611        let src = test_source();
1612        let errors = validate_type_specifications(&specs, "test", &src, None);
1613        assert!(errors.is_empty());
1614    }
1615
1616    #[test]
1617    fn validate_ratio_minimum_greater_than_maximum() {
1618        let specs = TypeSpecification::Ratio {
1619            minimum: Some(Decimal::from(2)),
1620            maximum: Some(Decimal::from(1)),
1621            decimals: None,
1622            units: crate::planning::semantics::RatioUnits::new(),
1623            help: String::new(),
1624            default: None,
1625        };
1626
1627        let src = test_source();
1628        let errors = validate_type_specifications(&specs, "test", &src, None);
1629        assert_eq!(errors.len(), 1);
1630        assert!(errors[0]
1631            .to_string()
1632            .contains("minimum 2 is greater than maximum 1"));
1633    }
1634
1635    #[test]
1636    fn validate_date_minimum_after_maximum() {
1637        let mut specs = TypeSpecification::date();
1638        specs = specs
1639            .apply_constraint(
1640                TypeConstraintCommand::Minimum,
1641                &[CommandArg::Label("2024-12-31".to_string())],
1642            )
1643            .unwrap();
1644        specs = specs
1645            .apply_constraint(
1646                TypeConstraintCommand::Maximum,
1647                &[CommandArg::Label("2024-01-01".to_string())],
1648            )
1649            .unwrap();
1650
1651        let src = test_source();
1652        let errors = validate_type_specifications(&specs, "test", &src, None);
1653        assert_eq!(errors.len(), 1);
1654        assert!(
1655            errors[0].to_string().contains("minimum")
1656                && errors[0].to_string().contains("is after maximum")
1657        );
1658    }
1659
1660    #[test]
1661    fn validate_date_valid_range() {
1662        let mut specs = TypeSpecification::date();
1663        specs = specs
1664            .apply_constraint(
1665                TypeConstraintCommand::Minimum,
1666                &[CommandArg::Label("2024-01-01".to_string())],
1667            )
1668            .unwrap();
1669        specs = specs
1670            .apply_constraint(
1671                TypeConstraintCommand::Maximum,
1672                &[CommandArg::Label("2024-12-31".to_string())],
1673            )
1674            .unwrap();
1675
1676        let src = test_source();
1677        let errors = validate_type_specifications(&specs, "test", &src, None);
1678        assert!(errors.is_empty());
1679    }
1680
1681    #[test]
1682    fn validate_time_minimum_after_maximum() {
1683        let mut specs = TypeSpecification::time();
1684        specs = specs
1685            .apply_constraint(
1686                TypeConstraintCommand::Minimum,
1687                &[CommandArg::Label("23:00:00".to_string())],
1688            )
1689            .unwrap();
1690        specs = specs
1691            .apply_constraint(
1692                TypeConstraintCommand::Maximum,
1693                &[CommandArg::Label("10:00:00".to_string())],
1694            )
1695            .unwrap();
1696
1697        let src = test_source();
1698        let errors = validate_type_specifications(&specs, "test", &src, None);
1699        assert_eq!(errors.len(), 1);
1700        assert!(
1701            errors[0].to_string().contains("minimum")
1702                && errors[0].to_string().contains("is after maximum")
1703        );
1704    }
1705
1706    #[test]
1707    fn validate_type_definition_with_invalid_constraints() {
1708        // This test now validates that type specification validation works correctly.
1709        // The actual validation happens during graph building, but we test the validation
1710        // function directly here.
1711        use crate::engine::Context;
1712        use crate::parsing::ast::{LemmaSpec, ParentType, PrimitiveKind, TypeDef};
1713        use crate::planning::types::PerSliceTypeResolver;
1714        use std::sync::Arc;
1715
1716        let spec = Arc::new(LemmaSpec::new("test".to_string()));
1717        let mut ctx = Context::new();
1718        ctx.insert_spec(Arc::clone(&spec), false)
1719            .expect("insert test spec");
1720        let type_def = TypeDef::Regular {
1721            source_location: crate::Source::new(
1722                "<test>",
1723                crate::parsing::ast::Span {
1724                    start: 0,
1725                    end: 0,
1726                    line: 1,
1727                    col: 0,
1728                },
1729            ),
1730            name: "invalid_money".to_string(),
1731            parent: ParentType::Primitive {
1732                primitive: PrimitiveKind::Number,
1733            },
1734            constraints: Some(vec![
1735                (
1736                    TypeConstraintCommand::Minimum,
1737                    vec![CommandArg::Number("100".to_string())],
1738                ),
1739                (
1740                    TypeConstraintCommand::Maximum,
1741                    vec![CommandArg::Number("50".to_string())],
1742                ),
1743            ]),
1744        };
1745
1746        let plan_hashes = crate::planning::PlanHashRegistry::default();
1747        let mut type_resolver = PerSliceTypeResolver::new(&ctx, None, &plan_hashes);
1748        type_resolver
1749            .register_type(&spec, type_def)
1750            .expect("Should register type");
1751        let resolved_types = type_resolver
1752            .resolve_named_types(&spec)
1753            .expect("Should resolve types");
1754
1755        // Validate the specifications
1756        let lemma_type = resolved_types
1757            .named_types
1758            .get("invalid_money")
1759            .expect("Should have invalid_money type");
1760        let src = test_source();
1761        let errors =
1762            validate_type_specifications(&lemma_type.specifications, "invalid_money", &src, None);
1763        assert!(!errors.is_empty());
1764        assert!(errors.iter().any(|e| e
1765            .to_string()
1766            .contains("minimum 100 is greater than maximum 50")));
1767    }
1768
1769    fn lt(spec: TypeSpecification) -> LemmaType {
1770        LemmaType::primitive(spec)
1771    }
1772
1773    #[test]
1774    fn interface_requirement_matrix_all_types_base_checks() {
1775        let bool_t = lt(TypeSpecification::boolean());
1776        let num_t = lt(TypeSpecification::number());
1777        let scale_t = lt(TypeSpecification::Scale {
1778            minimum: None,
1779            maximum: None,
1780            decimals: None,
1781            precision: None,
1782            units: ScaleUnits::from(vec![ScaleUnit {
1783                name: "eur".to_string(),
1784                value: Decimal::ONE,
1785            }]),
1786            help: String::new(),
1787            default: None,
1788        });
1789        let ratio_t = lt(TypeSpecification::Ratio {
1790            minimum: None,
1791            maximum: None,
1792            decimals: None,
1793            units: RatioUnits::from(vec![RatioUnit {
1794                name: "percent".to_string(),
1795                value: Decimal::from(100),
1796            }]),
1797            help: String::new(),
1798            default: None,
1799        });
1800        let text_t = lt(TypeSpecification::text());
1801        let date_t = lt(TypeSpecification::date());
1802        let time_t = lt(TypeSpecification::time());
1803        let duration_t = lt(TypeSpecification::duration());
1804        let veto_t = LemmaType::veto_type();
1805        let undetermined_t = LemmaType::undetermined_type();
1806
1807        assert!(rule_type_satisfies_requirement(
1808            &bool_t,
1809            &RuleRefRequirement::Base(BaseTypeRequirement::Boolean)
1810        ));
1811        assert!(rule_type_satisfies_requirement(
1812            &num_t,
1813            &RuleRefRequirement::Base(BaseTypeRequirement::Number)
1814        ));
1815        assert!(rule_type_satisfies_requirement(
1816            &scale_t,
1817            &RuleRefRequirement::Base(BaseTypeRequirement::Scale)
1818        ));
1819        assert!(rule_type_satisfies_requirement(
1820            &ratio_t,
1821            &RuleRefRequirement::Base(BaseTypeRequirement::Ratio)
1822        ));
1823        assert!(rule_type_satisfies_requirement(
1824            &text_t,
1825            &RuleRefRequirement::Base(BaseTypeRequirement::Text)
1826        ));
1827        assert!(rule_type_satisfies_requirement(
1828            &date_t,
1829            &RuleRefRequirement::Base(BaseTypeRequirement::Date)
1830        ));
1831        assert!(rule_type_satisfies_requirement(
1832            &time_t,
1833            &RuleRefRequirement::Base(BaseTypeRequirement::Time)
1834        ));
1835        assert!(rule_type_satisfies_requirement(
1836            &duration_t,
1837            &RuleRefRequirement::Base(BaseTypeRequirement::Duration)
1838        ));
1839
1840        assert!(!rule_type_satisfies_requirement(
1841            &num_t,
1842            &RuleRefRequirement::Base(BaseTypeRequirement::Boolean)
1843        ));
1844        assert!(!rule_type_satisfies_requirement(
1845            &scale_t,
1846            &RuleRefRequirement::Base(BaseTypeRequirement::Number)
1847        ));
1848        // veto is control flow, not type incompatibility -- satisfies any requirement
1849        assert!(rule_type_satisfies_requirement(
1850            &veto_t,
1851            &RuleRefRequirement::Base(BaseTypeRequirement::Any)
1852        ));
1853        assert!(
1854            std::panic::catch_unwind(|| {
1855                rule_type_satisfies_requirement(
1856                    &undetermined_t,
1857                    &RuleRefRequirement::Base(BaseTypeRequirement::Any),
1858                )
1859            })
1860            .is_err(),
1861            "should panic when rule_type_satisfies_requirement is called with undetermined type"
1862        );
1863    }
1864
1865    #[test]
1866    fn interface_requirement_matrix_unit_family_and_bounds_checks() {
1867        let money = LemmaType::new(
1868            "money".to_string(),
1869            TypeSpecification::Scale {
1870                minimum: Some(Decimal::ZERO),
1871                maximum: Some(Decimal::from(100)),
1872                decimals: None,
1873                precision: None,
1874                units: ScaleUnits::from(vec![
1875                    ScaleUnit {
1876                        name: "eur".to_string(),
1877                        value: Decimal::ONE,
1878                    },
1879                    ScaleUnit {
1880                        name: "usd".to_string(),
1881                        value: Decimal::new(11, 1),
1882                    },
1883                ]),
1884                help: String::new(),
1885                default: None,
1886            },
1887            crate::planning::semantics::TypeExtends::Primitive,
1888        );
1889        let weight = LemmaType::new(
1890            "weight".to_string(),
1891            TypeSpecification::Scale {
1892                minimum: None,
1893                maximum: None,
1894                decimals: None,
1895                precision: None,
1896                units: ScaleUnits::from(vec![ScaleUnit {
1897                    name: "kg".to_string(),
1898                    value: Decimal::ONE,
1899                }]),
1900                help: String::new(),
1901                default: None,
1902            },
1903            crate::planning::semantics::TypeExtends::Primitive,
1904        );
1905        let ratio = lt(TypeSpecification::Ratio {
1906            minimum: Some(Decimal::ZERO),
1907            maximum: Some(Decimal::from(100)),
1908            decimals: None,
1909            units: RatioUnits::from(vec![RatioUnit {
1910                name: "percent".to_string(),
1911                value: Decimal::from(100),
1912            }]),
1913            help: String::new(),
1914            default: None,
1915        });
1916        let bounded_number = lt(TypeSpecification::Number {
1917            minimum: Some(Decimal::ZERO),
1918            maximum: Some(Decimal::from(100)),
1919            decimals: None,
1920            precision: None,
1921            help: String::new(),
1922            default: None,
1923        });
1924
1925        assert!(rule_type_satisfies_requirement(
1926            &money,
1927            &RuleRefRequirement::ScaleMustContainUnit("eur".to_string())
1928        ));
1929        assert!(!rule_type_satisfies_requirement(
1930            &money,
1931            &RuleRefRequirement::ScaleMustContainUnit("gbp".to_string())
1932        ));
1933        assert!(rule_type_satisfies_requirement(
1934            &ratio,
1935            &RuleRefRequirement::RatioMustContainUnit("percent".to_string())
1936        ));
1937        assert!(!rule_type_satisfies_requirement(
1938            &ratio,
1939            &RuleRefRequirement::RatioMustContainUnit("permille".to_string())
1940        ));
1941        assert!(rule_type_satisfies_requirement(
1942            &money,
1943            &RuleRefRequirement::SameScaleFamilyAs(money.clone())
1944        ));
1945        assert!(!rule_type_satisfies_requirement(
1946            &money,
1947            &RuleRefRequirement::SameScaleFamilyAs(weight)
1948        ));
1949
1950        assert!(rule_type_satisfies_requirement(
1951            &bounded_number,
1952            &RuleRefRequirement::NumericLiteral(NumericLiteralConstraint {
1953                op: ComparisonComputation::GreaterThan,
1954                literal: Decimal::from(50),
1955                reference_on_left: true,
1956            })
1957        ));
1958        assert!(!rule_type_satisfies_requirement(
1959            &bounded_number,
1960            &RuleRefRequirement::NumericLiteral(NumericLiteralConstraint {
1961                op: ComparisonComputation::GreaterThan,
1962                literal: Decimal::from(500),
1963                reference_on_left: true,
1964            })
1965        ));
1966    }
1967}