use crate::corpus;
use crate::rules::{Rule, Stance};
const MIN_REVIEWED: usize = 12;
const MIN_NEGATIVES: usize = 4;
pub struct Verdict {
pub allowed: bool,
pub lines: Vec<(bool, String)>,
}
pub fn assess(rule: &Rule, to: Stance) -> Verdict {
assess_score(rule, to, &corpus::score(rule))
}
pub fn assess_score(rule: &Rule, to: Stance, score: &corpus::Score) -> Verdict {
let mut lines = Vec::new();
let mut allowed = true;
if to <= rule.default_stance && to == Stance::Observe {
return Verdict {
allowed: true,
lines,
};
}
let enough = score.reviewed >= MIN_REVIEWED;
lines.push((
enough,
format!("{} reviewed cases (need {MIN_REVIEWED})", score.reviewed),
));
allowed &= enough;
let negatives = score.negatives >= MIN_NEGATIVES;
lines.push((
negatives,
format!(
"{} of them are expected-negatives (need {MIN_NEGATIVES})",
score.negatives
),
));
allowed &= negatives;
let agrees = score.agrees();
lines.push((
agrees,
if agrees {
"the rule agrees with every judgement".to_string()
} else {
format!(
"{} judgement(s) the rule now disagrees with — run `amont-agent corpus check`",
score.disagreements.len()
)
},
));
allowed &= agrees;
lines.push((
true,
match score.precision() {
Some(p) => format!(
"precision {:.0}% over the reviewed set; measured {:.1} firings per 1000 calls",
p * 100.0,
rule.evidence.per_1000
),
None => format!(
"precision unmeasured; measured {:.1} firings per 1000 calls",
rule.evidence.per_1000
),
},
));
Verdict { allowed, lines }
}
pub fn set(rule: &Rule, to: Stance) -> Result<(), String> {
let key = crate::stance::key_for(rule);
let out = std::process::Command::new("git")
.args(["config", "--global", &key, to.as_str()])
.output()
.map_err(|e| format!("could not run git: {e}"))?;
if out.status.success() {
Ok(())
} else {
Err(String::from_utf8_lossy(&out.stderr).trim().to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::corpus::Verdict as CaseVerdict;
fn score_of(rule: &Rule, text: &str) -> corpus::Score {
corpus::score_cases(rule, &corpus::parse(text))
}
fn cases(matches: usize, negatives: usize) -> String {
let mut s = String::new();
for _ in 0..matches {
s.push_str(&corpus::line_for(
CaseVerdict::Match,
"git push origin main | tail -1",
));
}
for _ in 0..negatives {
s.push_str(&corpus::line_for(
CaseVerdict::NoMatch,
"git status --short",
));
}
s
}
#[test]
fn an_unreviewed_rule_cannot_be_promoted() {
let rule = crate::rules::by_id("bare-stash-pop").unwrap();
let score = corpus::score_cases(rule, &corpus::parse(""));
assert_eq!(score.reviewed, 0);
let v = assess_score(rule, Stance::Deny, &score);
assert!(!v.allowed);
}
#[test]
fn positives_without_negatives_are_not_enough() {
let rule = crate::rules::by_id("pipe-to-tail").unwrap();
let v = assess_score(rule, Stance::Deny, &score_of(rule, &cases(20, 0)));
assert!(!v.allowed, "twenty positives and no negatives passed");
assert!(v.lines.iter().any(|(ok, t)| !ok && t.contains("negatives")));
}
#[test]
fn a_reviewed_rule_that_still_agrees_may_be_promoted() {
let rule = crate::rules::by_id("pipe-to-tail").unwrap();
let v = assess_score(rule, Stance::Deny, &score_of(rule, &cases(12, 4)));
assert!(
v.allowed,
"{:?}",
v.lines.iter().map(|l| &l.1).collect::<Vec<_>>()
);
}
#[test]
fn a_disagreement_blocks_promotion() {
let rule = crate::rules::by_id("pipe-to-tail").unwrap();
let mut text = cases(12, 4);
text.push_str(&corpus::line_for(
CaseVerdict::NoMatch,
"git push origin main 2>&1 | tail -5",
));
let v = assess_score(rule, Stance::Deny, &score_of(rule, &text));
assert!(!v.allowed);
assert!(v.lines.iter().any(|(ok, t)| !ok && t.contains("disagrees")));
}
#[test]
fn demotion_needs_no_evidence() {
let rule = crate::rules::by_id("pipe-to-tail").unwrap();
let v = assess_score(rule, Stance::Observe, &score_of(rule, ""));
assert!(v.allowed);
}
}