1use std::collections::BTreeMap;
9use std::fmt;
10
11use serde::{Deserialize, Serialize};
12
13use crate::actionability::{Action, NeedsInput, Party, Refusal};
14use crate::decimal::Decimal;
15use crate::definition::ReasonKind;
16use crate::ids::{PlanId, SnapshotId};
17use crate::limits::RECORD_V1;
18use crate::schema;
19use crate::time::{DurationMs, Timestamp};
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
22#[serde(deny_unknown_fields)]
23pub struct Judgment {
24 pub schema: String,
25 pub plan_id: PlanId,
26 pub snapshot_id: SnapshotId,
27 pub evaluation_time_ms: Timestamp,
28 pub outcome: Outcome,
29 pub derivation: Derivation,
30 pub lineage: Lineage,
31 pub notices: Vec<Notice>,
33 pub trace: Vec<String>,
35}
36
37crate::document::versioned_document!(
38 Judgment,
39 [schema::JUDGMENT, schema::JUDGMENT_V2],
40 "rustev.judgment/1 or rustev.judgment/2",
41 RECORD_V1
42);
43
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
51#[serde(from = "OutcomeWire", into = "OutcomeWire")]
52pub enum Outcome {
53 Propose {
54 action: String,
55 params: BTreeMap<String, OutValue>,
56 },
57 Escalate {
58 reason: String,
59 },
60 Unresolved(Unresolved),
61 UnresolvedAction {
62 reason: Unresolved,
63 action: Action,
64 },
65 Refused {
66 refusals: Vec<Refusal>,
67 addressed_to: Party,
68 },
69 NeedsInput(Box<NeedsInput>),
70}
71
72impl Outcome {
73 pub fn unresolved(&self) -> Option<&Unresolved> {
75 match self {
76 Outcome::Unresolved(u) | Outcome::UnresolvedAction { reason: u, .. } => Some(u),
77 _ => None,
78 }
79 }
80}
81
82#[derive(Serialize, Deserialize, schemars::JsonSchema)]
83#[serde(rename_all = "snake_case", deny_unknown_fields)]
84enum OutcomeWire {
85 Propose {
86 action: String,
87 params: BTreeMap<String, OutValue>,
88 },
89 Escalate {
90 reason: String,
91 },
92 Unresolved(UnresolvedWire),
93 Refused {
94 refusals: Vec<Refusal>,
95 addressed_to: Party,
96 },
97 NeedsInput(Box<NeedsInput>),
98}
99
100#[derive(Serialize, Deserialize, schemars::JsonSchema)]
103#[serde(untagged)]
104enum UnresolvedWire {
105 Actionable(ActionableUnresolved),
106 Plain(Unresolved),
107}
108
109#[derive(Serialize, Deserialize, schemars::JsonSchema)]
110#[serde(deny_unknown_fields)]
111struct ActionableUnresolved {
112 reason: Unresolved,
113 action: Action,
114}
115
116impl From<OutcomeWire> for Outcome {
117 fn from(w: OutcomeWire) -> Self {
118 match w {
119 OutcomeWire::Propose { action, params } => Outcome::Propose { action, params },
120 OutcomeWire::Escalate { reason } => Outcome::Escalate { reason },
121 OutcomeWire::Unresolved(UnresolvedWire::Plain(u)) => Outcome::Unresolved(u),
122 OutcomeWire::Unresolved(UnresolvedWire::Actionable(a)) => Outcome::UnresolvedAction {
123 reason: a.reason,
124 action: a.action,
125 },
126 OutcomeWire::Refused {
127 refusals,
128 addressed_to,
129 } => Outcome::Refused {
130 refusals,
131 addressed_to,
132 },
133 OutcomeWire::NeedsInput(n) => Outcome::NeedsInput(n),
134 }
135 }
136}
137
138impl From<Outcome> for OutcomeWire {
139 fn from(o: Outcome) -> Self {
140 match o {
141 Outcome::Propose { action, params } => OutcomeWire::Propose { action, params },
142 Outcome::Escalate { reason } => OutcomeWire::Escalate { reason },
143 Outcome::Unresolved(u) => OutcomeWire::Unresolved(UnresolvedWire::Plain(u)),
144 Outcome::UnresolvedAction { reason, action } => {
145 OutcomeWire::Unresolved(UnresolvedWire::Actionable(ActionableUnresolved {
146 reason,
147 action,
148 }))
149 }
150 Outcome::Refused {
151 refusals,
152 addressed_to,
153 } => OutcomeWire::Refused {
154 refusals,
155 addressed_to,
156 },
157 Outcome::NeedsInput(n) => OutcomeWire::NeedsInput(n),
158 }
159 }
160}
161
162#[derive(
165 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, schemars::JsonSchema,
166)]
167#[serde(rename_all = "snake_case", deny_unknown_fields)]
168pub enum Unresolved {
169 MissingEvidence {
170 fields: Vec<String>,
171 },
172 StaleEvidence {
173 fields: Vec<String>,
174 },
175 InvalidInput {
176 fields: Vec<String>,
177 detail: String,
178 },
179 Abstained {
180 rule: String,
181 },
182 BackendUnavailable {
183 detail: String,
184 },
185 InvalidBackendOutput {
186 detail: String,
187 },
188 BudgetExhausted {
189 resource: String,
190 },
191 DeadlineExceeded,
192 Conflict {
193 facts: Vec<String>,
194 },
195 Unsupported {
196 capability: String,
197 },
198 InsufficientGrade {
200 fields: Vec<String>,
201 },
202 ScreenUnavailable {
204 #[serde(deserialize_with = "crate::screening::unavailable_fields")]
205 #[schemars(length(min = 1))]
206 fields: Vec<String>,
207 },
208}
209
210impl Unresolved {
211 pub fn kind(&self) -> ReasonKind {
212 match self {
213 Unresolved::ScreenUnavailable { .. } => ReasonKind::ScreenUnavailable,
214 Unresolved::MissingEvidence { .. } => ReasonKind::MissingEvidence,
215 Unresolved::StaleEvidence { .. } => ReasonKind::StaleEvidence,
216 Unresolved::InvalidInput { .. } => ReasonKind::InvalidInput,
217 Unresolved::Abstained { .. } => ReasonKind::Abstained,
218 Unresolved::BackendUnavailable { .. } => ReasonKind::BackendUnavailable,
219 Unresolved::InvalidBackendOutput { .. } => ReasonKind::InvalidBackendOutput,
220 Unresolved::BudgetExhausted { .. } => ReasonKind::BudgetExhausted,
221 Unresolved::DeadlineExceeded => ReasonKind::DeadlineExceeded,
222 Unresolved::Conflict { .. } => ReasonKind::Conflict,
223 Unresolved::Unsupported { .. } => ReasonKind::Unsupported,
224 Unresolved::InsufficientGrade { .. } => ReasonKind::InsufficientGrade,
225 }
226 }
227
228 pub fn missing<I: IntoIterator<Item = String>>(fields: I) -> Self {
230 Unresolved::MissingEvidence {
231 fields: sorted(fields),
232 }
233 }
234
235 pub fn stale<I: IntoIterator<Item = String>>(fields: I) -> Self {
237 Unresolved::StaleEvidence {
238 fields: sorted(fields),
239 }
240 }
241
242 pub fn invalid<I: IntoIterator<Item = String>>(fields: I, detail: impl Into<String>) -> Self {
244 Unresolved::InvalidInput {
245 fields: sorted(fields),
246 detail: detail.into(),
247 }
248 }
249
250 pub fn insufficient<I: IntoIterator<Item = String>>(fields: I) -> Self {
252 Unresolved::InsufficientGrade {
253 fields: sorted(fields),
254 }
255 }
256
257 pub fn screen_unavailable<I: IntoIterator<Item = String>>(fields: I) -> Self {
259 Unresolved::ScreenUnavailable {
260 fields: sorted(fields),
261 }
262 }
263
264 pub fn conflict<I: IntoIterator<Item = String>>(facts: I) -> Self {
266 Unresolved::Conflict {
267 facts: sorted(facts),
268 }
269 }
270}
271
272fn sorted<I: IntoIterator<Item = String>>(items: I) -> Vec<String> {
273 let mut v: Vec<String> = items.into_iter().collect();
274 v.sort();
275 v.dedup();
276 v
277}
278
279impl fmt::Display for Unresolved {
280 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281 match self {
282 Unresolved::ScreenUnavailable { fields } => {
283 write!(f, "screen_unavailable{{{}}}", fields.join(","))
284 }
285 Unresolved::MissingEvidence { fields } => {
286 write!(f, "missing_evidence{{{}}}", fields.join(","))
287 }
288 Unresolved::StaleEvidence { fields } => {
289 write!(f, "stale_evidence{{{}}}", fields.join(","))
290 }
291 Unresolved::InvalidInput { fields, detail } => {
292 write!(f, "invalid_input{{{}}}: {detail}", fields.join(","))
293 }
294 Unresolved::Abstained { rule } => write!(f, "abstained{{{rule}}}"),
295 Unresolved::BackendUnavailable { detail } => write!(f, "backend_unavailable: {detail}"),
296 Unresolved::InvalidBackendOutput { detail } => {
297 write!(f, "invalid_backend_output: {detail}")
298 }
299 Unresolved::BudgetExhausted { resource } => write!(f, "budget_exhausted{{{resource}}}"),
300 Unresolved::DeadlineExceeded => write!(f, "deadline_exceeded"),
301 Unresolved::Conflict { facts } => write!(f, "conflict{{{}}}", facts.join(",")),
302 Unresolved::Unsupported { capability } => write!(f, "unsupported{{{capability}}}"),
303 Unresolved::InsufficientGrade { fields } => {
304 write!(f, "insufficient_grade{{{}}}", fields.join(","))
305 }
306 }
307 }
308}
309
310#[derive(
312 Debug,
313 Clone,
314 Copy,
315 PartialEq,
316 Eq,
317 PartialOrd,
318 Ord,
319 Hash,
320 Serialize,
321 Deserialize,
322 schemars::JsonSchema,
323)]
324#[serde(rename_all = "kebab-case")]
325pub enum Derivation {
326 ExactDerived,
327 ModelDerived,
328 MixedDerived,
329}
330
331#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
333#[serde(deny_unknown_fields)]
334pub struct Lineage {
335 pub inputs: Vec<String>,
336 pub steps: Vec<String>,
337}
338
339#[derive(
340 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
341)]
342#[serde(deny_unknown_fields)]
343pub struct Notice {
344 pub kind: NoticeKind,
345 pub subject: String,
347 pub reason: String,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub action: Option<Action>,
351 #[serde(
353 default,
354 skip_serializing_if = "Option::is_none",
355 deserialize_with = "crate::screening::present"
356 )]
357 #[schemars(with = "crate::screening::ScreeningNotice")]
358 pub screening: Option<crate::screening::ScreeningNotice>,
359}
360
361#[derive(
362 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
363)]
364#[serde(rename_all = "snake_case")]
365pub enum NoticeKind {
366 UncalibratedThreshold,
367 Truncation,
368 Fallback,
369 FieldDropped,
371 ConstraintsRelaxed,
372 GradeDowngraded,
373 CheckNotice,
374 Screening,
375}
376
377#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
379#[serde(rename_all = "snake_case", deny_unknown_fields)]
380pub enum OutValue {
381 Bool(bool),
382 Integer(i64),
383 Decimal(Decimal),
384 Text(String),
385 Enum(String),
386 Timestamp(Timestamp),
387 Duration(DurationMs),
388 None,
390 List(Vec<OutValue>),
391 Record(BTreeMap<String, OutValue>),
392 Filtered(Filtered),
393 Shortlist(Shortlist),
394 Ranking(Ranking),
395}
396
397#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
399#[serde(deny_unknown_fields)]
400pub struct Filtered {
401 pub eligible: Vec<String>,
402 pub excluded: Vec<Excluded>,
403 pub counts: BTreeMap<String, u64>,
405}
406
407#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
408#[serde(deny_unknown_fields)]
409pub struct Excluded {
410 pub id: String,
411 pub reasons: Vec<String>,
412}
413
414#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
416#[serde(deny_unknown_fields)]
417pub struct Shortlist {
418 pub ids: Vec<String>,
419 pub truncated: u64,
421 pub excluded: Vec<Excluded>,
422}
423
424#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
426#[serde(deny_unknown_fields)]
427pub struct Ranking {
428 pub entries: Vec<RankEntry>,
429 pub excluded: Vec<Excluded>,
431}
432
433#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
434#[serde(deny_unknown_fields)]
435pub struct RankEntry {
436 pub candidate: String,
437 pub position: u64,
439 pub score: f64,
440 pub components: BTreeMap<String, f64>,
441}
442
443impl ReasonKind {
444 pub fn name(self) -> &'static str {
445 match self {
446 ReasonKind::ScreenUnavailable => "screen_unavailable",
447 ReasonKind::Skipped => "skipped",
448 ReasonKind::MissingEvidence => "missing_evidence",
449 ReasonKind::StaleEvidence => "stale_evidence",
450 ReasonKind::InvalidInput => "invalid_input",
451 ReasonKind::Abstained => "abstained",
452 ReasonKind::BackendUnavailable => "backend_unavailable",
453 ReasonKind::InvalidBackendOutput => "invalid_backend_output",
454 ReasonKind::BudgetExhausted => "budget_exhausted",
455 ReasonKind::DeadlineExceeded => "deadline_exceeded",
456 ReasonKind::Conflict => "conflict",
457 ReasonKind::Unsupported => "unsupported",
458 ReasonKind::InsufficientGrade => "insufficient_grade",
459 }
460 }
461}