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    let magnitude = effect.total_effect_ns();
273    writeln!(out, "    Effect: {:.1} ns ({})", magnitude, effect.pattern).unwrap();
274    writeln!(out, "      Shift: {:.1} ns", effect.shift_ns).unwrap();
275    writeln!(out, "      Tail:  {:.1} ns", effect.tail_ns).unwrap();
276    writeln!(
277        out,
278        "      95% CI: {:.1}\u{2013}{:.1} ns",
279        effect.credible_interval_ns.0, effect.credible_interval_ns.1
280    )
281    .unwrap();
282
283    writeln!(out).unwrap();
284    writeln!(out, "    Exploitability (heuristic):").unwrap();
285    let (vector, queries) = exploitability_info_colored(exploitability);
286    writeln!(out, "      Attack vector:  {}", vector).unwrap();
287    writeln!(out, "      Queries needed: {}", queries).unwrap();
288}
289
290fn format_inconclusive_body(out: &mut String, leak_probability: f64, effect: &EffectEstimate) {
291    writeln!(
292        out,
293        "    Current probability of leak: {:.1}%",
294        leak_probability * 100.0
295    )
296    .unwrap();
297
298    let magnitude = effect.total_effect_ns();
299    writeln!(
300        out,
301        "    Effect estimate: {:.1} ns ({})",
302        magnitude, effect.pattern
303    )
304    .unwrap();
305}
306
307fn format_research_outcome(out: &mut String, research: &ResearchOutcome) {
308    writeln!(out, "  Samples: {} per class", research.samples_used).unwrap();
309    writeln!(
310        out,
311        "  Quality: {}",
312        format_quality_colored(research.quality)
313    )
314    .unwrap();
315    writeln!(out).unwrap();
316
317    let status_line = match &research.status {
318        ResearchStatus::EffectDetected => bold_green("\u{1F50D} Effect Detected"),
319        ResearchStatus::NoEffectDetected => bold_cyan("\u{2713} No Effect Detected"),
320        ResearchStatus::ResolutionLimitReached => bold_yellow("\u{26A0} Resolution Limit Reached"),
321        ResearchStatus::QualityIssue(_) => bold_yellow("? Quality Issue"),
322        ResearchStatus::BudgetExhausted => bold_cyan("? Budget Exhausted"),
323    };
324    writeln!(out, "  {}", status_line).unwrap();
325    writeln!(out).unwrap();
326
327    writeln!(out, "    Max effect: {:.1} ns", research.max_effect_ns).unwrap();
328    writeln!(
329        out,
330        "    95% CI: [{:.1}, {:.1}] ns",
331        research.max_effect_ci.0, research.max_effect_ci.1
332    )
333    .unwrap();
334    writeln!(out, "    Measurement floor: {:.1} ns", research.theta_floor).unwrap();
335    writeln!(
336        out,
337        "    Detectable: {}",
338        if research.detectable { "yes" } else { "no" }
339    )
340    .unwrap();
341
342    writeln!(out).unwrap();
343    writeln!(
344        out,
345        "    Effect: {:.1} ns ({})",
346        research.effect.total_effect_ns(),
347        research.effect.pattern
348    )
349    .unwrap();
350    writeln!(out, "      Shift: {:.1} ns", research.effect.shift_ns).unwrap();
351    writeln!(out, "      Tail:  {:.1} ns", research.effect.tail_ns).unwrap();
352
353    if research.model_mismatch {
354        writeln!(out).unwrap();
355        writeln!(
356            out,
357            "    {} Model mismatch detected \u{2013} interpret with caution",
358            yellow("\u{26A0}")
359        )
360        .unwrap();
361        if let Some(ref caveat) = research.effect.interpretation_caveat {
362            writeln!(out, "      {}", caveat).unwrap();
363        }
364    }
365
366    // Include diagnostics
367    format_diagnostics_section(out, &research.diagnostics);
368    format_reproduction_line(out, &research.diagnostics);
369    format_debug_environment(out, &research.diagnostics);
370}
371
372// ============================================================================
373// Diagnostics formatting
374// ============================================================================
375
376/// Format the detailed diagnostics section.
377pub fn format_diagnostics_section(out: &mut String, diagnostics: &Diagnostics) {
378    writeln!(out).unwrap();
379    writeln!(out, "{}", SEPARATOR).unwrap();
380    writeln!(out).unwrap();
381    writeln!(out, "  Measurement Diagnostics").unwrap();
382    writeln!(out).unwrap();
383
384    writeln!(
385        out,
386        "    Dependence:   block length {} (ESS: {} / {} raw)",
387        diagnostics.dependence_length,
388        diagnostics.effective_sample_size,
389        diagnostics.calibration_samples
390    )
391    .unwrap();
392
393    let projection_status = if diagnostics.projection_mismatch_ok {
394        green("OK")
395    } else {
396        red("Mismatch")
397    };
398    writeln!(
399        out,
400        "    Projection:   Q = {:.1} ({})",
401        diagnostics.projection_mismatch_q, projection_status
402    )
403    .unwrap();
404
405    let mut outlier_line = format!(
406        "    Outliers:     baseline {:.2}%, sample {:.2}%",
407        diagnostics.outlier_rate_baseline * 100.0,
408        diagnostics.outlier_rate_sample * 100.0
409    );
410    if !diagnostics.outlier_asymmetry_ok {
411        outlier_line.push_str(&format!(" ({})", red("asymmetric")));
412    }
413    writeln!(out, "{}", outlier_line).unwrap();
414
415    writeln!(
416        out,
417        "    Calibration:  {} samples",
418        diagnostics.calibration_samples
419    )
420    .unwrap();
421
422    writeln!(out, "    Runtime:      {:.1}s", diagnostics.total_time_secs).unwrap();
423
424    // Warnings
425    if !diagnostics.warnings.is_empty() {
426        writeln!(out).unwrap();
427        writeln!(out, "  {} Warnings", yellow("\u{26A0}")).unwrap();
428        for warning in &diagnostics.warnings {
429            writeln!(out, "    \u{2022} {}", warning).unwrap();
430        }
431    }
432
433    // Quality issues with guidance
434    if !diagnostics.quality_issues.is_empty() {
435        writeln!(out).unwrap();
436        writeln!(out, "  {} Quality Issues", yellow("\u{26A0}")).unwrap();
437        for issue in &diagnostics.quality_issues {
438            let wrapped_msg = wrap_text(
439                &issue.message,
440                DEFAULT_WRAP_WIDTH,
441                20,
442                "                    ",
443            );
444            writeln!(
445                out,
446                "    \u{2022} {}: {}",
447                bold(&format!("{:?}", issue.code)),
448                wrapped_msg
449            )
450            .unwrap();
451            let wrapped_guidance = wrap_text(&issue.guidance, DEFAULT_WRAP_WIDTH, 8, "        ");
452            writeln!(out, "      \u{2192} {}", dim(&wrapped_guidance)).unwrap();
453        }
454    }
455}
456
457/// Format reproduction line for output.
458pub fn format_reproduction_line(out: &mut String, diagnostics: &Diagnostics) {
459    let mut parts = Vec::new();
460
461    if let Some(ref model) = diagnostics.attacker_model {
462        parts.push(format!("model={}", model));
463    }
464    if diagnostics.threshold_ns > 0.0 {
465        parts.push(format!("\u{03B8}={:.0}ns", diagnostics.threshold_ns));
466    }
467    if !diagnostics.timer_name.is_empty() {
468        parts.push(format!("timer={}", diagnostics.timer_name));
469    }
470
471    if !parts.is_empty() {
472        writeln!(out).unwrap();
473        writeln!(out, "  Reproduce: {}", parts.join(", ")).unwrap();
474    }
475}
476
477/// Format extended debug environment information.
478pub fn format_debug_environment(out: &mut String, diagnostics: &Diagnostics) {
479    writeln!(out).unwrap();
480    writeln!(out, "{}", SEPARATOR).unwrap();
481    writeln!(out).unwrap();
482    writeln!(out, "  Debug Information").unwrap();
483    writeln!(out).unwrap();
484
485    // Environment
486    writeln!(out, "    Environment:").unwrap();
487    let platform = if diagnostics.platform.is_empty() {
488        #[cfg(feature = "std")]
489        {
490            format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH)
491        }
492        #[cfg(not(feature = "std"))]
493        {
494            "unknown".to_string()
495        }
496    } else {
497        diagnostics.platform.clone()
498    };
499    writeln!(out, "      Platform:       {}", platform).unwrap();
500    writeln!(out, "      Rust version:   1.80").unwrap();
501    writeln!(out, "      Package:        tacet v0.1.5").unwrap();
502
503    // Configuration
504    writeln!(out).unwrap();
505    writeln!(out, "    Configuration:").unwrap();
506    if let Some(ref model) = diagnostics.attacker_model {
507        writeln!(out, "      Attacker model: {}", model).unwrap();
508    }
509    writeln!(
510        out,
511        "      Threshold (\u{03B8}):  {:.1} ns",
512        diagnostics.threshold_ns
513    )
514    .unwrap();
515    if !diagnostics.timer_name.is_empty() {
516        // Format timer line with fallback reason if present
517        let timer_display = match &diagnostics.timer_fallback_reason {
518            Some(reason) => format!("{} (fallback: {})", diagnostics.timer_name, reason),
519            None => diagnostics.timer_name.clone(),
520        };
521        writeln!(out, "      Timer:          {}", timer_display).unwrap();
522    }
523    writeln!(
524        out,
525        "      Resolution:     {:.1} ns",
526        diagnostics.timer_resolution_ns
527    )
528    .unwrap();
529    if diagnostics.discrete_mode {
530        writeln!(out, "      Discrete mode:  enabled").unwrap();
531    }
532
533    // Statistical summary
534    writeln!(out).unwrap();
535    writeln!(out, "    Statistical Summary:").unwrap();
536    writeln!(
537        out,
538        "      Calibration:    {} samples",
539        diagnostics.calibration_samples
540    )
541    .unwrap();
542    writeln!(
543        out,
544        "      Block length:   {}",
545        diagnostics.dependence_length
546    )
547    .unwrap();
548    writeln!(
549        out,
550        "      ESS:            {}",
551        diagnostics.effective_sample_size
552    )
553    .unwrap();
554    writeln!(
555        out,
556        "      Stationarity:   {:.2}x {}",
557        diagnostics.stationarity_ratio,
558        if diagnostics.stationarity_ok {
559            "OK"
560        } else {
561            "Suspect"
562        }
563    )
564    .unwrap();
565    writeln!(
566        out,
567        "      Projection:     Q={:.1} {}",
568        diagnostics.projection_mismatch_q,
569        if diagnostics.projection_mismatch_ok {
570            "OK"
571        } else {
572            "Mismatch"
573        }
574    )
575    .unwrap();
576
577    writeln!(out).unwrap();
578    writeln!(
579        out,
580        "    For bug reports, re-run with TIMING_ORACLE_DEBUG=1 and include both outputs"
581    )
582    .unwrap();
583}
584
585/// Format preflight validation section for verbose output.
586pub fn format_preflight_validation(out: &mut String, diagnostics: &Diagnostics) {
587    writeln!(out).unwrap();
588    writeln!(out, "{}", SEPARATOR).unwrap();
589    writeln!(out).unwrap();
590    writeln!(out, "  Preflight Checks").unwrap();
591
592    // Group warnings by category
593    let sanity: Vec<_> = diagnostics
594        .preflight_warnings
595        .iter()
596        .filter(|w| w.category == PreflightCategory::Sanity)
597        .collect();
598    let timer_sanity: Vec<_> = diagnostics
599        .preflight_warnings
600        .iter()
601        .filter(|w| w.category == PreflightCategory::TimerSanity)
602        .collect();
603    let autocorr: Vec<_> = diagnostics
604        .preflight_warnings
605        .iter()
606        .filter(|w| w.category == PreflightCategory::Autocorrelation)
607        .collect();
608    let system: Vec<_> = diagnostics
609        .preflight_warnings
610        .iter()
611        .filter(|w| w.category == PreflightCategory::System)
612        .collect();
613    let resolution: Vec<_> = diagnostics
614        .preflight_warnings
615        .iter()
616        .filter(|w| w.category == PreflightCategory::Resolution)
617        .collect();
618
619    // Result Integrity section
620    writeln!(out).unwrap();
621    writeln!(out, "    Result Integrity:").unwrap();
622
623    if sanity.is_empty() {
624        writeln!(out, "      Sanity (F-vs-F):    OK").unwrap();
625    } else {
626        for w in &sanity {
627            writeln!(
628                out,
629                "      Sanity (F-vs-F):    {}",
630                format_severity(w.severity)
631            )
632            .unwrap();
633            writeln!(out, "        {}", w.message).unwrap();
634        }
635    }
636
637    if timer_sanity.is_empty() {
638        writeln!(out, "      Timer monotonic:    OK").unwrap();
639    } else {
640        for w in &timer_sanity {
641            writeln!(
642                out,
643                "      Timer monotonic:    {}",
644                format_severity(w.severity)
645            )
646            .unwrap();
647            writeln!(out, "        {}", w.message).unwrap();
648        }
649    }
650
651    let stationarity_status = if diagnostics.stationarity_ok {
652        format!("OK {:.2}x", diagnostics.stationarity_ratio)
653    } else {
654        format!("Suspect {:.2}x", diagnostics.stationarity_ratio)
655    };
656    writeln!(out, "      Stationarity:       {}", stationarity_status).unwrap();
657
658    // Sampling Efficiency section
659    writeln!(out).unwrap();
660    writeln!(out, "    Sampling Efficiency:").unwrap();
661
662    if autocorr.is_empty() {
663        writeln!(out, "      Autocorrelation:    OK").unwrap();
664    } else {
665        for w in &autocorr {
666            writeln!(
667                out,
668                "      Autocorrelation:    {}",
669                format_severity(w.severity)
670            )
671            .unwrap();
672            writeln!(out, "        {}", w.message).unwrap();
673        }
674    }
675
676    let timer_name = if diagnostics.timer_name.is_empty() {
677        String::new()
678    } else {
679        format!(" ({})", diagnostics.timer_name)
680    };
681    if resolution.is_empty() {
682        writeln!(
683            out,
684            "      Timer resolution:   OK {:.1}ns{}",
685            diagnostics.timer_resolution_ns, timer_name
686        )
687        .unwrap();
688    } else {
689        for w in &resolution {
690            writeln!(
691                out,
692                "      Timer resolution:   {} {:.1}ns{}",
693                format_severity(w.severity),
694                diagnostics.timer_resolution_ns,
695                timer_name
696            )
697            .unwrap();
698            writeln!(out, "        {}", w.message).unwrap();
699        }
700    }
701
702    // System Configuration section
703    writeln!(out).unwrap();
704    writeln!(out, "    System:").unwrap();
705    if system.is_empty() {
706        writeln!(out, "      Configuration:      OK").unwrap();
707    } else {
708        for w in &system {
709            writeln!(out, "      \u{26A0} {}", w.message).unwrap();
710            if let Some(guidance) = &w.guidance {
711                writeln!(out, "        {}", guidance).unwrap();
712            }
713        }
714    }
715}
716
717/// Format measurement notes for non-verbose output.
718fn format_preflight_notes(out: &mut String, diagnostics: &Diagnostics) {
719    if diagnostics.preflight_warnings.is_empty() {
720        return;
721    }
722
723    let has_critical = diagnostics
724        .preflight_warnings
725        .iter()
726        .any(|w| w.severity == PreflightSeverity::ResultUndermining);
727
728    writeln!(out).unwrap();
729    if has_critical {
730        writeln!(out, "  \u{26A0} Measurement Notes:").unwrap();
731    } else {
732        writeln!(out, "  Measurement Notes:").unwrap();
733    }
734
735    for warning in &diagnostics.preflight_warnings {
736        let bullet = match warning.severity {
737            PreflightSeverity::ResultUndermining => "\u{2022}",
738            PreflightSeverity::Informational => "\u{2022}",
739        };
740        writeln!(out, "    {} {}", bullet, warning.message).unwrap();
741
742        // For resolution warnings, provide context-aware guidance based on fallback reason
743        let guidance = if warning.category == PreflightCategory::Resolution {
744            resolution_guidance_for_fallback(diagnostics.timer_fallback_reason.as_deref())
745        } else {
746            warning.guidance.clone()
747        };
748
749        if let Some(g) = guidance {
750            writeln!(out, "      {}", g).unwrap();
751        }
752    }
753}
754
755/// Generate context-aware guidance for resolution warnings based on timer fallback reason.
756fn resolution_guidance_for_fallback(fallback_reason: Option<&str>) -> Option<String> {
757    match fallback_reason {
758        Some("concurrent access") => Some(
759            "Cycle-accurate timing is locked by another process. \
760             If using cargo test, run with --test-threads=1."
761                .into(),
762        ),
763        Some("no sudo") => {
764            Some("Run with sudo to enable cycle-accurate timing (~0.3ns resolution).".into())
765        }
766        Some("unavailable") => Some(
767            "Cycle-accurate timing unavailable. Consider increasing max_samples \
768             or testing at a higher abstraction level."
769                .into(),
770        ),
771        Some("user requested") => Some(
772            "System timer was explicitly requested. For better resolution, \
773             use TimerSpec::Auto or TimerSpec::RequireCycleAccurate."
774                .into(),
775        ),
776        // x86_64 or already using cycle-accurate timer
777        None => Some(
778            "Timer resolution may be limiting measurement quality. \
779             Consider increasing max_samples or time_budget."
780                .into(),
781        ),
782        // Unknown fallback reason
783        Some(_) => Some("Timer resolution may be limiting measurement quality.".into()),
784    }
785}
786
787/// Format "Why This May Have Happened" section for Inconclusive outcomes.
788fn format_inconclusive_diagnostics(out: &mut String, diagnostics: &Diagnostics) {
789    let system_config: Vec<_> = diagnostics
790        .preflight_warnings
791        .iter()
792        .filter(|w| w.category == PreflightCategory::System)
793        .collect();
794    let resolution: Vec<_> = diagnostics
795        .preflight_warnings
796        .iter()
797        .filter(|w| w.category == PreflightCategory::Resolution)
798        .collect();
799
800    if system_config.is_empty() && resolution.is_empty() {
801        return;
802    }
803
804    writeln!(out).unwrap();
805    writeln!(out, "  \u{2139} Why This May Have Happened:").unwrap();
806
807    if !system_config.is_empty() {
808        writeln!(out).unwrap();
809        writeln!(out, "    System Configuration:").unwrap();
810        for warning in system_config {
811            writeln!(out, "      \u{2022} {}", warning.message).unwrap();
812        }
813    }
814
815    if !resolution.is_empty() {
816        writeln!(out).unwrap();
817        writeln!(out, "    Timer Resolution:").unwrap();
818        for warning in resolution {
819            writeln!(out, "      \u{2022} {}", warning.message).unwrap();
820            if let Some(guidance) = &warning.guidance {
821                writeln!(out, "      \u{2192} Tip: {}", guidance).unwrap();
822            }
823        }
824    }
825}
826
827// ============================================================================
828// Debug summary helpers
829// ============================================================================
830
831fn format_debug_core_metrics(
832    out: &mut String,
833    leak_probability: f64,
834    effect: &EffectEstimate,
835    quality: MeasurementQuality,
836    samples_used: usize,
837    diagnostics: &Diagnostics,
838) {
839    writeln!(out, "\u{2502} P(leak) = {:.1}%", leak_probability * 100.0).unwrap();
840    writeln!(
841        out,
842        "\u{2502} Effect  = {:.1}ns shift + {:.1}ns tail ({})",
843        effect.shift_ns, effect.tail_ns, effect.pattern
844    )
845    .unwrap();
846
847    let ess = diagnostics.effective_sample_size;
848    let efficiency = if samples_used > 0 {
849        libm::round(ess as f64 / samples_used as f64 * 100.0) as usize
850    } else {
851        0
852    };
853    writeln!(
854        out,
855        "\u{2502} Quality = {} (ESS: {} / {} raw, {}%)",
856        format_quality_colored(quality),
857        ess,
858        samples_used,
859        efficiency
860    )
861    .unwrap();
862}
863
864fn format_debug_warnings(out: &mut String, diagnostics: &Diagnostics) {
865    if diagnostics.warnings.is_empty() && diagnostics.quality_issues.is_empty() {
866        return;
867    }
868
869    writeln!(out, "\u{2502}").unwrap();
870    writeln!(out, "\u{2502} {} Warnings:", yellow("\u{26A0}")).unwrap();
871
872    for warning in &diagnostics.warnings {
873        writeln!(out, "\u{2502}   \u{2022} {}", warning).unwrap();
874    }
875    for issue in &diagnostics.quality_issues {
876        writeln!(
877            out,
878            "\u{2502}   \u{2022} {:?}: {}",
879            issue.code, issue.message
880        )
881        .unwrap();
882    }
883}
884
885fn format_debug_diagnostics(out: &mut String, diagnostics: &Diagnostics) {
886    writeln!(out, "\u{2502}").unwrap();
887    writeln!(out, "\u{2502} Diagnostics:").unwrap();
888
889    // Format timer line with fallback reason if present
890    let timer_suffix = match &diagnostics.timer_fallback_reason {
891        Some(reason) => format!(" (fallback: {})", reason),
892        None => {
893            if diagnostics.discrete_mode {
894                " (discrete)".to_string()
895            } else {
896                String::new()
897            }
898        }
899    };
900    writeln!(
901        out,
902        "\u{2502}   Timer: {}{}",
903        diagnostics.timer_name, timer_suffix
904    )
905    .unwrap();
906
907    let projection_status = if diagnostics.projection_mismatch_ok {
908        green("OK")
909    } else {
910        red("MISMATCH")
911    };
912    writeln!(
913        out,
914        "\u{2502}   Projection: Q = {:.1} ({})",
915        diagnostics.projection_mismatch_q, projection_status
916    )
917    .unwrap();
918
919    let stationarity_status = if diagnostics.stationarity_ok {
920        format!("{:.1}x", diagnostics.stationarity_ratio)
921    } else {
922        format!("{:.1}x {}", diagnostics.stationarity_ratio, red("DRIFT"))
923    };
924    writeln!(out, "\u{2502}   Stationarity: {}", stationarity_status).unwrap();
925
926    let outlier_note = if !diagnostics.outlier_asymmetry_ok {
927        format!(" ({})", red("asymmetric!"))
928    } else {
929        String::new()
930    };
931    writeln!(
932        out,
933        "\u{2502}   Outliers: {:.1}% / {:.1}%{}",
934        diagnostics.outlier_rate_baseline * 100.0,
935        diagnostics.outlier_rate_sample * 100.0,
936        outlier_note
937    )
938    .unwrap();
939
940    writeln!(
941        out,
942        "\u{2502}   Runtime: {:.1}s",
943        diagnostics.total_time_secs
944    )
945    .unwrap();
946}
947
948fn format_debug_research(out: &mut String, research: &ResearchOutcome) {
949    let status_str = match &research.status {
950        ResearchStatus::EffectDetected => green("Effect Detected"),
951        ResearchStatus::NoEffectDetected => green("No Effect Detected"),
952        ResearchStatus::ResolutionLimitReached => yellow("Resolution Limit"),
953        ResearchStatus::QualityIssue(_) => yellow("Quality Issue"),
954        ResearchStatus::BudgetExhausted => yellow("Budget Exhausted"),
955    };
956    writeln!(out, "\u{2502} Outcome = RESEARCH").unwrap();
957    writeln!(out, "\u{2502} Status = {}", status_str).unwrap();
958    writeln!(
959        out,
960        "\u{2502} Max Effect = {:.1}ns (CI: [{:.1}, {:.1}])",
961        research.max_effect_ns, research.max_effect_ci.0, research.max_effect_ci.1
962    )
963    .unwrap();
964    writeln!(
965        out,
966        "\u{2502} Floor = {:.1}ns, Detectable = {}",
967        research.theta_floor,
968        if research.detectable { "yes" } else { "no" }
969    )
970    .unwrap();
971    writeln!(
972        out,
973        "\u{2502} Effect = {:.1}ns shift + {:.1}ns tail ({})",
974        research.effect.shift_ns, research.effect.tail_ns, research.effect.pattern
975    )
976    .unwrap();
977
978    let ess = research.diagnostics.effective_sample_size;
979    let raw = research.samples_used;
980    let efficiency = if raw > 0 {
981        libm::round(ess as f64 / raw as f64 * 100.0) as usize
982    } else {
983        0
984    };
985    writeln!(
986        out,
987        "\u{2502} Quality = {} (ESS: {} / {} raw, {}%)",
988        format_quality_colored(research.quality),
989        ess,
990        raw,
991        efficiency
992    )
993    .unwrap();
994
995    if research.model_mismatch {
996        writeln!(out, "\u{2502}").unwrap();
997        writeln!(
998            out,
999            "\u{2502} {} Model mismatch detected",
1000            yellow("\u{26A0}")
1001        )
1002        .unwrap();
1003    }
1004
1005    format_debug_warnings(out, &research.diagnostics);
1006
1007    writeln!(out, "\u{2502}").unwrap();
1008    writeln!(out, "\u{2502} Diagnostics:").unwrap();
1009    writeln!(
1010        out,
1011        "\u{2502}   Timer: {:.1}ns resolution",
1012        research.diagnostics.timer_resolution_ns
1013    )
1014    .unwrap();
1015    writeln!(
1016        out,
1017        "\u{2502}   Runtime: {:.1}s",
1018        research.diagnostics.total_time_secs
1019    )
1020    .unwrap();
1021}
1022
1023// ============================================================================
1024// Utility functions
1025// ============================================================================
1026
1027/// Get exploitability info as plain text.
1028pub fn exploitability_info(exploit: Exploitability) -> (&'static str, &'static str) {
1029    match exploit {
1030        Exploitability::SharedHardwareOnly => {
1031            ("Shared hardware (SGX, containers)", "~1k on same core")
1032        }
1033        Exploitability::Http2Multiplexing => ("HTTP/2 multiplexing", "~100k concurrent"),
1034        Exploitability::StandardRemote => ("Standard remote timing", "~1k-10k"),
1035        Exploitability::ObviousLeak => ("Any (trivially observable)", "<100"),
1036    }
1037}
1038
1039/// Get exploitability info with colors.
1040fn exploitability_info_colored(exploit: Exploitability) -> (String, String) {
1041    match exploit {
1042        Exploitability::SharedHardwareOnly => (
1043            green("Shared hardware (SGX, containers)"),
1044            green("~1k on same core"),
1045        ),
1046        Exploitability::Http2Multiplexing => {
1047            (yellow("HTTP/2 multiplexing"), yellow("~100k concurrent"))
1048        }
1049        Exploitability::StandardRemote => (red("Standard remote timing"), red("~1k-10k")),
1050        Exploitability::ObviousLeak => (bold_red("Any (trivially observable)"), bold_red("<100")),
1051    }
1052}
1053
1054fn format_severity(severity: PreflightSeverity) -> &'static str {
1055    match severity {
1056        PreflightSeverity::ResultUndermining => "WARNING",
1057        PreflightSeverity::Informational => "INFO",
1058    }
1059}
1060
1061/// Wrap text to fit within a given width.
1062fn wrap_text(text: &str, width: usize, first_line_used: usize, cont_indent: &str) -> String {
1063    let first_available = width.saturating_sub(first_line_used);
1064    let cont_available = width.saturating_sub(cont_indent.len());
1065
1066    if first_available == 0 || cont_available == 0 {
1067        return text.to_string();
1068    }
1069
1070    let words: Vec<&str> = text.split_whitespace().collect();
1071    if words.is_empty() {
1072        return String::new();
1073    }
1074
1075    let mut lines = Vec::new();
1076    let mut current_line = String::new();
1077    let mut is_first_line = true;
1078
1079    for word in words {
1080        let available = if is_first_line {
1081            first_available
1082        } else {
1083            cont_available
1084        };
1085
1086        if current_line.is_empty() {
1087            current_line = word.to_string();
1088        } else if current_line.len() + 1 + word.len() <= available {
1089            current_line.push(' ');
1090            current_line.push_str(word);
1091        } else {
1092            lines.push((is_first_line, current_line));
1093            is_first_line = false;
1094            current_line = word.to_string();
1095        }
1096    }
1097
1098    if !current_line.is_empty() {
1099        lines.push((is_first_line, current_line));
1100    }
1101
1102    lines
1103        .into_iter()
1104        .map(|(is_first, line)| {
1105            if is_first {
1106                line
1107            } else {
1108                format!("{}{}", cont_indent, line)
1109            }
1110        })
1111        .collect::<Vec<_>>()
1112        .join("\n")
1113}