Skip to main content

rustev_core/
acquisition_eval.rs

1//! Pure staged acquisition evaluation. A caller supplies pages in request order.
2use super::*;
3#[path = "widening_eval.rs"]
4pub(super) mod widening;
5use rustev_contract::Document;
6use rustev_contract::acquisition::{
7    AcquisitionPage, AcquisitionRequest, NextPage, RequestPage, RequestValue, validate_page,
8};
9use rustev_contract::acquisition_record::*;
10use rustev_contract::acquisition_step::{Acquire, AcquireStop, Guard};
11use rustev_contract::definition::StepBody;
12use rustev_contract::grade::AsOf;
13use rustev_contract::ids::{AcquisitionRequestId, ContentDigest};
14use rustev_contract::time::DurationMs;
15
16/// A single pending page, with the exact identity computed by the core.
17#[derive(Debug, Clone, PartialEq)]
18pub struct PendingAcquisition {
19    pub step: String,
20    pub tier_index: u32,
21    pub page_index: u32,
22    pub source: String,
23    pub operation: String,
24    pub deadline_share: rustev_contract::Decimal,
25    pub request: AcquisitionRequest,
26    pub request_id: AcquisitionRequestId,
27}
28
29pub(super) struct State {
30    decl: Acquire,
31    operation: rustev_contract::acquisition::Operation,
32    pending: Option<PendingAcquisition>,
33    stop: Option<u64>,
34    values: Vec<Value>,
35    evidence: AcquisitionEvidence,
36    oldest: Option<Timestamp>,
37}
38
39impl Evaluation<'_> {
40    /// Pending pages in definition step order. Each step has at most one.
41    pub fn pending_acquisitions(&self) -> Vec<PendingAcquisition> {
42        self.compiled
43            .info
44            .step_ids
45            .iter()
46            .filter_map(|id| {
47                self.acquisitions
48                    .get(id)
49                    .and_then(|s| s.pending.clone())
50                    .or_else(|| self.widenings.get(id).and_then(|s| s.pending.clone()))
51            })
52            .collect()
53    }
54
55    pub(super) fn acquisition_evidence(&self) -> Option<Vec<AcquisitionEvidence>> {
56        let records: Vec<_> = self
57            .compiled
58            .info
59            .step_ids
60            .iter()
61            .filter_map(|id| {
62                self.acquisitions
63                    .get(id)
64                    .map(|s| s.evidence.clone())
65                    .or_else(|| self.widenings.get(id).map(|s| s.evidence.clone()))
66            })
67            .collect();
68        (!records.is_empty()).then_some(records)
69    }
70
71    fn acquisition_refs(&self, refs: &[String]) -> Result<Option<String>, Unresolved> {
72        let bases: Vec<_> = refs.iter().map(|r| crate::expr::base_ref_v2(r)).collect();
73        if let Some(u) = merge(bases.iter().filter_map(|r| self.status(r).err()).collect()) {
74            return Err(u);
75        }
76        Ok(bases
77            .iter()
78            .find_map(|r| self.skipped.get(r.strip_prefix("step:")?).cloned()))
79    }
80
81    // An explicitly absent input is observable by a presence probe. Other
82    // unresolved reads still propagate their reason (spec 036, 3.4.3).
83    fn acquisition_guard_refs(
84        &self,
85        predicate: &rustev_contract::definition::Predicate,
86        refs: &mut Vec<String>,
87    ) {
88        use rustev_contract::definition::Predicate;
89        match predicate {
90            Predicate::Present(r)
91                if r.strip_prefix("input:")
92                    .and_then(|name| self.input_quality.get(name.split('/').next()?))
93                    .is_some_and(|q| {
94                        matches!(
95                            q.presence,
96                            Some(rustev_contract::grade::Presence::Absent { .. })
97                        )
98                    }) => {}
99            Predicate::All(ps) | Predicate::Any(ps) => {
100                for p in ps {
101                    self.acquisition_guard_refs(p, refs);
102                }
103            }
104            Predicate::Not(p) => self.acquisition_guard_refs(p, refs),
105            p => crate::expr::predicate_refs(p, refs),
106        }
107    }
108
109    // A false or failed guard does not wait for request-only dependencies.
110    pub(super) fn acquisition_guard_terminal(&self, id: &str) -> bool {
111        let Some(StepBody::Acquire(a)) = self
112            .compiled
113            .plan
114            .definition
115            .steps
116            .iter()
117            .find(|s| s.id == id)
118            .map(|s| &s.body)
119        else {
120            return false;
121        };
122        let Guard::Predicate(p) = &a.when else {
123            return false;
124        };
125        let mut refs = vec![];
126        crate::expr::predicate_refs(p, &mut refs);
127        if !refs
128            .iter()
129            .all(|r| self.dep_done(&crate::expr::base_ref_v2(r)))
130        {
131            return false;
132        }
133        let mut status_refs = vec![];
134        self.acquisition_guard_refs(p, &mut status_refs);
135        if self.acquisition_refs(&status_refs).is_err() {
136            return true;
137        }
138        let mut values = vec![];
139        crate::compile::predicate_value_refs(p, &mut values);
140        if matches!(self.acquisition_refs(&values), Ok(Some(_))) {
141            return true;
142        }
143        let mut reads = vec![];
144        quality::predicate_reads(p, None, true, &mut reads);
145        self.acquisition_quality(&reads).is_err()
146            || !matches!(crate::expr::eval_predicate(p, &View::scope(self)), Ok(true))
147    }
148
149    fn acquisition_quality(&self, reads: &[quality::Read]) -> Result<(), Unresolved> {
150        for r in reads {
151            if View::skipped(self, &r.base) {
152                continue;
153            }
154            grading::check_read(
155                r,
156                &quality::read_ceiling(r, &self.compiled.info.trees),
157                &grading::read_meta(r, &self.metas),
158                self.now,
159            )?;
160        }
161        Ok(())
162    }
163
164    pub(super) fn start_acquisition(&mut self, id: &str, a: &crate::acquisition::Acquisition) {
165        if let Some(StepBody::Widen(w)) = self
166            .compiled
167            .plan
168            .definition
169            .steps
170            .iter()
171            .find(|s| s.id == id)
172            .map(|s| &s.body)
173        {
174            self.start_widening(id, w, a);
175            return;
176        }
177        let Some(StepBody::Acquire(decl)) = self
178            .compiled
179            .plan
180            .definition
181            .steps
182            .iter()
183            .find(|s| s.id == id)
184            .map(|s| &s.body)
185        else {
186            // A compiled acquisition always names an `acquire` step; record the
187            // broken invariant as the step's result so evaluation completes.
188            self.exact.insert(
189                id.into(),
190                Err(Unresolved::invalid(
191                    [],
192                    "acquisition step not found in the plan",
193                )),
194            );
195            return;
196        };
197        // Compilation refuses an acquisition without a bound source and
198        // operation (3.9.3), so this is a broken invariant, recorded like the
199        // one above; acquisition evidence needs the source and operation it
200        // would name, so none is written for it.
201        let (Some(operation), Some(binding)) = (a.operations.first(), a.plan.sources.first())
202        else {
203            self.exact.insert(
204                id.into(),
205                Err(Unresolved::invalid(
206                    [],
207                    "acquisition step has no bound source",
208                )),
209            );
210            return;
211        };
212        let mut state = State {
213            decl: decl.clone(),
214            operation: operation.clone(),
215            pending: None,
216            stop: None,
217            values: vec![],
218            oldest: None,
219            evidence: AcquisitionEvidence {
220                step: id.into(),
221                kind: AcquisitionKind::Acquire,
222                outcome: AcquisitionOutcome::Resolved,
223                stopped_by: AcquisitionStop::None,
224                truncated: 0,
225                request_constructed: false,
226                tiers: vec![TierEvidence {
227                    name: id.into(),
228                    outcome: TierOutcome::Empty,
229                    relaxation: 0,
230                    source: binding.source.name.clone(),
231                    operation: operation.name.clone(),
232                    request_constructed: false,
233                    items_received: 0,
234                    unique_items_added: 0,
235                    duplicates: 0,
236                    pages: vec![],
237                }],
238                items: vec![],
239            },
240        };
241        self.derivation
242            .insert(format!("step:{id}"), a.plan.derivation);
243        let built = (|| -> Result<Option<String>, Unresolved> {
244            if let Guard::Predicate(p) = &decl.when {
245                let mut refs = vec![];
246                self.acquisition_guard_refs(p, &mut refs);
247                self.acquisition_refs(&refs)?;
248                let mut values = vec![];
249                crate::compile::predicate_value_refs(p, &mut values);
250                if let Some(guard) = self.acquisition_refs(&values)? {
251                    return Ok(Some(guard));
252                }
253                let mut reads = vec![];
254                quality::predicate_reads(p, None, true, &mut reads);
255                self.acquisition_quality(&reads)?;
256                if !crate::expr::eval_predicate(p, &View::scope(self)).map_err(fault)? {
257                    return Ok(Some(id.into()));
258                }
259            }
260            let mut refs = vec![];
261            for e in decl.request.values() {
262                crate::expr::expr_refs(e, &mut refs);
263            }
264            if let AcquireStop::ItemsAtLeast { count } = &decl.stop {
265                crate::expr::expr_refs(count, &mut refs);
266            }
267            if let Some(guard) = self.acquisition_refs(&refs)? {
268                return Ok(Some(guard));
269            }
270            let mut reads = vec![];
271            for e in decl.request.values() {
272                quality::expr_reads(e, None, false, &mut reads);
273            }
274            if let AcquireStop::ItemsAtLeast { count } = &decl.stop {
275                quality::expr_reads(count, None, false, &mut reads);
276            }
277            self.acquisition_quality(&reads)?;
278            state.stop = match &decl.stop {
279                AcquireStop::None => None,
280                AcquireStop::ItemsAtLeast { count } => {
281                    match crate::expr::eval_expr(count, &View::scope(self)).map_err(fault)? {
282                        Value::Integer(n) if n > 0 => Some(n as u64),
283                        _ => return Err(Unresolved::invalid([], "stop count must be positive")),
284                    }
285                }
286            };
287            let mut fields = vec![];
288            let mut constructed = false;
289            for f in &operation.request {
290                fields.push(match decl.request.get(&f.name) {
291                    Some(e) => {
292                        let mut reads = vec![];
293                        crate::expr::expr_refs(e, &mut reads);
294                        constructed |= reads
295                            .iter()
296                            .any(|r| self.constructed.contains(&crate::expr::base_ref_v2(r)));
297                        RequestValue::Set {
298                            value: crate::expr::eval_expr(e, &View::scope(self))
299                                .map_err(fault)?
300                                .to_json(),
301                        }
302                    }
303                    None => RequestValue::Unset,
304                });
305            }
306            state.evidence.request_constructed = constructed;
307            if let Some(tier) = state.evidence.tiers.first_mut() {
308                tier.request_constructed = constructed;
309            }
310            let request = AcquisitionRequest {
311                schema: schema::ACQUISITION_REQUEST.into(),
312                source: binding.source.id.clone(),
313                operation: operation.name.clone(),
314                fields,
315                page: RequestPage::First,
316            };
317            state.pending = Some(make_pending(
318                id,
319                0,
320                request,
321                operation.max_request_bytes,
322                &decl.source,
323                decl.limits.deadline_share,
324            )?);
325            Ok(None)
326        })();
327        match built {
328            Ok(Some(guard)) => {
329                self.skipped.insert(id.into(), guard.clone());
330                state.evidence.outcome = AcquisitionOutcome::Skipped { guard };
331                if let Some(t) = state.evidence.tiers.first_mut() {
332                    t.outcome = TierOutcome::Skipped;
333                }
334            }
335            Err(u) => self.fail_acquisition(id, &mut state, u),
336            Ok(None) => {}
337        }
338        self.acquisitions.insert(id.into(), state);
339    }
340
341    fn fail_acquisition(&mut self, id: &str, state: &mut State, u: Unresolved) {
342        if let Some(p) = state.pending.take()
343            && let Some(t) = state.evidence.tiers.first_mut()
344            && t.pages.len() == p.page_index as usize
345        {
346            t.pages.push(PageEvidence {
347                request: p.request_id,
348                digest: PageDigest::None,
349                items: 0,
350                unique_items_added: 0,
351            });
352        }
353        state.values.clear();
354        state.evidence.items.clear();
355        state.evidence.outcome = AcquisitionOutcome::Unresolved(u.clone());
356        if let Some(t) = state.evidence.tiers.first_mut() {
357            t.outcome = TierOutcome::Failed { reason: u.clone() };
358            t.unique_items_added = 0;
359            for page in &mut t.pages {
360                page.unique_items_added = 0;
361            }
362        }
363        self.exact.insert(id.into(), Err(u));
364    }
365
366    /// Parse and validate page bytes against the pending request and bound contract.
367    /// An invalid page completes the step as `invalid_backend_output`.
368    pub fn supply_page(
369        &mut self,
370        step: &str,
371        tier_index: u32,
372        page_index: u32,
373        bytes: &[u8],
374    ) -> Result<(), SupplyError> {
375        if self.widenings.contains_key(step) {
376            return self.supply_widening_page(step, tier_index, page_index, bytes);
377        }
378        let pending = self
379            .acquisitions
380            .get(step)
381            .and_then(|s| s.pending.clone())
382            .filter(|p| p.tier_index == tier_index && p.page_index == page_index)
383            .ok_or_else(|| not_pending(step))?;
384        let Some(mut state) = self.acquisitions.remove(step) else {
385            return Err(not_pending(step));
386        };
387        let result = (|| -> Result<(), Unresolved> {
388            let page = AcquisitionPage::parse(bytes).map_err(|e| bad_page(e.to_string()))?;
389            validate_page(
390                &page,
391                &pending.request,
392                &state.operation,
393                self.now,
394                DurationMs(state.decl.limits.as_of_skew_ms),
395            )
396            .map_err(|e| bad_page(format!("{e:?}")))?;
397            let digest = ContentDigest::parse(&rustev_contract::canonical::tagged_digest(
398                schema::ACQUISITION_PAGE,
399                &page.canonical().map_err(|e| bad_page(e.to_string()))?,
400            ))
401            .map_err(|e| bad_page(e.to_string()))?;
402            if let Some(t) = state.evidence.tiers.first_mut() {
403                t.items_received += page.items.len() as u64;
404                t.pages.push(PageEvidence {
405                    request: pending.request_id.clone(),
406                    digest: PageDigest::Sha256(digest),
407                    items: page.items.len() as u64,
408                    unique_items_added: 0,
409                });
410            }
411            let age = self
412                .now
413                .as_ms()
414                .saturating_sub(page.as_of_ms.as_ms())
415                .max(0) as u64;
416            let stale: Vec<_> = state.operation.item.iter().filter(|f| matches!(f.freshness, rustev_contract::acquisition::Freshness::MaxAgeMs { value } if age > value)).map(|f| format!("{step}/{}", f.name)).collect();
417            if !stale.is_empty() {
418                return Err(Unresolved::stale(stale));
419            }
420            let room = state
421                .decl
422                .limits
423                .max_items
424                .saturating_sub(state.values.len() as u64);
425            let keep = (page.items.len() as u64).min(room) as usize;
426            let basis = Basis::Acquired { step: step.into() };
427            let source = state
428                .evidence
429                .tiers
430                .first()
431                .map(|t| t.source.clone())
432                .unwrap_or_default();
433            for item in page.items.iter().take(keep) {
434                let mut values = BTreeMap::new();
435                let mut fields = BTreeMap::new();
436                for f in &state.operation.item {
437                    let raw = item
438                        .get(&f.name)
439                        .ok_or_else(|| bad_page("missing item field"))?;
440                    values.insert(
441                        f.name.clone(),
442                        crate::value::decode_v2(&f.ty, raw).map_err(bad_page)?,
443                    );
444                    let mut meta = ValueMeta::ungraded(basis.clone());
445                    meta.grade = f.limit().into();
446                    meta.provenance = vec![f.provenance];
447                    meta.sources = vec![source.clone()];
448                    meta.as_of_ms = AsOf::Ms(page.as_of_ms);
449                    fields.insert(f.name.clone(), meta);
450                }
451                let mut meta = ValueMeta::combination(fields.values(), basis.clone());
452                meta.fields = Some(fields);
453                let value = Value::Record(values);
454                let digest = ContentDigest::parse(&grading::digest(&value.to_json()))
455                    .map_err(|e| bad_page(e.to_string()))?;
456                state.evidence.items.push(AcquiredItemEvidence {
457                    digest,
458                    meta,
459                    origin: None,
460                });
461                state.values.push(value);
462            }
463            if keep > 0 {
464                state.oldest = Some(state.oldest.map_or(page.as_of_ms, |t| t.min(page.as_of_ms)));
465            }
466            state.evidence.truncated += page.items.len() as u64 - keep as u64;
467            if let Some(t) = state.evidence.tiers.first_mut() {
468                t.unique_items_added += keep as u64;
469                if let Some(p) = t.pages.last_mut() {
470                    p.unique_items_added = keep as u64;
471                }
472                t.outcome = if state.values.is_empty() {
473                    TierOutcome::Empty
474                } else {
475                    TierOutcome::Ok
476                };
477            }
478            let continuation = match page.next {
479                NextPage::None => Err((AcquisitionStop::Exhausted, "exhausted")),
480                _ if state.stop.is_some_and(|n| state.values.len() as u64 >= n) => {
481                    Err((AcquisitionStop::StopCondition, "stop_condition"))
482                }
483                _ if u64::from(pending.page_index) + 1 >= state.decl.limits.max_pages => {
484                    Err((AcquisitionStop::PageLimit, "page_limit"))
485                }
486                _ if state.values.len() as u64 >= state.decl.limits.max_items => {
487                    Err((AcquisitionStop::ItemLimit, "item_limit"))
488                }
489                NextPage::Cursor { text } => Ok(RequestPage::Cursor { text }),
490                NextPage::Offset { integer } => Ok(RequestPage::Offset { integer }),
491            };
492            if let Err((stop, name)) = continuation {
493                state.pending = None;
494                state.evidence.stopped_by = stop;
495                let value = Value::Record(BTreeMap::from([
496                    ("items".into(), Value::List(state.values.clone())),
497                    ("stopped_by".into(), Value::Enum(name.into())),
498                    (
499                        "truncated".into(),
500                        Value::Integer(state.evidence.truncated as i64),
501                    ),
502                    (
503                        "as_of_ms".into(),
504                        Value::Timestamp(state.oldest.unwrap_or(page.as_of_ms)),
505                    ),
506                    (
507                        "request_constructed".into(),
508                        Value::Bool(state.evidence.request_constructed),
509                    ),
510                ]));
511                let mut list = ValueMeta::combination(
512                    state.evidence.items.iter().map(|i| &i.meta),
513                    basis.clone(),
514                );
515                list.items = Some(
516                    state
517                        .evidence
518                        .items
519                        .iter()
520                        .map(|i| i.meta.clone())
521                        .collect(),
522                );
523                let mut whole = list.clone();
524                whole.items = None;
525                whole.fields = Some(BTreeMap::from([("items".into(), list)]));
526                self.metas.insert(
527                    format!("step:{step}"),
528                    Meta {
529                        node: whole.grade,
530                        whole,
531                    },
532                );
533                self.exact_values.insert(step.into(), value.clone());
534                self.exact.insert(step.into(), Ok(value));
535                if state.evidence.truncated > 0 {
536                    self.notices.push(Notice {
537                        kind: NoticeKind::Truncation,
538                        subject: format!("step:{step}"),
539                        reason: format!(
540                            "{} acquisition items not retained",
541                            state.evidence.truncated
542                        ),
543                        action: None,
544                        screening: None,
545                    });
546                }
547            } else if let Ok(page) = continuation {
548                let mut request = pending.request.clone();
549                request.page = page;
550                state.pending = Some(make_pending(
551                    step,
552                    pending.page_index + 1,
553                    request,
554                    state.operation.max_request_bytes,
555                    &state.decl.source,
556                    state.decl.limits.deadline_share,
557                )?);
558            }
559            Ok(())
560        })();
561        if let Err(u) = result {
562            self.fail_acquisition(step, &mut state, u);
563        }
564        self.acquisitions.insert(step.into(), state);
565        self.advance();
566        Ok(())
567    }
568
569    /// Complete a pending acquisition with one of the runtime's allowed failures.
570    pub fn supply_acquisition_failure(
571        &mut self,
572        step: &str,
573        tier_index: u32,
574        page_index: u32,
575        reason: Unresolved,
576    ) -> Result<(), SupplyError> {
577        if self.widenings.contains_key(step) {
578            return self.supply_widening_failure(step, tier_index, page_index, reason);
579        }
580        let state = self
581            .acquisitions
582            .get(step)
583            .filter(|s| {
584                s.pending
585                    .as_ref()
586                    .is_some_and(|p| p.tier_index == tier_index && p.page_index == page_index)
587            })
588            .ok_or_else(|| not_pending(step))?;
589        let valid = match &reason {
590            Unresolved::MissingEvidence { fields } => fields == &[step.to_string()],
591            Unresolved::BackendUnavailable { detail } => {
592                state.evidence.tiers.first().is_some_and(|t| {
593                    detail
594                        .strip_prefix(&format!("source:{}", t.source))
595                        .is_some_and(|tail| tail.is_empty() || tail.starts_with([':', ' ']))
596                })
597            }
598            Unresolved::InvalidBackendOutput { .. } | Unresolved::DeadlineExceeded => true,
599            Unresolved::BudgetExhausted { resource } => {
600                matches!(resource.as_str(), "acquisition_calls" | "cost")
601            }
602            _ => false,
603        };
604        if !valid {
605            return Err(SupplyError::NotARuntimeReason(reason.kind()));
606        }
607        let Some(mut state) = self.acquisitions.remove(step) else {
608            return Err(not_pending(step));
609        };
610        self.fail_acquisition(step, &mut state, reason);
611        self.acquisitions.insert(step.into(), state);
612        self.advance();
613        Ok(())
614    }
615}
616fn make_pending(
617    step: &str,
618    page: u32,
619    request: AcquisitionRequest,
620    max_bytes: u64,
621    source: &str,
622    deadline_share: rustev_contract::Decimal,
623) -> Result<PendingAcquisition, Unresolved> {
624    let bytes = request
625        .canonical()
626        .map_err(|e| Unresolved::invalid([], e.to_string()))?;
627    if bytes.len() as u64 > max_bytes {
628        return Err(Unresolved::invalid([], "request exceeds max_request_bytes"));
629    }
630    let request_id = request
631        .id()
632        .map_err(|e| Unresolved::invalid([], e.to_string()))?;
633    Ok(PendingAcquisition {
634        step: step.into(),
635        tier_index: 0,
636        page_index: page,
637        source: source.into(),
638        operation: request.operation.clone(),
639        deadline_share,
640        request,
641        request_id,
642    })
643}
644fn not_pending(step: &str) -> SupplyError {
645    SupplyError::NotPending {
646        step: step.into(),
647        instance: vec![],
648    }
649}
650fn bad_page(detail: impl Into<String>) -> Unresolved {
651    Unresolved::InvalidBackendOutput {
652        detail: detail.into(),
653    }
654}
655fn fault(f: crate::expr::Fault) -> Unresolved {
656    match f {
657        crate::expr::Fault::Missing(s) | crate::expr::Fault::Absent(s) => Unresolved::missing([s]),
658        crate::expr::Fault::Conflict(s) => Unresolved::conflict([s]),
659        crate::expr::Fault::Invalid(s) => Unresolved::invalid([], s),
660    }
661}