// SDD-120 §8 — PR-vs-baseline comparator binary. // // Reads `current.json` (from §5 emit_baseline) and `baseline.json` // (extracted from the SAD-089 path at the merge-base by the §6 // workflow) and emits a markdown report applying: // // - Zero-tolerance metrics (SRS-219 clause a — capture_alloc_count): // 🔴 if c != 0 else 🟢. Yellow does not apply. // - Otherwise (t > 0): // delta_pct = (c - b) / b (or 0 if b == 0) // 🔴 if delta_pct > t // 🟡 if 0.5*t < delta_pct ≤ t // 🟢 otherwise (including improvements, delta_pct < 0) // // Always exits 0 — regression detection is rendered as a 🔴 marker // inside the markdown comment, NOT as a non-zero exit code // (SRS-218 clause 4). // // CLI: // cargo run --bin compare_baseline -- \ // --current ./current.json \ // --baseline ./baseline.json \ // --output ./report.md use std::collections::HashMap; use std::fs; use std::path::PathBuf; use std::process::ExitCode; use serde_json::Value; struct Args { current: PathBuf, baseline: PathBuf, output: PathBuf, } fn parse_args() -> Args { let mut current = PathBuf::from("current.json"); let mut baseline = PathBuf::from("baseline.json"); let mut output = PathBuf::from("report.md"); let mut it = std::env::args().skip(1); while let Some(a) = it.next() { match a.as_str() { "--current" => current = PathBuf::from(it.next().unwrap_or_default()), "--baseline" => baseline = PathBuf::from(it.next().unwrap_or_default()), "--output" => output = PathBuf::from(it.next().unwrap_or_default()), _ => {} } } Args { current, baseline, output, } } fn read_metrics(path: &PathBuf) -> (Option, HashMap) { let mut out = HashMap::new(); let text = match fs::read_to_string(path) { Ok(t) => t, Err(_) => return (None, out), }; if text.trim().is_empty() || text.trim() == "{}" { return (None, out); } let v: Value = match serde_json::from_str(&text) { Ok(v) => v, Err(_) => return (None, out), }; let metrics = v.get("metrics").and_then(|m| m.as_array()).cloned(); if let Some(arr) = metrics { for m in arr { let name = m .get("metric") .and_then(|s| s.as_str()) .unwrap_or("") .to_string(); let unit = m .get("unit") .and_then(|s| s.as_str()) .unwrap_or("") .to_string(); let value = m.get("value").and_then(|n| n.as_f64()); let tolerance = m .get("tolerance_pct") .and_then(|n| n.as_f64()) .unwrap_or(0.0); if !name.is_empty() { out.insert( name.clone(), MetricRow { metric: name, unit, value, tolerance_pct: tolerance, }, ); } } } (Some(v), out) } #[derive(Clone)] struct MetricRow { metric: String, unit: String, value: Option, tolerance_pct: f64, } fn fmt_value(v: Option) -> String { match v { Some(x) if x.is_finite() => { if x.abs() >= 1_000_000.0 { format!("{:.3e}", x) } else if x.abs() >= 1.0 { format!("{:.3}", x) } else { format!("{:.6}", x) } } _ => "n/a".to_string(), } } fn marker(b: Option, c: Option, t: f64) -> (String, String) { // Returns (marker, delta_str) let (b, c) = match (b, c) { (Some(b), Some(c)) => (b, c), _ => return ("⚪".to_string(), "n/a".to_string()), }; if t == 0.0 { // Zero-tolerance metric (SRS-219 clause a). if c != 0.0 { return ("🔴".to_string(), format!("{:+}", c as i64)); } else { return ("🟢".to_string(), "+0".to_string()); } } let delta_pct = if b > 0.0 { (c - b) / b } else { 0.0 }; let delta_str = format!("{:+.2}%", delta_pct * 100.0); let m = if delta_pct > t { "🔴" } else if delta_pct > 0.5 * t { "🟡" } else { "🟢" }; (m.to_string(), delta_str) } fn main() -> ExitCode { let args = parse_args(); let (cur_doc, cur) = read_metrics(&args.current); let (base_doc, base) = read_metrics(&args.baseline); let mut md = String::new(); md.push_str("\n"); md.push_str("### Chanora realtime-audio benchmark advisory (SDD-120)\n\n"); if cur_doc.is_none() { md.push_str("_No `current.json` found — bench harness did not produce results._\n"); fs::write(&args.output, &md).expect("write report"); return ExitCode::SUCCESS; } if base.is_empty() { md.push_str( "_Baseline not yet established at merge-base; first dispatch of \ `bench-baseline-update.yml` on the default branch will establish \ it. No red markers emitted on this first run._\n\n", ); md.push_str("| Metric | Unit | Current |\n"); md.push_str("|---|---|---|\n"); let mut keys: Vec<&String> = cur.keys().collect(); keys.sort(); for k in keys { let row = &cur[k]; md.push_str(&format!( "| `{}` | {} | {} |\n", row.metric, row.unit, fmt_value(row.value) )); } fs::write(&args.output, &md).expect("write report"); return ExitCode::SUCCESS; } if let Some(d) = base_doc.as_ref() { if let Some(sha) = d.get("git_sha").and_then(|s| s.as_str()) { md.push_str(&format!("Baseline `git_sha`: `{}`\n\n", sha)); } } md.push_str("| Metric | Unit | Baseline | Current | Δ | Tolerance | Status |\n"); md.push_str("|---|---|---|---|---|---|---|\n"); // Iterate over the union of metric names, sorted. let mut all: Vec = cur.keys().chain(base.keys()).cloned().collect(); all.sort(); all.dedup(); for name in all { let c = cur.get(&name); let b = base.get(&name); let unit = c .map(|r| r.unit.clone()) .or_else(|| b.map(|r| r.unit.clone())) .unwrap_or_default(); let tol = c .map(|r| r.tolerance_pct) .or_else(|| b.map(|r| r.tolerance_pct)) .unwrap_or(0.0); let (m, delta) = marker(b.and_then(|r| r.value), c.and_then(|r| r.value), tol); md.push_str(&format!( "| `{}` | {} | {} | {} | {} | {} | {} |\n", name, unit, fmt_value(b.and_then(|r| r.value)), fmt_value(c.and_then(|r| r.value)), delta, if tol == 0.0 { "0 (zero-tolerance)".to_string() } else { format!("{:.1}%", tol * 100.0) }, m, )); } md.push_str( "\n_Advisory only — this workflow never fails a check on regression \ (SRS-218 clause 4)._\n", ); fs::write(&args.output, &md).expect("write report"); ExitCode::SUCCESS }