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}