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