use crate::types::{CheckResult, CheckStatus};
pub const SIGNAL_CHECK_IDS: [&str; 4] = [
"p1-non-interactive",
"p2-json-output",
"p7-quiet",
"p6-no-color-behavioral",
];
pub fn classify(results: &[CheckResult]) -> Option<String> {
let mut warns = 0usize;
let mut matched = 0usize;
for signal_id in SIGNAL_CHECK_IDS {
debug_assert!(
results.iter().filter(|r| r.id == signal_id).count() <= 1,
"duplicate signal check ID in results[]: {signal_id}. \
Every behavioral check ID must be unique across the catalog; \
classify() uses iter().find() and would silently ignore the duplicate.",
);
let Some(r) = results.iter().find(|r| r.id == signal_id) else {
continue;
};
if is_audit_profile_suppression(&r.status) {
continue;
}
matched += 1;
if matches!(r.status, CheckStatus::Warn(_)) {
warns += 1;
}
}
if matched < SIGNAL_CHECK_IDS.len() {
return None;
}
Some(
match warns {
0..=1 => "agent-optimized",
2 => "mixed",
_ => "human-primary",
}
.to_string(),
)
}
pub fn classify_reason(results: &[CheckResult]) -> Option<&'static str> {
let mut any_suppressed = false;
let mut any_missing = false;
for signal_id in SIGNAL_CHECK_IDS {
match results.iter().find(|r| r.id == signal_id) {
None => any_missing = true,
Some(r) if is_audit_profile_suppression(&r.status) => any_suppressed = true,
Some(_) => {}
}
}
if !any_suppressed && !any_missing {
return None;
}
if any_suppressed {
Some("suppressed")
} else {
Some("insufficient_signal")
}
}
pub(crate) fn is_audit_profile_suppression(status: &CheckStatus) -> bool {
matches!(
status,
CheckStatus::Skip(e) if e.starts_with(crate::principles::registry::SUPPRESSION_EVIDENCE_PREFIX)
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::principles::registry;
use crate::types::{CheckGroup, CheckLayer, CheckResult, CheckStatus, Confidence};
fn signal_result(id: &str, status: CheckStatus) -> CheckResult {
CheckResult {
id: id.to_string(),
label: format!("Signal: {id}"),
group: CheckGroup::P1,
layer: CheckLayer::Behavioral,
status,
confidence: Confidence::High,
}
}
fn all_pass() -> Vec<CheckResult> {
SIGNAL_CHECK_IDS
.iter()
.map(|id| signal_result(id, CheckStatus::Pass))
.collect()
}
#[test]
fn four_passes_yields_agent_optimized() {
assert_eq!(classify(&all_pass()).as_deref(), Some("agent-optimized"));
}
#[test]
fn four_warns_yields_human_primary() {
let results: Vec<_> = SIGNAL_CHECK_IDS
.iter()
.map(|id| signal_result(id, CheckStatus::Warn("missing".into())))
.collect();
assert_eq!(classify(&results).as_deref(), Some("human-primary"));
}
#[test]
fn two_warns_yields_mixed() {
let mut results = all_pass();
results[0].status = CheckStatus::Warn("x".into());
results[1].status = CheckStatus::Warn("y".into());
assert_eq!(classify(&results).as_deref(), Some("mixed"));
}
#[test]
fn one_warn_yields_agent_optimized() {
let mut results = all_pass();
results[2].status = CheckStatus::Warn("soft".into());
assert_eq!(classify(&results).as_deref(), Some("agent-optimized"));
}
#[test]
fn three_warns_yields_human_primary() {
let mut results = all_pass();
results[0].status = CheckStatus::Warn("a".into());
results[1].status = CheckStatus::Warn("b".into());
results[2].status = CheckStatus::Warn("c".into());
assert_eq!(classify(&results).as_deref(), Some("human-primary"));
}
#[test]
fn missing_one_signal_returns_none() {
let results: Vec<_> = SIGNAL_CHECK_IDS[..3]
.iter()
.map(|id| signal_result(id, CheckStatus::Pass))
.collect();
assert!(classify(&results).is_none());
}
#[test]
fn organic_skipped_signal_counts_as_not_warn() {
let mut results = all_pass();
results[3].status = CheckStatus::Skip("no flags".into());
assert_eq!(classify(&results).as_deref(), Some("agent-optimized"));
}
#[test]
fn audit_profile_suppressed_signal_drops_denominator() {
let mut results = all_pass();
results[0].status = CheckStatus::Skip(format!(
"{}human-tui",
crate::principles::registry::SUPPRESSION_EVIDENCE_PREFIX
));
assert!(
classify(&results).is_none(),
"audit_profile-suppressed signal should drop denominator and force None",
);
}
#[test]
fn errored_signal_counts_as_not_warn() {
let mut results = all_pass();
results[0].status = CheckStatus::Error("runner crashed".into());
assert_eq!(classify(&results).as_deref(), Some("agent-optimized"));
}
#[test]
fn classify_reason_none_when_audience_has_label() {
let results = all_pass();
assert!(classify(&results).is_some());
assert_eq!(classify_reason(&results), None);
}
#[test]
fn classify_reason_suppressed_when_signal_audit_profile_suppressed() {
let mut results = all_pass();
results[0].status = CheckStatus::Skip(format!(
"{}human-tui",
crate::principles::registry::SUPPRESSION_EVIDENCE_PREFIX
));
assert!(classify(&results).is_none());
assert_eq!(classify_reason(&results), Some("suppressed"));
}
#[test]
fn classify_reason_insufficient_when_signal_missing() {
let results: Vec<CheckResult> = Vec::new();
assert!(classify(&results).is_none());
assert_eq!(classify_reason(&results), Some("insufficient_signal"));
}
#[test]
fn classify_reason_suppressed_dominates_missing() {
let results: Vec<CheckResult> = SIGNAL_CHECK_IDS
.iter()
.take(3) .enumerate()
.map(|(i, id)| {
let status = if i == 0 {
CheckStatus::Skip(format!(
"{}human-tui",
crate::principles::registry::SUPPRESSION_EVIDENCE_PREFIX
))
} else {
CheckStatus::Pass
};
signal_result(id, status)
})
.collect();
assert_eq!(classify_reason(&results), Some("suppressed"));
}
#[test]
#[should_panic(expected = "duplicate signal check ID")]
fn duplicate_signal_in_results_trips_debug_assert() {
let mut results = all_pass();
results.push(signal_result(SIGNAL_CHECK_IDS[0], CheckStatus::Pass));
classify(&results);
}
#[test]
fn non_signal_results_are_ignored() {
let mut results = all_pass();
results.push(signal_result("p6-sigpipe", CheckStatus::Warn("x".into())));
results.push(signal_result("p4-bad-args", CheckStatus::Warn("y".into())));
assert_eq!(classify(&results).as_deref(), Some("agent-optimized"));
}
#[test]
fn empty_results_returns_none() {
assert!(classify(&[]).is_none());
}
#[test]
fn signal_check_ids_are_present_in_behavioral_catalog() {
use crate::check::Check;
use crate::checks::behavioral::all_behavioral_checks;
let behavioral: Vec<Box<dyn Check>> = all_behavioral_checks();
let behavioral_ids: Vec<&str> = behavioral.iter().map(|c| c.id()).collect();
for signal_id in SIGNAL_CHECK_IDS {
assert!(
behavioral_ids.contains(&signal_id),
"signal check `{signal_id}` is missing from the behavioral catalog — \
a rename or removal broke the audience classifier. Update \
`SIGNAL_CHECK_IDS` in `src/scorecard/audience.rs` to match.",
);
}
}
#[test]
fn signal_ids_reference_known_principles() {
for signal_id in SIGNAL_CHECK_IDS {
let prefix = signal_id
.split_once('-')
.map(|(p, _)| p)
.expect("signal id has pN- prefix");
let hits = registry::REQUIREMENTS
.iter()
.filter(|r| format!("p{}", r.principle) == prefix)
.count();
assert!(
hits > 0,
"signal check `{signal_id}` has principle prefix `{prefix}` \
but no registry requirements match — classifier will \
silently drift from spec. Fix `SIGNAL_CHECK_IDS`.",
);
}
}
}