Skip to main content

tacet_core/
formatting.rs

1//! Formatting helpers for Outcome display.
2//!
3//! These functions produce output that can be used directly by Display/Debug
4//! implementations. When the `std` feature is enabled, output includes ANSI
5//! color codes (via the `colored` crate). Without `std`, output is plain text.
6
7extern crate alloc;
8
9use alloc::format;
10use alloc::string::{String, ToString};
11use alloc::vec::Vec;
12use core::fmt::Write;
13
14use crate::colors::{
15    bold, bold_cyan, bold_green, bold_red, bold_yellow, dim, green, red, yellow,
16};
17use crate::result::{
18    Diagnostics, EffectEstimate, Exploitability, MeasurementQuality, Outcome, PreflightCategory,
19    PreflightSeverity, ResearchOutcome, ResearchStatus,
20};
21
22/// Separator line used in output.
23pub const SEPARATOR: &str = "──────────────────────────────────────────────────────────────";
24
25/// Default terminal width for text wrapping.
26const DEFAULT_WRAP_WIDTH: usize = 72;
27
28// ============================================================================
29// Main formatting functions
30// ============================================================================
31
32/// Format an Outcome for human-readable output (no colors).
33///
34/// This produces the full output including all diagnostic sections.
35pub fn format_outcome_plain(outcome: &Outcome) -> String {
36    let mut out = String::new();
37
38    writeln!(out, "tacet").unwrap();
39    writeln!(out, "{}", SEPARATOR).unwrap();
40    writeln!(out).unwrap();
41
42    match outcome {
43        Outcome::Pass {
44            leak_probability,
45            effect,
46            samples_used,
47            quality,
48            diagnostics,
49            ..
50        } => {
51            format_header(&mut out, *samples_used, *quality);
52            writeln!(out, "  {}", bold_green("\u{2713} No timing leak detected")).unwrap();
53            writeln!(out).unwrap();
54            format_pass_body(&mut out, *leak_probability, effect);
55            format_preflight_notes(&mut out, diagnostics);
56            format_diagnostics_section(&mut out, diagnostics);
57            format_reproduction_line(&mut out, diagnostics);
58            format_debug_environment(&mut out, diagnostics);
59        }
60
61        Outcome::Fail {
62            leak_probability,
63            effect,
64            exploitability,
65            samples_used,
66            quality,
67            diagnostics,
68            ..
69        } => {
70            format_header(&mut out, *samples_used, *quality);
71            writeln!(out, "  {}", bold_yellow("\u{26A0} Timing leak detected")).unwrap();
72            writeln!(out).unwrap();
73            format_fail_body(&mut out, *leak_probability, effect, *exploitability);
74            format_preflight_notes(&mut out, diagnostics);
75            format_diagnostics_section(&mut out, diagnostics);
76            format_reproduction_line(&mut out, diagnostics);
77            format_debug_environment(&mut out, diagnostics);
78        }
79
80        Outcome::Inconclusive {
81            reason,
82            leak_probability,
83            effect,
84            samples_used,
85            quality,
86            diagnostics,
87            ..
88        } => {
89            format_header(&mut out, *samples_used, *quality);
90            writeln!(out, "  {}", bold_cyan("? Inconclusive")).unwrap();
91            writeln!(out, "    {}", reason).unwrap();
92            writeln!(out).unwrap();
93            format_inconclusive_body(&mut out, *leak_probability, effect);
94            format_inconclusive_diagnostics(&mut out, diagnostics);
95            format_preflight_validation(&mut out, diagnostics);
96            format_diagnostics_section(&mut out, diagnostics);
97            format_reproduction_line(&mut out, diagnostics);
98            format_debug_environment(&mut out, diagnostics);
99        }
100
101        Outcome::Unmeasurable {
102            operation_ns,
103            threshold_ns,
104            platform,
105            recommendation,
106        } => {
107            writeln!(out, "  {}", bold_yellow("\u{26A0} Operation too fast to measure reliably")).unwrap();
108            writeln!(out).unwrap();
109            writeln!(out, "    Estimated duration: ~{:.1} ns", operation_ns).unwrap();
110            writeln!(out, "    Minimum measurable: ~{:.1} ns", threshold_ns).unwrap();
111            writeln!(out, "    Platform: {}", platform).unwrap();
112            writeln!(out).unwrap();
113            writeln!(out, "    Recommendation: {}", recommendation).unwrap();
114            writeln!(out).unwrap();
115            writeln!(out, "{}", SEPARATOR).unwrap();
116            write!(
117                out,
118                "Note: Results are unmeasurable at this resolution; no leak probability is reported."
119            )
120            .unwrap();
121            return out;
122        }
123
124        Outcome::Research(research) => {
125            format_research_outcome(&mut out, research);
126        }
127    }
128
129    writeln!(out).unwrap();
130    writeln!(out, "{}", SEPARATOR).unwrap();
131
132    if matches!(outcome, Outcome::Fail { .. }) {
133        write!(
134            out,
135            "Note: Exploitability is a heuristic estimate based on effect magnitude."
136        )
137        .unwrap();
138    }
139
140    out
141}
142
143/// Format a compact debug summary (no colors).
144pub fn format_debug_summary_plain(outcome: &Outcome) -> String {
145    let mut out = String::new();
146
147    writeln!(out, "\u{250C}\u{2500} Debug Summary {}", "\u{2500}".repeat(40)).unwrap();
148
149    match outcome {
150        Outcome::Pass {
151            leak_probability,
152            effect,
153            quality,
154            samples_used,
155            diagnostics,
156            ..
157        }
158        | Outcome::Fail {
159            leak_probability,
160            effect,
161            quality,
162            samples_used,
163            diagnostics,
164            ..
165        }
166        | Outcome::Inconclusive {
167            leak_probability,
168            effect,
169            quality,
170            samples_used,
171            diagnostics,
172            ..
173        } => {
174            let outcome_type = match outcome {
175                Outcome::Pass { .. } => "PASS",
176                Outcome::Fail { .. } => "FAIL",
177                Outcome::Inconclusive { .. } => "INCONCLUSIVE",
178                _ => unreachable!(),
179            };
180            writeln!(out, "\u{2502} Outcome = {}", outcome_type).unwrap();
181            format_debug_core_metrics(&mut out, *leak_probability, effect, *quality, *samples_used, diagnostics);
182            format_debug_warnings(&mut out, diagnostics);
183            format_debug_diagnostics(&mut out, diagnostics);
184        }
185
186        Outcome::Unmeasurable {
187            operation_ns,
188            threshold_ns,
189            platform,
190            recommendation,
191        } => {
192            writeln!(out, "\u{2502} Outcome = UNMEASURABLE").unwrap();
193            writeln!(out, "\u{2502}   Operation: ~{:.1}ns", operation_ns).unwrap();
194            writeln!(out, "\u{2502}   Threshold: ~{:.1}ns", threshold_ns).unwrap();
195            writeln!(out, "\u{2502}   Platform: {}", platform).unwrap();
196            writeln!(out, "\u{2502}   Tip: {}", recommendation).unwrap();
197        }
198
199        Outcome::Research(research) => {
200            format_debug_research(&mut out, research);
201        }
202    }
203
204    write!(out, "\u{2514}{}", "\u{2500}".repeat(55)).unwrap();
205
206    out
207}
208
209// ============================================================================
210// Section formatting helpers
211// ============================================================================
212
213fn format_header(out: &mut String, samples_used: usize, quality: MeasurementQuality) {
214    writeln!(out, "  Samples: {} per class", samples_used).unwrap();
215    writeln!(out, "  Quality: {}", format_quality_colored(quality)).unwrap();
216    writeln!(out).unwrap();
217}
218
219/// Format quality with colors (when std feature is enabled).
220fn format_quality_colored(quality: MeasurementQuality) -> String {
221    match quality {
222        MeasurementQuality::Excellent => green("Excellent"),
223        MeasurementQuality::Good => green("Good"),
224        MeasurementQuality::Poor => yellow("Poor"),
225        MeasurementQuality::TooNoisy => yellow("too noisy"),
226    }
227}
228
229fn format_pass_body(out: &mut String, leak_probability: f64, effect: &EffectEstimate) {
230    writeln!(out, "    Probability of leak: {:.1}%", leak_probability * 100.0).unwrap();
231    writeln!(
232        out,
233        "    95% CI: {:.1}\u{2013}{:.1} ns",
234        effect.credible_interval_ns.0, effect.credible_interval_ns.1
235    )
236    .unwrap();
237}
238
239fn format_fail_body(
240    out: &mut String,
241    leak_probability: f64,
242    effect: &EffectEstimate,
243    exploitability: Exploitability,
244) {
245    writeln!(out, "    Probability of leak: {:.1}%", leak_probability * 100.0).unwrap();
246
247    let magnitude = effect.total_effect_ns();
248    writeln!(out, "    Effect: {:.1} ns ({})", magnitude, effect.pattern).unwrap();
249    writeln!(out, "      Shift: {:.1} ns", effect.shift_ns).unwrap();
250    writeln!(out, "      Tail:  {:.1} ns", effect.tail_ns).unwrap();
251    writeln!(
252        out,
253        "      95% CI: {:.1}\u{2013}{:.1} ns",
254        effect.credible_interval_ns.0, effect.credible_interval_ns.1
255    )
256    .unwrap();
257
258    writeln!(out).unwrap();
259    writeln!(out, "    Exploitability (heuristic):").unwrap();
260    let (vector, queries) = exploitability_info_colored(exploitability);
261    writeln!(out, "      Attack vector:  {}", vector).unwrap();
262    writeln!(out, "      Queries needed: {}", queries).unwrap();
263}
264
265fn format_inconclusive_body(out: &mut String, leak_probability: f64, effect: &EffectEstimate) {
266    writeln!(
267        out,
268        "    Current probability of leak: {:.1}%",
269        leak_probability * 100.0
270    )
271    .unwrap();
272
273    let magnitude = effect.total_effect_ns();
274    writeln!(out, "    Effect estimate: {:.1} ns ({})", magnitude, effect.pattern).unwrap();
275}
276
277fn format_research_outcome(out: &mut String, research: &ResearchOutcome) {
278    writeln!(out, "  Samples: {} per class", research.samples_used).unwrap();
279    writeln!(out, "  Quality: {}", format_quality_colored(research.quality)).unwrap();
280    writeln!(out).unwrap();
281
282    let status_line = match &research.status {
283        ResearchStatus::EffectDetected => bold_green("\u{1F50D} Effect Detected"),
284        ResearchStatus::NoEffectDetected => bold_cyan("\u{2713} No Effect Detected"),
285        ResearchStatus::ResolutionLimitReached => bold_yellow("\u{26A0} Resolution Limit Reached"),
286        ResearchStatus::QualityIssue(_) => bold_yellow("? Quality Issue"),
287        ResearchStatus::BudgetExhausted => bold_cyan("? Budget Exhausted"),
288    };
289    writeln!(out, "  {}", status_line).unwrap();
290    writeln!(out).unwrap();
291
292    writeln!(out, "    Max effect: {:.1} ns", research.max_effect_ns).unwrap();
293    writeln!(
294        out,
295        "    95% CI: [{:.1}, {:.1}] ns",
296        research.max_effect_ci.0, research.max_effect_ci.1
297    )
298    .unwrap();
299    writeln!(out, "    Measurement floor: {:.1} ns", research.theta_floor).unwrap();
300    writeln!(
301        out,
302        "    Detectable: {}",
303        if research.detectable { "yes" } else { "no" }
304    )
305    .unwrap();
306
307    writeln!(out).unwrap();
308    writeln!(
309        out,
310        "    Effect: {:.1} ns ({})",
311        research.effect.total_effect_ns(),
312        research.effect.pattern
313    )
314    .unwrap();
315    writeln!(out, "      Shift: {:.1} ns", research.effect.shift_ns).unwrap();
316    writeln!(out, "      Tail:  {:.1} ns", research.effect.tail_ns).unwrap();
317
318    if research.model_mismatch {
319        writeln!(out).unwrap();
320        writeln!(
321            out,
322            "    {} Model mismatch detected \u{2013} interpret with caution",
323            yellow("\u{26A0}")
324        )
325        .unwrap();
326        if let Some(ref caveat) = research.effect.interpretation_caveat {
327            writeln!(out, "      {}", caveat).unwrap();
328        }
329    }
330
331    // Include diagnostics
332    format_diagnostics_section(out, &research.diagnostics);
333    format_reproduction_line(out, &research.diagnostics);
334    format_debug_environment(out, &research.diagnostics);
335}
336
337// ============================================================================
338// Diagnostics formatting
339// ============================================================================
340
341/// Format the detailed diagnostics section.
342pub fn format_diagnostics_section(out: &mut String, diagnostics: &Diagnostics) {
343    writeln!(out).unwrap();
344    writeln!(out, "{}", SEPARATOR).unwrap();
345    writeln!(out).unwrap();
346    writeln!(out, "  Measurement Diagnostics").unwrap();
347    writeln!(out).unwrap();
348
349    writeln!(
350        out,
351        "    Dependence:   block length {} (ESS: {} / {} raw)",
352        diagnostics.dependence_length,
353        diagnostics.effective_sample_size,
354        diagnostics.calibration_samples
355    )
356    .unwrap();
357
358    let projection_status = if diagnostics.projection_mismatch_ok {
359        green("OK")
360    } else {
361        red("Mismatch")
362    };
363    writeln!(
364        out,
365        "    Projection:   Q = {:.1} ({})",
366        diagnostics.projection_mismatch_q, projection_status
367    )
368    .unwrap();
369
370    let mut outlier_line = format!(
371        "    Outliers:     baseline {:.2}%, sample {:.2}%",
372        diagnostics.outlier_rate_baseline * 100.0,
373        diagnostics.outlier_rate_sample * 100.0
374    );
375    if !diagnostics.outlier_asymmetry_ok {
376        outlier_line.push_str(&format!(" ({})", red("asymmetric")));
377    }
378    writeln!(out, "{}", outlier_line).unwrap();
379
380    writeln!(
381        out,
382        "    Calibration:  {} samples",
383        diagnostics.calibration_samples
384    )
385    .unwrap();
386
387    writeln!(out, "    Runtime:      {:.1}s", diagnostics.total_time_secs).unwrap();
388
389    // Warnings
390    if !diagnostics.warnings.is_empty() {
391        writeln!(out).unwrap();
392        writeln!(out, "  {} Warnings", yellow("\u{26A0}")).unwrap();
393        for warning in &diagnostics.warnings {
394            writeln!(out, "    \u{2022} {}", warning).unwrap();
395        }
396    }
397
398    // Quality issues with guidance
399    if !diagnostics.quality_issues.is_empty() {
400        writeln!(out).unwrap();
401        writeln!(out, "  {} Quality Issues", yellow("\u{26A0}")).unwrap();
402        for issue in &diagnostics.quality_issues {
403            let wrapped_msg = wrap_text(&issue.message, DEFAULT_WRAP_WIDTH, 20, "                    ");
404            writeln!(out, "    \u{2022} {}: {}", bold(&format!("{:?}", issue.code)), wrapped_msg).unwrap();
405            let wrapped_guidance = wrap_text(&issue.guidance, DEFAULT_WRAP_WIDTH, 8, "        ");
406            writeln!(out, "      \u{2192} {}", dim(&wrapped_guidance)).unwrap();
407        }
408    }
409}
410
411/// Format reproduction line for output.
412pub fn format_reproduction_line(out: &mut String, diagnostics: &Diagnostics) {
413    let mut parts = Vec::new();
414
415    if let Some(ref model) = diagnostics.attacker_model {
416        parts.push(format!("model={}", model));
417    }
418    if diagnostics.threshold_ns > 0.0 {
419        parts.push(format!("\u{03B8}={:.0}ns", diagnostics.threshold_ns));
420    }
421    if !diagnostics.timer_name.is_empty() {
422        parts.push(format!("timer={}", diagnostics.timer_name));
423    }
424
425    if !parts.is_empty() {
426        writeln!(out).unwrap();
427        writeln!(out, "  Reproduce: {}", parts.join(", ")).unwrap();
428    }
429}
430
431/// Format extended debug environment information.
432pub fn format_debug_environment(out: &mut String, diagnostics: &Diagnostics) {
433    writeln!(out).unwrap();
434    writeln!(out, "{}", SEPARATOR).unwrap();
435    writeln!(out).unwrap();
436    writeln!(out, "  Debug Information").unwrap();
437    writeln!(out).unwrap();
438
439    // Environment
440    writeln!(out, "    Environment:").unwrap();
441    let platform = if diagnostics.platform.is_empty() {
442        #[cfg(feature = "std")]
443        {
444            format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH)
445        }
446        #[cfg(not(feature = "std"))]
447        {
448            "unknown".to_string()
449        }
450    } else {
451        diagnostics.platform.clone()
452    };
453    writeln!(out, "      Platform:       {}", platform).unwrap();
454    writeln!(out, "      Rust version:   1.80").unwrap();
455    writeln!(out, "      Package:        tacet v0.1.5").unwrap();
456
457    // Configuration
458    writeln!(out).unwrap();
459    writeln!(out, "    Configuration:").unwrap();
460    if let Some(ref model) = diagnostics.attacker_model {
461        writeln!(out, "      Attacker model: {}", model).unwrap();
462    }
463    writeln!(out, "      Threshold (\u{03B8}):  {:.1} ns", diagnostics.threshold_ns).unwrap();
464    if !diagnostics.timer_name.is_empty() {
465        // Format timer line with fallback reason if present
466        let timer_display = match &diagnostics.timer_fallback_reason {
467            Some(reason) => format!("{} (fallback: {})", diagnostics.timer_name, reason),
468            None => diagnostics.timer_name.clone(),
469        };
470        writeln!(out, "      Timer:          {}", timer_display).unwrap();
471    }
472    writeln!(out, "      Resolution:     {:.1} ns", diagnostics.timer_resolution_ns).unwrap();
473    if diagnostics.discrete_mode {
474        writeln!(out, "      Discrete mode:  enabled").unwrap();
475    }
476
477    // Statistical summary
478    writeln!(out).unwrap();
479    writeln!(out, "    Statistical Summary:").unwrap();
480    writeln!(
481        out,
482        "      Calibration:    {} samples",
483        diagnostics.calibration_samples
484    )
485    .unwrap();
486    writeln!(out, "      Block length:   {}", diagnostics.dependence_length).unwrap();
487    writeln!(out, "      ESS:            {}", diagnostics.effective_sample_size).unwrap();
488    writeln!(
489        out,
490        "      Stationarity:   {:.2}x {}",
491        diagnostics.stationarity_ratio,
492        if diagnostics.stationarity_ok { "OK" } else { "Suspect" }
493    )
494    .unwrap();
495    writeln!(
496        out,
497        "      Projection:     Q={:.1} {}",
498        diagnostics.projection_mismatch_q,
499        if diagnostics.projection_mismatch_ok { "OK" } else { "Mismatch" }
500    )
501    .unwrap();
502
503    writeln!(out).unwrap();
504    writeln!(
505        out,
506        "    For bug reports, re-run with TIMING_ORACLE_DEBUG=1 and include both outputs"
507    )
508    .unwrap();
509}
510
511/// Format preflight validation section for verbose output.
512pub fn format_preflight_validation(out: &mut String, diagnostics: &Diagnostics) {
513    writeln!(out).unwrap();
514    writeln!(out, "{}", SEPARATOR).unwrap();
515    writeln!(out).unwrap();
516    writeln!(out, "  Preflight Checks").unwrap();
517
518    // Group warnings by category
519    let sanity: Vec<_> = diagnostics
520        .preflight_warnings
521        .iter()
522        .filter(|w| w.category == PreflightCategory::Sanity)
523        .collect();
524    let timer_sanity: Vec<_> = diagnostics
525        .preflight_warnings
526        .iter()
527        .filter(|w| w.category == PreflightCategory::TimerSanity)
528        .collect();
529    let autocorr: Vec<_> = diagnostics
530        .preflight_warnings
531        .iter()
532        .filter(|w| w.category == PreflightCategory::Autocorrelation)
533        .collect();
534    let system: Vec<_> = diagnostics
535        .preflight_warnings
536        .iter()
537        .filter(|w| w.category == PreflightCategory::System)
538        .collect();
539    let resolution: Vec<_> = diagnostics
540        .preflight_warnings
541        .iter()
542        .filter(|w| w.category == PreflightCategory::Resolution)
543        .collect();
544
545    // Result Integrity section
546    writeln!(out).unwrap();
547    writeln!(out, "    Result Integrity:").unwrap();
548
549    if sanity.is_empty() {
550        writeln!(out, "      Sanity (F-vs-F):    OK").unwrap();
551    } else {
552        for w in &sanity {
553            writeln!(
554                out,
555                "      Sanity (F-vs-F):    {}",
556                format_severity(w.severity)
557            )
558            .unwrap();
559            writeln!(out, "        {}", w.message).unwrap();
560        }
561    }
562
563    if timer_sanity.is_empty() {
564        writeln!(out, "      Timer monotonic:    OK").unwrap();
565    } else {
566        for w in &timer_sanity {
567            writeln!(
568                out,
569                "      Timer monotonic:    {}",
570                format_severity(w.severity)
571            )
572            .unwrap();
573            writeln!(out, "        {}", w.message).unwrap();
574        }
575    }
576
577    let stationarity_status = if diagnostics.stationarity_ok {
578        format!("OK {:.2}x", diagnostics.stationarity_ratio)
579    } else {
580        format!("Suspect {:.2}x", diagnostics.stationarity_ratio)
581    };
582    writeln!(out, "      Stationarity:       {}", stationarity_status).unwrap();
583
584    // Sampling Efficiency section
585    writeln!(out).unwrap();
586    writeln!(out, "    Sampling Efficiency:").unwrap();
587
588    if autocorr.is_empty() {
589        writeln!(out, "      Autocorrelation:    OK").unwrap();
590    } else {
591        for w in &autocorr {
592            writeln!(
593                out,
594                "      Autocorrelation:    {}",
595                format_severity(w.severity)
596            )
597            .unwrap();
598            writeln!(out, "        {}", w.message).unwrap();
599        }
600    }
601
602    let timer_name = if diagnostics.timer_name.is_empty() {
603        String::new()
604    } else {
605        format!(" ({})", diagnostics.timer_name)
606    };
607    if resolution.is_empty() {
608        writeln!(
609            out,
610            "      Timer resolution:   OK {:.1}ns{}",
611            diagnostics.timer_resolution_ns, timer_name
612        )
613        .unwrap();
614    } else {
615        for w in &resolution {
616            writeln!(
617                out,
618                "      Timer resolution:   {} {:.1}ns{}",
619                format_severity(w.severity),
620                diagnostics.timer_resolution_ns,
621                timer_name
622            )
623            .unwrap();
624            writeln!(out, "        {}", w.message).unwrap();
625        }
626    }
627
628    // System Configuration section
629    writeln!(out).unwrap();
630    writeln!(out, "    System:").unwrap();
631    if system.is_empty() {
632        writeln!(out, "      Configuration:      OK").unwrap();
633    } else {
634        for w in &system {
635            writeln!(out, "      \u{26A0} {}", w.message).unwrap();
636            if let Some(guidance) = &w.guidance {
637                writeln!(out, "        {}", guidance).unwrap();
638            }
639        }
640    }
641}
642
643/// Format measurement notes for non-verbose output.
644fn format_preflight_notes(out: &mut String, diagnostics: &Diagnostics) {
645    if diagnostics.preflight_warnings.is_empty() {
646        return;
647    }
648
649    let has_critical = diagnostics
650        .preflight_warnings
651        .iter()
652        .any(|w| w.severity == PreflightSeverity::ResultUndermining);
653
654    writeln!(out).unwrap();
655    if has_critical {
656        writeln!(out, "  \u{26A0} Measurement Notes:").unwrap();
657    } else {
658        writeln!(out, "  Measurement Notes:").unwrap();
659    }
660
661    for warning in &diagnostics.preflight_warnings {
662        let bullet = match warning.severity {
663            PreflightSeverity::ResultUndermining => "\u{2022}",
664            PreflightSeverity::Informational => "\u{2022}",
665        };
666        writeln!(out, "    {} {}", bullet, warning.message).unwrap();
667
668        // For resolution warnings, provide context-aware guidance based on fallback reason
669        let guidance = if warning.category == PreflightCategory::Resolution {
670            resolution_guidance_for_fallback(diagnostics.timer_fallback_reason.as_deref())
671        } else {
672            warning.guidance.clone()
673        };
674
675        if let Some(g) = guidance {
676            writeln!(out, "      {}", g).unwrap();
677        }
678    }
679}
680
681/// Generate context-aware guidance for resolution warnings based on timer fallback reason.
682fn resolution_guidance_for_fallback(fallback_reason: Option<&str>) -> Option<String> {
683    match fallback_reason {
684        Some("concurrent access") => Some(
685            "Cycle-accurate timing is locked by another process. \
686             If using cargo test, run with --test-threads=1."
687                .into(),
688        ),
689        Some("no sudo") => Some(
690            "Run with sudo to enable cycle-accurate timing (~0.3ns resolution)."
691                .into(),
692        ),
693        Some("unavailable") => Some(
694            "Cycle-accurate timing unavailable. Consider increasing max_samples \
695             or testing at a higher abstraction level."
696                .into(),
697        ),
698        Some("user requested") => Some(
699            "System timer was explicitly requested. For better resolution, \
700             use TimerSpec::Auto or TimerSpec::RequireCycleAccurate."
701                .into(),
702        ),
703        // x86_64 or already using cycle-accurate timer
704        None => Some(
705            "Timer resolution may be limiting measurement quality. \
706             Consider increasing max_samples or time_budget."
707                .into(),
708        ),
709        // Unknown fallback reason
710        Some(_) => Some(
711            "Timer resolution may be limiting measurement quality.".into(),
712        ),
713    }
714}
715
716/// Format "Why This May Have Happened" section for Inconclusive outcomes.
717fn format_inconclusive_diagnostics(out: &mut String, diagnostics: &Diagnostics) {
718    let system_config: Vec<_> = diagnostics
719        .preflight_warnings
720        .iter()
721        .filter(|w| w.category == PreflightCategory::System)
722        .collect();
723    let resolution: Vec<_> = diagnostics
724        .preflight_warnings
725        .iter()
726        .filter(|w| w.category == PreflightCategory::Resolution)
727        .collect();
728
729    if system_config.is_empty() && resolution.is_empty() {
730        return;
731    }
732
733    writeln!(out).unwrap();
734    writeln!(out, "  \u{2139} Why This May Have Happened:").unwrap();
735
736    if !system_config.is_empty() {
737        writeln!(out).unwrap();
738        writeln!(out, "    System Configuration:").unwrap();
739        for warning in system_config {
740            writeln!(out, "      \u{2022} {}", warning.message).unwrap();
741        }
742    }
743
744    if !resolution.is_empty() {
745        writeln!(out).unwrap();
746        writeln!(out, "    Timer Resolution:").unwrap();
747        for warning in resolution {
748            writeln!(out, "      \u{2022} {}", warning.message).unwrap();
749            if let Some(guidance) = &warning.guidance {
750                writeln!(out, "      \u{2192} Tip: {}", guidance).unwrap();
751            }
752        }
753    }
754}
755
756// ============================================================================
757// Debug summary helpers
758// ============================================================================
759
760fn format_debug_core_metrics(
761    out: &mut String,
762    leak_probability: f64,
763    effect: &EffectEstimate,
764    quality: MeasurementQuality,
765    samples_used: usize,
766    diagnostics: &Diagnostics,
767) {
768    writeln!(out, "\u{2502} P(leak) = {:.1}%", leak_probability * 100.0).unwrap();
769    writeln!(
770        out,
771        "\u{2502} Effect  = {:.1}ns shift + {:.1}ns tail ({})",
772        effect.shift_ns, effect.tail_ns, effect.pattern
773    )
774    .unwrap();
775
776    let ess = diagnostics.effective_sample_size;
777    let efficiency = if samples_used > 0 {
778        libm::round(ess as f64 / samples_used as f64 * 100.0) as usize
779    } else {
780        0
781    };
782    writeln!(
783        out,
784        "\u{2502} Quality = {} (ESS: {} / {} raw, {}%)",
785        format_quality_colored(quality), ess, samples_used, efficiency
786    )
787    .unwrap();
788}
789
790fn format_debug_warnings(out: &mut String, diagnostics: &Diagnostics) {
791    if diagnostics.warnings.is_empty() && diagnostics.quality_issues.is_empty() {
792        return;
793    }
794
795    writeln!(out, "\u{2502}").unwrap();
796    writeln!(out, "\u{2502} {} Warnings:", yellow("\u{26A0}")).unwrap();
797
798    for warning in &diagnostics.warnings {
799        writeln!(out, "\u{2502}   \u{2022} {}", warning).unwrap();
800    }
801    for issue in &diagnostics.quality_issues {
802        writeln!(out, "\u{2502}   \u{2022} {:?}: {}", issue.code, issue.message).unwrap();
803    }
804}
805
806fn format_debug_diagnostics(out: &mut String, diagnostics: &Diagnostics) {
807    writeln!(out, "\u{2502}").unwrap();
808    writeln!(out, "\u{2502} Diagnostics:").unwrap();
809
810    // Format timer line with fallback reason if present
811    let timer_suffix = match &diagnostics.timer_fallback_reason {
812        Some(reason) => format!(" (fallback: {})", reason),
813        None => if diagnostics.discrete_mode { " (discrete)".to_string() } else { String::new() },
814    };
815    writeln!(
816        out,
817        "\u{2502}   Timer: {}{}",
818        diagnostics.timer_name,
819        timer_suffix
820    )
821    .unwrap();
822
823    let projection_status = if diagnostics.projection_mismatch_ok {
824        green("OK")
825    } else {
826        red("MISMATCH")
827    };
828    writeln!(
829        out,
830        "\u{2502}   Projection: Q = {:.1} ({})",
831        diagnostics.projection_mismatch_q, projection_status
832    )
833    .unwrap();
834
835    let stationarity_status = if diagnostics.stationarity_ok {
836        format!("{:.1}x", diagnostics.stationarity_ratio)
837    } else {
838        format!("{:.1}x {}", diagnostics.stationarity_ratio, red("DRIFT"))
839    };
840    writeln!(out, "\u{2502}   Stationarity: {}", stationarity_status).unwrap();
841
842    let outlier_note = if !diagnostics.outlier_asymmetry_ok {
843        format!(" ({})", red("asymmetric!"))
844    } else {
845        String::new()
846    };
847    writeln!(
848        out,
849        "\u{2502}   Outliers: {:.1}% / {:.1}%{}",
850        diagnostics.outlier_rate_baseline * 100.0,
851        diagnostics.outlier_rate_sample * 100.0,
852        outlier_note
853    )
854    .unwrap();
855
856    writeln!(out, "\u{2502}   Runtime: {:.1}s", diagnostics.total_time_secs).unwrap();
857}
858
859fn format_debug_research(out: &mut String, research: &ResearchOutcome) {
860    let status_str = match &research.status {
861        ResearchStatus::EffectDetected => green("Effect Detected"),
862        ResearchStatus::NoEffectDetected => green("No Effect Detected"),
863        ResearchStatus::ResolutionLimitReached => yellow("Resolution Limit"),
864        ResearchStatus::QualityIssue(_) => yellow("Quality Issue"),
865        ResearchStatus::BudgetExhausted => yellow("Budget Exhausted"),
866    };
867    writeln!(out, "\u{2502} Outcome = RESEARCH").unwrap();
868    writeln!(out, "\u{2502} Status = {}", status_str).unwrap();
869    writeln!(
870        out,
871        "\u{2502} Max Effect = {:.1}ns (CI: [{:.1}, {:.1}])",
872        research.max_effect_ns, research.max_effect_ci.0, research.max_effect_ci.1
873    )
874    .unwrap();
875    writeln!(
876        out,
877        "\u{2502} Floor = {:.1}ns, Detectable = {}",
878        research.theta_floor,
879        if research.detectable { "yes" } else { "no" }
880    )
881    .unwrap();
882    writeln!(
883        out,
884        "\u{2502} Effect = {:.1}ns shift + {:.1}ns tail ({})",
885        research.effect.shift_ns, research.effect.tail_ns, research.effect.pattern
886    )
887    .unwrap();
888
889    let ess = research.diagnostics.effective_sample_size;
890    let raw = research.samples_used;
891    let efficiency = if raw > 0 {
892        libm::round(ess as f64 / raw as f64 * 100.0) as usize
893    } else {
894        0
895    };
896    writeln!(
897        out,
898        "\u{2502} Quality = {} (ESS: {} / {} raw, {}%)",
899        format_quality_colored(research.quality), ess, raw, efficiency
900    )
901    .unwrap();
902
903    if research.model_mismatch {
904        writeln!(out, "\u{2502}").unwrap();
905        writeln!(out, "\u{2502} {} Model mismatch detected", yellow("\u{26A0}")).unwrap();
906    }
907
908    format_debug_warnings(out, &research.diagnostics);
909
910    writeln!(out, "\u{2502}").unwrap();
911    writeln!(out, "\u{2502} Diagnostics:").unwrap();
912    writeln!(
913        out,
914        "\u{2502}   Timer: {:.1}ns resolution",
915        research.diagnostics.timer_resolution_ns
916    )
917    .unwrap();
918    writeln!(
919        out,
920        "\u{2502}   Runtime: {:.1}s",
921        research.diagnostics.total_time_secs
922    )
923    .unwrap();
924}
925
926// ============================================================================
927// Utility functions
928// ============================================================================
929
930/// Get exploitability info as plain text.
931pub fn exploitability_info(exploit: Exploitability) -> (&'static str, &'static str) {
932    match exploit {
933        Exploitability::SharedHardwareOnly => ("Shared hardware (SGX, containers)", "~1k on same core"),
934        Exploitability::Http2Multiplexing => ("HTTP/2 multiplexing", "~100k concurrent"),
935        Exploitability::StandardRemote => ("Standard remote timing", "~1k-10k"),
936        Exploitability::ObviousLeak => ("Any (trivially observable)", "<100"),
937    }
938}
939
940/// Get exploitability info with colors.
941fn exploitability_info_colored(exploit: Exploitability) -> (String, String) {
942    match exploit {
943        Exploitability::SharedHardwareOnly => (
944            green("Shared hardware (SGX, containers)"),
945            green("~1k on same core"),
946        ),
947        Exploitability::Http2Multiplexing => (
948            yellow("HTTP/2 multiplexing"),
949            yellow("~100k concurrent"),
950        ),
951        Exploitability::StandardRemote => (
952            red("Standard remote timing"),
953            red("~1k-10k"),
954        ),
955        Exploitability::ObviousLeak => (
956            bold_red("Any (trivially observable)"),
957            bold_red("<100"),
958        ),
959    }
960}
961
962fn format_severity(severity: PreflightSeverity) -> &'static str {
963    match severity {
964        PreflightSeverity::ResultUndermining => "WARNING",
965        PreflightSeverity::Informational => "INFO",
966    }
967}
968
969/// Wrap text to fit within a given width.
970fn wrap_text(text: &str, width: usize, first_line_used: usize, cont_indent: &str) -> String {
971    let first_available = width.saturating_sub(first_line_used);
972    let cont_available = width.saturating_sub(cont_indent.len());
973
974    if first_available == 0 || cont_available == 0 {
975        return text.to_string();
976    }
977
978    let words: Vec<&str> = text.split_whitespace().collect();
979    if words.is_empty() {
980        return String::new();
981    }
982
983    let mut lines = Vec::new();
984    let mut current_line = String::new();
985    let mut is_first_line = true;
986
987    for word in words {
988        let available = if is_first_line {
989            first_available
990        } else {
991            cont_available
992        };
993
994        if current_line.is_empty() {
995            current_line = word.to_string();
996        } else if current_line.len() + 1 + word.len() <= available {
997            current_line.push(' ');
998            current_line.push_str(word);
999        } else {
1000            lines.push((is_first_line, current_line));
1001            is_first_line = false;
1002            current_line = word.to_string();
1003        }
1004    }
1005
1006    if !current_line.is_empty() {
1007        lines.push((is_first_line, current_line));
1008    }
1009
1010    lines
1011        .into_iter()
1012        .map(|(is_first, line)| {
1013            if is_first {
1014                line
1015            } else {
1016                format!("{}{}", cont_indent, line)
1017            }
1018        })
1019        .collect::<Vec<_>>()
1020        .join("\n")
1021}