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::Is => ComparisonComputation::Is,
873        ComparisonComputation::IsNot => ComparisonComputation::IsNot,
874    };
875    NumericLiteralConstraint {
876        op,
877        literal: rule.literal,
878        reference_on_left: true,
879    }
880}
881
882fn numeric_literal_constraint_satisfied(
883    lemma_type: &LemmaType,
884    rule: NumericLiteralConstraint,
885) -> bool {
886    let Some((minimum, maximum)) = numeric_bounds(lemma_type) else {
887        return false;
888    };
889    let normalized = normalize_literal_constraint(rule);
890    match normalized.op {
891        ComparisonComputation::GreaterThan => maximum.is_none_or(|max| max > normalized.literal),
892        ComparisonComputation::GreaterThanOrEqual => {
893            maximum.is_none_or(|max| max >= normalized.literal)
894        }
895        ComparisonComputation::LessThan => minimum.is_none_or(|min| min < normalized.literal),
896        ComparisonComputation::LessThanOrEqual => {
897            minimum.is_none_or(|min| min <= normalized.literal)
898        }
899        ComparisonComputation::Is => {
900            minimum.is_none_or(|min| min <= normalized.literal)
901                && maximum.is_none_or(|max| max >= normalized.literal)
902        }
903        ComparisonComputation::IsNot => {
904            !(minimum == Some(normalized.literal) && maximum == Some(normalized.literal))
905        }
906    }
907}
908
909fn rule_type_satisfies_requirement(
910    lemma_type: &LemmaType,
911    requirement: &RuleRefRequirement,
912) -> bool {
913    if lemma_type.is_undetermined() {
914        unreachable!("BUG: rule_type_satisfies_requirement called with undetermined type; this type exists only during type inference")
915    }
916    // veto is control flow, not a type incompatibility -- it propagates at runtime
917    if lemma_type.vetoed() {
918        return true;
919    }
920    match requirement {
921        RuleRefRequirement::Base(kind) => base_requirement_satisfied(lemma_type, *kind),
922        RuleRefRequirement::ScaleMustContainUnit(unit) => {
923            lemma_type.is_scale() && has_scale_unit(lemma_type, unit)
924        }
925        RuleRefRequirement::RatioMustContainUnit(unit) => {
926            lemma_type.is_ratio() && has_ratio_unit(lemma_type, unit)
927        }
928        RuleRefRequirement::SameBaseAs(other) => lemma_type.has_same_base_type(other),
929        RuleRefRequirement::SameScaleFamilyAs(other) => {
930            lemma_type.is_scale() && other.is_scale() && lemma_type.same_scale_family(other)
931        }
932        RuleRefRequirement::ArithmeticCompatibleWithNumber => {
933            lemma_type.is_number() || lemma_type.is_ratio()
934        }
935        RuleRefRequirement::ArithmeticCompatibleWithRatio => {
936            lemma_type.is_number()
937                || lemma_type.is_ratio()
938                || lemma_type.is_scale()
939                || lemma_type.is_duration()
940        }
941        RuleRefRequirement::ArithmeticCompatibleWithScale(other) => {
942            lemma_type.is_number()
943                || lemma_type.is_ratio()
944                || (lemma_type.is_scale()
945                    && other.is_scale()
946                    && lemma_type.same_scale_family(other))
947        }
948        RuleRefRequirement::ArithmeticCompatibleWithDuration => {
949            lemma_type.is_number() || lemma_type.is_ratio() || lemma_type.is_duration()
950        }
951        RuleRefRequirement::NumericLiteral(rule) => {
952            numeric_literal_constraint_satisfied(lemma_type, rule.clone())
953        }
954    }
955}
956
957fn infer_interface_expression_type(
958    expr: &Expression,
959    rule_entries: &IndexMap<RulePath, RuleEntryForBindingCheck>,
960    facts: &IndexMap<FactPath, FactData>,
961) -> Option<LemmaType> {
962    match &expr.kind {
963        ExpressionKind::Literal(lv) => Some(lv.lemma_type.clone()),
964        ExpressionKind::FactPath(fp) => facts.get(fp).and_then(|f| f.schema_type().cloned()),
965        ExpressionKind::RulePath(rp) => rule_entries.get(rp).map(|r| r.rule_type.clone()),
966        _ => None,
967    }
968}
969
970fn numeric_literal_from_expression(expr: &Expression) -> Option<Decimal> {
971    let ExpressionKind::Literal(lv) = &expr.kind else {
972        return None;
973    };
974    match &lv.value {
975        ValueKind::Number(n) => Some(*n),
976        _ => None,
977    }
978}
979
980fn collect_expected_requirements_for_rule_ref(
981    expr: &Expression,
982    rule_path: &RulePath,
983    expected: RuleRefRequirement,
984    rule_entries: &IndexMap<RulePath, RuleEntryForBindingCheck>,
985    facts: &IndexMap<FactPath, FactData>,
986) -> Vec<(Option<Source>, RuleRefRequirement)> {
987    let mut out = Vec::new();
988    match &expr.kind {
989        ExpressionKind::RulePath(rp) => {
990            if rp == rule_path {
991                out.push((expr.source_location.clone(), expected));
992            }
993        }
994        ExpressionKind::LogicalAnd(left, right) => {
995            out.extend(collect_expected_requirements_for_rule_ref(
996                left,
997                rule_path,
998                RuleRefRequirement::Base(BaseTypeRequirement::Boolean),
999                rule_entries,
1000                facts,
1001            ));
1002            out.extend(collect_expected_requirements_for_rule_ref(
1003                right,
1004                rule_path,
1005                RuleRefRequirement::Base(BaseTypeRequirement::Boolean),
1006                rule_entries,
1007                facts,
1008            ));
1009        }
1010        ExpressionKind::LogicalNegation(operand, _) => {
1011            out.extend(collect_expected_requirements_for_rule_ref(
1012                operand,
1013                rule_path,
1014                RuleRefRequirement::Base(BaseTypeRequirement::Boolean),
1015                rule_entries,
1016                facts,
1017            ));
1018        }
1019        ExpressionKind::Comparison(left, op, right) => {
1020            out.extend(collect_expected_requirements_for_rule_ref(
1021                left,
1022                rule_path,
1023                RuleRefRequirement::Base(BaseTypeRequirement::Comparable),
1024                rule_entries,
1025                facts,
1026            ));
1027            out.extend(collect_expected_requirements_for_rule_ref(
1028                right,
1029                rule_path,
1030                RuleRefRequirement::Base(BaseTypeRequirement::Comparable),
1031                rule_entries,
1032                facts,
1033            ));
1034
1035            if let ExpressionKind::RulePath(rp) = &left.kind {
1036                if rp == rule_path {
1037                    if let Some(other_type) =
1038                        infer_interface_expression_type(right, rule_entries, facts)
1039                    {
1040                        out.push((
1041                            expr.source_location.clone(),
1042                            RuleRefRequirement::SameBaseAs(other_type.clone()),
1043                        ));
1044                        if other_type.is_scale() {
1045                            out.push((
1046                                expr.source_location.clone(),
1047                                RuleRefRequirement::SameScaleFamilyAs(other_type),
1048                            ));
1049                        }
1050                    }
1051                    if let Some(lit) = numeric_literal_from_expression(right) {
1052                        out.push((
1053                            expr.source_location.clone(),
1054                            RuleRefRequirement::NumericLiteral(NumericLiteralConstraint {
1055                                op: op.clone(),
1056                                literal: lit,
1057                                reference_on_left: true,
1058                            }),
1059                        ));
1060                    }
1061                }
1062            }
1063            if let ExpressionKind::RulePath(rp) = &right.kind {
1064                if rp == rule_path {
1065                    if let Some(other_type) =
1066                        infer_interface_expression_type(left, rule_entries, facts)
1067                    {
1068                        out.push((
1069                            expr.source_location.clone(),
1070                            RuleRefRequirement::SameBaseAs(other_type.clone()),
1071                        ));
1072                        if other_type.is_scale() {
1073                            out.push((
1074                                expr.source_location.clone(),
1075                                RuleRefRequirement::SameScaleFamilyAs(other_type),
1076                            ));
1077                        }
1078                    }
1079                    if let Some(lit) = numeric_literal_from_expression(left) {
1080                        out.push((
1081                            expr.source_location.clone(),
1082                            RuleRefRequirement::NumericLiteral(NumericLiteralConstraint {
1083                                op: op.clone(),
1084                                literal: lit,
1085                                reference_on_left: false,
1086                            }),
1087                        ));
1088                    }
1089                }
1090            }
1091        }
1092        ExpressionKind::Arithmetic(left, _, right) => {
1093            out.extend(collect_expected_requirements_for_rule_ref(
1094                left,
1095                rule_path,
1096                RuleRefRequirement::Base(BaseTypeRequirement::Numeric),
1097                rule_entries,
1098                facts,
1099            ));
1100            out.extend(collect_expected_requirements_for_rule_ref(
1101                right,
1102                rule_path,
1103                RuleRefRequirement::Base(BaseTypeRequirement::Numeric),
1104                rule_entries,
1105                facts,
1106            ));
1107
1108            if let ExpressionKind::RulePath(rp) = &left.kind {
1109                if rp == rule_path {
1110                    if let Some(other_type) =
1111                        infer_interface_expression_type(right, rule_entries, facts)
1112                    {
1113                        if other_type.is_scale() {
1114                            out.push((
1115                                expr.source_location.clone(),
1116                                RuleRefRequirement::ArithmeticCompatibleWithScale(other_type),
1117                            ));
1118                        } else if other_type.is_number() || other_type.is_ratio() {
1119                            out.push((
1120                                expr.source_location.clone(),
1121                                if other_type.is_number() {
1122                                    RuleRefRequirement::ArithmeticCompatibleWithNumber
1123                                } else {
1124                                    RuleRefRequirement::ArithmeticCompatibleWithRatio
1125                                },
1126                            ));
1127                        } else if other_type.is_duration() {
1128                            out.push((
1129                                expr.source_location.clone(),
1130                                RuleRefRequirement::ArithmeticCompatibleWithDuration,
1131                            ));
1132                        }
1133                    }
1134                }
1135            }
1136            if let ExpressionKind::RulePath(rp) = &right.kind {
1137                if rp == rule_path {
1138                    if let Some(other_type) =
1139                        infer_interface_expression_type(left, rule_entries, facts)
1140                    {
1141                        if other_type.is_scale() {
1142                            out.push((
1143                                expr.source_location.clone(),
1144                                RuleRefRequirement::ArithmeticCompatibleWithScale(other_type),
1145                            ));
1146                        } else if other_type.is_number() || other_type.is_ratio() {
1147                            out.push((
1148                                expr.source_location.clone(),
1149                                if other_type.is_number() {
1150                                    RuleRefRequirement::ArithmeticCompatibleWithNumber
1151                                } else {
1152                                    RuleRefRequirement::ArithmeticCompatibleWithRatio
1153                                },
1154                            ));
1155                        } else if other_type.is_duration() {
1156                            out.push((
1157                                expr.source_location.clone(),
1158                                RuleRefRequirement::ArithmeticCompatibleWithDuration,
1159                            ));
1160                        }
1161                    }
1162                }
1163            }
1164        }
1165        ExpressionKind::UnitConversion(source, target) => {
1166            let constraint = match target {
1167                SemanticConversionTarget::Duration(_) => {
1168                    RuleRefRequirement::Base(BaseTypeRequirement::Duration)
1169                }
1170                SemanticConversionTarget::ScaleUnit(unit) => {
1171                    RuleRefRequirement::ScaleMustContainUnit(unit.clone())
1172                }
1173                SemanticConversionTarget::RatioUnit(unit) => {
1174                    RuleRefRequirement::RatioMustContainUnit(unit.clone())
1175                }
1176            };
1177            out.extend(collect_expected_requirements_for_rule_ref(
1178                source,
1179                rule_path,
1180                constraint,
1181                rule_entries,
1182                facts,
1183            ));
1184        }
1185        ExpressionKind::MathematicalComputation(_, operand) => {
1186            out.extend(collect_expected_requirements_for_rule_ref(
1187                operand,
1188                rule_path,
1189                RuleRefRequirement::Base(BaseTypeRequirement::Number),
1190                rule_entries,
1191                facts,
1192            ));
1193        }
1194        ExpressionKind::DateRelative(_, date_expr, tolerance) => {
1195            out.extend(collect_expected_requirements_for_rule_ref(
1196                date_expr,
1197                rule_path,
1198                RuleRefRequirement::Base(BaseTypeRequirement::Date),
1199                rule_entries,
1200                facts,
1201            ));
1202            if let Some(tol) = tolerance {
1203                out.extend(collect_expected_requirements_for_rule_ref(
1204                    tol,
1205                    rule_path,
1206                    RuleRefRequirement::Base(BaseTypeRequirement::Duration),
1207                    rule_entries,
1208                    facts,
1209                ));
1210            }
1211        }
1212        ExpressionKind::DateCalendar(_, _, date_expr) => {
1213            out.extend(collect_expected_requirements_for_rule_ref(
1214                date_expr,
1215                rule_path,
1216                RuleRefRequirement::Base(BaseTypeRequirement::Date),
1217                rule_entries,
1218                facts,
1219            ));
1220        }
1221        ExpressionKind::Literal(_)
1222        | ExpressionKind::FactPath(_)
1223        | ExpressionKind::Veto(_)
1224        | ExpressionKind::Now => {}
1225    }
1226    out
1227}
1228
1229fn spec_interface_error(
1230    source: &Source,
1231    message: impl Into<String>,
1232    spec_context: Option<Arc<LemmaSpec>>,
1233    related_spec: Option<Arc<LemmaSpec>>,
1234) -> Error {
1235    Error::validation_with_context(
1236        message.into(),
1237        Some(source.clone()),
1238        None::<String>,
1239        spec_context,
1240        related_spec,
1241    )
1242}
1243
1244/// Validate cross-spec IO contracts for spec-reference bindings.
1245///
1246/// Enforces:
1247/// - required referenced rule names exist on the provider spec
1248/// - referenced rule result types satisfy structural requirements implied by
1249///   the consumer expression context (base kind, units, scale family, bounds)
1250///
1251/// This runs at planning time and reports binding-site errors when a provider
1252/// interface is incompatible with consumer expectations.
1253pub fn validate_spec_interfaces(
1254    referenced_rules: &HashMap<Vec<String>, HashSet<String>>,
1255    spec_ref_facts: &[(FactPath, Arc<LemmaSpec>, Source)],
1256    facts: &IndexMap<FactPath, FactData>,
1257    rule_entries: &IndexMap<RulePath, RuleEntryForBindingCheck>,
1258    main_spec: &Arc<LemmaSpec>,
1259) -> Result<(), Vec<Error>> {
1260    let mut errors = Vec::new();
1261
1262    for (fact_path, spec_arc, fact_source) in spec_ref_facts {
1263        let mut full_path: Vec<String> =
1264            fact_path.segments.iter().map(|s| s.fact.clone()).collect();
1265        full_path.push(fact_path.fact.clone());
1266
1267        let Some(required_rules) = referenced_rules.get(&full_path) else {
1268            continue;
1269        };
1270
1271        let spec = spec_arc.as_ref();
1272        let spec_rule_names: HashSet<&str> = spec.rules.iter().map(|r| r.name.as_str()).collect();
1273
1274        for required_rule in required_rules {
1275            if !spec_rule_names.contains(required_rule.as_str()) {
1276                errors.push(spec_interface_error(
1277                    fact_source,
1278                    format!(
1279                        "Spec '{}' referenced by '{}' is missing required rule '{}'",
1280                        spec.name, fact_path, required_rule
1281                    ),
1282                    Some(Arc::clone(main_spec)),
1283                    Some(Arc::clone(spec_arc)),
1284                ));
1285                continue;
1286            }
1287
1288            let mut ref_segments = fact_path.segments.clone();
1289            ref_segments.push(crate::planning::semantics::PathSegment {
1290                fact: fact_path.fact.clone(),
1291                spec: spec.name.clone(),
1292            });
1293            let ref_rule_path = RulePath::new(ref_segments, required_rule.clone());
1294            let Some(ref_entry) = rule_entries.get(&ref_rule_path) else {
1295                let binding_path_str = fact_path
1296                    .segments
1297                    .iter()
1298                    .map(|s| s.fact.as_str())
1299                    .collect::<Vec<_>>()
1300                    .join(".");
1301                let binding_path_str = if binding_path_str.is_empty() {
1302                    fact_path.fact.clone()
1303                } else {
1304                    format!("{}.{}", binding_path_str, fact_path.fact)
1305                };
1306                errors.push(spec_interface_error(
1307                    fact_source,
1308                    format!(
1309                        "Fact binding '{}' sets spec reference to '{}', but interface validation could not resolve rule path '{}.{}' for contract checking",
1310                        binding_path_str, spec.name, fact_path.fact, required_rule
1311                    ),
1312                    Some(Arc::clone(main_spec)),
1313                    Some(Arc::clone(spec_arc)),
1314                ));
1315                continue;
1316            };
1317            let ref_rule_type = &ref_entry.rule_type;
1318
1319            for (_referencing_path, entry) in rule_entries {
1320                if !entry.depends_on_rules.contains(&ref_rule_path) {
1321                    continue;
1322                }
1323                let expected =
1324                    RuleRefRequirement::Base(lemma_type_to_base_requirement(&entry.rule_type));
1325                for (_condition, result_expr) in &entry.branches {
1326                    let requirements = collect_expected_requirements_for_rule_ref(
1327                        result_expr,
1328                        &ref_rule_path,
1329                        expected.clone(),
1330                        rule_entries,
1331                        facts,
1332                    );
1333                    for (_source, requirement) in requirements {
1334                        if !rule_type_satisfies_requirement(ref_rule_type, &requirement) {
1335                            let report_source = fact_source;
1336
1337                            let binding_path_str = fact_path
1338                                .segments
1339                                .iter()
1340                                .map(|s| s.fact.as_str())
1341                                .collect::<Vec<_>>()
1342                                .join(".");
1343                            let binding_path_str = if binding_path_str.is_empty() {
1344                                fact_path.fact.clone()
1345                            } else {
1346                                format!("{}.{}", binding_path_str, fact_path.fact)
1347                            };
1348
1349                            errors.push(spec_interface_error(
1350                                report_source,
1351                                format!(
1352                                    "Fact binding '{}' sets spec reference to '{}', but that spec's rule '{}' has result type {}; the referencing expression expects a {} value",
1353                                    binding_path_str,
1354                                    spec.name,
1355                                    required_rule,
1356                                    ref_rule_type.name(),
1357                                    requirement.describe(),
1358                                ),
1359                                Some(Arc::clone(main_spec)),
1360                                Some(Arc::clone(spec_arc)),
1361                            ));
1362                        }
1363                    }
1364                }
1365            }
1366        }
1367    }
1368
1369    if errors.is_empty() {
1370        Ok(())
1371    } else {
1372        Err(errors)
1373    }
1374}
1375
1376/// Validate that a registry spec (`from_registry == true`) does not contain
1377/// bare (non-`@`) references. The registry is responsible for rewriting all
1378/// spec references to use `@`-prefixed names before serving the bundle.
1379///
1380/// Returns a list of bare reference names found, empty if valid.
1381pub fn collect_bare_registry_refs(spec: &LemmaSpec) -> Vec<String> {
1382    if !spec.from_registry {
1383        return Vec::new();
1384    }
1385    let mut bare: Vec<String> = Vec::new();
1386    for fact in &spec.facts {
1387        match &fact.value {
1388            FactValue::SpecReference(r) if !r.from_registry => {
1389                bare.push(r.name.clone());
1390            }
1391            FactValue::TypeDeclaration { from: Some(r), .. } if !r.from_registry => {
1392                bare.push(r.name.clone());
1393            }
1394            _ => {}
1395        }
1396    }
1397    for type_def in &spec.types {
1398        match type_def {
1399            TypeDef::Import { from, .. } if !from.from_registry => {
1400                bare.push(from.name.clone());
1401            }
1402            TypeDef::Inline { from: Some(r), .. } if !r.from_registry => {
1403                bare.push(r.name.clone());
1404            }
1405            _ => {}
1406        }
1407    }
1408    bare
1409}
1410
1411#[cfg(test)]
1412mod tests {
1413    use super::*;
1414    use crate::parsing::ast::{CommandArg, TypeConstraintCommand};
1415    use crate::planning::semantics::{
1416        LemmaType, RatioUnit, RatioUnits, ScaleUnit, ScaleUnits, TypeSpecification,
1417    };
1418    use rust_decimal::Decimal;
1419
1420    fn test_source() -> Source {
1421        Source::new(
1422            "<test>",
1423            crate::parsing::ast::Span {
1424                start: 0,
1425                end: 0,
1426                line: 1,
1427                col: 0,
1428            },
1429        )
1430    }
1431
1432    #[test]
1433    fn validate_number_minimum_greater_than_maximum() {
1434        let mut specs = TypeSpecification::number();
1435        specs = specs
1436            .apply_constraint(
1437                TypeConstraintCommand::Minimum,
1438                &[CommandArg::Number("100".to_string())],
1439            )
1440            .unwrap();
1441        specs = specs
1442            .apply_constraint(
1443                TypeConstraintCommand::Maximum,
1444                &[CommandArg::Number("50".to_string())],
1445            )
1446            .unwrap();
1447
1448        let src = test_source();
1449        let errors = validate_type_specifications(&specs, "test", &src, None);
1450        assert_eq!(errors.len(), 1);
1451        assert!(errors[0]
1452            .to_string()
1453            .contains("minimum 100 is greater than maximum 50"));
1454    }
1455
1456    #[test]
1457    fn validate_number_valid_range() {
1458        let mut specs = TypeSpecification::number();
1459        specs = specs
1460            .apply_constraint(
1461                TypeConstraintCommand::Minimum,
1462                &[CommandArg::Number("0".to_string())],
1463            )
1464            .unwrap();
1465        specs = specs
1466            .apply_constraint(
1467                TypeConstraintCommand::Maximum,
1468                &[CommandArg::Number("100".to_string())],
1469            )
1470            .unwrap();
1471
1472        let src = test_source();
1473        let errors = validate_type_specifications(&specs, "test", &src, None);
1474        assert!(errors.is_empty());
1475    }
1476
1477    #[test]
1478    fn validate_number_default_below_minimum() {
1479        let specs = TypeSpecification::Number {
1480            minimum: Some(Decimal::from(10)),
1481            maximum: None,
1482            decimals: None,
1483            precision: None,
1484            help: String::new(),
1485            default: Some(Decimal::from(5)),
1486        };
1487
1488        let src = test_source();
1489        let errors = validate_type_specifications(&specs, "test", &src, None);
1490        assert_eq!(errors.len(), 1);
1491        assert!(errors[0]
1492            .to_string()
1493            .contains("default value 5 is less than minimum 10"));
1494    }
1495
1496    #[test]
1497    fn validate_number_default_above_maximum() {
1498        let specs = TypeSpecification::Number {
1499            minimum: None,
1500            maximum: Some(Decimal::from(100)),
1501            decimals: None,
1502            precision: None,
1503            help: String::new(),
1504            default: Some(Decimal::from(150)),
1505        };
1506
1507        let src = test_source();
1508        let errors = validate_type_specifications(&specs, "test", &src, None);
1509        assert_eq!(errors.len(), 1);
1510        assert!(errors[0]
1511            .to_string()
1512            .contains("default value 150 is greater than maximum 100"));
1513    }
1514
1515    #[test]
1516    fn validate_number_default_valid() {
1517        let specs = TypeSpecification::Number {
1518            minimum: Some(Decimal::from(0)),
1519            maximum: Some(Decimal::from(100)),
1520            decimals: None,
1521            precision: None,
1522            help: String::new(),
1523            default: Some(Decimal::from(50)),
1524        };
1525
1526        let src = test_source();
1527        let errors = validate_type_specifications(&specs, "test", &src, None);
1528        assert!(errors.is_empty());
1529    }
1530
1531    #[test]
1532    fn validate_text_minimum_greater_than_maximum() {
1533        let mut specs = TypeSpecification::text();
1534        specs = specs
1535            .apply_constraint(
1536                TypeConstraintCommand::Minimum,
1537                &[CommandArg::Number("100".to_string())],
1538            )
1539            .unwrap();
1540        specs = specs
1541            .apply_constraint(
1542                TypeConstraintCommand::Maximum,
1543                &[CommandArg::Number("50".to_string())],
1544            )
1545            .unwrap();
1546
1547        let src = test_source();
1548        let errors = validate_type_specifications(&specs, "test", &src, None);
1549        assert_eq!(errors.len(), 1);
1550        assert!(errors[0]
1551            .to_string()
1552            .contains("minimum length 100 is greater than maximum length 50"));
1553    }
1554
1555    #[test]
1556    fn validate_text_length_inconsistent_with_minimum() {
1557        let mut specs = TypeSpecification::text();
1558        specs = specs
1559            .apply_constraint(
1560                TypeConstraintCommand::Minimum,
1561                &[CommandArg::Number("10".to_string())],
1562            )
1563            .unwrap();
1564        specs = specs
1565            .apply_constraint(
1566                TypeConstraintCommand::Length,
1567                &[CommandArg::Number("5".to_string())],
1568            )
1569            .unwrap();
1570
1571        let src = test_source();
1572        let errors = validate_type_specifications(&specs, "test", &src, None);
1573        assert_eq!(errors.len(), 1);
1574        assert!(errors[0]
1575            .to_string()
1576            .contains("length 5 is less than minimum 10"));
1577    }
1578
1579    #[test]
1580    fn validate_text_default_not_in_options() {
1581        let specs = TypeSpecification::Text {
1582            minimum: None,
1583            maximum: None,
1584            length: None,
1585            options: vec!["red".to_string(), "blue".to_string()],
1586            help: String::new(),
1587            default: Some("green".to_string()),
1588        };
1589
1590        let src = test_source();
1591        let errors = validate_type_specifications(&specs, "test", &src, None);
1592        assert_eq!(errors.len(), 1);
1593        assert!(errors[0]
1594            .to_string()
1595            .contains("default value 'green' is not in allowed options"));
1596    }
1597
1598    #[test]
1599    fn validate_text_default_valid_in_options() {
1600        let specs = TypeSpecification::Text {
1601            minimum: None,
1602            maximum: None,
1603            length: None,
1604            options: vec!["red".to_string(), "blue".to_string()],
1605            help: String::new(),
1606            default: Some("red".to_string()),
1607        };
1608
1609        let src = test_source();
1610        let errors = validate_type_specifications(&specs, "test", &src, None);
1611        assert!(errors.is_empty());
1612    }
1613
1614    #[test]
1615    fn validate_ratio_minimum_greater_than_maximum() {
1616        let specs = TypeSpecification::Ratio {
1617            minimum: Some(Decimal::from(2)),
1618            maximum: Some(Decimal::from(1)),
1619            decimals: None,
1620            units: crate::planning::semantics::RatioUnits::new(),
1621            help: String::new(),
1622            default: None,
1623        };
1624
1625        let src = test_source();
1626        let errors = validate_type_specifications(&specs, "test", &src, None);
1627        assert_eq!(errors.len(), 1);
1628        assert!(errors[0]
1629            .to_string()
1630            .contains("minimum 2 is greater than maximum 1"));
1631    }
1632
1633    #[test]
1634    fn validate_date_minimum_after_maximum() {
1635        let mut specs = TypeSpecification::date();
1636        specs = specs
1637            .apply_constraint(
1638                TypeConstraintCommand::Minimum,
1639                &[CommandArg::Label("2024-12-31".to_string())],
1640            )
1641            .unwrap();
1642        specs = specs
1643            .apply_constraint(
1644                TypeConstraintCommand::Maximum,
1645                &[CommandArg::Label("2024-01-01".to_string())],
1646            )
1647            .unwrap();
1648
1649        let src = test_source();
1650        let errors = validate_type_specifications(&specs, "test", &src, None);
1651        assert_eq!(errors.len(), 1);
1652        assert!(
1653            errors[0].to_string().contains("minimum")
1654                && errors[0].to_string().contains("is after maximum")
1655        );
1656    }
1657
1658    #[test]
1659    fn validate_date_valid_range() {
1660        let mut specs = TypeSpecification::date();
1661        specs = specs
1662            .apply_constraint(
1663                TypeConstraintCommand::Minimum,
1664                &[CommandArg::Label("2024-01-01".to_string())],
1665            )
1666            .unwrap();
1667        specs = specs
1668            .apply_constraint(
1669                TypeConstraintCommand::Maximum,
1670                &[CommandArg::Label("2024-12-31".to_string())],
1671            )
1672            .unwrap();
1673
1674        let src = test_source();
1675        let errors = validate_type_specifications(&specs, "test", &src, None);
1676        assert!(errors.is_empty());
1677    }
1678
1679    #[test]
1680    fn validate_time_minimum_after_maximum() {
1681        let mut specs = TypeSpecification::time();
1682        specs = specs
1683            .apply_constraint(
1684                TypeConstraintCommand::Minimum,
1685                &[CommandArg::Label("23:00:00".to_string())],
1686            )
1687            .unwrap();
1688        specs = specs
1689            .apply_constraint(
1690                TypeConstraintCommand::Maximum,
1691                &[CommandArg::Label("10:00:00".to_string())],
1692            )
1693            .unwrap();
1694
1695        let src = test_source();
1696        let errors = validate_type_specifications(&specs, "test", &src, None);
1697        assert_eq!(errors.len(), 1);
1698        assert!(
1699            errors[0].to_string().contains("minimum")
1700                && errors[0].to_string().contains("is after maximum")
1701        );
1702    }
1703
1704    #[test]
1705    fn validate_type_definition_with_invalid_constraints() {
1706        // This test now validates that type specification validation works correctly.
1707        // The actual validation happens during graph building, but we test the validation
1708        // function directly here.
1709        use crate::engine::Context;
1710        use crate::parsing::ast::{LemmaSpec, ParentType, PrimitiveKind, TypeDef};
1711        use crate::planning::types::PerSliceTypeResolver;
1712        use std::sync::Arc;
1713
1714        let spec = Arc::new(LemmaSpec::new("test".to_string()));
1715        let mut ctx = Context::new();
1716        ctx.insert_spec(Arc::clone(&spec), false)
1717            .expect("insert test spec");
1718        let type_def = TypeDef::Regular {
1719            source_location: crate::Source::new(
1720                "<test>",
1721                crate::parsing::ast::Span {
1722                    start: 0,
1723                    end: 0,
1724                    line: 1,
1725                    col: 0,
1726                },
1727            ),
1728            name: "invalid_money".to_string(),
1729            parent: ParentType::Primitive {
1730                primitive: PrimitiveKind::Number,
1731            },
1732            constraints: Some(vec![
1733                (
1734                    TypeConstraintCommand::Minimum,
1735                    vec![CommandArg::Number("100".to_string())],
1736                ),
1737                (
1738                    TypeConstraintCommand::Maximum,
1739                    vec![CommandArg::Number("50".to_string())],
1740                ),
1741            ]),
1742        };
1743
1744        let plan_hashes = crate::planning::PlanHashRegistry::default();
1745        let mut type_resolver = PerSliceTypeResolver::new(&ctx, None, &plan_hashes);
1746        type_resolver
1747            .register_type(&spec, type_def)
1748            .expect("Should register type");
1749        let resolved_types = type_resolver
1750            .resolve_named_types(&spec)
1751            .expect("Should resolve types");
1752
1753        // Validate the specifications
1754        let lemma_type = resolved_types
1755            .named_types
1756            .get("invalid_money")
1757            .expect("Should have invalid_money type");
1758        let src = test_source();
1759        let errors =
1760            validate_type_specifications(&lemma_type.specifications, "invalid_money", &src, None);
1761        assert!(!errors.is_empty());
1762        assert!(errors.iter().any(|e| e
1763            .to_string()
1764            .contains("minimum 100 is greater than maximum 50")));
1765    }
1766
1767    fn lt(spec: TypeSpecification) -> LemmaType {
1768        LemmaType::primitive(spec)
1769    }
1770
1771    #[test]
1772    fn interface_requirement_matrix_all_types_base_checks() {
1773        let bool_t = lt(TypeSpecification::boolean());
1774        let num_t = lt(TypeSpecification::number());
1775        let scale_t = lt(TypeSpecification::Scale {
1776            minimum: None,
1777            maximum: None,
1778            decimals: None,
1779            precision: None,
1780            units: ScaleUnits::from(vec![ScaleUnit {
1781                name: "eur".to_string(),
1782                value: Decimal::ONE,
1783            }]),
1784            help: String::new(),
1785            default: None,
1786        });
1787        let ratio_t = lt(TypeSpecification::Ratio {
1788            minimum: None,
1789            maximum: None,
1790            decimals: None,
1791            units: RatioUnits::from(vec![RatioUnit {
1792                name: "percent".to_string(),
1793                value: Decimal::from(100),
1794            }]),
1795            help: String::new(),
1796            default: None,
1797        });
1798        let text_t = lt(TypeSpecification::text());
1799        let date_t = lt(TypeSpecification::date());
1800        let time_t = lt(TypeSpecification::time());
1801        let duration_t = lt(TypeSpecification::duration());
1802        let veto_t = LemmaType::veto_type();
1803        let undetermined_t = LemmaType::undetermined_type();
1804
1805        assert!(rule_type_satisfies_requirement(
1806            &bool_t,
1807            &RuleRefRequirement::Base(BaseTypeRequirement::Boolean)
1808        ));
1809        assert!(rule_type_satisfies_requirement(
1810            &num_t,
1811            &RuleRefRequirement::Base(BaseTypeRequirement::Number)
1812        ));
1813        assert!(rule_type_satisfies_requirement(
1814            &scale_t,
1815            &RuleRefRequirement::Base(BaseTypeRequirement::Scale)
1816        ));
1817        assert!(rule_type_satisfies_requirement(
1818            &ratio_t,
1819            &RuleRefRequirement::Base(BaseTypeRequirement::Ratio)
1820        ));
1821        assert!(rule_type_satisfies_requirement(
1822            &text_t,
1823            &RuleRefRequirement::Base(BaseTypeRequirement::Text)
1824        ));
1825        assert!(rule_type_satisfies_requirement(
1826            &date_t,
1827            &RuleRefRequirement::Base(BaseTypeRequirement::Date)
1828        ));
1829        assert!(rule_type_satisfies_requirement(
1830            &time_t,
1831            &RuleRefRequirement::Base(BaseTypeRequirement::Time)
1832        ));
1833        assert!(rule_type_satisfies_requirement(
1834            &duration_t,
1835            &RuleRefRequirement::Base(BaseTypeRequirement::Duration)
1836        ));
1837
1838        assert!(!rule_type_satisfies_requirement(
1839            &num_t,
1840            &RuleRefRequirement::Base(BaseTypeRequirement::Boolean)
1841        ));
1842        assert!(!rule_type_satisfies_requirement(
1843            &scale_t,
1844            &RuleRefRequirement::Base(BaseTypeRequirement::Number)
1845        ));
1846        // veto is control flow, not type incompatibility -- satisfies any requirement
1847        assert!(rule_type_satisfies_requirement(
1848            &veto_t,
1849            &RuleRefRequirement::Base(BaseTypeRequirement::Any)
1850        ));
1851        assert!(
1852            std::panic::catch_unwind(|| {
1853                rule_type_satisfies_requirement(
1854                    &undetermined_t,
1855                    &RuleRefRequirement::Base(BaseTypeRequirement::Any),
1856                )
1857            })
1858            .is_err(),
1859            "should panic when rule_type_satisfies_requirement is called with undetermined type"
1860        );
1861    }
1862
1863    #[test]
1864    fn interface_requirement_matrix_unit_family_and_bounds_checks() {
1865        let money = LemmaType::new(
1866            "money".to_string(),
1867            TypeSpecification::Scale {
1868                minimum: Some(Decimal::ZERO),
1869                maximum: Some(Decimal::from(100)),
1870                decimals: None,
1871                precision: None,
1872                units: ScaleUnits::from(vec![
1873                    ScaleUnit {
1874                        name: "eur".to_string(),
1875                        value: Decimal::ONE,
1876                    },
1877                    ScaleUnit {
1878                        name: "usd".to_string(),
1879                        value: Decimal::new(11, 1),
1880                    },
1881                ]),
1882                help: String::new(),
1883                default: None,
1884            },
1885            crate::planning::semantics::TypeExtends::Primitive,
1886        );
1887        let weight = LemmaType::new(
1888            "weight".to_string(),
1889            TypeSpecification::Scale {
1890                minimum: None,
1891                maximum: None,
1892                decimals: None,
1893                precision: None,
1894                units: ScaleUnits::from(vec![ScaleUnit {
1895                    name: "kg".to_string(),
1896                    value: Decimal::ONE,
1897                }]),
1898                help: String::new(),
1899                default: None,
1900            },
1901            crate::planning::semantics::TypeExtends::Primitive,
1902        );
1903        let ratio = lt(TypeSpecification::Ratio {
1904            minimum: Some(Decimal::ZERO),
1905            maximum: Some(Decimal::from(100)),
1906            decimals: None,
1907            units: RatioUnits::from(vec![RatioUnit {
1908                name: "percent".to_string(),
1909                value: Decimal::from(100),
1910            }]),
1911            help: String::new(),
1912            default: None,
1913        });
1914        let bounded_number = lt(TypeSpecification::Number {
1915            minimum: Some(Decimal::ZERO),
1916            maximum: Some(Decimal::from(100)),
1917            decimals: None,
1918            precision: None,
1919            help: String::new(),
1920            default: None,
1921        });
1922
1923        assert!(rule_type_satisfies_requirement(
1924            &money,
1925            &RuleRefRequirement::ScaleMustContainUnit("eur".to_string())
1926        ));
1927        assert!(!rule_type_satisfies_requirement(
1928            &money,
1929            &RuleRefRequirement::ScaleMustContainUnit("gbp".to_string())
1930        ));
1931        assert!(rule_type_satisfies_requirement(
1932            &ratio,
1933            &RuleRefRequirement::RatioMustContainUnit("percent".to_string())
1934        ));
1935        assert!(!rule_type_satisfies_requirement(
1936            &ratio,
1937            &RuleRefRequirement::RatioMustContainUnit("permille".to_string())
1938        ));
1939        assert!(rule_type_satisfies_requirement(
1940            &money,
1941            &RuleRefRequirement::SameScaleFamilyAs(money.clone())
1942        ));
1943        assert!(!rule_type_satisfies_requirement(
1944            &money,
1945            &RuleRefRequirement::SameScaleFamilyAs(weight)
1946        ));
1947
1948        assert!(rule_type_satisfies_requirement(
1949            &bounded_number,
1950            &RuleRefRequirement::NumericLiteral(NumericLiteralConstraint {
1951                op: ComparisonComputation::GreaterThan,
1952                literal: Decimal::from(50),
1953                reference_on_left: true,
1954            })
1955        ));
1956        assert!(!rule_type_satisfies_requirement(
1957            &bounded_number,
1958            &RuleRefRequirement::NumericLiteral(NumericLiteralConstraint {
1959                op: ComparisonComputation::GreaterThan,
1960                literal: Decimal::from(500),
1961                reference_on_left: true,
1962            })
1963        ));
1964    }
1965}