use std::collections::BTreeMap;
use std::fmt;
use serde::{Deserialize, Serialize};
use crate::actionability::{Action, NeedsInput, Party, Refusal};
use crate::decimal::Decimal;
use crate::definition::ReasonKind;
use crate::ids::{PlanId, SnapshotId};
use crate::limits::RECORD_V1;
use crate::schema;
use crate::time::{DurationMs, Timestamp};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Judgment {
pub schema: String,
pub plan_id: PlanId,
pub snapshot_id: SnapshotId,
pub evaluation_time_ms: Timestamp,
pub outcome: Outcome,
pub derivation: Derivation,
pub lineage: Lineage,
pub notices: Vec<Notice>,
pub trace: Vec<String>,
}
crate::document::versioned_document!(
Judgment,
[schema::JUDGMENT, schema::JUDGMENT_V2],
"rustev.judgment/1 or rustev.judgment/2",
RECORD_V1,
check = Judgment::extraction_version
);
impl Judgment {
pub(crate) fn extraction_version(&self) -> Result<(), String> {
fn attributed(v: &OutValue) -> bool {
match v {
OutValue::Attributed { .. } => true,
OutValue::List(items) => items.iter().any(attributed),
OutValue::Record(fields) => fields.values().any(attributed),
_ => false,
}
}
if self.schema != schema::JUDGMENT_V2
&& (self.lineage.producers.is_some()
|| matches!(&self.outcome, Outcome::Propose { params, .. } if params.values().any(attributed)))
{
return Err("attributed values and producers require rustev.judgment/2".into());
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(from = "OutcomeWire", into = "OutcomeWire")]
pub enum Outcome {
Propose {
action: String,
params: BTreeMap<String, OutValue>,
},
Escalate {
reason: String,
},
Unresolved(Unresolved),
UnresolvedAction {
reason: Unresolved,
action: Action,
},
Refused {
refusals: Vec<Refusal>,
addressed_to: Party,
},
NeedsInput(Box<NeedsInput>),
}
impl Outcome {
pub fn unresolved(&self) -> Option<&Unresolved> {
match self {
Outcome::Unresolved(u) | Outcome::UnresolvedAction { reason: u, .. } => Some(u),
_ => None,
}
}
}
#[derive(Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
enum OutcomeWire {
Propose {
action: String,
params: BTreeMap<String, OutValue>,
},
Escalate {
reason: String,
},
Unresolved(UnresolvedWire),
Refused {
refusals: Vec<Refusal>,
addressed_to: Party,
},
NeedsInput(Box<NeedsInput>),
}
#[derive(Serialize, Deserialize, schemars::JsonSchema)]
#[serde(untagged)]
enum UnresolvedWire {
Actionable(ActionableUnresolved),
Plain(Unresolved),
}
#[derive(Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
struct ActionableUnresolved {
reason: Unresolved,
action: Action,
}
impl From<OutcomeWire> for Outcome {
fn from(w: OutcomeWire) -> Self {
match w {
OutcomeWire::Propose { action, params } => Outcome::Propose { action, params },
OutcomeWire::Escalate { reason } => Outcome::Escalate { reason },
OutcomeWire::Unresolved(UnresolvedWire::Plain(u)) => Outcome::Unresolved(u),
OutcomeWire::Unresolved(UnresolvedWire::Actionable(a)) => Outcome::UnresolvedAction {
reason: a.reason,
action: a.action,
},
OutcomeWire::Refused {
refusals,
addressed_to,
} => Outcome::Refused {
refusals,
addressed_to,
},
OutcomeWire::NeedsInput(n) => Outcome::NeedsInput(n),
}
}
}
impl From<Outcome> for OutcomeWire {
fn from(o: Outcome) -> Self {
match o {
Outcome::Propose { action, params } => OutcomeWire::Propose { action, params },
Outcome::Escalate { reason } => OutcomeWire::Escalate { reason },
Outcome::Unresolved(u) => OutcomeWire::Unresolved(UnresolvedWire::Plain(u)),
Outcome::UnresolvedAction { reason, action } => {
OutcomeWire::Unresolved(UnresolvedWire::Actionable(ActionableUnresolved {
reason,
action,
}))
}
Outcome::Refused {
refusals,
addressed_to,
} => OutcomeWire::Refused {
refusals,
addressed_to,
},
Outcome::NeedsInput(n) => OutcomeWire::NeedsInput(n),
}
}
}
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum Unresolved {
MissingEvidence {
fields: Vec<String>,
},
StaleEvidence {
fields: Vec<String>,
},
InvalidInput {
fields: Vec<String>,
detail: String,
},
Abstained {
rule: String,
},
BackendUnavailable {
detail: String,
},
InvalidBackendOutput {
detail: String,
},
BudgetExhausted {
resource: String,
},
DeadlineExceeded,
Conflict {
facts: Vec<String>,
},
Unsupported {
capability: String,
},
InsufficientGrade {
fields: Vec<String>,
},
ScreenUnavailable {
#[serde(deserialize_with = "crate::screening::unavailable_fields")]
#[schemars(length(min = 1))]
fields: Vec<String>,
},
}
impl Unresolved {
pub fn kind(&self) -> ReasonKind {
match self {
Unresolved::ScreenUnavailable { .. } => ReasonKind::ScreenUnavailable,
Unresolved::MissingEvidence { .. } => ReasonKind::MissingEvidence,
Unresolved::StaleEvidence { .. } => ReasonKind::StaleEvidence,
Unresolved::InvalidInput { .. } => ReasonKind::InvalidInput,
Unresolved::Abstained { .. } => ReasonKind::Abstained,
Unresolved::BackendUnavailable { .. } => ReasonKind::BackendUnavailable,
Unresolved::InvalidBackendOutput { .. } => ReasonKind::InvalidBackendOutput,
Unresolved::BudgetExhausted { .. } => ReasonKind::BudgetExhausted,
Unresolved::DeadlineExceeded => ReasonKind::DeadlineExceeded,
Unresolved::Conflict { .. } => ReasonKind::Conflict,
Unresolved::Unsupported { .. } => ReasonKind::Unsupported,
Unresolved::InsufficientGrade { .. } => ReasonKind::InsufficientGrade,
}
}
pub fn missing<I: IntoIterator<Item = String>>(fields: I) -> Self {
Unresolved::MissingEvidence {
fields: sorted(fields),
}
}
pub fn stale<I: IntoIterator<Item = String>>(fields: I) -> Self {
Unresolved::StaleEvidence {
fields: sorted(fields),
}
}
pub fn invalid<I: IntoIterator<Item = String>>(fields: I, detail: impl Into<String>) -> Self {
Unresolved::InvalidInput {
fields: sorted(fields),
detail: detail.into(),
}
}
pub fn insufficient<I: IntoIterator<Item = String>>(fields: I) -> Self {
Unresolved::InsufficientGrade {
fields: sorted(fields),
}
}
pub fn screen_unavailable<I: IntoIterator<Item = String>>(fields: I) -> Self {
Unresolved::ScreenUnavailable {
fields: sorted(fields),
}
}
pub fn conflict<I: IntoIterator<Item = String>>(facts: I) -> Self {
Unresolved::Conflict {
facts: sorted(facts),
}
}
}
fn sorted<I: IntoIterator<Item = String>>(items: I) -> Vec<String> {
let mut v: Vec<String> = items.into_iter().collect();
v.sort();
v.dedup();
v
}
impl fmt::Display for Unresolved {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Unresolved::ScreenUnavailable { fields } => {
write!(f, "screen_unavailable{{{}}}", fields.join(","))
}
Unresolved::MissingEvidence { fields } => {
write!(f, "missing_evidence{{{}}}", fields.join(","))
}
Unresolved::StaleEvidence { fields } => {
write!(f, "stale_evidence{{{}}}", fields.join(","))
}
Unresolved::InvalidInput { fields, detail } => {
write!(f, "invalid_input{{{}}}: {detail}", fields.join(","))
}
Unresolved::Abstained { rule } => write!(f, "abstained{{{rule}}}"),
Unresolved::BackendUnavailable { detail } => write!(f, "backend_unavailable: {detail}"),
Unresolved::InvalidBackendOutput { detail } => {
write!(f, "invalid_backend_output: {detail}")
}
Unresolved::BudgetExhausted { resource } => write!(f, "budget_exhausted{{{resource}}}"),
Unresolved::DeadlineExceeded => write!(f, "deadline_exceeded"),
Unresolved::Conflict { facts } => write!(f, "conflict{{{}}}", facts.join(",")),
Unresolved::Unsupported { capability } => write!(f, "unsupported{{{capability}}}"),
Unresolved::InsufficientGrade { fields } => {
write!(f, "insufficient_grade{{{}}}", fields.join(","))
}
}
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
schemars::JsonSchema,
)]
#[serde(rename_all = "kebab-case")]
pub enum Derivation {
ExactDerived,
ModelDerived,
MixedDerived,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Lineage {
pub inputs: Vec<String>,
pub steps: Vec<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::screening::present"
)]
pub producers: Option<crate::extraction_results::Producers>,
}
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
)]
#[serde(deny_unknown_fields)]
pub struct Notice {
pub kind: NoticeKind,
pub subject: String,
pub reason: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub action: Option<Action>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::screening::present"
)]
#[schemars(with = "crate::screening::ScreeningNotice")]
pub screening: Option<crate::screening::ScreeningNotice>,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum NoticeKind {
UncalibratedThreshold,
Truncation,
Fallback,
FieldDropped,
ConstraintsRelaxed,
GradeDowngraded,
CheckNotice,
Screening,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum OutValue {
Attributed {
value: Box<OutValue>,
derivation: Derivation,
origin: crate::extraction_results::AcceptedOrigin,
},
Bool(bool),
Integer(i64),
Decimal(Decimal),
Text(String),
Enum(String),
Timestamp(Timestamp),
Duration(DurationMs),
None,
List(Vec<OutValue>),
Record(BTreeMap<String, OutValue>),
Filtered(Filtered),
Shortlist(Shortlist),
Ranking(Ranking),
Selection(crate::ranking_result::Selection),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Filtered {
pub eligible: Vec<String>,
pub excluded: Vec<Excluded>,
pub counts: BTreeMap<String, u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Excluded {
pub id: String,
pub reasons: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Shortlist {
pub ids: Vec<String>,
pub truncated: u64,
pub excluded: Vec<Excluded>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Ranking {
pub entries: Vec<RankEntry>,
pub excluded: Vec<Excluded>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct RankEntry {
pub candidate: String,
pub position: u64,
pub score: f64,
pub components: BTreeMap<String, f64>,
}
impl ReasonKind {
pub fn name(self) -> &'static str {
match self {
ReasonKind::ScreenUnavailable => "screen_unavailable",
ReasonKind::Skipped => "skipped",
ReasonKind::MissingEvidence => "missing_evidence",
ReasonKind::StaleEvidence => "stale_evidence",
ReasonKind::InvalidInput => "invalid_input",
ReasonKind::Abstained => "abstained",
ReasonKind::BackendUnavailable => "backend_unavailable",
ReasonKind::InvalidBackendOutput => "invalid_backend_output",
ReasonKind::BudgetExhausted => "budget_exhausted",
ReasonKind::DeadlineExceeded => "deadline_exceeded",
ReasonKind::Conflict => "conflict",
ReasonKind::Unsupported => "unsupported",
ReasonKind::InsufficientGrade => "insufficient_grade",
}
}
}