use amberfork_model::{DiffResult, Recovery};
use crate::ddmin::Reduction;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Candidate {
pub(crate) bad_step: usize,
pub(crate) good_step: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Labeling {
pub(crate) origin_step: Option<usize>,
pub(crate) propagation: Vec<usize>,
pub(crate) recovered: Recovery,
}
pub(crate) fn fork_candidates(diff: &DiffResult) -> Vec<Candidate> {
let Some(fork) = diff.fork else {
return Vec::new();
};
diff.alignment
.get(fork.index..)
.unwrap_or(&[])
.iter()
.map_while(|mv| {
Some(Candidate {
bad_step: mv.b_idx?,
good_step: mv.a_idx?,
})
})
.collect()
}
pub(crate) fn relabel(
reduction: &Reduction,
candidates: &[Candidate],
region: &[usize],
) -> Labeling {
match reduction {
Reduction::Minimized(indices) => {
let cause: Vec<usize> = indices
.iter()
.filter_map(|&i| candidates.get(i).map(|c| c.bad_step))
.collect();
let origin_step = cause.iter().copied().min();
let propagation = region
.iter()
.copied()
.filter(|step| !cause.contains(step))
.collect();
Labeling {
origin_step,
propagation,
recovered: Recovery::Recovered,
}
}
Reduction::Persisted => static_labels(region, Recovery::NotRecovered),
Reduction::Inconclusive => static_labels(region, Recovery::Unverified),
}
}
fn static_labels(region: &[usize], recovered: Recovery) -> Labeling {
Labeling {
origin_step: region.first().copied(),
propagation: region.get(1..).unwrap_or(&[]).to_vec(),
recovered,
}
}
#[cfg(test)]
mod tests {
use super::*;
use amberfork_model::{Fork, Meta, Move, RunPair, RunRef, Source};
fn run_ref(id: &str, n_steps: usize) -> RunRef {
RunRef {
id: id.to_string(),
task: None,
outcome: None,
n_steps,
}
}
fn diff_with(alignment: Vec<Move>, fork: Option<Fork>) -> DiffResult {
DiffResult {
runs: RunPair {
a: run_ref("good", alignment.len()),
b: run_ref("bad", alignment.len()),
},
alignment,
fork,
field_diffs: Vec::new(),
attribution: None,
deltas: None,
warnings: Vec::new(),
meta: Meta::current(Source::Record),
}
}
fn fork_at(index: usize) -> Fork {
Fork {
index,
a_step: Some(index),
b_step: Some(index),
confidence: 0.9,
}
}
fn candidate(bad_step: usize, good_step: usize) -> Candidate {
Candidate {
bad_step,
good_step,
}
}
#[test]
fn candidates_are_the_two_sided_moves_from_the_fork_onward() {
let alignment = vec![
Move::sync(0, 0, 0.0, 1.0),
Move::sync(1, 1, 0.9, 0.1), Move::sync(2, 2, 0.8, 0.2),
Move::log(3, 0.6, 0.9),
];
let got = fork_candidates(&diff_with(alignment, Some(fork_at(1))));
assert_eq!(got, vec![candidate(1, 1), candidate(2, 2)]);
}
#[test]
fn the_candidate_run_stops_at_the_first_one_sided_move() {
let alignment = vec![
Move::sync(0, 0, 0.0, 1.0),
Move::sync(1, 1, 0.9, 0.1), Move::model(2, 0.7, 0.8), Move::sync(3, 2, 0.8, 0.2),
];
let got = fork_candidates(&diff_with(alignment, Some(fork_at(1))));
assert_eq!(got, vec![candidate(1, 1)]);
}
#[test]
fn a_one_sided_fork_has_no_candidates() {
let alignment = vec![Move::sync(0, 0, 0.0, 1.0), Move::log(1, 0.7, 0.9)];
let fork = Fork {
index: 1,
a_step: None,
b_step: Some(1),
confidence: 0.5,
};
assert!(fork_candidates(&diff_with(alignment, Some(fork))).is_empty());
}
#[test]
fn no_fork_has_no_candidates() {
let alignment = vec![Move::sync(0, 0, 0.0, 1.0)];
assert!(fork_candidates(&diff_with(alignment, None)).is_empty());
}
#[test]
fn a_single_cause_labels_the_whole_tail_propagation() {
let candidates = [candidate(1, 1), candidate(2, 2), candidate(3, 3)];
let region = [1, 2, 3];
let labeling = relabel(&Reduction::Minimized(vec![0]), &candidates, ®ion);
assert_eq!(labeling.origin_step, Some(1));
assert_eq!(labeling.propagation, vec![2, 3]);
assert_eq!(labeling.recovered, Recovery::Recovered);
}
#[test]
fn an_independent_downstream_fault_is_pulled_out_of_propagation() {
let candidates = [candidate(1, 1), candidate(2, 2), candidate(3, 3)];
let region = [1, 2, 3];
let labeling = relabel(&Reduction::Minimized(vec![0, 2]), &candidates, ®ion);
assert_eq!(labeling.origin_step, Some(1));
assert_eq!(
labeling.propagation,
vec![2],
"step 3 is origination, not propagation"
);
assert_eq!(labeling.recovered, Recovery::Recovered);
}
#[test]
fn a_region_that_never_recovers_keeps_static_labels_as_not_recovered() {
let candidates = [candidate(1, 1), candidate(2, 2)];
let region = [1, 2, 3]; let labeling = relabel(&Reduction::Persisted, &candidates, ®ion);
assert_eq!(labeling.origin_step, Some(1));
assert_eq!(
labeling.propagation,
vec![2, 3],
"static tail preserved verbatim"
);
assert_eq!(labeling.recovered, Recovery::NotRecovered);
}
#[test]
fn an_inconclusive_oracle_keeps_static_labels_as_unverified() {
let candidates = [candidate(1, 1), candidate(2, 2)];
let region = [1, 2];
let labeling = relabel(&Reduction::Inconclusive, &candidates, ®ion);
assert_eq!(labeling.origin_step, Some(1));
assert_eq!(labeling.propagation, vec![2]);
assert_eq!(labeling.recovered, Recovery::Unverified);
}
}