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::{EffectPattern, 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    /// Uniform shift component in nanoseconds (β[0]).
74    pub shift_ns: f64,
75    /// Tail effect component in nanoseconds (β[1]).
76    pub tail_ns: f64,
77    /// Standard error of the shift component.
78    pub shift_se: f64,
79    /// Standard error of the tail component.
80    pub tail_se: f64,
81    /// 95% credible interval lower bound for total effect.
82    pub ci_low_ns: f64,
83    /// 95% credible interval upper bound for total effect.
84    pub ci_high_ns: f64,
85    /// Canonical effect pattern classification (from draws).
86    pub pattern: EffectPattern,
87    /// Posterior probability of timing leak: P(max_k |δ_k| > θ | data).
88    pub leak_probability: f64,
89    /// Projection mismatch Q statistic.
90    pub projection_mismatch_q: f64,
91    /// Number of samples used in this posterior computation.
92    pub n: usize,
93    /// Posterior mean of latent scale λ.
94    pub lambda_mean: f64,
95    /// Whether the λ chain mixed well.
96    pub lambda_mixing_ok: bool,
97    /// Posterior mean of likelihood precision κ.
98    pub kappa_mean: f64,
99    /// Coefficient of variation of κ.
100    pub kappa_cv: f64,
101    /// Effective sample size of κ chain.
102    pub kappa_ess: f64,
103    /// Whether the κ chain mixed well.
104    pub kappa_mixing_ok: bool,
105}
106
107impl PosteriorSummary {
108    /// Compute the total effect magnitude (L2 norm of shift and tail).
109    pub fn total_effect_ns(&self) -> f64 {
110        crate::math::sqrt(self.shift_ns * self.shift_ns + self.tail_ns * self.tail_ns)
111    }
112
113    /// Determine measurement quality from the effect standard error.
114    pub fn measurement_quality(&self) -> MeasurementQuality {
115        // MDE is approximately 2x the effect standard error
116        MeasurementQuality::from_mde_ns(self.shift_se * 2.0)
117    }
118
119    /// Determine exploitability from the total effect magnitude.
120    pub fn exploitability(&self) -> Exploitability {
121        Exploitability::from_effect_ns(self.total_effect_ns())
122    }
123}
124
125// ============================================================================
126// Calibration Summary
127// ============================================================================
128
129/// FFI-friendly summary of Calibration data.
130///
131/// Contains the essential scalar fields from `Calibration` needed by bindings.
132#[derive(Debug, Clone)]
133pub struct CalibrationSummary {
134    /// Block length from Politis-White algorithm.
135    pub block_length: usize,
136    /// Number of calibration samples collected per class.
137    pub calibration_samples: usize,
138    /// Whether discrete mode is active (< 10% unique values).
139    pub discrete_mode: bool,
140    /// Timer resolution in nanoseconds.
141    pub timer_resolution_ns: f64,
142    /// User's requested threshold in nanoseconds.
143    pub theta_ns: f64,
144    /// Effective threshold used for inference.
145    pub theta_eff: f64,
146    /// Initial measurement floor at calibration time.
147    pub theta_floor_initial: f64,
148    /// Timer tick floor component.
149    pub theta_tick: f64,
150    /// Minimum detectable effect (shift) in nanoseconds.
151    pub mde_shift_ns: f64,
152    /// Minimum detectable effect (tail) in nanoseconds.
153    pub mde_tail_ns: f64,
154    /// Bootstrap-calibrated threshold for projection mismatch Q.
155    pub projection_mismatch_thresh: f64,
156    /// Measured throughput (samples per second).
157    pub samples_per_second: f64,
158}
159
160// ============================================================================
161// Effect Summary
162// ============================================================================
163
164/// FFI-friendly effect estimate.
165///
166/// Contains the decomposed timing effect with credible interval and pattern.
167#[derive(Debug, Clone)]
168pub struct EffectSummary {
169    /// Uniform shift component in nanoseconds.
170    pub shift_ns: f64,
171    /// Tail effect component in nanoseconds.
172    pub tail_ns: f64,
173    /// 95% credible interval lower bound for total effect.
174    pub ci_low_ns: f64,
175    /// 95% credible interval upper bound for total effect.
176    pub ci_high_ns: f64,
177    /// Canonical effect pattern classification.
178    pub pattern: EffectPattern,
179    /// Interpretation caveat if model fit is poor.
180    pub interpretation_caveat: Option<String>,
181}
182
183impl EffectSummary {
184    /// Compute the total effect magnitude.
185    pub fn total_effect_ns(&self) -> f64 {
186        crate::math::sqrt(self.shift_ns * self.shift_ns + self.tail_ns * self.tail_ns)
187    }
188}
189
190impl Default for EffectSummary {
191    fn default() -> Self {
192        Self {
193            shift_ns: 0.0,
194            tail_ns: 0.0,
195            ci_low_ns: 0.0,
196            ci_high_ns: 0.0,
197            pattern: EffectPattern::Indeterminate,
198            interpretation_caveat: None,
199        }
200    }
201}
202
203// ============================================================================
204// Diagnostics Summary
205// ============================================================================
206
207/// FFI-friendly diagnostics.
208///
209/// Contains scalar diagnostic information from posterior and calibration.
210#[derive(Debug, Clone)]
211pub struct DiagnosticsSummary {
212    /// Block length used for bootstrap resampling.
213    pub dependence_length: usize,
214    /// Effective sample size accounting for autocorrelation.
215    pub effective_sample_size: usize,
216    /// Ratio of post-test variance to calibration variance.
217    pub stationarity_ratio: f64,
218    /// Whether stationarity check passed.
219    pub stationarity_ok: bool,
220    /// Projection mismatch Q statistic.
221    pub projection_mismatch_q: f64,
222    /// Whether projection mismatch is acceptable.
223    pub projection_mismatch_ok: bool,
224    /// Whether discrete mode was used (low timer resolution).
225    pub discrete_mode: bool,
226    /// Timer resolution in nanoseconds.
227    pub timer_resolution_ns: f64,
228    /// Posterior mean of latent scale λ.
229    pub lambda_mean: f64,
230    /// Whether λ chain mixed well.
231    pub lambda_mixing_ok: bool,
232    /// Posterior mean of likelihood precision κ.
233    pub kappa_mean: f64,
234    /// Coefficient of variation of κ.
235    pub kappa_cv: f64,
236    /// Effective sample size of κ chain.
237    pub kappa_ess: f64,
238    /// Whether κ chain mixed well.
239    pub kappa_mixing_ok: bool,
240}
241
242impl Default for DiagnosticsSummary {
243    fn default() -> Self {
244        Self {
245            dependence_length: 0,
246            effective_sample_size: 0,
247            stationarity_ratio: 1.0,
248            stationarity_ok: true,
249            projection_mismatch_q: 0.0,
250            projection_mismatch_ok: true,
251            discrete_mode: false,
252            timer_resolution_ns: 0.0,
253            lambda_mean: 1.0,
254            lambda_mixing_ok: true,
255            kappa_mean: 1.0,
256            kappa_cv: 0.0,
257            kappa_ess: 0.0,
258            kappa_mixing_ok: true,
259        }
260    }
261}
262
263// ============================================================================
264// Outcome Summary
265// ============================================================================
266
267/// FFI-friendly outcome summary.
268///
269/// Contains all information needed by bindings to construct their result types.
270#[derive(Debug, Clone)]
271pub struct OutcomeSummary {
272    /// Outcome type discriminant.
273    pub outcome_type: OutcomeType,
274    /// Posterior probability of timing leak.
275    pub leak_probability: f64,
276    /// Number of samples used per class.
277    pub samples_per_class: usize,
278    /// Elapsed time in seconds.
279    pub elapsed_secs: f64,
280    /// Effect estimate.
281    pub effect: EffectSummary,
282    /// Measurement quality assessment.
283    pub quality: MeasurementQuality,
284    /// Exploitability assessment.
285    pub exploitability: Exploitability,
286    /// Inconclusive reason (if outcome_type is Inconclusive).
287    pub inconclusive_reason: InconclusiveReasonKind,
288    /// Human-readable recommendation or guidance.
289    pub recommendation: String,
290    /// User's requested threshold in nanoseconds.
291    pub theta_user: f64,
292    /// Effective threshold used for inference.
293    pub theta_eff: f64,
294    /// Measurement floor in nanoseconds.
295    pub theta_floor: f64,
296    /// Timer tick floor component.
297    pub theta_tick: f64,
298    /// Whether the user's threshold is achievable at max_samples.
299    pub achievable_at_max: bool,
300    /// Diagnostics information.
301    pub diagnostics: DiagnosticsSummary,
302    /// Minimum detectable effect (shift) in nanoseconds.
303    pub mde_shift_ns: f64,
304    /// Minimum detectable effect (tail) in nanoseconds.
305    pub mde_tail_ns: f64,
306}
307
308impl OutcomeSummary {
309    /// Check if the outcome is conclusive (Pass or Fail).
310    pub fn is_conclusive(&self) -> bool {
311        matches!(self.outcome_type, OutcomeType::Pass | OutcomeType::Fail)
312    }
313
314    /// Check if a leak was detected.
315    pub fn is_leak_detected(&self) -> bool {
316        matches!(self.outcome_type, OutcomeType::Fail)
317    }
318}