1use std::collections::BTreeMap;
21use std::fmt;
22use std::panic::{AssertUnwindSafe, catch_unwind};
23use std::sync::{Arc, Mutex};
24
25use rustev_contract::definition::{Operation, StepBody};
26use rustev_contract::ids::{ContentDigest, SnapshotId};
27use rustev_contract::live::{
28 Admission, AttemptRole, AuthorizationOutcome, AuthorizationReference, AuthorizationRefusal,
29 AuthorizationRequest, CapturePosture, ComparisonSection, DeliveryObservation, Dispatch,
30 Exposure, Historical, HistoricalLink, HistoricalRetention, LiveEffect, LiveReexecution,
31 MAX_EFFECTS, RetentionPosture, RunLink,
32};
33use rustev_contract::plan::{PlanExecution, PlanStepDetail};
34use rustev_contract::run::{Charge, CoreEvidence, CostBound, RunRecord, Termination};
35use rustev_contract::scope::{Handle, Scope};
36use rustev_contract::snapshot::Snapshot;
37use rustev_contract::time::Timestamp;
38use rustev_contract::{Document, Identified, schema};
39use rustev_core::evaluate::SemanticRequest;
40use rustev_core::seams::CancelSignal;
41
42use crate::driver::{DETAIL_BYTES, DecisionCtx};
43use crate::wait::cut;
44use crate::{
45 CaptureConfig, CaptureConfigError, CaptureOutcome, Completion, DecideError, Decided,
46 DecisionRequest, Delivery, DeliveryFailure, PreparedPlan, Rejection, Runtime, Target, capture,
47};
48
49#[derive(Debug)]
53pub enum DispatchAnswer {
54 Allow {
57 answers: ContentDigest,
58 reference: AuthorizationReference,
59 valid_for_ms: u64,
60 },
61 Refuse {
62 reference: Option<AuthorizationReference>,
63 detail: String,
64 },
65}
66
67pub trait LiveAuthorizer: Send + Sync {
73 fn authorize(&self, request: &AuthorizationRequest) -> Result<Option<DispatchAnswer>, String>;
74
75 fn authorize_acquisition(
77 &self,
78 _request: &AcquisitionAuthorizationRequest,
79 ) -> Result<Option<DispatchAnswer>, String> {
80 Ok(None)
81 }
82}
83
84#[derive(Debug, Clone)]
86pub struct AcquisitionAuthorizationRequest {
87 pub attempt: crate::AcquisitionSpendAttempt,
88 pub subject: Handle,
89 pub scope: Scope,
90 pub step: String,
91 pub tier: u32,
92 pub page: u32,
93 pub reserved_units: u64,
94 pub remaining_ms: u64,
95}
96impl AcquisitionAuthorizationRequest {
97 pub fn digest(&self) -> Result<ContentDigest, rustev_contract::canonical::CanonicalError> {
98 use rustev_contract::canonical::{canonical_bytes, tagged_digest};
99 let a = &self.attempt;
100 let bytes = canonical_bytes(&serde_json::json!({
101 "decision_id": a.decision_id, "attempt_id": a.attempt_id,
102 "source": a.source, "operation": a.operation, "request": a.request,
103 "bound": a.bound, "subject": self.subject, "scope": self.scope,
104 "step": self.step, "tier": self.tier, "page": self.page,
105 "reserved_units": self.reserved_units, "remaining_ms": self.remaining_ms,
106 }))?;
107 Ok(ContentDigest::parse(&tagged_digest(
108 "rustev.live-acquisition-authorization/1",
109 &bytes,
110 ))
111 .expect("computed digest"))
112 }
113}
114
115#[derive(Debug, Clone, Default)]
117pub struct SourceOutcome {
118 pub incomparable: Option<String>,
119 pub captures: BTreeMap<String, Vec<ContentDigest>>,
120 pub refusals: Vec<(String, String)>,
121}
122
123#[derive(Debug, Clone)]
125pub struct LiveRequest {
126 pub decision_id: String,
128 pub snapshot: Snapshot,
130 pub expected_snapshot_id: Option<SnapshotId>,
134 pub evaluation_time: Timestamp,
136 pub deadline_ms: Option<u64>,
138 pub principal_handle: Vec<u8>,
140 pub subject: Handle,
142 pub scope: Scope,
143 pub capture: Option<CaptureConfig>,
145 pub historical: Option<HistoricalLink>,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
153pub enum LiveRefusal {
154 SpendAcquisitionUnsupported,
156 HistoricalDecisionId,
158 InvalidHistoricalLink,
160 SnapshotIdentity {
162 stated: SnapshotId,
163 computed: SnapshotId,
164 },
165 InvalidSnapshot(String),
167 Capture(CaptureConfigError),
168 Unbounded {
170 worst_case: u64,
171 max: u64,
172 },
173 Rejected(Rejection),
175 GenerationFailed(String),
178}
179
180impl fmt::Display for LiveRefusal {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 match self {
183 LiveRefusal::SpendAcquisitionUnsupported => {
184 f.write_str("gated live acquisition dispatch is not implemented")
185 }
186 LiveRefusal::HistoricalDecisionId => {
187 f.write_str("a live re-execution needs a new decision id, not the historical one")
188 }
189 LiveRefusal::InvalidHistoricalLink => {
190 f.write_str("the historical link's decision id is not 1 to 256 bytes")
191 }
192 LiveRefusal::SnapshotIdentity { stated, computed } => write!(
193 f,
194 "identity mismatch: the snapshot is {computed}, not the stated {stated}"
195 ),
196 LiveRefusal::InvalidSnapshot(e) => write!(f, "the snapshot has no identity: {e}"),
197 LiveRefusal::Capture(e) => write!(f, "{e}"),
198 LiveRefusal::Unbounded { worst_case, max } => write!(
199 f,
200 "the plan can propose {worst_case} attempts; a live document records at most {max}"
201 ),
202 LiveRefusal::Rejected(r) => write!(f, "not admitted: {r:?}"),
203 LiveRefusal::GenerationFailed(detail) => write!(f, "generation failed: {detail}"),
204 }
205 }
206}
207
208impl std::error::Error for LiveRefusal {}
209
210#[derive(Debug)]
214pub struct LiveResult {
215 pub spend_refusal: Option<crate::SpendRefusal>,
217 pub completion: Completion,
218 pub record: Arc<RunRecord>,
220 pub delivery: Result<Delivery, DeliveryFailure>,
221 pub capture: Option<CaptureOutcome>,
223 pub document: LiveReexecution,
224}
225
226struct Presented {
228 step: String,
229 instance: Vec<String>,
230 target: u32,
231 role: AttemptRole,
232 attempt_id: String,
233 backend_id: String,
234 artifact: rustev_contract::ids::ArtifactId,
235 request: rustev_contract::ids::RequestId,
236 presented: ContentDigest,
237 exposure: Exposure,
238 remaining_ms: u64,
239 authorization: AuthorizationOutcome,
240}
241
242pub(crate) struct LiveCtx<'l> {
244 authorizer: &'l dyn LiveAuthorizer,
245 spend: Option<crate::LiveSpend<'l>>,
246 sources: crate::LiveSources<'l>,
247 spend_refusal: Mutex<Option<crate::SpendRefusal>>,
248 acquisition_incomparable: Mutex<Option<String>>,
249 acquisition_captures: Mutex<BTreeMap<String, Vec<ContentDigest>>>,
250 acquisition_refusals: Mutex<Vec<(String, String)>>,
251 subject: &'l Handle,
252 scope: &'l Scope,
253 operations: BTreeMap<String, Operation>,
254 presented: Mutex<Vec<Presented>>,
255}
256
257pub(crate) struct Proposal<'p> {
259 pub req: &'p SemanticRequest,
260 pub target_index: usize,
261 pub target: &'p Target,
262 pub role: AttemptRole,
263 pub attempt_id: &'p str,
264 pub bound: CostBound,
265 pub reserved: u64,
266}
267
268impl LiveCtx<'_> {
269 pub(crate) fn captured_page(
270 &self,
271 r: &rustev_core::evaluate::PendingAcquisition,
272 ) -> Option<Result<Vec<u8>, ()>> {
273 self.spend?;
274 let posture = self
275 .sources
276 .acquisitions
277 .iter()
278 .find(|p| p.source == r.request.source)?;
279 if posture.posture != crate::AcquisitionPosture::Captured {
280 return None;
281 }
282 let id = r.request.id().ok();
283 let mut pages = self
286 .sources
287 .pages
288 .iter()
289 .filter(|p| Some(&p.request) == id.as_ref())
290 .filter_map(|p| p.record_canonical().ok());
291 let first = pages.next();
292 let ambiguous = first.as_ref().is_some_and(|b| pages.any(|p| &p != b));
293 if let Some(bytes) = first.filter(|_| !ambiguous) {
294 let digest = rustev_contract::acquisition::AcquisitionPage::parse(&bytes)
295 .expect("canonical page")
296 .record_digest()
297 .expect("canonical page digest");
298 let mut captures = self
299 .acquisition_captures
300 .lock()
301 .unwrap_or_else(|e| e.into_inner());
302 let digests = captures.entry(r.request.source.to_string()).or_default();
303 if !digests.contains(&digest) {
304 digests.push(digest);
305 digests.sort();
306 }
307 Some(Ok(bytes))
308 } else {
309 let mut reason = self
310 .acquisition_incomparable
311 .lock()
312 .unwrap_or_else(|e| e.into_inner());
313 if reason.is_none() || ambiguous {
314 *reason = Some(
315 if ambiguous {
316 "ambiguous-output"
317 } else {
318 "acquisition-not-captured"
319 }
320 .into(),
321 );
322 }
323 Some(Err(()))
324 }
325 }
326
327 pub(crate) fn source_spend(
328 &self,
329 cx: &DecisionCtx<'_, '_>,
330 r: &rustev_core::evaluate::PendingAcquisition,
331 attempt_id: &str,
332 bound: CostBound,
333 reserved: u64,
334 ) -> Result<Option<crate::spend::HeldSpend<'_>>, ()> {
335 let Some(spend) = self.spend else {
336 return Ok(None);
337 };
338 let refuse = |code: &str| {
339 self.acquisition_refusals
340 .lock()
341 .unwrap_or_else(|e| e.into_inner())
342 .push((attempt_id.into(), code.into()));
343 };
344 if let Some(r) = self.spend_refusal() {
345 refuse(&r.code);
346 return Err(());
347 }
348 let attempt = crate::AcquisitionSpendAttempt {
349 decision_id: cx.decision_id.into(),
350 attempt_id: attempt_id.into(),
351 source: r.request.source.clone(),
352 operation: r.operation.clone(),
353 request: r.request.id().map_err(|_| ())?,
354 bound,
355 };
356 let now = cx.rt.clock.now();
357 let asked = AcquisitionAuthorizationRequest {
358 attempt,
359 subject: self.subject.clone(),
360 scope: self.scope.clone(),
361 step: r.step.clone(),
362 tier: r.tier_index,
363 page: r.page_index,
364 reserved_units: reserved,
365 remaining_ms: cx.deadline.saturating_sub(now),
366 };
367 let digest = asked.digest().map_err(|_| ())?;
368 let until = match catch_unwind(AssertUnwindSafe(|| {
369 self.authorizer.authorize_acquisition(&asked)
370 })) {
371 Ok(Ok(Some(DispatchAnswer::Allow {
372 answers,
373 valid_for_ms,
374 ..
375 }))) if answers == digest && cx.rt.clock.now() < now.saturating_add(valid_for_ms) => {
376 now.saturating_add(valid_for_ms)
377 }
378 _ => {
379 refuse("authorization_refused");
380 return Err(());
381 }
382 };
383 let mut refusal = self.spend_refusal.lock().unwrap_or_else(|e| e.into_inner());
384 if let Some(r) = &*refusal {
385 refuse(&r.code);
386 return Err(());
387 }
388 let result = if bound == CostBound::Unknown {
389 Err(crate::SpendRefusal::new("spend-exposure-unknown"))
390 } else {
391 spend.gate.reserve_acquisition(&asked.attempt)
392 };
393 match result {
394 Ok(ticket) => {
395 let held = crate::spend::HeldSpend::new(spend.gate, ticket);
396 if cx.rt.clock.now() >= until {
397 held.release();
398 refuse("authorization_refused");
399 Err(())
400 } else {
401 Ok(Some(held))
402 }
403 }
404 Err(r) => {
405 refuse(&r.code);
406 *refusal = Some(r);
407 Err(())
408 }
409 }
410 }
411
412 pub(crate) fn spend_refusal(&self) -> Option<crate::SpendRefusal> {
413 self.spend_refusal
414 .lock()
415 .unwrap_or_else(|e| e.into_inner())
416 .clone()
417 }
418
419 pub(crate) fn reserve_spend(
420 &self,
421 cx: &DecisionCtx<'_, '_>,
422 p: Proposal<'_>,
423 ) -> Result<Option<crate::spend::HeldSpend<'_>>, crate::SpendRefusal> {
424 let Some(spend) = self.spend else {
425 return Ok(None);
426 };
427 let mut refusal = self.spend_refusal.lock().unwrap_or_else(|e| e.into_inner());
428 if let Some(r) = &*refusal {
429 return Err(r.clone());
430 }
431 let binding = spend
432 .bindings
433 .iter()
434 .find(|b| b.backend_id == p.target.backend_id && b.artifact == p.target.artifact);
435 let result = match binding {
436 None => Err(crate::SpendRefusal::new("spend-unit")),
437 Some(binding) => spend.gate.reserve(&crate::SpendAttempt {
438 decision_id: cx.decision_id.to_string(),
439 attempt_id: p.attempt_id.to_string(),
440 backend_id: p.target.backend_id.clone(),
441 target: p.target_index as u32,
442 artifact: p.target.artifact.clone(),
443 operation: self.operations[&p.req.step],
444 bound: p.bound,
445 cost_unit: binding.cost_unit,
446 }),
447 };
448 match result {
449 Ok(ticket) => Ok(Some(crate::spend::HeldSpend::new(spend.gate, ticket))),
450 Err(r) => {
451 *refusal = Some(r.clone());
452 Err(r)
453 }
454 }
455 }
456
457 pub(crate) fn expire_authorization(&self, attempt_id: &str) {
458 let mut presented = self.presented.lock().unwrap_or_else(|e| e.into_inner());
459 if let Some(p) = presented.iter_mut().find(|p| p.attempt_id == attempt_id)
460 && let AuthorizationOutcome::Allowed { reference } = &p.authorization
461 {
462 p.authorization = AuthorizationOutcome::Refused {
463 refusal: AuthorizationRefusal::Expired,
464 reference: Some(reference.clone()),
465 detail: "authorization expired during spend reservation".into(),
466 };
467 }
468 }
469
470 pub(crate) fn authorize(
474 &self,
475 cx: &DecisionCtx<'_, '_>,
476 p: Proposal<'_>,
477 ) -> Result<u64, AuthorizationRefusal> {
478 let now = cx.rt.clock.now();
479 let identity = cx
480 .ev()
481 .request_document(
482 &p.req.step,
483 &p.req.instance,
484 p.target_index as u32,
485 self.scope,
486 )
487 .map_err(|e| e.to_string())
488 .and_then(|d| d.id().map_err(|e| e.to_string()));
489 let request = match identity {
490 Ok(id) => id,
491 Err(_) => return Err(AuthorizationRefusal::Errored),
494 };
495 let asked = AuthorizationRequest {
496 decision_id: cx.decision_id.to_string(),
497 subject: self.subject.clone(),
498 scope: self.scope.clone(),
499 step: p.req.step.clone(),
500 instance: p.req.instance.clone(),
501 operation: self.operations[&p.req.step],
502 target: p.target_index as u32,
503 role: p.role,
504 backend_id: p.target.backend_id.clone(),
505 artifact: p.target.artifact.clone(),
506 request: request.clone(),
507 attempt_id: p.attempt_id.to_string(),
508 exposure: Exposure {
509 bound: p.bound,
510 reserved_units: p.reserved,
511 },
512 remaining_ms: cx.deadline.saturating_sub(now),
513 };
514 let Ok(digest) = asked.digest() else {
515 return Err(AuthorizationRefusal::Errored);
516 };
517 let refused = |refusal, reference, detail: &str| AuthorizationOutcome::Refused {
518 refusal,
519 reference,
520 detail: cut(detail, DETAIL_BYTES),
521 };
522 let mut valid_until = now;
523 let outcome = match catch_unwind(AssertUnwindSafe(|| self.authorizer.authorize(&asked))) {
524 Err(p) => refused(
525 AuthorizationRefusal::Errored,
526 None,
527 &format!(
528 "authorizer panicked: {}",
529 crate::wait::panic_detail(p.as_ref())
530 ),
531 ),
532 Ok(Err(detail)) => refused(AuthorizationRefusal::Errored, None, &detail),
533 Ok(Ok(None)) => refused(AuthorizationRefusal::Missing, None, "no answer"),
534 Ok(Ok(Some(DispatchAnswer::Refuse { reference, detail }))) => {
535 refused(AuthorizationRefusal::Refused, reference, &detail)
536 }
537 Ok(Ok(Some(DispatchAnswer::Allow {
538 answers,
539 reference,
540 valid_for_ms,
541 }))) => {
542 if answers != digest {
543 refused(
544 AuthorizationRefusal::Mismatched,
545 Some(reference),
546 "the answer names another request",
547 )
548 } else if cx.rt.clock.now() >= now.saturating_add(valid_for_ms) {
549 refused(
550 AuthorizationRefusal::Expired,
551 Some(reference),
552 &format!("the answer was valid for {valid_for_ms} ms"),
553 )
554 } else {
555 valid_until = now.saturating_add(valid_for_ms);
556 AuthorizationOutcome::Allowed { reference }
557 }
558 }
559 };
560 let result = match &outcome {
561 AuthorizationOutcome::Allowed { .. } => Ok(valid_until),
562 AuthorizationOutcome::Refused { refusal, .. } => Err(*refusal),
563 };
564 self.presented
565 .lock()
566 .unwrap_or_else(|e| e.into_inner())
567 .push(Presented {
568 step: asked.step,
569 instance: asked.instance,
570 target: asked.target,
571 role: asked.role,
572 attempt_id: asked.attempt_id,
573 backend_id: asked.backend_id,
574 artifact: asked.artifact,
575 request,
576 presented: digest,
577 exposure: asked.exposure,
578 remaining_ms: asked.remaining_ms,
579 authorization: outcome,
580 });
581 result
582 }
583}
584
585fn worst_case_attempts(plan: &PreparedPlan) -> u64 {
589 match &plan.compiled().plan.execution {
590 PlanExecution::Declared(d) => d.policy.max_attempts_per_decision,
591 PlanExecution::None => plan
592 .compiled()
593 .plan
594 .steps
595 .iter()
596 .filter_map(|s| match &s.detail {
597 PlanStepDetail::Semantic(b) => Some(b.max_requests),
598 PlanStepDetail::Rank(b) => Some(b.max_requests),
599 _ => None,
600 })
601 .fold(0, u64::saturating_add),
602 }
603}
604
605impl Runtime {
606 pub async fn reexecute_live(
644 &self,
645 plan: &PreparedPlan,
646 req: LiveRequest,
647 authorizer: &dyn LiveAuthorizer,
648 cancel: &CancelSignal,
649 ) -> Result<LiveResult, LiveRefusal> {
650 self.reexecute_live_inner(
651 plan,
652 req,
653 authorizer,
654 cancel,
655 None,
656 crate::LiveSources::default(),
657 )
658 .await
659 .map(|(result, _)| result)
660 }
661
662 pub async fn reexecute_live_with_gate(
667 &self,
668 plan: &PreparedPlan,
669 req: LiveRequest,
670 authorizer: &dyn LiveAuthorizer,
671 cancel: &CancelSignal,
672 spend: crate::LiveSpend<'_>,
673 ) -> Result<LiveResult, LiveRefusal> {
674 if plan
675 .compiled()
676 .plan
677 .acquisitions
678 .as_ref()
679 .is_some_and(|a| !a.is_empty())
680 {
681 return Err(LiveRefusal::SpendAcquisitionUnsupported);
682 }
683 self.reexecute_live_with_sources(
684 plan,
685 req,
686 authorizer,
687 cancel,
688 spend,
689 crate::LiveSources::default(),
690 )
691 .await
692 .map(|(result, _)| result)
693 }
694
695 pub async fn reexecute_live_with_sources(
697 &self,
698 plan: &PreparedPlan,
699 req: LiveRequest,
700 authorizer: &dyn LiveAuthorizer,
701 cancel: &CancelSignal,
702 spend: crate::LiveSpend<'_>,
703 sources: crate::LiveSources<'_>,
704 ) -> Result<(LiveResult, SourceOutcome), LiveRefusal> {
705 self.reexecute_live_inner(plan, req, authorizer, cancel, Some(spend), sources)
706 .await
707 }
708
709 async fn reexecute_live_inner(
710 &self,
711 plan: &PreparedPlan,
712 req: LiveRequest,
713 authorizer: &dyn LiveAuthorizer,
714 cancel: &CancelSignal,
715 spend: Option<crate::LiveSpend<'_>>,
716 sources: crate::LiveSources<'_>,
717 ) -> Result<(LiveResult, SourceOutcome), LiveRefusal> {
718 if spend.is_some_and(|_| {
719 plan.compiled()
720 .plan
721 .acquisitions
722 .iter()
723 .flatten()
724 .flat_map(|a| &a.sources)
725 .any(|a| {
726 sources
727 .acquisitions
728 .iter()
729 .filter(|p| p.source == a.source.id)
730 .count()
731 != 1
732 })
733 }) {
734 return Err(LiveRefusal::SpendAcquisitionUnsupported);
735 }
736 if req
737 .historical
738 .as_ref()
739 .is_some_and(|h| h.decision_id == req.decision_id)
740 {
741 return Err(LiveRefusal::HistoricalDecisionId);
742 }
743 if req
744 .historical
745 .as_ref()
746 .is_some_and(|h| h.decision_id.is_empty() || h.decision_id.len() > 256)
747 {
748 return Err(LiveRefusal::InvalidHistoricalLink);
749 }
750 let snapshot_id = req
751 .snapshot
752 .id()
753 .map_err(|e| LiveRefusal::InvalidSnapshot(e.to_string()))?;
754 if let Some(stated) = &req.expected_snapshot_id
755 && *stated != snapshot_id
756 {
757 return Err(LiveRefusal::SnapshotIdentity {
758 stated: stated.clone(),
759 computed: snapshot_id,
760 });
761 }
762 if let Some(c) = &req.capture {
763 c.check().map_err(LiveRefusal::Capture)?;
764 }
765 let worst_case = worst_case_attempts(plan);
766 if worst_case > MAX_EFFECTS as u64 {
767 return Err(LiveRefusal::Unbounded {
768 worst_case,
769 max: MAX_EFFECTS as u64,
770 });
771 }
772
773 let operations = plan
774 .compiled()
775 .plan
776 .definition
777 .steps
778 .iter()
779 .filter_map(|s| match &s.body {
780 StepBody::Semantic(d) => Some((s.id.clone(), d.operation)),
781 StepBody::Rank(_) => {
782 Some((s.id.clone(), rustev_contract::definition::Operation::Rank))
783 }
784 _ => None,
785 })
786 .collect();
787 let live = LiveCtx {
788 authorizer,
789 spend,
790 sources,
791 spend_refusal: Mutex::new(None),
792 acquisition_incomparable: Mutex::new(None),
793 acquisition_captures: Mutex::new(BTreeMap::new()),
794 acquisition_refusals: Mutex::new(vec![]),
795 subject: &req.subject,
796 scope: &req.scope,
797 operations,
798 presented: Mutex::new(vec![]),
799 };
800 let decision = DecisionRequest {
801 decision_id: req.decision_id.clone(),
802 snapshot: req.snapshot.clone(),
803 evaluation_time: req.evaluation_time,
804 deadline_ms: req.deadline_ms,
805 principal_handle: req.principal_handle.clone(),
806 };
807 let mut capturing = req.capture.as_ref().map(capture::Capturing::new);
808 let result = self
809 .run(
810 plan,
811 decision,
812 cancel,
813 capturing.as_mut(),
814 Some(&live),
815 None,
816 None,
817 &BTreeMap::new(),
818 )
819 .await;
820 let (record, delivery) = match result {
821 Err(DecideError::GenerationFailed(detail)) => {
822 return Err(LiveRefusal::GenerationFailed(detail));
823 }
824 Err(DecideError::Rejected(r)) => return Err(LiveRefusal::Rejected(r)),
825 Ok(Decided {
826 record, delivery, ..
827 }) => (record, Ok(delivery)),
828 Err(DecideError::EvidenceNotDelivered { record, failure }) => (record, Err(failure)),
829 };
830 let capture = capturing.map(|c| {
831 CaptureOutcome::Captured(c.finish(
832 &req.decision_id,
833 record.termination == Termination::Cancelled,
834 ))
835 });
836 let spend_refusal = live.spend_refusal();
837 let presented = live
838 .presented
839 .into_inner()
840 .unwrap_or_else(|e| e.into_inner());
841 let document = document(&req, snapshot_id, &record, &delivery, &capture, presented);
842 let completion = match &record.core {
843 CoreEvidence::Recorded(e) if record.termination == Termination::Judged => {
844 Completion::Judged(Box::new(e.judgment.clone()))
845 }
846 _ => Completion::Cancelled,
847 };
848 let sources = SourceOutcome {
849 incomparable: live
850 .acquisition_incomparable
851 .into_inner()
852 .unwrap_or_else(|e| e.into_inner()),
853 captures: live
854 .acquisition_captures
855 .into_inner()
856 .unwrap_or_else(|e| e.into_inner()),
857 refusals: live
858 .acquisition_refusals
859 .into_inner()
860 .unwrap_or_else(|e| e.into_inner()),
861 };
862 Ok((
863 LiveResult {
864 spend_refusal,
865 completion,
866 record,
867 delivery,
868 capture,
869 document,
870 },
871 sources,
872 ))
873 }
874}
875
876fn document(
877 req: &LiveRequest,
878 snapshot_id: SnapshotId,
879 record: &RunRecord,
880 delivery: &Result<Delivery, DeliveryFailure>,
881 capture: &Option<CaptureOutcome>,
882 presented: Vec<Presented>,
883) -> LiveReexecution {
884 let attempts: BTreeMap<&str, &rustev_contract::run::AttemptRecord> = record
885 .requests
886 .iter()
887 .flat_map(|r| &r.attempts)
888 .map(|a| (a.attempt_id.as_str(), a))
889 .collect();
890 let effects = presented
891 .into_iter()
892 .map(|p| {
893 let dispatch = match attempts.get(p.attempt_id.as_str()) {
894 Some(a) => Dispatch::Dispatched {
895 end: a.end.clone(),
896 cancellation: a.cancellation,
897 remote: a.remote,
898 charge: a.cost.charge,
899 },
900 None => Dispatch::NotDispatched,
901 };
902 LiveEffect {
903 step: p.step,
904 instance: p.instance,
905 target: p.target,
906 role: p.role,
907 attempt_id: p.attempt_id,
908 backend_id: p.backend_id,
909 artifact: p.artifact,
910 request: p.request,
911 presented: p.presented,
912 exposure: p.exposure,
913 remaining_ms: p.remaining_ms,
914 authorization: p.authorization,
915 dispatch,
916 }
917 })
918 .collect();
919 let unreconciled = attempts
920 .values()
921 .filter(|a| a.cost.charge == Charge::Unknown)
922 .map(|a| a.attempt_id.clone())
923 .collect();
924 let delivery = match delivery {
925 Ok(Delivery::Acknowledged { receipt }) => DeliveryObservation::Acknowledged {
926 receipt: cut(receipt, DETAIL_BYTES),
927 },
928 Ok(Delivery::Queued(_)) => DeliveryObservation::Queued,
929 Ok(Delivery::Dropped { dropped_total }) => DeliveryObservation::Dropped {
930 dropped_total: *dropped_total,
931 },
932 Err(f) => DeliveryObservation::NotDelivered {
933 detail: cut(&f.detail, DETAIL_BYTES),
934 uncertain: f.uncertain,
935 },
936 };
937 let capture = match (&req.capture, capture) {
938 (Some(c), Some(CaptureOutcome::Captured(k))) => CapturePosture::On {
939 scope: c.scope.clone(),
940 max_bytes: c.max_bytes,
941 status: k.status,
942 },
943 _ => CapturePosture::Off,
944 };
945 LiveReexecution {
946 schema: schema::LIVE_REEXECUTION.into(),
947 decision_id: req.decision_id.clone(),
948 plan_id: record.plan_id.clone(),
949 snapshot_id,
950 evaluation_time_ms: req.evaluation_time,
951 subject: req.subject.clone(),
952 scope: req.scope.clone(),
953 historical: match &req.historical {
954 Some(h) => Historical::Linked(h.clone()),
955 None => Historical::None,
956 },
957 admission: Admission {
958 queued_ms: record.timing.queued_ms,
959 deadline_ms: record.timing.deadline_ms,
960 },
961 run_digest: record.record_digest().ok(),
962 run: RunLink {
963 termination: record.termination,
964 cost: record.cost,
965 },
966 effects,
967 unreconciled,
968 delivery,
969 retention: RetentionPosture {
970 capture,
971 historical: if req.historical.is_some() {
972 HistoricalRetention::NotObserved
973 } else {
974 HistoricalRetention::NoHistory
975 },
976 },
977 comparison: ComparisonSection::NotRequested,
978 }
979}