use std::collections::HashMap;
use async_trait::async_trait;
use car_eventlog::harness_adapt::{diagnose_from_jsonl, HarnessIntervention, InterventionLayer};
use serde::{Deserialize, Serialize};
use super::contract::OutcomeContract;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AbTask {
pub id: String,
pub intent: String,
pub contract: OutcomeContract,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repo: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Arm {
Native,
External(String),
}
impl Arm {
pub fn label(&self) -> String {
match self {
Self::Native => "native".to_string(),
Self::External(id) => id.clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ArmOutcome {
pub passed: bool,
pub iterations: u32,
#[serde(default)]
pub cost_usd: f64,
pub wall_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transcript_path: Option<String>,
#[serde(default)]
pub infra_failed: bool,
}
impl ArmOutcome {
pub fn infra(reason: impl Into<String>, wall_ms: u64) -> Self {
Self {
passed: false,
iterations: 0,
cost_usd: 0.0,
wall_ms,
error: Some(reason.into()),
transcript_path: None,
infra_failed: true,
}
}
pub fn scorable(&self) -> bool {
!self.infra_failed
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AbCell {
pub task_id: String,
pub native: ArmOutcome,
pub external: ArmOutcome,
pub external_engine: String,
}
impl AbCell {
pub fn paired_scorable(&self) -> bool {
self.native.scorable() && self.external.scorable()
}
}
const CHI2_CRIT_05: f64 = 3.841;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PairedStats {
pub paired_tasks: usize,
pub native_passes: usize,
pub external_passes: usize,
pub native_pass_rate: f64,
pub external_pass_rate: f64,
pub pass_rate_delta: f64,
pub both_pass: usize,
pub native_only: usize,
pub external_only: usize,
pub both_fail: usize,
pub mcnemar_chi2: f64,
pub mcnemar_significant_05: bool,
pub native_infra_failures: usize,
pub external_infra_failures: usize,
pub native_mean_cost_usd: f64,
pub external_mean_cost_usd: f64,
pub native_cost_per_pass: Option<f64>,
pub external_cost_per_pass: Option<f64>,
}
impl PairedStats {
fn from_cells(cells: &[AbCell]) -> Self {
let native_infra_failures = cells.iter().filter(|c| c.native.infra_failed).count();
let external_infra_failures = cells.iter().filter(|c| c.external.infra_failed).count();
let paired: Vec<&AbCell> = cells.iter().filter(|c| c.paired_scorable()).collect();
let paired_tasks = paired.len();
let native_passes = paired.iter().filter(|c| c.native.passed).count();
let external_passes = paired.iter().filter(|c| c.external.passed).count();
let rate = |n: usize| {
if paired_tasks == 0 {
0.0
} else {
n as f64 / paired_tasks as f64
}
};
let native_pass_rate = rate(native_passes);
let external_pass_rate = rate(external_passes);
let both_pass = paired
.iter()
.filter(|c| c.native.passed && c.external.passed)
.count();
let native_only = paired
.iter()
.filter(|c| c.native.passed && !c.external.passed)
.count();
let external_only = paired
.iter()
.filter(|c| !c.native.passed && c.external.passed)
.count();
let both_fail = paired
.iter()
.filter(|c| !c.native.passed && !c.external.passed)
.count();
let b = native_only as f64;
let c = external_only as f64;
let discordant = b + c;
let mcnemar_chi2 = if discordant > 0.0 {
let num = (b - c).abs() - 1.0;
let num = num.max(0.0);
num * num / discordant
} else {
0.0
};
let mcnemar_significant_05 = discordant > 0.0 && mcnemar_chi2 > CHI2_CRIT_05;
let mean = |sel: &dyn Fn(&AbCell) -> Option<f64>| {
let xs: Vec<f64> = cells.iter().filter_map(sel).collect();
if xs.is_empty() {
0.0
} else {
xs.iter().sum::<f64>() / xs.len() as f64
}
};
let native_mean_cost_usd = mean(&|c| c.native.scorable().then_some(c.native.cost_usd));
let external_mean_cost_usd =
mean(&|c| c.external.scorable().then_some(c.external.cost_usd));
let cost_per_pass = |sel: &dyn Fn(&AbCell) -> &ArmOutcome| {
let scorable: Vec<&ArmOutcome> =
cells.iter().map(sel).filter(|o| o.scorable()).collect();
let passes = scorable.iter().filter(|o| o.passed).count();
if passes == 0 {
None
} else {
let total: f64 = scorable.iter().map(|o| o.cost_usd).sum();
Some(total / passes as f64)
}
};
let native_cost_per_pass = cost_per_pass(&|c| &c.native);
let external_cost_per_pass = cost_per_pass(&|c| &c.external);
Self {
paired_tasks,
native_passes,
external_passes,
native_pass_rate,
external_pass_rate,
pass_rate_delta: native_pass_rate - external_pass_rate,
both_pass,
native_only,
external_only,
both_fail,
mcnemar_chi2,
mcnemar_significant_05,
native_infra_failures,
external_infra_failures,
native_mean_cost_usd,
external_mean_cost_usd,
native_cost_per_pass,
external_cost_per_pass,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AbReport {
pub backbone: Option<String>,
pub external_engine: String,
pub cells: Vec<AbCell>,
pub stats: PairedStats,
}
impl AbReport {
pub fn from_cells(cells: Vec<AbCell>, backbone: Option<String>) -> Self {
let external_engine = cells
.first()
.map(|c| c.external_engine.clone())
.unwrap_or_default();
let stats = PairedStats::from_cells(&cells);
Self {
backbone,
external_engine,
cells,
stats,
}
}
pub fn summary_line(&self) -> String {
let s = &self.stats;
let sig = if s.mcnemar_significant_05 {
"significant (p<.05)"
} else {
"not significant"
};
format!(
"native {}/{} ({:.0}%) vs {} {}/{} ({:.0}%) over {} paired tasks — delta {:+.1} pp, McNemar χ²={:.2} {}",
s.native_passes,
s.paired_tasks,
s.native_pass_rate * 100.0,
self.external_engine,
s.external_passes,
s.paired_tasks,
s.external_pass_rate * 100.0,
s.paired_tasks,
s.pass_rate_delta * 100.0,
s.mcnemar_chi2,
sig,
)
}
}
#[async_trait]
pub trait AbArmRunner: Send + Sync {
async fn run_arm(&self, task: &AbTask, arm: &Arm) -> ArmOutcome;
}
pub async fn run_ab_suite(
tasks: &[AbTask],
external_engine: &str,
backbone: Option<String>,
runner: &dyn AbArmRunner,
) -> AbReport {
run_ab_suite_resumable(
tasks,
external_engine,
backbone,
runner,
Vec::new(),
None,
|_| {},
)
.await
}
pub async fn run_ab_suite_resumable(
tasks: &[AbTask],
external_engine: &str,
backbone: Option<String>,
runner: &dyn AbArmRunner,
done: Vec<AbCell>,
limit: Option<usize>,
mut on_cell: impl FnMut(&AbCell),
) -> AbReport {
let done_ids: std::collections::HashSet<String> =
done.iter().map(|c| c.task_id.clone()).collect();
let mut cells = done;
let mut ran = 0usize;
for task in tasks {
if done_ids.contains(&task.id) {
continue; }
if limit.is_some_and(|lim| ran >= lim) {
break; }
let native = runner.run_arm(task, &Arm::Native).await;
let external = runner
.run_arm(task, &Arm::External(external_engine.to_string()))
.await;
let cell = AbCell {
task_id: task.id.clone(),
native,
external,
external_engine: external_engine.to_string(),
};
on_cell(&cell); cells.push(cell);
ran += 1;
}
AbReport::from_cells(cells, backbone)
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RoundAttribution {
pub harness_addressable_losses: Vec<String>,
pub backbone_bound_losses: Vec<String>,
pub interventions: Vec<HarnessIntervention>,
}
impl RoundAttribution {
pub fn has_lever(&self) -> bool {
!self.interventions.is_empty()
}
}
pub fn is_evolution_actionable(layer: InterventionLayer) -> bool {
matches!(
layer,
InterventionLayer::EnvironmentContract
| InterventionLayer::ActionRealization
| InterventionLayer::TrajectoryRegulation
)
}
pub fn attribute_round(
report: &AbReport,
read_events: impl Fn(&str) -> Option<String>,
min_occurrences: usize,
) -> RoundAttribution {
let mut addressable = Vec::new();
let mut backbone = Vec::new();
let mut merged: HashMap<(String, String), HarnessIntervention> = HashMap::new();
for cell in &report.cells {
if !cell.paired_scorable() || cell.native.passed {
continue;
}
let jsonl = cell
.native
.transcript_path
.as_deref()
.and_then(&read_events)
.unwrap_or_default();
let diag = diagnose_from_jsonl(&jsonl, min_occurrences);
let actionable: Vec<HarnessIntervention> = diag
.interventions
.into_iter()
.filter(|iv| is_evolution_actionable(iv.layer))
.collect();
if actionable.is_empty() {
backbone.push(cell.task_id.clone());
} else {
addressable.push(cell.task_id.clone());
for iv in actionable {
let key = (format!("{:?}", iv.layer), iv.target.clone());
merged
.entry(key)
.and_modify(|e| e.evidence_count += iv.evidence_count)
.or_insert(iv);
}
}
}
let mut interventions: Vec<HarnessIntervention> = merged.into_values().collect();
interventions.sort_by(|a, b| b.evidence_count.cmp(&a.evidence_count));
RoundAttribution {
harness_addressable_losses: addressable,
backbone_bound_losses: backbone,
interventions,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn contract() -> OutcomeContract {
OutcomeContract {
description: "tests pass".into(),
checks: vec![],
}
}
fn task(id: &str) -> AbTask {
AbTask {
id: id.into(),
intent: format!("do {id}"),
contract: contract(),
repo: None,
}
}
fn done(passed: bool, cost: f64) -> ArmOutcome {
ArmOutcome {
passed,
iterations: 1,
cost_usd: cost,
wall_ms: 10,
error: None,
transcript_path: None,
infra_failed: false,
}
}
struct Scripted(HashMap<(String, String), ArmOutcome>);
#[async_trait]
impl AbArmRunner for Scripted {
async fn run_arm(&self, task: &AbTask, arm: &Arm) -> ArmOutcome {
self.0
.get(&(task.id.clone(), arm.label()))
.cloned()
.unwrap_or_else(|| ArmOutcome::infra("no script", 0))
}
}
fn script(entries: Vec<(&str, &str, ArmOutcome)>) -> Scripted {
Scripted(
entries
.into_iter()
.map(|(t, a, o)| ((t.to_string(), a.to_string()), o))
.collect(),
)
}
#[tokio::test]
async fn paired_delta_and_mcnemar_over_discordant_pairs() {
let tasks: Vec<AbTask> = (0..5).map(|i| task(&format!("t{i}"))).collect();
let runner = script(vec![
("t0", "native", done(true, 0.10)),
("t0", "codex", done(false, 0.20)),
("t1", "native", done(true, 0.10)),
("t1", "codex", done(false, 0.20)),
("t2", "native", done(true, 0.10)),
("t2", "codex", done(false, 0.20)),
("t3", "native", done(true, 0.10)),
("t3", "codex", done(true, 0.20)),
("t4", "native", done(false, 0.10)),
("t4", "codex", done(false, 0.20)),
]);
let report = run_ab_suite(&tasks, "codex", Some("parslee/reasoning".into()), &runner).await;
let s = &report.stats;
assert_eq!(s.paired_tasks, 5);
assert_eq!(s.native_passes, 4);
assert_eq!(s.external_passes, 1);
assert_eq!(s.native_only, 3);
assert_eq!(s.external_only, 0);
assert_eq!(s.both_pass, 1);
assert_eq!(s.both_fail, 1);
assert!((s.pass_rate_delta - 0.6).abs() < 1e-9);
assert!((s.mcnemar_chi2 - 4.0 / 3.0).abs() < 1e-9);
assert!(!s.mcnemar_significant_05);
assert!((s.native_cost_per_pass.unwrap() - 0.50 / 4.0).abs() < 1e-9);
assert!((s.external_cost_per_pass.unwrap() - 1.0).abs() < 1e-9);
}
#[tokio::test]
async fn resumable_skips_done_caps_by_limit_and_observes_each_cell() {
let tasks: Vec<AbTask> = (0..4).map(|i| task(&format!("t{i}"))).collect();
let runner = script(vec![
("t0", "native", done(true, 0.0)),
("t0", "codex", done(false, 0.0)),
("t1", "native", done(true, 0.0)),
("t1", "codex", done(false, 0.0)),
("t2", "native", done(true, 0.0)),
("t2", "codex", done(false, 0.0)),
("t3", "native", done(true, 0.0)),
("t3", "codex", done(false, 0.0)),
]);
let cell = |id: &str| AbCell {
task_id: id.into(),
native: done(true, 0.0),
external: done(false, 0.0),
external_engine: "codex".into(),
};
let mut observed: Vec<String> = Vec::new();
let report = run_ab_suite_resumable(
&tasks,
"codex",
None,
&runner,
vec![cell("t0")],
Some(1),
|c| observed.push(c.task_id.clone()),
)
.await;
assert_eq!(observed, vec!["t1"], "only the ONE new task fires on_cell");
assert_eq!(report.stats.paired_tasks, 2, "t0 (resumed) + t1 (new)");
let mut observed2: Vec<String> = Vec::new();
let report2 = run_ab_suite_resumable(
&tasks,
"codex",
None,
&runner,
vec![cell("t0"), cell("t1")],
None,
|c| observed2.push(c.task_id.clone()),
)
.await;
assert_eq!(observed2, vec!["t2", "t3"]);
assert_eq!(report2.stats.paired_tasks, 4, "all scored once resumed");
}
#[tokio::test]
async fn strong_discordance_is_significant() {
let tasks: Vec<AbTask> = (0..10).map(|i| task(&format!("t{i}"))).collect();
let mut entries = Vec::new();
for i in 0..10 {
let id = format!("t{i}");
entries.push((id.clone(), "native".to_string(), done(true, 0.0)));
entries.push((id.clone(), "codex".to_string(), done(false, 0.0)));
}
let runner = Scripted(entries.into_iter().map(|(t, a, o)| ((t, a), o)).collect());
let report = run_ab_suite(&tasks, "codex", None, &runner).await;
assert!((report.stats.mcnemar_chi2 - 8.1).abs() < 1e-9);
assert!(report.stats.mcnemar_significant_05);
}
#[tokio::test]
async fn infra_failures_are_excluded_from_the_denominator() {
let tasks: Vec<AbTask> = (0..3).map(|i| task(&format!("t{i}"))).collect();
let runner = script(vec![
("t0", "native", done(true, 0.0)),
("t0", "codex", done(true, 0.0)),
("t1", "native", done(true, 0.0)),
("t1", "codex", done(false, 0.0)),
("t2", "native", done(true, 0.0)),
("t2", "codex", ArmOutcome::infra("codex launch failed", 5)),
]);
let report = run_ab_suite(&tasks, "codex", None, &runner).await;
let s = &report.stats;
assert_eq!(s.paired_tasks, 2, "t2 excluded — external infra failure");
assert_eq!(s.external_infra_failures, 1);
assert_eq!(s.native_passes, 2);
assert_eq!(s.external_passes, 1);
}
#[tokio::test]
async fn empty_suite_is_well_defined() {
let report = run_ab_suite(&[], "codex", None, &NoRunner).await;
assert_eq!(report.stats.paired_tasks, 0);
assert_eq!(report.stats.native_pass_rate, 0.0);
assert!(!report.stats.mcnemar_significant_05);
assert_eq!(report.stats.native_cost_per_pass, None);
}
struct NoRunner;
#[async_trait]
impl AbArmRunner for NoRunner {
async fn run_arm(&self, _task: &AbTask, _arm: &Arm) -> ArmOutcome {
ArmOutcome::infra("unused", 0)
}
}
#[test]
fn summary_line_is_readable() {
let cells = vec![AbCell {
task_id: "t0".into(),
native: done(true, 0.1),
external: done(false, 0.2),
external_engine: "codex".into(),
}];
let report = AbReport::from_cells(cells, Some("parslee/reasoning".into()));
let line = report.summary_line();
assert!(line.contains("native 1/1"));
assert!(line.contains("codex 0/1"));
}
fn lost_with_transcript(key: &str) -> ArmOutcome {
ArmOutcome {
passed: false,
iterations: 3,
cost_usd: 0.0,
wall_ms: 10,
error: None,
transcript_path: Some(key.into()),
infra_failed: false,
}
}
fn failing_transcript(action: &str, err: &str, n: usize) -> String {
(0..n)
.map(|_| {
format!(
r#"{{"kind":"action_failed","action_id":"{action}","data":{{"error":"{err}"}}}}"#
)
})
.collect::<Vec<_>>()
.join("\n")
}
fn report_with(cells: Vec<AbCell>) -> AbReport {
AbReport::from_cells(cells, Some("parslee/reasoning".into()))
}
#[test]
fn native_loss_with_recurring_failure_is_harness_addressable() {
let cells = vec![AbCell {
task_id: "t0".into(),
native: lost_with_transcript("t0.jsonl"),
external: done(true, 0.0), external_engine: "codex".into(),
}];
let report = report_with(cells);
let events: HashMap<String, String> = [(
"t0.jsonl".to_string(),
failing_transcript("run_command", "exited 1 at runtime", 3),
)]
.into_iter()
.collect();
let attr = attribute_round(&report, |p| events.get(p).cloned(), 2);
assert_eq!(attr.harness_addressable_losses, vec!["t0".to_string()]);
assert!(attr.backbone_bound_losses.is_empty());
assert!(attr.has_lever());
assert_eq!(attr.interventions.len(), 1);
assert_eq!(
attr.interventions[0].layer,
InterventionLayer::TrajectoryRegulation
);
assert_eq!(attr.interventions[0].evidence_count, 3);
}
#[test]
fn clean_native_loss_is_backbone_bound() {
let cells = vec![AbCell {
task_id: "t1".into(),
native: lost_with_transcript("t1.jsonl"),
external: done(false, 0.0),
external_engine: "codex".into(),
}];
let report = report_with(cells);
let events: HashMap<String, String> = [("t1.jsonl".to_string(), String::new())]
.into_iter()
.collect();
let attr = attribute_round(&report, |p| events.get(p).cloned(), 2);
assert!(attr.harness_addressable_losses.is_empty());
assert_eq!(attr.backbone_bound_losses, vec!["t1".to_string()]);
assert!(!attr.has_lever());
}
#[test]
fn interventions_merge_and_rank_across_losing_cells() {
let cells = vec![
AbCell {
task_id: "a".into(),
native: lost_with_transcript("a.jsonl"),
external: done(true, 0.0),
external_engine: "codex".into(),
},
AbCell {
task_id: "b".into(),
native: lost_with_transcript("b.jsonl"),
external: done(true, 0.0),
external_engine: "codex".into(),
},
AbCell {
task_id: "c".into(),
native: lost_with_transcript("c.jsonl"),
external: done(true, 0.0),
external_engine: "codex".into(),
},
AbCell {
task_id: "d".into(),
native: done(true, 0.0),
external: done(true, 0.0),
external_engine: "codex".into(),
},
];
let report = report_with(cells);
let events: HashMap<String, String> = [
(
"a.jsonl".to_string(),
failing_transcript("run_command", "runtime boom", 2),
),
(
"b.jsonl".to_string(),
failing_transcript("run_command", "runtime boom", 3),
),
(
"c.jsonl".to_string(),
failing_transcript("edit_file", "runtime splat", 2),
),
]
.into_iter()
.collect();
let attr = attribute_round(&report, |p| events.get(p).cloned(), 2);
assert_eq!(attr.harness_addressable_losses.len(), 3);
assert_eq!(
attr.interventions.len(),
2,
"run_command merged, edit_file distinct"
);
assert_eq!(attr.interventions[0].target, "run_command");
assert_eq!(attr.interventions[0].evidence_count, 5);
assert_eq!(attr.interventions[1].target, "edit_file");
}
}