use serde::{Deserialize, Serialize};
use crate::evidence::EvidenceRecord;
use crate::execution::FailureClass;
use crate::ids::{ArtifactId, ExecutionPolicyId, PlanId};
use crate::judgment::Unresolved;
use crate::limits::RECORD_V1;
use crate::optimization::{OptimizationOutcome, OptimizationRecord};
use crate::remote::{MAX_TEXT_BYTES, RemoteCode, ServedIdentity};
use crate::schema;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct RunRecord {
pub schema: String,
pub decision_id: String,
pub plan_id: PlanId,
pub execution: RecordedExecution,
pub termination: Termination,
pub core: CoreEvidence,
pub timing: Timing,
pub requests: Vec<RequestRecord>,
pub cost: CostSummary,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remote_evidence: Option<Vec<RemoteEvidence>>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub remote_evidence_truncated: bool,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::acquisition_record::list"
)]
#[schemars(
with = "Vec<crate::acquisition_record::AcquisitionRecord>",
length(min = 1, max = 4096)
)]
pub acquisitions: Option<Vec<crate::acquisition_record::AcquisitionRecord>>,
}
crate::document::versioned_document!(
RunRecord,
[schema::RUN, schema::RUN_V2, schema::RUN_V3],
"rustev.run/1 or rustev.run/2 or rustev.run/3",
RECORD_V1,
check = RunRecord::parsed_members_fit
);
pub const MAX_REMOTE_EVIDENCE: usize = 256;
pub const MAX_REMOTE_EVIDENCE_ENTRY_BYTES: usize = 8 * 1024;
pub const MAX_REMOTE_EVIDENCE_INSTANCE: usize = 8;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RunVersionError {
NotVersion2,
Members(&'static str),
}
impl std::fmt::Display for RunVersionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RunVersionError::NotVersion2 => f.write_str("only rustev.run/2 projects to version 1"),
RunVersionError::Members(why) => write!(f, "run record members: {why}"),
}
}
}
impl std::error::Error for RunVersionError {}
impl RunRecord {
pub fn check_version_members(&self) -> Result<(), RunVersionError> {
if self.schema != schema::RUN_V3
&& self
.requests
.iter()
.any(|r| r.screen.is_some() || r.result == RequestResult::Withdrawn)
{
return Err(RunVersionError::Members("screening requires rustev.run/3"));
}
if let Some(pages) = &self.acquisitions {
if self.schema != schema::RUN_V3 {
return Err(RunVersionError::Members(
"acquisitions require rustev.run/3",
));
}
if pages.iter().map(|p| p.attempts.len()).sum::<usize>() > 4096 {
return Err(RunVersionError::Members("too many acquisition attempts"));
}
}
let v2 = self.schema == schema::RUN_V2;
match &self.remote_evidence {
None if v2 => Err(RunVersionError::Members(
"rustev.run/2 needs remote_evidence",
)),
Some(_) if !v2 => Err(RunVersionError::Members(
"remote_evidence is a rustev.run/2 member",
)),
_ if self.remote_evidence_truncated && !v2 => Err(RunVersionError::Members(
"remote_evidence_truncated is a rustev.run/2 member",
)),
Some(list) if list.len() > MAX_REMOTE_EVIDENCE => {
Err(RunVersionError::Members("too many remote evidence entries"))
}
Some(list) if !list.iter().all(RemoteEvidence::is_bounded) => Err(
RunVersionError::Members("a remote evidence entry beyond its bounds"),
),
_ => Ok(()),
}
}
fn parsed_members_fit(&self) -> Result<(), String> {
if let CoreEvidence::Recorded(e) = &self.core
&& e.generation.is_some()
&& (self.schema != schema::RUN_V3 || e.schema != schema::EVIDENCE_V2)
{
return Err("generation evidence requires rustev.run/3 and rustev.evidence/2".into());
}
if let CoreEvidence::Recorded(e) = &self.core
&& (self.schema != schema::RUN_V3 || e.schema != schema::EVIDENCE_V2)
&& (e.acquisitions.is_some() || e.steps.iter().any(|s| s.constructed))
{
return Err("acquisition evidence requires rustev.evidence/2".into());
}
self.check_version_members().map_err(|e| e.to_string())
}
pub fn project_v1(&self) -> Result<RunRecord, RunVersionError> {
if self.schema != schema::RUN_V2 {
return Err(RunVersionError::NotVersion2);
}
self.check_version_members()?;
Ok(RunRecord {
schema: schema::RUN.into(),
remote_evidence: None,
remote_evidence_truncated: false,
..self.clone()
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct RemoteEvidence {
pub attempt_id: String,
pub step: String,
pub instance: Vec<String>,
pub exchange: ExchangeReference,
pub requested_identity: Option<String>,
pub served: ServedIdentity,
pub binding: ArtifactId,
pub protocol: String,
pub endpoint_class: String,
pub code: Option<RemoteCode>,
pub remote_state: RemoteState,
pub charge_provenance: ChargeProvenance,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub batch: Option<BatchMembership>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub principal_digest: Option<String>,
}
impl RemoteEvidence {
pub fn is_bounded(&self) -> bool {
self.strings_within(MAX_TEXT_BYTES)
&& crate::canonical::canonical_bytes(self)
.is_ok_and(|b| b.len() <= MAX_REMOTE_EVIDENCE_ENTRY_BYTES)
}
fn strings_within(&self, max: usize) -> bool {
let t = |s: &str| s.len() <= max;
let o = |s: &Option<String>| s.as_deref().is_none_or(t);
t(&self.attempt_id)
&& t(&self.step)
&& self.instance.len() <= MAX_REMOTE_EVIDENCE_INSTANCE
&& self.instance.iter().all(|i| t(i))
&& self.exchange.is_bounded(max)
&& o(&self.requested_identity)
&& o(&self.served.identity)
&& t(&self.protocol)
&& t(&self.endpoint_class)
&& self.batch.as_ref().is_none_or(|b| t(&b.batch_id))
&& o(&self.principal_digest)
}
pub fn bounded(self) -> Self {
let e = self.cut(MAX_TEXT_BYTES);
if e.is_bounded() { e } else { e.cut(32) }
}
fn cut(mut self, max: usize) -> Self {
let c = |s: &mut String| {
if s.len() > max {
*s = crate::remote::cut_to(s, max);
}
};
c(&mut self.attempt_id);
c(&mut self.step);
self.instance.truncate(MAX_REMOTE_EVIDENCE_INSTANCE);
self.instance.iter_mut().for_each(c);
if let ExchangeReference::Retained {
digest,
schema,
locator,
..
} = &mut self.exchange
{
c(digest);
c(schema);
c(locator);
}
if let Some(r) = &mut self.requested_identity {
c(r);
}
if let Some(s) = &mut self.served.identity {
c(s);
}
c(&mut self.protocol);
c(&mut self.endpoint_class);
if let Some(b) = &mut self.batch {
c(&mut b.batch_id);
}
if let Some(p) = &mut self.principal_digest {
c(p);
}
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum ExchangeReference {
Retained {
digest: String,
schema: String,
retention: RetentionClass,
locator: String,
},
Unavailable {
reason: EvidenceUnavailable,
},
}
impl ExchangeReference {
fn is_bounded(&self, max: usize) -> bool {
match self {
ExchangeReference::Retained {
digest,
schema,
locator,
..
} => [digest, schema, locator].iter().all(|s| s.len() <= max),
ExchangeReference::Unavailable { .. } => true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum RetentionClass {
DigestOnly,
BytesRetained,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceUnavailable {
NoExchange,
Pending,
NotDelivered,
NoLocator,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ChargeProvenance {
Reported,
Attributed,
NothingSent,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct BatchMembership {
pub batch_id: String,
pub index: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum RecordedExecution {
None,
Declared(ExecutionPolicyId),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Termination {
Judged,
Cancelled,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum CoreEvidence {
Recorded(Box<EvidenceRecord>),
NotProduced,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Timing {
pub queued_ms: u64,
pub elapsed_ms: u64,
pub deadline_ms: u64,
pub deadline_expired: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct RequestRecord {
pub step: String,
pub instance: Vec<String>,
pub result: RequestResult,
pub attempts: Vec<AttemptRecord>,
pub transitions: Vec<Transition>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub optimization: Option<OptimizationRecord>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::screening::present"
)]
#[schemars(with = "crate::screening::ScreenRequest")]
pub screen: Option<crate::screening::ScreenRequest>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SuppliedFrom<'r> {
pub target: u32,
pub attempt: Option<&'r AttemptRecord>,
pub reused_from: Option<&'r str>,
}
impl RequestRecord {
pub fn supplied_from(&self) -> Option<SuppliedFrom<'_>> {
match &self.result {
RequestResult::NotSupplied | RequestResult::Withdrawn => None,
RequestResult::Output { target } => Some(SuppliedFrom {
target: *target,
attempt: self.attempts.last(),
reused_from: self
.optimization
.as_ref()
.filter(|o| self.attempts.is_empty() && o.outcome == OptimizationOutcome::Hit)
.and_then(|o| o.source_attempt_id.as_deref()),
}),
RequestResult::Failed(_) => {
let target = self
.transitions
.iter()
.rev()
.find_map(|t| match t {
Transition::Fallback { to, .. } => Some(*to),
_ => None,
})
.unwrap_or(0);
let n = self.attempts.len() as u32;
let moved_on = self.transitions.iter().any(|t| {
matches!(t, Transition::Retry { after, .. } | Transition::Fallback { after, .. } if *after == n)
});
Some(SuppliedFrom {
target,
attempt: if moved_on { None } else { self.attempts.last() },
reused_from: None,
})
}
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum RequestResult {
Output { target: u32 },
Failed(Unresolved),
NotSupplied,
Withdrawn,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct AttemptRecord {
pub attempt_id: String,
pub target: u32,
pub backend_id: String,
pub artifact: ArtifactId,
pub dispatched_ms: u64,
pub ended_ms: u64,
pub end: AttemptEnd,
pub cancellation: Cancellation,
pub remote: RemoteState,
pub cost: AttemptCost,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum AttemptEnd {
Output,
Failed {
class: FailureClass,
detail: String,
},
Deadline,
Cancelled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum Cancellation {
NotRequested,
Requested { answer: CancelAnswer },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum CancelAnswer {
Stopped,
Unconfirmed,
NotObserved,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum RemoteState {
Finished,
Stopped,
PossiblyContinuing,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct AttemptCost {
pub bound: CostBound,
pub reserved: u64,
pub charge: Charge,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum CostModel {
Bounded,
Estimated,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum CostBound {
Bounded {
max_units: u64,
},
Estimated {
units: u64,
},
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum Charge {
Observed { units: u64 },
Estimated { units: u64 },
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum Transition {
Retry {
after: u32,
class: FailureClass,
delay_ms: u64,
},
Fallback {
after: u32,
class: FailureClass,
to: u32,
},
Stop { after: u32, reason: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum CostMode {
Unlimited,
Hard,
Estimated,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CostSummary {
pub mode: CostMode,
pub limit: u64,
pub observed: u64,
pub estimated: u64,
pub liability: u64,
pub bound_violations: u64,
pub final_cost: FinalCost,
pub within_guaranteed_cap: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum FinalCost {
Known,
IncludesEstimates,
Unknown,
}