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() -> 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            Arc::from("spec test\nfact x: 1"),
914        )
915    }
916
917    #[test]
918    fn validate_number_minimum_greater_than_maximum() {
919        let mut specs = TypeSpecification::number();
920        specs = specs
921            .apply_constraint("minimum", &[CommandArg::Number("100".to_string())])
922            .unwrap();
923        specs = specs
924            .apply_constraint("maximum", &[CommandArg::Number("50".to_string())])
925            .unwrap();
926
927        let src = test_source();
928        let errors = validate_type_specifications(&specs, "test", &src);
929        assert_eq!(errors.len(), 1);
930        assert!(errors[0]
931            .to_string()
932            .contains("minimum 100 is greater than maximum 50"));
933    }
934
935    #[test]
936    fn validate_number_valid_range() {
937        let mut specs = TypeSpecification::number();
938        specs = specs
939            .apply_constraint("minimum", &[CommandArg::Number("0".to_string())])
940            .unwrap();
941        specs = specs
942            .apply_constraint("maximum", &[CommandArg::Number("100".to_string())])
943            .unwrap();
944
945        let src = test_source();
946        let errors = validate_type_specifications(&specs, "test", &src);
947        assert!(errors.is_empty());
948    }
949
950    #[test]
951    fn validate_number_default_below_minimum() {
952        let specs = TypeSpecification::Number {
953            minimum: Some(Decimal::from(10)),
954            maximum: None,
955            decimals: None,
956            precision: None,
957            help: String::new(),
958            default: Some(Decimal::from(5)),
959        };
960
961        let src = test_source();
962        let errors = validate_type_specifications(&specs, "test", &src);
963        assert_eq!(errors.len(), 1);
964        assert!(errors[0]
965            .to_string()
966            .contains("default value 5 is less than minimum 10"));
967    }
968
969    #[test]
970    fn validate_number_default_above_maximum() {
971        let specs = TypeSpecification::Number {
972            minimum: None,
973            maximum: Some(Decimal::from(100)),
974            decimals: None,
975            precision: None,
976            help: String::new(),
977            default: Some(Decimal::from(150)),
978        };
979
980        let src = test_source();
981        let errors = validate_type_specifications(&specs, "test", &src);
982        assert_eq!(errors.len(), 1);
983        assert!(errors[0]
984            .to_string()
985            .contains("default value 150 is greater than maximum 100"));
986    }
987
988    #[test]
989    fn validate_number_default_valid() {
990        let specs = TypeSpecification::Number {
991            minimum: Some(Decimal::from(0)),
992            maximum: Some(Decimal::from(100)),
993            decimals: None,
994            precision: None,
995            help: String::new(),
996            default: Some(Decimal::from(50)),
997        };
998
999        let src = test_source();
1000        let errors = validate_type_specifications(&specs, "test", &src);
1001        assert!(errors.is_empty());
1002    }
1003
1004    #[test]
1005    fn validate_text_minimum_greater_than_maximum() {
1006        let mut specs = TypeSpecification::text();
1007        specs = specs
1008            .apply_constraint("minimum", &[CommandArg::Number("100".to_string())])
1009            .unwrap();
1010        specs = specs
1011            .apply_constraint("maximum", &[CommandArg::Number("50".to_string())])
1012            .unwrap();
1013
1014        let src = test_source();
1015        let errors = validate_type_specifications(&specs, "test", &src);
1016        assert_eq!(errors.len(), 1);
1017        assert!(errors[0]
1018            .to_string()
1019            .contains("minimum length 100 is greater than maximum length 50"));
1020    }
1021
1022    #[test]
1023    fn validate_text_length_inconsistent_with_minimum() {
1024        let mut specs = TypeSpecification::text();
1025        specs = specs
1026            .apply_constraint("minimum", &[CommandArg::Number("10".to_string())])
1027            .unwrap();
1028        specs = specs
1029            .apply_constraint("length", &[CommandArg::Number("5".to_string())])
1030            .unwrap();
1031
1032        let src = test_source();
1033        let errors = validate_type_specifications(&specs, "test", &src);
1034        assert_eq!(errors.len(), 1);
1035        assert!(errors[0]
1036            .to_string()
1037            .contains("length 5 is less than minimum 10"));
1038    }
1039
1040    #[test]
1041    fn validate_text_default_not_in_options() {
1042        let specs = TypeSpecification::Text {
1043            minimum: None,
1044            maximum: None,
1045            length: None,
1046            options: vec!["red".to_string(), "blue".to_string()],
1047            help: String::new(),
1048            default: Some("green".to_string()),
1049        };
1050
1051        let src = test_source();
1052        let errors = validate_type_specifications(&specs, "test", &src);
1053        assert_eq!(errors.len(), 1);
1054        assert!(errors[0]
1055            .to_string()
1056            .contains("default value 'green' is not in allowed options"));
1057    }
1058
1059    #[test]
1060    fn validate_text_default_valid_in_options() {
1061        let specs = TypeSpecification::Text {
1062            minimum: None,
1063            maximum: None,
1064            length: None,
1065            options: vec!["red".to_string(), "blue".to_string()],
1066            help: String::new(),
1067            default: Some("red".to_string()),
1068        };
1069
1070        let src = test_source();
1071        let errors = validate_type_specifications(&specs, "test", &src);
1072        assert!(errors.is_empty());
1073    }
1074
1075    #[test]
1076    fn validate_ratio_minimum_greater_than_maximum() {
1077        let specs = TypeSpecification::Ratio {
1078            minimum: Some(Decimal::from(2)),
1079            maximum: Some(Decimal::from(1)),
1080            decimals: None,
1081            units: crate::planning::semantics::RatioUnits::new(),
1082            help: String::new(),
1083            default: None,
1084        };
1085
1086        let src = test_source();
1087        let errors = validate_type_specifications(&specs, "test", &src);
1088        assert_eq!(errors.len(), 1);
1089        assert!(errors[0]
1090            .to_string()
1091            .contains("minimum 2 is greater than maximum 1"));
1092    }
1093
1094    #[test]
1095    fn validate_date_minimum_after_maximum() {
1096        let mut specs = TypeSpecification::date();
1097        specs = specs
1098            .apply_constraint("minimum", &[CommandArg::Label("2024-12-31".to_string())])
1099            .unwrap();
1100        specs = specs
1101            .apply_constraint("maximum", &[CommandArg::Label("2024-01-01".to_string())])
1102            .unwrap();
1103
1104        let src = test_source();
1105        let errors = validate_type_specifications(&specs, "test", &src);
1106        assert_eq!(errors.len(), 1);
1107        assert!(
1108            errors[0].to_string().contains("minimum")
1109                && errors[0].to_string().contains("is after maximum")
1110        );
1111    }
1112
1113    #[test]
1114    fn validate_date_valid_range() {
1115        let mut specs = TypeSpecification::date();
1116        specs = specs
1117            .apply_constraint("minimum", &[CommandArg::Label("2024-01-01".to_string())])
1118            .unwrap();
1119        specs = specs
1120            .apply_constraint("maximum", &[CommandArg::Label("2024-12-31".to_string())])
1121            .unwrap();
1122
1123        let src = test_source();
1124        let errors = validate_type_specifications(&specs, "test", &src);
1125        assert!(errors.is_empty());
1126    }
1127
1128    #[test]
1129    fn validate_time_minimum_after_maximum() {
1130        let mut specs = TypeSpecification::time();
1131        specs = specs
1132            .apply_constraint("minimum", &[CommandArg::Label("23:00:00".to_string())])
1133            .unwrap();
1134        specs = specs
1135            .apply_constraint("maximum", &[CommandArg::Label("10:00:00".to_string())])
1136            .unwrap();
1137
1138        let src = test_source();
1139        let errors = validate_type_specifications(&specs, "test", &src);
1140        assert_eq!(errors.len(), 1);
1141        assert!(
1142            errors[0].to_string().contains("minimum")
1143                && errors[0].to_string().contains("is after maximum")
1144        );
1145    }
1146
1147    #[test]
1148    fn validate_type_definition_with_invalid_constraints() {
1149        // This test now validates that type specification validation works correctly.
1150        // The actual validation happens during graph building, but we test the validation
1151        // function directly here.
1152        use crate::parsing::ast::{LemmaSpec, TypeDef};
1153        use crate::planning::types::TypeResolver;
1154        use std::sync::Arc;
1155
1156        let spec = Arc::new(LemmaSpec::new("test".to_string()));
1157        let type_def = TypeDef::Regular {
1158            source_location: crate::Source::new(
1159                "<test>",
1160                crate::parsing::ast::Span {
1161                    start: 0,
1162                    end: 0,
1163                    line: 1,
1164                    col: 0,
1165                },
1166                Arc::from("spec test\nfact x: 1"),
1167            ),
1168            name: "invalid_money".to_string(),
1169            parent: "number".to_string(),
1170            constraints: Some(vec![
1171                (
1172                    "minimum".to_string(),
1173                    vec![CommandArg::Number("100".to_string())],
1174                ),
1175                (
1176                    "maximum".to_string(),
1177                    vec![CommandArg::Number("50".to_string())],
1178                ),
1179            ]),
1180        };
1181
1182        // Register and resolve the type to get its specifications
1183        let mut sources = HashMap::new();
1184        sources.insert("<test>".to_string(), String::new());
1185        let mut type_resolver = TypeResolver::new();
1186        type_resolver
1187            .register_type(&spec, type_def)
1188            .expect("Should register type");
1189        let resolved_types = type_resolver
1190            .resolve_named_types(&spec)
1191            .expect("Should resolve types");
1192
1193        // Validate the specifications
1194        let lemma_type = resolved_types
1195            .named_types
1196            .get("invalid_money")
1197            .expect("Should have invalid_money type");
1198        let src = test_source();
1199        let errors =
1200            validate_type_specifications(&lemma_type.specifications, "invalid_money", &src);
1201        assert!(!errors.is_empty());
1202        assert!(errors.iter().any(|e| e
1203            .to_string()
1204            .contains("minimum 100 is greater than maximum 50")));
1205    }
1206}