1use super::terminal::ConfidenceInterval;
7
8#[derive(Debug, Clone)]
11pub struct FalsifiabilityGate {
12 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 #[must_use]
27 pub fn new(gateway_threshold: f32) -> Self {
28 Self { gateway_threshold }
29 }
30
31 #[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 #[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#[derive(Debug, Clone, PartialEq)]
68pub enum GateResult {
69 Passed {
71 score: f32,
73 },
74 Failed {
76 score: f32,
78 reason: String,
80 },
81}
82
83impl GateResult {
84 #[must_use]
86 pub fn is_passed(&self) -> bool {
87 matches!(self, Self::Passed { .. })
88 }
89
90 #[must_use]
92 pub fn score(&self) -> f32 {
93 match self {
94 Self::Passed { score } | Self::Failed { score, .. } => *score,
95 }
96 }
97}
98
99#[derive(Debug, Clone)]
101pub struct FalsificationCondition {
102 pub description: String,
104 pub operator: ComparisonOperator,
106 pub target: f32,
108}
109
110impl FalsificationCondition {
111 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub enum ComparisonOperator {
138 LessThan,
140 LessOrEqual,
142 GreaterThan,
144 GreaterOrEqual,
146 Equal,
148 NotEqual,
150}
151
152#[derive(Debug, Clone)]
154pub struct FalsifiableHypothesis {
155 pub id: String,
157 pub null_hypothesis: String,
159 pub threshold: f32,
161 pub actual: Option<f32>,
163 pub confidence_interval: Option<ConfidenceInterval>,
165 pub falsification_conditions: Vec<FalsificationCondition>,
167 pub falsifiability_score: f32,
169 pub falsified: bool,
171}
172
173impl FalsifiableHypothesis {
174 #[must_use]
176 pub fn builder(id: &str) -> FalsifiableHypothesisBuilder {
177 FalsifiableHypothesisBuilder::new(id)
178 }
179
180 #[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) .build()
196 }
197
198 #[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) .build()
214 }
215
216 #[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) .build()
232 }
233
234 #[must_use]
236 pub fn evaluate(&self, actual: f32) -> FalsifiableHypothesis {
237 let mut result = self.clone();
238 result.actual = Some(actual);
239
240 result.falsified = self
242 .falsification_conditions
243 .iter()
244 .any(|c| c.is_falsified(actual));
245
246 result
247 }
248}
249
250#[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 #[must_use]
264 pub fn new(id: &str) -> Self {
265 Self {
266 id: id.to_string(),
267 falsifiability_score: 15.0, ..Default::default()
269 }
270 }
271
272 #[must_use]
274 pub fn null_hypothesis(mut self, hypothesis: &str) -> Self {
275 self.null_hypothesis = hypothesis.to_string();
276 self
277 }
278
279 #[must_use]
281 pub fn threshold(mut self, threshold: f32) -> Self {
282 self.threshold = threshold;
283 self
284 }
285
286 #[must_use]
288 pub fn confidence_interval(mut self, ci: ConfidenceInterval) -> Self {
289 self.confidence_interval = Some(ci);
290 self
291 }
292
293 #[must_use]
295 pub fn falsification_condition(mut self, condition: FalsificationCondition) -> Self {
296 self.falsification_conditions.push(condition);
297 self
298 }
299
300 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
325pub enum FalsificationLayer {
326 Unit,
328 Property,
330 Mutation,
332}
333
334impl FalsificationLayer {
335 #[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 #[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 #[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), FalsifiableHypothesis::max_gap_size("H0-COV-02", 0.15), ];
413 let result = gate.evaluate_all(&hypotheses);
414 assert!(!result.is_passed()); }
416
417 #[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)); assert!(!cond.is_falsified(0.90)); }
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)); assert!(!cond.is_falsified(0.10)); }
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 #[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 #[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 #[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#[cfg(test)]
551mod proptest_tests {
552 use super::*;
553 use proptest::prelude::*;
554
555 proptest! {
560 #[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 #[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 #[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 proptest! {
602 #[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 #[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 #[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 #[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 proptest! {
651 #[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 #[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 #[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 #[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 proptest! {
688 #[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 #[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}