#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Counts {
Graded {
major: u32,
moderate: u32,
minor: u32,
},
Advisory {
findings: u32,
},
}
pub struct Verdict {
pub reviewer: String,
pub counts: Counts,
pub token: String,
pub resets: u32,
pub session: String,
}
impl Counts {
pub fn major(self) -> u32 {
match self {
Counts::Graded { major, .. } => major,
Counts::Advisory { .. } => 0,
}
}
pub fn render(self) -> String {
match self {
Counts::Graded {
major,
moderate,
minor,
} => format!("major={major} moderate={moderate} minor={minor}"),
Counts::Advisory { findings } => format!("findings={findings}"),
}
}
}
impl Verdict {
pub fn blocks(&self) -> bool {
self.counts.major() > 0
}
}
pub fn total(verdicts: &[Verdict]) -> Counts {
verdicts
.iter()
.map(|v| v.counts)
.reduce(|a, b| match (a, b) {
(
Counts::Graded {
major,
moderate,
minor,
},
Counts::Graded {
major: m,
moderate: d,
minor: n,
},
) => Counts::Graded {
major: major.saturating_add(m),
moderate: moderate.saturating_add(d),
minor: minor.saturating_add(n),
},
(Counts::Advisory { findings }, Counts::Advisory { findings: f }) => Counts::Advisory {
findings: findings.saturating_add(f),
},
(first, _) => first,
})
.unwrap_or(Counts::Graded {
major: 0,
moderate: 0,
minor: 0,
})
}
pub fn counts_shape(simple: bool) -> &'static str {
if simple {
"findings=<n>"
} else {
"major=<n> moderate=<n> minor=<n>"
}
}
pub fn key_for(gate: &str) -> String {
format!("Reviewed-{gate}")
}
fn counts_from(slots: [Option<u32>; 4]) -> Result<Counts, String> {
match slots {
[Some(major), Some(moderate), Some(minor), None] => Ok(Counts::Graded {
major,
moderate,
minor,
}),
[None, None, None, Some(findings)] => Ok(Counts::Advisory { findings }),
[None, None, None, None] => {
Err("no counts: major=/moderate=/minor=, or findings=".to_string())
}
_ => Err(
"counts are either major=, moderate= and minor= together, or findings= alone"
.to_string(),
),
}
}
fn slot_of(name: &str) -> Option<usize> {
match name {
"major" => Some(0),
"moderate" => Some(1),
"minor" => Some(2),
"findings" => Some(3),
"resets" => Some(4),
_ => None,
}
}
struct Fields {
reviewer: Option<String>,
token: Option<String>,
numbers: [Option<u32>; 5],
}
fn read_field(f: &mut Fields, field: &str) -> Result<(), String> {
let (name, raw) = field
.split_once('=')
.ok_or_else(|| format!("field '{field}' is not name=value"))?;
let taken = match name {
"reviewer" => f.reviewer.is_some(),
"token" => f.token.is_some(),
other => slot_of(other).is_some_and(|s| f.numbers[s].is_some()),
};
if taken {
return Err(format!("{name}= is given more than once"));
}
match name {
"reviewer" => f.reviewer = Some(raw.to_string()),
"token" => f.token = Some(raw.to_string()),
other => {
let slot = slot_of(other).ok_or_else(|| format!("unknown field '{other}'"))?;
let value = raw
.parse()
.map_err(|_| format!("{name}={raw} is not a number"))?;
f.numbers[slot] = Some(value);
}
}
Ok(())
}
fn parse_value(value: &str) -> Result<Verdict, String> {
let mut fields = Fields {
reviewer: None,
token: None,
numbers: [None; 5],
};
for field in value.split_whitespace() {
read_field(&mut fields, field)?;
}
let reviewer = fields
.reviewer
.filter(|r| !r.is_empty())
.ok_or("no reviewer= named")?;
let token = fields
.token
.filter(|t| !t.is_empty())
.ok_or("no token=: it is issued by `git agent-verdict attest`")?;
let [major, moderate, minor, findings, resets] = fields.numbers;
Ok(Verdict {
reviewer,
counts: counts_from([major, moderate, minor, findings])?,
token,
resets: resets.unwrap_or(0),
session: String::new(),
})
}
pub fn parse_for(gate: &str, block: &str) -> Result<Vec<Verdict>, String> {
let key = key_for(gate);
let mut verdicts = Vec::new();
for line in block.lines() {
let Some(value) = line.strip_prefix(&key).and_then(|r| r.strip_prefix(':')) else {
continue;
};
verdicts.push(parse_value(value.trim()).map_err(|e| format!("{key}: {e}"))?);
}
Ok(verdicts)
}
pub fn render(gate: &str, verdict: &Verdict) -> String {
let counts = verdict.counts.render();
let resets = match verdict.resets {
0 => String::new(),
n => format!(" resets={n}"),
};
let key = key_for(gate);
format!(
"{key}: reviewer={} {counts} token={}{resets}",
verdict.reviewer, verdict.token
)
}
pub fn is_agent_coauthor(line: &str) -> bool {
let Some((key, value)) = line.split_once(':') else {
return false;
};
key.eq_ignore_ascii_case("co-authored-by") && value.contains("@anthropic.com")
}
pub fn present_but_unparsed(gate: &str, raw: &str, block: &str) -> bool {
let key = key_for(gate);
raw.lines().any(|l| l.trim_start().starts_with(&key)) && !block.contains(&key)
}
#[cfg(test)]
mod tests {
use super::*;
fn one(block: &str) -> Result<Vec<Verdict>, String> {
parse_for("standards", block)
}
#[test]
fn a_repeated_count_cannot_bury_a_blocker() {
let line = "Reviewed-standards: reviewer=opus major=1 major=0 moderate=0 minor=0 token=ab";
assert!(one(line).is_err());
}
#[test]
fn the_two_count_shapes_cannot_be_mixed() {
let line =
"Reviewed-standards: reviewer=opus major=0 moderate=0 minor=0 findings=2 token=ab";
assert!(one(line).is_err());
}
#[test]
fn another_gates_trailer_is_not_this_gates() {
let line = "Reviewed-prose: reviewer=opus major=0 moderate=0 minor=0 token=ab";
assert!(one(line).unwrap().is_empty());
}
#[test]
fn a_trailer_outside_the_trailing_paragraph_is_detected() {
let raw = "subject\n\nReviewed-standards: reviewer=opus major=0 moderate=0 minor=0 token=ab\n\nbody\n";
assert!(present_but_unparsed("standards", raw, ""));
}
}