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 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 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 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 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 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}