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}