Skip to main content

rustev_contract/
run.rs

1//! Run records, `rustev.run/1` (spec 003, 3.9): what the runtime delivers for
2//! one admitted decision. The core's evidence record is carried unchanged in
3//! its own member; everything else is a runtime observation, recorded and
4//! never promised to repeat (principle XIII).
5//!
6//! Also the cost and cancellation vocabulary the backend seam reports in
7//! (spec 003, 3.5 and 3.7), so an outside reader needs this crate alone.
8
9use serde::{Deserialize, Serialize};
10
11use crate::evidence::EvidenceRecord;
12use crate::execution::FailureClass;
13use crate::ids::{ArtifactId, ExecutionPolicyId, PlanId};
14use crate::judgment::Unresolved;
15use crate::limits::RECORD_V1;
16use crate::optimization::{OptimizationOutcome, OptimizationRecord};
17use crate::remote::{MAX_TEXT_BYTES, RemoteCode, ServedIdentity};
18use crate::schema;
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
21#[serde(deny_unknown_fields)]
22pub struct RunRecord {
23    pub schema: String,
24    /// Supplied by the caller; the delivery key sinks deduplicate by.
25    pub decision_id: String,
26    pub plan_id: PlanId,
27    pub execution: RecordedExecution,
28    pub termination: Termination,
29    pub core: CoreEvidence,
30    pub timing: Timing,
31    /// In the order the core listed them.
32    pub requests: Vec<RequestRecord>,
33    pub cost: CostSummary,
34    /// `rustev.run/2` only (spec 020, 3.2): at most one entry per remote
35    /// attempt, in attempt order, at most [`MAX_REMOTE_EVIDENCE`]. Absent in
36    /// every other version, so their bytes are unchanged.
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub remote_evidence: Option<Vec<RemoteEvidence>>,
39    /// `rustev.run/2` only: remote attempts beyond the bound have no entry.
40    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
41    pub remote_evidence_truncated: bool,
42    /// Spec 036: omitted when the plan has no acquisition step.
43    #[serde(
44        default,
45        skip_serializing_if = "Option::is_none",
46        deserialize_with = "crate::acquisition_record::list"
47    )]
48    #[schemars(
49        with = "Vec<crate::acquisition_record::AcquisitionRecord>",
50        length(min = 1, max = 4096)
51    )]
52    pub acquisitions: Option<Vec<crate::acquisition_record::AcquisitionRecord>>,
53}
54
55// `rustev.run/3` is `rustev.run/1` with its core member holding
56// `rustev.evidence/2` (spec 032, 3.10.3); `rustev.run/2` is `rustev.run/1`
57// with a remote evidence list (spec 020, 3.2).
58crate::document::versioned_document!(
59    RunRecord,
60    [schema::RUN, schema::RUN_V2, schema::RUN_V3],
61    "rustev.run/1 or rustev.run/2 or rustev.run/3",
62    RECORD_V1,
63    check = RunRecord::parsed_members_fit
64);
65
66/// The most remote evidence entries one `rustev.run/2` record holds.
67pub const MAX_REMOTE_EVIDENCE: usize = 256;
68/// The most canonical bytes of one remote evidence entry, so the list never
69/// exceeds 2 MiB.
70pub const MAX_REMOTE_EVIDENCE_ENTRY_BYTES: usize = 8 * 1024;
71/// The most instance ids an entry names.
72pub const MAX_REMOTE_EVIDENCE_INSTANCE: usize = 8;
73
74/// Why a record has no version 1 projection, or does not fit its version.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum RunVersionError {
77    /// Only a `rustev.run/2` record projects to version 1.
78    NotVersion2,
79    /// A version member is present where the schema has none, absent where
80    /// it is required, or beyond its bounds.
81    Members(&'static str),
82}
83
84impl std::fmt::Display for RunVersionError {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        match self {
87            RunVersionError::NotVersion2 => f.write_str("only rustev.run/2 projects to version 1"),
88            RunVersionError::Members(why) => write!(f, "run record members: {why}"),
89        }
90    }
91}
92
93impl std::error::Error for RunVersionError {}
94
95impl RunRecord {
96    /// Whether the version-specific members fit the schema: the remote
97    /// evidence list exactly in `rustev.run/2`, within its bounds.
98    pub fn check_version_members(&self) -> Result<(), RunVersionError> {
99        if self.schema != schema::RUN_V3
100            && self
101                .requests
102                .iter()
103                .any(|r| r.screen.is_some() || r.result == RequestResult::Withdrawn)
104        {
105            return Err(RunVersionError::Members("screening requires rustev.run/3"));
106        }
107        if let Some(pages) = &self.acquisitions {
108            if self.schema != schema::RUN_V3 {
109                return Err(RunVersionError::Members(
110                    "acquisitions require rustev.run/3",
111                ));
112            }
113            if pages.iter().map(|p| p.attempts.len()).sum::<usize>() > 4096 {
114                return Err(RunVersionError::Members("too many acquisition attempts"));
115            }
116        }
117        let v2 = self.schema == schema::RUN_V2;
118        match &self.remote_evidence {
119            None if v2 => Err(RunVersionError::Members(
120                "rustev.run/2 needs remote_evidence",
121            )),
122            Some(_) if !v2 => Err(RunVersionError::Members(
123                "remote_evidence is a rustev.run/2 member",
124            )),
125            _ if self.remote_evidence_truncated && !v2 => Err(RunVersionError::Members(
126                "remote_evidence_truncated is a rustev.run/2 member",
127            )),
128            Some(list) if list.len() > MAX_REMOTE_EVIDENCE => {
129                Err(RunVersionError::Members("too many remote evidence entries"))
130            }
131            Some(list) if !list.iter().all(RemoteEvidence::is_bounded) => Err(
132                RunVersionError::Members("a remote evidence entry beyond its bounds"),
133            ),
134            _ => Ok(()),
135        }
136    }
137
138    /// The parser's version rule: a record carries the remote evidence
139    /// members exactly in `rustev.run/2`, within their bounds, so a version
140    /// 1 or 3 record with them, or a version 2 record without the list, is
141    /// refused when read, not only when projected.
142    fn parsed_members_fit(&self) -> Result<(), String> {
143        if let CoreEvidence::Recorded(e) = &self.core
144            && e.generation.is_some()
145            && (self.schema != schema::RUN_V3 || e.schema != schema::EVIDENCE_V2)
146        {
147            return Err("generation evidence requires rustev.run/3 and rustev.evidence/2".into());
148        }
149        if let CoreEvidence::Recorded(e) = &self.core
150            && (self.schema != schema::RUN_V3 || e.schema != schema::EVIDENCE_V2)
151            && (e.acquisitions.is_some() || e.steps.iter().any(|s| s.constructed))
152        {
153            return Err("acquisition evidence requires rustev.evidence/2".into());
154        }
155
156        self.check_version_members().map_err(|e| e.to_string())
157    }
158
159    /// The deterministic version 1 projection of a `rustev.run/2` record
160    /// (spec 020, 3.2): the remote evidence removed, the schema tag
161    /// changed, every other member exactly as it is, so the canonical bytes
162    /// are the version 1 representation. Refused, never partial, for any
163    /// other record.
164    pub fn project_v1(&self) -> Result<RunRecord, RunVersionError> {
165        if self.schema != schema::RUN_V2 {
166            return Err(RunVersionError::NotVersion2);
167        }
168        self.check_version_members()?;
169        Ok(RunRecord {
170            schema: schema::RUN.into(),
171            remote_evidence: None,
172            remote_evidence_truncated: false,
173            ..self.clone()
174        })
175    }
176}
177
178/// One remote attempt's evidence reference in `rustev.run/2` (spec 020,
179/// 3.2, I-4). It names the exchange record, never embeds it: no request or
180/// response body, credential, secret address, unredacted principal
181/// reference, provider reasoning or header.
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
183#[serde(deny_unknown_fields)]
184pub struct RemoteEvidence {
185    pub attempt_id: String,
186    /// The decision step and logical request (instance) the attempt served.
187    pub step: String,
188    pub instance: Vec<String>,
189    pub exchange: ExchangeReference,
190    /// What the adapter asked for; `None` when it names no model.
191    pub requested_identity: Option<String>,
192    pub served: ServedIdentity,
193    /// The adapter binding (its artifact), the protocol or foreign mapping
194    /// version, and the endpoint class (never a secret URL).
195    pub binding: ArtifactId,
196    pub protocol: String,
197    pub endpoint_class: String,
198    /// The remote error code of a failed attempt.
199    pub code: Option<RemoteCode>,
200    /// The attempt's final remote state, as the run record has it.
201    pub remote_state: RemoteState,
202    pub charge_provenance: ChargeProvenance,
203    /// Present when the attempt was a member of a batched exchange.
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub batch: Option<BatchMembership>,
206    /// The principal reference's evidence digest, only when one was sent.
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub principal_digest: Option<String>,
209}
210
211impl RemoteEvidence {
212    /// Every string within [`MAX_TEXT_BYTES`], the instance within
213    /// [`MAX_REMOTE_EVIDENCE_INSTANCE`] ids, and the canonical bytes within
214    /// [`MAX_REMOTE_EVIDENCE_ENTRY_BYTES`].
215    pub fn is_bounded(&self) -> bool {
216        self.strings_within(MAX_TEXT_BYTES)
217            && crate::canonical::canonical_bytes(self)
218                .is_ok_and(|b| b.len() <= MAX_REMOTE_EVIDENCE_ENTRY_BYTES)
219    }
220
221    fn strings_within(&self, max: usize) -> bool {
222        let t = |s: &str| s.len() <= max;
223        let o = |s: &Option<String>| s.as_deref().is_none_or(t);
224        t(&self.attempt_id)
225            && t(&self.step)
226            && self.instance.len() <= MAX_REMOTE_EVIDENCE_INSTANCE
227            && self.instance.iter().all(|i| t(i))
228            && self.exchange.is_bounded(max)
229            && o(&self.requested_identity)
230            && o(&self.served.identity)
231            && t(&self.protocol)
232            && t(&self.endpoint_class)
233            && self.batch.as_ref().is_none_or(|b| t(&b.batch_id))
234            && o(&self.principal_digest)
235    }
236
237    /// The entry cut to its bounds: strings to [`MAX_TEXT_BYTES`], and when
238    /// the canonical bytes are still beyond the entry bound (strings that
239    /// escape to several bytes each), to 32 bytes. Deterministic.
240    pub fn bounded(self) -> Self {
241        let e = self.cut(MAX_TEXT_BYTES);
242        if e.is_bounded() { e } else { e.cut(32) }
243    }
244
245    fn cut(mut self, max: usize) -> Self {
246        let c = |s: &mut String| {
247            if s.len() > max {
248                *s = crate::remote::cut_to(s, max);
249            }
250        };
251        c(&mut self.attempt_id);
252        c(&mut self.step);
253        self.instance.truncate(MAX_REMOTE_EVIDENCE_INSTANCE);
254        self.instance.iter_mut().for_each(c);
255        if let ExchangeReference::Retained {
256            digest,
257            schema,
258            locator,
259            ..
260        } = &mut self.exchange
261        {
262            c(digest);
263            c(schema);
264            c(locator);
265        }
266        if let Some(r) = &mut self.requested_identity {
267            c(r);
268        }
269        if let Some(s) = &mut self.served.identity {
270            c(s);
271        }
272        c(&mut self.protocol);
273        c(&mut self.endpoint_class);
274        if let Some(b) = &mut self.batch {
275            c(&mut b.batch_id);
276        }
277        if let Some(p) = &mut self.principal_digest {
278            c(p);
279        }
280        self
281    }
282}
283
284/// Where the exchange record of an attempt is, or why it cannot be named.
285#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
286#[serde(rename_all = "snake_case", deny_unknown_fields)]
287pub enum ExchangeReference {
288    Retained {
289        /// The exchange record's record digest.
290        digest: String,
291        /// The exchange record's schema.
292        schema: String,
293        retention: RetentionClass,
294        /// An opaque host reference, never a URL with credentials.
295        locator: String,
296    },
297    Unavailable {
298        reason: EvidenceUnavailable,
299    },
300}
301
302impl ExchangeReference {
303    fn is_bounded(&self, max: usize) -> bool {
304        match self {
305            ExchangeReference::Retained {
306                digest,
307                schema,
308                locator,
309                ..
310            } => [digest, schema, locator].iter().all(|s| s.len() <= max),
311            ExchangeReference::Unavailable { .. } => true,
312        }
313    }
314}
315
316/// What the host retained of an exchange.
317#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
318#[serde(rename_all = "snake_case")]
319pub enum RetentionClass {
320    /// The record with digests only (spec 009, 3.9.3).
321    DigestOnly,
322    /// The record and the request and response bytes.
323    BytesRetained,
324}
325
326/// Why an attempt's exchange record cannot be named (spec 020, 3.2): stated,
327/// never reconstructed.
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
329#[serde(rename_all = "snake_case")]
330pub enum EvidenceUnavailable {
331    /// Nothing was exchanged: the attempt was refused before any request.
332    NoExchange,
333    /// The exchange had not ended when the record was built.
334    Pending,
335    /// The exchange sink refused or failed the record.
336    NotDelivered,
337    /// The exchange sink accepted the record but named no locator.
338    NoLocator,
339}
340
341/// How the attempt's charge was established.
342#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
343#[serde(rename_all = "snake_case")]
344pub enum ChargeProvenance {
345    /// Reported by the remote side for this attempt's own exchange.
346    Reported,
347    /// A share of a batched exchange's reported charge.
348    Attributed,
349    /// No request byte left the process: nothing was charged.
350    NothingSent,
351    /// Not known; the reservation stays liability.
352    Unknown,
353}
354
355/// An attempt's place in a batched exchange.
356#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
357#[serde(deny_unknown_fields)]
358pub struct BatchMembership {
359    pub batch_id: String,
360    pub index: u32,
361}
362
363#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
364#[serde(rename_all = "snake_case", deny_unknown_fields)]
365pub enum RecordedExecution {
366    None,
367    Declared(ExecutionPolicyId),
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
371#[serde(rename_all = "snake_case")]
372pub enum Termination {
373    /// Every request resolved and the core finished a judgment.
374    Judged,
375    /// The caller cancelled; no judgment was finished.
376    Cancelled,
377}
378
379#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
380#[serde(rename_all = "snake_case", deny_unknown_fields)]
381pub enum CoreEvidence {
382    /// Exactly what the core's `finish` returned.
383    Recorded(Box<EvidenceRecord>),
384    NotProduced,
385}
386
387/// Monotonic milliseconds from submission; never wall-clock time.
388#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
389#[serde(deny_unknown_fields)]
390pub struct Timing {
391    pub queued_ms: u64,
392    pub elapsed_ms: u64,
393    pub deadline_ms: u64,
394    pub deadline_expired: bool,
395}
396
397#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
398#[serde(deny_unknown_fields)]
399pub struct RequestRecord {
400    pub step: String,
401    pub instance: Vec<String>,
402    pub result: RequestResult,
403    pub attempts: Vec<AttemptRecord>,
404    pub transitions: Vec<Transition>,
405    /// Absent for unoptimized execution, preserving existing record bytes.
406    #[serde(default, skip_serializing_if = "Option::is_none")]
407    pub optimization: Option<OptimizationRecord>,
408    /// Screening cache and text-unit observations (spec 035, 3.13.2).
409    #[serde(
410        default,
411        skip_serializing_if = "Option::is_none",
412        deserialize_with = "crate::screening::present"
413    )]
414    #[schemars(with = "crate::screening::ScreenRequest")]
415    pub screen: Option<crate::screening::ScreenRequest>,
416}
417
418/// Where a supplied value came from (spec 004, 3.2).
419#[derive(Debug, Clone, Copy, PartialEq)]
420pub struct SuppliedFrom<'r> {
421    /// The producing target of an output; for a failure, the `to` of the
422    /// last `fallback` transition, else 0.
423    pub target: u32,
424    /// The producing attempt: an output's last attempt; a failure's last
425    /// attempt only when no `retry` or `fallback` transition has an `after`
426    /// equal to its ordinal.
427    pub attempt: Option<&'r AttemptRecord>,
428    /// For an output reused from a cache entry (spec 017, 3.8): the attempt
429    /// of an earlier decision that produced it. Set only when the record has
430    /// no attempt of its own and its optimization record names a hit.
431    pub reused_from: Option<&'r str>,
432}
433
434impl RequestRecord {
435    /// Where this request's supplied value came from; `None` when nothing was
436    /// supplied. The one derivation runtime capture and replay both use.
437    pub fn supplied_from(&self) -> Option<SuppliedFrom<'_>> {
438        match &self.result {
439            RequestResult::NotSupplied | RequestResult::Withdrawn => None,
440            RequestResult::Output { target } => Some(SuppliedFrom {
441                target: *target,
442                attempt: self.attempts.last(),
443                reused_from: self
444                    .optimization
445                    .as_ref()
446                    .filter(|o| self.attempts.is_empty() && o.outcome == OptimizationOutcome::Hit)
447                    .and_then(|o| o.source_attempt_id.as_deref()),
448            }),
449            RequestResult::Failed(_) => {
450                let target = self
451                    .transitions
452                    .iter()
453                    .rev()
454                    .find_map(|t| match t {
455                        Transition::Fallback { to, .. } => Some(*to),
456                        _ => None,
457                    })
458                    .unwrap_or(0);
459                let n = self.attempts.len() as u32;
460                let moved_on = self.transitions.iter().any(|t| {
461                    matches!(t, Transition::Retry { after, .. } | Transition::Fallback { after, .. } if *after == n)
462                });
463                Some(SuppliedFrom {
464                    target,
465                    attempt: if moved_on { None } else { self.attempts.last() },
466                    reused_from: None,
467                })
468            }
469        }
470    }
471}
472
473#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
474#[serde(rename_all = "snake_case", deny_unknown_fields)]
475pub enum RequestResult {
476    /// An output was supplied, from this target (0 is the bound backend).
477    Output { target: u32 },
478    /// This runtime reason was supplied.
479    Failed(Unresolved),
480    /// Nothing was supplied: the decision was cancelled first.
481    NotSupplied,
482    /// Not dispatched because a screening refusal decided the outcome.
483    Withdrawn,
484}
485
486#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
487#[serde(deny_unknown_fields)]
488pub struct AttemptRecord {
489    /// `<decision id>/<step>/<instance ids joined by ','>/<n>`, each
490    /// component with `%`, `/` and `,` percent-escaped.
491    pub attempt_id: String,
492    pub target: u32,
493    pub backend_id: String,
494    pub artifact: ArtifactId,
495    pub dispatched_ms: u64,
496    pub ended_ms: u64,
497    pub end: AttemptEnd,
498    pub cancellation: Cancellation,
499    pub remote: RemoteState,
500    pub cost: AttemptCost,
501}
502
503#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
504#[serde(rename_all = "snake_case", deny_unknown_fields)]
505pub enum AttemptEnd {
506    /// A valid output.
507    Output,
508    Failed {
509        class: FailureClass,
510        detail: String,
511    },
512    /// The decision deadline passed while it was in flight.
513    Deadline,
514    /// The caller cancelled while it was in flight.
515    Cancelled,
516}
517
518/// What is known about cancelling an attempt (spec 003, 3.7.3).
519#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
520#[serde(rename_all = "snake_case", deny_unknown_fields)]
521pub enum Cancellation {
522    NotRequested,
523    Requested { answer: CancelAnswer },
524}
525
526#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
527#[serde(rename_all = "snake_case")]
528pub enum CancelAnswer {
529    /// The backend confirmed that its work and charges stopped.
530    Stopped,
531    /// The backend answered without confirming a remote stop.
532    Unconfirmed,
533    /// The runtime stopped waiting before any answer.
534    NotObserved,
535}
536
537#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
538#[serde(rename_all = "snake_case")]
539pub enum RemoteState {
540    /// The backend returned a result of its own accord.
541    Finished,
542    /// The backend acknowledged a cancellation as stopped.
543    Stopped,
544    /// Nothing establishes that remote work or charges stopped.
545    PossiblyContinuing,
546}
547
548#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
549#[serde(deny_unknown_fields)]
550pub struct AttemptCost {
551    pub bound: CostBound,
552    pub reserved: u64,
553    pub charge: Charge,
554}
555
556/// A backend's static cost disclosure.
557#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
558#[serde(rename_all = "snake_case")]
559pub enum CostModel {
560    /// Every call has an enforceable upper bound.
561    Bounded,
562    /// Calls have estimates, not bounds.
563    Estimated,
564    Unknown,
565}
566
567/// A backend's per-call cost disclosure, before dispatch.
568#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
569#[serde(rename_all = "snake_case", deny_unknown_fields)]
570pub enum CostBound {
571    /// The call cannot cost more than this.
572    Bounded {
573        max_units: u64,
574    },
575    Estimated {
576        units: u64,
577    },
578    Unknown,
579}
580
581/// What an attempt cost, as far as is known when it ends.
582#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
583#[serde(rename_all = "snake_case", deny_unknown_fields)]
584pub enum Charge {
585    Observed { units: u64 },
586    Estimated { units: u64 },
587    Unknown,
588}
589
590#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
591#[serde(rename_all = "snake_case", deny_unknown_fields)]
592pub enum Transition {
593    /// Retry the same target after attempt `after`.
594    Retry {
595        after: u32,
596        class: FailureClass,
597        delay_ms: u64,
598    },
599    /// Move to fallback target `to` after attempt `after`.
600    Fallback {
601        after: u32,
602        class: FailureClass,
603        to: u32,
604    },
605    /// Stop after attempt `after` (0 when nothing was dispatched).
606    Stop { after: u32, reason: String },
607}
608
609#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
610#[serde(rename_all = "snake_case")]
611pub enum CostMode {
612    Unlimited,
613    Hard,
614    Estimated,
615}
616
617/// A decision's cost at the moment its record was built (spec 003, 3.5.6).
618#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
619#[serde(deny_unknown_fields)]
620pub struct CostSummary {
621    pub mode: CostMode,
622    /// 0 under `unlimited`.
623    pub limit: u64,
624    pub observed: u64,
625    pub estimated: u64,
626    /// Reservations kept because the final cost is unknown.
627    pub liability: u64,
628    /// Observed charges above a `bounded` reservation.
629    pub bound_violations: u64,
630    pub final_cost: FinalCost,
631    /// True only under `hard` with no liability and no bound violation.
632    pub within_guaranteed_cap: bool,
633}
634
635#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
636#[serde(rename_all = "snake_case")]
637pub enum FinalCost {
638    Known,
639    IncludesEstimates,
640    Unknown,
641}