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>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spot_audit_event: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub families: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub families_derived: Option<bool>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub amendments: Vec<Amendment>,
}
impl Entry {
pub fn effective_verdict(&self) -> Option<&str> {
match self.amendments.last() {
Some(a) => Some(a.new_verdict.as_str()),
None => self.verdict.as_deref(),
}
}
pub fn effective_note(&self) -> &str {
match self.amendments.last() {
Some(a) => a.new_note.as_str(),
None => self.note.as_str(),
}
}
pub fn missing_required_note(&self) -> bool {
self.effective_verdict().is_some_and(verdict_requires_note)
&& self.effective_note().trim().is_empty()
}
pub fn needs_attention(&self) -> bool {
self.verdict.is_none() || self.missing_required_note()
}
pub fn is_judged_defect(&self) -> bool {
self.effective_verdict()
.is_some_and(|v| matches!(v, "wrong" | "incomplete"))
}
pub fn is_judged_correct(&self) -> bool {
self.effective_verdict() == Some("correct")
}
pub fn is_unclassified(&self) -> bool {
self.is_judged_defect() && self.families.is_empty()
}
pub fn has_family(&self, family: &str) -> bool {
self.families.iter().any(|f| f == family)
}
pub fn is_display_only(&self) -> bool {
self.is_judged_defect() && self.families.len() == 1 && self.families[0] == "display-only"
}
pub fn validate_families(&self) -> anyhow::Result<()> {
for (i, family) in self.families.iter().enumerate() {
if family_meaning(family).is_none() {
anyhow::bail!(
"{:?}: unrecognized defect family {family:?} — expected one of: {}",
self.tool,
family_names().join(", ")
);
}
if self.families[..i].contains(family) {
anyhow::bail!("{:?}: defect family {family:?} listed twice", self.tool);
}
}
if !self.families.is_empty() {
if self.families_derived.is_none() {
anyhow::bail!(
"{:?} carries family labels with no `families_derived` provenance — a machine \
reading of a reviewer's note must never be mistakable for the reviewer's own \
classification",
self.tool
);
}
if !self.is_judged_defect() {
anyhow::bail!(
"{:?} is {:?}, which names no defect, yet carries family labels {:?} — a \
family describes what is wrong, so labelling a non-defect would put this \
tool in a detector's expected-fires set on a verdict that says nothing is \
wrong with it",
self.tool,
self.effective_verdict().unwrap_or("pending"),
self.families,
);
}
}
Ok(())
}
}
pub struct DefectFamily {
pub name: &'static str,
pub meaning: &'static str,
}
pub const DEFECT_FAMILIES: &[DefectFamily] = &[
DefectFamily {
name: "bundled-short-flag",
meaning: "a bundle of boolean short flags (`[-abcXYZ]`) collapses into one flag `-a` \
carrying the rest as a value, instead of N separate flags",
},
DefectFamily {
name: "single-dash-long",
meaning: "a single-dash long option (`-help`, `-fdump-scos`) splits into a one-character \
short flag plus the remainder as a value name (the K1 pre-tag's shape)",
},
DefectFamily {
name: "repeated-char-flag",
meaning: "a repeated-character flag (`-vv`, `-dd`, `-kk`) is stored as its single-letter \
form carrying the repeat as a required value (`-v` + value `\"v\"`) rather than \
as the doubled flag itself — extracted, but as the wrong shape, not absent",
},
DefectFamily {
name: "brace-alternation-flag",
meaning: "a flag written as a brace alternation of its own spellings (`{-i|--input} \
<file>`, `{-v | --version}`) is dropped entirely or keeps a brace as its value",
},
DefectFamily {
name: "dropped-alias",
meaning: "one half of a documented short/long alias pair is missing from the extracted \
flag (`-p` kept, `--pid` dropped, or the reverse)",
},
DefectFamily {
name: "value-name-mangled",
meaning: "a flag's value spec is mis-captured: an alternative form, an alias spelling, or \
a second accepted type is swallowed into or dropped from `value_name`",
},
DefectFamily {
name: "missing-flag-description",
meaning: "flags are extracted but carry no description text, though the help text \
attaches one",
},
DefectFamily {
name: "section-header-bleed",
meaning: "text belonging to a section heading is absorbed into a flag, a description, or \
a node name",
},
DefectFamily {
name: "unparsed-flag",
meaning: "flag spellings plainly present in the help text produce no flag at all — a \
partial recall gap, distinct from `verbatim-fallback`'s total one. A SYMPTOM, \
not a shape: its five labelled tools are five unrelated dispositions and no \
detector generalizes them; see the comment above",
},
DefectFamily {
name: "unparsed-subcommand",
meaning: "subcommand names are plainly present in the help text but no child node is \
produced for them — four unrelated grammars share this label; see the comment \
above, only shape A (the dash-separated command table) is fixed",
},
DefectFamily {
name: "unparsed-positional",
meaning: "a positional operand in the usage line (`<destination>`, `pid`) is never \
extracted — the IR has nowhere to put it",
},
DefectFamily {
name: "unmodeled-help-shape",
meaning: "the help text is structured in a way the grammar has no model for at all \
(topic-partitioned `--help=<topic>` pages, a combinatorial synopsis that \
reprints the tool name per variant, `KEY=VALUE` operands, a settings/variables \
table that is not a flag list, multi-column layouts). A LABEL FOR FIVE \
UNRELATED LAYOUTS, not a shape: its six labels are five tools and five \
shapes, and no detector generalizes them; see the comment above",
},
DefectFamily {
name: "wrong-stream",
meaning: "the tool wrote its real help to one stream and a banner or decorator to the \
other, and the parser read the decorator — the whole tree is built from the \
wrong bytes",
},
DefectFamily {
name: "verbatim-fallback",
meaning: "help text was captured but no structure came out of it at all, so the tool \
falls back to verbatim display",
},
DefectFamily {
name: "display-only",
meaning: "the extraction is right and the defect is in how the TUI renders it (width, \
wrapping, a truncated bracket) — recorded as not-an-extraction-defect rather \
than dropped, so it cannot be mistaken for one",
},
DefectFamily {
name: "no-usable-help",
meaning: "the tool yields no help text to parse under the allowlisted probe argv (prints \
nothing, errors, opens a REPL, or emits something that is not help) — a \
property of the tool, not of the parser",
},
];
pub fn family_names() -> Vec<&'static str> {
DEFECT_FAMILIES.iter().map(|f| f.name).collect()
}
pub fn family_meaning(name: &str) -> Option<&'static str> {
DEFECT_FAMILIES
.iter()
.find(|f| f.name == name)
.map(|f| f.meaning)
}
pub fn parse_family(word: &str) -> anyhow::Result<&'static str> {
DEFECT_FAMILIES
.iter()
.find(|f| f.name == word)
.map(|f| f.name)
.ok_or_else(|| {
anyhow::anyhow!(
"unrecognized defect family {word:?} — expected one of: {}",
family_names().join(", ")
)
})
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Amendment {
pub previous_verdict: String,
pub new_verdict: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub new_note: String,
pub reason: String,
}
pub fn amend(
entry: &mut Entry,
new_verdict: &str,
new_note: String,
reason: String,
) -> anyhow::Result<()> {
let Some(previous_verdict) = entry.effective_verdict().map(str::to_string) else {
anyhow::bail!(
"{:?} has no verdict yet — nothing to amend (record an initial verdict first, via \
`xtask audit review`/`ingest` or `mandible --review`)",
entry.tool
);
};
if reason.trim().is_empty() {
anyhow::bail!(
"amending {:?} needs a reason — an amendment with nothing recorded about why is \
exactly the unauditable change this mechanism exists to prevent",
entry.tool
);
}
if verdict_requires_note(new_verdict) && new_note.trim().is_empty() {
anyhow::bail!(
"amending {:?} to {new_verdict:?} needs a note — the same obligation an ordinary \
wrong/incomplete verdict carries, now aimed at the corrected value",
entry.tool
);
}
if previous_verdict == new_verdict {
anyhow::bail!(
"{:?} is already {new_verdict:?} (after any prior amendments) — nothing to amend",
entry.tool
);
}
entry.amendments.push(Amendment {
previous_verdict,
new_verdict: new_verdict.to_string(),
new_note,
reason,
});
Ok(())
}
#[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 validate_families(&self) -> anyhow::Result<()> {
for entry in &self.entries {
entry.validate_families()?;
}
Ok(())
}
pub fn unclassified(&self) -> impl Iterator<Item = &Entry> + '_ {
self.entries.iter().filter(|e| e.is_unclassified())
}
}
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,
spot_audit_event: None,
families: Vec::new(),
families_derived: None,
amendments: Vec::new(),
}
}
fn labelled(tool: &str, verdict: &str, families: &[&str]) -> Entry {
Entry {
families: families.iter().map(|f| f.to_string()).collect(),
families_derived: Some(true),
..entry(tool, Some(verdict), "a real finding")
}
}
#[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,
spot_audit_event: None,
families: vec!["unparsed-subcommand".to_string()],
families_derived: Some(true),
amendments: vec![Amendment {
previous_verdict: "incomplete".to_string(),
new_verdict: "wrong".to_string(),
new_note: "actually a genuine parser defect, not just unfetched help"
.to_string(),
reason: "re-read after a related tool surfaced the same shape".to_string(),
}],
},
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()),
spot_audit_event: Some("bundled-short-flag-942890d".to_string()),
families: Vec::new(),
families_derived: None,
amendments: Vec::new(),
},
],
};
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].note, "subcommand help never fetched");
assert_eq!(loaded.entries[0].k3, Some(true));
assert_eq!(loaded.entries[0].families, vec!["unparsed-subcommand"]);
assert_eq!(loaded.entries[0].families_derived, Some(true));
assert!(loaded.entries[1].families.is_empty());
assert_eq!(loaded.entries[0].amendments.len(), 1);
assert_eq!(
loaded.entries[0].amendments[0].previous_verdict,
"incomplete"
);
assert_eq!(loaded.entries[0].amendments[0].new_verdict, "wrong");
assert_eq!(loaded.entries[0].effective_verdict(), Some("wrong"));
assert_eq!(loaded.entries[1].amendments.len(), 0);
assert_eq!(
loaded.entries[1].include_reason.as_deref(),
Some("unaudited promotion")
);
assert_eq!(
loaded.entries[1].spot_audit_event.as_deref(),
Some("bundled-short-flag-942890d")
);
assert!(loaded.entries[0].spot_audit_event.is_none());
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"));
}
#[test]
fn a_manifest_with_no_amendments_field_still_loads() {
let tmp = tempfile::tempdir().unwrap();
let path = verdict_path(tmp.path(), 99);
let raw = r#"
[meta]
seed = 99
sample_size = 1
[[entry]]
tool = "tmux"
stratum = "ok"
verdict = "correct"
k1 = true
"#;
std::fs::write(&path, raw).unwrap();
let loaded = load(&path).unwrap();
assert_eq!(loaded.entries.len(), 1);
assert!(loaded.entries[0].amendments.is_empty());
assert_eq!(loaded.entries[0].effective_verdict(), Some("correct"));
assert_eq!(loaded.entries[0].effective_note(), "");
}
#[test]
fn amend_appends_history_without_touching_the_original_fields() {
let mut e = entry("tmux", Some("correct"), "");
amend(
&mut e,
"wrong",
"bundled-short-flag collapse, same shape judged wrong elsewhere".to_string(),
"reviewer missed the same defect confirmed on other tools in this review".to_string(),
)
.unwrap();
assert_eq!(e.verdict.as_deref(), Some("correct"), "original preserved");
assert_eq!(e.note, "", "original note preserved");
assert_eq!(e.effective_verdict(), Some("wrong"));
assert_eq!(
e.effective_note(),
"bundled-short-flag collapse, same shape judged wrong elsewhere"
);
assert_eq!(e.amendments.len(), 1);
assert_eq!(e.amendments[0].previous_verdict, "correct");
assert_eq!(e.amendments[0].new_verdict, "wrong");
assert!(!e.amendments[0].reason.is_empty());
}
#[test]
fn amend_refuses_a_blank_reason() {
let mut e = entry("tmux", Some("correct"), "");
let err = amend(
&mut e,
"wrong",
"a real finding".to_string(),
" ".to_string(),
)
.unwrap_err();
assert!(err.to_string().contains("reason"));
assert!(
e.amendments.is_empty(),
"a rejected amendment leaves no trace"
);
}
#[test]
fn amend_refuses_a_wrong_verdict_with_no_new_note() {
let mut e = entry("tmux", Some("correct"), "");
let err = amend(&mut e, "wrong", "".to_string(), "a real reason".to_string()).unwrap_err();
assert!(err.to_string().contains("note"));
assert!(e.amendments.is_empty());
}
#[test]
fn amend_to_correct_needs_no_note() {
let mut e = entry("openssl", Some("wrong"), "flags missing");
amend(
&mut e,
"correct",
String::new(),
"re-read against a later capture; the flags were there after all".to_string(),
)
.unwrap();
assert_eq!(e.effective_verdict(), Some("correct"));
assert_eq!(e.effective_note(), "");
}
#[test]
fn amend_refuses_an_entry_with_no_verdict_yet() {
let mut e = entry("tmux", None, "");
let err = amend(&mut e, "wrong", "note".to_string(), "reason".to_string()).unwrap_err();
assert!(err.to_string().contains("no verdict yet"));
}
#[test]
fn amend_refuses_a_no_op_amendment() {
let mut e = entry("tmux", Some("correct"), "");
let err = amend(&mut e, "correct", String::new(), "reason".to_string()).unwrap_err();
assert!(err.to_string().contains("already"));
}
#[test]
fn a_second_amendment_chains_onto_the_first() {
let mut e = entry("tmux", Some("correct"), "");
amend(
&mut e,
"wrong",
"first finding".to_string(),
"first reason".to_string(),
)
.unwrap();
amend(
&mut e,
"incomplete",
"actually just incomplete, not fully wrong".to_string(),
"reconsidered after further review".to_string(),
)
.unwrap();
assert_eq!(e.amendments.len(), 2);
assert_eq!(e.amendments[0].previous_verdict, "correct");
assert_eq!(e.amendments[0].new_verdict, "wrong");
assert_eq!(e.amendments[1].previous_verdict, "wrong");
assert_eq!(e.amendments[1].new_verdict, "incomplete");
assert_eq!(e.effective_verdict(), Some("incomplete"));
}
#[test]
fn an_amendment_round_trips_through_save_and_load() {
let tmp = tempfile::tempdir().unwrap();
let path = verdict_path(tmp.path(), 2);
let mut e = entry("tmux", Some("correct"), "");
amend(
&mut e,
"wrong",
"bundled-short-flag collapse".to_string(),
"reviewer inconsistency caught in reconciliation".to_string(),
)
.unwrap();
let file = AuditFile {
meta: AuditMeta {
seed: 2,
sample_size: 1,
},
entries: vec![e],
};
save(&path, &file).unwrap();
let loaded = load(&path).unwrap();
assert_eq!(loaded.entries[0].verdict.as_deref(), Some("correct"));
assert_eq!(loaded.entries[0].effective_verdict(), Some("wrong"));
assert_eq!(
loaded.entries[0].amendments[0].reason,
"reviewer inconsistency caught in reconciliation"
);
}
#[test]
fn a_manifest_with_no_families_field_still_loads() {
let tmp = tempfile::tempdir().unwrap();
let path = verdict_path(tmp.path(), 98);
std::fs::write(
&path,
"[meta]\nseed = 98\nsample_size = 1\n\n[[entry]]\ntool = \"tcpdump\"\nstratum = \
\"ok\"\nverdict = \"wrong\"\nnote = \"single dash issue\"\n",
)
.unwrap();
let loaded = load(&path).unwrap();
assert!(loaded.entries[0].families.is_empty());
assert_eq!(loaded.entries[0].families_derived, None);
loaded.validate_families().unwrap();
assert!(loaded.entries[0].is_unclassified());
}
#[test]
fn every_family_name_is_kebab_case_and_unique() {
let mut seen = Vec::new();
for f in DEFECT_FAMILIES {
assert!(
f.name
.chars()
.all(|c| c.is_ascii_lowercase() || c == '-' || c.is_ascii_digit()),
"{:?} is not kebab-case",
f.name
);
assert!(!f.meaning.trim().is_empty(), "{:?} has no meaning", f.name);
assert!(!seen.contains(&f.name), "{:?} listed twice", f.name);
seen.push(f.name);
}
}
#[test]
fn parse_family_accepts_the_set_and_names_it_on_failure() {
assert_eq!(
parse_family("bundled-short-flag").unwrap(),
"bundled-short-flag"
);
let err = parse_family("bundled_short_flag").unwrap_err().to_string();
assert!(err.contains("unrecognized defect family"));
assert!(
err.contains("bundled-short-flag"),
"the error must name the valid set, not just reject: {err}"
);
assert!(family_meaning("no-such-family").is_none());
}
#[test]
fn families_without_recorded_provenance_are_refused() {
let mut e = entry("tcpdump", Some("wrong"), "single dash issue");
e.families = vec!["bundled-short-flag".to_string()];
let err = e.validate_families().unwrap_err().to_string();
assert!(err.contains("families_derived"), "{err}");
e.families_derived = Some(true);
e.validate_families().unwrap();
}
#[test]
fn an_unrecognized_or_duplicated_family_is_refused() {
let mut e = labelled("tcpdump", "wrong", &["not-a-real-family"]);
assert!(e.validate_families().is_err());
e.families = vec![
"bundled-short-flag".to_string(),
"bundled-short-flag".to_string(),
];
let err = e.validate_families().unwrap_err().to_string();
assert!(err.contains("twice"), "{err}");
}
#[test]
fn a_correct_verdict_may_not_carry_family_labels() {
let e = labelled("tmux", "correct", &["bundled-short-flag"]);
let err = e.validate_families().unwrap_err().to_string();
assert!(err.contains("names no defect"), "{err}");
}
#[test]
fn an_amended_verdict_decides_whether_labels_are_allowed() {
let mut e = labelled("tmux", "correct", &["bundled-short-flag"]);
assert!(e.validate_families().is_err());
e.verdict = Some("correct".to_string());
amend(
&mut e,
"wrong",
"bundled-short-flag collapse".to_string(),
"reviewer inconsistency caught in reconciliation".to_string(),
)
.unwrap();
e.validate_families().unwrap();
assert!(e.is_judged_defect());
assert!(!e.is_judged_correct());
assert!(e.has_family("bundled-short-flag"));
assert!(!e.is_unclassified());
}
#[test]
fn skip_is_neither_a_judged_defect_nor_a_judged_correct() {
let e = entry("xzgrep", Some("skip"), "");
assert!(!e.is_judged_defect());
assert!(!e.is_judged_correct());
assert!(!e.is_unclassified());
}
#[test]
fn is_display_only_true_for_a_pure_display_only_verdict() {
let e = labelled("pcre2-config", "wrong", &["display-only"]);
assert!(e.is_display_only());
assert!(e.is_judged_defect());
assert!(!e.is_unclassified());
}
#[test]
fn is_display_only_false_when_a_real_family_rides_along() {
let e = labelled("tcpdump", "wrong", &["bundled-short-flag", "display-only"]);
assert!(!e.is_display_only());
}
#[test]
fn is_display_only_false_off_a_judged_defect() {
assert!(!labelled("tmux", "correct", &[]).is_display_only());
assert!(!entry("xzgrep", Some("skip"), "").is_display_only());
assert!(!entry("fresh", None, "").is_display_only());
}
#[test]
fn unclassified_lists_judged_defects_with_no_label() {
let file = AuditFile {
meta: AuditMeta {
seed: 2,
sample_size: 4,
},
entries: vec![
labelled("tcpdump", "wrong", &["bundled-short-flag"]),
entry("pptpsetup", Some("incomplete"), "bad parse"),
entry("wall", Some("correct"), ""),
entry("xzgrep", Some("skip"), ""),
],
};
file.validate_families().unwrap();
let names: Vec<&str> = file.unclassified().map(|e| e.tool.as_str()).collect();
assert_eq!(names, vec!["pptpsetup"]);
}
}