1use std::collections::BTreeSet;
11
12use rustev_contract::eval_report::{MetricValue, Split};
13use rustev_contract::ids::{ContentDigest, EvaluatorConfigId};
14use rustev_contract::judgment::Derivation;
15use rustev_contract::limits::REPLAY_V1;
16use rustev_contract::scope::Handle;
17use rustev_contract::{Document, Identified};
18use serde::{Deserialize, Serialize};
19
20use crate::config::EvaluatorConfig;
21use crate::eval_cases::{CaseMeasure, CaseMeasures, EVAL_CASES, EvalCases, form_in};
22use crate::host_run::{
23 self, Assertion, HostCases, HostDataset, HostError, HostMetric, HostOutcome, HostRunInputs,
24 code, metric,
25};
26use crate::repeat::{
27 BandDoc, CohortPlan, CohortReport, CohortStatus, FigureDoc, HeldOut, IntervalDoc, MetricReport,
28 SplitRole, cohort_reports, split_role,
29};
30use crate::repeat_config::RepeatConfig;
31use crate::repeat_stats::{Figure, MAX_RUNS};
32use crate::scoring::ScorerKind;
33
34pub const HOST_REPEAT_REPORT: &str = "rustev.host-repeat-report/1";
35pub const HOST_LEDGER_UNSUPPORTED: &str = "host-ledger-unsupported";
37pub const TOP_K_NOT_OBSERVED: &str = "top-k-not-observed";
39pub const DUPLICATE_RUN: &str = "duplicate-run";
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum HostSource {
46 Host,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "snake_case")]
52pub enum NotChecked {
53 NotChecked,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum HostObservation {
61 Host,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(deny_unknown_fields)]
68pub struct HostFlow {
69 pub system: Handle,
70 pub flow: Handle,
71 pub flow_version: Handle,
72}
73
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75#[serde(deny_unknown_fields)]
76pub struct HostRepeatReport {
77 pub schema: String,
78 pub assertion: Assertion,
79 pub source: HostSource,
80 pub repeat_config: ContentDigest,
81 pub evaluator_config: EvaluatorConfigId,
82 pub host: HostFlow,
83 pub dataset: HostDataset,
84 pub split: Split,
85 pub split_role: SplitRole,
86 pub cost_unit: Option<String>,
87 pub runs: Vec<HostRunRef>,
88 pub scorers: Vec<ScorerCheck>,
89 pub stable_cohort: Vec<String>,
91 pub held_out: Option<HeldOut>,
93 pub cohorts: Vec<HostCohortReport>,
94}
95
96crate::doc::eval_document!(HostRepeatReport, HOST_REPEAT_REPORT, REPLAY_V1);
97
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(deny_unknown_fields)]
100pub struct HostRunRef {
101 pub host_run: ContentDigest,
103 pub run: Handle,
104 pub cases: ContentDigest,
106 pub seed: Option<String>,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(deny_unknown_fields)]
114pub struct ScorerCheck {
115 pub scorer: String,
116 pub scorer_reproduction: NotChecked,
117 pub judge_runs: Option<NotChecked>,
120}
121
122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
123#[serde(deny_unknown_fields)]
124pub struct HostCohortReport {
125 pub name: String,
126 pub status: CohortStatus,
127 pub cases: u64,
128 pub stable_cases: u64,
129 pub min_cases: Option<u64>,
130 pub metrics: Vec<HostMetricReport>,
131}
132
133#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
134#[serde(deny_unknown_fields)]
135pub struct HostMetricReport {
136 pub name: String,
137 pub derivation: Derivation,
138 pub observation: Option<HostObservation>,
140 pub unit: Option<String>,
142 pub runs: Vec<FigureDoc>,
143 pub mean: FigureDoc,
144 pub sd: FigureDoc,
145 pub min: FigureDoc,
146 pub max: FigureDoc,
147 pub pooled: FigureDoc,
148 pub interval: IntervalDoc,
149 pub band: BandDoc,
150}
151
152pub fn observed(name: &str) -> bool {
154 name.starts_with("host/latency_ms/") || name == metric::COST_PER_1000
155}
156
157pub struct HostSetInputs<'a> {
160 pub config: &'a RepeatConfig,
161 pub evaluator: &'a EvaluatorConfig,
162 pub runs: &'a [HostRunInputs<'a>],
163}
164
165#[derive(Debug, Clone, PartialEq)]
167pub struct HostSet {
168 pub report: HostRepeatReport,
169 pub cases: Vec<HostCases>,
170}
171
172fn mixed(detail: impl Into<String>) -> HostError {
173 HostError::new(code::MIXED_RUNS, detail)
174}
175
176fn repeat_config(detail: impl Into<String>) -> HostError {
177 HostError::new(code::REPEAT_CONFIG, detail)
178}
179
180fn check_runs(runs: &[HostRunInputs<'_>]) -> Result<(), HostError> {
182 let first = runs[0].record;
183 let views = runs[0].views.expect("bound");
184 let ids = |r: &crate::host_run::HostRun| -> Vec<(String, Vec<String>)> {
185 r.cases
186 .iter()
187 .map(|c| (c.case.clone(), c.subgroups.clone()))
188 .collect()
189 };
190 let mut handles = BTreeSet::new();
191 for (i, run) in runs.iter().enumerate() {
192 let r = run.record;
193 let v = run.views.expect("bound");
194 let same = r.host.system == first.host.system
195 && r.host.flow == first.host.flow
196 && r.host.flow_version == first.host.flow_version
197 && r.dataset == first.dataset
198 && r.split == first.split
199 && r.cost_unit == first.cost_unit
200 && ids(r) == ids(first)
201 && v.dataset == views.dataset
202 && v.plan == views.plan;
203 if !same {
204 return Err(mixed(
205 "the runs differ in host system, flow, flow version, dataset, split, cost unit, \
206 cases, subgroups, or the views' dataset or plan member",
207 )
208 .at(i));
209 }
210 if !handles.insert(r.host.run.clone()) {
211 return Err(mixed(format!(
212 "{DUPLICATE_RUN}: host.run {:?} is repeated",
213 r.host.run.as_str()
214 ))
215 .at(i));
216 }
217 }
218 Ok(())
219}
220
221fn as_eval_cases(c: &HostCases) -> EvalCases {
224 EvalCases {
225 schema: EVAL_CASES.into(),
226 report: c.host_run.clone(),
227 top_k: None,
228 cases: c
229 .cases
230 .iter()
231 .map(|m| CaseMeasures {
232 case: m.case.clone(),
233 run: None,
234 outcome: match &m.outcome {
235 HostOutcome::Answered => "answered".into(),
236 HostOutcome::Abstained => "abstained".into(),
237 HostOutcome::Failed(r) => format!("failed:{r}"),
238 },
239 top_k: None,
240 measures: m.measures.clone(),
241 })
242 .collect(),
243 }
244}
245
246pub fn import(inputs: &HostSetInputs<'_>) -> Result<HostSet, HostError> {
250 let config = inputs.config;
251 config.check().map_err(|e| repeat_config(e.to_string()))?;
252 if config.top_k.is_some() {
253 return Err(repeat_config(format!(
254 "{TOP_K_NOT_OBSERVED}: a host run carries no ranking parameter Rustev can read"
255 )));
256 }
257 let runs = inputs.runs;
258 let n = runs.len();
259 if !(1..=MAX_RUNS).contains(&n) {
260 return Err(repeat_config(format!("{n} runs; a set has 1 to 64")));
261 }
262 let mut measured = vec![];
263 for (i, run) in runs.iter().enumerate() {
264 measured.push(host_run::measure(inputs.evaluator, run).map_err(|e| e.at(i))?);
265 }
266 check_runs(runs)?;
267 let first = runs[0].record;
268 let info: &[HostMetric] = &measured[0].metrics;
269 let names: Vec<&str> = info.iter().map(|m| m.name.as_str()).collect();
270 config
271 .check_metrics(&names.iter().map(|s| s.to_string()).collect())
272 .map_err(|e| repeat_config(e.to_string()))?;
273
274 let cases: Vec<HostCases> = measured.iter().map(|m| m.cases.clone()).collect();
275 let eval: Vec<EvalCases> = cases.iter().map(as_eval_cases).collect();
276 let docs: Vec<&EvalCases> = eval.iter().collect();
277 let stable: Vec<usize> = (0..first.cases.len())
278 .filter(|&c| {
279 runs.iter()
280 .all(|r| !matches!(r.record.cases[c].outcome, HostOutcome::Failed(_)))
281 })
282 .collect();
283 let subgroups: Vec<&[String]> = first.cases.iter().map(|c| c.subgroups.as_slice()).collect();
284 let form = |m: &str| host_run::form(m).or_else(|| form_in(m, &docs));
285 let no_observation = |_: &str| None;
286 let across = |m: &str| -> Option<Vec<Figure>> {
289 measured
290 .iter()
291 .map(|r| match &r.metrics.iter().find(|x| x.name == m)?.value {
292 Some(MetricValue::Measured { value, .. }) if value.is_finite() => {
293 Some(Figure::Known(*value))
294 }
295 Some(MetricValue::Unknown { reason }) => Some(Figure::Unknown {
296 reason: reason.clone(),
297 }),
298 Some(_) => Some(Figure::Unknown {
299 reason: "non-finite value".into(),
300 }),
301 None => None,
302 })
303 .collect()
304 };
305 let reports = cohort_reports(&CohortPlan {
306 config,
307 n,
308 deterministic: false,
310 subgroups: &subgroups,
311 stable: &stable,
312 names: &names,
313 docs: &docs,
314 form: &form,
315 observation: &no_observation,
316 across: &across,
317 })
318 .map_err(|e| HostError::new(e.code(), e.to_string()))?;
319
320 let cohorts = reports
321 .into_iter()
322 .map(|c| host_cohort(c, info, &cases, &subgroups, first.cost_unit.as_deref()))
323 .collect();
324 let mut run_refs = vec![];
325 for (r, c) in runs.iter().zip(&cases) {
326 let digest = |d: Result<ContentDigest, _>| {
327 d.map_err(|_| HostError::new(code::HOST_RUN, "a document has no record digest"))
328 };
329 run_refs.push(HostRunRef {
330 host_run: digest(r.record.record_digest())?,
331 run: r.record.host.run.clone(),
332 cases: digest(c.record_digest())?,
333 seed: None,
334 });
335 }
336 let declared = host_run::declarations(inputs.evaluator)?;
337 let role = split_role(first.split);
338 let report = HostRepeatReport {
339 schema: HOST_REPEAT_REPORT.into(),
340 assertion: Assertion::Host,
341 source: HostSource::Host,
342 repeat_config: config
343 .id()
344 .map_err(|_| repeat_config("the repeat configuration is not canonical"))?,
345 evaluator_config: inputs
346 .evaluator
347 .id()
348 .map_err(|e| HostError::new(code::CONFIG, e.to_string()))?,
349 host: HostFlow {
350 system: first.host.system.clone(),
351 flow: first.host.flow.clone(),
352 flow_version: first.host.flow_version.clone(),
353 },
354 dataset: first.dataset.clone(),
355 split: first.split,
356 split_role: role,
357 cost_unit: first.cost_unit.clone(),
358 runs: run_refs,
359 scorers: declared
360 .scorers
361 .iter()
362 .map(|s| ScorerCheck {
363 scorer: s.id.clone(),
364 scorer_reproduction: NotChecked::NotChecked,
365 judge_runs: (s.kind == ScorerKind::Model).then_some(NotChecked::NotChecked),
366 })
367 .collect(),
368 stable_cohort: stable
369 .iter()
370 .map(|&c| first.cases[c].case.clone())
371 .collect(),
372 held_out: (role == SplitRole::HeldOut).then(|| HeldOut::Unknown {
373 reason: HOST_LEDGER_UNSUPPORTED.into(),
374 }),
375 cohorts,
376 };
377 Ok(HostSet { report, cases })
378}
379
380fn host_cohort(
384 c: CohortReport,
385 info: &[HostMetric],
386 cases: &[HostCases],
387 subgroups: &[&[String]],
388 cost_unit: Option<&str>,
389) -> HostCohortReport {
390 let members: Vec<usize> = (0..subgroups.len())
391 .filter(|&i| c.name == "all" || subgroups[i].contains(&c.name))
392 .collect();
393 let metrics = c
394 .metrics
395 .into_iter()
396 .map(|m| {
397 let derivation = info
398 .iter()
399 .find(|x| x.name == m.name)
400 .map_or(Derivation::ExactDerived, |x| x.derivation);
401 let not_declared = m.name == metric::COST_PER_1000 && cost_unit.is_none();
402 let MetricReport {
403 name,
404 mut runs,
405 mean,
406 sd,
407 min,
408 max,
409 mut pooled,
410 interval,
411 band,
412 ..
413 } = if not_declared {
414 unknown(m, host_run::COST_NOT_DECLARED)
415 } else {
416 m
417 };
418 for (r, figure) in runs.iter_mut().enumerate() {
419 if let (FigureDoc::Unknown(_), Some(why)) =
420 (&figure, first_unknown(cases, r, &members, &name))
421 && !not_declared
422 {
423 *figure = FigureDoc::Unknown(why);
424 }
425 }
426 if let FigureDoc::Unknown(_) = pooled
428 && !not_declared
429 && let Some(why) = (0..runs.len()).find_map(|r| {
430 Some(format!(
431 "run {r}: {}",
432 first_unknown(cases, r, &members, &name)?
433 ))
434 })
435 {
436 pooled = FigureDoc::Unknown(why);
437 }
438 HostMetricReport {
439 observation: observed(&name).then_some(HostObservation::Host),
440 unit: (name == metric::COST_PER_1000)
441 .then(|| cost_unit.map(str::to_string))
442 .flatten(),
443 name,
444 derivation,
445 runs,
446 mean,
447 sd,
448 min,
449 max,
450 pooled,
451 interval,
452 band,
453 }
454 })
455 .collect();
456 HostCohortReport {
457 name: c.name,
458 status: c.status,
459 cases: c.cases,
460 stable_cases: c.stable_cases,
461 min_cases: c.min_cases,
462 metrics,
463 }
464}
465
466fn unknown(m: MetricReport, reason: &str) -> MetricReport {
468 let u = || FigureDoc::Unknown(reason.into());
469 MetricReport {
470 runs: m.runs.iter().map(|_| u()).collect(),
471 mean: u(),
472 sd: u(),
473 min: u(),
474 max: u(),
475 pooled: u(),
476 interval: IntervalDoc::Unknown {
477 reason: reason.into(),
478 undefined: 0,
479 },
480 band: BandDoc::Unknown(reason.into()),
481 ..m
482 }
483}
484
485fn first_unknown(cases: &[HostCases], r: usize, members: &[usize], metric: &str) -> Option<String> {
488 members.iter().find_map(|&i| {
489 let c = &cases[r].cases[i];
490 match c.measures.get(metric) {
491 Some(CaseMeasure::Unknown { reason }) => Some(format!("case {}: {reason}", c.case)),
492 _ => None,
493 }
494 })
495}
496
497pub fn reassemble(
500 inputs: &HostSetInputs<'_>,
501 report: &HostRepeatReport,
502 cases: &[HostCases],
503) -> Result<HostSet, HostError> {
504 let again = import(inputs)?;
505 let bytes = |d: &dyn Fn() -> Result<Vec<u8>, rustev_contract::canonical::CanonicalError>| {
506 d().map_err(|e| HostError::new(code::RECONCILE, e.to_string()))
507 };
508 let same_report =
509 bytes(&|| again.report.record_canonical())? == bytes(&|| report.record_canonical())?;
510 let canonical = |d: &HostCases| {
513 d.record_canonical().map_err(|e| {
514 HostError::new(
515 code::RECONCILE,
516 format!("a case document is not canonical: {e}"),
517 )
518 })
519 };
520 let mut same_cases = again.cases.len() == cases.len();
521 for (a, b) in again.cases.iter().zip(cases) {
522 same_cases &= canonical(a)? == canonical(b)?;
523 }
524 if !same_report || !same_cases {
525 return Err(HostError::new(
526 code::RECONCILE,
527 "the import output does not reproduce its host repeat report and case measures",
528 ));
529 }
530 Ok(again)
531}