use comfy_table::{Cell, Color, Table};
use parlov_core::{
EndpointVerdict, ImpactClass, ObservabilityStatus, OracleResult, OracleVerdict, Severity,
};
use crate::table_rows::{add_chain_row, add_probe_row, add_repro_rows, add_verbose_rows};
use crate::ScanFinding;
#[must_use]
pub fn render_table(result: &OracleResult) -> String {
let mut table = Table::new();
table.set_header(vec![
"Oracle",
"Verdict",
"Severity",
"Confidence",
"Evidence",
]);
let verdict_cell = verdict_cell(result.verdict);
let sev_cell = severity_cell(result.severity.as_ref());
let conf_cell = confidence_cell(result.confidence, result.impact_class);
let oracle_label = result.class.to_string();
let evidence = result.primary_evidence();
table.add_row(vec![
Cell::new(&oracle_label),
verdict_cell,
sev_cell,
conf_cell,
Cell::new(evidence),
]);
add_metadata_rows(&mut table, result);
table.to_string()
}
fn add_metadata_rows(table: &mut Table, result: &OracleResult) {
if let Some(label) = &result.label {
add_detail_row(table, "Label", label);
}
if let Some(leaks) = &result.leaks {
add_detail_row(table, "Leaks", leaks);
}
if let Some(rfc_basis) = &result.rfc_basis {
add_detail_row(table, "RFC Basis", rfc_basis);
}
}
fn add_detail_row(table: &mut Table, key: &str, value: &str) {
table.add_row(vec![
Cell::new(""),
Cell::new(""),
Cell::new(key),
Cell::new(value),
]);
}
#[must_use]
pub fn render_scan_table(findings: &[ScanFinding]) -> String {
let mut table = Table::new();
table.set_header(vec![
"Strategy",
"Method",
"Verdict",
"Severity",
"Confidence",
"Evidence",
]);
if findings.is_empty() {
return table.to_string();
}
for f in findings {
let evidence = f.result.primary_evidence();
table.add_row(vec![
Cell::new(&f.strategy_name),
Cell::new(&f.method),
verdict_cell(f.result.verdict),
severity_cell(f.result.severity.as_ref()),
confidence_cell(f.result.confidence, f.result.impact_class),
Cell::new(evidence),
]);
add_probe_row(&mut table, f);
add_chain_row(&mut table, f);
add_repro_rows(&mut table, f);
add_verbose_rows(&mut table, f);
}
table.to_string()
}
fn verdict_cell(verdict: OracleVerdict) -> Cell {
let (label, color) = match verdict {
OracleVerdict::Confirmed => ("Confirmed", Color::Red),
OracleVerdict::Likely => ("Likely", Color::Yellow),
OracleVerdict::Inconclusive => ("Inconclusive", Color::Blue),
OracleVerdict::NotPresent => ("NotPresent", Color::Green),
};
Cell::new(label).fg(color)
}
fn severity_cell(severity: Option<&Severity>) -> Cell {
match severity {
Some(Severity::High) => Cell::new("High").fg(Color::Red),
Some(Severity::Medium) => Cell::new("Medium").fg(Color::Yellow),
Some(Severity::Low) => Cell::new("Low").fg(Color::Cyan),
None => Cell::new("\u{2014}"),
}
}
fn confidence_cell(confidence: u8, impact_class: Option<ImpactClass>) -> Cell {
if confidence == 0 {
return Cell::new("\u{2014}");
}
let label = match impact_class {
Some(ic) => format!("{confidence} ({})", impact_class_label(ic)),
None => format!("{confidence}"),
};
Cell::new(label)
}
fn impact_class_label(ic: ImpactClass) -> &'static str {
match ic {
ImpactClass::High => "High",
ImpactClass::Medium => "Medium",
ImpactClass::Low => "Low",
}
}
#[must_use]
pub fn render_endpoint_verdict_table(
verdict: &EndpointVerdict,
findings: &[ScanFinding],
) -> String {
let mut table = Table::new();
table.set_header(vec![
"Oracle / Strategy",
"Method",
"Verdict",
"Severity",
"Confidence",
"Evidence",
]);
add_verdict_summary_row(&mut table, verdict);
for f in findings {
table.add_row(vec![
Cell::new(&f.strategy_name),
Cell::new(&f.method),
verdict_cell(f.result.verdict),
severity_cell(f.result.severity.as_ref()),
confidence_cell(f.result.confidence, f.result.impact_class),
Cell::new(f.result.primary_evidence()),
]);
add_probe_row(&mut table, f);
add_chain_row(&mut table, f);
add_repro_rows(&mut table, f);
add_verbose_rows(&mut table, f);
}
table.to_string()
}
fn add_verdict_summary_row(table: &mut Table, verdict: &EndpointVerdict) {
use parlov_core::EndpointStopReason;
let posterior_pct = format!("{:.0}%", verdict.posterior_probability * 100.0);
let stop = verdict.stop_reason.as_ref().map_or("—", |r| match r {
EndpointStopReason::EarlyAccept => "EarlyAccept",
EndpointStopReason::EarlyReject => "EarlyReject",
EndpointStopReason::ExhaustedPlan => "ExhaustedPlan",
});
let strategies = format!("{}/{}", verdict.strategies_run, verdict.strategies_total);
let summary_label = format!("Endpoint [{posterior_pct}] {strategies} {stop}");
table.add_row(vec![
Cell::new(summary_label),
Cell::new(""),
verdict_cell(verdict.verdict),
severity_cell(verdict.severity.as_ref()),
Cell::new(""),
Cell::new(""),
]);
add_observability_rows(table, verdict);
}
fn add_observability_rows(table: &mut Table, verdict: &EndpointVerdict) {
if verdict.observability_status == ObservabilityStatus::EvidenceObserved {
return;
}
let obs_label = match &verdict.block_summary {
Some(bs) => format!(
"{} ({}/{} opportunities blocked by {})",
verdict.observability_status,
bs.blocked_before_oracle_layer,
bs.expected_observation_opportunities,
bs.dominant_block_family,
),
None => verdict.observability_status.to_string(),
};
table.add_row(vec![
Cell::new(""),
Cell::new(""),
Cell::new("Observability"),
Cell::new(obs_label),
Cell::new(""),
Cell::new(""),
]);
if let Some(action) = verdict
.block_summary
.as_ref()
.and_then(|bs| bs.operator_action.as_ref())
{
table.add_row(vec![
Cell::new(""),
Cell::new(""),
Cell::new("Action"),
Cell::new(action),
Cell::new(""),
Cell::new(""),
]);
}
}
#[cfg(test)]
#[path = "table_tests.rs"]
mod tests;