metrale_bench/benchmarks/vision/score.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: The vision benchmark's cells and verdict. A run whose no-image
4//! control did not hold is VACUOUS, whatever the other cells say.
5//!
6//! Owner: bench, vision.
7//! Invariants: none beyond the types.
8
9use std::fmt;
10
11/// 2026-09-26: How one geometry cell came out.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum GeomCell {
14 /// 2026-09-26: The measured vision tokens equal the prediction.
15 Match {
16 fixture: &'static str,
17 tokens: usize,
18 },
19 /// 2026-09-26: The measured vision tokens differ from the prediction.
20 Mismatch {
21 fixture: &'static str,
22 want: usize,
23 got: usize,
24 },
25 /// 2026-09-26: Not asserted: the server refused the image as over its
26 /// encoder's capacity.
27 Unmeasured { fixture: &'static str, why: String },
28 /// 2026-09-26: The request, or the vision-token subtraction, failed.
29 Error { fixture: &'static str, msg: String },
30}
31
32/// 2026-09-26: How one capability probe came out.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum ProbeCell {
35 Pass { id: &'static str },
36 Fail { id: &'static str, reply: String },
37 Error { id: &'static str, msg: String },
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum Verdict {
42 Pass,
43 Fail,
44 /// 2026-09-26: The no-image control answered as though it saw an image, so
45 /// the capability probes are not evidence.
46 Vacuous,
47}
48
49impl fmt::Display for Verdict {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 f.write_str(match self {
52 Verdict::Pass => "PASS",
53 Verdict::Fail => "FAIL",
54 Verdict::Vacuous => "VACUOUS",
55 })
56 }
57}
58
59/// 2026-09-26: Does a reply satisfy a probe's expectations?
60///
61/// Case-insensitive term matching. A numeric term matches where no digit
62/// touches it; any other term must stand alone as a word
63/// (`first_standalone_term`). Every `want_all` term must match and no
64/// `want_none` term may.
65pub fn reply_matches(reply: &str, want_all: &[&str], want_none: &[&str]) -> bool {
66 let hay = reply.to_lowercase();
67 let contains_term = |term: &str| {
68 if !term.is_empty() && term.chars().all(|ch| ch.is_ascii_digit()) {
69 // 2026-09-26: The HD fixture's label is `07_hd_1280x720 1280x720`
70 // (`scripts/gen_test_images.py`), where letters and `_` touch 1280.
71 hay.match_indices(term).any(|(at, _)| {
72 !hay[..at]
73 .chars()
74 .next_back()
75 .is_some_and(|ch| ch.is_ascii_digit())
76 && !hay[at + term.len()..]
77 .chars()
78 .next()
79 .is_some_and(|ch| ch.is_ascii_digit())
80 })
81 } else {
82 crate::benchmarks::first_standalone_term(&hay, term).is_some()
83 }
84 };
85 want_all.iter().all(|term| contains_term(term))
86 && !want_none.iter().any(|term| contains_term(term))
87}
88
89/// 2026-09-26: Fold the geometry and probe cells into one verdict: Vacuous
90/// when the control did not hold, whatever else happened; otherwise Fail on
91/// any geometry `Mismatch` or `Error` or any probe that did not pass; otherwise
92/// Pass. `Unmeasured` cells do not fail the run.
93pub fn verdict(geom: &[GeomCell], probes: &[ProbeCell], control_held: bool) -> Verdict {
94 let geom_bad = geom
95 .iter()
96 .any(|c| matches!(c, GeomCell::Mismatch { .. }) || matches!(c, GeomCell::Error { .. }));
97 let probes_bad = probes.iter().any(|c| !matches!(c, ProbeCell::Pass { .. }));
98
99 if !control_held {
100 return Verdict::Vacuous;
101 }
102 if geom_bad || probes_bad {
103 return Verdict::Fail;
104 }
105 Verdict::Pass
106}
107
108/// 2026-09-26: Fold the integrity and concurrency legs into the verdict: a
109/// Pass becomes Fail when an integrity leg failed or the concurrency sweep is
110/// not clean. Fail and Vacuous stay as they are.
111pub fn with_runtime_checks(
112 base: Verdict,
113 integrity_failed: bool,
114 concurrency_clean: bool,
115) -> Verdict {
116 match base {
117 Verdict::Pass if integrity_failed || !concurrency_clean => Verdict::Fail,
118 other => other,
119 }
120}
121
122/// 2026-09-26: How many geometry cells asserted something (`Match` or
123/// `Mismatch`). Reported beside the verdict, because a run where every cell was
124/// `Unmeasured` passes `verdict`.
125pub fn asserted_cells(geom: &[GeomCell]) -> usize {
126 geom.iter()
127 .filter(|c| matches!(c, GeomCell::Match { .. } | GeomCell::Mismatch { .. }))
128 .count()
129}
130
131#[cfg(test)]
132#[path = "score_tests.rs"]
133mod score_tests;