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::{DateTimeValue, FactValue, LemmaSpec, TimeValue, TypeDef};
7use crate::planning::semantics::{
8    Expression, ExpressionKind, FactPath, LemmaType, RulePath, SemanticConversionTarget,
9    TypeSpecification,
10};
11use crate::Error;
12use crate::Source;
13use indexmap::IndexMap;
14use rust_decimal::Decimal;
15use std::cmp::Ordering;
16use std::collections::{HashMap, HashSet};
17use std::sync::Arc;
18
19/// Validate that TypeSpecification constraints are internally consistent
20///
21/// This checks:
22/// - minimum <= maximum (for types that support ranges)
23/// - default values satisfy all constraints
24/// - length constraints are consistent (for Text)
25/// - precision/decimals are within valid ranges
26///
27/// Returns a vector of errors (empty if valid)
28pub fn validate_type_specifications(
29    specs: &TypeSpecification,
30    type_name: &str,
31    source: &Source,
32    spec_context: Option<Arc<LemmaSpec>>,
33) -> Vec<Error> {
34    let mut errors = Vec::new();
35
36    match specs {
37        TypeSpecification::Scale {
38            minimum,
39            maximum,
40            decimals,
41            precision,
42            default,
43            units,
44            ..
45        } => {
46            // Validate range consistency
47            if let (Some(min), Some(max)) = (minimum, maximum) {
48                if min > max {
49                    errors.push(Error::validation_with_context(
50                        format!(
51                            "Type '{}' has invalid range: minimum {} is greater than maximum {}",
52                            type_name, min, max
53                        ),
54                        Some(source.clone()),
55                        None::<String>,
56                        spec_context.clone(),
57                        None,
58                    ));
59                }
60            }
61
62            // Validate decimals range (0-28 is rust_decimal limit)
63            if let Some(d) = decimals {
64                if *d > 28 {
65                    errors.push(Error::validation_with_context(
66                        format!(
67                            "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
68                            type_name, d
69                        ),
70                        Some(source.clone()),
71                        None::<String>,
72                        spec_context.clone(),
73                        None,
74                    ));
75                }
76            }
77
78            // Validate precision is positive if set
79            if let Some(prec) = precision {
80                if *prec <= Decimal::ZERO {
81                    errors.push(Error::validation_with_context(
82                        format!(
83                            "Type '{}' has invalid precision: {}. Must be positive",
84                            type_name, prec
85                        ),
86                        Some(source.clone()),
87                        None::<String>,
88                        spec_context.clone(),
89                        None,
90                    ));
91                }
92            }
93
94            // Validate default value constraints
95            if let Some((def_value, def_unit)) = default {
96                // Validate that the default unit exists
97                if !units.iter().any(|u| u.name == *def_unit) {
98                    errors.push(Error::validation_with_context(
99                        format!(
100                            "Type '{}' default unit '{}' is not a valid unit. Valid units: {}",
101                            type_name,
102                            def_unit,
103                            units
104                                .iter()
105                                .map(|u| u.name.clone())
106                                .collect::<Vec<_>>()
107                                .join(", ")
108                        ),
109                        Some(source.clone()),
110                        None::<String>,
111                        spec_context.clone(),
112                        None,
113                    ));
114                }
115                if let Some(min) = minimum {
116                    if *def_value < *min {
117                        errors.push(Error::validation_with_context(
118                            format!(
119                                "Type '{}' default value {} {} is less than minimum {}",
120                                type_name, def_value, def_unit, min
121                            ),
122                            Some(source.clone()),
123                            None::<String>,
124                            spec_context.clone(),
125                            None,
126                        ));
127                    }
128                }
129                if let Some(max) = maximum {
130                    if *def_value > *max {
131                        errors.push(Error::validation_with_context(
132                            format!(
133                                "Type '{}' default value {} {} is greater than maximum {}",
134                                type_name, def_value, def_unit, max
135                            ),
136                            Some(source.clone()),
137                            None::<String>,
138                            spec_context.clone(),
139                            None,
140                        ));
141                    }
142                }
143            }
144
145            // Scale types must have at least one unit (required for parsing and conversion)
146            if units.is_empty() {
147                errors.push(Error::validation_with_context(
148                    format!(
149                        "Type '{}' is a scale type but has no units. Scale types must define at least one unit (e.g. -> unit eur 1).",
150                        type_name
151                    ),
152                    Some(source.clone()),
153                    None::<String>,
154                    spec_context.clone(),
155                    None,
156                ));
157            }
158
159            // Validate units (if present)
160            if !units.is_empty() {
161                let mut seen_names: Vec<String> = Vec::new();
162                for unit in units.iter() {
163                    // Validate unit name is not empty
164                    if unit.name.trim().is_empty() {
165                        errors.push(Error::validation_with_context(
166                            format!(
167                                "Type '{}' has a unit with empty name. Unit names cannot be empty.",
168                                type_name
169                            ),
170                            Some(source.clone()),
171                            None::<String>,
172                            spec_context.clone(),
173                            None,
174                        ));
175                    }
176
177                    // Validate unit names are unique within the type (case-insensitive)
178                    let lower_name = unit.name.to_lowercase();
179                    if seen_names
180                        .iter()
181                        .any(|seen| seen.to_lowercase() == lower_name)
182                    {
183                        errors.push(Error::validation_with_context(
184                            format!("Type '{}' has duplicate unit name '{}' (case-insensitive). Unit names must be unique within a type.", type_name, unit.name),
185                            Some(source.clone()),
186                            None::<String>,
187                            spec_context.clone(),
188                            None,
189                        ));
190                    } else {
191                        seen_names.push(unit.name.clone());
192                    }
193
194                    // Validate unit values are positive (conversion factors relative to type base of 1)
195                    if unit.value <= Decimal::ZERO {
196                        errors.push(Error::validation_with_context(
197                            format!("Type '{}' has unit '{}' with invalid value {}. Unit values must be positive (conversion factor relative to type base).", type_name, unit.name, unit.value),
198                            Some(source.clone()),
199                            None::<String>,
200                            spec_context.clone(),
201                            None,
202                        ));
203                    }
204                }
205            }
206        }
207        TypeSpecification::Number {
208            minimum,
209            maximum,
210            decimals,
211            precision,
212            default,
213            ..
214        } => {
215            // Validate range consistency
216            if let (Some(min), Some(max)) = (minimum, maximum) {
217                if min > max {
218                    errors.push(Error::validation_with_context(
219                        format!(
220                            "Type '{}' has invalid range: minimum {} is greater than maximum {}",
221                            type_name, min, max
222                        ),
223                        Some(source.clone()),
224                        None::<String>,
225                        spec_context.clone(),
226                        None,
227                    ));
228                }
229            }
230
231            // Validate decimals range (0-28 is rust_decimal limit)
232            if let Some(d) = decimals {
233                if *d > 28 {
234                    errors.push(Error::validation_with_context(
235                        format!(
236                            "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
237                            type_name, d
238                        ),
239                        Some(source.clone()),
240                        None::<String>,
241                        spec_context.clone(),
242                        None,
243                    ));
244                }
245            }
246
247            // Validate precision is positive if set
248            if let Some(prec) = precision {
249                if *prec <= Decimal::ZERO {
250                    errors.push(Error::validation_with_context(
251                        format!(
252                            "Type '{}' has invalid precision: {}. Must be positive",
253                            type_name, prec
254                        ),
255                        Some(source.clone()),
256                        None::<String>,
257                        spec_context.clone(),
258                        None,
259                    ));
260                }
261            }
262
263            // Validate default value constraints
264            if let Some(def) = default {
265                if let Some(min) = minimum {
266                    if *def < *min {
267                        errors.push(Error::validation_with_context(
268                            format!(
269                                "Type '{}' default value {} is less than minimum {}",
270                                type_name, def, min
271                            ),
272                            Some(source.clone()),
273                            None::<String>,
274                            spec_context.clone(),
275                            None,
276                        ));
277                    }
278                }
279                if let Some(max) = maximum {
280                    if *def > *max {
281                        errors.push(Error::validation_with_context(
282                            format!(
283                                "Type '{}' default value {} is greater than maximum {}",
284                                type_name, def, max
285                            ),
286                            Some(source.clone()),
287                            None::<String>,
288                            spec_context.clone(),
289                            None,
290                        ));
291                    }
292                }
293            }
294            // Note: Number types are dimensionless and cannot have units (validated in apply_constraint)
295        }
296
297        TypeSpecification::Ratio {
298            minimum,
299            maximum,
300            decimals,
301            default,
302            units,
303            ..
304        } => {
305            // Validate decimals range (0-28 is rust_decimal limit)
306            if let Some(d) = decimals {
307                if *d > 28 {
308                    errors.push(Error::validation_with_context(
309                        format!(
310                            "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
311                            type_name, d
312                        ),
313                        Some(source.clone()),
314                        None::<String>,
315                        spec_context.clone(),
316                        None,
317                    ));
318                }
319            }
320
321            // Validate range consistency
322            if let (Some(min), Some(max)) = (minimum, maximum) {
323                if min > max {
324                    errors.push(Error::validation_with_context(
325                        format!(
326                            "Type '{}' has invalid range: minimum {} is greater than maximum {}",
327                            type_name, min, max
328                        ),
329                        Some(source.clone()),
330                        None::<String>,
331                        spec_context.clone(),
332                        None,
333                    ));
334                }
335            }
336
337            // Validate default value constraints
338            if let Some(def) = default {
339                if let Some(min) = minimum {
340                    if *def < *min {
341                        errors.push(Error::validation_with_context(
342                            format!(
343                                "Type '{}' default value {} is less than minimum {}",
344                                type_name, def, min
345                            ),
346                            Some(source.clone()),
347                            None::<String>,
348                            spec_context.clone(),
349                            None,
350                        ));
351                    }
352                }
353                if let Some(max) = maximum {
354                    if *def > *max {
355                        errors.push(Error::validation_with_context(
356                            format!(
357                                "Type '{}' default value {} is greater than maximum {}",
358                                type_name, def, max
359                            ),
360                            Some(source.clone()),
361                            None::<String>,
362                            spec_context.clone(),
363                            None,
364                        ));
365                    }
366                }
367            }
368
369            // Validate units (if present)
370            // Types can have zero units (e.g., type ratio: number -> ratio) - this is valid
371            // Only validate if units are defined
372            if !units.is_empty() {
373                let mut seen_names: Vec<String> = Vec::new();
374                for unit in units.iter() {
375                    // Validate unit name is not empty
376                    if unit.name.trim().is_empty() {
377                        errors.push(Error::validation_with_context(
378                            format!(
379                                "Type '{}' has a unit with empty name. Unit names cannot be empty.",
380                                type_name
381                            ),
382                            Some(source.clone()),
383                            None::<String>,
384                            spec_context.clone(),
385                            None,
386                        ));
387                    }
388
389                    // Validate unit names are unique within the type (case-insensitive)
390                    let lower_name = unit.name.to_lowercase();
391                    if seen_names
392                        .iter()
393                        .any(|seen| seen.to_lowercase() == lower_name)
394                    {
395                        errors.push(Error::validation_with_context(
396                            format!("Type '{}' has duplicate unit name '{}' (case-insensitive). Unit names must be unique within a type.", type_name, unit.name),
397                            Some(source.clone()),
398                            None::<String>,
399                            spec_context.clone(),
400                            None,
401                        ));
402                    } else {
403                        seen_names.push(unit.name.clone());
404                    }
405
406                    // Validate unit values are positive (conversion factors relative to type base of 1)
407                    if unit.value <= Decimal::ZERO {
408                        errors.push(Error::validation_with_context(
409                            format!("Type '{}' has unit '{}' with invalid value {}. Unit values must be positive (conversion factor relative to type base).", type_name, unit.name, unit.value),
410                            Some(source.clone()),
411                            None::<String>,
412                            spec_context.clone(),
413                            None,
414                        ));
415                    }
416                }
417            }
418        }
419
420        TypeSpecification::Text {
421            minimum,
422            maximum,
423            length,
424            options,
425            default,
426            ..
427        } => {
428            // Validate range consistency
429            if let (Some(min), Some(max)) = (minimum, maximum) {
430                if min > max {
431                    errors.push(Error::validation_with_context(
432                        format!("Type '{}' has invalid range: minimum length {} is greater than maximum length {}", type_name, min, max),
433                        Some(source.clone()),
434                        None::<String>,
435                        spec_context.clone(),
436                        None,
437                    ));
438                }
439            }
440
441            // Validate length consistency
442            if let Some(len) = length {
443                if let Some(min) = minimum {
444                    if *len < *min {
445                        errors.push(Error::validation_with_context(
446                            format!("Type '{}' has inconsistent length constraint: length {} is less than minimum {}", type_name, len, min),
447                            Some(source.clone()),
448                            None::<String>,
449                            spec_context.clone(),
450                            None,
451                        ));
452                    }
453                }
454                if let Some(max) = maximum {
455                    if *len > *max {
456                        errors.push(Error::validation_with_context(
457                            format!("Type '{}' has inconsistent length constraint: length {} is greater than maximum {}", type_name, len, max),
458                            Some(source.clone()),
459                            None::<String>,
460                            spec_context.clone(),
461                            None,
462                        ));
463                    }
464                }
465            }
466
467            // Validate default value constraints
468            if let Some(def) = default {
469                let def_len = def.len();
470
471                if let Some(min) = minimum {
472                    if def_len < *min {
473                        errors.push(Error::validation_with_context(
474                            format!(
475                                "Type '{}' default value length {} is less than minimum {}",
476                                type_name, def_len, min
477                            ),
478                            Some(source.clone()),
479                            None::<String>,
480                            spec_context.clone(),
481                            None,
482                        ));
483                    }
484                }
485                if let Some(max) = maximum {
486                    if def_len > *max {
487                        errors.push(Error::validation_with_context(
488                            format!(
489                                "Type '{}' default value length {} is greater than maximum {}",
490                                type_name, def_len, max
491                            ),
492                            Some(source.clone()),
493                            None::<String>,
494                            spec_context.clone(),
495                            None,
496                        ));
497                    }
498                }
499                if let Some(len) = length {
500                    if def_len != *len {
501                        errors.push(Error::validation_with_context(
502                            format!("Type '{}' default value length {} does not match required length {}", type_name, def_len, len),
503                            Some(source.clone()),
504                            None::<String>,
505                            spec_context.clone(),
506                            None,
507                        ));
508                    }
509                }
510                if !options.is_empty() && !options.contains(def) {
511                    errors.push(Error::validation_with_context(
512                        format!(
513                            "Type '{}' default value '{}' is not in allowed options: {:?}",
514                            type_name, def, options
515                        ),
516                        Some(source.clone()),
517                        None::<String>,
518                        spec_context.clone(),
519                        None,
520                    ));
521                }
522            }
523        }
524
525        TypeSpecification::Date {
526            minimum,
527            maximum,
528            default,
529            ..
530        } => {
531            // Validate range consistency
532            if let (Some(min), Some(max)) = (minimum, maximum) {
533                if compare_date_values(min, max) == Ordering::Greater {
534                    errors.push(Error::validation_with_context(
535                        format!(
536                            "Type '{}' has invalid date range: minimum {} is after maximum {}",
537                            type_name, min, max
538                        ),
539                        Some(source.clone()),
540                        None::<String>,
541                        spec_context.clone(),
542                        None,
543                    ));
544                }
545            }
546
547            // Validate default value constraints
548            if let Some(def) = default {
549                if let Some(min) = minimum {
550                    if compare_date_values(def, min) == Ordering::Less {
551                        errors.push(Error::validation_with_context(
552                            format!(
553                                "Type '{}' default date {} is before minimum {}",
554                                type_name, def, min
555                            ),
556                            Some(source.clone()),
557                            None::<String>,
558                            spec_context.clone(),
559                            None,
560                        ));
561                    }
562                }
563                if let Some(max) = maximum {
564                    if compare_date_values(def, max) == Ordering::Greater {
565                        errors.push(Error::validation_with_context(
566                            format!(
567                                "Type '{}' default date {} is after maximum {}",
568                                type_name, def, max
569                            ),
570                            Some(source.clone()),
571                            None::<String>,
572                            spec_context.clone(),
573                            None,
574                        ));
575                    }
576                }
577            }
578        }
579
580        TypeSpecification::Time {
581            minimum,
582            maximum,
583            default,
584            ..
585        } => {
586            // Validate range consistency
587            if let (Some(min), Some(max)) = (minimum, maximum) {
588                if compare_time_values(min, max) == Ordering::Greater {
589                    errors.push(Error::validation_with_context(
590                        format!(
591                            "Type '{}' has invalid time range: minimum {} is after maximum {}",
592                            type_name, min, max
593                        ),
594                        Some(source.clone()),
595                        None::<String>,
596                        spec_context.clone(),
597                        None,
598                    ));
599                }
600            }
601
602            // Validate default value constraints
603            if let Some(def) = default {
604                if let Some(min) = minimum {
605                    if compare_time_values(def, min) == Ordering::Less {
606                        errors.push(Error::validation_with_context(
607                            format!(
608                                "Type '{}' default time {} is before minimum {}",
609                                type_name, def, min
610                            ),
611                            Some(source.clone()),
612                            None::<String>,
613                            spec_context.clone(),
614                            None,
615                        ));
616                    }
617                }
618                if let Some(max) = maximum {
619                    if compare_time_values(def, max) == Ordering::Greater {
620                        errors.push(Error::validation_with_context(
621                            format!(
622                                "Type '{}' default time {} is after maximum {}",
623                                type_name, def, max
624                            ),
625                            Some(source.clone()),
626                            None::<String>,
627                            spec_context.clone(),
628                            None,
629                        ));
630                    }
631                }
632            }
633        }
634
635        TypeSpecification::Boolean { .. } | TypeSpecification::Duration { .. } => {
636            // No constraint validation needed for these types
637        }
638        TypeSpecification::Veto { .. } => {
639            // Veto is not a user-declarable type, so validation should not be called on it
640            // But if it is, there's nothing to validate
641        }
642        TypeSpecification::Undetermined => unreachable!(
643            "BUG: validate_type_specification_constraints called with Undetermined sentinel type; this type exists only during type inference"
644        ),
645    }
646
647    errors
648}
649
650/// Compare two DateTimeValue instances for ordering
651fn compare_date_values(left: &DateTimeValue, right: &DateTimeValue) -> Ordering {
652    // Compare by year, month, day, hour, minute, second
653    left.year
654        .cmp(&right.year)
655        .then_with(|| left.month.cmp(&right.month))
656        .then_with(|| left.day.cmp(&right.day))
657        .then_with(|| left.hour.cmp(&right.hour))
658        .then_with(|| left.minute.cmp(&right.minute))
659        .then_with(|| left.second.cmp(&right.second))
660}
661
662/// Compare two TimeValue instances for ordering
663fn compare_time_values(left: &TimeValue, right: &TimeValue) -> Ordering {
664    // Compare by hour, minute, second
665    left.hour
666        .cmp(&right.hour)
667        .then_with(|| left.minute.cmp(&right.minute))
668        .then_with(|| left.second.cmp(&right.second))
669}
670
671// -----------------------------------------------------------------------------
672// Spec interface validation (required rule names + rule result types)
673// -----------------------------------------------------------------------------
674
675/// Rule data needed to validate spec interfaces (inference snapshot before apply).
676pub struct RuleEntryForBindingCheck {
677    pub rule_type: LemmaType,
678    pub depends_on_rules: std::collections::BTreeSet<RulePath>,
679    pub branches: Vec<(Option<Expression>, Expression)>,
680}
681
682#[derive(Clone, Copy, Debug)]
683enum ExpectedRuleTypeConstraint {
684    Numeric,
685    Boolean,
686    Comparable,
687    Number,
688    Duration,
689    Ratio,
690    Scale,
691    Any,
692}
693
694fn lemma_type_to_expected_constraint(lemma_type: &LemmaType) -> ExpectedRuleTypeConstraint {
695    if lemma_type.is_boolean() {
696        return ExpectedRuleTypeConstraint::Boolean;
697    }
698    if lemma_type.is_number() {
699        return ExpectedRuleTypeConstraint::Number;
700    }
701    if lemma_type.is_scale() {
702        return ExpectedRuleTypeConstraint::Scale;
703    }
704    if lemma_type.is_duration() {
705        return ExpectedRuleTypeConstraint::Duration;
706    }
707    if lemma_type.is_ratio() {
708        return ExpectedRuleTypeConstraint::Ratio;
709    }
710    if lemma_type.is_text() || lemma_type.is_date() || lemma_type.is_time() {
711        return ExpectedRuleTypeConstraint::Comparable;
712    }
713    ExpectedRuleTypeConstraint::Any
714}
715
716fn rule_type_satisfies_constraint(
717    lemma_type: &LemmaType,
718    constraint: ExpectedRuleTypeConstraint,
719) -> bool {
720    match constraint {
721        ExpectedRuleTypeConstraint::Any => true,
722        ExpectedRuleTypeConstraint::Boolean => lemma_type.is_boolean(),
723        ExpectedRuleTypeConstraint::Number => lemma_type.is_number(),
724        ExpectedRuleTypeConstraint::Duration => lemma_type.is_duration(),
725        ExpectedRuleTypeConstraint::Ratio => lemma_type.is_ratio(),
726        ExpectedRuleTypeConstraint::Scale => lemma_type.is_scale(),
727        ExpectedRuleTypeConstraint::Numeric => {
728            lemma_type.is_number() || lemma_type.is_scale() || lemma_type.is_ratio()
729        }
730        ExpectedRuleTypeConstraint::Comparable => {
731            lemma_type.is_boolean()
732                || lemma_type.is_text()
733                || lemma_type.is_number()
734                || lemma_type.is_ratio()
735                || lemma_type.is_date()
736                || lemma_type.is_time()
737                || lemma_type.is_scale()
738                || lemma_type.is_duration()
739        }
740    }
741}
742
743fn collect_expected_constraints_for_rule_ref(
744    expr: &Expression,
745    rule_path: &RulePath,
746    expected: ExpectedRuleTypeConstraint,
747) -> Vec<(Option<Source>, ExpectedRuleTypeConstraint)> {
748    let mut out = Vec::new();
749    match &expr.kind {
750        ExpressionKind::RulePath(rp) => {
751            if rp == rule_path {
752                out.push((expr.source_location.clone(), expected));
753            }
754        }
755        ExpressionKind::LogicalAnd(left, right) => {
756            out.extend(collect_expected_constraints_for_rule_ref(
757                left,
758                rule_path,
759                ExpectedRuleTypeConstraint::Boolean,
760            ));
761            out.extend(collect_expected_constraints_for_rule_ref(
762                right,
763                rule_path,
764                ExpectedRuleTypeConstraint::Boolean,
765            ));
766        }
767        ExpressionKind::LogicalNegation(operand, _) => {
768            out.extend(collect_expected_constraints_for_rule_ref(
769                operand,
770                rule_path,
771                ExpectedRuleTypeConstraint::Boolean,
772            ));
773        }
774        ExpressionKind::Comparison(left, _, right) => {
775            out.extend(collect_expected_constraints_for_rule_ref(
776                left,
777                rule_path,
778                ExpectedRuleTypeConstraint::Comparable,
779            ));
780            out.extend(collect_expected_constraints_for_rule_ref(
781                right,
782                rule_path,
783                ExpectedRuleTypeConstraint::Comparable,
784            ));
785        }
786        ExpressionKind::Arithmetic(left, _, right) => {
787            out.extend(collect_expected_constraints_for_rule_ref(
788                left,
789                rule_path,
790                ExpectedRuleTypeConstraint::Numeric,
791            ));
792            out.extend(collect_expected_constraints_for_rule_ref(
793                right,
794                rule_path,
795                ExpectedRuleTypeConstraint::Numeric,
796            ));
797        }
798        ExpressionKind::UnitConversion(source, target) => {
799            let constraint = match target {
800                SemanticConversionTarget::Duration(_) => ExpectedRuleTypeConstraint::Duration,
801                SemanticConversionTarget::ScaleUnit(_) => ExpectedRuleTypeConstraint::Scale,
802                SemanticConversionTarget::RatioUnit(_) => ExpectedRuleTypeConstraint::Ratio,
803            };
804            out.extend(collect_expected_constraints_for_rule_ref(
805                source, rule_path, constraint,
806            ));
807        }
808        ExpressionKind::MathematicalComputation(_, operand) => {
809            out.extend(collect_expected_constraints_for_rule_ref(
810                operand,
811                rule_path,
812                ExpectedRuleTypeConstraint::Number,
813            ));
814        }
815        ExpressionKind::DateRelative(_, date_expr, tolerance) => {
816            out.extend(collect_expected_constraints_for_rule_ref(
817                date_expr,
818                rule_path,
819                ExpectedRuleTypeConstraint::Comparable,
820            ));
821            if let Some(tol) = tolerance {
822                out.extend(collect_expected_constraints_for_rule_ref(
823                    tol,
824                    rule_path,
825                    ExpectedRuleTypeConstraint::Duration,
826                ));
827            }
828        }
829        ExpressionKind::DateCalendar(_, _, date_expr) => {
830            out.extend(collect_expected_constraints_for_rule_ref(
831                date_expr,
832                rule_path,
833                ExpectedRuleTypeConstraint::Comparable,
834            ));
835        }
836        ExpressionKind::Literal(_)
837        | ExpressionKind::FactPath(_)
838        | ExpressionKind::Veto(_)
839        | ExpressionKind::Now => {}
840    }
841    out
842}
843
844fn expected_constraint_name(c: ExpectedRuleTypeConstraint) -> &'static str {
845    match c {
846        ExpectedRuleTypeConstraint::Numeric => "numeric (number, scale, or ratio)",
847        ExpectedRuleTypeConstraint::Boolean => "boolean",
848        ExpectedRuleTypeConstraint::Comparable => "comparable",
849        ExpectedRuleTypeConstraint::Number => "number",
850        ExpectedRuleTypeConstraint::Duration => "duration",
851        ExpectedRuleTypeConstraint::Ratio => "ratio",
852        ExpectedRuleTypeConstraint::Scale => "scale",
853        ExpectedRuleTypeConstraint::Any => "any",
854    }
855}
856
857fn spec_interface_error(
858    source: &Source,
859    message: impl Into<String>,
860    spec_context: Option<Arc<LemmaSpec>>,
861    related_spec: Option<Arc<LemmaSpec>>,
862) -> Error {
863    Error::validation_with_context(
864        message.into(),
865        Some(source.clone()),
866        None::<String>,
867        spec_context,
868        related_spec,
869    )
870}
871
872/// Validate that every spec-ref fact path's referenced spec has the required rules
873/// and that each such rule's result type satisfies what the referencing rules expect.
874pub fn validate_spec_interfaces(
875    referenced_rules: &HashMap<Vec<String>, HashSet<String>>,
876    spec_ref_facts: &[(FactPath, Arc<LemmaSpec>, Source)],
877    rule_entries: &IndexMap<RulePath, RuleEntryForBindingCheck>,
878    main_spec: &Arc<LemmaSpec>,
879) -> Result<(), Vec<Error>> {
880    let mut errors = Vec::new();
881
882    for (fact_path, spec_arc, fact_source) in spec_ref_facts {
883        let mut full_path: Vec<String> =
884            fact_path.segments.iter().map(|s| s.fact.clone()).collect();
885        full_path.push(fact_path.fact.clone());
886
887        let Some(required_rules) = referenced_rules.get(&full_path) else {
888            continue;
889        };
890
891        let spec = spec_arc.as_ref();
892        let spec_rule_names: HashSet<&str> = spec.rules.iter().map(|r| r.name.as_str()).collect();
893
894        for required_rule in required_rules {
895            if !spec_rule_names.contains(required_rule.as_str()) {
896                errors.push(spec_interface_error(
897                    fact_source,
898                    format!(
899                        "Spec '{}' referenced by '{}' is missing required rule '{}'",
900                        spec.name, fact_path, required_rule
901                    ),
902                    Some(Arc::clone(main_spec)),
903                    Some(Arc::clone(spec_arc)),
904                ));
905                continue;
906            }
907
908            let ref_rule_path = RulePath::new(fact_path.segments.clone(), required_rule.clone());
909            let Some(ref_entry) = rule_entries.get(&ref_rule_path) else {
910                continue;
911            };
912            let ref_rule_type = &ref_entry.rule_type;
913
914            for (_referencing_path, entry) in rule_entries {
915                if !entry.depends_on_rules.contains(&ref_rule_path) {
916                    continue;
917                }
918                let expected = lemma_type_to_expected_constraint(&entry.rule_type);
919                for (_condition, result_expr) in &entry.branches {
920                    let constraints = collect_expected_constraints_for_rule_ref(
921                        result_expr,
922                        &ref_rule_path,
923                        expected,
924                    );
925                    for (_source, constraint) in constraints {
926                        if !rule_type_satisfies_constraint(ref_rule_type, constraint) {
927                            let report_source = fact_source;
928
929                            let binding_path_str = fact_path
930                                .segments
931                                .iter()
932                                .map(|s| s.fact.as_str())
933                                .collect::<Vec<_>>()
934                                .join(".");
935                            let binding_path_str = if binding_path_str.is_empty() {
936                                fact_path.fact.clone()
937                            } else {
938                                format!("{}.{}", binding_path_str, fact_path.fact)
939                            };
940
941                            errors.push(spec_interface_error(
942                                report_source,
943                                format!(
944                                    "Fact binding '{}' sets spec reference to '{}', but that spec's rule '{}' has result type {}; the referencing expression expects a {} value",
945                                    binding_path_str,
946                                    spec.name,
947                                    required_rule,
948                                    ref_rule_type.name(),
949                                    expected_constraint_name(constraint),
950                                ),
951                                Some(Arc::clone(main_spec)),
952                                Some(Arc::clone(spec_arc)),
953                            ));
954                        }
955                    }
956                }
957            }
958        }
959    }
960
961    if errors.is_empty() {
962        Ok(())
963    } else {
964        Err(errors)
965    }
966}
967
968/// Validate that a registry spec (`from_registry == true`) does not contain
969/// bare (non-`@`) references. The registry is responsible for rewriting all
970/// spec references to use `@`-prefixed names before serving the bundle.
971///
972/// Returns a list of bare reference names found, empty if valid.
973pub fn collect_bare_registry_refs(spec: &LemmaSpec) -> Vec<String> {
974    if !spec.from_registry {
975        return Vec::new();
976    }
977    let mut bare: Vec<String> = Vec::new();
978    for fact in &spec.facts {
979        match &fact.value {
980            FactValue::SpecReference(r) if !r.from_registry => {
981                bare.push(r.name.clone());
982            }
983            FactValue::TypeDeclaration { from: Some(r), .. } if !r.from_registry => {
984                bare.push(r.name.clone());
985            }
986            _ => {}
987        }
988    }
989    for type_def in &spec.types {
990        match type_def {
991            TypeDef::Import { from, .. } if !from.from_registry => {
992                bare.push(from.name.clone());
993            }
994            TypeDef::Inline { from: Some(r), .. } if !r.from_registry => {
995                bare.push(r.name.clone());
996            }
997            _ => {}
998        }
999    }
1000    bare
1001}
1002
1003#[cfg(test)]
1004mod tests {
1005    use super::*;
1006    use crate::parsing::ast::{CommandArg, TypeConstraintCommand};
1007    use crate::planning::semantics::TypeSpecification;
1008    use rust_decimal::Decimal;
1009
1010    fn test_source() -> Source {
1011        Source::new(
1012            "<test>",
1013            crate::parsing::ast::Span {
1014                start: 0,
1015                end: 0,
1016                line: 1,
1017                col: 0,
1018            },
1019        )
1020    }
1021
1022    #[test]
1023    fn validate_number_minimum_greater_than_maximum() {
1024        let mut specs = TypeSpecification::number();
1025        specs = specs
1026            .apply_constraint(
1027                TypeConstraintCommand::Minimum,
1028                &[CommandArg::Number("100".to_string())],
1029            )
1030            .unwrap();
1031        specs = specs
1032            .apply_constraint(
1033                TypeConstraintCommand::Maximum,
1034                &[CommandArg::Number("50".to_string())],
1035            )
1036            .unwrap();
1037
1038        let src = test_source();
1039        let errors = validate_type_specifications(&specs, "test", &src, None);
1040        assert_eq!(errors.len(), 1);
1041        assert!(errors[0]
1042            .to_string()
1043            .contains("minimum 100 is greater than maximum 50"));
1044    }
1045
1046    #[test]
1047    fn validate_number_valid_range() {
1048        let mut specs = TypeSpecification::number();
1049        specs = specs
1050            .apply_constraint(
1051                TypeConstraintCommand::Minimum,
1052                &[CommandArg::Number("0".to_string())],
1053            )
1054            .unwrap();
1055        specs = specs
1056            .apply_constraint(
1057                TypeConstraintCommand::Maximum,
1058                &[CommandArg::Number("100".to_string())],
1059            )
1060            .unwrap();
1061
1062        let src = test_source();
1063        let errors = validate_type_specifications(&specs, "test", &src, None);
1064        assert!(errors.is_empty());
1065    }
1066
1067    #[test]
1068    fn validate_number_default_below_minimum() {
1069        let specs = TypeSpecification::Number {
1070            minimum: Some(Decimal::from(10)),
1071            maximum: None,
1072            decimals: None,
1073            precision: None,
1074            help: String::new(),
1075            default: Some(Decimal::from(5)),
1076        };
1077
1078        let src = test_source();
1079        let errors = validate_type_specifications(&specs, "test", &src, None);
1080        assert_eq!(errors.len(), 1);
1081        assert!(errors[0]
1082            .to_string()
1083            .contains("default value 5 is less than minimum 10"));
1084    }
1085
1086    #[test]
1087    fn validate_number_default_above_maximum() {
1088        let specs = TypeSpecification::Number {
1089            minimum: None,
1090            maximum: Some(Decimal::from(100)),
1091            decimals: None,
1092            precision: None,
1093            help: String::new(),
1094            default: Some(Decimal::from(150)),
1095        };
1096
1097        let src = test_source();
1098        let errors = validate_type_specifications(&specs, "test", &src, None);
1099        assert_eq!(errors.len(), 1);
1100        assert!(errors[0]
1101            .to_string()
1102            .contains("default value 150 is greater than maximum 100"));
1103    }
1104
1105    #[test]
1106    fn validate_number_default_valid() {
1107        let specs = TypeSpecification::Number {
1108            minimum: Some(Decimal::from(0)),
1109            maximum: Some(Decimal::from(100)),
1110            decimals: None,
1111            precision: None,
1112            help: String::new(),
1113            default: Some(Decimal::from(50)),
1114        };
1115
1116        let src = test_source();
1117        let errors = validate_type_specifications(&specs, "test", &src, None);
1118        assert!(errors.is_empty());
1119    }
1120
1121    #[test]
1122    fn validate_text_minimum_greater_than_maximum() {
1123        let mut specs = TypeSpecification::text();
1124        specs = specs
1125            .apply_constraint(
1126                TypeConstraintCommand::Minimum,
1127                &[CommandArg::Number("100".to_string())],
1128            )
1129            .unwrap();
1130        specs = specs
1131            .apply_constraint(
1132                TypeConstraintCommand::Maximum,
1133                &[CommandArg::Number("50".to_string())],
1134            )
1135            .unwrap();
1136
1137        let src = test_source();
1138        let errors = validate_type_specifications(&specs, "test", &src, None);
1139        assert_eq!(errors.len(), 1);
1140        assert!(errors[0]
1141            .to_string()
1142            .contains("minimum length 100 is greater than maximum length 50"));
1143    }
1144
1145    #[test]
1146    fn validate_text_length_inconsistent_with_minimum() {
1147        let mut specs = TypeSpecification::text();
1148        specs = specs
1149            .apply_constraint(
1150                TypeConstraintCommand::Minimum,
1151                &[CommandArg::Number("10".to_string())],
1152            )
1153            .unwrap();
1154        specs = specs
1155            .apply_constraint(
1156                TypeConstraintCommand::Length,
1157                &[CommandArg::Number("5".to_string())],
1158            )
1159            .unwrap();
1160
1161        let src = test_source();
1162        let errors = validate_type_specifications(&specs, "test", &src, None);
1163        assert_eq!(errors.len(), 1);
1164        assert!(errors[0]
1165            .to_string()
1166            .contains("length 5 is less than minimum 10"));
1167    }
1168
1169    #[test]
1170    fn validate_text_default_not_in_options() {
1171        let specs = TypeSpecification::Text {
1172            minimum: None,
1173            maximum: None,
1174            length: None,
1175            options: vec!["red".to_string(), "blue".to_string()],
1176            help: String::new(),
1177            default: Some("green".to_string()),
1178        };
1179
1180        let src = test_source();
1181        let errors = validate_type_specifications(&specs, "test", &src, None);
1182        assert_eq!(errors.len(), 1);
1183        assert!(errors[0]
1184            .to_string()
1185            .contains("default value 'green' is not in allowed options"));
1186    }
1187
1188    #[test]
1189    fn validate_text_default_valid_in_options() {
1190        let specs = TypeSpecification::Text {
1191            minimum: None,
1192            maximum: None,
1193            length: None,
1194            options: vec!["red".to_string(), "blue".to_string()],
1195            help: String::new(),
1196            default: Some("red".to_string()),
1197        };
1198
1199        let src = test_source();
1200        let errors = validate_type_specifications(&specs, "test", &src, None);
1201        assert!(errors.is_empty());
1202    }
1203
1204    #[test]
1205    fn validate_ratio_minimum_greater_than_maximum() {
1206        let specs = TypeSpecification::Ratio {
1207            minimum: Some(Decimal::from(2)),
1208            maximum: Some(Decimal::from(1)),
1209            decimals: None,
1210            units: crate::planning::semantics::RatioUnits::new(),
1211            help: String::new(),
1212            default: None,
1213        };
1214
1215        let src = test_source();
1216        let errors = validate_type_specifications(&specs, "test", &src, None);
1217        assert_eq!(errors.len(), 1);
1218        assert!(errors[0]
1219            .to_string()
1220            .contains("minimum 2 is greater than maximum 1"));
1221    }
1222
1223    #[test]
1224    fn validate_date_minimum_after_maximum() {
1225        let mut specs = TypeSpecification::date();
1226        specs = specs
1227            .apply_constraint(
1228                TypeConstraintCommand::Minimum,
1229                &[CommandArg::Label("2024-12-31".to_string())],
1230            )
1231            .unwrap();
1232        specs = specs
1233            .apply_constraint(
1234                TypeConstraintCommand::Maximum,
1235                &[CommandArg::Label("2024-01-01".to_string())],
1236            )
1237            .unwrap();
1238
1239        let src = test_source();
1240        let errors = validate_type_specifications(&specs, "test", &src, None);
1241        assert_eq!(errors.len(), 1);
1242        assert!(
1243            errors[0].to_string().contains("minimum")
1244                && errors[0].to_string().contains("is after maximum")
1245        );
1246    }
1247
1248    #[test]
1249    fn validate_date_valid_range() {
1250        let mut specs = TypeSpecification::date();
1251        specs = specs
1252            .apply_constraint(
1253                TypeConstraintCommand::Minimum,
1254                &[CommandArg::Label("2024-01-01".to_string())],
1255            )
1256            .unwrap();
1257        specs = specs
1258            .apply_constraint(
1259                TypeConstraintCommand::Maximum,
1260                &[CommandArg::Label("2024-12-31".to_string())],
1261            )
1262            .unwrap();
1263
1264        let src = test_source();
1265        let errors = validate_type_specifications(&specs, "test", &src, None);
1266        assert!(errors.is_empty());
1267    }
1268
1269    #[test]
1270    fn validate_time_minimum_after_maximum() {
1271        let mut specs = TypeSpecification::time();
1272        specs = specs
1273            .apply_constraint(
1274                TypeConstraintCommand::Minimum,
1275                &[CommandArg::Label("23:00:00".to_string())],
1276            )
1277            .unwrap();
1278        specs = specs
1279            .apply_constraint(
1280                TypeConstraintCommand::Maximum,
1281                &[CommandArg::Label("10:00:00".to_string())],
1282            )
1283            .unwrap();
1284
1285        let src = test_source();
1286        let errors = validate_type_specifications(&specs, "test", &src, None);
1287        assert_eq!(errors.len(), 1);
1288        assert!(
1289            errors[0].to_string().contains("minimum")
1290                && errors[0].to_string().contains("is after maximum")
1291        );
1292    }
1293
1294    #[test]
1295    fn validate_type_definition_with_invalid_constraints() {
1296        // This test now validates that type specification validation works correctly.
1297        // The actual validation happens during graph building, but we test the validation
1298        // function directly here.
1299        use crate::engine::Context;
1300        use crate::parsing::ast::{LemmaSpec, ParentType, PrimitiveKind, TypeDef};
1301        use crate::planning::types::PerSliceTypeResolver;
1302        use std::sync::Arc;
1303
1304        let spec = Arc::new(LemmaSpec::new("test".to_string()));
1305        let mut ctx = Context::new();
1306        ctx.insert_spec(Arc::clone(&spec), false)
1307            .expect("insert test spec");
1308        let type_def = TypeDef::Regular {
1309            source_location: crate::Source::new(
1310                "<test>",
1311                crate::parsing::ast::Span {
1312                    start: 0,
1313                    end: 0,
1314                    line: 1,
1315                    col: 0,
1316                },
1317            ),
1318            name: "invalid_money".to_string(),
1319            parent: ParentType::Primitive(PrimitiveKind::Number),
1320            constraints: Some(vec![
1321                (
1322                    TypeConstraintCommand::Minimum,
1323                    vec![CommandArg::Number("100".to_string())],
1324                ),
1325                (
1326                    TypeConstraintCommand::Maximum,
1327                    vec![CommandArg::Number("50".to_string())],
1328                ),
1329            ]),
1330        };
1331
1332        let plan_hashes = crate::planning::PlanHashRegistry::default();
1333        let mut type_resolver = PerSliceTypeResolver::new(&ctx, None, &plan_hashes);
1334        type_resolver
1335            .register_type(&spec, type_def)
1336            .expect("Should register type");
1337        let resolved_types = type_resolver
1338            .resolve_named_types(&spec)
1339            .expect("Should resolve types");
1340
1341        // Validate the specifications
1342        let lemma_type = resolved_types
1343            .named_types
1344            .get("invalid_money")
1345            .expect("Should have invalid_money type");
1346        let src = test_source();
1347        let errors =
1348            validate_type_specifications(&lemma_type.specifications, "invalid_money", &src, None);
1349        assert!(!errors.is_empty());
1350        assert!(errors.iter().any(|e| e
1351            .to_string()
1352            .contains("minimum 100 is greater than maximum 50")));
1353    }
1354}