1use 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#[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 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 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 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 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 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 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 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}