use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::assertions::{AssertionFailure, ExpectationName};
use crate::config::EvalConfig;
use crate::corpus::{ItemFingerprint, ItemId, Tag};
use crate::judge::{CriterionOutcome, JudgeCriterion, ratio};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ReliabilityCategory {
SideEffectIntegrity,
OperationalClaimIntegrity,
SemanticInterpretation,
Clarification,
Abandonment,
ProviderFailure,
UserExperience,
}
impl ReliabilityCategory {
#[must_use]
pub const fn of(expectation: ExpectationName) -> Self {
match expectation {
ExpectationName::Commands
| ExpectationName::ForbiddenCommand
| ExpectationName::Events
| ExpectationName::ForbiddenEvent
| ExpectationName::TruncatedLedger
| ExpectationName::CaseRevision
| ExpectationName::CaseState
| ExpectationName::WorkflowState
| ExpectationName::CaseCount => Self::SideEffectIntegrity,
ExpectationName::Outcome
| ExpectationName::ResponseBlocks
| ExpectationName::TurnPhase => Self::OperationalClaimIntegrity,
ExpectationName::Acts | ExpectationName::TargetResolution => {
Self::SemanticInterpretation
}
ExpectationName::InteractionStatus => Self::Clarification,
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::SideEffectIntegrity => "side_effect_integrity",
Self::OperationalClaimIntegrity => "operational_claim_integrity",
Self::SemanticInterpretation => "semantic_interpretation",
Self::Clarification => "clarification",
Self::Abandonment => "abandonment",
Self::ProviderFailure => "provider_failure",
Self::UserExperience => "user_experience",
}
}
}
impl std::fmt::Display for ReliabilityCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SampleReport {
pub sample: u32,
#[serde(default)]
pub failures: Vec<AssertionFailure>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub harness_error: Option<String>,
pub signature: String,
#[serde(default)]
pub judge: Vec<CriterionOutcome>,
#[serde(default)]
pub provider_failures: usize,
#[serde(default)]
pub cards_created: usize,
#[serde(default)]
pub acts_proposed: usize,
#[serde(default)]
pub acts_refused: usize,
#[serde(default)]
pub commands_journaled: usize,
#[serde(default)]
pub abandoned: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub discarded_answers: Vec<String>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub answer: String,
#[serde(default)]
pub tasks: crate::understanding::TaskScores,
}
impl SampleReport {
#[must_use]
pub fn passed(&self) -> bool {
self.failures.is_empty() && self.harness_error.is_none()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Variance {
pub samples: usize,
pub distinct_behaviours: usize,
pub pass_rate: f64,
pub pass_variance: f64,
pub behaviours: Vec<BehaviourCount>,
}
impl Variance {
#[must_use]
pub const fn is_flaky(&self) -> bool {
self.distinct_behaviours > 1
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BehaviourCount {
pub signature: String,
pub count: usize,
pub passed: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CriterionSummary {
pub criterion: JudgeCriterion,
pub samples_judged: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mean_score: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_score: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_score: Option<u8>,
pub mean_vote_spread: f64,
pub failed_votes: usize,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ItemReport {
pub id: ItemId,
pub name: String,
#[serde(default)]
pub tags: Vec<Tag>,
#[serde(default)]
pub fingerprint: ItemFingerprint,
pub samples: Vec<SampleReport>,
}
impl ItemReport {
#[must_use]
pub fn total_samples(&self) -> usize {
self.samples.len()
}
#[must_use]
pub fn refused_proposals(&self) -> usize {
self.samples
.iter()
.filter(|sample| sample.harness_error.is_none() && sample.acts_refused > 0)
.count()
}
#[must_use]
pub fn samples_with_discards(&self) -> usize {
self.samples
.iter()
.filter(|sample| sample.harness_error.is_none() && !sample.discarded_answers.is_empty())
.count()
}
#[must_use]
pub fn samples_passed(&self) -> usize {
self.samples.iter().filter(|s| s.passed()).count()
}
#[must_use]
pub fn deterministic_pass_rate(&self) -> f64 {
ratio(self.samples_passed(), self.total_samples())
}
#[must_use]
pub fn is_flaky(&self) -> bool {
let passed = self.samples_passed();
passed > 0 && passed < self.total_samples()
}
#[must_use]
pub fn variance(&self) -> Variance {
let mut behaviours: Vec<BehaviourCount> = Vec::new();
for sample in &self.samples {
match behaviours
.iter_mut()
.find(|found| found.signature == sample.signature)
{
Some(found) => found.count += 1,
None => behaviours.push(BehaviourCount {
signature: sample.signature.clone(),
count: 1,
passed: sample.passed(),
}),
}
}
behaviours.sort_by(|left, right| {
right
.count
.cmp(&left.count)
.then_with(|| left.signature.cmp(&right.signature))
});
let pass_rate = self.deterministic_pass_rate();
Variance {
samples: self.total_samples(),
distinct_behaviours: behaviours.len(),
pass_rate,
pass_variance: pass_rate * (1.0 - pass_rate),
behaviours,
}
}
#[must_use]
pub fn failures(&self) -> Vec<&AssertionFailure> {
self.samples
.iter()
.flat_map(|sample| sample.failures.iter())
.collect()
}
#[must_use]
pub fn samples_failing(&self, category: ReliabilityCategory) -> usize {
self.samples
.iter()
.filter(|sample| {
sample
.failures
.iter()
.any(|failure| ReliabilityCategory::of(failure.expectation) == category)
})
.count()
}
#[must_use]
pub fn judge_summaries(&self) -> Vec<CriterionSummary> {
let mut summaries = Vec::new();
for criterion in JudgeCriterion::ALL {
let outcomes: Vec<&CriterionOutcome> = self
.samples
.iter()
.flat_map(|sample| sample.judge.iter())
.filter(|outcome| outcome.criterion == criterion)
.collect();
if outcomes.is_empty() {
continue;
}
summaries.push(summarize(criterion, &outcomes));
}
summaries
}
}
fn summarize(criterion: JudgeCriterion, outcomes: &[&CriterionOutcome]) -> CriterionSummary {
let majorities: Vec<u8> = outcomes
.iter()
.filter_map(|outcome| outcome.majority_score())
.collect();
let mean_score = if majorities.is_empty() {
None
} else {
let total: u32 = majorities.iter().map(|score| u32::from(*score)).sum();
Some(f64::from(total) / precise(majorities.len()))
};
let spread_total: u32 = outcomes
.iter()
.map(|outcome| u32::from(outcome.spread()))
.sum();
CriterionSummary {
criterion,
samples_judged: outcomes.len(),
mean_score,
min_score: majorities.iter().copied().min(),
max_score: majorities.iter().copied().max(),
mean_vote_spread: f64::from(spread_total) / precise(outcomes.len()),
failed_votes: outcomes.iter().map(|o| o.failed_votes()).sum(),
}
}
fn precise(value: usize) -> f64 {
if value == 0 {
return 1.0;
}
#[allow(clippy::cast_precision_loss)]
{
value as f64
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Reliability {
pub side_effect_integrity: CategoryStats,
pub operational_claim_integrity: CategoryStats,
pub semantic_interpretation: CategoryStats,
pub clarification_integrity: CategoryStats,
pub clarification_rate: f64,
pub abandonment_rate: f64,
pub provider_failure_rate: f64,
pub user_experience: Vec<CriterionSummary>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CategoryStats {
pub samples: usize,
pub failing_samples: usize,
pub failures: usize,
}
impl CategoryStats {
#[must_use]
pub fn failure_rate(&self) -> f64 {
ratio(self.failing_samples, self.samples)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EvalReport {
pub suite: String,
pub generated_at: DateTime<Utc>,
pub config: EvalConfig,
pub items: Vec<ItemReport>,
}
impl EvalReport {
#[must_use]
pub fn new(
suite: impl Into<String>,
generated_at: DateTime<Utc>,
config: EvalConfig,
items: Vec<ItemReport>,
) -> Self {
Self {
suite: suite.into(),
generated_at,
config,
items,
}
}
#[must_use]
pub fn item(&self, id: &ItemId) -> Option<&ItemReport> {
self.items.iter().find(|item| &item.id == id)
}
#[must_use]
pub fn total_samples(&self) -> usize {
self.items.iter().map(ItemReport::total_samples).sum()
}
#[must_use]
pub fn samples_passed(&self) -> usize {
self.items.iter().map(ItemReport::samples_passed).sum()
}
#[must_use]
pub fn deterministic_pass_rate(&self) -> f64 {
ratio(self.samples_passed(), self.total_samples())
}
#[must_use]
pub fn flaky_items(&self) -> Vec<&ItemReport> {
self.items.iter().filter(|item| item.is_flaky()).collect()
}
#[must_use]
pub fn reliability(&self) -> Reliability {
let samples = self.total_samples();
let all: Vec<&SampleReport> = self
.items
.iter()
.flat_map(|item| item.samples.iter())
.collect();
Reliability {
side_effect_integrity: self.stats(ReliabilityCategory::SideEffectIntegrity),
operational_claim_integrity: self.stats(ReliabilityCategory::OperationalClaimIntegrity),
semantic_interpretation: self.stats(ReliabilityCategory::SemanticInterpretation),
clarification_integrity: self.stats(ReliabilityCategory::Clarification),
clarification_rate: ratio(all.iter().filter(|s| s.cards_created > 0).count(), samples),
abandonment_rate: ratio(all.iter().filter(|s| s.abandoned).count(), samples),
provider_failure_rate: ratio(
all.iter().filter(|s| s.provider_failures > 0).count(),
samples,
),
user_experience: self.judge_summaries(),
}
}
#[must_use]
pub fn judge_summaries(&self) -> Vec<CriterionSummary> {
let mut summaries = Vec::new();
for criterion in JudgeCriterion::ALL {
let outcomes: Vec<&CriterionOutcome> = self
.items
.iter()
.flat_map(|item| item.samples.iter())
.flat_map(|sample| sample.judge.iter())
.filter(|outcome| outcome.criterion == criterion)
.collect();
if outcomes.is_empty() {
continue;
}
summaries.push(summarize(criterion, &outcomes));
}
summaries
}
fn stats(&self, category: ReliabilityCategory) -> CategoryStats {
let mut stats = CategoryStats {
samples: self.total_samples(),
..CategoryStats::default()
};
for item in &self.items {
stats.failing_samples += item.samples_failing(category);
stats.failures += item
.failures()
.into_iter()
.filter(|failure| ReliabilityCategory::of(failure.expectation) == category)
.count();
}
stats
}
pub fn to_json(&self) -> Result<String, ReportError> {
serde_json::to_string_pretty(self).map_err(|error| ReportError::Serialize {
message: error.to_string(),
})
}
pub fn from_json(source: &str) -> Result<Self, ReportError> {
serde_json::from_str(source).map_err(|error| ReportError::Deserialize {
message: error.to_string(),
})
}
#[must_use]
pub fn summary(&self) -> String {
use std::fmt::Write as _;
let mut out = String::new();
let _ = writeln!(
out,
"suite {}: {} items, {} samples/item, {} votes/sample",
self.suite,
self.items.len(),
self.config.execution.samples_per_item,
self.config.judging.votes_per_sample
);
let _ = writeln!(
out,
"deterministic pass rate {:.0}% ({}/{} samples)",
self.deterministic_pass_rate() * 100.0,
self.samples_passed(),
self.total_samples()
);
let refused: usize = self.items.iter().map(ItemReport::refused_proposals).sum();
let measured: usize = self
.items
.iter()
.flat_map(|item| &item.samples)
.filter(|sample| sample.harness_error.is_none())
.count();
if measured > 0 {
#[allow(clippy::cast_precision_loss)] let share = refused as f64 / measured as f64 * 100.0;
let _ = writeln!(
out,
"refused proposals {share:.0}% ({refused}/{measured} measured samples): \
the model proposed an act and nothing was journaled"
);
let discarded: usize = self
.items
.iter()
.map(ItemReport::samples_with_discards)
.sum();
#[allow(clippy::cast_precision_loss)] let share = discarded as f64 / measured as f64 * 100.0;
let _ = writeln!(
out,
"discarded answers {share:.0}% ({discarded}/{measured} measured samples): \
the runtime threw the model's answer away whole{}",
match self.discard_codes() {
codes if codes.is_empty() => String::new(),
codes => format!(": {codes}"),
}
);
}
if let Some(line) = self.task_accuracy() {
let _ = writeln!(out, "understanding by task: {line}");
}
for item in &self.items {
write_item(&mut out, item);
}
write_reliability(&mut out, &self.reliability());
out
}
#[must_use]
pub fn task_accuracy(&self) -> Option<String> {
let samples: Vec<&SampleReport> = self
.items
.iter()
.flat_map(|item| &item.samples)
.filter(|sample| sample.harness_error.is_none())
.collect();
let tasks = crate::understanding::TaskScores::default().by_task();
let line: Vec<String> = tasks
.iter()
.enumerate()
.filter_map(|(at, (task, _))| {
let scored: Vec<bool> = samples
.iter()
.filter_map(|sample| sample.tasks.by_task()[at].1)
.collect();
if scored.is_empty() {
return None;
}
let passed = scored.iter().filter(|pass| **pass).count();
Some(format!("{task} {passed}/{}", scored.len()))
})
.collect();
(!line.is_empty()).then(|| line.join(" · "))
}
#[must_use]
pub fn discard_codes(&self) -> String {
let mut counts: std::collections::BTreeMap<&str, usize> = std::collections::BTreeMap::new();
for sample in self.items.iter().flat_map(|item| &item.samples) {
for code in &sample.discarded_answers {
*counts.entry(code.as_str()).or_default() += 1;
}
}
let mut ordered: Vec<(&str, usize)> = counts.into_iter().collect();
ordered.sort_by(|left, right| right.1.cmp(&left.1).then_with(|| left.0.cmp(right.0)));
ordered
.iter()
.map(|(code, count)| format!("{code}×{count}"))
.collect::<Vec<_>>()
.join(", ")
}
#[must_use]
pub fn gate(&self, thresholds: &GateThresholds) -> GateOutcome {
let mut violations = Vec::new();
let rate = self.deterministic_pass_rate();
if rate < thresholds.min_deterministic_pass_rate {
violations.push(GateViolation {
scope: self.suite.clone(),
rule: "min_deterministic_pass_rate".to_owned(),
detail: format!("{rate:.3} < {:.3}", thresholds.min_deterministic_pass_rate),
});
}
let reliability = self.reliability();
if thresholds.forbid_side_effect_failures
&& reliability.side_effect_integrity.failing_samples > 0
{
violations.push(GateViolation {
scope: self.suite.clone(),
rule: "forbid_side_effect_failures".to_owned(),
detail: format!(
"{} samples failed a side-effect integrity assertion",
reliability.side_effect_integrity.failing_samples
),
});
}
if reliability.abandonment_rate > thresholds.max_abandonment_rate {
violations.push(GateViolation {
scope: self.suite.clone(),
rule: "max_abandonment_rate".to_owned(),
detail: format!(
"{:.3} > {:.3}",
reliability.abandonment_rate, thresholds.max_abandonment_rate
),
});
}
if !thresholds.allow_flaky_items {
for item in self.flaky_items() {
violations.push(GateViolation {
scope: item.id.to_string(),
rule: "allow_flaky_items".to_owned(),
detail: format!(
"{}/{} samples passed",
item.samples_passed(),
item.total_samples()
),
});
}
}
if let Some(minimum) = thresholds.min_judge_score {
for summary in &reliability.user_experience {
if summary.mean_score.is_some_and(|score| score < minimum) {
violations.push(GateViolation {
scope: summary.criterion.to_string(),
rule: "min_judge_score".to_owned(),
detail: format!(
"{:.2} < {minimum:.2}",
summary.mean_score.unwrap_or_default()
),
});
}
}
}
GateOutcome {
passed: violations.is_empty(),
violations,
}
}
}
fn write_item(out: &mut String, item: &ItemReport) {
use std::fmt::Write as _;
let variance = item.variance();
let _ = writeln!(
out,
" {}: {}/{} samples passed, {} distinct behaviour(s){}",
item.id,
item.samples_passed(),
item.total_samples(),
variance.distinct_behaviours,
if item.is_flaky() { ", FLAKY" } else { "" }
);
for sample in &item.samples {
if let Some(error) = &sample.harness_error {
let _ = writeln!(out, " sample {} not measured: {error}", sample.sample);
}
}
let discarded = item.samples_with_discards();
if discarded > 0 {
let mut codes: Vec<&str> = item
.samples
.iter()
.flat_map(|sample| sample.discarded_answers.iter().map(String::as_str))
.collect();
codes.sort_unstable();
codes.dedup();
let measured = item
.samples
.iter()
.filter(|sample| sample.harness_error.is_none())
.count();
let _ = writeln!(
out,
" {discarded}/{measured} sample(s) had an answer discarded whole: {}",
codes.join(", ")
);
}
for failure in item.failures() {
let _ = writeln!(
out,
" [{}] {failure}",
ReliabilityCategory::of(failure.expectation)
);
}
for summary in item.judge_summaries() {
let _ = writeln!(
out,
" judge {}: mean {:.2} over {} sample(s), vote spread {:.2}",
summary.criterion,
summary.mean_score.unwrap_or_default(),
summary.samples_judged,
summary.mean_vote_spread
);
}
}
fn write_reliability(out: &mut String, reliability: &Reliability) {
use std::fmt::Write as _;
let _ = writeln!(out, "reliability (spec §26.3, categories kept apart):");
for (label, stats) in [
("side-effect integrity", reliability.side_effect_integrity),
(
"operational claim integrity",
reliability.operational_claim_integrity,
),
(
"semantic interpretation",
reliability.semantic_interpretation,
),
(
"clarification integrity",
reliability.clarification_integrity,
),
] {
let _ = writeln!(
out,
" {label}: {} failing sample(s), {} failure(s)",
stats.failing_samples, stats.failures
);
}
let _ = writeln!(
out,
" clarification rate {:.0}%, abandonment rate {:.0}%, provider failure rate {:.0}%",
reliability.clarification_rate * 100.0,
reliability.abandonment_rate * 100.0,
reliability.provider_failure_rate * 100.0
);
for summary in &reliability.user_experience {
let _ = writeln!(
out,
" user experience, {}: mean {:.2}, vote spread {:.2}",
summary.criterion,
summary.mean_score.unwrap_or_default(),
summary.mean_vote_spread
);
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct GateThresholds {
pub min_deterministic_pass_rate: f64,
pub max_abandonment_rate: f64,
pub allow_flaky_items: bool,
pub forbid_side_effect_failures: bool,
pub min_judge_score: Option<f64>,
}
impl Default for GateThresholds {
fn default() -> Self {
Self {
min_deterministic_pass_rate: 1.0,
max_abandonment_rate: 0.0,
allow_flaky_items: false,
forbid_side_effect_failures: true,
min_judge_score: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GateOutcome {
pub passed: bool,
pub violations: Vec<GateViolation>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GateViolation {
pub scope: String,
pub rule: String,
pub detail: String,
}
impl std::fmt::Display for GateViolation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} [{}]: {}", self.rule, self.scope, self.detail)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ReportError {
#[error("report could not be serialized: {message}")]
Serialize {
message: String,
},
#[error("report could not be read: {message}")]
Deserialize {
message: String,
},
}
#[cfg(test)]
mod tests {
#[test]
fn a_refused_proposal_is_counted_and_a_carried_one_is_not() {
let mut item = ItemReport {
id: ItemId::new("i"),
name: "An item".to_owned(),
tags: Vec::new(),
fingerprint: ItemFingerprint::default(),
samples: vec![sample(1, "A", false)],
};
item.samples[0].acts_proposed = 1;
item.samples[0].acts_refused = 0;
item.samples[0].commands_journaled = 1;
assert_eq!(item.refused_proposals(), 0);
item.samples[0].acts_refused = 1;
item.samples[0].commands_journaled = 0;
assert_eq!(item.refused_proposals(), 1);
item.samples[0].acts_refused = 0;
assert_eq!(item.refused_proposals(), 0);
item.samples[0].acts_proposed = 2;
item.samples[0].acts_refused = 1;
item.samples[0].commands_journaled = 1;
assert_eq!(item.refused_proposals(), 1);
item.samples[0].acts_proposed = 0;
item.samples[0].acts_refused = 0;
assert_eq!(item.refused_proposals(), 0);
item.samples[0].acts_proposed = 1;
item.samples[0].acts_refused = 1;
item.samples[0].harness_error = Some("no world".to_owned());
assert_eq!(item.refused_proposals(), 0);
}
use super::*;
use crate::judge::{JudgeVerdict, JudgeVote};
fn sample(index: u32, signature: &str, failing: bool) -> SampleReport {
SampleReport {
sample: index,
failures: if failing {
vec![AssertionFailure::new(
ExpectationName::Commands,
"[a]",
"[b]",
)]
} else {
Vec::new()
},
harness_error: None,
signature: signature.to_owned(),
judge: Vec::new(),
acts_proposed: 0,
acts_refused: 0,
commands_journaled: 0,
provider_failures: 0,
cards_created: 0,
abandoned: false,
discarded_answers: Vec::new(),
answer: String::new(),
tasks: Default::default(),
}
}
fn report(samples: Vec<SampleReport>) -> EvalReport {
EvalReport::new(
"s",
DateTime::from_timestamp(0, 0).unwrap_or_default(),
EvalConfig::default(),
vec![ItemReport {
id: ItemId::new("i"),
name: "An item".to_owned(),
tags: Vec::new(),
fingerprint: ItemFingerprint::default(),
samples,
}],
)
}
#[test]
fn a_discarded_answer_is_reported_even_when_every_sample_passed() {
let mut passing = sample(1, "A", false);
passing.discarded_answers = vec!["evidence".to_owned()];
let mut clean = sample(2, "A", false);
clean.discarded_answers.clear();
let report = report(vec![passing, clean]);
assert_eq!(report.items[0].samples_with_discards(), 1);
assert!(
(report.deterministic_pass_rate() - 1.0).abs() < 1e-9,
"every sample passed, which is the whole point of the number"
);
let summary = report.summary();
assert!(
summary.contains("discarded answers 50% (1/2 measured samples)"),
"the run-wide line states the rate: {summary}"
);
assert!(
summary.contains("evidence×1"),
"and which code it was, because the answer differs by code: {summary}"
);
assert!(
summary.contains("1/2 sample(s) had an answer discarded whole: evidence"),
"the item says it next to its own numbers too: {summary}"
);
}
#[test]
fn a_sample_that_lost_three_answers_is_one_sample() {
let mut struggled = sample(1, "A", false);
struggled.discarded_answers = vec![
"evidence".to_owned(),
"evidence".to_owned(),
"schema_violation".to_owned(),
];
let report = report(vec![struggled, sample(2, "A", false)]);
assert_eq!(report.items[0].samples_with_discards(), 1);
assert_eq!(report.discard_codes(), "evidence×2, schema_violation×1");
}
#[test]
fn an_unmeasured_sample_discards_nothing() {
let mut never_ran = sample(1, "A", false);
never_ran.discarded_answers = vec!["evidence".to_owned()];
never_ran.harness_error = Some("no world".to_owned());
let report = report(vec![never_ran]);
assert_eq!(report.items[0].samples_with_discards(), 0);
}
#[test]
fn variance_counts_distinct_behaviours_not_failures() {
let report = report(vec![
sample(1, "A", false),
sample(2, "B", true),
sample(3, "A", false),
]);
let variance = report.items[0].variance();
assert_eq!(variance.distinct_behaviours, 2);
assert!((variance.pass_rate - 2.0 / 3.0).abs() < 1e-9);
assert!(variance.pass_variance > 0.0);
assert_eq!(variance.behaviours[0].count, 2);
assert!(report.items[0].is_flaky());
}
#[test]
fn a_side_effect_failure_is_not_averaged_into_a_language_score() {
let mut failing = sample(1, "A", true);
failing.judge = vec![CriterionOutcome {
criterion: JudgeCriterion::Tone,
votes: vec![JudgeVote {
vote: 1,
verdict: Some(JudgeVerdict {
score: 5,
reason: "fine".to_owned(),
}),
error: None,
}],
}];
let report = report(vec![failing]);
let reliability = report.reliability();
assert_eq!(reliability.side_effect_integrity.failing_samples, 1);
assert_eq!(reliability.user_experience[0].mean_score, Some(5.0));
assert!((report.deterministic_pass_rate() - 0.0).abs() < 1e-9);
}
#[test]
fn the_default_gate_refuses_a_side_effect_failure() {
let outcome = report(vec![sample(1, "A", true)]).gate(&GateThresholds::default());
assert!(!outcome.passed);
assert!(
outcome
.violations
.iter()
.any(|v| v.rule == "forbid_side_effect_failures"),
"{:?}",
outcome.violations
);
}
#[test]
fn a_report_round_trips_through_its_machine_readable_form() {
let report = report(vec![sample(1, "A", false)]);
let json = report.to_json().unwrap();
assert_eq!(EvalReport::from_json(&json).unwrap(), report);
}
}