Watch
0
0
Fork
You've already forked hyperhive
0

review classifier: the review table shows a pass/fail symbol, certainty against threshold, and an emoji on the overall line

The review body's table is now: result | check | certainty / threshold |
reason. The result cell is a symbol for what the row did to the review,
after polarity (✅ counted and passed, ❌ counted and failed, ❔ not
counted or no answer), then the model's verdict. The overall line carries
✅ / ❌. Polarity, threshold and counted are folded into those cells.

The prompt asks for a reason under 350 characters; the enforced cap
stays 500, so an overshoot is truncated less often.
This commit is contained in:
atlas 2026-10-14 23:31:46 +02:00 • committed by mara
commit 731cef4c2d
5 changed files with 172 additions and 43 deletions

View file

@ -681,13 +681,13 @@ fn review_for(outcome: &Outcome) -> Ruling {
);
Ruling::no_verdict(reason, rows)
} else {
let (event, said) = match overall {
Overall::Approve => (Event::Approve, "approve"),
Overall::RequestChanges => (Event::RequestChanges, "request changes"),
let event = match overall {
Overall::Approve => Event::Approve,
Overall::RequestChanges => Event::RequestChanges,
};
Ruling {
event,
body: format!("{}\n**Overall: {said}.**\n", table(rows)),
body: format!("{}\n{}", table(rows), overall_line(*overall)),
reason: None,
}
}
@ -711,32 +711,18 @@ fn no_verdict(reason: &str, rows: &[Row]) -> String {
/// The result table, one row per classification.
fn table(rows: &[Row]) -> String {
let mut out = String::from(
"| classification | polarity | verdict | certainty | threshold | counted | reason |\n\
| --- | --- | --- | --- | --- | --- | --- |\n",
"| result | check | certainty / threshold | reason |\n\
| --- | --- | --- | --- |\n",
);
for r in rows {
let polarity = match r.polarity {
swarm_queue_client::review_classifier::Polarity::Positive => "positive",
swarm_queue_client::review_classifier::Polarity::Negative => "negative",
};
let (verdict, certainty, counted, reason) = if let Some(error) = &r.error {
("no answer", String::new(), "no", error.as_str())
} else {
let verdict = match r.verdict {
Verdict::Match => "match",
Verdict::NoMatch => "no match",
Verdict::NotApplicable => "not applicable",
};
let counted = match (r.counted, r.passed) {
(false, _) => "no",
(true, true) => "yes ✅",
(true, false) => "yes ❌",
};
(verdict, r.certainty.to_string(), counted, r.reason.as_str())
let (certainty, reason) = match &r.error {
Some(error) => ("—".to_owned(), error.as_str()),
None => (r.certainty.to_string(), r.reason.as_str()),
};
let _ = writeln!(
out,
"| {} | {polarity} | {verdict} | {certainty} | {} | {counted} | {} |",
"| {} | {} | {certainty} / {} | {} |",
result(r),
cell(&r.name),
r.threshold,
cell(reason)
@ -745,6 +731,32 @@ fn table(rows: &[Row]) -> String {
out
}
/// A row's result cell: the symbol for what the row did to the review, then
/// the model's verdict. ✅ is a counted row that passed its polarity, ❌ a
/// counted row that failed it, ❔ a row that doesn't count: below its
/// threshold, not applicable, or without an answer. So `match` is ✅ under a
/// positive rule and ❌ under a negative one.
fn result(r: &Row) -> &'static str {
match (r.error.is_some(), r.counted, r.passed, r.verdict) {
(true, ..) => "❔ no answer",
(_, true, true, Verdict::Match) => "✅ match",
(_, true, true, Verdict::NoMatch) => "✅ no match",
(_, true, false, Verdict::Match) => "❌ match",
(_, true, false, Verdict::NoMatch) => "❌ no match",
(_, _, _, Verdict::Match) => "❔ match",
(_, _, _, Verdict::NoMatch) => "❔ no match",
(_, _, _, Verdict::NotApplicable) => "❔ unsure",
}
}
/// The closing line of a review: the overall verdict, with its symbol.
fn overall_line(overall: Overall) -> &'static str {
match overall {
Overall::Approve => "**Overall: ✅ approve**\n",
Overall::RequestChanges => "**Overall: ❌ request changes**\n",
}
}
/// `text` as one table cell's inline text: on one line, with every character
/// that could end the cell, start markup, math (`$`) or raw HTML escaped, and a zero-width
/// space after every `@`, `#`, `!`, `://` and `www.`, so it carries no live
@ -834,6 +846,97 @@ mod tests {
}
}
/// A row as the aggregation produces it for `polarity`, `verdict`,
/// `certainty` against `threshold`.
fn answered(polarity: Polarity, verdict: Verdict, certainty: u8, threshold: u8) -> Row {
let counted = verdict != Verdict::NotApplicable && certainty >= threshold;
let wanted = match polarity {
Polarity::Positive => Verdict::Match,
Polarity::Negative => Verdict::NoMatch,
};
Row {
name: "no-ads".to_owned(),
polarity,
verdict,
certainty,
threshold,
counted,
passed: counted && verdict == wanted,
reason: "Because.".to_owned(),
error: None,
}
}
#[test]
fn a_row_shows_whether_it_passed_not_the_raw_verdict() {
use Polarity::{Negative, Positive};
use Verdict::{Match, NoMatch, NotApplicable};
for (case, r, expected) in [
(
"positive match passes",
answered(Positive, Match, 85, 65),
"| ✅ match | no-ads | 85 / 65 | Because. |",
),
(
"negative match fails",
answered(Negative, Match, 85, 65),
"| ❌ match | no-ads | 85 / 65 | Because. |",
),
(
"negative no match passes",
answered(Negative, NoMatch, 97, 80),
"| ✅ no match | no-ads | 97 / 80 | Because. |",
),
(
"positive no match fails",
answered(Positive, NoMatch, 97, 80),
"| ❌ no match | no-ads | 97 / 80 | Because. |",
),
(
"not applicable is unsure",
answered(Negative, NotApplicable, 98, 70),
"| ❔ unsure | no-ads | 98 / 70 | Because. |",
),
(
"below the threshold does not count",
answered(Positive, Match, 41, 60),
"| ❔ match | no-ads | 41 / 60 | Because. |",
),
(
"no answer shows its error",
Row {
error: Some("timed out".to_owned()),
verdict: NotApplicable,
certainty: 0,
counted: false,
passed: false,
reason: String::new(),
..answered(Positive, Match, 0, 65)
},
"| ❔ no answer | no-ads | — / 65 | timed out |",
),
] {
let body = table(&[r]);
assert_eq!(
body,
format!(
"| result | check | certainty / threshold | reason |\n\
| --- | --- | --- | --- |\n{expected}\n"
),
"{case}"
);
}
}
#[test]
fn the_overall_line_carries_its_emoji() {
assert_eq!(overall_line(Overall::Approve), "**Overall: ✅ approve**\n");
assert_eq!(
overall_line(Overall::RequestChanges),
"**Overall: ❌ request changes**\n"
);
}
fn request(name: &str) -> Requested {
Requested {
owner: "o".to_owned(),
@ -1437,7 +1540,7 @@ mod tests {
assert_eq!(commit.as_deref(), Some("abc123"));
assert_eq!(review.event, "APPROVED");
assert!(
review.body.ends_with("**Overall: approve.**\n"),
review.body.ends_with("**Overall: ✅ approve**\n"),
"{}",
review.body
);
@ -1451,7 +1554,11 @@ mod tests {
)
.await;
assert_eq!(review.event, "REQUEST_CHANGES");
assert!(review.body.contains("| yes ❌ |"), "{}", review.body);
assert!(
review.body.contains("| ❌ match | datetime | 90 / 80 |"),
"{}",
review.body
);
}
#[tokio::test]
@ -1499,12 +1606,12 @@ mod tests {
assert_eq!(body.lines().count(), 3, "{body}");
assert!(!line.contains("@mara"), "{line}");
assert!(line.contains("@\u{200b}mara"), "{line}");
// Seven cells: eight unescaped pipes.
// Four cells: five unescaped pipes.
let unescaped = line
.char_indices()
.filter(|(i, c)| *c == '|' && (*i == 0 || line.as_bytes()[i - 1] != b'\\'))
.count();
assert_eq!(unescaped, 8, "{line}");
assert_eq!(unescaped, 5, "{line}");
assert!(
line.contains("\\!\u{200b}\\[\\]") && line.contains("\\<b\\>"),
"{line}"
@ -1531,7 +1638,7 @@ mod tests {
.char_indices()
.filter(|(i, c)| *c == '|' && (*i == 0 || line.as_bytes()[i - 1] != b'\\'))
.count();
assert_eq!(unescaped, 8, "{line}");
assert_eq!(unescaped, 5, "{line}");
}
#[test]