diff --git a/src/main.rs b/src/main.rs index b4009e8..b31e0f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -468,8 +468,11 @@ enum Commands { headless: bool, }, - /// GUI review of a saved report (requires `--features gui` at build time) - #[cfg(feature = "gui")] + /// GUI review of a saved report. + /// + /// `--headless` always works (text panel summaries to stdout). The + /// windowed renderer requires `--features gui` at build time because + /// eframe/egui raise MSRV above 1.85.0. Gui { /// Assault report JSON file #[arg(value_name = "REPORT")] @@ -1701,14 +1704,22 @@ fn run_main() -> Result<()> { } } - #[cfg(feature = "gui")] Commands::Gui { report, headless } => { let content = read_report_bounded(&report)?; let assault_report: AssaultReport = serde_json::from_str(&content)?; if headless { - report::ReportGui::run_headless(assault_report)?; + report::gui_text::run_headless(assault_report)?; } else { - report::ReportGui::run(assault_report)?; + #[cfg(feature = "gui")] + { + report::ReportGui::run(assault_report)?; + } + #[cfg(not(feature = "gui"))] + { + anyhow::bail!( + "windowed GUI requires the `gui` feature; rebuild with `cargo build --features gui`, or pass --headless" + ); + } } } diff --git a/src/report/gui.rs b/src/report/gui.rs index b473adb..5036c30 100644 --- a/src/report/gui.rs +++ b/src/report/gui.rs @@ -1,6 +1,11 @@ // SPDX-License-Identifier: MPL-2.0 //! Minimal GUI for reviewing assault reports, system images, and temporal diffs. +//! +//! This module is compiled only when the `gui` feature is enabled because the +//! `eframe`/`egui` dependency chain raises MSRV above the 1.85.0 baseline. +//! The text-only `gui --headless` path lives in [`super::gui_text`] and is +//! always compiled. use crate::mass_panic::imaging::SystemImage; use crate::mass_panic::temporal::TemporalDiff; @@ -65,99 +70,6 @@ impl ReportGui { Ok(()) } - /// Run without a display server: print a structured text summary of all panels. - /// - /// Safe to call in CI or headless environments with no Wayland/X11 display. - /// Promotes the gui subcommand from Grade E to Grade D. - pub fn run_headless(report: AssaultReport) -> Result<()> { - let assail = &report.assail_report; - let formatter = ReportFormatter::new(); - - println!("PANIC-ATTACK GUI REPORT (headless)"); - println!(); - - println!("=== Summary ==="); - println!("Language: {:?}", assail.language); - println!( - "Score: {:.1}/100", - report.overall_assessment.robustness_score - ); - println!("Crashes: {}", report.total_crashes); - println!("Signatures: {}", report.total_signatures); - println!("Weak points: {}", assail.weak_points.len()); - println!("Frameworks: {:?}", assail.frameworks); - println!(); - - println!("=== Assail ==="); - println!("Program: {}", assail.program_path.display()); - println!( - "Stats: lines={} unsafe={} panics={} unwraps={}", - assail.statistics.total_lines, - assail.statistics.unsafe_blocks, - assail.statistics.panic_sites, - assail.statistics.unwrap_calls - ); - for wp in &assail.weak_points { - let loc = wp - .location - .as_deref() - .or(wp.file.as_deref()) - .unwrap_or(""); - println!( - " [{:?}] {:?} @ {} — {}", - wp.severity, wp.category, loc, wp.description - ); - } - println!(); - - println!("=== File Risk ==="); - for detail in formatter.file_risk_details(assail) { - println!(" {}", detail); - } - println!(); - - println!("=== Matrix ==="); - println!( - "Dependency edges: {} Taint rows: {}", - assail.dependency_graph.edges.len(), - assail.taint_matrix.rows.len() - ); - for detail in formatter.taint_matrix_details(assail) { - println!(" {}", detail); - } - println!(); - - println!("=== Attacks ==="); - for result in &report.attack_results { - let status = if result.skipped { - "skipped" - } else if result.success { - "passed" - } else { - "failed" - }; - println!( - " {:?}: {} crashes={} duration={:.2}s", - result.axis, - status, - result.crashes.len(), - result.duration.as_secs_f64() - ); - } - println!(); - - println!("=== Assessment ==="); - for issue in &report.overall_assessment.critical_issues { - println!(" CRITICAL: {}", issue); - } - for rec in &report.overall_assessment.recommendations { - println!(" REC: {}", rec); - } - println!(); - - Ok(()) - } - /// Attempt to load a JSON file as either an `AssaultReport`, `SystemImage`, /// or `TemporalDiff`. Detection is format-based: we try each in turn and /// keep the first successful parse. diff --git a/src/report/gui_text.rs b/src/report/gui_text.rs new file mode 100644 index 0000000..3c0e2cb --- /dev/null +++ b/src/report/gui_text.rs @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MPL-2.0 + +//! Headless GUI report renderer — text-only output, no display server. +//! +//! This is the `gui --headless` path. It is always compiled (independent of the +//! `gui` feature flag) so the readiness test `readiness_d_gui_headless_runs` +//! and CI integrations keep working in MSRV-clean default builds. The +//! windowed renderer in [`crate::report::gui`] is feature-gated because eframe +//! raises MSRV above 1.85.0. + +use crate::report::formatter::ReportFormatter; +use crate::types::AssaultReport; +use anyhow::Result; + +/// Print a structured text summary of every GUI panel. +/// +/// Safe to call in CI or headless environments with no Wayland/X11 display. +/// Promotes the `gui` subcommand from Grade E to Grade D. +pub fn run_headless(report: AssaultReport) -> Result<()> { + let assail = &report.assail_report; + let formatter = ReportFormatter::new(); + + println!("PANIC-ATTACK GUI REPORT (headless)"); + println!(); + + println!("=== Summary ==="); + println!("Language: {:?}", assail.language); + println!( + "Score: {:.1}/100", + report.overall_assessment.robustness_score + ); + println!("Crashes: {}", report.total_crashes); + println!("Signatures: {}", report.total_signatures); + println!("Weak points: {}", assail.weak_points.len()); + println!("Frameworks: {:?}", assail.frameworks); + println!(); + + println!("=== Assail ==="); + println!("Program: {}", assail.program_path.display()); + println!( + "Stats: lines={} unsafe={} panics={} unwraps={}", + assail.statistics.total_lines, + assail.statistics.unsafe_blocks, + assail.statistics.panic_sites, + assail.statistics.unwrap_calls + ); + for wp in &assail.weak_points { + let loc = wp + .location + .as_deref() + .or(wp.file.as_deref()) + .unwrap_or(""); + println!( + " [{:?}] {:?} @ {} — {}", + wp.severity, wp.category, loc, wp.description + ); + } + println!(); + + println!("=== File Risk ==="); + for detail in formatter.file_risk_details(assail) { + println!(" {}", detail); + } + println!(); + + println!("=== Matrix ==="); + println!( + "Dependency edges: {} Taint rows: {}", + assail.dependency_graph.edges.len(), + assail.taint_matrix.rows.len() + ); + for detail in formatter.taint_matrix_details(assail) { + println!(" {}", detail); + } + println!(); + + println!("=== Attacks ==="); + for result in &report.attack_results { + let status = if result.skipped { + "skipped" + } else if result.success { + "passed" + } else { + "failed" + }; + println!( + " {:?}: {} crashes={} duration={:.2}s", + result.axis, + status, + result.crashes.len(), + result.duration.as_secs_f64() + ); + } + println!(); + + println!("=== Assessment ==="); + for issue in &report.overall_assessment.critical_issues { + println!(" CRITICAL: {}", issue); + } + for rec in &report.overall_assessment.recommendations { + println!(" REC: {}", rec); + } + println!(); + + Ok(()) +} diff --git a/src/report/mod.rs b/src/report/mod.rs index d933d4a..585d21b 100644 --- a/src/report/mod.rs +++ b/src/report/mod.rs @@ -7,6 +7,7 @@ pub mod formatter; pub mod generator; #[cfg(feature = "gui")] pub mod gui; +pub mod gui_text; pub mod migration; pub mod output; pub mod sarif;