use crate::{Outcome, SchemaVersion};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RunPair {
pub a: RunRef,
pub b: RunRef,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RunRef {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub task: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub outcome: Option<Outcome>,
pub n_steps: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MoveKind {
Sync,
Log,
Model,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Move {
pub kind: MoveKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub a_idx: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub b_idx: Option<usize>,
pub cost: f64,
pub confidence: f64,
}
impl Move {
#[must_use]
pub fn sync(a_idx: usize, b_idx: usize, cost: f64, confidence: f64) -> Self {
Self {
kind: MoveKind::Sync,
a_idx: Some(a_idx),
b_idx: Some(b_idx),
cost,
confidence,
}
}
#[must_use]
pub fn log(b_idx: usize, cost: f64, confidence: f64) -> Self {
Self {
kind: MoveKind::Log,
a_idx: None,
b_idx: Some(b_idx),
cost,
confidence,
}
}
#[must_use]
pub fn model(a_idx: usize, cost: f64, confidence: f64) -> Self {
Self {
kind: MoveKind::Model,
a_idx: Some(a_idx),
b_idx: None,
cost,
confidence,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Fork {
pub index: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub a_step: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub b_step: Option<usize>,
pub confidence: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FieldDiffKind {
Added,
Removed,
Changed,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FieldDiff {
pub step: usize,
pub path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub before: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after: Option<Value>,
pub kind: FieldDiffKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AttributionMode {
Static,
Counterfactual,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Recovery {
Recovered,
NotRecovered,
Unverified,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Counterfactual {
pub recovered: Recovery,
pub runs: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Attribution {
pub mode: AttributionMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub origin_step: Option<usize>,
#[serde(default)]
pub propagation: Vec<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub counterfactual: Option<Counterfactual>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cause_label: Option<String>,
pub confidence: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Warning {
pub code: WarningCode,
pub msg: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum WarningCode {
UnmappedAttributes,
ContentAbsent,
SchemaVersionMismatch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Source {
Passive,
Record,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Meta {
pub schema_version: SchemaVersion,
pub source: Source,
}
impl Meta {
#[must_use]
pub fn current(source: Source) -> Self {
Self {
schema_version: SchemaVersion::current(),
source,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DiffResult {
pub runs: RunPair,
#[serde(default)]
pub alignment: Vec<Move>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fork: Option<Fork>,
#[serde(default)]
pub field_diffs: Vec<FieldDiff>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attribution: Option<Attribution>,
#[serde(default)]
pub warnings: Vec<Warning>,
pub meta: Meta,
}
impl DiffResult {
#[must_use]
pub fn fork_step_observed(&self) -> Option<usize> {
let fork = self.fork?;
if fork.b_step.is_some() {
return fork.b_step;
}
let before_fork = self.alignment.len().min(fork.index);
let consumed = self.alignment[..before_fork]
.iter()
.filter(|m| m.b_idx.is_some())
.count();
Some(consumed.min(self.runs.b.n_steps.saturating_sub(1)))
}
}