1use std::collections::BTreeMap;
14use std::path::{Path, PathBuf};
15
16use super::check::{
17 GateStatus, record_is_for, record_is_required_subject, record_still_stands,
18 records_newest_first,
19};
20use super::group::ShardRecord;
21use super::record::{GateBaseline, GateRecord, read_baseline, read_record};
22
23fn standing_shards(
27 root: &Path,
28 baseline: &GateBaseline,
29 group: &'static str,
30 sha: &str,
31 gate: &super::coverage::GateCoverage,
32) -> Vec<(GateRecord, PathBuf)> {
33 records_newest_first(root, group)
34 .into_iter()
35 .filter_map(|path| {
36 let r = read_record(&path).ok()?;
37 (r.shard().is_some()
38 && record_is_for(&r, group, &path)
39 && record_is_required_subject(baseline, &r, group, &path)
40 && record_still_stands(root, sha, &r, gate))
41 .then_some((r, path))
42 })
43 .collect()
44}
45
46fn shard_problems(root: &Path, label: &str, record: &GateRecord, path: &Path) -> Vec<String> {
50 let mut problems = Vec::new();
51 if record.frame_status_failed() {
52 problems.push(format!(
53 "{label}: the run itself failed: {}",
54 record.verdict_reason
55 ));
56 }
57 if !record.dirty_paths.is_empty() {
58 problems.push(format!(
59 "{label}: measured from a dirty tree — {} uncommitted invalidation-set \
60 file(s) when the run started ({}), so the binary was not {}",
61 record.dirty_paths.len(),
62 record.dirty_paths.join(", "),
63 record.git_sha
64 ));
65 }
66 if let Err(why) = super::signing::verify_record(root, path, &record.git_sha, record.recorded_at)
67 {
68 problems.push(format!("{label}: {why}"));
69 }
70 problems
71}
72
73fn label(group: &str, shard: (usize, usize)) -> String {
74 format!("{group}[{}/{}]", shard.0, shard.1)
75}
76
77pub(super) fn check_group(
81 root: &Path,
82 group: &'static super::group::BenchmarkGroup,
83 sha: &str,
84) -> GateStatus {
85 use crate::benchmarks::bfcl::aggregate;
86
87 let baseline = match read_baseline(root, group.id) {
88 Ok(b) => b,
89 Err(e) => return GateStatus::Missing(format!("baseline unreadable: {e:#}")),
90 };
91 let Some(gate) = super::coverage::find(group.id) else {
92 return GateStatus::Missing(format!("{} has no coverage entry", group.id));
93 };
94
95 let candidates = standing_shards(root, &baseline, group.id, sha, gate);
96 let shards: Vec<ShardRecord> = candidates
97 .iter()
98 .enumerate()
99 .filter_map(|(handle, (r, _))| {
100 let (index, count) = r.shard()?;
101 Some(ShardRecord {
102 index,
103 count,
104 git_sha: r.git_sha.clone(),
105 recorded_at: r.recorded_at,
106 handle,
107 })
108 })
109 .collect();
110 let partition = match super::group::select_partition(group.id, &shards) {
111 Ok(p) => p,
112 Err(fault) => {
113 let mut why = vec![fault.to_string()];
114 if let Some(note) = whole_draw_note(root, group) {
115 why.push(note);
116 }
117 if let Some(note) = why_stale(root, group.id, sha, gate) {
118 why.push(note);
119 }
120 return GateStatus::Missing(why.join(" "));
121 }
122 };
123
124 let count = partition.count;
125 let mut tallies: Vec<BTreeMap<String, aggregate::Tally>> = Vec::new();
126 let mut problems: Vec<String> = Vec::new();
127 let mut newest: Option<GateRecord> = None;
128 for handle in partition.handles {
129 let (record, path) = &candidates[handle];
130 let lbl = label(group.id, record.shard().unwrap_or((0, count)));
131 problems.extend(shard_problems(root, &lbl, record, path));
132 let Some(t) = aggregate::tallies_from_metrics(&record.metrics) else {
135 return GateStatus::Missing(format!(
136 "{lbl} has a covering record but no per-subset tallies — it was \
137 measured by a binary older than the shard split, so the group \
138 cannot be aggregated. Re-run it at this commit."
139 ));
140 };
141 let errs = record
142 .metrics
143 .get("transport_errors")
144 .copied()
145 .unwrap_or(0.0);
146 if errs > 0.0 {
147 return GateStatus::Fail(vec![format!(
148 "{lbl} recorded {errs:.0} transport failures. Each was scored as \
149 \"made no call\", which is the CORRECT answer on the irrelevance \
150 subsets, so a degraded shard can raise the aggregate while \
151 measuring less of the draw. Re-run it."
152 )]);
153 }
154 tallies.push(t);
155 if newest
156 .as_ref()
157 .is_none_or(|n| record.recorded_at > n.recorded_at)
158 {
159 newest = Some(record.clone());
160 }
161 }
162 if !problems.is_empty() {
163 return GateStatus::Fail(problems);
164 }
165
166 let agg = aggregate::aggregate(&aggregate::union(&tallies));
167 let Some(mut record) = newest else {
168 return GateStatus::Missing(format!("{} has no shards", group.id));
169 };
170 record.benchmark_id = group.id.to_string();
173 record
174 .metrics
175 .insert("overall_accuracy".into(), agg.overall_accuracy);
176 record.metrics.insert(
177 "normalized_single_turn_score".into(),
178 agg.normalized_single_turn_score,
179 );
180 record
181 .metrics
182 .insert("samples".into(), agg.total_samples as f64);
183 record.metrics.insert("shard.count".into(), count as f64);
184
185 match super::scoring::check_record(&record, &baseline) {
186 None => GateStatus::Pass,
187 Some(breaches) => GateStatus::Fail(breaches),
188 }
189}
190
191pub fn shards_owed(
201 root: &Path,
202 group: &'static super::group::BenchmarkGroup,
203 sha: &str,
204 wanted: usize,
205) -> Vec<(usize, usize)> {
206 let fresh = |n: usize| (0..n).map(|i| (i, n)).collect::<Vec<_>>();
207 let (Ok(baseline), Some(gate)) = (
208 read_baseline(root, group.id),
209 super::coverage::find(group.id),
210 ) else {
211 return fresh(wanted);
212 };
213 let candidates = standing_shards(root, &baseline, group.id, sha, gate);
214 let clean: Vec<ShardRecord> = candidates
215 .iter()
216 .enumerate()
217 .filter_map(|(handle, (record, path))| {
218 let (index, count) = record.shard()?;
219 shard_problems(root, &label(group.id, (index, count)), record, path)
220 .is_empty()
221 .then(|| ShardRecord {
222 index,
223 count,
224 git_sha: record.git_sha.clone(),
225 recorded_at: record.recorded_at,
226 handle,
227 })
228 })
229 .collect();
230 if super::group::select_partition(group.id, &clean).is_ok() {
231 return Vec::new();
232 }
233 let held = super::group::held_by(&clean);
235 match held
236 .iter()
237 .filter(|((_, commit), _)| commit.starts_with(sha) || sha.starts_with(commit.as_str()))
238 .max_by_key(|((count, _), idx)| (idx.len() * 1000 / *count, *count))
239 {
240 Some(((count, _), idx)) => (0..*count)
241 .filter(|i| !idx.contains_key(i))
242 .map(|i| (i, *count))
243 .collect(),
244 None => fresh(wanted),
245 }
246}
247
248fn why_stale(
252 root: &Path,
253 group: &str,
254 sha: &str,
255 gate: &super::coverage::GateCoverage,
256) -> Option<String> {
257 let newest = records_newest_first(root, group)
258 .into_iter()
259 .find_map(|p| {
260 read_record(&p)
261 .ok()
262 .filter(|r| r.shard().is_some())
263 .map(|r| (r, p))
264 })?;
265 let (record, path) = newest;
266 let name = path
267 .file_name()
268 .unwrap_or_default()
269 .to_string_lossy()
270 .into_owned();
271 if record_still_stands(root, sha, &record, gate) {
272 return None;
273 }
274 let why = super::check_paths::invalidating_paths(root, sha, &record.git_sha, gate)?;
275 Some(if why.is_empty() {
276 format!(
277 "Newest shard record is for {} ({name}) — its recorded build inputs do not \
278 match this commit.",
279 record.git_sha
280 )
281 } else {
282 format!(
283 "Newest shard record is for {} ({name}) — invalidated by {}.",
284 record.git_sha,
285 super::check_fmt::summarize_paths(&why)
286 )
287 })
288}
289
290fn whole_draw_note(root: &Path, group: &super::group::BenchmarkGroup) -> Option<String> {
293 let whole: Vec<PathBuf> = records_newest_first(root, group.id)
294 .into_iter()
295 .filter(|p| read_record(p).is_ok_and(|r| r.shard().is_none()))
296 .collect();
297 let newest = whole.first()?;
298 Some(format!(
299 "The {} whole-draw record(s) under {} (newest {}) no longer satisfy the gate: since \
300 2026-09-13 a group is certified only by a complete partition of shards — see \
301 gate::group.",
302 whole.len(),
303 group.id,
304 newest.file_name().unwrap_or_default().to_string_lossy(),
305 ))
306}