use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Entry {
pub tool: String,
pub stratum: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verdict: Option<String>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub note: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub k1: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub k2: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub k3: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub include_reason: Option<String>,
}
impl Entry {
pub fn missing_required_note(&self) -> bool {
self.verdict.as_deref().is_some_and(verdict_requires_note) && self.note.trim().is_empty()
}
pub fn needs_attention(&self) -> bool {
self.verdict.is_none() || self.missing_required_note()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditFile {
pub meta: AuditMeta,
#[serde(default, rename = "entry")]
pub entries: Vec<Entry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditMeta {
pub seed: u64,
pub sample_size: usize,
}
impl AuditFile {
pub fn pending(&self) -> impl Iterator<Item = usize> + '_ {
self.entries
.iter()
.enumerate()
.filter(|(_, e)| e.verdict.is_none())
.map(|(i, _)| i)
}
pub fn needing_attention(&self) -> impl Iterator<Item = usize> + '_ {
self.entries
.iter()
.enumerate()
.filter(|(_, e)| e.needs_attention())
.map(|(i, _)| i)
}
}
pub fn verdict_requires_note(verdict: &str) -> bool {
matches!(verdict, "wrong" | "incomplete")
}
pub fn verdict_path(dir: &Path, seed: u64) -> PathBuf {
dir.join(format!("{seed}.toml"))
}
pub fn load(path: &Path) -> anyhow::Result<AuditFile> {
let raw = std::fs::read_to_string(path).map_err(|e| {
anyhow::anyhow!(
"reading {}: {e} (run `xtask audit sample` first)",
path.display()
)
})?;
toml::from_str(&raw).map_err(|e| anyhow::anyhow!("parsing {}: {e}", path.display()))
}
pub fn save(path: &Path, file: &AuditFile) -> anyhow::Result<()> {
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent)
.map_err(|e| anyhow::anyhow!("creating {}: {e}", parent.display()))?;
}
}
let text = toml::to_string_pretty(file)
.map_err(|e| anyhow::anyhow!("serializing {}: {e}", path.display()))?;
std::fs::write(path, text).map_err(|e| anyhow::anyhow!("writing {}: {e}", path.display()))
}
pub fn parse_verdict_word(word: &str) -> anyhow::Result<&'static str> {
match word {
"c" | "correct" => Ok("correct"),
"i" | "incomplete" => Ok("incomplete"),
"w" | "wrong" => Ok("wrong"),
"s" | "skip" => Ok("skip"),
other => anyhow::bail!(
"unrecognized verdict {other:?} — expected one of: c/correct, i/incomplete, w/wrong, s/skip"
),
}
}
pub fn extract_tag_override(text: &mut String, key: &str) -> Option<bool> {
let true_tok = format!("{key}=true");
let false_tok = format!("{key}=false");
let mut found = None;
let kept: Vec<&str> = text
.split_whitespace()
.filter(|tok| {
if tok.eq_ignore_ascii_case(&true_tok) {
found = Some(true);
false
} else if tok.eq_ignore_ascii_case(&false_tok) {
found = Some(false);
false
} else {
true
}
})
.collect();
*text = kept.join(" ");
found
}
pub fn tag_display(label: &str, tag: Option<bool>, override_syntax: &str) -> String {
match tag {
Some(true) => format!(
"{label}: suggested TRUE — leave as-is to confirm, or add `{override_syntax}=false` \
to your verdict to override"
),
Some(false) => format!(
"{label}: suggested FALSE (fabrications present but not fully explained by the \
known class — worth a real look) — add `{override_syntax}=true` to override"
),
None => format!("{label}: not flagged (nothing of this class detected)"),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(tool: &str, verdict: Option<&str>, note: &str) -> Entry {
Entry {
tool: tool.to_string(),
stratum: "ok".to_string(),
verdict: verdict.map(str::to_string),
note: note.to_string(),
k1: None,
k2: None,
k3: None,
include_reason: None,
}
}
#[test]
fn only_wrong_and_incomplete_require_a_note() {
assert!(verdict_requires_note("wrong"));
assert!(verdict_requires_note("incomplete"));
assert!(!verdict_requires_note("correct"));
assert!(!verdict_requires_note("skip"));
}
#[test]
fn a_blank_or_whitespace_note_does_not_satisfy_the_obligation() {
assert!(entry("a", Some("wrong"), "").missing_required_note());
assert!(entry("a", Some("wrong"), " ").missing_required_note());
assert!(!entry("a", Some("wrong"), "descriptions off by one").missing_required_note());
assert!(!entry("a", Some("correct"), "").missing_required_note());
assert!(!entry("a", None, "").missing_required_note());
}
#[test]
fn the_walk_revisits_a_verdict_whose_required_note_is_missing() {
let file = AuditFile {
meta: AuditMeta {
seed: 2,
sample_size: 4,
},
entries: vec![
entry("noted", Some("wrong"), "real finding"),
entry("bare", Some("wrong"), ""),
entry("fine", Some("correct"), ""),
entry("fresh", None, ""),
],
};
assert_eq!(file.pending().collect::<Vec<_>>(), vec![3]);
assert_eq!(file.needing_attention().collect::<Vec<_>>(), vec![1, 3]);
}
#[test]
fn verdict_path_joins_seed_as_a_toml_filename() {
assert_eq!(
verdict_path(Path::new("audit"), 42),
Path::new("audit/42.toml")
);
}
#[test]
fn save_then_load_round_trips_every_field() {
let tmp = tempfile::tempdir().unwrap();
let path = verdict_path(tmp.path(), 7);
let file = AuditFile {
meta: AuditMeta {
seed: 7,
sample_size: 2,
},
entries: vec![
Entry {
tool: "openssl".to_string(),
stratum: "suspicious".to_string(),
verdict: Some("incomplete".to_string()),
note: "subcommand help never fetched".to_string(),
k1: None,
k2: Some(false),
k3: Some(true),
include_reason: None,
},
Entry {
tool: "zoxide".to_string(),
stratum: "ok".to_string(),
verdict: None,
note: String::new(),
k1: None,
k2: None,
k3: None,
include_reason: Some("unaudited promotion".to_string()),
},
],
};
save(&path, &file).unwrap();
let loaded = load(&path).unwrap();
assert_eq!(loaded.meta.seed, 7);
assert_eq!(loaded.meta.sample_size, 2);
assert_eq!(loaded.entries.len(), 2);
assert_eq!(loaded.entries[0].tool, "openssl");
assert_eq!(loaded.entries[0].verdict.as_deref(), Some("incomplete"));
assert_eq!(loaded.entries[0].k3, Some(true));
assert_eq!(
loaded.entries[1].include_reason.as_deref(),
Some("unaudited promotion")
);
assert_eq!(loaded.pending().collect::<Vec<_>>(), vec![1]);
}
#[test]
fn load_of_a_missing_file_names_the_sample_command() {
let tmp = tempfile::tempdir().unwrap();
let path = verdict_path(tmp.path(), 1);
let err = load(&path).unwrap_err();
assert!(err.to_string().contains("xtask audit sample"));
}
#[test]
fn parse_verdict_word_accepts_short_and_long_forms() {
assert_eq!(parse_verdict_word("c").unwrap(), "correct");
assert_eq!(parse_verdict_word("correct").unwrap(), "correct");
assert_eq!(parse_verdict_word("i").unwrap(), "incomplete");
assert_eq!(parse_verdict_word("incomplete").unwrap(), "incomplete");
assert_eq!(parse_verdict_word("w").unwrap(), "wrong");
assert_eq!(parse_verdict_word("wrong").unwrap(), "wrong");
assert_eq!(parse_verdict_word("s").unwrap(), "skip");
assert_eq!(parse_verdict_word("skip").unwrap(), "skip");
assert!(parse_verdict_word("maybe").is_err());
}
#[test]
fn extract_tag_override_pulls_the_token_out_of_the_note() {
let mut note =
"the extra flags were genuinely wrong k1=false not the gcc defect".to_string();
let k1 = extract_tag_override(&mut note, "k1");
assert_eq!(k1, Some(false));
assert_eq!(
note, "the extra flags were genuinely wrong not the gcc defect",
"the token is removed, the rest of the note survives untouched"
);
}
#[test]
fn extract_tag_override_is_case_insensitive_and_absent_returns_none() {
let mut note = "K1=TRUE looks like the known defect".to_string();
assert_eq!(extract_tag_override(&mut note, "k1"), Some(true));
assert_eq!(extract_tag_override(&mut note, "k2"), None);
}
#[test]
fn extract_tag_override_handles_three_keys_in_one_note() {
let mut note = "k1=true k2=false k3=true mixed causes".to_string();
assert_eq!(extract_tag_override(&mut note, "k1"), Some(true));
assert_eq!(extract_tag_override(&mut note, "k2"), Some(false));
assert_eq!(extract_tag_override(&mut note, "k3"), Some(true));
assert_eq!(note, "mixed causes");
}
#[test]
fn tag_display_names_every_state() {
assert!(tag_display("K3", Some(true), "k3").contains("suggested TRUE"));
assert!(tag_display("K3", Some(false), "k3").contains("suggested FALSE"));
assert!(tag_display("K3", None, "k3").contains("not flagged"));
}
}