## Coverage Report

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

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

    /home/noah/src/realizar/src/audit.rs

    Line
    Count
    Source
    1
    //! Audit Trail and Provenance Logging
    2
    //!
    3
    //! Per spec §12: Comprehensive audit record for every inference request.
    4
    //! Implements GDPR Article 13/14 and SOC 2 compliance requirements.
    5
    //!
    6
    //! ## Toyota Way: Jidoka (Built-in Quality)
    7
    //!
    8
    //! Every inference operation includes automatic quality checks:
    9
    //! - CRC32 checksum verification
    10
    //! - NaN/Inf detection in outputs
    11
    //! - Latency anomaly detection
    12
    //! - Confidence score validation
    13
    //!
    14
    //! ## Key Features
    15
    //!
    16
    //! - Full provenance tracking (model hash, distillation lineage)
    17
    //! - Latency breakdown (preprocessing, inference, postprocessing)
    18
    //! - Quality gates (NaN check, confidence check)
    19
    //! - Batched async flush for high throughput
    20
    21
    // Module-level clippy allows for audit infrastructure
    22
    #![allow(clippy::must_use_candidate)] // Builder pattern methods don't need must_use
    23
    #![allow(clippy::return_self_not_must_use)] // Builder pattern methods return Self
    24
    #![allow(clippy::missing_errors_doc)] // Error conditions are self-explanatory
    25
    26
    use chrono::{DateTime, Utc};
    27
    use serde::{Deserialize, Serialize};
    28
    use std::collections::VecDeque;
    29
    use std::sync::atomic::{AtomicU64, Ordering};
    30
    use std::sync::Mutex;
    31
    use std::time::Duration;
    32
    use uuid::Uuid;
    33
    34
    /// Comprehensive audit record for every inference request
    35
    /// Per GDPR Article 13/14 and SOC 2 compliance requirements
    36
    #[derive(Debug, Clone, Serialize, Deserialize)]
    37
    pub struct AuditRecord {
    38
        // === Identification ===
    39
        /// Unique request identifier
    40
        pub request_id: String,
    41
        /// Timestamp (ISO 8601 with timezone)
    42
        pub timestamp: DateTime<Utc>,
    43
        /// Client identifier (hashed for privacy)
    44
        pub client_id_hash: Option<String>,
    45
    46
        // === Model Information ===
    47
        /// Model file SHA256 hash (provenance)
    48
        pub model_hash: String,
    49
        /// Model version string
    50
        pub model_version: String,
    51
        /// Model type (LogisticRegression, RandomForest, etc.)
    52
        pub model_type: String,
    53
        /// Distillation lineage (if applicable)
    54
        pub distillation_teacher_hash: Option<String>,
    55
    56
        // === Request Details ===
    57
        /// Input feature dimensions
    58
        pub input_dims: Vec<usize>,
    59
        /// Input feature hash (for reproducibility without storing raw data)
    60
        pub input_hash: String,
    61
        /// Request options (explain, confidence threshold, etc.)
    62
        pub options: AuditOptions,
    63
    64
        // === Response Details ===
    65
        /// Output prediction (class or value)
    66
        pub prediction: serde_json::Value,
    67
        /// Confidence score (if classification)
    68
        pub confidence: Option<f32>,
    69
        /// Explanation summary (if explainability enabled)
    70
        pub explanation_summary: Option<String>,
    71
    72
        // === Performance ===
    73
        /// Total latency in milliseconds
    74
        pub latency_ms: f64,
    75
        /// Breakdown: preprocessing, inference, postprocessing
    76
        pub latency_breakdown: LatencyBreakdown,
    77
        /// Peak memory usage in bytes
    78
        pub memory_peak_bytes: u64,
    79
    80
        // === Quality Checks (Jidoka) ===
    81
        /// Did output pass NaN/Inf check?
    82
        pub quality_nan_check: bool,
    83
        /// Did output pass confidence threshold?
    84
        pub quality_confidence_check: bool,
    85
        /// Any warnings generated?
    86
        pub warnings: Vec<String>,
    87
    }
    88
    89
    impl AuditRecord {
    90
        /// Create a new audit record with minimal required fields
    91
    21
        pub fn new(request_id: Uuid, model_hash: &str, model_type: &str) -> Self {
    92
    21
            Self {
    93
    21
                request_id: request_id.to_string(),
    94
    21
                timestamp: Utc::now(),
    95
    21
                client_id_hash: None,
    96
    21
                model_hash: model_hash.to_string(),
    97
    21
                model_version: String::new(),
    98
    21
                model_type: model_type.to_string(),
    99
    21
                distillation_teacher_hash: None,
    100
    21
                input_dims: Vec::new(),
    101
    21
                input_hash: String::new(),
    102
    21
                options: AuditOptions::default(),
    103
    21
                prediction: serde_json::Value::Null,
    104
    21
                confidence: None,
    105
    21
                explanation_summary: None,
    106
    21
                latency_ms: 0.0,
    107
    21
                latency_breakdown: LatencyBreakdown::default(),
    108
    21
                memory_peak_bytes: 0,
    109
    21
                quality_nan_check: true,
    110
    21
                quality_confidence_check: true,
    111
    21
                warnings: Vec::new(),
    112
    21
            }
    113
    21
        }
    114
    115
        /// Builder pattern: set client ID hash
    116
    1
        pub fn with_client_hash(mut self, hash: impl Into<String>) -> Self {
    117
    1
            self.client_id_hash = Some(hash.into());
    118
    1
            self
    119
    1
        }
    120
    121
        /// Builder pattern: set model version
    122
    1
        pub fn with_model_version(mut self, version: impl Into<String>) -> Self {
    123
    1
            self.model_version = version.into();
    124
    1
            self
    125
    1
        }
    126
    127
        /// Builder pattern: set input dimensions
    128
    11
        pub fn with_input_dims(mut self, dims: Vec<usize>) -> Self {
    129
    11
            self.input_dims = dims;
    130
    11
            self
    131
    11
        }
    132
    133
        /// Builder pattern: set input hash
    134
    1
        pub fn with_input_hash(mut self, hash: impl Into<String>) -> Self {
    135
    1
            self.input_hash = hash.into();
    136
    1
            self
    137
    1
        }
    138
    139
        /// Builder pattern: set prediction result
    140
    4
        pub fn with_prediction(mut self, prediction: serde_json::Value) -> Self {
    141
    4
            self.prediction = prediction;
    142
    4
            self
    143
    4
        }
    144
    145
        /// Builder pattern: set confidence score
    146
    1
        pub fn with_confidence(mut self, confidence: f32) -> Self {
    147
    1
            self.confidence = Some(confidence);
    148
    1
            self
    149
    1
        }
    150
    151
        /// Builder pattern: set latency
    152
    1
        pub fn with_latency(mut self, latency: Duration) -> Self {
    153
    1
            self.latency_ms = latency.as_secs_f64() * 1000.0;
    154
    1
            self
    155
    1
        }
    156
    157
        /// Builder pattern: set latency breakdown
    158
    0
        pub fn with_latency_breakdown(mut self, breakdown: LatencyBreakdown) -> Self {
    159
    0
            self.latency_breakdown = breakdown;
    160
    0
            self
    161
    0
        }
    162
    163
        /// Builder pattern: add a warning
    164
    1
        pub fn with_warning(mut self, warning: impl Into<String>) -> Self {
    165
    1
            self.warnings.push(warning.into());
    166
    1
            self
    167
    1
        }
    168
    169
        /// Builder pattern: set quality check results
    170
    1
        pub fn with_quality_checks(mut self, nan_check: bool, confidence_check: bool) -> Self {
    171
    1
            self.quality_nan_check = nan_check;
    172
    1
            self.quality_confidence_check = confidence_check;
    173
    1
            self
    174
    1
        }
    175
    176
        /// Builder pattern: set distillation teacher hash
    177
    1
        pub fn with_distillation_teacher(mut self, teacher_hash: impl Into<String>) -> Self {
    178
    1
            self.distillation_teacher_hash = Some(teacher_hash.into());
    179
    1
            self
    180
    1
        }
    181
    }
    182
    183
    /// Request options included in audit record
    184
    #[derive(Debug, Clone, Default, Serialize, Deserialize)]
    185
    pub struct AuditOptions {
    186
        /// Was explainability requested?
    187
        pub explain: bool,
    188
        /// Confidence threshold (if any)
    189
        pub confidence_threshold: Option<f32>,
    190
        /// Max tokens (for LLM requests)
    191
        pub max_tokens: Option<usize>,
    192
        /// Temperature (for LLM requests)
    193
        pub temperature: Option<f32>,
    194
    }
    195
    196
    impl AuditOptions {
    197
        /// Create options for APR model request
    198
    2
        pub fn apr(explain: bool, confidence_threshold: Option<f32>) -> Self {
    199
    2
            Self {
    200
    2
                explain,
    201
    2
                confidence_threshold,
    202
    2
                max_tokens: None,
    203
    2
                temperature: None,
    204
    2
            }
    205
    2
        }
    206
    207
        /// Create options for LLM request
    208
    2
        pub fn llm(max_tokens: usize, temperature: f32) -> Self {
    209
    2
            Self {
    210
    2
                explain: false,
    211
    2
                confidence_threshold: None,
    212
    2
                max_tokens: Some(max_tokens),
    213
    2
                temperature: Some(temperature),
    214
    2
            }
    215
    2
        }
    216
    }
    217
    218
    /// Latency breakdown for detailed performance analysis
    219
    #[derive(Debug, Clone, Default, Serialize, Deserialize)]
    220
    pub struct LatencyBreakdown {
    221
        /// Preprocessing time in milliseconds
    222
        pub preprocessing_ms: f64,
    223
        /// Inference time in milliseconds
    224
        pub inference_ms: f64,
    225
        /// Postprocessing time in milliseconds
    226
        pub postprocessing_ms: f64,
    227
        /// Explanation generation time (if enabled)
    228
        pub explanation_ms: Option<f64>,
    229
    }
    230
    231
    impl LatencyBreakdown {
    232
        /// Create a new latency breakdown
    233
    4
        pub fn new(preprocessing_ms: f64, inference_ms: f64, postprocessing_ms: f64) -> Self {
    234
    4
            Self {
    235
    4
                preprocessing_ms,
    236
    4
                inference_ms,
    237
    4
                postprocessing_ms,
    238
    4
                explanation_ms: None,
    239
    4
            }
    240
    4
        }
    241
    242
        /// Add explanation time
    243
    3
        pub fn with_explanation(mut self, explanation_ms: f64) -> Self {
    244
    3
            self.explanation_ms = Some(explanation_ms);
    245
    3
            self
    246
    3
        }
    247
    248
        /// Total time in milliseconds
    249
    2
        pub fn total_ms(&self) -> f64 {
    250
    2
            self.preprocessing_ms
    251
    2
                + self.inference_ms
    252
    2
                + self.postprocessing_ms
    253
    2
                + self.explanation_ms.unwrap_or(0.0)
    254
    2
        }
    255
    }
    256
    257
    /// Model provenance tracking (Jidoka: full traceability)
    258
    #[derive(Debug, Clone, Serialize, Deserialize)]
    259
    pub struct ProvenanceChain {
    260
        /// Original training data hash
    261
        pub training_data_hash: Option<String>,
    262
        /// Training code commit SHA
    263
        pub training_code_sha: Option<String>,
    264
        /// Training environment specification
    265
        pub training_env: Option<TrainingEnv>,
    266
        /// Distillation chain (for distilled models)
    267
        pub distillation_chain: Vec<DistillationStep>,
    268
        /// Quantization provenance
    269
        pub quantization: Option<QuantizationProvenance>,
    270
        /// Signature chain (all signers)
    271
        pub signatures: Vec<SignatureRecord>,
    272
    }
    273
    274
    impl Default for ProvenanceChain {
    275
    0
        fn default() -> Self {
    276
    0
            Self::new()
    277
    0
        }
    278
    }
    279
    280
    impl ProvenanceChain {
    281
        /// Create an empty provenance chain
    282
    5
        pub fn new() -> Self {
    283
    5
            Self {
    284
    5
                training_data_hash: None,
    285
    5
                training_code_sha: None,
    286
    5
                training_env: None,
    287
    5
                distillation_chain: Vec::new(),
    288
    5
                quantization: None,
    289
    5
                signatures: Vec::new(),
    290
    5
            }
    291
    5
        }
    292
    293
        /// Add training data hash
    294
    2
        pub fn with_training_data(mut self, hash: impl Into<String>) -> Self {
    295
    2
            self.training_data_hash = Some(hash.into());
    296
    2
            self
    297
    2
        }
    298
    299
        /// Add training code commit
    300
    2
        pub fn with_code_sha(mut self, sha: impl Into<String>) -> Self {
    301
    2
            self.training_code_sha = Some(sha.into());
    302
    2
            self
    303
    2
        }
    304
    305
        /// Add distillation step
    306
    1
        pub fn add_distillation(&mut self, step: DistillationStep) {
    307
    1
            self.distillation_chain.push(step);
    308
    1
        }
    309
    310
        /// Add signature
    311
    1
        pub fn add_signature(&mut self, signature: SignatureRecord) {
    312
    1
            self.signatures.push(signature);
    313
    1
        }
    314
    }
    315
    316
    /// Training environment specification
    317
    #[derive(Debug, Clone, Serialize, Deserialize)]
    318
    pub struct TrainingEnv {
    319
        /// Framework version (e.g., "aprender 0.1.0")
    320
        pub framework: String,
    321
        /// Hardware description
    322
        pub hardware: String,
    323
        /// Random seed used
    324
        pub seed: Option<u64>,
    325
    }
    326
    327
    /// Distillation step in provenance chain
    328
    #[derive(Debug, Clone, Serialize, Deserialize)]
    329
    pub struct DistillationStep {
    330
        /// Teacher model hash
    331
        pub teacher_hash: String,
    332
        /// Distillation method (Standard, Progressive, Ensemble)
    333
        pub method: String,
    334
        /// Temperature used
    335
        pub temperature: f32,
    336
        /// Alpha (soft vs hard loss weight)
    337
        pub alpha: f32,
    338
        /// Final distillation loss
    339
        pub final_loss: f32,
    340
        /// Timestamp of distillation
    341
        pub timestamp: DateTime<Utc>,
    342
    }
    343
    344
    /// Quantization provenance
    345
    #[derive(Debug, Clone, Serialize, Deserialize)]
    346
    pub struct QuantizationProvenance {
    347
        /// Original model hash (before quantization)
    348
        pub original_hash: String,
    349
        /// Quantization method (Q4_0, Q8_0, AWQ, etc.)
    350
        pub method: String,
    351
        /// Bits per weight
    352
        pub bits: u8,
    353
        /// Calibration data hash (if applicable)
    354
        pub calibration_hash: Option<String>,
    355
    }
    356
    357
    /// Signature record for signed models
    358
    #[derive(Debug, Clone, Serialize, Deserialize)]
    359
    pub struct SignatureRecord {
    360
        /// Signer identity (e.g., "paiml-release-key")
    361
        pub signer: String,
    362
        /// Signature algorithm (Ed25519)
    363
        pub algorithm: String,
    364
        /// Signature timestamp
    365
        pub timestamp: DateTime<Utc>,
    366
        /// Signature bytes (base64 encoded)
    367
        pub signature: String,
    368
    }
    369
    370
    /// Audit sink trait for different output destinations
    371
    pub trait AuditSink: Send + Sync {
    372
        /// Write a batch of audit records
    373
        fn write_batch(&self, records: &[AuditRecord]) -> Result<(), AuditError>;
    374
    375
        /// Flush any pending writes
    376
        fn flush(&self) -> Result<(), AuditError>;
    377
    }
    378
    379
    /// In-memory audit sink for testing
    380
    #[derive(Debug, Default)]
    381
    pub struct InMemoryAuditSink {
    382
        records: Mutex<Vec<AuditRecord>>,
    383
    }
    384
    385
    impl InMemoryAuditSink {
    386
        /// Create a new in-memory sink
    387
    141
        pub fn new() -> Self {
    388
    141
            Self {
    389
    141
                records: Mutex::new(Vec::new()),
    390
    141
            }
    391
    141
        }
    392
    393
        /// Get all stored records
    394
    2
        pub fn records(&self) -> Vec<AuditRecord> {
    395
    2
            self.records.lock().expect("test").clone()
    396
    2
        }
    397
    398
        /// Get record count
    399
    3
        pub fn count(&self) -> usize {
    400
    3
            self.records.lock().expect("test").len()
    401
    3
        }
    402
    }
    403
    404
    impl AuditSink for InMemoryAuditSink {
    405
    5
        fn write_batch(&self, records: &[AuditRecord]) -> Result<(), AuditError> {
    406
    5
            let mut storage = self.records.lock().expect("test");
    407
    5
            storage.extend(records.iter().cloned());
    408
    5
            Ok(())
    409
    5
        }
    410
    411
    2
        fn flush(&self) -> Result<(), AuditError> {
    412
    2
            Ok(())
    413
    2
        }
    414
    }
    415
    416
    /// JSON file audit sink
    417
    pub struct JsonFileAuditSink {
    418
        path: std::path::PathBuf,
    419
    }
    420
    421
    impl JsonFileAuditSink {
    422
        /// Create a new JSON file sink
    423
    5
        pub fn new(path: impl Into<std::path::PathBuf>) -> Self {
    424
    5
            Self { path: path.into() }
    425
    5
        }
    426
    }
    427
    428
    impl AuditSink for JsonFileAuditSink {
    429
    4
        fn write_batch(&self, records: &[AuditRecord]) -> Result<(), AuditError> {
    430
            use std::fs::OpenOptions;
    431
            use std::io::Write;
    432
    433
    4
            let mut file = OpenOptions::new()
    434
    4
                .create(true)
    435
    4
                .append(true)
    436
    4
                .open(&self.path)
    437
    4
                .map_err(|e| AuditError::IoError(e0.to_string0()))?0;
    438
    439
    9
            for record5 in records {
    440
    5
                let json = serde_json::to_string(record)
    441
    5
                    .map_err(|e| AuditError::SerializationError(e0.to_string0()))?0;
    442
    5
                writeln!(file, "{}", json).map_err(|e| AuditError::IoError(e0.to_string0()))?0;
    443
            }
    444
    445
    4
            Ok(())
    446
    4
        }
    447
    448
    2
        fn flush(&self) -> Result<(), AuditError> {
    449
    2
            Ok(())
    450
    2
        }
    451
    }
    452
    453
    /// Audit error types
    454
    #[derive(Debug, Clone)]
    455
    pub enum AuditError {
    456
        /// IO error writing audit records
    457
        IoError(String),
    458
        /// Serialization error
    459
        SerializationError(String),
    460
        /// Record not found
    461
        NotFound(String),
    462
    }
    463
    464
    impl std::fmt::Display for AuditError {
    465
    3
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    466
    3
            match self {
    467
    1
                Self::IoError(msg) => write!(f, "Audit IO error: {msg}"),
    468
    1
                Self::SerializationError(msg) => write!(f, "Audit serialization error: {msg}"),
    469
    1
                Self::NotFound(id) => write!(f, "Audit record not found: {id}"),
    470
            }
    471
    3
        }
    472
    }
    473
    474
    impl std::error::Error for AuditError {}
    475
    476
    /// High-performance audit logger with batching
    477
    pub struct AuditLogger {
    478
        /// Output sink (file, database, event stream)
    479
        sink: Box<dyn AuditSink>,
    480
        /// Batch buffer for efficiency
    481
        buffer: Mutex<VecDeque<AuditRecord>>,
    482
        /// Buffer size threshold for auto-flush
    483
        buffer_threshold: usize,
    484
        /// Total records logged (for metrics)
    485
        total_logged: AtomicU64,
    486
        /// Model hash for provenance
    487
        model_hash: String,
    488
        /// Load timestamp
    489
        load_timestamp: DateTime<Utc>,
    490
    }
    491
    492
    impl AuditLogger {
    493
        /// Create a new audit logger with default settings
    494
    139
        pub fn new(sink: Box<dyn AuditSink>) -> Self {
    495
    139
            Self {
    496
    139
                sink,
    497
    139
                buffer: Mutex::new(VecDeque::new()),
    498
    139
                buffer_threshold: 1000,
    499
    139
                total_logged: AtomicU64::new(0),
    500
    139
                model_hash: String::new(),
    501
    139
                load_timestamp: Utc::now(),
    502
    139
            }
    503
    139
        }
    504
    505
        /// Create an audit logger with in-memory sink (for testing)
    506
    4
        pub fn in_memory() -> (Self, std::sync::Arc<InMemoryAuditSink>) {
    507
    4
            let sink = std::sync::Arc::new(InMemoryAuditSink::new());
    508
    4
            let logger = Self::new(Box::new(InMemorySinkWrapper(sink.clone())));
    509
    4
            (logger, sink)
    510
    4
        }
    511
    512
        /// Set the model hash for provenance tracking
    513
    134
        pub fn with_model_hash(mut self, hash: impl Into<String>) -> Self {
    514
    134
            self.model_hash = hash.into();
    515
    134
            self
    516
    134
        }
    517
    518
        /// Set the load timestamp
    519
    1
        pub fn with_load_timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
    520
    1
            self.load_timestamp = timestamp;
    521
    1
            self
    522
    1
        }
    523
    524
        /// Set buffer threshold for auto-flush
    525
    1
        pub fn with_buffer_threshold(mut self, threshold: usize) -> Self {
    526
    1
            self.buffer_threshold = threshold;
    527
    1
            self
    528
    1
        }
    529
    530
        /// Log a request (returns the request ID)
    531
    10
        pub fn log_request(&self, model_type: &str, input_dims: &[usize]) -> Uuid {
    532
    10
            let request_id = Uuid::new_v4();
    533
    10
            let record = AuditRecord::new(request_id, &self.model_hash, model_type)
    534
    10
                .with_input_dims(input_dims.to_vec());
    535
    536
    10
            let mut buffer = self.buffer.lock().expect("test");
    537
    10
            buffer.push_back(record);
    538
    539
    10
            request_id
    540
    10
        }
    541
    542
        /// Complete a request with response data
    543
    10
        pub fn log_response(
    544
    10
            &self,
    545
    10
            request_id: Uuid,
    546
    10
            prediction: serde_json::Value,
    547
    10
            latency: Duration,
    548
    10
            confidence: Option<f32>,
    549
    10
        ) {
    550
    10
            let mut buffer = self.buffer.lock().expect("test");
    551
    552
            // Find and update the record
    553
    10
            if let Some(record) = buffer
    554
    10
                .iter_mut()
    555
    23
                .find10(|r| r.request_id == request_id.to_string())
    556
    10
            {
    557
    10
                record.prediction = prediction;
    558
    10
                record.latency_ms = latency.as_secs_f64() * 1000.0;
    559
    10
                record.confidence = confidence;
    560
    10
            }0
    561
    562
            // Auto-flush if buffer exceeds threshold
    563
    10
            if buffer.len() >= self.buffer_threshold {
    564
    1
                let _ = self.flush_buffer_locked(&mut buffer);
    565
    9
            }
    566
    10
        }
    567
    568
        /// Manually flush the buffer
    569
    4
        pub fn flush(&self) -> Result<(), AuditError> {
    570
    4
            let mut buffer = self.buffer.lock().expect("test");
    571
    4
            self.flush_buffer_locked(&mut buffer)
    572
    4
        }
    573
    574
        /// Internal flush with lock already held
    575
    5
        fn flush_buffer_locked(&self, buffer: &mut VecDeque<AuditRecord>) -> Result<(), AuditError> {
    576
    5
            if buffer.is_empty() {
    577
    1
                return Ok(());
    578
    4
            }
    579
    580
    4
            let records: Vec<AuditRecord> = buffer.drain(..).collect();
    581
    4
            let count = records.len() as u64;
    582
    583
    4
            self.sink.write_batch(&records)?0;
    584
    4
            self.total_logged.fetch_add(count, Ordering::Relaxed);
    585
    586
    4
            Ok(())
    587
    5
        }
    588
    589
        /// Get total records logged
    590
    1
        pub fn total_logged(&self) -> u64 {
    591
    1
            self.total_logged.load(Ordering::Relaxed)
    592
    1
        }
    593
    594
        /// Get current buffer size
    595
    3
        pub fn buffer_size(&self) -> usize {
    596
    3
            self.buffer.lock().expect("test").len()
    597
    3
        }
    598
    599
        /// Get model hash
    600
    1
        pub fn model_hash(&self) -> &str {
    601
    1
            &self.model_hash
    602
    1
        }
    603
    604
        /// Get load timestamp
    605
    1
        pub fn load_timestamp(&self) -> DateTime<Utc> {
    606
    1
            self.load_timestamp
    607
    1
        }
    608
    }
    609
    610
    /// Wrapper to make Arc<InMemoryAuditSink> implement AuditSink
    611
    struct InMemorySinkWrapper(std::sync::Arc<InMemoryAuditSink>);
    612
    613
    impl AuditSink for InMemorySinkWrapper {
    614
    3
        fn write_batch(&self, records: &[AuditRecord]) -> Result<(), AuditError> {
    615
    3
            self.0.write_batch(records)
    616
    3
        }
    617
    618
    0
        fn flush(&self) -> Result<(), AuditError> {
    619
    0
            self.0.flush()
    620
    0
        }
    621
    }
    622
    623
    // ============================================================================
    624
    // EXTREME TDD TESTS
    625
    // ============================================================================
    626
    627
    #[cfg(test)]
    628
    mod tests {
    629
        use super::*;
    630
        use std::time::Duration;
    631
    632
        // -------------------------------------------------------------------------
    633
        // AuditRecord Tests
    634
        // -------------------------------------------------------------------------
    635
    636
        #[test]
    637
    1
        fn test_audit_record_new() {
    638
    1
            let request_id = Uuid::new_v4();
    639
    1
            let record = AuditRecord::new(request_id, "abc123", "LogisticRegression");
    640
    641
    1
            assert_eq!(record.request_id, request_id.to_string());
    642
    1
            assert_eq!(record.model_hash, "abc123");
    643
    1
            assert_eq!(record.model_type, "LogisticRegression");
    644
    1
            assert!(record.quality_nan_check);
    645
    1
            assert!(record.quality_confidence_check);
    646
    1
        }
    647
    648
        #[test]
    649
    1
        fn test_audit_record_builder_pattern() {
    650
    1
            let request_id = Uuid::new_v4();
    651
    1
            let record = AuditRecord::new(request_id, "hash123", "RandomForest")
    652
    1
                .with_client_hash("client_hash_456")
    653
    1
                .with_model_version("1.0.0")
    654
    1
                .with_input_dims(vec![784])
    655
    1
                .with_input_hash("input_hash_789")
    656
    1
                .with_prediction(serde_json::json!({"class": 1}))
    657
    1
                .with_confidence(0.95)
    658
    1
                .with_latency(Duration::from_millis(50))
    659
    1
                .with_warning("Low confidence")
    660
    1
                .with_quality_checks(true, false);
    661
    662
    1
            assert_eq!(record.client_id_hash, Some("client_hash_456".to_string()));
    663
    1
            assert_eq!(record.model_version, "1.0.0");
    664
    1
            assert_eq!(record.input_dims, vec![784]);
    665
    1
            assert_eq!(record.input_hash, "input_hash_789");
    666
    1
            assert_eq!(record.prediction, serde_json::json!({"class": 1}));
    667
    1
            assert_eq!(record.confidence, Some(0.95));
    668
    1
            assert!((record.latency_ms - 50.0).abs() < 0.1);
    669
    1
            assert_eq!(record.warnings, vec!["Low confidence"]);
    670
    1
            assert!(record.quality_nan_check);
    671
    1
            assert!(!record.quality_confidence_check);
    672
    1
        }
    673
    674
        #[test]
    675
    1
        fn test_audit_record_serialization() {
    676
    1
            let request_id = Uuid::new_v4();
    677
    1
            let record =
    678
    1
                AuditRecord::new(request_id, "hash", "KNN").with_prediction(serde_json::json!(5));
    679
    680
    1
            let json = serde_json::to_string(&record).expect("test");
    681
    1
            let deserialized: AuditRecord = serde_json::from_str(&json).expect("test");
    682
    683
    1
            assert_eq!(deserialized.model_type, "KNN");
    684
    1
            assert_eq!(deserialized.prediction, serde_json::json!(5));
    685
    1
        }
    686
    687
        #[test]
    688
    1
        fn test_audit_record_with_distillation_teacher() {
    689
    1
            let record = AuditRecord::new(Uuid::new_v4(), "student", "MLP")
    690
    1
                .with_distillation_teacher("teacher_hash_abc");
    691
    692
    1
            assert_eq!(
    693
                record.distillation_teacher_hash,
    694
    1
                Some("teacher_hash_abc".to_string())
    695
            );
    696
    1
        }
    697
    698
        // -------------------------------------------------------------------------
    699
        // AuditOptions Tests
    700
        // -------------------------------------------------------------------------
    701
    702
        #[test]
    703
    1
        fn test_audit_options_apr() {
    704
    1
            let options = AuditOptions::apr(true, Some(0.8));
    705
    706
    1
            assert!(options.explain);
    707
    1
            assert_eq!(options.confidence_threshold, Some(0.8));
    708
    1
            assert!(options.max_tokens.is_none());
    709
    1
            assert!(options.temperature.is_none());
    710
    1
        }
    711
    712
        #[test]
    713
    1
        fn test_audit_options_llm() {
    714
    1
            let options = AuditOptions::llm(256, 0.7);
    715
    716
    1
            assert!(!options.explain);
    717
    1
            assert!(options.confidence_threshold.is_none());
    718
    1
            assert_eq!(options.max_tokens, Some(256));
    719
    1
            assert_eq!(options.temperature, Some(0.7));
    720
    1
        }
    721
    722
        // -------------------------------------------------------------------------
    723
        // LatencyBreakdown Tests
    724
        // -------------------------------------------------------------------------
    725
    726
        #[test]
    727
    1
        fn test_latency_breakdown_new() {
    728
    1
            let breakdown = LatencyBreakdown::new(1.5, 10.0, 0.5);
    729
    730
    1
            assert!((breakdown.preprocessing_ms - 1.5).abs() < 0.001);
    731
    1
            assert!((breakdown.inference_ms - 10.0).abs() < 0.001);
    732
    1
            assert!((breakdown.postprocessing_ms - 0.5).abs() < 0.001);
    733
    1
            assert!(breakdown.explanation_ms.is_none());
    734
    1
        }
    735
    736
        #[test]
    737
    1
        fn test_latency_breakdown_with_explanation() {
    738
    1
            let breakdown = LatencyBreakdown::new(1.0, 5.0, 1.0).with_explanation(3.0);
    739
    740
    1
            assert_eq!(breakdown.explanation_ms, Some(3.0));
    741
    1
        }
    742
    743
        #[test]
    744
    1
        fn test_latency_breakdown_total() {
    745
    1
            let breakdown = LatencyBreakdown::new(1.0, 5.0, 2.0);
    746
    1
            assert!((breakdown.total_ms() - 8.0).abs() < 0.001);
    747
    748
    1
            let with_explain = breakdown.with_explanation(2.0);
    749
    1
            assert!((with_explain.total_ms() - 10.0).abs() < 0.001);
    750
    1
        }
    751
    752
        // -------------------------------------------------------------------------
    753
        // ProvenanceChain Tests
    754
        // -------------------------------------------------------------------------
    755
    756
        #[test]
    757
    1
        fn test_provenance_chain_new() {
    758
    1
            let chain = ProvenanceChain::new();
    759
    760
    1
            assert!(chain.training_data_hash.is_none());
    761
    1
            assert!(chain.training_code_sha.is_none());
    762
    1
            assert!(chain.distillation_chain.is_empty());
    763
    1
            assert!(chain.signatures.is_empty());
    764
    1
        }
    765
    766
        #[test]
    767
    1
        fn test_provenance_chain_builder() {
    768
    1
            let chain = ProvenanceChain::new()
    769
    1
                .with_training_data("data_hash_123")
    770
    1
                .with_code_sha("abc123def");
    771
    772
    1
            assert_eq!(chain.training_data_hash, Some("data_hash_123".to_string()));
    773
    1
            assert_eq!(chain.training_code_sha, Some("abc123def".to_string()));
    774
    1
        }
    775
    776
        #[test]
    777
    1
        fn test_provenance_chain_add_distillation() {
    778
    1
            let mut chain = ProvenanceChain::new();
    779
    1
            chain.add_distillation(DistillationStep {
    780
    1
                teacher_hash: "teacher123".to_string(),
    781
    1
                method: "Standard".to_string(),
    782
    1
                temperature: 4.0,
    783
    1
                alpha: 0.5,
    784
    1
                final_loss: 0.01,
    785
    1
                timestamp: Utc::now(),
    786
    1
            });
    787
    788
    1
            assert_eq!(chain.distillation_chain.len(), 1);
    789
    1
            assert_eq!(chain.distillation_chain[0].teacher_hash, "teacher123");
    790
    1
        }
    791
    792
        #[test]
    793
    1
        fn test_provenance_chain_add_signature() {
    794
    1
            let mut chain = ProvenanceChain::new();
    795
    1
            chain.add_signature(SignatureRecord {
    796
    1
                signer: "paiml-release".to_string(),
    797
    1
                algorithm: "Ed25519".to_string(),
    798
    1
                timestamp: Utc::now(),
    799
    1
                signature: "base64sig==".to_string(),
    800
    1
            });
    801
    802
    1
            assert_eq!(chain.signatures.len(), 1);
    803
    1
            assert_eq!(chain.signatures[0].signer, "paiml-release");
    804
    1
        }
    805
    806
        // -------------------------------------------------------------------------
    807
        // InMemoryAuditSink Tests
    808
        // -------------------------------------------------------------------------
    809
    810
        #[test]
    811
    1
        fn test_in_memory_sink_write_batch() {
    812
    1
            let sink = InMemoryAuditSink::new();
    813
    1
            let records = vec![
    814
    1
                AuditRecord::new(Uuid::new_v4(), "h1", "LR"),
    815
    1
                AuditRecord::new(Uuid::new_v4(), "h2", "RF"),
    816
            ];
    817
    818
    1
            sink.write_batch(&records).expect("test");
    819
    820
    1
            assert_eq!(sink.count(), 2);
    821
    1
            let stored = sink.records();
    822
    1
            assert_eq!(stored[0].model_type, "LR");
    823
    1
            assert_eq!(stored[1].model_type, "RF");
    824
    1
        }
    825
    826
        #[test]
    827
    1
        fn test_in_memory_sink_flush() {
    828
    1
            let sink = InMemoryAuditSink::new();
    829
    1
            assert!(sink.flush().is_ok());
    830
    1
        }
    831
    832
        // -------------------------------------------------------------------------
    833
        // AuditLogger Tests
    834
        // -------------------------------------------------------------------------
    835
    836
        #[test]
    837
    1
        fn test_audit_logger_in_memory() {
    838
    1
            let (logger, sink) = AuditLogger::in_memory();
    839
    840
    1
            let request_id = logger.log_request("LogisticRegression", &[784]);
    841
    1
            logger.log_response(
    842
    1
                request_id,
    843
    1
                serde_json::json!({"class": 1}),
    844
    1
                Duration::from_millis(5),
    845
    1
                Some(0.95),
    846
            );
    847
    848
    1
            assert_eq!(logger.buffer_size(), 1);
    849
    1
            logger.flush().expect("test");
    850
    1
            assert_eq!(logger.buffer_size(), 0);
    851
    1
            assert_eq!(sink.count(), 1);
    852
    1
        }
    853
    854
        #[test]
    855
    1
        fn test_audit_logger_with_model_hash() {
    856
    1
            let (logger, _sink) = AuditLogger::in_memory();
    857
    1
            let logger = logger.with_model_hash("sha256_abc123");
    858
    859
    1
            assert_eq!(logger.model_hash(), "sha256_abc123");
    860
    1
        }
    861
    862
        #[test]
    863
    1
        fn test_audit_logger_total_logged() {
    864
    1
            let (logger, _sink) = AuditLogger::in_memory();
    865
    866
    6
            for _ in 0..5 {
    867
    5
                let id = logger.log_request("Test", &[10]);
    868
    5
                logger.log_response(id, serde_json::json!(0), Duration::from_millis(1), None);
    869
    5
            }
    870
    871
    1
            logger.flush().expect("test");
    872
    1
            assert_eq!(logger.total_logged(), 5);
    873
    1
        }
    874
    875
        #[test]
    876
    1
        fn test_audit_logger_auto_flush() {
    877
    1
            let sink = std::sync::Arc::new(InMemoryAuditSink::new());
    878
    1
            let logger =
    879
    1
                AuditLogger::new(Box::new(InMemorySinkWrapper(sink.clone()))).with_buffer_threshold(3);
    880
    881
            // Log 3 requests - should auto-flush
    882
    4
            for _ in 0..3 {
    883
    3
                let id = logger.log_request("Test", &[5]);
    884
    3
                logger.log_response(id, serde_json::json!(1), Duration::from_millis(1), None);
    885
    3
            }
    886
    887
            // Buffer should be empty after auto-flush
    888
    1
            assert_eq!(logger.buffer_size(), 0);
    889
    1
            assert_eq!(sink.count(), 3);
    890
    1
        }
    891
    892
        #[test]
    893
    1
        fn test_audit_logger_load_timestamp() {
    894
    1
            let timestamp = Utc::now();
    895
    1
            let (logger, _) = AuditLogger::in_memory();
    896
    1
            let logger = logger.with_load_timestamp(timestamp);
    897
    898
    1
            assert_eq!(logger.load_timestamp(), timestamp);
    899
    1
        }
    900
    901
        // -------------------------------------------------------------------------
    902
        // AuditError Tests
    903
        // -------------------------------------------------------------------------
    904
    905
        #[test]
    906
    1
        fn test_audit_error_display() {
    907
    1
            let io_err = AuditError::IoError("disk full".to_string());
    908
    1
            assert!(io_err.to_string().contains("disk full"));
    909
    910
    1
            let ser_err = AuditError::SerializationError("invalid json".to_string());
    911
    1
            assert!(ser_err.to_string().contains("invalid json"));
    912
    913
    1
            let not_found = AuditError::NotFound("abc-123".to_string());
    914
    1
            assert!(not_found.to_string().contains("abc-123"));
    915
    1
        }
    916
    917
        // -------------------------------------------------------------------------
    918
        // Serialization Round-trip Tests
    919
        // -------------------------------------------------------------------------
    920
    921
        #[test]
    922
    1
        fn test_latency_breakdown_serialization() {
    923
    1
            let breakdown = LatencyBreakdown::new(1.0, 5.0, 2.0).with_explanation(1.5);
    924
    1
            let json = serde_json::to_string(&breakdown).expect("test");
    925
    1
            let restored: LatencyBreakdown = serde_json::from_str(&json).expect("test");
    926
    927
    1
            assert!((restored.inference_ms - 5.0).abs() < 0.001);
    928
    1
            assert_eq!(restored.explanation_ms, Some(1.5));
    929
    1
        }
    930
    931
        #[test]
    932
    1
        fn test_provenance_chain_serialization() {
    933
    1
            let chain = ProvenanceChain::new()
    934
    1
                .with_training_data("data123")
    935
    1
                .with_code_sha("commit456");
    936
    937
    1
            let json = serde_json::to_string(&chain).expect("test");
    938
    1
            let restored: ProvenanceChain = serde_json::from_str(&json).expect("test");
    939
    940
    1
            assert_eq!(restored.training_data_hash, Some("data123".to_string()));
    941
    1
            assert_eq!(restored.training_code_sha, Some("commit456".to_string()));
    942
    1
        }
    943
    944
        #[test]
    945
    1
        fn test_quantization_provenance_serialization() {
    946
    1
            let quant = QuantizationProvenance {
    947
    1
                original_hash: "original_model_hash".to_string(),
    948
    1
                method: "Q4_K_M".to_string(),
    949
    1
                bits: 4,
    950
    1
                calibration_hash: Some("calibration_data_hash".to_string()),
    951
    1
            };
    952
    953
    1
            let json = serde_json::to_string(&quant).expect("test");
    954
    1
            let restored: QuantizationProvenance = serde_json::from_str(&json).expect("test");
    955
    956
    1
            assert_eq!(restored.method, "Q4_K_M");
    957
    1
            assert_eq!(restored.bits, 4);
    958
    1
        }
    959
    960
        // -------------------------------------------------------------------------
    961
        // JsonFileAuditSink Tests (95% coverage push)
    962
        // -------------------------------------------------------------------------
    963
    964
        #[test]
    965
    1
        fn test_json_file_audit_sink_new() {
    966
            // Just verify sink can be created without panic
    967
    1
            let _sink = JsonFileAuditSink::new("/tmp/test_audit.jsonl");
    968
    1
        }
    969
    970
        #[test]
    971
    1
        fn test_json_file_audit_sink_write_batch() {
    972
            use std::fs;
    973
    1
            let path = std::env::temp_dir().join("audit_test_write_batch.jsonl");
    974
    1
            let sink = JsonFileAuditSink::new(&path);
    975
    976
    1
            let records = vec![
    977
    1
                AuditRecord::new(Uuid::new_v4(), "hash1", "LR")
    978
    1
                    .with_prediction(serde_json::json!({"class": 0})),
    979
    1
                AuditRecord::new(Uuid::new_v4(), "hash2", "RF")
    980
    1
                    .with_prediction(serde_json::json!({"class": 1})),
    981
            ];
    982
    983
    1
            sink.write_batch(&records).expect("write_batch");
    984
    1
            sink.flush().expect("flush");
    985
    986
            // Verify file was written
    987
    1
            let content = fs::read_to_string(&path).expect("read file");
    988
    1
            assert!(content.contains("hash1"));
    989
    1
            assert!(content.contains("hash2"));
    990
    1
            assert!(content.lines().count() == 2);
    991
    992
    1
            let _ = fs::remove_file(&path);
    993
    1
        }
    994
    995
        #[test]
    996
    1
        fn test_json_file_audit_sink_append() {
    997
            use std::fs;
    998
    1
            let path = std::env::temp_dir().join("audit_test_append.jsonl");
    999
    1
            let sink = JsonFileAuditSink::new(&path);
    1000
    1001
            // Write first batch
    1002
    1
            let records1 = vec![AuditRecord::new(Uuid::new_v4(), "h1", "T1")];
    1003
    1
            sink.write_batch(&records1).expect("write 1");
    1004
    1005
            // Write second batch (should append)
    1006
    1
            let records2 = vec![AuditRecord::new(Uuid::new_v4(), "h2", "T2")];
    1007
    1
            sink.write_batch(&records2).expect("write 2");
    1008
    1009
    1
            let content = fs::read_to_string(&path).expect("read");
    1010
    1
            assert_eq!(content.lines().count(), 2);
    1011
    1012
    1
            let _ = fs::remove_file(&path);
    1013
    1
        }
    1014
    1015
        #[test]
    1016
    1
        fn test_json_file_audit_sink_flush_noop() {
    1017
    1
            let sink = JsonFileAuditSink::new("/tmp/nonexistent_audit.jsonl");
    1018
            // Flush does nothing but returns Ok
    1019
    1
            assert!(sink.flush().is_ok());
    1020
    1
        }
    1021
    1022
        #[test]
    1023
    1
        fn test_audit_logger_with_file_sink() {
    1024
            use std::fs;
    1025
    1
            let path = std::env::temp_dir().join("audit_test_logger_file.jsonl");
    1026
    1
            let sink = JsonFileAuditSink::new(&path);
    1027
    1
            let logger = AuditLogger::new(Box::new(sink));
    1028
    1029
    1
            let id = logger.log_request("FileTest", &[100, 200]);
    1030
    1
            logger.log_response(
    1031
    1
                id,
    1032
    1
                serde_json::json!(42),
    1033
    1
                Duration::from_millis(10),
    1034
    1
                Some(0.99),
    1035
            );
    1036
    1
            logger.flush().expect("flush");
    1037
    1038
    1
            let content = fs::read_to_string(&path).expect("read");
    1039
    1
            assert!(content.contains("FileTest"));
    1040
    1
            assert!(content.contains("100"));
    1041
    1
            assert!(content.contains("200"));
    1042
    1043
    1
            let _ = fs::remove_file(&path);
    1044
    1
        }
    1045
    1046
        #[test]
    1047
    1
        fn test_audit_record_with_explanation_summary() {
    1048
    1
            let record = AuditRecord::new(Uuid::new_v4(), "h", "T");
    1049
            // Verify explanation_summary is None by default
    1050
    1
            assert!(record.explanation_summary.is_none());
    1051
    1
        }
    1052
    1053
        #[test]
    1054
    1
        fn test_audit_options_default_values() {
    1055
    1
            let apr_opts = AuditOptions::apr(false, None);
    1056
    1
            assert!(!apr_opts.explain);
    1057
    1
            assert!(apr_opts.confidence_threshold.is_none());
    1058
    1059
    1
            let llm_opts = AuditOptions::llm(100, 1.0);
    1060
    1
            assert_eq!(llm_opts.max_tokens, Some(100));
    1061
    1
            assert_eq!(llm_opts.temperature, Some(1.0));
    1062
    1
        }
    1063
    }