Skip to main content

lemma/planning/
validation.rs

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