1use rustev_contract::eval_report::Split;
13use rustev_contract::ids::{ContentDigest, EvaluatorConfigId};
14use rustev_contract::judgment::Derivation;
15use rustev_contract::limits::REPLAY_V1;
16use rustev_contract::{Document, Identified};
17use serde::{Deserialize, Serialize};
18
19use crate::baseline::{self, BASELINE, Reproducibility, valid_name};
20use crate::eval_cases::{CaseMeasure, EVAL_CASES};
21use crate::host_repeat::{
22 HOST_REPEAT_REPORT, HostCohortReport, HostFlow, HostObservation, HostRepeatReport, HostRunRef,
23 HostSetInputs, HostSource, reassemble,
24};
25use crate::host_run::{
26 Assertion, HOST_CASES, HOST_RUN, HostCases, HostDataset, HostError, code, declarations, metric,
27};
28use crate::repeat::{
29 BandDoc, CohortReport, CohortStatus, FigureDoc, HeldOut, IntervalDoc, MetricReport,
30 Observation, REPEAT_REPORT, SplitRole,
31};
32use crate::repeat_config::RepeatConfig;
33use crate::selection::{
34 ClauseOutcome, ClauseResult, SELECTION, SelectionGate, Side, Unranked, Verdict, judge,
35 sort_ranks,
36};
37
38pub const HOST_BASELINE: &str = "rustev.host-baseline/1";
39pub const HOST_SELECTION: &str = "rustev.host-selection/1";
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum Provenance {
44 Rustev,
46 Host,
48}
49
50pub fn provenance(schema: &str) -> Option<Provenance> {
52 match schema {
53 HOST_RUN | HOST_CASES | HOST_REPEAT_REPORT | HOST_BASELINE | HOST_SELECTION => {
54 Some(Provenance::Host)
55 }
56 REPEAT_REPORT | BASELINE | EVAL_CASES | SELECTION => Some(Provenance::Rustev),
57 _ => None,
58 }
59}
60
61pub fn one_provenance(schemas: &[&str]) -> Result<Option<Provenance>, HostError> {
65 let mut seen = None;
66 for p in schemas.iter().filter_map(|s| provenance(s)) {
67 match seen {
68 Some(q) if q != p => {
69 return Err(HostError::new(
70 code::MIXED_PROVENANCE,
71 "host and Rustev-evaluated documents are never combined or compared",
72 ));
73 }
74 _ => seen = Some(p),
75 }
76 }
77 Ok(seen)
78}
79
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81#[serde(deny_unknown_fields)]
82pub struct HostBaseline {
83 pub schema: String,
84 pub assertion: Assertion,
85 pub name: String,
86 pub host_repeat_report: ContentDigest,
88 pub runs: Vec<HostRunRef>,
89 pub host: HostFlow,
90 pub dataset: HostDataset,
91 pub split: Split,
92 pub split_role: SplitRole,
93 pub cost_unit: Option<String>,
94 pub evaluator_config: EvaluatorConfigId,
95 pub repeat_config: ContentDigest,
96 pub source: HostSource,
97 pub n: u64,
98 pub recorded_at_ms: i64,
100 pub held_out: Option<HeldOut>,
101 pub cohorts: Vec<HostBaselineCohort>,
102}
103
104crate::doc::eval_document!(HostBaseline, HOST_BASELINE, REPLAY_V1);
105
106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
107#[serde(deny_unknown_fields)]
108pub struct HostBaselineCohort {
109 pub name: String,
110 pub status: CohortStatus,
111 pub metrics: Vec<HostBaselineMetric>,
112}
113
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
115#[serde(deny_unknown_fields)]
116pub struct HostBaselineMetric {
117 pub name: String,
118 pub derivation: Derivation,
119 pub observation: Option<HostObservation>,
120 pub unit: Option<String>,
121 pub pooled: FigureDoc,
122 pub interval: IntervalDoc,
123 pub band: BandDoc,
124}
125
126pub struct HostSetEvidence<'a> {
128 pub report: &'a HostRepeatReport,
129 pub cases: &'a [HostCases],
130 pub inputs: HostSetInputs<'a>,
131}
132
133pub fn record(
136 name: &str,
137 set: &HostSetEvidence<'_>,
138 recorded_at_ms: i64,
139) -> Result<HostBaseline, HostError> {
140 if !valid_name(name) {
141 return Err(HostError::new(
142 "baseline",
143 "a baseline name is 1 to 64 bytes of [A-Za-z0-9._-], starting alphanumeric",
144 ));
145 }
146 reassemble(&set.inputs, set.report, set.cases)?;
147 let r = set.report;
148 Ok(HostBaseline {
149 schema: HOST_BASELINE.into(),
150 assertion: Assertion::Host,
151 name: name.into(),
152 host_repeat_report: r
153 .record_digest()
154 .map_err(|e| HostError::new(code::RECONCILE, e.to_string()))?,
155 runs: r.runs.clone(),
156 host: r.host.clone(),
157 dataset: r.dataset.clone(),
158 split: r.split,
159 split_role: r.split_role,
160 cost_unit: r.cost_unit.clone(),
161 evaluator_config: r.evaluator_config.clone(),
162 repeat_config: r.repeat_config.clone(),
163 source: r.source,
164 n: r.runs.len() as u64,
165 recorded_at_ms,
166 held_out: r.held_out.clone(),
167 cohorts: r
168 .cohorts
169 .iter()
170 .map(|c| HostBaselineCohort {
171 name: c.name.clone(),
172 status: c.status,
173 metrics: c
174 .metrics
175 .iter()
176 .map(|m| HostBaselineMetric {
177 name: m.name.clone(),
178 derivation: m.derivation,
179 observation: m.observation,
180 unit: m.unit.clone(),
181 pooled: m.pooled.clone(),
182 interval: m.interval.clone(),
183 band: m.band.clone(),
184 })
185 .collect(),
186 })
187 .collect(),
188 })
189}
190
191pub fn verify_baseline(
194 baseline: &HostBaseline,
195 set: &HostSetEvidence<'_>,
196) -> Result<(), HostError> {
197 if record(&baseline.name, set, baseline.recorded_at_ms)? != *baseline {
198 return Err(HostError::new(
199 "baseline",
200 "the host baseline does not bind its import output",
201 ));
202 }
203 Ok(())
204}
205
206fn as_metric(m: &crate::host_repeat::HostMetricReport) -> MetricReport {
209 MetricReport {
210 name: m.name.clone(),
211 observation: m
212 .observation
213 .map(|HostObservation::Host| Observation::Candidate),
214 runs: m.runs.clone(),
215 mean: m.mean.clone(),
216 sd: m.sd.clone(),
217 min: m.min.clone(),
218 max: m.max.clone(),
219 pooled: m.pooled.clone(),
220 interval: m.interval.clone(),
221 band: m.band.clone(),
222 }
223}
224
225fn as_cohorts(cohorts: &[HostCohortReport]) -> Vec<CohortReport> {
226 cohorts
227 .iter()
228 .map(|c| CohortReport {
229 name: c.name.clone(),
230 status: c.status,
231 cases: c.cases,
232 stable_cases: c.stable_cases,
233 min_cases: c.min_cases,
234 metrics: c.metrics.iter().map(as_metric).collect(),
235 })
236 .collect()
237}
238
239pub fn reproducibility(
243 baseline: &HostBaseline,
244 run: &HostRepeatReport,
245 config: &RepeatConfig,
246) -> Reproducibility {
247 let mut reasons = vec![];
248 if config.id().ok().as_ref() != Some(&baseline.repeat_config) {
249 reasons.push("the repeat configuration is not the baseline's".to_string());
250 }
251 if run.host != baseline.host
252 || run.dataset != baseline.dataset
253 || run.split != baseline.split
254 || run.evaluator_config != baseline.evaluator_config
255 || run.repeat_config != baseline.repeat_config
256 {
257 reasons.push(
258 "the host set's system, flow, flow version, dataset, split, evaluator \
259 configuration or repeat configuration is not the baseline's"
260 .into(),
261 );
262 }
263 let base: Vec<baseline::BaselineCohort> = baseline
264 .cohorts
265 .iter()
266 .map(|c| baseline::BaselineCohort {
267 name: c.name.clone(),
268 status: c.status,
269 metrics: c
270 .metrics
271 .iter()
272 .map(|m| baseline::BaselineMetric {
273 name: m.name.clone(),
274 observation: None,
275 pooled: m.pooled.clone(),
276 interval: m.interval.clone(),
277 band: m.band.clone(),
278 })
279 .collect(),
280 })
281 .collect();
282 baseline::stability(reasons, &base, &as_cohorts(&run.cohorts), config)
283}
284
285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
289#[serde(deny_unknown_fields)]
290pub struct HostSelection {
291 pub schema: String,
292 pub assertion: Assertion,
293 pub gate: ContentDigest,
294 pub baseline: ContentDigest,
296 pub candidates: Vec<HostCandidateSelection>,
297 pub ranked: Vec<String>,
298 pub unranked: Vec<Unranked>,
299}
300
301crate::doc::eval_document!(HostSelection, HOST_SELECTION, REPLAY_V1);
302
303#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
304#[serde(deny_unknown_fields)]
305pub struct HostCandidateSelection {
306 pub name: String,
307 pub host_repeat_report: ContentDigest,
308 pub host: HostFlow,
309 pub verdict: Verdict,
310 pub clauses: Vec<ClauseResult>,
311}
312
313impl HostSelection {
314 pub fn exit_code(&self) -> u8 {
316 if self
317 .candidates
318 .iter()
319 .any(|c| matches!(c.verdict, Verdict::Unknown { .. }))
320 {
321 12
322 } else if self
323 .candidates
324 .iter()
325 .any(|c| c.verdict == Verdict::Qualifies)
326 {
327 0
328 } else {
329 11
330 }
331 }
332}
333
334pub struct NamedHostCandidate<'a> {
335 pub name: &'a str,
336 pub evidence: HostSetEvidence<'a>,
337}
338
339fn comparable(b: &HostSetEvidence<'_>, c: &HostSetEvidence<'_>) -> Result<(), String> {
344 let (br, cr) = (b.report, c.report);
345 if br.host.system != cr.host.system {
346 return Err("host system mismatch".into());
347 }
348 if br.dataset != cr.dataset || br.split != cr.split {
349 return Err("dataset or split mismatch".into());
350 }
351 if br.evaluator_config != cr.evaluator_config {
352 return Err("evaluator configuration mismatch".into());
353 }
354 if br.repeat_config != cr.repeat_config {
355 return Err("repeat configuration mismatch".into());
356 }
357 if br.cost_unit != cr.cost_unit {
358 return Err("cost unit mismatch".into());
359 }
360 let ids = |e: &HostSetEvidence<'_>| -> Option<Vec<String>> {
361 Some(
362 e.cases
363 .first()?
364 .cases
365 .iter()
366 .map(|c| c.case.clone())
367 .collect(),
368 )
369 };
370 if ids(b) != ids(c) {
371 return Err("case lists or order differ".into());
372 }
373 Ok(())
374}
375
376fn counts(cases: &HostCases, name: &str) -> (u64, u64) {
378 cases
379 .cases
380 .iter()
381 .filter_map(|c| match c.measures.get(name) {
382 Some(CaseMeasure::Count { num, den }) => Some((*num, *den)),
383 _ => None,
384 })
385 .fold((0, 0), |(n, d), (a, b)| (n + a, d + b))
386}
387
388fn coverage(gate: &SelectionGate, side: &str, e: &HostSetEvidence<'_>) -> Vec<ClauseResult> {
392 let mut out = vec![];
393 for (r, cases) in e.cases.iter().enumerate() {
394 let (failed, all) = counts(cases, metric::FAILED_RATE);
395 let (scored, scored_all) = counts(cases, metric::SCORED_RATE);
396 for (label, num, den, min) in [
397 (
398 "comparable_coverage",
399 all - failed,
400 all,
401 gate.min_comparable_coverage,
402 ),
403 (
404 "labeled_coverage",
405 scored,
406 scored_all,
407 gate.min_labeled_coverage,
408 ),
409 ] {
410 let result = if den == 0 {
411 ClauseOutcome::Unknown {
412 reason: "coverage has a zero denominator".into(),
413 }
414 } else if i128::from(num) * 1_000_000_000 >= min.units() * i128::from(den) {
415 ClauseOutcome::Pass {
416 reason: "coverage minimum".into(),
417 }
418 } else {
419 ClauseOutcome::Fail {
420 reason: "coverage minimum".into(),
421 }
422 };
423 out.push(ClauseResult {
424 clause: format!("{side}.runs[{r}].{label}"),
425 result,
426 slices: vec![],
427 });
428 }
429 }
430 out
431}
432
433fn opaque_metrics(e: &HostSetEvidence<'_>) -> Vec<String> {
434 declarations(e.inputs.evaluator)
435 .map(|d| {
436 d.metric_names()
437 .into_iter()
438 .filter(|(_, (_, opaque))| *opaque)
439 .map(|(m, _)| m)
440 .collect()
441 })
442 .unwrap_or_default()
443}
444
445pub fn evaluate_selection(
449 gate: &SelectionGate,
450 baseline: &HostBaseline,
451 base: &HostSetEvidence<'_>,
452 candidates: &[NamedHostCandidate<'_>],
453) -> Result<HostSelection, String> {
454 gate.check()?;
455 SelectionGate::parse(&gate.canonical().map_err(|e| e.to_string())?)
456 .map_err(|e| e.to_string())?;
457 crate::comparison::check_name_list(&candidates.iter().map(|c| c.name).collect::<Vec<_>>())?;
458 let base_ok = verify_baseline(baseline, base).map_err(|e| e.to_string());
459 let base_cohorts = as_cohorts(&base.report.cohorts);
460 let base_opaque = opaque_metrics(base);
461 let base_side = Side {
462 cohorts: &base_cohorts,
463 split_role: base.report.split_role,
464 held_out: base.report.held_out.as_ref(),
465 opaque: &|m: &str| base_opaque.iter().any(|x| x == m),
466 host: true,
467 };
468 let mut decisions = vec![];
469 let mut ranks = vec![];
470 let mut unranked = vec![];
471 for c in candidates {
472 let e = &c.evidence;
473 let precondition = base_ok
474 .clone()
475 .and_then(|()| comparable(base, e))
476 .and_then(|()| {
477 reassemble(&e.inputs, e.report, e.cases)
478 .map(|_| ())
479 .map_err(|e| e.to_string())
480 });
481 let reason = precondition.err();
482 let mut clauses = vec![ClauseResult {
483 clause: "preconditions".into(),
484 result: match &reason {
485 Some(r) => ClauseOutcome::Unknown { reason: r.clone() },
486 None => ClauseOutcome::Pass {
487 reason: "bindings verified".into(),
488 },
489 },
490 slices: vec![],
491 }];
492 if reason.is_none() {
493 clauses.extend(coverage(gate, "baseline", base));
494 clauses.extend(coverage(gate, "candidate", e));
495 }
496 let cohorts = as_cohorts(&e.report.cohorts);
497 let opaque = opaque_metrics(e);
498 let side = Side {
499 cohorts: &cohorts,
500 split_role: e.report.split_role,
501 held_out: e.report.held_out.as_ref(),
502 opaque: &|m: &str| opaque.iter().any(|x| x == m),
503 host: true,
504 };
505 let (verdict, clauses) = judge(
506 gate,
507 &base_side,
508 &side,
509 reason.as_deref(),
510 clauses,
511 c.name,
512 &mut ranks,
513 &mut unranked,
514 );
515 decisions.push(HostCandidateSelection {
516 name: c.name.into(),
517 host_repeat_report: e.report.record_digest().map_err(|e| e.to_string())?,
518 host: e.report.host.clone(),
519 verdict,
520 clauses,
521 });
522 }
523 sort_ranks(gate, &mut ranks);
524 let doc = HostSelection {
525 schema: HOST_SELECTION.into(),
526 assertion: Assertion::Host,
527 gate: gate.id().map_err(|e| e.to_string())?,
528 baseline: baseline.record_digest().map_err(|e| e.to_string())?,
529 candidates: decisions,
530 ranked: ranks.into_iter().map(|(n, _)| n).collect(),
531 unranked,
532 };
533 HostSelection::parse(&doc.record_canonical().map_err(|e| e.to_string())?)
534 .map_err(|e| e.to_string())
535}