Skip to main content

jugar_probar/pixel_coverage/
falsification.rs

1//! Popperian Falsification Framework (PIXEL-001 v2.1 Phase 5)
2//!
3//! Implements the falsifiability gateway and hypothesis testing per Popper's
4//! scientific methodology. Claims that cannot be falsified are not science.
5
6use super::terminal::ConfidenceInterval;
7
8/// Popperian Falsifiability Gate (Jidoka - stop-the-line)
9/// If hypothesis cannot be falsified, the entire analysis is invalid.
10#[derive(Debug, Clone)]
11pub struct FalsifiabilityGate {
12    /// Minimum threshold for falsifiability score (0-25)
13    pub gateway_threshold: f32,
14}
15
16impl Default for FalsifiabilityGate {
17    fn default() -> Self {
18        Self {
19            gateway_threshold: 15.0,
20        }
21    }
22}
23
24impl FalsifiabilityGate {
25    /// Create a new gate with custom threshold
26    #[must_use]
27    pub fn new(gateway_threshold: f32) -> Self {
28        Self { gateway_threshold }
29    }
30
31    /// Evaluate a hypothesis against the falsifiability gateway
32    #[must_use]
33    pub fn evaluate(&self, hypothesis: &FalsifiableHypothesis) -> GateResult {
34        if hypothesis.falsifiability_score < self.gateway_threshold {
35            GateResult::Failed {
36                score: 0.0,
37                reason: "INSUFFICIENT FALSIFIABILITY - NOT EVALUABLE AS SCIENCE".to_string(),
38            }
39        } else {
40            GateResult::Passed {
41                score: hypothesis.falsifiability_score,
42            }
43        }
44    }
45
46    /// Evaluate multiple hypotheses, returning overall result
47    #[must_use]
48    pub fn evaluate_all(&self, hypotheses: &[FalsifiableHypothesis]) -> GateResult {
49        for h in hypotheses {
50            if let result @ GateResult::Failed { .. } = self.evaluate(h) {
51                return result;
52            }
53        }
54
55        let total_score: f32 = hypotheses.iter().map(|h| h.falsifiability_score).sum();
56        let avg_score = if hypotheses.is_empty() {
57            0.0
58        } else {
59            total_score / hypotheses.len() as f32
60        };
61
62        GateResult::Passed { score: avg_score }
63    }
64}
65
66/// Result of falsifiability gate evaluation
67#[derive(Debug, Clone, PartialEq)]
68pub enum GateResult {
69    /// Hypothesis passed the gateway
70    Passed {
71        /// Falsifiability score achieved
72        score: f32,
73    },
74    /// Hypothesis failed the gateway
75    Failed {
76        /// Score assigned (usually 0)
77        score: f32,
78        /// Reason for failure
79        reason: String,
80    },
81}
82
83impl GateResult {
84    /// Check if result is passed
85    #[must_use]
86    pub fn is_passed(&self) -> bool {
87        matches!(self, Self::Passed { .. })
88    }
89
90    /// Get the score
91    #[must_use]
92    pub fn score(&self) -> f32 {
93        match self {
94            Self::Passed { score } | Self::Failed { score, .. } => *score,
95        }
96    }
97}
98
99/// A condition that would falsify the hypothesis
100#[derive(Debug, Clone)]
101pub struct FalsificationCondition {
102    /// Description of the condition
103    pub description: String,
104    /// Operator for comparison
105    pub operator: ComparisonOperator,
106    /// Target value
107    pub target: f32,
108}
109
110impl FalsificationCondition {
111    /// Create a new condition
112    #[must_use]
113    pub fn new(description: &str, operator: ComparisonOperator, target: f32) -> Self {
114        Self {
115            description: description.to_string(),
116            operator,
117            target,
118        }
119    }
120
121    /// Check if condition is met (hypothesis is falsified)
122    #[must_use]
123    pub fn is_falsified(&self, actual: f32) -> bool {
124        match self.operator {
125            ComparisonOperator::LessThan => actual < self.target,
126            ComparisonOperator::LessOrEqual => actual <= self.target,
127            ComparisonOperator::GreaterThan => actual > self.target,
128            ComparisonOperator::GreaterOrEqual => actual >= self.target,
129            ComparisonOperator::Equal => (actual - self.target).abs() < f32::EPSILON,
130            ComparisonOperator::NotEqual => (actual - self.target).abs() >= f32::EPSILON,
131        }
132    }
133}
134
135/// Comparison operators for falsification conditions
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub enum ComparisonOperator {
138    /// <
139    LessThan,
140    /// <=
141    LessOrEqual,
142    /// >
143    GreaterThan,
144    /// >=
145    GreaterOrEqual,
146    /// ==
147    Equal,
148    /// !=
149    NotEqual,
150}
151
152/// A falsifiable hypothesis about pixel coverage (full Popperian specification)
153#[derive(Debug, Clone)]
154pub struct FalsifiableHypothesis {
155    /// Hypothesis ID (e.g., "H0-COV-01")
156    pub id: String,
157    /// Hâ‚€: The null hypothesis to falsify
158    pub null_hypothesis: String,
159    /// Measurable threshold (falsification criterion)
160    pub threshold: f32,
161    /// Actual measured value
162    pub actual: Option<f32>,
163    /// Confidence interval for statistical rigor
164    pub confidence_interval: Option<ConfidenceInterval>,
165    /// What would falsify this claim
166    pub falsification_conditions: Vec<FalsificationCondition>,
167    /// Falsifiability score (0-25, gate requires >= 15)
168    pub falsifiability_score: f32,
169    /// Whether the hypothesis has been falsified
170    pub falsified: bool,
171}
172
173impl FalsifiableHypothesis {
174    /// Create a new hypothesis builder
175    #[must_use]
176    pub fn builder(id: &str) -> FalsifiableHypothesisBuilder {
177        FalsifiableHypothesisBuilder::new(id)
178    }
179
180    /// Create a standard coverage threshold hypothesis
181    #[must_use]
182    pub fn coverage_threshold(id: &str, threshold: f32) -> Self {
183        Self::builder(id)
184            .null_hypothesis(&format!(
185                "Coverage exceeds {:.0}% of screen pixels",
186                threshold * 100.0
187            ))
188            .threshold(threshold)
189            .falsification_condition(FalsificationCondition::new(
190                &format!("Coverage < {:.0}%", threshold * 100.0),
191                ComparisonOperator::LessThan,
192                threshold,
193            ))
194            .falsifiability_score(20.0) // High falsifiability: clear measurement
195            .build()
196    }
197
198    /// Create a gap size hypothesis
199    #[must_use]
200    pub fn max_gap_size(id: &str, max_gap_percent: f32) -> Self {
201        Self::builder(id)
202            .null_hypothesis(&format!(
203                "No gap region exceeds {:.0}% of total area",
204                max_gap_percent * 100.0
205            ))
206            .threshold(max_gap_percent)
207            .falsification_condition(FalsificationCondition::new(
208                &format!("Gap > {:.0}% detected", max_gap_percent * 100.0),
209                ComparisonOperator::GreaterThan,
210                max_gap_percent,
211            ))
212            .falsifiability_score(22.0) // Very high falsifiability: specific region
213            .build()
214    }
215
216    /// Create an SSIM threshold hypothesis
217    #[must_use]
218    pub fn ssim_threshold(id: &str, min_ssim: f32) -> Self {
219        Self::builder(id)
220            .null_hypothesis(&format!(
221                "Rendered heatmap matches reference within SSIM >= {:.2}",
222                min_ssim
223            ))
224            .threshold(min_ssim)
225            .falsification_condition(FalsificationCondition::new(
226                &format!("SSIM < {:.2}", min_ssim),
227                ComparisonOperator::LessThan,
228                min_ssim,
229            ))
230            .falsifiability_score(25.0) // Maximum falsifiability: pixel-perfect
231            .build()
232    }
233
234    /// Evaluate the hypothesis with actual measurement
235    #[must_use]
236    pub fn evaluate(&self, actual: f32) -> FalsifiableHypothesis {
237        let mut result = self.clone();
238        result.actual = Some(actual);
239
240        // Check all falsification conditions
241        result.falsified = self
242            .falsification_conditions
243            .iter()
244            .any(|c| c.is_falsified(actual));
245
246        result
247    }
248}
249
250/// Builder for FalsifiableHypothesis
251#[derive(Debug, Default)]
252pub struct FalsifiableHypothesisBuilder {
253    id: String,
254    null_hypothesis: String,
255    threshold: f32,
256    confidence_interval: Option<ConfidenceInterval>,
257    falsification_conditions: Vec<FalsificationCondition>,
258    falsifiability_score: f32,
259}
260
261impl FalsifiableHypothesisBuilder {
262    /// Create new builder
263    #[must_use]
264    pub fn new(id: &str) -> Self {
265        Self {
266            id: id.to_string(),
267            falsifiability_score: 15.0, // Default to gateway threshold
268            ..Default::default()
269        }
270    }
271
272    /// Set null hypothesis
273    #[must_use]
274    pub fn null_hypothesis(mut self, hypothesis: &str) -> Self {
275        self.null_hypothesis = hypothesis.to_string();
276        self
277    }
278
279    /// Set threshold
280    #[must_use]
281    pub fn threshold(mut self, threshold: f32) -> Self {
282        self.threshold = threshold;
283        self
284    }
285
286    /// Set confidence interval
287    #[must_use]
288    pub fn confidence_interval(mut self, ci: ConfidenceInterval) -> Self {
289        self.confidence_interval = Some(ci);
290        self
291    }
292
293    /// Add falsification condition
294    #[must_use]
295    pub fn falsification_condition(mut self, condition: FalsificationCondition) -> Self {
296        self.falsification_conditions.push(condition);
297        self
298    }
299
300    /// Set falsifiability score
301    #[must_use]
302    pub fn falsifiability_score(mut self, score: f32) -> Self {
303        self.falsifiability_score = score.clamp(0.0, 25.0);
304        self
305    }
306
307    /// Build the hypothesis
308    #[must_use]
309    pub fn build(self) -> FalsifiableHypothesis {
310        FalsifiableHypothesis {
311            id: self.id,
312            null_hypothesis: self.null_hypothesis,
313            threshold: self.threshold,
314            actual: None,
315            confidence_interval: self.confidence_interval,
316            falsification_conditions: self.falsification_conditions,
317            falsifiability_score: self.falsifiability_score,
318            falsified: false,
319        }
320    }
321}
322
323/// Layer of falsification testing
324#[derive(Debug, Clone, Copy, PartialEq, Eq)]
325pub enum FalsificationLayer {
326    /// L1: Unit tests - direct falsification via assertions
327    Unit,
328    /// L2: Property tests - statistical falsification via proptest
329    Property,
330    /// L3: Mutation tests - meta-falsification via mutation score
331    Mutation,
332}
333
334impl FalsificationLayer {
335    /// Get the layer number
336    #[must_use]
337    pub fn number(&self) -> u8 {
338        match self {
339            Self::Unit => 1,
340            Self::Property => 2,
341            Self::Mutation => 3,
342        }
343    }
344
345    /// Get layer description
346    #[must_use]
347    pub fn description(&self) -> &'static str {
348        match self {
349            Self::Unit => "Direct falsification via assertions",
350            Self::Property => "Statistical falsification via proptest",
351            Self::Mutation => "Meta-falsification via mutation score",
352        }
353    }
354}
355
356#[cfg(test)]
357#[allow(clippy::unwrap_used)]
358mod tests {
359    use super::*;
360
361    // =========================================================================
362    // FalsifiabilityGate Tests (H0-GATE-XX)
363    // =========================================================================
364
365    #[test]
366    fn h0_gate_01_default_threshold() {
367        let gate = FalsifiabilityGate::default();
368        assert!((gate.gateway_threshold - 15.0).abs() < f32::EPSILON);
369    }
370
371    #[test]
372    fn h0_gate_02_custom_threshold() {
373        let gate = FalsifiabilityGate::new(20.0);
374        assert!((gate.gateway_threshold - 20.0).abs() < f32::EPSILON);
375    }
376
377    #[test]
378    fn h0_gate_03_evaluate_pass() {
379        let gate = FalsifiabilityGate::default();
380        let hypothesis = FalsifiableHypothesis::coverage_threshold("H0-COV-01", 0.85);
381        let result = gate.evaluate(&hypothesis);
382        assert!(result.is_passed());
383        assert!((result.score() - 20.0).abs() < f32::EPSILON);
384    }
385
386    #[test]
387    fn h0_gate_04_evaluate_fail() {
388        let gate = FalsifiabilityGate::new(23.0);
389        let hypothesis = FalsifiableHypothesis::coverage_threshold("H0-COV-01", 0.85);
390        let result = gate.evaluate(&hypothesis);
391        assert!(!result.is_passed());
392        assert!((result.score() - 0.0).abs() < f32::EPSILON);
393    }
394
395    #[test]
396    fn h0_gate_05_evaluate_all_pass() {
397        let gate = FalsifiabilityGate::default();
398        let hypotheses = vec![
399            FalsifiableHypothesis::coverage_threshold("H0-COV-01", 0.85),
400            FalsifiableHypothesis::max_gap_size("H0-COV-02", 0.15),
401        ];
402        let result = gate.evaluate_all(&hypotheses);
403        assert!(result.is_passed());
404    }
405
406    #[test]
407    fn h0_gate_06_evaluate_all_fail() {
408        let gate = FalsifiabilityGate::new(24.0);
409        let hypotheses = vec![
410            FalsifiableHypothesis::coverage_threshold("H0-COV-01", 0.85), // score: 20
411            FalsifiableHypothesis::max_gap_size("H0-COV-02", 0.15),       // score: 22
412        ];
413        let result = gate.evaluate_all(&hypotheses);
414        assert!(!result.is_passed()); // First fails immediately
415    }
416
417    // =========================================================================
418    // FalsificationCondition Tests (H0-COND-XX)
419    // =========================================================================
420
421    #[test]
422    fn h0_cond_01_less_than() {
423        let cond =
424            FalsificationCondition::new("Coverage < 85%", ComparisonOperator::LessThan, 0.85);
425        assert!(cond.is_falsified(0.80)); // 80% < 85%, falsified
426        assert!(!cond.is_falsified(0.90)); // 90% >= 85%, not falsified
427    }
428
429    #[test]
430    fn h0_cond_02_greater_than() {
431        let cond = FalsificationCondition::new("Gap > 15%", ComparisonOperator::GreaterThan, 0.15);
432        assert!(cond.is_falsified(0.20)); // 20% > 15%, falsified
433        assert!(!cond.is_falsified(0.10)); // 10% <= 15%, not falsified
434    }
435
436    #[test]
437    fn h0_cond_03_equal() {
438        let cond = FalsificationCondition::new("Score == 1.0", ComparisonOperator::Equal, 1.0);
439        assert!(cond.is_falsified(1.0));
440        assert!(!cond.is_falsified(0.99));
441    }
442
443    // =========================================================================
444    // FalsifiableHypothesis Tests (H0-HYP-XX)
445    // =========================================================================
446
447    #[test]
448    fn h0_hyp_01_coverage_threshold_pass() {
449        let hypothesis = FalsifiableHypothesis::coverage_threshold("H0-COV-01", 0.85);
450        let result = hypothesis.evaluate(0.90);
451        assert!(!result.falsified);
452        assert!((result.actual.unwrap() - 0.90).abs() < f32::EPSILON);
453    }
454
455    #[test]
456    fn h0_hyp_02_coverage_threshold_fail() {
457        let hypothesis = FalsifiableHypothesis::coverage_threshold("H0-COV-01", 0.85);
458        let result = hypothesis.evaluate(0.80);
459        assert!(result.falsified);
460    }
461
462    #[test]
463    fn h0_hyp_03_gap_size_pass() {
464        let hypothesis = FalsifiableHypothesis::max_gap_size("H0-COV-02", 0.15);
465        let result = hypothesis.evaluate(0.10);
466        assert!(!result.falsified);
467    }
468
469    #[test]
470    fn h0_hyp_04_gap_size_fail() {
471        let hypothesis = FalsifiableHypothesis::max_gap_size("H0-COV-02", 0.15);
472        let result = hypothesis.evaluate(0.20);
473        assert!(result.falsified);
474    }
475
476    #[test]
477    fn h0_hyp_05_ssim_threshold() {
478        let hypothesis = FalsifiableHypothesis::ssim_threshold("H0-VIS-01", 0.99);
479        assert!((hypothesis.falsifiability_score - 25.0).abs() < f32::EPSILON);
480        let result = hypothesis.evaluate(0.985);
481        assert!(result.falsified);
482    }
483
484    #[test]
485    fn h0_hyp_06_builder() {
486        let hypothesis = FalsifiableHypothesis::builder("H0-CUSTOM")
487            .null_hypothesis("Custom hypothesis")
488            .threshold(0.75)
489            .falsifiability_score(18.0)
490            .falsification_condition(FalsificationCondition::new(
491                "Value < 75%",
492                ComparisonOperator::LessThan,
493                0.75,
494            ))
495            .build();
496
497        assert_eq!(hypothesis.id, "H0-CUSTOM");
498        assert!((hypothesis.falsifiability_score - 18.0).abs() < f32::EPSILON);
499        assert_eq!(hypothesis.falsification_conditions.len(), 1);
500    }
501
502    // =========================================================================
503    // FalsificationLayer Tests (H0-LAYER-XX)
504    // =========================================================================
505
506    #[test]
507    fn h0_layer_01_numbers() {
508        assert_eq!(FalsificationLayer::Unit.number(), 1);
509        assert_eq!(FalsificationLayer::Property.number(), 2);
510        assert_eq!(FalsificationLayer::Mutation.number(), 3);
511    }
512
513    #[test]
514    fn h0_layer_02_descriptions() {
515        assert!(FalsificationLayer::Unit.description().contains("assertion"));
516        assert!(FalsificationLayer::Property
517            .description()
518            .contains("proptest"));
519        assert!(FalsificationLayer::Mutation
520            .description()
521            .contains("mutation"));
522    }
523
524    // =========================================================================
525    // GateResult Tests (H0-RESULT-XX)
526    // =========================================================================
527
528    #[test]
529    fn h0_result_01_passed() {
530        let result = GateResult::Passed { score: 20.0 };
531        assert!(result.is_passed());
532        assert!((result.score() - 20.0).abs() < f32::EPSILON);
533    }
534
535    #[test]
536    fn h0_result_02_failed() {
537        let result = GateResult::Failed {
538            score: 0.0,
539            reason: "Test failure".to_string(),
540        };
541        assert!(!result.is_passed());
542        assert!((result.score() - 0.0).abs() < f32::EPSILON);
543    }
544}
545
546// =============================================================================
547// Property-Based Tests (Extreme TDD - L2 Falsification Layer)
548// =============================================================================
549
550#[cfg(test)]
551mod proptest_tests {
552    use super::*;
553    use proptest::prelude::*;
554
555    // =========================================================================
556    // FalsifiabilityGate Property Tests (PROP-GATE-XX)
557    // =========================================================================
558
559    proptest! {
560        /// PROP-GATE-01: Gate with 0 threshold always passes
561        #[test]
562        fn prop_gate_01_zero_threshold_passes(score in 0.0f32..=25.0) {
563            let gate = FalsifiabilityGate::new(0.0);
564            let hypothesis = FalsifiableHypothesis::builder("H0-TEST")
565                .null_hypothesis("Test")
566                .falsifiability_score(score)
567                .build();
568            let result = gate.evaluate(&hypothesis);
569            prop_assert!(result.is_passed());
570        }
571
572        /// PROP-GATE-02: Gate with 25 threshold only passes scores >= 25
573        #[test]
574        fn prop_gate_02_max_threshold(score in 0.0f32..25.0) {
575            let gate = FalsifiabilityGate::new(25.0);
576            let hypothesis = FalsifiableHypothesis::builder("H0-TEST")
577                .null_hypothesis("Test")
578                .falsifiability_score(score)
579                .build();
580            let result = gate.evaluate(&hypothesis);
581            prop_assert!(!result.is_passed(), "Score {} should fail threshold 25", score);
582        }
583
584        /// PROP-GATE-03: Score exactly at threshold passes
585        #[test]
586        fn prop_gate_03_exact_threshold(threshold in 0.0f32..=25.0) {
587            let gate = FalsifiabilityGate::new(threshold);
588            let hypothesis = FalsifiableHypothesis::builder("H0-TEST")
589                .null_hypothesis("Test")
590                .falsifiability_score(threshold)
591                .build();
592            let result = gate.evaluate(&hypothesis);
593            prop_assert!(result.is_passed());
594        }
595    }
596
597    // =========================================================================
598    // Hypothesis Property Tests (PROP-HYP-XX)
599    // =========================================================================
600
601    proptest! {
602        /// PROP-HYP-01: Coverage hypothesis falsified when actual < threshold
603        #[test]
604        fn prop_hyp_01_coverage_falsified(
605            threshold in 0.01f32..=1.0,
606            delta in 0.01f32..=0.5
607        ) {
608            let actual = (threshold - delta).max(0.0);
609            let hypothesis = FalsifiableHypothesis::coverage_threshold("H0-COV", threshold);
610            let result = hypothesis.evaluate(actual);
611            prop_assert!(result.falsified, "Should be falsified: {} < {}", actual, threshold);
612        }
613
614        /// PROP-HYP-02: Coverage hypothesis not falsified when actual >= threshold
615        #[test]
616        fn prop_hyp_02_coverage_not_falsified(
617            threshold in 0.0f32..=0.99,
618            delta in 0.0f32..=0.5
619        ) {
620            let actual = (threshold + delta).min(1.0);
621            let hypothesis = FalsifiableHypothesis::coverage_threshold("H0-COV", threshold);
622            let result = hypothesis.evaluate(actual);
623            prop_assert!(!result.falsified, "Should not be falsified: {} >= {}", actual, threshold);
624        }
625
626        /// PROP-HYP-03: Gap hypothesis falsified when actual > max
627        #[test]
628        fn prop_hyp_03_gap_falsified(
629            max_gap in 0.0f32..=0.99,
630            delta in 0.01f32..=0.5
631        ) {
632            let actual = (max_gap + delta).min(1.0);
633            let hypothesis = FalsifiableHypothesis::max_gap_size("H0-GAP", max_gap);
634            let result = hypothesis.evaluate(actual);
635            prop_assert!(result.falsified, "Should be falsified: {} > {}", actual, max_gap);
636        }
637
638        /// PROP-HYP-04: SSIM hypothesis score is maximum (25)
639        #[test]
640        fn prop_hyp_04_ssim_max_score(threshold in 0.0f32..=1.0) {
641            let hypothesis = FalsifiableHypothesis::ssim_threshold("H0-SSIM", threshold);
642            prop_assert!((hypothesis.falsifiability_score - 25.0).abs() < f32::EPSILON);
643        }
644    }
645
646    // =========================================================================
647    // Condition Property Tests (PROP-COND-XX)
648    // =========================================================================
649
650    proptest! {
651        /// PROP-COND-01: LessThan falsifies when actual < target
652        #[test]
653        fn prop_cond_01_less_than(target in -100.0f32..=100.0, delta in 0.01f32..=50.0) {
654            let cond = FalsificationCondition::new("Test", ComparisonOperator::LessThan, target);
655            let actual = target - delta;
656            prop_assert!(cond.is_falsified(actual));
657        }
658
659        /// PROP-COND-02: GreaterThan falsifies when actual > target
660        #[test]
661        fn prop_cond_02_greater_than(target in -100.0f32..=100.0, delta in 0.01f32..=50.0) {
662            let cond = FalsificationCondition::new("Test", ComparisonOperator::GreaterThan, target);
663            let actual = target + delta;
664            prop_assert!(cond.is_falsified(actual));
665        }
666
667        /// PROP-COND-03: Equal falsifies when actual == target
668        #[test]
669        fn prop_cond_03_equal(target in -100.0f32..=100.0) {
670            let cond = FalsificationCondition::new("Test", ComparisonOperator::Equal, target);
671            prop_assert!(cond.is_falsified(target));
672        }
673
674        /// PROP-COND-04: NotEqual falsifies when actual != target
675        #[test]
676        fn prop_cond_04_not_equal(target in -100.0f32..=100.0, delta in 0.01f32..=50.0) {
677            let cond = FalsificationCondition::new("Test", ComparisonOperator::NotEqual, target);
678            let actual = target + delta;
679            prop_assert!(cond.is_falsified(actual));
680        }
681    }
682
683    // =========================================================================
684    // Builder Property Tests (PROP-BUILD-XX)
685    // =========================================================================
686
687    proptest! {
688        /// PROP-BUILD-01: Builder preserves ID
689        #[test]
690        fn prop_build_01_preserves_id(id in "[A-Z0-9-]{1,20}") {
691            let hypothesis = FalsifiableHypothesis::builder(&id)
692                .null_hypothesis("Test")
693                .build();
694            prop_assert_eq!(hypothesis.id, id);
695        }
696
697        /// PROP-BUILD-02: Builder clamps score to [0, 25]
698        #[test]
699        fn prop_build_02_clamps_score(score in -100.0f32..=100.0) {
700            let hypothesis = FalsifiableHypothesis::builder("H0-TEST")
701                .null_hypothesis("Test")
702                .falsifiability_score(score)
703                .build();
704            prop_assert!(hypothesis.falsifiability_score >= 0.0);
705            prop_assert!(hypothesis.falsifiability_score <= 25.0);
706        }
707    }
708}