Skip to main content

tacet_core/
ffi_summary.rs

1//! FFI-friendly summary types for tacet bindings.
2//!
3//! This module provides scalar-only summary types that can be easily converted
4//! to FFI structures in both NAPI and C bindings. These types extract the
5//! essential information from internal types like `Posterior`, `Calibration`,
6//! and `AdaptiveOutcome` without exposing nalgebra matrices.
7//!
8//! The goal is to:
9//! 1. Centralize conversion logic that was duplicated in bindings
10//! 2. Use canonical effect pattern classification (dominance-based from draws)
11//! 3. Provide consistent behavior across all FFI boundaries
12
13extern crate alloc;
14use alloc::string::String;
15
16use crate::result::{Exploitability, MeasurementQuality};
17
18// ============================================================================
19// Outcome Type Enum
20// ============================================================================
21
22/// FFI-friendly outcome type discriminant.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[repr(u8)]
25pub enum OutcomeType {
26    /// No timing leak detected.
27    Pass = 0,
28    /// Timing leak detected.
29    Fail = 1,
30    /// Could not reach a decision.
31    Inconclusive = 2,
32    /// Threshold was elevated beyond tolerance.
33    ThresholdElevated = 3,
34}
35
36// ============================================================================
37// Inconclusive Reason Kind
38// ============================================================================
39
40/// FFI-friendly inconclusive reason discriminant.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
42#[repr(u8)]
43pub enum InconclusiveReasonKind {
44    /// Not applicable (outcome is not Inconclusive).
45    #[default]
46    None = 0,
47    /// Posterior approximately equals prior after calibration.
48    DataTooNoisy = 1,
49    /// Posterior stopped updating despite new data.
50    NotLearning = 2,
51    /// Estimated time to decision exceeds budget.
52    WouldTakeTooLong = 3,
53    /// Time budget exhausted.
54    TimeBudgetExceeded = 4,
55    /// Sample limit reached.
56    SampleBudgetExceeded = 5,
57    /// Measurement conditions changed during test.
58    ConditionsChanged = 6,
59    /// Threshold was elevated due to measurement noise.
60    ThresholdElevated = 7,
61}
62
63// ============================================================================
64// Posterior Summary
65// ============================================================================
66
67/// FFI-friendly summary of a Posterior distribution (all scalars).
68///
69/// Extracts all relevant information from a `Posterior` struct without
70/// exposing nalgebra matrices.
71#[derive(Debug, Clone)]
72pub struct PosteriorSummary {
73    /// Maximum effect across all deciles: max_k |δ_k| in nanoseconds.
74    pub max_effect_ns: f64,
75    /// 95% credible interval lower bound for max effect.
76    pub ci_low_ns: f64,
77    /// 95% credible interval upper bound for max effect.
78    pub ci_high_ns: f64,
79    /// Posterior probability of timing leak: P(max_k |δ_k| > θ | data).
80    pub leak_probability: f64,
81    /// Number of samples used in this posterior computation.
82    pub n: usize,
83    /// Posterior mean of latent scale λ.
84    pub lambda_mean: f64,
85    /// Whether the λ chain mixed well.
86    pub lambda_mixing_ok: bool,
87    /// Posterior mean of likelihood precision κ.
88    pub kappa_mean: f64,
89    /// Coefficient of variation of κ.
90    pub kappa_cv: f64,
91    /// Effective sample size of κ chain.
92    pub kappa_ess: f64,
93    /// Whether the κ chain mixed well.
94    pub kappa_mixing_ok: bool,
95}
96
97impl PosteriorSummary {
98    /// Get the maximum effect magnitude.
99    pub fn total_effect_ns(&self) -> f64 {
100        self.max_effect_ns
101    }
102
103    /// Determine measurement quality from the credible interval width.
104    pub fn measurement_quality(&self) -> MeasurementQuality {
105        // MDE is approximately half the CI width
106        let ci_width = self.ci_high_ns - self.ci_low_ns;
107        MeasurementQuality::from_mde_ns(ci_width / 2.0)
108    }
109
110    /// Determine exploitability from the maximum effect magnitude.
111    pub fn exploitability(&self) -> Exploitability {
112        Exploitability::from_effect_ns(self.max_effect_ns)
113    }
114}
115
116// ============================================================================
117// Calibration Summary
118// ============================================================================
119
120/// FFI-friendly summary of Calibration data.
121///
122/// Contains the essential scalar fields from `Calibration` needed by bindings.
123#[derive(Debug, Clone)]
124pub struct CalibrationSummary {
125    /// Block length from Politis-White algorithm.
126    pub block_length: usize,
127    /// Number of calibration samples collected per class.
128    pub calibration_samples: usize,
129    /// Whether discrete mode is active (< 10% unique values).
130    pub discrete_mode: bool,
131    /// Timer resolution in nanoseconds.
132    pub timer_resolution_ns: f64,
133    /// User's requested threshold in nanoseconds.
134    pub theta_ns: f64,
135    /// Effective threshold used for inference.
136    pub theta_eff: f64,
137    /// Initial measurement floor at calibration time.
138    pub theta_floor_initial: f64,
139    /// Timer tick floor component.
140    pub theta_tick: f64,
141    /// Minimum detectable effect in nanoseconds.
142    pub mde_ns: f64,
143    /// Measured throughput (samples per second).
144    pub samples_per_second: f64,
145}
146
147// ============================================================================
148// Effect Summary
149// ============================================================================
150
151/// FFI-friendly effect estimate.
152///
153/// Contains the timing effect with credible interval.
154#[derive(Debug, Clone)]
155pub struct EffectSummary {
156    /// Maximum effect in nanoseconds: max_k |δ_k|.
157    pub max_effect_ns: f64,
158    /// 95% credible interval lower bound for max effect.
159    pub ci_low_ns: f64,
160    /// 95% credible interval upper bound for max effect.
161    pub ci_high_ns: f64,
162}
163
164impl EffectSummary {
165    /// Get the maximum effect magnitude.
166    pub fn total_effect_ns(&self) -> f64 {
167        self.max_effect_ns
168    }
169}
170
171impl Default for EffectSummary {
172    fn default() -> Self {
173        Self {
174            max_effect_ns: 0.0,
175            ci_low_ns: 0.0,
176            ci_high_ns: 0.0,
177        }
178    }
179}
180
181// ============================================================================
182// Diagnostics Summary
183// ============================================================================
184
185/// FFI-friendly diagnostics.
186///
187/// Contains scalar diagnostic information from posterior and calibration.
188#[derive(Debug, Clone)]
189pub struct DiagnosticsSummary {
190    /// Block length used for bootstrap resampling.
191    pub dependence_length: usize,
192    /// Effective sample size accounting for autocorrelation.
193    pub effective_sample_size: usize,
194    /// Ratio of post-test variance to calibration variance.
195    pub stationarity_ratio: f64,
196    /// Whether stationarity check passed.
197    pub stationarity_ok: bool,
198    /// Whether discrete mode was used (low timer resolution).
199    pub discrete_mode: bool,
200    /// Timer resolution in nanoseconds.
201    pub timer_resolution_ns: f64,
202    /// Posterior mean of latent scale λ.
203    pub lambda_mean: f64,
204    /// Whether λ chain mixed well.
205    pub lambda_mixing_ok: bool,
206    /// Posterior mean of likelihood precision κ.
207    pub kappa_mean: f64,
208    /// Coefficient of variation of κ.
209    pub kappa_cv: f64,
210    /// Effective sample size of κ chain.
211    pub kappa_ess: f64,
212    /// Whether κ chain mixed well.
213    pub kappa_mixing_ok: bool,
214}
215
216impl Default for DiagnosticsSummary {
217    fn default() -> Self {
218        Self {
219            dependence_length: 0,
220            effective_sample_size: 0,
221            stationarity_ratio: 1.0,
222            stationarity_ok: true,
223            discrete_mode: false,
224            timer_resolution_ns: 0.0,
225            lambda_mean: 1.0,
226            lambda_mixing_ok: true,
227            kappa_mean: 1.0,
228            kappa_cv: 0.0,
229            kappa_ess: 0.0,
230            kappa_mixing_ok: true,
231        }
232    }
233}
234
235// ============================================================================
236// Outcome Summary
237// ============================================================================
238
239/// FFI-friendly outcome summary.
240///
241/// Contains all information needed by bindings to construct their result types.
242#[derive(Debug, Clone)]
243pub struct OutcomeSummary {
244    /// Outcome type discriminant.
245    pub outcome_type: OutcomeType,
246    /// Posterior probability of timing leak.
247    pub leak_probability: f64,
248    /// Number of samples used per class.
249    pub samples_per_class: usize,
250    /// Elapsed time in seconds.
251    pub elapsed_secs: f64,
252    /// Effect estimate.
253    pub effect: EffectSummary,
254    /// Measurement quality assessment.
255    pub quality: MeasurementQuality,
256    /// Exploitability assessment.
257    pub exploitability: Exploitability,
258    /// Inconclusive reason (if outcome_type is Inconclusive).
259    pub inconclusive_reason: InconclusiveReasonKind,
260    /// Human-readable recommendation or guidance.
261    pub recommendation: String,
262    /// User's requested threshold in nanoseconds.
263    pub theta_user: f64,
264    /// Effective threshold used for inference.
265    pub theta_eff: f64,
266    /// Measurement floor in nanoseconds.
267    pub theta_floor: f64,
268    /// Timer tick floor component.
269    pub theta_tick: f64,
270    /// Whether the user's threshold is achievable at max_samples.
271    pub achievable_at_max: bool,
272    /// Diagnostics information.
273    pub diagnostics: DiagnosticsSummary,
274    /// Minimum detectable effect in nanoseconds.
275    pub mde_ns: f64,
276}
277
278impl OutcomeSummary {
279    /// Check if the outcome is conclusive (Pass or Fail).
280    pub fn is_conclusive(&self) -> bool {
281        matches!(self.outcome_type, OutcomeType::Pass | OutcomeType::Fail)
282    }
283
284    /// Check if a leak was detected.
285    pub fn is_leak_detected(&self) -> bool {
286        matches!(self.outcome_type, OutcomeType::Fail)
287    }
288}