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