use std::collections::BTreeMap;
use std::num::NonZeroUsize;
use std::path::{Path, PathBuf};
use std::process::Command;
use serde::{Deserialize, Serialize};
use crate::exec::CapturedOutput;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Ord, PartialOrd)]
pub enum Verdict {
Match,
NotApplicable,
Differ,
SuiFailOnly,
NixFailOnly,
BothFail,
SuiTimeout,
NixTimeout,
}
impl Verdict {
#[must_use]
pub fn glyph(self) -> char {
match self {
Verdict::Match => '.',
Verdict::NotApplicable => 'a',
Verdict::Differ => 'D',
Verdict::SuiFailOnly => 'S',
Verdict::NixFailOnly => 'N',
Verdict::BothFail => '?',
Verdict::SuiTimeout => 's',
Verdict::NixTimeout => 'n',
}
}
#[must_use]
pub fn glyph_styled(self) -> String {
let g = self.glyph().to_string();
match self {
Verdict::Match => crate::style::success(&g),
Verdict::NotApplicable => crate::style::muted(&g),
Verdict::Differ => crate::style::error(&g),
Verdict::SuiFailOnly => crate::style::error(&g),
Verdict::NixFailOnly => crate::style::warn(&g),
Verdict::BothFail => crate::style::warn(&g),
Verdict::SuiTimeout => crate::style::pending(&g),
Verdict::NixTimeout => crate::style::pending(&g),
}
}
#[must_use]
pub fn is_pass(self) -> bool {
matches!(self, Verdict::Match | Verdict::NotApplicable)
}
#[must_use]
pub fn name(self) -> &'static str {
match self {
Verdict::Match => "Match",
Verdict::NotApplicable => "NotApplicable",
Verdict::Differ => "Differ",
Verdict::SuiFailOnly => "SuiFailOnly",
Verdict::NixFailOnly => "NixFailOnly",
Verdict::BothFail => "BothFail",
Verdict::SuiTimeout => "SuiTimeout",
Verdict::NixTimeout => "NixTimeout",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProbeKind {
Eval,
Rebuild,
BuiltinSmoke,
}
impl ProbeKind {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
ProbeKind::Eval => "eval",
ProbeKind::Rebuild => "rebuild",
ProbeKind::BuiltinSmoke => "builtin-smoke",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TargetOs {
Darwin,
Linux,
Other,
}
impl TargetOs {
#[must_use]
pub fn current() -> Self {
match std::env::consts::OS {
"macos" => TargetOs::Darwin,
"linux" => TargetOs::Linux,
_ => TargetOs::Other,
}
}
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
TargetOs::Darwin => "darwin",
TargetOs::Linux => "linux",
TargetOs::Other => "other",
}
}
}
#[must_use]
pub fn current_nix_system() -> String {
let arch = match std::env::consts::ARCH {
"x86_64" => "x86_64",
"aarch64" => "aarch64",
other => other,
};
let os = match TargetOs::current() {
TargetOs::Darwin => "darwin",
TargetOs::Linux => "linux",
TargetOs::Other => std::env::consts::OS,
};
format!("{arch}-{os}")
}
#[must_use]
pub fn current_hostname() -> String {
if let Ok(s) = std::fs::read_to_string("/etc/hostname") {
let trimmed = s.trim();
if !trimmed.is_empty() {
return trimmed.split('.').next().unwrap_or(trimmed).to_string();
}
}
if let Ok(s) = std::env::var("HOSTNAME") {
if !s.is_empty() {
return s.split('.').next().unwrap_or(&s).to_string();
}
}
if let Ok(out) = std::process::Command::new("hostname").arg("-s").output() {
if out.status.success() {
let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !s.is_empty() {
return s;
}
}
}
"unknown".to_string()
}
#[derive(Debug, Clone)]
pub struct ProbeContext {
pub flake_path: PathBuf,
pub flake_label: String,
pub host: String,
pub system: String,
pub user: String,
pub os: TargetOs,
}
impl ProbeContext {
#[must_use]
pub fn current(flake_path: PathBuf) -> Self {
let flake_label = flake_path
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| flake_path.display().to_string());
Self {
flake_path,
flake_label,
host: current_hostname(),
system: current_nix_system(),
user: std::env::var("USER").unwrap_or_else(|_| "unknown".into()),
os: TargetOs::current(),
}
}
#[must_use]
pub fn substitute(&self, template: &str) -> String {
template
.replace("$FLAKE", &self.flake_path.display().to_string())
.replace("$HOST", &self.host)
.replace("$SYSTEM", &self.system)
.replace("$USER", &self.user)
}
}
pub trait ParityCheck {
fn name(&self) -> &str;
fn tags(&self) -> &[String];
fn kind(&self) -> ProbeKind;
fn applies(&self, _ctx: &ProbeContext) -> bool {
true
}
fn sui_invocation(&self, ctx: &ProbeContext, sui_bin: &Path) -> Command;
fn nix_invocation(&self, ctx: &ProbeContext, nix_bin: &Path) -> Command;
fn classify(&self, sui: &CapturedOutput, nix: &CapturedOutput) -> Verdict {
default_classify(sui, nix, |s, n| s.stdout.trim() == n.stdout.trim())
}
}
pub fn default_classify(
sui: &CapturedOutput,
nix: &CapturedOutput,
compare_ok: impl FnOnce(&CapturedOutput, &CapturedOutput) -> bool,
) -> Verdict {
match (sui.timed_out, nix.timed_out) {
(true, _) => return Verdict::SuiTimeout,
(_, true) => return Verdict::NixTimeout,
(false, false) => {}
}
match (sui.success, nix.success) {
(true, true) => if compare_ok(sui, nix) { Verdict::Match } else { Verdict::Differ },
(false, true) => Verdict::SuiFailOnly,
(true, false) => Verdict::NixFailOnly,
(false, false) => Verdict::BothFail,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShadowReport {
pub generated_at: String,
pub generator: String,
pub host: String,
pub system: String,
pub os: String,
pub user: String,
pub sui_version: Option<String>,
pub nix_version: Option<String>,
pub records: Vec<ProbeRecord>,
pub tally: BTreeMap<String, usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Examined {
count: NonZeroUsize,
worst: Verdict,
}
impl Examined {
#[must_use]
pub fn count(&self) -> NonZeroUsize {
self.count
}
#[must_use]
pub fn worst(&self) -> Verdict {
self.worst
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SweepVerdict {
AllPassed { examined: Examined },
Diverged {
examined: Examined,
diverged: NonZeroUsize,
},
Vacuous,
Reclassified {
examined: Examined,
reclassified: NonZeroUsize,
},
BelowFloor {
examined: Examined,
enforced: usize,
floor: NonZeroUsize,
},
}
const fn is_enforced(verdict: Verdict, from: Option<Verdict>) -> bool {
from.is_none() && !matches!(verdict, Verdict::NotApplicable)
}
#[must_use]
pub fn enforced_count<I>(rows: I) -> usize
where
I: IntoIterator<Item = (Verdict, Option<Verdict>)>,
{
rows.into_iter()
.filter(|(v, from)| is_enforced(*v, *from))
.count()
}
impl SweepVerdict {
#[must_use]
pub fn classify<I>(rows: I, floor: NonZeroUsize) -> Self
where
I: IntoIterator<Item = (Verdict, Option<Verdict>)>,
{
let mut count = 0usize;
let mut worst: Option<Verdict> = None;
let mut diverged = 0usize;
let mut reclassified = 0usize;
let mut enforced = 0usize;
for (verdict, from) in rows {
count += 1;
worst = Some(worst.map_or(verdict, |w| w.max(verdict)));
if !verdict.is_pass() {
diverged += 1;
}
if from.is_some() {
reclassified += 1;
}
if is_enforced(verdict, from) {
enforced += 1;
}
}
let (Some(worst), Some(count)) = (worst, NonZeroUsize::new(count)) else {
return Self::Vacuous;
};
let examined = Examined { count, worst };
if let Some(diverged) = NonZeroUsize::new(diverged) {
return Self::Diverged { examined, diverged };
}
if enforced < floor.get() {
return Self::BelowFloor {
examined,
enforced,
floor,
};
}
match NonZeroUsize::new(reclassified) {
Some(reclassified) => Self::Reclassified {
examined,
reclassified,
},
None => Self::AllPassed { examined },
}
}
#[must_use]
pub const fn is_pass(&self) -> bool {
matches!(self, Self::AllPassed { .. })
}
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::AllPassed { .. } => "AllPassed",
Self::Diverged { .. } => "Diverged",
Self::Vacuous => "Vacuous",
Self::Reclassified { .. } => "Reclassified",
Self::BelowFloor { .. } => "BelowFloor",
}
}
#[must_use]
pub const fn examined(&self) -> Option<Examined> {
match self {
Self::AllPassed { examined }
| Self::Diverged { examined, .. }
| Self::Reclassified { examined, .. }
| Self::BelowFloor { examined, .. } => Some(*examined),
Self::Vacuous => None,
}
}
#[must_use]
pub const fn reclassified(&self) -> Option<NonZeroUsize> {
match self {
Self::Reclassified { reclassified, .. } => Some(*reclassified),
Self::AllPassed { .. }
| Self::Diverged { .. }
| Self::Vacuous
| Self::BelowFloor { .. } => None,
}
}
#[must_use]
pub fn refusal(&self) -> Option<String> {
match self {
Self::AllPassed { .. } => None,
Self::Vacuous => Some(
"VACUOUS — 0 rows were examined, so nothing was proven".to_string(),
),
Self::Diverged { diverged, examined } => Some(format!(
"DIVERGED — {diverged} of {} examined row(s) did not pass",
examined.count()
)),
Self::BelowFloor {
enforced,
floor,
examined,
} => Some(format!(
"BELOW FLOOR — only {enforced} of {} examined row(s) were actually enforced, \
below the committed floor of {floor}. The gate compared too little to make \
a claim; this is the vacuity guard, not a byte-divergence",
examined.count()
)),
Self::Reclassified {
reclassified,
examined,
} => Some(format!(
"RECLASSIFIED — {reclassified} of {} examined row(s) were moved out of the \
enforced set by a run-time flag, so the corpus-wide seal is not claimed",
examined.count()
)),
}
}
}
impl ShadowReport {
#[must_use]
pub fn verdict(&self) -> SweepVerdict {
SweepVerdict::classify(
self.records.iter().map(|r| (r.verdict, r.reclassified_from)),
NonZeroUsize::MIN,
)
}
#[must_use]
pub fn reclassified_count(&self) -> usize {
self.records
.iter()
.filter(|r| r.reclassified_from.is_some())
.count()
}
#[must_use]
pub fn all_pass(&self) -> bool {
self.verdict().is_pass()
}
#[must_use]
pub fn divergence_count(&self) -> usize {
self.records.iter().filter(|r| !r.verdict.is_pass()).count()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProbeRecord {
pub name: String,
pub kind: ProbeKind,
pub tags: Vec<String>,
pub flake: String,
pub sui_argv: Vec<String>,
pub nix_argv: Vec<String>,
pub sui_exit: Option<i32>,
pub nix_exit: Option<i32>,
pub sui_stdout_excerpt: String,
pub nix_stdout_excerpt: String,
pub sui_stderr_excerpt: String,
pub nix_stderr_excerpt: String,
pub sui_duration_ms: u128,
pub nix_duration_ms: u128,
pub sui_timed_out: bool,
pub nix_timed_out: bool,
pub verdict: Verdict,
#[serde(default)]
pub reclassified_from: Option<Verdict>,
}
#[must_use]
pub fn excerpt(s: &str, max: usize) -> String {
if s.len() <= max {
return s.to_string();
}
let mut end = max;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
format!("{}…", &s[..end])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verdict_pass_set() {
assert!(Verdict::Match.is_pass());
assert!(Verdict::NotApplicable.is_pass());
assert!(!Verdict::Differ.is_pass());
assert!(!Verdict::SuiFailOnly.is_pass());
assert!(!Verdict::SuiTimeout.is_pass());
}
#[test]
fn substitute_replaces_all_placeholders() {
let ctx = ProbeContext {
flake_path: PathBuf::from("/tmp/myflake"),
flake_label: "myflake".into(),
host: "cid".into(),
system: "aarch64-darwin".into(),
user: "drzzln".into(),
os: TargetOs::Darwin,
};
let out = ctx.substitute("path:$FLAKE host=$HOST sys=$SYSTEM user=$USER");
assert_eq!(out, "path:/tmp/myflake host=cid sys=aarch64-darwin user=drzzln");
}
#[test]
fn excerpt_truncates_at_char_boundary() {
let s = "ab".repeat(100);
let e = excerpt(&s, 20);
assert!(e.ends_with('…'));
assert!(e.chars().count() <= 25);
}
#[test]
fn default_classify_handles_full_matrix() {
let ok = mk_out(true, false);
let fail = mk_out(false, false);
let timeout = mk_out(false, true);
let always_eq = |_s: &CapturedOutput, _n: &CapturedOutput| true;
let always_neq = |_s: &CapturedOutput, _n: &CapturedOutput| false;
assert_eq!(default_classify(&ok, &ok, always_eq), Verdict::Match);
assert_eq!(default_classify(&ok, &ok, always_neq), Verdict::Differ);
assert_eq!(default_classify(&fail, &ok, always_eq), Verdict::SuiFailOnly);
assert_eq!(default_classify(&ok, &fail, always_eq), Verdict::NixFailOnly);
assert_eq!(default_classify(&fail, &fail, always_eq), Verdict::BothFail);
assert_eq!(default_classify(&timeout, &ok, always_eq), Verdict::SuiTimeout);
assert_eq!(default_classify(&ok, &timeout, always_eq), Verdict::NixTimeout);
}
fn mk_out(success: bool, timed_out: bool) -> CapturedOutput {
CapturedOutput {
exit_code: if success { Some(0) } else { Some(1) },
success,
stdout: String::new(),
stderr: String::new(),
duration: std::time::Duration::from_millis(1),
timed_out,
}
}
#[test]
fn vacuous_sweep_is_not_a_pass() {
let report = empty_report();
assert_eq!(report.verdict(), SweepVerdict::Vacuous);
assert!(
!report.all_pass(),
"a sweep that ran ZERO probes must not report all-pass",
);
assert_eq!(report.divergence_count(), 0);
}
#[test]
fn pass_arm_carries_a_non_empty_witness() {
let report = report_with(&[Verdict::Match, Verdict::NotApplicable]);
match report.verdict() {
SweepVerdict::AllPassed { examined } => {
assert_eq!(examined.count().get(), 2);
assert_eq!(examined.worst(), Verdict::NotApplicable);
}
other => panic!("expected AllPassed, got {other:?}"),
}
}
#[test]
fn witness_worst_uses_the_eight_way_ordering() {
assert!(Verdict::Differ < Verdict::BothFail);
assert!(Verdict::Match < Verdict::Differ);
let report = report_with(&[Verdict::Match, Verdict::BothFail, Verdict::Differ]);
match report.verdict() {
SweepVerdict::Diverged { examined, diverged } => {
assert_eq!(diverged.get(), 2);
assert_eq!(examined.count().get(), 3);
assert_eq!(examined.worst(), Verdict::BothFail);
}
other => panic!("expected Diverged, got {other:?}"),
}
}
#[test]
fn vacuous_has_no_witness() {
assert!(SweepVerdict::Vacuous.examined().is_none());
assert!(!SweepVerdict::Vacuous.is_pass());
}
#[test]
fn verdict_is_rederived_across_the_serde_border() {
let report = report_with(&[Verdict::Match, Verdict::Differ]);
let json = serde_json::to_string(&report).expect("serialize");
let back: ShadowReport = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.verdict(), report.verdict());
assert!(!back.all_pass());
let empty_json = serde_json::to_string(&empty_report()).expect("serialize");
let back_empty: ShadowReport = serde_json::from_str(&empty_json).expect("deserialize");
assert_eq!(back_empty.verdict(), SweepVerdict::Vacuous);
}
fn report_with_reclassified(verdicts: &[Verdict], n: usize) -> ShadowReport {
let mut report = report_with(verdicts);
for r in report.records.iter_mut().take(n) {
r.reclassified_from = Some(Verdict::SuiFailOnly);
}
report
}
#[test]
fn reclassified_rows_are_not_a_pass() {
let report = report_with_reclassified(&[Verdict::Match, Verdict::Match], 1);
let v = report.verdict();
assert!(
!v.is_pass(),
"a narrowed denominator must not report a pass: {v:?}"
);
assert_eq!(v.reclassified().map(NonZeroUsize::get), Some(1));
assert!(v.examined().is_some());
}
#[test]
fn an_unnarrowed_sweep_still_passes() {
let report = report_with(&[Verdict::Match, Verdict::Match]);
assert!(report.verdict().is_pass());
assert_eq!(report.verdict().reclassified(), None);
assert_eq!(report.reclassified_count(), 0);
}
#[test]
fn divergence_outranks_reclassification() {
let report = report_with_reclassified(&[Verdict::Differ, Verdict::Match], 1);
assert!(matches!(
report.verdict(),
SweepVerdict::Diverged { .. }
));
}
#[test]
fn reclassified_is_none_for_whole_corpus_verdicts() {
assert_eq!(report_with(&[Verdict::Match]).verdict().reclassified(), None);
assert_eq!(
report_with(&[Verdict::Differ]).verdict().reclassified(),
None
);
assert_eq!(empty_report().verdict().reclassified(), None);
assert_eq!(empty_report().verdict(), SweepVerdict::Vacuous);
}
fn rows(v: Verdict, n: usize) -> Vec<(Verdict, Option<Verdict>)> {
std::iter::repeat_n((v, None), n).collect()
}
fn floor(n: usize) -> NonZeroUsize {
NonZeroUsize::new(n).expect("test floor must be non-zero")
}
#[test]
fn a_collapsed_enforced_set_is_not_a_pass() {
let all_reclassified: Vec<_> = std::iter::repeat_n(
(Verdict::NotApplicable, Some(Verdict::NixFailOnly)),
40,
)
.collect();
let v = SweepVerdict::classify(all_reclassified, floor(20));
assert!(
!v.is_pass(),
"a gate that compared nothing must not certify everything: {v:?}"
);
match v {
SweepVerdict::BelowFloor {
enforced,
floor,
examined,
} => {
assert_eq!(enforced, 0);
assert_eq!(floor.get(), 20);
assert_eq!(examined.count().get(), 40);
}
other => panic!("expected BelowFloor, got {other:?}"),
}
}
#[test]
fn the_floor_is_checked_before_the_pass_arms() {
let v = SweepVerdict::classify(rows(Verdict::Match, 3), floor(10));
assert!(matches!(v, SweepVerdict::BelowFloor { enforced: 3, .. }), "{v:?}");
assert!(!v.is_pass());
}
#[test]
fn self_skipped_rows_narrow_the_denominator_too() {
let mut r = rows(Verdict::Match, 2);
r.extend(rows(Verdict::NotApplicable, 8));
let v = SweepVerdict::classify(r, floor(5));
assert_eq!(v.reclassified(), None, "nothing was FLAG-reclassified");
assert!(
matches!(v, SweepVerdict::BelowFloor { enforced: 2, .. }),
"the floor must still catch it: {v:?}"
);
}
#[test]
fn clearing_the_floor_with_reclassified_rows_still_withholds_the_seal() {
let mut r = rows(Verdict::Match, 6);
r.push((Verdict::NotApplicable, Some(Verdict::NixFailOnly)));
let v = SweepVerdict::classify(r, floor(5));
assert!(!v.is_pass(), "{v:?}");
assert_eq!(v.reclassified().map(NonZeroUsize::get), Some(1));
}
#[test]
fn clearing_the_floor_cleanly_is_a_pass() {
let v = SweepVerdict::classify(rows(Verdict::Match, 41), floor(20));
assert!(v.is_pass(), "{v:?}");
assert_eq!(v.examined().map(|e| e.count().get()), Some(41));
}
#[test]
fn divergence_outranks_the_floor() {
let mut r = rows(Verdict::Differ, 1);
r.extend(rows(Verdict::NotApplicable, 9));
let v = SweepVerdict::classify(r, floor(20));
assert!(matches!(v, SweepVerdict::Diverged { .. }), "{v:?}");
}
#[test]
fn zero_rows_is_vacuous_not_below_floor() {
let v = SweepVerdict::classify(Vec::new(), floor(20));
assert_eq!(v, SweepVerdict::Vacuous);
}
#[test]
fn every_refusal_carries_a_reason() {
assert_eq!(
SweepVerdict::classify(rows(Verdict::Match, 5), floor(1)).refusal(),
None,
"a pass has no reason to give"
);
for v in [
SweepVerdict::classify(Vec::new(), floor(1)),
SweepVerdict::classify(rows(Verdict::Differ, 2), floor(1)),
SweepVerdict::classify(rows(Verdict::Match, 1), floor(9)),
SweepVerdict::classify(
vec![(Verdict::Match, None), (Verdict::NotApplicable, Some(Verdict::SuiFailOnly))],
floor(1),
),
] {
let r = v.refusal();
assert!(r.is_some(), "{v:?} must explain itself");
assert!(!r.unwrap().is_empty());
}
}
fn report_with(verdicts: &[Verdict]) -> ShadowReport {
let mut report = empty_report();
report.records = verdicts
.iter()
.map(|v| ProbeRecord {
name: "p".into(),
kind: ProbeKind::Eval,
tags: vec![],
flake: "f".into(),
sui_argv: vec![],
nix_argv: vec![],
sui_exit: Some(0),
nix_exit: Some(0),
sui_stdout_excerpt: String::new(),
nix_stdout_excerpt: String::new(),
sui_stderr_excerpt: String::new(),
nix_stderr_excerpt: String::new(),
sui_duration_ms: 0,
nix_duration_ms: 0,
sui_timed_out: false,
nix_timed_out: false,
verdict: *v,
reclassified_from: None,
})
.collect();
report
}
fn empty_report() -> ShadowReport {
ShadowReport {
generated_at: "2026-07-28T00:00:00Z".into(),
generator: "sui-sweep test".into(),
host: "cid".into(),
system: "aarch64-darwin".into(),
os: "darwin".into(),
user: "drzzln".into(),
sui_version: None,
nix_version: None,
records: Vec::new(),
tally: BTreeMap::new(),
}
}
#[test]
fn shadow_report_pass_counts_records() {
let rec_pass = ProbeRecord {
name: "p".into(), kind: ProbeKind::Eval, tags: vec![],
flake: "f".into(), sui_argv: vec![], nix_argv: vec![],
sui_exit: Some(0), nix_exit: Some(0),
sui_stdout_excerpt: String::new(), nix_stdout_excerpt: String::new(),
sui_stderr_excerpt: String::new(), nix_stderr_excerpt: String::new(),
sui_duration_ms: 0, nix_duration_ms: 0,
sui_timed_out: false, nix_timed_out: false,
verdict: Verdict::Match,
reclassified_from: None,
};
let mut rec_fail = rec_pass.clone();
rec_fail.verdict = Verdict::Differ;
let report = ShadowReport {
generated_at: "2026-05-22T00:00:00Z".into(),
generator: "sui-sweep 0.1".into(),
host: "cid".into(),
system: "aarch64-darwin".into(),
os: "darwin".into(),
user: "drzzln".into(),
sui_version: None, nix_version: None,
records: vec![rec_pass.clone(), rec_fail, rec_pass],
tally: BTreeMap::new(),
};
assert!(!report.all_pass());
assert_eq!(report.divergence_count(), 1);
}
}