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