## Coverage Report

#### Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /home/noah/src/trueno/src/simulation/jidoka.rs

    Line
    Count
    Source
    1
    //! Jidoka Guard (Built-in Quality: Stop on Defect)
    2
    //!
    3
    //! Implements Toyota Production System's Jidoka principle:
    4
    //! stop production when a defect is detected.
    5
    6
    /// Jidoka condition that triggers stop
    7
    #[derive(Debug, Clone, PartialEq)]
    8
    pub enum JidokaCondition {
    9
        /// NaN detected in output
    10
        NanDetected,
    11
        /// Infinity detected in output
    12
        InfDetected,
    13
        /// Cross-backend divergence exceeds tolerance
    14
        BackendDivergence {
    15
            /// Tolerance threshold
    16
            tolerance: f32,
    17
        },
    18
        /// Performance regression exceeds threshold
    19
        PerformanceRegression {
    20
            /// Threshold percentage
    21
            threshold_pct: f32,
    22
        },
    23
        /// Determinism failure (same seed, different output)
    24
        DeterminismFailure,
    25
    }
    26
    27
    /// Jidoka action on condition trigger
    28
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    29
    pub enum JidokaAction {
    30
        /// Stop immediately and report
    31
        Stop,
    32
        /// Log and continue (soft Jidoka)
    33
        LogAndContinue,
    34
        /// Trigger visual diff report
    35
        VisualReport,
    36
    }
    37
    38
    /// Jidoka error types
    39
    #[derive(Debug, Clone)]
    40
    pub enum JidokaError {
    41
        /// NaN values detected
    42
        NanDetected {
    43
            /// Context description
    44
            context: String,
    45
            /// Indices of NaN values
    46
            indices: Vec<usize>,
    47
        },
    48
        /// Infinity values detected
    49
        InfDetected {
    50
            /// Context description
    51
            context: String,
    52
            /// Indices of infinite values
    53
            indices: Vec<usize>,
    54
        },
    55
        /// Backend divergence detected
    56
        BackendDivergence {
    57
            /// Context description
    58
            context: String,
    59
            /// Maximum difference found
    60
            max_diff: f32,
    61
            /// Tolerance threshold
    62
            tolerance: f32,
    63
        },
    64
        /// Performance regression detected
    65
        PerformanceRegression {
    66
            /// Context description
    67
            context: String,
    68
            /// Actual regression percentage
    69
            regression_pct: f32,
    70
            /// Threshold percentage
    71
            threshold_pct: f32,
    72
        },
    73
        /// Determinism failure detected
    74
        DeterminismFailure {
    75
            /// Context description
    76
            context: String,
    77
            /// First differing index
    78
            first_diff_index: usize,
    79
        },
    80
    }
    81
    82
    impl std::fmt::Display for JidokaError {
    83
    0
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    84
    0
            match self {
    85
    0
                Self::NanDetected { context, indices } => {
    86
    0
                    write!(
    87
    0
                        f,
    88
    0
                        "Jidoka: NaN detected at {context} (indices: {indices:?})"
    89
                    )
    90
                }
    91
    0
                Self::InfDetected { context, indices } => {
    92
    0
                    write!(
    93
    0
                        f,
    94
    0
                        "Jidoka: Infinity detected at {context} (indices: {indices:?})"
    95
                    )
    96
                }
    97
                Self::BackendDivergence {
    98
    0
                    context,
    99
    0
                    max_diff,
    100
    0
                    tolerance,
    101
                } => {
    102
    0
                    write!(
    103
    0
                        f,
    104
    0
                        "Jidoka: Backend divergence at {context} (max_diff: {max_diff}, tolerance: {tolerance})"
    105
                    )
    106
                }
    107
                Self::PerformanceRegression {
    108
    0
                    context,
    109
    0
                    regression_pct,
    110
    0
                    threshold_pct,
    111
                } => {
    112
    0
                    write!(
    113
    0
                        f,
    114
    0
                        "Jidoka: Performance regression at {context} ({regression_pct:.2}% > {threshold_pct:.2}%)"
    115
                    )
    116
                }
    117
                Self::DeterminismFailure {
    118
    0
                    context,
    119
    0
                    first_diff_index,
    120
                } => {
    121
    0
                    write!(
    122
    0
                        f,
    123
    0
                        "Jidoka: Determinism failure at {context} (first diff at index {first_diff_index})"
    124
                    )
    125
                }
    126
            }
    127
    0
        }
    128
    }
    129
    130
    impl std::error::Error for JidokaError {}
    131
    132
    /// Jidoka guard for simulation tests
    133
    ///
    134
    /// Implements Toyota Production System's Jidoka principle:
    135
    /// stop production when a defect is detected.
    136
    #[derive(Debug, Clone)]
    137
    pub struct JidokaGuard {
    138
        /// Condition that triggers stop
    139
        pub condition: JidokaCondition,
    140
        /// Action to take on trigger
    141
        pub action: JidokaAction,
    142
        /// Context for debugging
    143
        pub context: String,
    144
    }
    145
    146
    impl JidokaGuard {
    147
        /// Create a new Jidoka guard
    148
        #[must_use]
    149
    0
        pub fn new(
    150
    0
            condition: JidokaCondition,
    151
    0
            action: JidokaAction,
    152
    0
            context: impl Into<String>,
    153
    0
        ) -> Self {
    154
    0
            Self {
    155
    0
                condition,
    156
    0
                action,
    157
    0
                context: context.into(),
    158
    0
            }
    159
    0
        }
    160
    161
        /// Create a NaN detection guard
    162
        #[must_use]
    163
    0
        pub fn nan_guard(context: impl Into<String>) -> Self {
    164
    0
            Self::new(JidokaCondition::NanDetected, JidokaAction::Stop, context)
    165
    0
        }
    166
    167
        /// Create an infinity detection guard
    168
        #[must_use]
    169
    0
        pub fn inf_guard(context: impl Into<String>) -> Self {
    170
    0
            Self::new(JidokaCondition::InfDetected, JidokaAction::Stop, context)
    171
    0
        }
    172
    173
        /// Create a backend divergence guard
    174
        #[must_use]
    175
    0
        pub fn divergence_guard(tolerance: f32, context: impl Into<String>) -> Self {
    176
    0
            Self::new(
    177
    0
                JidokaCondition::BackendDivergence { tolerance },
    178
    0
                JidokaAction::Stop,
    179
    0
                context,
    180
            )
    181
    0
        }
    182
    183
        /// Check output for NaN/Inf and return error if found
    184
        ///
    185
        /// # Errors
    186
        ///
    187
        /// Returns `JidokaError` if the condition is triggered
    188
    0
        pub fn check_output(&self, output: &[f32]) -> Result<(), JidokaError> {
    189
    0
            match &self.condition {
    190
                JidokaCondition::NanDetected => {
    191
    0
                    let nan_indices: Vec<usize> = output
    192
    0
                        .iter()
    193
    0
                        .enumerate()
    194
    0
                        .filter(|(_, x)| x.is_nan())
    195
    0
                        .map(|(i, _)| i)
    196
    0
                        .collect();
    197
    198
    0
                    if !nan_indices.is_empty() {
    199
    0
                        return Err(JidokaError::NanDetected {
    200
    0
                            context: self.context.clone(),
    201
    0
                            indices: nan_indices,
    202
    0
                        });
    203
    0
                    }
    204
                }
    205
                JidokaCondition::InfDetected => {
    206
    0
                    let inf_indices: Vec<usize> = output
    207
    0
                        .iter()
    208
    0
                        .enumerate()
    209
    0
                        .filter(|(_, x)| x.is_infinite())
    210
    0
                        .map(|(i, _)| i)
    211
    0
                        .collect();
    212
    213
    0
                    if !inf_indices.is_empty() {
    214
    0
                        return Err(JidokaError::InfDetected {
    215
    0
                            context: self.context.clone(),
    216
    0
                            indices: inf_indices,
    217
    0
                        });
    218
    0
                    }
    219
                }
    220
    0
                _ => {} // Other conditions handled by compare methods
    221
            }
    222
    0
            Ok(())
    223
    0
        }
    224
    225
        /// Compare two outputs for backend divergence
    226
        ///
    227
        /// # Errors
    228
        ///
    229
        /// Returns `JidokaError` if divergence exceeds tolerance
    230
    0
        pub fn check_divergence(&self, a: &[f32], b: &[f32]) -> Result<(), JidokaError> {
    231
    0
            if let JidokaCondition::BackendDivergence { tolerance } = &self.condition {
    232
    0
                let max_diff = a
    233
    0
                    .iter()
    234
    0
                    .zip(b.iter())
    235
    0
                    .map(|(x, y)| (x - y).abs())
    236
    0
                    .fold(0.0_f32, f32::max);
    237
    238
    0
                if max_diff > *tolerance {
    239
    0
                    return Err(JidokaError::BackendDivergence {
    240
    0
                        context: self.context.clone(),
    241
    0
                        max_diff,
    242
    0
                        tolerance: *tolerance,
    243
    0
                    });
    244
    0
                }
    245
    0
            }
    246
    0
            Ok(())
    247
    0
        }
    248
    249
        /// Check for determinism (same inputs should produce same outputs)
    250
        ///
    251
        /// # Errors
    252
        ///
    253
        /// Returns `JidokaError` if outputs differ
    254
    0
        pub fn check_determinism(&self, a: &[f32], b: &[f32]) -> Result<(), JidokaError> {
    255
    0
            if let JidokaCondition::DeterminismFailure = &self.condition {
    256
    0
                for (i, (x, y)) in a.iter().zip(b.iter()).enumerate() {
    257
                    // Use bitwise comparison for exact equality
    258
    0
                    if x.to_bits() != y.to_bits() {
    259
    0
                        return Err(JidokaError::DeterminismFailure {
    260
    0
                            context: self.context.clone(),
    261
    0
                            first_diff_index: i,
    262
    0
                        });
    263
    0
                    }
    264
                }
    265
    0
            }
    266
    0
            Ok(())
    267
    0
        }
    268
    }
    269
    270
    #[cfg(test)]
    271
    mod tests {
    272
        use super::*;
    273
    274
        #[test]
    275
        fn test_jidoka_nan_detection() {
    276
            // Falsifiable claim B-027
    277
            let guard = JidokaGuard::nan_guard("test_operation");
    278
            let output_with_nan = vec![1.0, 2.0, f32::NAN, 4.0];
    279
    280
            let result = guard.check_output(&output_with_nan);
    281
            assert!(result.is_err());
    282
    283
            if let Err(JidokaError::NanDetected { indices, .. }) = result {
    284
                assert_eq!(indices, vec![2]);
    285
            } else {
    286
                panic!("Expected NanDetected error");
    287
            }
    288
        }
    289
    290
        #[test]
    291
        fn test_jidoka_nan_no_false_positive() {
    292
            let guard = JidokaGuard::nan_guard("test_operation");
    293
            let clean_output = vec![1.0, 2.0, 3.0, 4.0];
    294
    295
            let result = guard.check_output(&clean_output);
    296
            assert!(result.is_ok());
    297
        }
    298
    299
        #[test]
    300
        fn test_jidoka_inf_detection() {
    301
            // Falsifiable claim B-028
    302
            let guard = JidokaGuard::inf_guard("test_operation");
    303
            let output_with_inf = vec![1.0, f32::INFINITY, 3.0, f32::NEG_INFINITY];
    304
    305
            let result = guard.check_output(&output_with_inf);
    306
            assert!(result.is_err());
    307
    308
            if let Err(JidokaError::InfDetected { indices, .. }) = result {
    309
                assert_eq!(indices, vec![1, 3]);
    310
            } else {
    311
                panic!("Expected InfDetected error");
    312
            }
    313
        }
    314
    315
        #[test]
    316
        fn test_jidoka_divergence_detection() {
    317
            // Falsifiable claim A-004
    318
            let guard = JidokaGuard::divergence_guard(1e-5, "cross_backend");
    319
            let a = vec![1.0, 2.0, 3.0, 4.0];
    320
            let b = vec![1.0, 2.0, 3.1, 4.0]; // 0.1 diff at index 2
    321
    322
            let result = guard.check_divergence(&a, &b);
    323
            assert!(result.is_err());
    324
    325
            if let Err(JidokaError::BackendDivergence { max_diff, .. }) = result {
    326
                assert!((max_diff - 0.1).abs() < 1e-6);
    327
            } else {
    328
                panic!("Expected BackendDivergence error");
    329
            }
    330
        }
    331
    332
        #[test]
    333
        fn test_jidoka_divergence_within_tolerance() {
    334
            let guard = JidokaGuard::divergence_guard(1e-5, "cross_backend");
    335
            let a = vec![1.0, 2.0, 3.0, 4.0];
    336
            let b = vec![1.0, 2.0, 3.0 + 1e-7, 4.0]; // Within tolerance
    337
    338
            let result = guard.check_divergence(&a, &b);
    339
            assert!(result.is_ok());
    340
        }
    341
    342
        #[test]
    343
        fn test_jidoka_determinism_check() {
    344
            // Falsifiable claim B-017
    345
            let guard = JidokaGuard::new(
    346
                JidokaCondition::DeterminismFailure,
    347
                JidokaAction::Stop,
    348
                "determinism_test",
    349
            );
    350
    351
            let a = vec![1.0, 2.0, 3.0, 4.0];
    352
            let b = vec![1.0, 2.0, 3.0, 4.0];
    353
    354
            let result = guard.check_determinism(&a, &b);
    355
            assert!(result.is_ok());
    356
        }
    357
    358
        #[test]
    359
        fn test_jidoka_determinism_failure() {
    360
            let guard = JidokaGuard::new(
    361
                JidokaCondition::DeterminismFailure,
    362
                JidokaAction::Stop,
    363
                "determinism_test",
    364
            );
    365
    366
            let a: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0];
    367
            let b: Vec<f32> = vec![1.0, 2.0, 3.000_001, 4.0]; // Different bit pattern
    368
    369
            // Verify they actually have different bits
    370
            assert_ne!(a[2].to_bits(), b[2].to_bits(), "Test values must differ");
    371
    372
            let result = guard.check_determinism(&a, &b);
    373
            assert!(result.is_err());
    374
    375
            if let Err(JidokaError::DeterminismFailure {
    376
                first_diff_index, ..
    377
            }) = result
    378
            {
    379
                assert_eq!(first_diff_index, 2);
    380
            } else {
    381
                panic!("Expected DeterminismFailure error");
    382
            }
    383
        }
    384
    385
        #[test]
    386
        fn test_jidoka_error_display() {
    387
            let err = JidokaError::NanDetected {
    388
                context: "test".to_string(),
    389
                indices: vec![0, 2],
    390
            };
    391
            let display = format!("{err}");
    392
            assert!(display.contains("NaN"));
    393
            assert!(display.contains("test"));
    394
    395
            let err2 = JidokaError::BackendDivergence {
    396
                context: "cross".to_string(),
    397
                max_diff: 0.01,
    398
                tolerance: 0.001,
    399
            };
    400
            let display2 = format!("{err2}");
    401
            assert!(display2.contains("divergence"));
    402
        }
    403
    404
        #[test]
    405
        fn test_empty_output_checks() {
    406
            let guard = JidokaGuard::nan_guard("empty_test");
    407
            let result = guard.check_output(&[]);
    408
            assert!(result.is_ok());
    409
        }
    410
    411
        #[test]
    412
        fn test_single_element_checks() {
    413
            let guard = JidokaGuard::nan_guard("single_test");
    414
    415
            assert!(guard.check_output(&[1.0]).is_ok());
    416
            assert!(guard.check_output(&[f32::NAN]).is_err());
    417
        }
    418
    419
        #[test]
    420
        fn test_jidoka_condition_clone() {
    421
            let condition = JidokaCondition::BackendDivergence { tolerance: 1e-5 };
    422
            let cloned = condition.clone();
    423
            assert_eq!(condition, cloned);
    424
        }
    425
    426
        #[test]
    427
        fn test_jidoka_action_eq() {
    428
            assert_eq!(JidokaAction::Stop, JidokaAction::Stop);
    429
            assert_ne!(JidokaAction::Stop, JidokaAction::LogAndContinue);
    430
        }
    431
    }