1use std::collections::{BTreeMap, BTreeSet};
10use std::fmt;
11
12#[path = "acquisition_eval.rs"]
13mod acquisition_eval;
14pub use acquisition_eval::PendingAcquisition;
15
16use rustev_contract::actionability::Party;
17use rustev_contract::canonical::canonical_value_bytes;
18use rustev_contract::definition::{
19 Candidates, Freshness, ItemSource, Operation, ProvenanceClass, ReasonKind, RequiredKind,
20};
21use rustev_contract::descriptor::OutputKind;
22use rustev_contract::evidence::{
23 CheckEvidence, CheckReason, CheckResult, Corroboration, CorroborationOutcome, EvidenceRecord,
24 InputEvidence, InputStatus, JoinEvidence, MatchStatus, MergeEvidence, ReadEvidence,
25 StepEvidence, StepStatus,
26};
27use rustev_contract::grade::{Basis, Grade, ValueGrade, ValueMeta};
28use rustev_contract::ids::SnapshotId;
29use rustev_contract::judgment::{
30 Derivation, Judgment, Lineage, Notice, NoticeKind, OutValue, Outcome, Unresolved,
31};
32use rustev_contract::output::{BackendOutputDoc, RawOutput};
33use rustev_contract::plan::{PlanExecution, PlanStepDetail};
34use rustev_contract::request::RequestDoc;
35use rustev_contract::scope::Scope as ReplayScope;
36use rustev_contract::screening::{
37 OnBlock, OnUnavailable, ScreenConsequence, ScreenVerdict, ScreeningEvidence, ScreeningItemId,
38};
39use rustev_contract::snapshot::{Entry, Snapshot};
40use rustev_contract::time::Timestamp;
41use rustev_contract::{Identified, schema};
42use serde_json::{Map, Value as Json, json};
43
44use crate::calibrate::{self, BindingCheck, CalibrationInput};
45use crate::compile::{Compiled, StepInfo};
46use crate::expr::{Ref, Scope};
47use crate::grading::{self, InputQuality, Meta};
48use crate::kinds::{Distribution, ModelScore, OrdinalLevel, ScoreScope, SelectedLabel};
49use crate::ops::{OpArgs, OpContext};
50use crate::policy::{self, Seen, View};
51use crate::quality;
52use crate::sem::{SemInstances, SemKind, SemValue};
53use crate::value::{Value, decode};
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum StartError {
58 AcquisitionNotAvailable,
60 GenerationNotAvailable,
61 Schema(String),
63 UndeclaredField(String),
65 InvalidProducer(String),
67 NotCanonical(String),
69}
70
71impl fmt::Display for StartError {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 match self {
74 StartError::GenerationNotAvailable => {
75 write!(f, "generation evaluation is not available")
76 }
77 StartError::AcquisitionNotAvailable => {
78 write!(f, "staged acquisition evaluation is not available")
79 }
80 StartError::Schema(s) => write!(f, "snapshot schema {s:?}"),
81 StartError::UndeclaredField(s) => {
82 write!(f, "snapshot entry for undeclared field {s:?}")
83 }
84 StartError::InvalidProducer(s) => write!(f, "invalid_producer: {s}"),
85 StartError::NotCanonical(s) => write!(f, "snapshot has no canonical form: {s}"),
86 }
87 }
88}
89
90impl std::error::Error for StartError {}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
94pub enum SupplyError {
95 NotPending { step: String, instance: Vec<String> },
97 NotARuntimeReason(ReasonKind),
100}
101
102impl fmt::Display for SupplyError {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 match self {
105 SupplyError::NotPending { step, instance } => {
106 write!(f, "no pending request {step}{instance:?}")
107 }
108 SupplyError::NotARuntimeReason(k) => write!(f, "{} is not a runtime failure", k.name()),
109 }
110 }
111}
112
113impl std::error::Error for SupplyError {}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
117pub enum RequestIdentityError {
118 NotPending { step: String, instance: Vec<String> },
120 UnknownTarget { step: String, target: u32 },
123 ProjectionNotUtf8 { step: String },
126}
127
128impl fmt::Display for RequestIdentityError {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 match self {
131 RequestIdentityError::NotPending { step, instance } => {
132 write!(f, "no pending request {step}{instance:?}")
133 }
134 RequestIdentityError::UnknownTarget { step, target } => {
135 write!(f, "step {step} binds no target {target}")
136 }
137 RequestIdentityError::ProjectionNotUtf8 { step } => {
138 write!(f, "the projection of {step} is not UTF-8")
139 }
140 }
141 }
142}
143
144impl std::error::Error for RequestIdentityError {}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct NotReady {
149 pub pending: usize,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct SemanticRequest {
155 pub step: String,
156 pub instance: Vec<String>,
157 pub backend_id: String,
158 pub projection: Vec<u8>,
161}
162
163#[derive(Debug, Clone, PartialEq)]
165pub enum Supplied {
166 Output(RawOutput),
167 Failed(Unresolved),
168}
169
170#[derive(Debug, Clone)]
171enum SemState {
172 Unresolved(Unresolved),
173 Instances {
174 values: SemInstances,
175 expected: BTreeSet<Vec<String>>,
176 },
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub struct FailureContext {
185 pub origin: rustev_contract::actionability::Origin,
186 pub determinism: rustev_contract::definition::Determinism,
187}
188
189pub struct Evaluation<'c> {
191 constructed: BTreeSet<String>,
192 skipped: BTreeMap<String, String>,
193 acquisitions: BTreeMap<String, acquisition_eval::State>,
194 widenings: BTreeMap<String, acquisition_eval::widening::State>,
195 compiled: &'c Compiled,
196 now: Timestamp,
197 snapshot_id: SnapshotId,
198 inputs_status: BTreeMap<String, Result<(), Unresolved>>,
199 input_values: BTreeMap<String, Value>,
200 input_evidence: BTreeMap<String, InputStatus>,
201 present: BTreeSet<String>,
202 exact: BTreeMap<String, Result<Value, Unresolved>>,
203 exact_values: BTreeMap<String, Value>,
204 semantic: BTreeMap<String, SemState>,
205 derivation: BTreeMap<String, Derivation>,
206 lineage: BTreeMap<String, Lineage>,
207 pending: BTreeMap<(String, Vec<String>), SemanticRequest>,
208 issued: u64,
209 screening: BTreeMap<(String, Vec<String>), ScreeningEvidence>,
210 screen_refusals: Vec<rustev_contract::actionability::Refusal>,
211 screen_inputs: BTreeMap<String, Value>,
212 notices: Vec<Notice>,
213 metas: BTreeMap<String, Meta>,
216 input_quality: BTreeMap<String, InputQuality>,
217 step_quality: BTreeMap<String, StepQuality>,
218 policy_failures: BTreeMap<String, Unresolved>,
221 checks: crate::actionable::CheckRun,
223 observed: BTreeMap<String, Vec<ProvenanceClass>>,
226 failures: BTreeMap<(String, Vec<String>), FailureContext>,
228}
229
230#[derive(Debug, Clone, Default)]
232struct StepQuality {
233 join: Option<Vec<JoinEvidence>>,
234 merge: Option<Vec<MergeEvidence>>,
235 corroboration: Option<Corroboration>,
236}
237
238#[derive(Clone)]
241struct ModelNode {
242 value: Json,
243 children: BTreeMap<String, ModelNode>,
244 screen: Option<ScreeningEvidence>,
245}
246impl ModelNode {
247 fn plain(value: Json) -> Self {
248 let children = match &value {
249 Json::Object(m) => m
250 .iter()
251 .map(|(k, v)| (k.clone(), Self::plain(v.clone())))
252 .collect(),
253 Json::Array(a) => a
254 .iter()
255 .enumerate()
256 .map(|(i, v)| (i.to_string(), Self::plain(v.clone())))
257 .collect(),
258 _ => BTreeMap::new(),
259 };
260 Self {
261 value,
262 children,
263 screen: None,
264 }
265 }
266 fn render(&self, unavailable: &mut BTreeSet<String>) -> Json {
267 if let Some(e) = &self.screen {
268 match e.consequence {
269 ScreenConsequence::DropField | ScreenConsequence::DropText => {
270 return rustev_contract::screening::withheld_screen_blocked();
271 }
272 ScreenConsequence::FailClosed | ScreenConsequence::SkipGeneration => {
273 unavailable.insert(e.field.clone());
274 }
275 _ => {}
276 }
277 }
278 match &self.value {
279 Json::Object(_) => Json::Object(
280 self.children
281 .iter()
282 .map(|(k, v)| (k.clone(), v.render(unavailable)))
283 .collect(),
284 ),
285 Json::Array(a) => Json::Array(
286 (0..a.len())
287 .map(|i| {
288 self.children
292 .get(&i.to_string())
293 .map(|child| child.render(unavailable))
294 .unwrap_or(Json::Null)
295 })
296 .collect(),
297 ),
298 other => other.clone(),
299 }
300 }
301 fn field(&self, path: &str) -> Option<&Self> {
302 path.split('/').try_fold(self, |v, f| v.children.get(f))
303 }
304 fn elements(&self) -> Vec<&Self> {
305 match &self.value {
306 Json::Array(a) => (0..a.len())
307 .filter_map(|i| self.children.get(&i.to_string()))
308 .collect(),
309 _ => self.children.values().collect(),
310 }
311 }
312 fn matching(&self, value: &Json) -> Option<&Self> {
313 (&self.value == value).then_some(self)
314 }
315 fn copied(value: Json, sources: &[Self]) -> Self {
316 if let Some(source) = sources.iter().find_map(|s| s.matching(&value)) {
317 return source.clone();
318 }
319 let mut result = Self::plain(value);
320 for child in result.children.values_mut() {
321 *child = Self::copied(child.value.clone(), sources);
322 }
323 result
324 }
325}
326
327fn validate_input(
328 name: &str,
329 decl: &rustev_contract::definition::InputDecl,
330 ty: &crate::value::Ty,
331 entries: &[&Entry],
332 now: Timestamp,
333) -> Result<(Value, &'static str, InputStatus), Unresolved> {
334 let accepted: Vec<&&Entry> = entries
335 .iter()
336 .filter(|e| decl.provenance.contains(&e.provenance))
337 .collect();
338 if let Some(first) = accepted.first() {
340 let a = canonical_value_bytes(&first.value).ok();
341 if accepted
342 .iter()
343 .any(|e| canonical_value_bytes(&e.value).ok() != a)
344 {
345 return Err(Unresolved::conflict([name.to_string()]));
346 }
347 }
348 if accepted.iter().any(|e| e.as_of_ms > now) {
350 return Err(Unresolved::invalid(
351 [name.to_string()],
352 "as_of is after the evaluation time",
353 ));
354 }
355 let Some(newest) = accepted.iter().max_by_key(|e| e.as_of_ms) else {
357 return Err(Unresolved::missing([name.to_string()]));
358 };
359 let value =
361 decode(ty, &newest.value).map_err(|e| Unresolved::invalid([name.to_string()], e))?;
362 if let Freshness::MaxAgeMs(max) = decl.freshness {
364 let age = now.since(newest.as_of_ms).map(|d| d.0).unwrap_or(0);
365 if age > max {
366 return Err(Unresolved::stale([name.to_string()]));
367 }
368 }
369 let class = if accepted
371 .iter()
372 .any(|e| e.provenance == ProvenanceClass::ModelDerived)
373 {
374 "model"
375 } else {
376 "exact"
377 };
378 Ok((
379 value,
380 class,
381 InputStatus::Resolved {
382 provenance: newest.provenance,
383 as_of_ms: newest.as_of_ms,
384 source: newest.source.clone(),
385 },
386 ))
387}
388
389fn remediation_value(v: &OutValue) -> Option<rustev_contract::actionability::RemediationValue> {
391 use rustev_contract::actionability::RemediationValue as R;
392 Some(match v {
393 OutValue::Bool(b) => R::Bool(*b),
394 OutValue::Integer(i) => R::Integer(*i),
395 OutValue::Decimal(d) => R::Decimal(*d),
396 OutValue::Text(t) => R::Text(t.clone()),
397 OutValue::Enum(e) => R::Enum(e.clone()),
398 OutValue::Timestamp(t) => R::Timestamp(*t),
399 OutValue::Duration(d) => R::Duration(*d),
400 OutValue::List(l) => R::List(l.iter().map(remediation_value).collect::<Option<_>>()?),
401 _ => return None,
402 })
403}
404
405fn lower(m: &mut ValueMeta, to: Grade) {
408 m.grade = m.grade.combine(to.into());
409 for f in m.fields.iter_mut().flat_map(|f| f.values_mut()) {
410 lower(f, to);
411 }
412 for i in m.items.iter_mut().flatten() {
413 lower(i, to);
414 }
415}
416
417fn merge(reasons: Vec<Unresolved>) -> Option<Unresolved> {
419 let first = reasons.first()?.clone();
420 Some(match &first {
421 Unresolved::MissingEvidence { .. } => {
422 Unresolved::missing(reasons.iter().flat_map(|r| match r {
423 Unresolved::MissingEvidence { fields } => fields.clone(),
424 _ => vec![],
425 }))
426 }
427 Unresolved::StaleEvidence { .. } => {
428 Unresolved::stale(reasons.iter().flat_map(|r| match r {
429 Unresolved::StaleEvidence { fields } => fields.clone(),
430 _ => vec![],
431 }))
432 }
433 _ => first,
434 })
435}
436
437fn combine(classes: impl IntoIterator<Item = Derivation>) -> Derivation {
438 if classes.into_iter().all(|c| c == Derivation::ExactDerived) {
439 Derivation::ExactDerived
440 } else {
441 Derivation::MixedDerived
442 }
443}
444
445impl<'c> Evaluation<'c> {
446 pub fn start(
449 compiled: &'c Compiled,
450 snapshot: &Snapshot,
451 now: Timestamp,
452 ) -> Result<Self, StartError> {
453 let v2 = compiled.info.v2;
456 let accepted =
457 snapshot.schema == schema::SNAPSHOT || (v2 && snapshot.schema == schema::SNAPSHOT_V2);
458 if !accepted {
459 return Err(StartError::Schema(
460 if snapshot.schema == schema::SNAPSHOT_V2 {
461 format!(
462 "snapshot_version: {} is not accepted by {}",
463 schema::SNAPSHOT_V2,
464 schema::PLAN
465 )
466 } else {
467 snapshot.schema.clone()
468 },
469 ));
470 }
471 if snapshot.schema == schema::SNAPSHOT && snapshot.entries.iter().any(|e| e.grade.is_some())
472 {
473 return Err(StartError::Schema(format!(
474 "snapshot_version: a {} entry claims a grade",
475 schema::SNAPSHOT
476 )));
477 }
478 if snapshot.schema == schema::SNAPSHOT
479 && snapshot.entries.iter().any(|e| e.producer.is_some())
480 {
481 return Err(StartError::Schema(
482 "snapshot_version: producer requires rustev.snapshot/2".into(),
483 ));
484 }
485 let info = &compiled.info;
486 for e in &snapshot.entries {
487 if !info.inputs.contains_key(&e.field) {
488 return Err(StartError::UndeclaredField(e.field.clone()));
489 }
490 if e.producer.is_some() && e.provenance != ProvenanceClass::ModelDerived {
491 return Err(StartError::InvalidProducer(e.field.clone()));
492 }
493 }
494 let snapshot_id = snapshot
495 .id()
496 .map_err(|e| StartError::NotCanonical(e.to_string()))?;
497 let mut ev = Evaluation {
498 constructed: BTreeSet::new(),
499 skipped: BTreeMap::new(),
500 acquisitions: BTreeMap::new(),
501 widenings: BTreeMap::new(),
502 compiled,
503 now,
504 snapshot_id,
505 inputs_status: BTreeMap::new(),
506 input_values: BTreeMap::new(),
507 input_evidence: BTreeMap::new(),
508 present: BTreeSet::new(),
509 exact: BTreeMap::new(),
510 exact_values: BTreeMap::new(),
511 semantic: BTreeMap::new(),
512 derivation: BTreeMap::new(),
513 lineage: BTreeMap::new(),
514 pending: BTreeMap::new(),
515 issued: 0,
516 screening: BTreeMap::new(),
517 screen_refusals: Vec::new(),
518 screen_inputs: BTreeMap::new(),
519 notices: compiled.plan.notices.clone(),
520 metas: BTreeMap::new(),
521 input_quality: BTreeMap::new(),
522 step_quality: BTreeMap::new(),
523 policy_failures: BTreeMap::new(),
524 checks: Default::default(),
525 observed: BTreeMap::new(),
526 failures: BTreeMap::new(),
527 };
528 for (name, (ty, decl)) in &info.inputs {
529 let entries: Vec<&Entry> = snapshot
530 .entries
531 .iter()
532 .filter(|e| &e.field == name)
533 .collect();
534 let key = format!("input:{name}");
535 let mut accepted: Vec<ProvenanceClass> = entries
536 .iter()
537 .filter(|e| decl.provenance.contains(&e.provenance))
538 .map(|e| e.provenance)
539 .collect();
540 accepted.sort();
541 accepted.dedup();
542 ev.observed.insert(name.clone(), accepted);
543 if v2 {
547 ev.start_input_v2(name, decl, &entries);
548 if let Some(InputStatus::Resolved { provenance, .. }) = ev.input_evidence.get(name)
549 {
550 ev.observed.insert(name.clone(), vec![*provenance]);
551 }
552 continue;
553 }
554 match validate_input(name, decl, ty, &entries, now) {
555 Ok((v, class, evidence)) => {
556 ev.inputs_status.insert(name.clone(), Ok(()));
557 ev.input_values.insert(name.clone(), v);
558 ev.input_evidence.insert(name.clone(), evidence);
559 ev.present.insert(name.clone());
560 ev.derivation.insert(
561 key.clone(),
562 if class == "model" {
563 Derivation::ModelDerived
564 } else {
565 Derivation::ExactDerived
566 },
567 );
568 }
569 Err(u) => {
570 ev.inputs_status.insert(name.clone(), Err(u.clone()));
571 ev.input_evidence
572 .insert(name.clone(), InputStatus::Unresolved(u));
573 ev.derivation.insert(key.clone(), Derivation::ExactDerived);
574 }
575 }
576 ev.lineage.insert(
577 key,
578 Lineage {
579 inputs: vec![name.clone()],
580 steps: vec![],
581 },
582 );
583 }
584 if compiled.plan.actionability.is_some() {
588 ev.run_checks();
589 }
590 ev.screen_inputs = ev.input_values.clone();
591 if ev.checks.terminal.is_none() {
592 ev.advance();
593 }
594 Ok(ev)
595 }
596
597 fn run_checks(&mut self) {
599 use crate::actionable::{PendingNotice, PendingRefusal, Terminal};
600 use rustev_contract::actionability::{
601 Address, CheckRecord, CheckStatus, Consequence, DefaultConsequence,
602 };
603 let Some(a) = self.compiled.plan.definition.actionability.as_ref() else {
604 return;
605 };
606 let mut relaxed: BTreeMap<String, String> = BTreeMap::new();
607 for c in &a.checks {
608 let record = |status| CheckRecord {
609 check: c.id.clone(),
610 subject: c.subject.clone(),
611 status,
612 };
613 if let Some(by) = relaxed.get(&c.id) {
614 self.checks
615 .records
616 .push(record(CheckStatus::Relaxed { by: by.clone() }));
617 continue;
618 }
619 if let Err(u) = self.status(&format!("input:{}", c.subject)) {
620 self.checks
621 .records
622 .push(record(CheckStatus::NotEvaluated { reason: u }));
623 continue;
624 }
625 let mut refs = Vec::new();
626 crate::expr::predicate_refs(&c.test, &mut refs);
627 let mut reads: Vec<String> = refs
631 .iter()
632 .filter(|r| r.starts_with("input:"))
633 .map(|r| crate::expr::base_ref(r))
634 .collect();
635 reads.sort();
636 reads.dedup();
637 let unresolved = reads.iter().find_map(|r| self.status(r).err());
638 let tested = match unresolved {
639 Some(u) => Err(u),
640 None => crate::expr::eval_predicate(&c.test, &View::scope(self))
641 .map_err(|f| Unresolved::invalid([c.subject.clone()], format!("{f:?}"))),
642 };
643 let passed = match tested {
644 Err(u) => {
645 let merged = merge(vec![u.clone()]).unwrap_or(u.clone());
648 self.inputs_status
649 .insert(c.subject.clone(), Err(merged.clone()));
650 self.present.remove(&c.subject);
651 self.checks
652 .records
653 .push(record(CheckStatus::Unevaluable { reason: merged }));
654 continue;
655 }
656 Ok(p) => p,
657 };
658 if passed {
659 self.checks.records.push(record(CheckStatus::Passed));
660 continue;
661 }
662 let class = match self.input_evidence.get(&c.subject) {
665 Some(InputStatus::Resolved { provenance, .. }) => *provenance,
666 _ => {
667 self.checks.records.push(record(CheckStatus::NotEvaluated {
668 reason: Unresolved::missing([c.subject.clone()]),
669 }));
670 continue;
671 }
672 };
673 let consequence = match c.on_fail.by_provenance.get(&class) {
674 Some(k) => k.clone(),
675 None => match &c.on_fail.default {
676 DefaultConsequence::Consequence(k) => k.clone(),
677 DefaultConsequence::None => continue,
680 },
681 };
682 self.checks.records.push(record(CheckStatus::Failed {
683 class,
684 consequence: consequence.kind().into(),
685 }));
686 let causes: Vec<rustev_contract::actionability::Cause> = reads
687 .iter()
688 .filter_map(|r| r.strip_prefix("input:"))
689 .map(|f| self.input_cause(f))
690 .collect();
691 let notice = |kind, subject: String, reason: &str, address: &Address| PendingNotice {
692 kind,
693 subject,
694 reason: reason.into(),
695 causes: causes.clone(),
696 address: address.clone(),
697 };
698 match &consequence {
699 Consequence::DropField { notice: n, address } => {
700 self.inputs_status.insert(
702 c.subject.clone(),
703 Err(Unresolved::missing([c.subject.clone()])),
704 );
705 self.input_values.remove(&c.subject);
706 self.present.remove(&c.subject);
707 self.metas.remove(&format!("input:{}", c.subject));
708 self.checks.dropped.insert(c.subject.clone(), c.id.clone());
709 self.checks.notices.push(notice(
710 NoticeKind::FieldDropped,
711 format!("input:{}", c.subject),
712 n,
713 address,
714 ));
715 }
716 Consequence::Relax {
717 group,
718 notice: n,
719 address,
720 } => {
721 if let Some(g) = a.constraint_groups.iter().find(|g| &g.name == group) {
722 for id in &g.checks {
723 relaxed.entry(id.clone()).or_insert_with(|| c.id.clone());
724 }
725 }
726 self.checks.notices.push(notice(
727 NoticeKind::ConstraintsRelaxed,
728 format!("group:{group}"),
729 n,
730 address,
731 ));
732 }
733 Consequence::DowngradeGrade {
734 to,
735 notice: n,
736 address,
737 } => {
738 if let Some(m) = self.metas.get_mut(&format!("input:{}", c.subject)) {
739 lower(&mut m.whole, *to);
740 m.node = m.node.combine((*to).into());
741 }
742 self.checks.notices.push(notice(
743 NoticeKind::GradeDowngraded,
744 format!("input:{}", c.subject),
745 n,
746 address,
747 ));
748 }
749 Consequence::ContinueWithNotice { notice: n, address } => {
750 self.checks.notices.push(notice(
751 NoticeKind::CheckNotice,
752 format!("check:{}", c.id),
753 n,
754 address,
755 ));
756 }
757 Consequence::Refuse {
758 remediation,
759 address,
760 } => {
761 self.checks.refusals.push(PendingRefusal {
762 check: c.id.clone(),
763 subject: c.subject.clone(),
764 causes: causes.clone(),
765 remediation: remediation.clone(),
766 address: address.clone(),
767 });
768 if self.checks.terminal.is_none() {
769 self.checks.terminal = Some(Terminal::Refused);
770 }
771 if matches!(self.checks.terminal, Some(Terminal::Refused)) {
772 self.checks.deciding_reads.extend(reads.iter().cloned());
773 }
774 }
775 Consequence::Abstain => {
776 if self.checks.terminal.is_none() {
777 self.checks.terminal = Some(Terminal::Abstain {
778 check: c.id.clone(),
779 });
780 self.checks.deciding_reads = reads.clone();
781 }
782 }
783 Consequence::Escalate { reason } => {
784 if self.checks.terminal.is_none() {
785 self.checks.terminal = Some(Terminal::Escalate {
786 check: c.id.clone(),
787 reason: reason.clone(),
788 });
789 self.checks.deciding_reads = reads.clone();
790 }
791 }
792 }
793 }
794 self.checks.deciding_reads.sort();
795 self.checks.deciding_reads.dedup();
796 }
797
798 fn input_cause(&self, field: &str) -> rustev_contract::actionability::Cause {
801 use rustev_contract::actionability::{Cause, CauseProvenance};
802 let provenance = match self.observed.get(field).map(Vec::as_slice) {
808 Some([one]) => CauseProvenance::Observed { class: *one },
809 _ => CauseProvenance::Accepted {
810 classes: self
811 .compiled
812 .info
813 .inputs
814 .get(field)
815 .map(|(_, d)| {
816 let mut v = d.provenance.clone();
817 v.sort();
818 v
819 })
820 .unwrap_or_default(),
821 },
822 };
823 Cause::Input {
824 field: field.into(),
825 provenance,
826 }
827 }
828
829 fn reason_causes(
831 &self,
832 u: &Unresolved,
833 site: &str,
834 ) -> Vec<rustev_contract::actionability::Cause> {
835 use rustev_contract::actionability::{Cause, CauseProvenance, Origin};
836 match u {
837 Unresolved::Conflict { .. } => crate::actionable::reason_fields(u)
838 .into_iter()
839 .flat_map(|f| {
840 let classes = self.observed.get(&f).cloned().unwrap_or_default();
841 if classes.is_empty() {
842 vec![self.input_cause(&f)]
843 } else {
844 classes
845 .into_iter()
846 .map(|class| Cause::Input {
847 field: f.clone(),
848 provenance: CauseProvenance::Observed { class },
849 })
850 .collect()
851 }
852 })
853 .collect(),
854 Unresolved::MissingEvidence { .. }
855 | Unresolved::StaleEvidence { .. }
856 | Unresolved::InvalidInput { .. }
857 | Unresolved::InsufficientGrade { .. } => crate::actionable::reason_fields(u)
858 .into_iter()
859 .map(|f| self.input_cause(&f))
860 .collect(),
861 Unresolved::ScreenUnavailable { fields } => self
862 .screening
863 .iter()
864 .filter(|(_, e)| {
865 fields.contains(&e.field)
866 && matches!(e.verdict, ScreenVerdict::Unavailable { .. })
867 })
868 .map(|(key, e)| Cause::Backend {
869 step: e.step.clone(),
870 origin: self
871 .failures
872 .get(key)
873 .map_or(Origin::Upstream, |c| c.origin),
874 })
875 .collect(),
876 Unresolved::Abstained { rule } => vec![Cause::Policy {
877 site: if site.starts_with("check:") {
878 site.into()
879 } else {
880 format!("rule:{rule}")
881 },
882 }],
883 Unresolved::BackendUnavailable { .. } | Unresolved::InvalidBackendOutput { .. } => {
884 vec![Cause::Backend {
885 step: self.failed_step(u).unwrap_or_default(),
886 origin: self
889 .failure_context(u)
890 .map(|c| c.origin)
891 .unwrap_or(Origin::Upstream),
892 }]
893 }
894 Unresolved::BudgetExhausted { resource } => vec![Cause::Budget {
895 resource: resource.clone(),
896 }],
897 Unresolved::DeadlineExceeded => vec![Cause::Deadline],
898 Unresolved::Unsupported { capability } => vec![Cause::Capability {
899 name: capability.clone(),
900 }],
901 }
902 }
903
904 fn failed_request(&self, u: &Unresolved) -> Option<(String, Vec<String>)> {
907 self.compiled
908 .info
909 .step_ids
910 .iter()
911 .find_map(|id| match self.semantic.get(id) {
912 Some(SemState::Unresolved(x)) if x.kind() == u.kind() => Some((id.clone(), vec![])),
913 Some(SemState::Instances { values, .. }) => values
914 .iter()
915 .find(|(_, v)| matches!(v, Err(x) if x.kind() == u.kind()))
916 .map(|(k, _)| (id.clone(), k.clone())),
917 _ => None,
918 })
919 }
920
921 fn failed_step(&self, u: &Unresolved) -> Option<String> {
923 self.failed_request(u).map(|(s, _)| s)
924 }
925
926 fn failure_context(&self, u: &Unresolved) -> Option<FailureContext> {
928 self.failed_request(u)
929 .and_then(|k| self.failures.get(&k).copied())
930 }
931
932 fn reason_action(
935 &self,
936 u: &Unresolved,
937 site: &str,
938 declared: Option<(
939 &Option<rustev_contract::actionability::RemediationUse>,
940 &rustev_contract::actionability::Address,
941 )>,
942 ) -> rustev_contract::actionability::Action {
943 use rustev_contract::actionability::Address;
944 let step = self.failed_step(u);
945 let determinism = self.failure_context(u).map(|c| c.determinism).or_else(|| {
948 step.as_ref()
949 .and_then(|s| self.compiled.info.determinism.get(s))
950 .copied()
951 });
952 let transient = crate::actionable::transient(u, determinism);
953 let cc = self.caller_controlled();
954 let computed = Address::Computed;
955 let (address, remediation) = match declared {
956 Some((r, a)) => (a, r.as_ref().map(|r| self.resolve_remediation(r))),
957 None => (&computed, None),
958 };
959 crate::actionable::action(
960 self.reason_causes(u, site),
961 crate::actionable::ActionInputs {
962 caller_controlled: &cc,
963 address,
964 declared: remediation,
965 computed: crate::actionable::computed_remediation(u, transient, step.as_deref()),
966 transient,
967 },
968 )
969 }
970
971 fn caller_controlled(&self) -> Vec<ProvenanceClass> {
972 self.compiled
973 .plan
974 .definition
975 .actionability
976 .as_ref()
977 .map(|a| a.caller_controlled.clone())
978 .unwrap_or_default()
979 }
980
981 fn resolve_remediation(
985 &self,
986 r: &rustev_contract::actionability::RemediationUse,
987 ) -> (rustev_contract::actionability::Remediation, Option<Party>) {
988 use rustev_contract::actionability::{Remediation, RemediationUse};
989 let declared = self
990 .compiled
991 .plan
992 .definition
993 .actionability
994 .as_ref()
995 .and_then(|a| a.remediations.iter().find(|d| d.code == r.code()));
996 let party = declared.map(|d| d.for_party);
997 let params = match r {
998 RemediationUse::Code(_) => BTreeMap::new(),
999 RemediationUse::WithParams(c) => c.params.clone(),
1000 };
1001 let mut values = BTreeMap::new();
1002 for (name, p) in ¶ms {
1003 let v = match p {
1004 rustev_contract::definition::ParamSource::Lit(l) => {
1005 Some(Value::from_literal(l).to_out())
1006 }
1007 rustev_contract::definition::ParamSource::Ref(x) => {
1008 crate::expr::eval_ref(x, &View::scope(self))
1009 .ok()
1010 .map(|v| v.to_out())
1011 }
1012 };
1013 match v.and_then(|v| remediation_value(&v)) {
1014 Some(v) => {
1015 values.insert(name.clone(), v);
1016 }
1017 None => return (Remediation::None, party),
1018 }
1019 }
1020 (
1021 Remediation::Some {
1022 code: r.code().into(),
1023 params: values,
1024 },
1025 party,
1026 )
1027 }
1028
1029 pub fn now(&self) -> Timestamp {
1031 self.now
1032 }
1033
1034 pub fn screening_refused(&self) -> bool {
1037 !self.screen_refusals.is_empty()
1038 }
1039
1040 pub fn pending(&self) -> Vec<SemanticRequest> {
1042 let rank: BTreeMap<&str, usize> = self
1043 .compiled
1044 .info
1045 .order
1046 .iter()
1047 .enumerate()
1048 .map(|(i, s)| (s.as_str(), i))
1049 .collect();
1050 let mut v: Vec<_> = self.pending.values().cloned().collect();
1051 v.sort_by(|a, b| {
1052 rank[a.step.as_str()]
1053 .cmp(&rank[b.step.as_str()])
1054 .then_with(|| a.instance.cmp(&b.instance))
1055 });
1056 v
1057 }
1058
1059 pub fn request_document(
1064 &self,
1065 step: &str,
1066 instance: &[String],
1067 target: u32,
1068 scope: &ReplayScope,
1069 ) -> Result<RequestDoc, RequestIdentityError> {
1070 let not_pending = || RequestIdentityError::NotPending {
1071 step: step.into(),
1072 instance: instance.to_vec(),
1073 };
1074 let req = self
1075 .pending
1076 .get(&(step.to_string(), instance.to_vec()))
1077 .ok_or_else(not_pending)?;
1078 let plan = &self.compiled.plan;
1079 let Some(PlanStepDetail::Semantic(b)) =
1080 plan.steps.iter().find(|s| s.id == step).map(|s| &s.detail)
1081 else {
1082 return Err(not_pending());
1084 };
1085 let unknown = || RequestIdentityError::UnknownTarget {
1086 step: step.into(),
1087 target,
1088 };
1089 let (backend_id, artifact, descriptor) = if target == 0 {
1090 (&b.backend_id, &b.artifact, &b.descriptor)
1091 } else {
1092 let PlanExecution::Declared(d) = &plan.execution else {
1093 return Err(unknown());
1094 };
1095 let f = d
1096 .fallbacks
1097 .iter()
1098 .find(|f| f.step == step && f.target == target)
1099 .ok_or_else(unknown)?;
1100 (&f.backend_id, &f.artifact, &f.descriptor)
1101 };
1102 Ok(RequestDoc {
1103 schema: schema::REQUEST.into(),
1104 scope: scope.clone(),
1105 backend_id: backend_id.clone(),
1106 artifact: artifact.clone(),
1107 descriptor: descriptor.clone(),
1108 projection: String::from_utf8(req.projection.clone())
1110 .map_err(|_| RequestIdentityError::ProjectionNotUtf8 { step: step.into() })?,
1111 output: b.output.clone(),
1112 requires: b.requires,
1113 normalization: b.normalization,
1114 })
1115 }
1116
1117 pub fn supply_doc(&mut self, doc: &BackendOutputDoc) -> Result<(), SupplyError> {
1119 let bound = match self
1120 .compiled
1121 .plan
1122 .steps
1123 .iter()
1124 .find(|s| s.id == doc.step)
1125 .map(|s| &s.detail)
1126 {
1127 Some(PlanStepDetail::Semantic(b)) => Some(b.artifact.clone()),
1128 _ => None,
1129 };
1130 if bound.as_ref() != Some(&doc.artifact) {
1131 self.take(&doc.step, &doc.instance)?;
1132 return self.record(
1133 &doc.step,
1134 doc.instance.clone(),
1135 Err(Unresolved::InvalidBackendOutput {
1136 detail: format!("artifact {} is not the bound artifact", doc.artifact),
1137 }),
1138 );
1139 }
1140 self.supply(
1141 &doc.step,
1142 &doc.instance,
1143 Supplied::Output(doc.output.clone()),
1144 )
1145 }
1146
1147 pub fn note_failure(&mut self, step: &str, instance: &[String], context: FailureContext) {
1151 self.failures
1152 .insert((step.to_string(), instance.to_vec()), context);
1153 }
1154
1155 pub fn supply(
1157 &mut self,
1158 step: &str,
1159 instance: &[String],
1160 s: Supplied,
1161 ) -> Result<(), SupplyError> {
1162 if let Supplied::Failed(u) = &s {
1163 let k = u.kind();
1164 if !matches!(
1165 k,
1166 ReasonKind::BackendUnavailable
1167 | ReasonKind::InvalidBackendOutput
1168 | ReasonKind::BudgetExhausted
1169 | ReasonKind::DeadlineExceeded
1170 ) {
1171 return Err(SupplyError::NotARuntimeReason(k));
1172 }
1173 }
1174 let req = self.take(step, instance)?;
1175 let value = match s {
1176 Supplied::Failed(u) => Err(u),
1177 Supplied::Output(raw) => self.validate_output(step, &req, &raw),
1178 };
1179 self.record(step, instance.to_vec(), value)
1180 }
1181
1182 pub fn check_output(
1187 &self,
1188 step: &str,
1189 instance: &[String],
1190 raw: &RawOutput,
1191 ) -> Result<Result<(), Unresolved>, SupplyError> {
1192 let req = self
1193 .pending
1194 .get(&(step.to_string(), instance.to_vec()))
1195 .ok_or_else(|| SupplyError::NotPending {
1196 step: step.into(),
1197 instance: instance.to_vec(),
1198 })?;
1199 Ok(self.validate_output(step, req, raw).map(|_| ()))
1200 }
1201
1202 fn take(&mut self, step: &str, instance: &[String]) -> Result<SemanticRequest, SupplyError> {
1203 self.pending
1204 .remove(&(step.to_string(), instance.to_vec()))
1205 .ok_or_else(|| SupplyError::NotPending {
1206 step: step.into(),
1207 instance: instance.to_vec(),
1208 })
1209 }
1210
1211 fn record(
1212 &mut self,
1213 step: &str,
1214 instance: Vec<String>,
1215 value: Result<SemValue, Unresolved>,
1216 ) -> Result<(), SupplyError> {
1217 self.record_screen(step, &instance, &value);
1218 if let Some(SemState::Instances { values, .. }) = self.semantic.get_mut(step) {
1219 values.insert(instance, value);
1220 }
1221 if self.screen_refusals.is_empty() {
1222 self.advance();
1223 } else {
1224 self.pending.clear();
1225 }
1226 Ok(())
1227 }
1228
1229 fn validate_output(
1230 &self,
1231 step: &str,
1232 req: &SemanticRequest,
1233 raw: &RawOutput,
1234 ) -> Result<SemValue, Unresolved> {
1235 let bad = |d: String| Unresolved::InvalidBackendOutput { detail: d };
1236 let Some(StepInfo::Semantic {
1237 decl,
1238 binding: Some(b),
1239 calibration,
1240 kind,
1241 ..
1242 }) = self.compiled.info.steps.get(step)
1243 else {
1244 return Err(bad(format!("{step} is not a bound semantic step")));
1245 };
1246 let tol = self.compiled.plan.definition.limits.distribution_tolerance;
1247 let offered = match raw {
1248 RawOutput::TypedRecord { .. } => {
1249 return Err(bad("typed_record is not a decision value".into()));
1250 }
1251 RawOutput::TextRecord(_) => {
1252 return Err(bad("text_record is not a decision value".into()));
1253 }
1254 RawOutput::Screen { .. } => OutputKind::Screen,
1255 RawOutput::Label(_) => OutputKind::Label,
1256 RawOutput::Distribution(_) => OutputKind::Distribution,
1257 RawOutput::Logits(_) => OutputKind::Logits,
1258 RawOutput::Scores(_) => OutputKind::Scores,
1259 };
1260 if offered != b.output {
1261 return Err(bad(format!(
1262 "{offered:?} where the binding declares {:?}",
1263 b.output
1264 )));
1265 }
1266 let e = |k: crate::kinds::KindError| bad(k.0);
1267 match (b.requires, raw) {
1268 (
1269 RequiredKind::ScreenVerdict,
1270 RawOutput::Screen {
1271 blocked,
1272 categories,
1273 ..
1274 },
1275 ) => {
1276 let Some(declared) = self.compiled.info.screen_categories.get(&b.backend_id) else {
1279 return Err(bad("screening backend has no screen profile".into()));
1280 };
1281 let unique: BTreeSet<_> = categories.iter().collect();
1282 if unique.len() != categories.len()
1283 || categories.iter().any(|c| !declared.contains(c))
1284 {
1285 return Err(bad("invalid screening categories".into()));
1286 }
1287 let mut categories = categories.clone();
1288 categories.sort();
1289 Ok(SemValue::Screen {
1290 blocked: *blocked,
1291 categories,
1292 })
1293 }
1294 (RequiredKind::Label, RawOutput::Label(l)) => {
1295 let label = SelectedLabel::new(&decl.options, l).map_err(e)?;
1296 Ok(if decl.operation == Operation::Rubric {
1297 SemValue::Ordinal(OrdinalLevel::from_label(label))
1298 } else {
1299 SemValue::Label(label)
1300 })
1301 }
1302 (
1303 RequiredKind::Distribution | RequiredKind::OrdinalDistribution,
1304 RawOutput::Distribution(m) | RawOutput::Logits(m),
1305 ) => {
1306 let d = if offered == OutputKind::Logits {
1307 Distribution::from_logits(&decl.options, m, tol)
1308 } else {
1309 Distribution::new(&decl.options, m, tol)
1310 }
1311 .map_err(e)?;
1312 Ok(if *kind == SemKind::Ordinal(true) {
1313 SemValue::Ordinal(OrdinalLevel::from_distribution(d))
1314 } else {
1315 SemValue::Distribution(d)
1316 })
1317 }
1318 (
1319 RequiredKind::CalibratedProbability,
1320 RawOutput::Distribution(m) | RawOutput::Logits(m),
1321 ) => {
1322 let Some((art, cid)) = calibration else {
1323 return Err(bad("no calibration is bound".into()));
1324 };
1325 let check = BindingCheck {
1326 artifact: &b.artifact,
1327 task: &decl.task,
1328 question: step,
1329 options: &decl.options,
1330 };
1331 let input = if offered == OutputKind::Logits {
1332 CalibrationInput::Logits(m)
1333 } else {
1334 CalibrationInput::Distribution(m)
1335 };
1336 calibrate::apply(art, cid, &check, input, tol)
1337 .map(SemValue::Calibrated)
1338 .map_err(|x| bad(x.to_string()))
1339 }
1340 (RequiredKind::Scores, RawOutput::Scores(m) | RawOutput::Logits(m)) => {
1341 let candidates = self.rank_candidates(decl);
1342 let keys: BTreeSet<&String> = m.keys().collect();
1343 let want: BTreeSet<&String> = candidates.iter().collect();
1344 if keys != want {
1345 return Err(bad("scores are not keyed exactly by the candidates".into()));
1346 }
1347 let scope = ScoreScope {
1348 plan: self.compiled.id.clone(),
1349 step: step.to_string(),
1350 instance: req.instance.clone(),
1351 };
1352 candidates
1353 .iter()
1354 .map(|c| ModelScore::new(scope.clone(), c.clone(), m[c]).map_err(e))
1355 .collect::<Result<Vec<_>, _>>()
1356 .map(SemValue::Scores)
1357 }
1358 _ => Err(bad("output does not match the required kind".into())),
1359 }
1360 }
1361
1362 fn is_screen_ref(&self, reference: &str) -> bool {
1363 self.compiled.plan.screening.as_ref().is_some_and(|p| {
1364 p.steps
1365 .iter()
1366 .any(|s| reference == format!("step:{}", s.step))
1367 })
1368 }
1369
1370 fn record_screen(&mut self, step: &str, key: &[String], value: &Result<SemValue, Unresolved>) {
1371 use rustev_contract::actionability::{Refusal, Remediation};
1372 use rustev_contract::screening::{ScreeningNotice, screen_text_digest};
1373 let Some(plan) = &self.compiled.plan.screening else {
1374 return;
1376 };
1377 let Some(s) = plan.steps.iter().find(|s| s.step == step) else {
1378 return;
1380 };
1381 if self.screening.contains_key(&(step.into(), key.to_vec())) {
1382 return;
1384 }
1385 let Some(entry) = plan.screening.entries.iter().find(|e| e.id == s.entry) else {
1386 return;
1389 };
1390 let projection = match self.compiled.info.steps.get(step) {
1391 Some(StepInfo::Semantic { decl, .. }) => {
1392 crate::compile::screening::request_projection(decl, key, &self.screen_inputs)
1393 .ok()
1394 .and_then(|bytes| serde_json::from_slice::<Json>(&bytes).ok())
1395 }
1396 _ => None,
1397 };
1398 let text = projection
1399 .as_ref()
1400 .and_then(|p| p.get("values"))
1401 .and_then(|v| v.get("text"))
1402 .and_then(Json::as_str);
1403 let invalid = Err(Unresolved::InvalidBackendOutput {
1404 detail: "invalid screening projection or verdict".into(),
1405 });
1406 let value = if text.is_none()
1407 || !matches!(
1408 value,
1409 Ok(SemValue::Screen { .. } | SemValue::ScreenEmpty) | Err(_)
1410 ) {
1411 &invalid
1412 } else {
1413 value
1414 };
1415 let (verdict, categories, consequence) = match value {
1416 Ok(SemValue::ScreenEmpty) => (ScreenVerdict::Empty, vec![], ScreenConsequence::None),
1417 Ok(SemValue::Screen {
1418 blocked: false,
1419 categories,
1420 }) => (
1421 ScreenVerdict::Pass,
1422 categories.clone(),
1423 ScreenConsequence::None,
1424 ),
1425 Ok(SemValue::Screen {
1426 blocked: true,
1427 categories,
1428 }) => (
1429 ScreenVerdict::Blocked,
1430 categories.clone(),
1431 match entry.on_block {
1432 OnBlock::Refuse => ScreenConsequence::Refuse,
1433 OnBlock::DropField => ScreenConsequence::DropField,
1434 OnBlock::DropText => ScreenConsequence::DropText,
1435 OnBlock::DropItem => ScreenConsequence::DropItem,
1436 },
1437 ),
1438 Err(reason) => (
1439 ScreenVerdict::Unavailable {
1440 reason: reason.clone(),
1441 },
1442 vec![],
1443 match entry.on_unavailable {
1444 OnUnavailable::FailClosed => ScreenConsequence::FailClosed,
1445 OnUnavailable::SkipGeneration => ScreenConsequence::SkipGeneration,
1446 OnUnavailable::FailOpen => ScreenConsequence::FailOpen,
1447 },
1448 ),
1449 _ => (
1451 ScreenVerdict::Unavailable {
1452 reason: Unresolved::InvalidBackendOutput {
1453 detail: "not a screening verdict".into(),
1454 },
1455 },
1456 vec![],
1457 ScreenConsequence::FailClosed,
1458 ),
1459 };
1460 let name = s
1461 .field
1462 .trim_start_matches("input:")
1463 .split('/')
1464 .next()
1465 .unwrap_or_default()
1466 .trim_end_matches("[]");
1467 if consequence == ScreenConsequence::Refuse {
1468 let subject = key.first().map_or_else(
1469 || s.field.clone(),
1470 |id| s.field.replace("[]", &format!("[{id}]")),
1471 );
1472 let action = crate::actionable::action(
1473 vec![self.input_cause(name)],
1474 crate::actionable::ActionInputs {
1475 caller_controlled: &self.caller_controlled(),
1476 address: &entry.address,
1477 declared: None,
1478 computed: Remediation::with("screen_blocked", []),
1479 transient: false,
1480 },
1481 );
1482 self.screen_refusals.push(Refusal {
1483 check: format!("screen:{}", entry.id),
1484 subject,
1485 action,
1486 });
1487 }
1488 if consequence == ScreenConsequence::DropItem
1492 && let (ScreeningItemId::Field(field), Some(Value::List(items))) =
1493 (&entry.item_id, self.input_values.get_mut(name))
1494 {
1495 items.retain(|v| !matches!(v, Value::Record(m) if matches!(m.get(&field.field), Some(Value::Text(id) | Value::Enum(id)) if key.first() == Some(id))));
1496 }
1497 if consequence != ScreenConsequence::None {
1498 if let Some(n) = self.notices.iter_mut().find(|n| {
1499 n.screening.as_ref().is_some_and(|n| {
1500 n.entry == entry.id && n.field == s.field && n.consequence == consequence
1501 })
1502 }) {
1503 if let Some(notice) = n.screening.as_mut() {
1504 notice.items.extend(key.iter().cloned());
1505 notice.items.sort();
1506 notice.items.dedup();
1507 }
1508 } else {
1509 self.notices.push(Notice {
1510 kind: NoticeKind::Screening,
1511 subject: s.field.clone(),
1512 reason: "screening consequence".into(),
1513 action: None,
1514 screening: Some(ScreeningNotice {
1515 entry: entry.id.clone(),
1516 field: s.field.clone(),
1517 consequence,
1518 items: key.to_vec(),
1519 }),
1520 });
1521 }
1522 }
1523 self.screening.insert(
1524 (step.into(), key.to_vec()),
1525 ScreeningEvidence {
1526 entry: entry.id.clone(),
1527 field: s.field.clone(),
1528 item: key.first().cloned(),
1529 text_digest: screen_text_digest(text.unwrap_or_default()),
1532 verdict,
1533 categories,
1534 consequence,
1535 backend_id: s.binding.backend_id.clone(),
1536 artifact: s.binding.artifact.clone(),
1537 descriptor: s.binding.descriptor.clone(),
1538 step: step.into(),
1539 },
1540 );
1541 }
1542
1543 fn rank_candidates(&self, decl: &rustev_contract::definition::SemanticDecl) -> Vec<String> {
1544 match &decl.candidates {
1545 Candidates::From(r) => r
1546 .strip_prefix("step:")
1547 .and_then(|s| self.exact_values.get(s))
1548 .and_then(Value::ids)
1549 .unwrap_or_default(),
1550 Candidates::None => vec![],
1551 }
1552 }
1553
1554 fn dep_done(&self, r: &str) -> bool {
1555 match r.strip_prefix("step:") {
1556 Some(s) => self.step_done(s),
1557 None => true,
1558 }
1559 }
1560
1561 fn step_done(&self, s: &str) -> bool {
1562 if self.exact.contains_key(s) || self.skipped.contains_key(s) {
1563 return true;
1564 }
1565 match self.semantic.get(s) {
1566 Some(SemState::Unresolved(_)) => true,
1567 Some(SemState::Instances { values, expected }) => {
1568 expected.iter().all(|k| values.contains_key(k))
1569 }
1570 None => false,
1571 }
1572 }
1573
1574 fn status(&self, r: &str) -> Result<(), Unresolved> {
1576 if let Some(n) = r.strip_prefix("input:") {
1577 return self
1578 .inputs_status
1579 .get(n)
1580 .cloned()
1581 .unwrap_or_else(|| Err(Unresolved::missing([n.to_string()])));
1582 }
1583 let s = r.strip_prefix("step:").unwrap_or(r);
1584 if let Some(v) = self.exact.get(s) {
1585 return v.as_ref().map(|_| ()).map_err(Clone::clone);
1586 }
1587 match self.semantic.get(s) {
1588 Some(SemState::Unresolved(u)) => Err(u.clone()),
1589 Some(SemState::Instances { values, expected })
1590 if expected.len() == 1 && expected.contains(&Vec::new()) =>
1591 {
1592 match values.get(&Vec::new()) {
1593 Some(Err(u)) => Err(u.clone()),
1594 _ => Ok(()),
1595 }
1596 }
1597 _ => Ok(()),
1598 }
1599 }
1600
1601 fn lineage_of(&self, deps: &[String]) -> Lineage {
1602 let mut inputs = BTreeSet::new();
1603 let mut steps = BTreeSet::new();
1604 for d in deps {
1605 if let Some(n) = d.strip_prefix("input:") {
1606 inputs.insert(n.to_string());
1607 } else if let Some(s) = d.strip_prefix("step:") {
1608 steps.insert(s.to_string());
1609 if let Some(l) = self.lineage.get(d) {
1610 inputs.extend(l.inputs.iter().cloned());
1611 steps.extend(l.steps.iter().cloned());
1612 }
1613 }
1614 }
1615 Lineage {
1616 inputs: inputs.into_iter().collect(),
1617 steps: steps.into_iter().collect(),
1618 }
1619 }
1620
1621 fn advance(&mut self) {
1622 let compiled = self.compiled;
1623 let info = &compiled.info;
1624 loop {
1625 let mut progressed = false;
1626 for id in &info.order {
1627 if matches!(info.steps.get(id), Some(StepInfo::Generate))
1628 || self.exact.contains_key(id)
1629 || self.semantic.contains_key(id)
1630 || self.skipped.contains_key(id)
1631 || self.acquisitions.contains_key(id)
1632 || self.widenings.contains_key(id)
1633 {
1634 continue;
1635 }
1636 let deps = &info.deps[id];
1637 if !deps.iter().all(|d| self.dep_done(d)) && !self.acquisition_guard_terminal(id) {
1638 continue;
1639 }
1640 let skipped = deps.iter().find_map(|d| {
1641 let guard = self.skipped.get(d.strip_prefix("step:")?)?;
1642 let propagate = match &info.steps[id] {
1643 StepInfo::Acquisition(_) => false,
1644 StepInfo::Exact { args, .. } => !crate::compile::probe_only(args, d),
1645 _ => true,
1646 };
1647 propagate.then(|| guard.clone())
1648 });
1649 if let Some(guard) = skipped {
1650 self.skipped.insert(id.clone(), guard);
1651 self.lineage
1652 .insert(format!("step:{id}"), self.lineage_of(deps));
1653 progressed = true;
1654 continue;
1655 }
1656 progressed = true;
1657 let key = format!("step:{id}");
1658 let lineage = self.lineage_of(deps);
1659 match &info.steps[id] {
1660 StepInfo::Generate => unreachable!("generation evaluation refused at start"),
1661 StepInfo::Acquisition(a) => self.start_acquisition(id, a),
1663 StepInfo::Exact { args, .. } => {
1664 if matches!(args, OpArgs::ComposeKey(_))
1665 || deps.iter().any(|d| self.constructed.contains(d))
1666 {
1667 self.constructed.insert(key.clone());
1668 }
1669 let derivation = combine(deps.iter().map(|d| {
1670 self.derivation
1671 .get(d)
1672 .copied()
1673 .unwrap_or(Derivation::ExactDerived)
1674 }));
1675 let v2 = info.v2;
1676 let unresolved = if matches!(args, OpArgs::FieldPresence(_)) {
1677 None
1678 } else {
1679 merge(
1680 deps.iter()
1681 .filter(|d| !self.is_screen_ref(d))
1682 .filter_map(|d| self.status(d).err())
1683 .collect(),
1684 )
1685 };
1686 let result = match unresolved {
1687 Some(u) => Err(u),
1688 None if v2 => {
1689 let (r, meta, q) = self.eval_exact_v2(id, args, &lineage.inputs);
1690 if let Some(m) = meta {
1691 self.metas.insert(key.clone(), m);
1692 }
1693 self.step_quality.insert(id.clone(), q);
1694 r
1695 }
1696 None => self.eval_op(id, args, &lineage.inputs),
1697 };
1698 if let Ok(v) = &result {
1699 self.exact_values.insert(id.clone(), v.clone());
1700 } else {
1701 self.constructed.remove(&key);
1702 }
1703 self.exact.insert(id.clone(), result);
1704 self.derivation.insert(key.clone(), derivation);
1705 }
1706 StepInfo::Corroborate { decl, .. } => {
1707 let (claim, _) = quality::claim_path(decl);
1710 let derivation = self
1711 .derivation
1712 .get(&claim)
1713 .copied()
1714 .unwrap_or(Derivation::ExactDerived);
1715 if self.constructed.contains(&claim) {
1716 self.constructed.insert(key.clone());
1717 }
1718 let (result, meta, q) = self.eval_corroborate(id, decl);
1719 if let Ok(v) = &result {
1721 self.exact_values.insert(id.clone(), v.clone());
1722 } else {
1723 self.constructed.remove(&key);
1724 }
1725 if let Some(m) = meta {
1726 self.metas.insert(key.clone(), m);
1727 }
1728 self.step_quality.insert(id.clone(), q);
1729 self.exact.insert(id.clone(), result);
1730 self.derivation.insert(key.clone(), derivation);
1731 }
1732 StepInfo::Semantic { decl, .. } => {
1733 self.derivation
1734 .insert(key.clone(), Derivation::ModelDerived);
1735 let state = self.start_semantic(id, deps);
1736 self.semantic.insert(id.clone(), state);
1737 if info.v2 {
1738 let mut m = grading::combine_reads(
1742 &quality::semantic_reads(decl),
1743 &self.metas,
1744 Basis::Computed,
1745 );
1746 m.grade = match m.grade.graded() {
1747 Some(g) => g.min(Grade::Estimated).into(),
1748 None => ValueGrade::Ungraded,
1749 };
1750 let node = m.grade;
1751 self.metas.insert(key.clone(), Meta { whole: m, node });
1752 }
1753 }
1754 }
1755 self.lineage.insert(key, lineage);
1756 }
1757 if !progressed {
1758 break;
1759 }
1760 }
1761 }
1762
1763 fn start_input_v2(
1765 &mut self,
1766 name: &str,
1767 decl: &rustev_contract::definition::InputDecl,
1768 entries: &[&Entry],
1769 ) {
1770 let compiled = self.compiled;
1771 let key = format!("input:{name}");
1772 let grading = compiled.info.bound.inputs.get(name);
1773 let (res, q) = grading::validate_input(name, decl, entries, grading, self.now);
1774 match res {
1775 Ok((v, derived, used)) => {
1776 self.inputs_status.insert(name.into(), Ok(()));
1777 self.input_values.insert(name.into(), v);
1778 self.input_evidence.insert(
1779 name.into(),
1780 InputStatus::Resolved {
1781 provenance: used.provenance,
1782 as_of_ms: used.as_of_ms,
1783 source: used.source.clone(),
1784 },
1785 );
1786 self.present.insert(name.into());
1787 self.derivation.insert(
1788 key.clone(),
1789 match derived {
1790 grading::Derived::Model => Derivation::ModelDerived,
1791 grading::Derived::Exact => Derivation::ExactDerived,
1792 },
1793 );
1794 if let Some(m) = &q.meta {
1795 self.metas.insert(key.clone(), m.clone());
1796 }
1797 }
1798 Err(u) => {
1799 self.inputs_status.insert(name.into(), Err(u.clone()));
1800 self.input_evidence
1801 .insert(name.into(), InputStatus::Unresolved(u));
1802 self.derivation
1803 .insert(key.clone(), Derivation::ExactDerived);
1804 }
1805 }
1806 self.input_quality.insert(name.into(), q);
1807 self.lineage.insert(
1808 key,
1809 Lineage {
1810 inputs: vec![name.to_string()],
1811 steps: vec![],
1812 },
1813 );
1814 }
1815
1816 fn value_and_meta(&self, r: &str) -> Result<(Value, ValueMeta), Unresolved> {
1818 let read = quality::whole_read(r)
1819 .ok_or_else(|| Unresolved::invalid([], format!("{r} reads nothing")))?;
1820 self.status(&read.base)?;
1821 let v = crate::expr::eval_ref(r, &View::scope(self)).map_err(|f| match f {
1822 crate::expr::Fault::Absent(p) | crate::expr::Fault::Missing(p) => {
1823 Unresolved::missing([p])
1824 }
1825 crate::expr::Fault::Conflict(c) => Unresolved::conflict([c]),
1826 crate::expr::Fault::Invalid(d) => Unresolved::invalid([], d),
1827 })?;
1828 Ok((v, grading::read_meta(&read, &self.metas)))
1829 }
1830
1831 fn eval_exact_v2(
1834 &self,
1835 id: &str,
1836 args: &OpArgs,
1837 lineage_inputs: &[String],
1838 ) -> (Result<Value, Unresolved>, Option<Meta>, StepQuality) {
1839 let info = &self.compiled.info;
1840 let none = StepQuality::default();
1841 let reads = quality::op_reads(args);
1842 for r in &reads {
1843 if View::skipped(self, &r.base) && crate::compile::probe_only(args, &r.base) {
1844 continue;
1845 }
1846 let ceiling = quality::read_ceiling(r, &info.trees);
1847 let meta = grading::read_meta(r, &self.metas);
1848 if let Err(u) = grading::check_read(r, &ceiling, &meta, self.now) {
1849 return (Err(u), None, none);
1850 }
1851 }
1852 let mut here = grading::combine_reads(&reads, &self.metas, Basis::Computed);
1853 if self.constructed.contains(&format!("step:{id}")) {
1854 here.grade = ValueGrade::Hint;
1855 }
1856 match args {
1857 OpArgs::JoinKeyed(a) => {
1858 let joined = (|| {
1859 let (lv, lm) = self.value_and_meta(&a.left)?;
1860 let (rv, rm) = self.value_and_meta(&a.right)?;
1861 let left = a.left.split_once(':').map_or(a.left.as_str(), |(_, p)| p);
1863 let key_path = format!("{left}/*/{}", a.key);
1864 grading::join((&lv, &lm), &a.key, &key_path, (&rv, &rm), here.clone())
1865 .map_err(|d| Unresolved::invalid(lineage_inputs.iter().cloned(), d))
1866 })();
1867 match joined {
1868 Ok((v, whole)) => {
1869 let join = match &v {
1870 Value::Joined(entries) => entries
1871 .iter()
1872 .map(|e| JoinEvidence {
1873 id: e.id.clone(),
1874 status: match e.matched {
1875 crate::value::JoinMatch::Matched(_) => MatchStatus::Matched,
1876 crate::value::JoinMatch::Unmatched { .. } => {
1877 MatchStatus::Unmatched
1878 }
1879 crate::value::JoinMatch::MatchedAbsent(_) => {
1880 MatchStatus::MatchedAbsent
1881 }
1882 },
1883 })
1884 .collect(),
1885 _ => vec![],
1886 };
1887 let node = here.grade;
1888 (
1889 Ok(v),
1890 Some(Meta { whole, node }),
1891 StepQuality {
1892 join: Some(join),
1893 ..Default::default()
1894 },
1895 )
1896 }
1897 Err(u) => (Err(u), None, none),
1898 }
1899 }
1900 OpArgs::MergeFields(m) => {
1901 let merged = (|| {
1902 let (bv, bm) = self.value_and_meta(&m.base)?;
1903 let (ov, om) = self.value_and_meta(&m.overlay)?;
1904 grading::merge(m, id, (&bv, &bm), (&ov, &om))
1905 .map_err(|d| Unresolved::invalid(lineage_inputs.iter().cloned(), d))
1906 })();
1907 match merged {
1908 Ok((v, whole, evidence)) => {
1909 let node = whole.grade;
1910 (
1911 Ok(v),
1912 Some(Meta { whole, node }),
1913 StepQuality {
1914 merge: Some(evidence),
1915 ..Default::default()
1916 },
1917 )
1918 }
1919 Err(u) => (Err(u), None, none),
1920 }
1921 }
1922 _ => {
1923 let result = self.eval_op(id, args, lineage_inputs);
1924 let node = here.grade;
1925 let meta = result.is_ok().then_some(Meta { whole: here, node });
1926 (result, meta, none)
1927 }
1928 }
1929 }
1930
1931 fn eval_corroborate(
1933 &self,
1934 id: &str,
1935 decl: &rustev_contract::definition::CorroborateDecl,
1936 ) -> (Result<Value, Unresolved>, Option<Meta>, StepQuality) {
1937 use rustev_contract::definition::{CorroborationMode, OnContradiction, OnFalse};
1938 let info = &self.compiled.info;
1939 let claim_path = quality::claim_path(decl);
1940 let (claim, claim_meta) = match self.value_and_meta(&decl.claim) {
1942 Ok(x) => x,
1943 Err(u) => return (Err(u), None, StepQuality::default()),
1944 };
1945 let scope = View::scope(self);
1946 let mut checks = Vec::new();
1947 let mut contradicting: Vec<String> = Vec::new();
1948 for (check, reads) in decl.by.iter().zip(quality::check_reads(decl)) {
1949 let mut reason: Option<CheckReason> = None;
1950 let mut records = Vec::new();
1951 for r in &reads {
1952 let name = r.name();
1953 let (digest, grade) = match self.value_and_meta(&name) {
1954 Ok((v, m)) => (Some(grading::digest(&v.to_json())), m.grade),
1955 Err(u) => {
1956 reason.get_or_insert(CheckReason::Unresolved(u));
1957 (None, ValueGrade::Ungraded)
1958 }
1959 };
1960 let meta = grading::read_meta(r, &self.metas);
1961 if reason.is_none() {
1962 let ceiling = quality::read_ceiling(r, &info.trees);
1963 if let Err(u) = grading::check_read(r, &ceiling, &meta, self.now) {
1964 reason = Some(CheckReason::Unresolved(u));
1965 }
1966 }
1967 if reason.is_none()
1970 && !quality::reads_claim(r, &claim_path)
1971 && meta.grade.graded().is_none_or(|g| g < decl.upgrade_to)
1972 {
1973 reason = Some(CheckReason::BelowGrade { read: name.clone() });
1974 }
1975 records.push(ReadEvidence {
1976 reference: name,
1977 digest,
1978 grade,
1979 });
1980 }
1981 let (result, reason) = match reason {
1982 Some(r) => (CheckResult::NoEvidence, r),
1983 None => match crate::expr::eval_predicate(&check.predicate, &scope) {
1984 Ok(true) => (CheckResult::Supports, CheckReason::True),
1985 Ok(false) => (
1986 match check.on_false {
1987 OnFalse::Contradicts => CheckResult::Contradicts,
1988 OnFalse::NoEvidence => CheckResult::NoEvidence,
1989 },
1990 CheckReason::False(check.on_false),
1991 ),
1992 Err(crate::expr::Fault::Absent(p)) => (
1993 CheckResult::NoEvidence,
1994 CheckReason::Unresolved(Unresolved::missing([p])),
1995 ),
1996 Err(f) => (
1997 CheckResult::NoEvidence,
1998 CheckReason::Unresolved(Unresolved::invalid([], format!("{f:?}"))),
1999 ),
2000 },
2001 };
2002 if result == CheckResult::Contradicts {
2003 contradicting.extend(records.iter().map(|r| r.reference.clone()));
2004 }
2005 checks.push(CheckEvidence {
2006 result,
2007 reason,
2008 reads: records,
2009 });
2010 }
2011 let supports = checks
2012 .iter()
2013 .filter(|c| c.result == CheckResult::Supports)
2014 .count();
2015 let outcome = if !contradicting.is_empty() {
2016 CorroborationOutcome::Contradicted
2017 } else if match decl.mode {
2018 CorroborationMode::All => supports == checks.len(),
2019 CorroborationMode::Any => supports > 0,
2020 } {
2021 CorroborationOutcome::Corroborated
2022 } else {
2023 CorroborationOutcome::Uncorroborated
2024 };
2025 let claim_name = if claim_path.1.is_empty() {
2026 claim_path.0.clone()
2027 } else {
2028 format!("{}/{}", claim_path.0, claim_path.1.join("/"))
2029 };
2030 let (result, meta) = match (outcome, decl.on_contradiction) {
2031 (CorroborationOutcome::Corroborated, _) => {
2032 let m = grading::raise(&claim_meta, decl.upgrade_to, id);
2033 (Ok(claim.clone()), Some(m))
2034 }
2035 (CorroborationOutcome::Uncorroborated, _) => {
2036 (Ok(claim.clone()), Some(claim_meta.clone()))
2037 }
2038 (CorroborationOutcome::Contradicted, OnContradiction::Conflict) => {
2039 let mut facts = vec![claim_name];
2040 facts.extend(contradicting);
2041 (Err(Unresolved::conflict(facts)), None)
2042 }
2043 (CorroborationOutcome::Contradicted, OnContradiction::TreatAsHint) => {
2044 (Ok(claim.clone()), Some(grading::lower_to_hint(&claim_meta)))
2045 }
2046 (CorroborationOutcome::Contradicted, OnContradiction::Abstain) => (
2047 Err(Unresolved::Abstained {
2048 rule: id.to_string(),
2049 }),
2050 None,
2051 ),
2052 };
2053 let record = Corroboration {
2054 claim: decl.claim.clone(),
2055 claim_digest: grading::digest(&claim.to_json()),
2056 grade_before: claim_meta.grade,
2057 grade_after: meta.as_ref().map(|m| m.grade).unwrap_or(claim_meta.grade),
2058 outcome,
2059 checks,
2060 };
2061 (
2062 result,
2063 meta.map(|whole| {
2064 let node = whole.grade;
2065 Meta { whole, node }
2066 }),
2067 StepQuality {
2068 corroboration: Some(record),
2069 ..Default::default()
2070 },
2071 )
2072 }
2073
2074 fn eval_op(
2075 &self,
2076 _id: &str,
2077 args: &OpArgs,
2078 lineage_inputs: &[String],
2079 ) -> Result<Value, Unresolved> {
2080 let semantic: BTreeMap<String, SemInstances> = self
2081 .semantic
2082 .iter()
2083 .filter_map(|(k, v)| match v {
2084 SemState::Instances { values, .. } => Some((k.clone(), values.clone())),
2085 SemState::Unresolved(_) => None,
2086 })
2087 .collect();
2088 let binds: BTreeMap<String, Vec<String>> = self
2089 .compiled
2090 .info
2091 .steps
2092 .iter()
2093 .filter_map(|(k, s)| match s {
2094 StepInfo::Semantic { decl, .. } => Some((
2095 k.clone(),
2096 decl.for_each.iter().map(|f| f.bind.clone()).collect(),
2097 )),
2098 StepInfo::Exact { .. }
2099 | StepInfo::Corroborate { .. }
2100 | StepInfo::Acquisition(_)
2101 | StepInfo::Generate => None,
2102 })
2103 .collect();
2104 let cx = OpContext {
2105 scope: Scope {
2106 constructed: Some(&self.constructed),
2107 now: self.now,
2108 inputs: &self.input_values,
2109 steps: &self.exact_values,
2110 present: &self.present,
2111 item: None,
2112 v2: self.compiled.info.v2,
2113 },
2114 semantic: &semantic,
2115 inputs_status: &self.inputs_status,
2116 lineage_inputs,
2117 binds: &binds,
2118 };
2119 args.eval(&cx)
2120 }
2121
2122 fn start_semantic(&mut self, id: &str, deps: &[String]) -> SemState {
2123 let compiled = self.compiled;
2124 let detail = compiled
2125 .plan
2126 .steps
2127 .iter()
2128 .find(|s| s.id == id)
2129 .map(|s| &s.detail);
2130 if let Some(PlanStepDetail::Unsupported { capability }) = detail {
2131 return SemState::Unresolved(Unresolved::Unsupported {
2132 capability: capability.clone(),
2133 });
2134 }
2135 if let Some(u) = merge(
2136 deps.iter()
2137 .filter(|d| !self.is_screen_ref(d))
2138 .filter_map(|d| self.status(d).err())
2139 .collect(),
2140 ) {
2141 return SemState::Unresolved(u);
2142 }
2143 let Some(StepInfo::Semantic {
2144 decl,
2145 binding: Some(binding),
2146 ..
2147 }) = compiled.info.steps.get(id)
2148 else {
2149 return SemState::Unresolved(Unresolved::Unsupported {
2150 capability: id.to_string(),
2151 });
2152 };
2153 let mut keys: Vec<Vec<String>> = vec![vec![]];
2155 for f in &decl.for_each {
2156 let ids = f
2157 .over
2158 .strip_prefix("step:")
2159 .and_then(|s| self.exact_values.get(s))
2160 .and_then(Value::ids)
2161 .unwrap_or_default();
2162 keys = keys
2163 .into_iter()
2164 .flat_map(|k| {
2165 ids.iter()
2166 .map(move |i| [k.clone(), vec![i.clone()]].concat())
2167 })
2168 .collect();
2169 }
2170 let candidates = self.rank_candidates(decl);
2171 let mut values: SemInstances = BTreeMap::new();
2172 let mut expected = BTreeSet::new();
2173 let limits = &compiled.plan.definition.limits;
2174 let mut truncated = 0u64;
2175 for key in keys {
2176 expected.insert(key.clone());
2177 let projection = match self.projection(decl, &key, &candidates) {
2178 Ok(p) => p,
2179 Err(u) => {
2180 values.insert(key, Err(u));
2181 continue;
2182 }
2183 };
2184 if decl.requires == RequiredKind::ScreenVerdict {
2185 let empty = serde_json::from_slice::<Json>(&projection)
2186 .is_ok_and(|envelope| envelope["values"]["text"] == "");
2187 if empty {
2188 let value = Ok(SemValue::ScreenEmpty);
2189 self.record_screen(id, &key, &value);
2190 values.insert(key, value);
2191 continue;
2192 }
2193 }
2194 if projection.len() as u64 > binding.max_projection_bytes {
2195 values.insert(
2198 key,
2199 Err(Unresolved::BudgetExhausted {
2200 resource: "projection_bytes".into(),
2201 }),
2202 );
2203 continue;
2204 }
2205 if self.issued >= limits.max_semantic_requests {
2206 truncated += 1;
2207 values.insert(
2208 key,
2209 Err(Unresolved::BudgetExhausted {
2210 resource: "semantic_requests".into(),
2211 }),
2212 );
2213 continue;
2214 }
2215 self.issued += 1;
2216 self.pending.insert(
2217 (id.to_string(), key.clone()),
2218 SemanticRequest {
2219 step: id.to_string(),
2220 instance: key,
2221 backend_id: binding.backend_id.clone(),
2222 projection,
2223 },
2224 );
2225 }
2226 if truncated > 0 {
2227 self.notices.push(Notice {
2228 screening: None,
2229 kind: NoticeKind::Truncation,
2230 subject: format!("step:{id}"),
2231 reason: format!("{truncated} request(s) not issued: max_semantic_requests reached"),
2232 action: None,
2233 });
2234 }
2235 for (key, value) in &values {
2236 self.record_screen(id, key, value);
2237 }
2238 SemState::Instances { values, expected }
2239 }
2240
2241 fn model_node(&self, reference: &str) -> ModelNode {
2242 let (base, path) = reference
2243 .split_once('/')
2244 .map_or((reference, None), |(b, p)| (b, Some(p)));
2245 let mut node = if let Some(name) = base.strip_prefix("input:") {
2246 let mut node = ModelNode::plain(
2247 self.input_values
2248 .get(name)
2249 .map(Value::to_json)
2250 .unwrap_or(Json::Null),
2251 );
2252 for e in self.screening.values() {
2253 let (input, field) = e
2254 .field
2255 .trim_start_matches("input:")
2256 .split_once('/')
2257 .map_or((e.field.trim_start_matches("input:"), None), |(i, f)| {
2258 (i, Some(f))
2259 });
2260 if input.trim_end_matches("[]") != name {
2261 continue;
2262 }
2263 let target = if let Some(item) = &e.item {
2264 let Some(plan) = self.compiled.plan.screening.as_ref() else {
2265 continue;
2267 };
2268 let Some(entry) = plan.screening.entries.iter().find(|s| s.id == e.entry)
2269 else {
2270 continue;
2272 };
2273 let ScreeningItemId::Field(id) = &entry.item_id else {
2274 continue;
2277 };
2278 node.children
2279 .values_mut()
2280 .find(|v| v.value.get(&id.field).and_then(Json::as_str) == Some(item))
2281 } else {
2282 Some(&mut node)
2283 };
2284 if let Some(target) = target {
2288 let target = match field {
2289 Some(f) => target.children.get_mut(f),
2290 None => Some(target),
2291 };
2292 if let Some(target) = target {
2293 target.screen = Some(e.clone());
2294 }
2295 }
2296 }
2297 node
2298 } else if let Some(id) = base.strip_prefix("step:") {
2299 let output = self
2300 .exact_values
2301 .get(id)
2302 .map(Value::to_json)
2303 .unwrap_or(Json::Null);
2304 match self.compiled.info.steps.get(id) {
2305 Some(StepInfo::Corroborate { decl, .. }) => self.model_node(&decl.claim),
2306 Some(StepInfo::Exact { args, .. }) => {
2307 let sources: Vec<ModelNode> = match args {
2308 OpArgs::WindowFilter(a) => self
2309 .model_node(&a.list)
2310 .elements()
2311 .into_iter()
2312 .cloned()
2313 .collect(),
2314 OpArgs::Extremum(a) => self
2315 .model_node(&a.list)
2316 .elements()
2317 .into_iter()
2318 .filter_map(|n| n.field(&a.field).cloned())
2319 .collect(),
2320 OpArgs::FilterWithReasons(a) => self
2321 .model_node(&a.list)
2322 .elements()
2323 .into_iter()
2324 .filter_map(|n| n.field(&a.id_field).cloned())
2325 .collect(),
2326 OpArgs::TopK(a) => self
2327 .model_node(&a.list)
2328 .elements()
2329 .into_iter()
2330 .filter_map(|n| n.field(&a.id_field).cloned())
2331 .collect(),
2332 OpArgs::WeightedRank(a) => vec![self.model_node(&a.candidates)],
2333 OpArgs::ComposeKey(a) => {
2336 let mut refs = vec![];
2337 for part in &a.parts {
2338 crate::expr::expr_refs(&part.value, &mut refs);
2339 }
2340 refs.iter().map(|r| self.model_node(r)).collect()
2341 }
2342 OpArgs::JoinKeyed(a) => {
2343 let left = self.model_node(&a.left);
2344 let right = self.model_node(&a.right);
2345 let mut node = ModelNode::plain(output);
2346 for (index, target) in &mut node.children {
2347 if let Some(source) = left.children.get(index) {
2348 let key = source.children.get(&a.key).unwrap_or(source);
2349 if key.value == target.value["id"] {
2350 target.children.insert("id".into(), key.clone());
2351 }
2352 }
2353 if let Some(id) = target.value["id"].as_str()
2354 && let Some(source) = right.children.get(id)
2355 {
2356 target.children.insert("match".into(), source.clone());
2357 }
2358 }
2359 return match path {
2360 Some(p) => node
2361 .field(p)
2362 .cloned()
2363 .unwrap_or_else(|| ModelNode::plain(Json::Null)),
2364 None => node,
2365 };
2366 }
2367 OpArgs::MergeFields(a) => {
2368 let mut node = ModelNode::plain(output);
2371 let base = self.model_node(&a.base);
2372 let overlay = self.model_node(&a.overlay);
2373 if let Some(records) =
2374 self.step_quality.get(id).and_then(|q| q.merge.as_ref())
2375 {
2376 for r in records {
2377 if let rustev_contract::evidence::MergedField::Taken {
2378 side,
2379 ..
2380 } = r.result
2381 {
2382 let source = match side {
2383 rustev_contract::grade::MergeSide::Base => &base,
2384 rustev_contract::grade::MergeSide::Overlay => &overlay,
2385 };
2386 let (source, target) = if r.item.is_empty() {
2387 (Some(source), Some(&mut node))
2388 } else {
2389 (
2390 overlay.children.iter().find(|(_, n)| n.value["id"].as_str() == Some(&r.item))
2391 .and_then(|(i, _)| source.children.get(i))
2392 .and_then(|n| if side == rustev_contract::grade::MergeSide::Overlay { n.children.get("match") } else { Some(n) }),
2393 overlay.children.iter().find(|(_, n)| n.value["id"].as_str() == Some(&r.item))
2394 .and_then(|(i, _)| node.children.get_mut(i)),
2395 )
2396 };
2397 if let (Some(source), Some(target)) = (source, target)
2398 && let Some(field) = source.children.get(&r.field)
2399 {
2400 target.children.insert(r.field.clone(), field.clone());
2401 }
2402 }
2403 }
2404 }
2405 return match path {
2406 Some(p) => node
2407 .field(p)
2408 .cloned()
2409 .unwrap_or_else(|| ModelNode::plain(Json::Null)),
2410 None => node,
2411 };
2412 }
2413 OpArgs::CountWhere(_)
2414 | OpArgs::TimestampDiff(_)
2415 | OpArgs::Compare(_)
2416 | OpArgs::Arith(_)
2417 | OpArgs::FxConvert(_)
2418 | OpArgs::MemberOf(_)
2419 | OpArgs::FieldPresence(_) => vec![],
2420 };
2421 if matches!(args, OpArgs::FilterWithReasons(_) | OpArgs::TopK(_)) {
2422 let mut node = ModelNode::plain(output);
2423 for name in ["eligible", "ids"] {
2424 if let Some(ids) = node.children.get_mut(name) {
2425 *ids = ModelNode::copied(ids.value.clone(), &sources);
2426 }
2427 }
2428 if let Some(excluded) = node.children.get_mut("excluded") {
2429 for item in excluded.children.values_mut() {
2430 if let Some(id) = item.children.get_mut("id") {
2431 *id = ModelNode::copied(id.value.clone(), &sources);
2432 }
2433 }
2434 }
2435 node
2436 } else {
2437 ModelNode::copied(output, &sources)
2438 }
2439 }
2440 _ => ModelNode::plain(output),
2441 }
2442 } else {
2443 ModelNode::plain(Json::Null)
2444 };
2445 if let Some(path) = path {
2446 node = node
2447 .field(path)
2448 .cloned()
2449 .unwrap_or_else(|| ModelNode::plain(Json::Null));
2450 }
2451 node
2452 }
2453
2454 fn projection(
2455 &self,
2456 decl: &rustev_contract::definition::SemanticDecl,
2457 key: &[String],
2458 candidates: &[String],
2459 ) -> Result<Vec<u8>, Unresolved> {
2460 if decl.requires == rustev_contract::definition::RequiredKind::ScreenVerdict {
2461 return crate::compile::screening::request_projection(decl, key, &self.screen_inputs);
2462 }
2463 if self.compiled.plan.screening.is_none() {
2464 return self.legacy_projection(decl, key, candidates);
2465 }
2466 let mut values = Map::new();
2467 let mut instance = Map::new();
2468 let mut unavailable = BTreeSet::new();
2469 for (f, id) in decl.for_each.iter().zip(key) {
2470 instance.insert(f.bind.clone(), Json::String(id.clone()));
2471 }
2472 for r in &decl.project {
2473 let node = if let Some(bind) = r.strip_prefix("bind:") {
2474 match decl.for_each.iter().zip(key).find(|(f, _)| f.bind == bind) {
2475 Some((f, id)) => match &f.items {
2476 ItemSource::List { list, id_field } => self
2477 .model_node(list)
2478 .children
2479 .values()
2480 .find(|n| n.value.get(id_field).and_then(Json::as_str) == Some(id))
2481 .cloned()
2482 .ok_or_else(|| {
2483 Unresolved::invalid(
2484 [list.clone()],
2485 format!("no item with id {id:?}"),
2486 )
2487 })?,
2488 ItemSource::None => ModelNode::plain(Json::Null),
2489 },
2490 None => ModelNode::plain(Json::Null),
2491 }
2492 } else {
2493 self.model_node(r)
2494 };
2495 values.insert(r.clone(), node.render(&mut unavailable));
2496 }
2497 for (f, id) in decl.for_each.iter().zip(key) {
2499 let ids = self.model_node(&f.over);
2500 let ids = ids
2501 .children
2502 .get("eligible")
2503 .or_else(|| ids.children.get("ids"))
2504 .unwrap_or(&ids);
2505 for n in ids
2506 .elements()
2507 .into_iter()
2508 .filter(|n| n.value.as_str() == Some(id))
2509 {
2510 n.render(&mut unavailable);
2511 }
2512 }
2513 if let Candidates::From(r) = &decl.candidates {
2514 self.model_node(r).render(&mut unavailable);
2515 }
2516 if !unavailable.is_empty() {
2517 return Err(Unresolved::ScreenUnavailable {
2518 fields: unavailable.into_iter().collect(),
2519 });
2520 }
2521 let envelope = json!({
2522 "candidates": candidates,
2523 "instance": instance,
2524 "operation": decl.operation,
2525 "options": decl.options,
2526 "question": decl.question,
2527 "task": decl.task,
2528 "values": values,
2529 });
2530 canonical_value_bytes(&envelope).map_err(|e| Unresolved::invalid([], e.to_string()))
2531 }
2532
2533 fn legacy_projection(
2534 &self,
2535 decl: &rustev_contract::definition::SemanticDecl,
2536 key: &[String],
2537 candidates: &[String],
2538 ) -> Result<Vec<u8>, Unresolved> {
2539 if decl.requires == rustev_contract::definition::RequiredKind::ScreenVerdict {
2540 return crate::compile::screening::request_projection(decl, key, &self.screen_inputs);
2541 }
2542 let mut values = Map::new();
2543 let mut instance = Map::new();
2544 for (f, id) in decl.for_each.iter().zip(key) {
2545 instance.insert(f.bind.clone(), Json::String(id.clone()));
2546 }
2547 for r in &decl.project {
2548 let v = match Ref::parse(r) {
2549 Ok(Ref::Input(n, _)) => self.input_values.get(&n).map(Value::to_json),
2550 Ok(Ref::Step(_)) if self.compiled.info.v2 => {
2551 crate::expr::eval_ref(r, &View::scope(self))
2552 .ok()
2553 .map(|v| v.to_json())
2554 }
2555 Ok(Ref::Step(n)) => self.exact_values.get(&n).map(Value::to_json),
2556 Ok(Ref::Bind(b)) => {
2557 let pos = decl.for_each.iter().position(|f| f.bind == b);
2558 match pos.map(|p| (&decl.for_each[p], &key[p])) {
2559 Some((f, id)) => match &f.items {
2560 ItemSource::List { list, id_field } => {
2561 let name = list.trim_start_matches("input:");
2562 let found = match self.input_values.get(name) {
2563 Some(Value::List(items)) => items.iter().find(|it| match it {
2564 Value::Record(m) => matches!(m.get(id_field), Some(Value::Text(s) | Value::Enum(s)) if s == id),
2565 _ => false,
2566 }),
2567 _ => None,
2568 };
2569 match found {
2570 Some(it) => Some(it.to_json()),
2571 None => {
2572 return Err(Unresolved::invalid(
2573 [name.to_string()],
2574 format!("no item with id {id:?}"),
2575 ));
2576 }
2577 }
2578 }
2579 ItemSource::None => None,
2580 },
2581 None => None,
2582 }
2583 }
2584 _ => None,
2585 };
2586 values.insert(r.clone(), v.unwrap_or(Json::Null));
2587 }
2588 let envelope = json!({
2589 "candidates": candidates,
2590 "instance": instance,
2591 "operation": decl.operation,
2592 "options": decl.options,
2593 "question": decl.question,
2594 "task": decl.task,
2595 "values": values,
2596 });
2597 canonical_value_bytes(&envelope).map_err(|e| Unresolved::invalid([], e.to_string()))
2598 }
2599
2600 pub fn finish(self, decision_id: &str) -> Result<(Judgment, EvidenceRecord), NotReady> {
2602 if !self.compiled.info.generation.is_empty() {
2603 return Err(NotReady {
2604 pending: self.pending.len().max(1),
2605 });
2606 }
2607 self.finish_decision(decision_id)
2608 }
2609
2610 pub fn begin_generation(
2613 self,
2614 decision_id: &str,
2615 ) -> Result<
2616 crate::generation_eval::GenerationEvaluation<'c>,
2617 crate::generation_eval::GenerationError,
2618 > {
2619 let compiled = self.compiled;
2620 let issued = self.issued;
2621 let inputs = self.input_values.clone();
2622 let exact = self.exact_values.clone();
2623 let lineage = self.lineage.clone();
2624 let (judgment, evidence) = self
2625 .finish_decision(decision_id)
2626 .map_err(crate::generation_eval::GenerationError::NotReady)?;
2627 crate::generation_eval::GenerationEvaluation::start(
2628 compiled,
2629 judgment,
2630 evidence,
2631 (inputs, exact),
2632 lineage,
2633 issued,
2634 )
2635 }
2636
2637 fn finish_decision(
2638 mut self,
2639 decision_id: &str,
2640 ) -> Result<(Judgment, EvidenceRecord), NotReady> {
2641 if !self.pending.is_empty() || !self.pending_acquisitions().is_empty() {
2642 return Err(NotReady {
2643 pending: self.pending.len() + self.pending_acquisitions().len(),
2644 });
2645 }
2646 let compiled = self.compiled;
2647 let v2 = compiled.info.v2;
2648 let actionable = compiled.plan.actionability.is_some() || compiled.plan.screening.is_some();
2649 let (outcome, reads, trace, site, handler) = if !self.screen_refusals.is_empty() {
2651 let refusals = self.screen_refusals.clone();
2652 let addressed_to = Party::combine(refusals.iter().map(|r| r.action.addressed_to));
2653 let reads = self
2654 .screening
2655 .values()
2656 .filter(|e| e.consequence == ScreenConsequence::Refuse)
2657 .map(|e| format!("step:{}", e.step))
2658 .collect();
2659 (
2660 Outcome::Refused {
2661 refusals,
2662 addressed_to,
2663 },
2664 reads,
2665 vec!["screening".into()],
2666 None,
2667 None,
2668 )
2669 } else {
2670 match self.checks.terminal.clone() {
2671 Some(t) => {
2672 use crate::actionable::Terminal;
2673 let (outcome, trace) = match &t {
2674 Terminal::Refused => (
2675 Outcome::Refused {
2676 refusals: vec![],
2677 addressed_to: Party::None,
2678 },
2679 self.checks
2680 .refusals
2681 .iter()
2682 .map(|r| format!("check:{}", r.check))
2683 .collect(),
2684 ),
2685 Terminal::Abstain { check } => (
2686 Outcome::Unresolved(Unresolved::Abstained {
2687 rule: check.clone(),
2688 }),
2689 vec![format!("check:{check}")],
2690 ),
2691 Terminal::Escalate { check, reason } => (
2692 Outcome::Escalate {
2693 reason: reason.clone(),
2694 },
2695 vec![format!("check:{check}")],
2696 ),
2697 };
2698 let site = trace.first().cloned();
2699 (
2700 outcome,
2701 self.checks.deciding_reads.clone(),
2702 trace,
2703 site,
2704 None,
2705 )
2706 }
2707 None => {
2708 if v2 {
2709 let mut failures = BTreeMap::new();
2713 for (_, c) in quality::policy_conds(&compiled.plan.definition) {
2714 let mut reads = Vec::new();
2715 quality::cond_reads(c, &mut reads);
2716 for r in &reads {
2717 if failures.contains_key(&r.base) || self.status(&r.base).is_err() {
2718 continue;
2719 }
2720 let ceiling = quality::read_ceiling(r, &compiled.info.trees);
2721 let meta = grading::read_meta(r, &self.metas);
2722 if let Err(u) = grading::check_read(r, &ceiling, &meta, self.now) {
2723 failures.insert(r.base.clone(), u);
2724 }
2725 }
2726 }
2727 self.policy_failures = failures;
2728 }
2729 let result = policy::evaluate(&compiled.plan.definition.policy, &self);
2730 (
2731 result.outcome,
2732 result.reads,
2733 result.trace,
2734 result.site,
2735 result.handler,
2736 )
2737 }
2738 }
2739 };
2740 let derivation = if reads.is_empty() {
2741 Derivation::ExactDerived
2742 } else {
2743 combine(reads.iter().map(|r| {
2744 match self.derivation.get(r) {
2746 Some(Derivation::ExactDerived) => Derivation::ExactDerived,
2747 _ => Derivation::MixedDerived,
2748 }
2749 }))
2750 };
2751 let lineage = self.lineage_of(&reads);
2752 let mut notices = self.notices.clone();
2753 let outcome = if actionable {
2754 notices = self.actionable_notices(notices);
2755 self.actionable_outcome(outcome, site.as_deref(), handler)
2756 } else {
2757 outcome
2758 };
2759 notices.sort();
2760 notices.dedup();
2761 let judgment = Judgment {
2762 schema: if actionable {
2763 schema::JUDGMENT_V2
2764 } else {
2765 schema::JUDGMENT
2766 }
2767 .into(),
2768 plan_id: compiled.id.clone(),
2769 snapshot_id: self.snapshot_id.clone(),
2770 evaluation_time_ms: self.now,
2771 outcome,
2772 derivation,
2773 lineage,
2774 notices,
2775 trace,
2776 };
2777 let inputs = self
2778 .input_evidence
2779 .iter()
2780 .map(|(k, v)| {
2781 let q = self.input_quality.get(k).cloned().unwrap_or_default();
2782 InputEvidence {
2783 field: k.clone(),
2784 status: v.clone(),
2785 meta: q.meta.map(|m| m.whole),
2786 presence: q.presence,
2787 entries: q.entries,
2788 resolution: q.resolution,
2789 dropped: self.checks.dropped.get(k).map(|check| {
2790 rustev_contract::evidence::DroppedBy {
2791 check: check.clone(),
2792 }
2793 }),
2794 }
2795 })
2796 .collect();
2797 let mut steps = Vec::new();
2798 for id in &compiled.info.step_ids {
2799 if let Some(guard) = self.skipped.get(id) {
2800 steps.push(StepEvidence::new(
2801 id.clone(),
2802 vec![],
2803 StepStatus::Skipped {
2804 guard: guard.clone(),
2805 },
2806 ));
2807 continue;
2808 }
2809 let derivation = self
2810 .derivation
2811 .get(&format!("step:{id}"))
2812 .copied()
2813 .unwrap_or(Derivation::ExactDerived);
2814 match (
2815 &compiled.info.steps[id],
2816 self.exact.get(id),
2817 self.semantic.get(id),
2818 ) {
2819 (StepInfo::Exact { ty, .. } | StepInfo::Corroborate { ty, .. }, Some(r), _) => {
2820 steps.push(StepEvidence::new(
2821 id.clone(),
2822 vec![],
2823 match r {
2824 Ok(_) => StepStatus::Resolved {
2825 kind: ty.name(),
2826 derivation,
2827 },
2828 Err(u) => StepStatus::Unresolved(u.clone()),
2829 },
2830 ))
2831 }
2832 (StepInfo::Acquisition(a), Some(r), _) => steps.push(StepEvidence::new(
2833 id.clone(),
2834 vec![],
2835 match r {
2836 Ok(_) => StepStatus::Resolved {
2837 kind: a.ty.name(),
2838 derivation,
2839 },
2840 Err(u) => StepStatus::Unresolved(u.clone()),
2841 },
2842 )),
2843 (StepInfo::Semantic { kind, .. }, _, Some(SemState::Instances { values, .. })) => {
2844 for (k, v) in values {
2845 steps.push(StepEvidence::new(
2846 id.clone(),
2847 k.clone(),
2848 match v {
2849 Ok(_) => StepStatus::Resolved {
2850 kind: if self.is_screen_ref(&format!("step:{id}")) {
2851 "screen_verdict".into()
2852 } else {
2853 kind.name().into()
2854 },
2855 derivation,
2856 },
2857 Err(u) => StepStatus::Unresolved(u.clone()),
2858 },
2859 ));
2860 }
2861 }
2862 (_, _, Some(SemState::Unresolved(u))) => steps.push(StepEvidence::new(
2863 id.clone(),
2864 vec![],
2865 StepStatus::Unresolved(u.clone()),
2866 )),
2867 _ => {}
2868 }
2869 }
2870 if v2 {
2871 for s in &mut steps {
2874 s.constructed = self.constructed.contains(&format!("step:{}", s.step));
2875 let key = format!("step:{}", s.step);
2876 s.meta = self.metas.get(&key).map(|m| m.whole.clone());
2877 if s.instance.is_empty()
2878 && let Some(q) = self.step_quality.get(&s.step)
2879 {
2880 s.join = q.join.clone();
2881 s.merge = q.merge.clone();
2882 s.corroboration = q.corroboration.clone();
2883 }
2884 }
2885 }
2886 let screening = compiled.plan.screening.as_ref().map(|p| {
2887 p.steps
2888 .iter()
2889 .flat_map(|s| {
2890 self.screening
2891 .iter()
2892 .filter(|((id, _), _)| id == &s.step)
2893 .map(|(_, e)| e.clone())
2894 })
2895 .collect()
2896 });
2897 let evidence = EvidenceRecord {
2898 generation: None,
2899 acquisitions: self.acquisition_evidence(),
2900 screening: screening.filter(|entries: &Vec<ScreeningEvidence>| !entries.is_empty()),
2901 schema: if v2 {
2902 schema::EVIDENCE_V2
2903 } else {
2904 schema::EVIDENCE
2905 }
2906 .into(),
2907 decision_id: decision_id.into(),
2908 plan_id: compiled.id.clone(),
2909 definition_id: compiled.plan.definition_id.clone(),
2910 snapshot_id: self.snapshot_id.clone(),
2911 evaluation_time_ms: self.now,
2912 inputs,
2913 steps,
2914 judgment: judgment.clone(),
2915 checks: compiled
2916 .plan
2917 .actionability
2918 .is_some()
2919 .then(|| self.checks.records.clone()),
2920 };
2921 Ok((judgment, evidence))
2922 }
2923
2924 fn actionable_notices(&self, notices: Vec<Notice>) -> Vec<Notice> {
2927 use rustev_contract::actionability::{Address, Cause, Remediation, RemediationValue};
2928 let cc = self.caller_controlled();
2929 let computed = Address::Computed;
2930 let mut out: Vec<Notice> = notices
2931 .into_iter()
2932 .map(|mut n| {
2933 let name = n
2934 .subject
2935 .strip_prefix("step:")
2936 .unwrap_or(&n.subject)
2937 .to_string();
2938 let (causes, remediation) = match n.kind {
2939 NoticeKind::Truncation => (
2940 vec![Cause::Budget {
2941 resource: "semantic_requests".into(),
2942 }],
2943 Remediation::with(
2944 "raise_budget",
2945 [(
2946 "resource".to_string(),
2947 RemediationValue::Text("semantic_requests".into()),
2948 )],
2949 ),
2950 ),
2951 NoticeKind::UncalibratedThreshold | NoticeKind::Fallback => (
2952 vec![Cause::Capability { name: name.clone() }],
2953 Remediation::with(
2954 "provide_capability",
2955 [("name".to_string(), RemediationValue::Text(name))],
2956 ),
2957 ),
2958 NoticeKind::FieldDropped
2961 | NoticeKind::ConstraintsRelaxed
2962 | NoticeKind::GradeDowngraded
2963 | NoticeKind::CheckNotice
2964 | NoticeKind::Screening => return n,
2965 };
2966 n.action = Some(crate::actionable::action(
2967 causes,
2968 crate::actionable::ActionInputs {
2969 caller_controlled: &cc,
2970 address: &computed,
2971 declared: None,
2972 computed: remediation,
2973 transient: false,
2974 },
2975 ));
2976 n
2977 })
2978 .collect();
2979 for p in &self.checks.notices {
2980 out.push(Notice {
2981 screening: None,
2982 kind: p.kind,
2983 subject: p.subject.clone(),
2984 reason: p.reason.clone(),
2985 action: Some(crate::actionable::action(
2986 p.causes.clone(),
2987 crate::actionable::ActionInputs {
2988 caller_controlled: &cc,
2989 address: &p.address,
2990 declared: None,
2991 computed: Remediation::None,
2992 transient: false,
2993 },
2994 )),
2995 });
2996 }
2997 out
2998 }
2999
3000 fn actionable_outcome(
3003 &self,
3004 outcome: Outcome,
3005 site: Option<&str>,
3006 handler: Option<usize>,
3007 ) -> Outcome {
3008 use rustev_contract::actionability::{Action, Cause, Override, Refusal, Remediation};
3009 let cc = self.caller_controlled();
3010 match outcome {
3011 outcome @ Outcome::Refused { .. } if !self.screen_refusals.is_empty() => outcome,
3012 Outcome::Refused { .. } => {
3013 let refusals: Vec<Refusal> = self
3014 .checks
3015 .refusals
3016 .iter()
3017 .map(|r| Refusal {
3018 check: r.check.clone(),
3019 subject: r.subject.clone(),
3020 action: crate::actionable::action(
3021 r.causes.clone(),
3022 crate::actionable::ActionInputs {
3023 caller_controlled: &cc,
3024 address: &r.address,
3025 declared: Some(self.resolve_remediation(&r.remediation)),
3026 computed: Remediation::None,
3027 transient: false,
3028 },
3029 ),
3030 })
3031 .collect();
3032 let addressed_to = Party::combine(refusals.iter().map(|r| r.action.addressed_to));
3033 Outcome::Refused {
3034 refusals,
3035 addressed_to,
3036 }
3037 }
3038 Outcome::Unresolved(u) => {
3039 let site = site.map(str::to_string).unwrap_or_else(|| match &u {
3040 Unresolved::Abstained { rule } => format!("rule:{rule}"),
3041 _ => "policy".into(),
3042 });
3043 let declared = handler
3044 .and_then(|i| self.compiled.plan.definition.policy.on_unresolved.get(i))
3045 .and_then(|h| match &h.action {
3046 rustev_contract::definition::HandlerAction::PropagateWith {
3047 remediation,
3048 address,
3049 } => Some((remediation, address)),
3050 _ => None,
3051 });
3052 let action = self.reason_action(&u, &site, declared);
3053 Outcome::UnresolvedAction { reason: u, action }
3054 }
3055 Outcome::NeedsInput(mut n) => {
3056 let declared = self
3057 .compiled
3058 .plan
3059 .definition
3060 .actionability
3061 .as_ref()
3062 .map(|a| a.questions.clone())
3063 .unwrap_or_default();
3064 n.questions = std::mem::take(&mut n.questions)
3068 .into_iter()
3069 .filter_map(|q| declared.iter().find(|d| d.id == q.id).cloned())
3070 .collect();
3071 let ids: Vec<String> = n.questions.iter().map(|q| q.id.clone()).collect();
3072 let site = site.unwrap_or("policy").to_string();
3073 let bytes = rustev_contract::canonical::record_canonical_bytes(&json!({
3074 "plan_id": self.compiled.id.to_string(),
3075 "snapshot_id": self.snapshot_id.to_string(),
3076 "evaluation_time_ms": self.now,
3077 "site": site,
3078 "fields": n.fields,
3079 "questions": ids,
3080 }))
3081 .expect("continuation inputs have canonical bytes");
3084 let digest = rustev_contract::canonical::tagged_digest(
3085 rustev_contract::actionability::CONTINUATION_TAG,
3086 &bytes,
3087 );
3088 n.continuation = serde_json::from_value(json!(digest))
3091 .expect("a tagged digest parses as a digest");
3092 let causes: Vec<Cause> = n
3093 .fields
3094 .iter()
3095 .map(|f| {
3096 let classes = self
3097 .compiled
3098 .info
3099 .inputs
3100 .get(f)
3101 .map(|(_, d)| {
3102 let mut v: Vec<ProvenanceClass> = d
3103 .provenance
3104 .iter()
3105 .filter(|c| cc.contains(c))
3106 .copied()
3107 .collect();
3108 v.sort();
3109 v
3110 })
3111 .unwrap_or_default();
3112 Cause::Input {
3113 field: f.clone(),
3114 provenance: rustev_contract::actionability::CauseProvenance::Accepted {
3115 classes,
3116 },
3117 }
3118 })
3119 .collect();
3120 n.action = Action {
3122 addressed_to: Party::Caller,
3123 computed: Party::Caller,
3124 override_: Override::None,
3125 remediation: Remediation::with(
3126 "provide_fields",
3127 [(
3128 "fields".to_string(),
3129 Remediation::texts(n.fields.iter().cloned()),
3130 )],
3131 ),
3132 caused_by: crate::actionable::sort_causes(causes),
3133 transient: false,
3134 };
3135 Outcome::NeedsInput(n)
3136 }
3137 other => other,
3138 }
3139 }
3140
3141 pub fn exact_value(&self, step: &str) -> Option<&Value> {
3143 self.exact_values.get(step)
3144 }
3145
3146 pub fn status_of(&self, r: &str) -> Result<(), Unresolved> {
3148 self.status(r)
3149 }
3150}
3151
3152impl View for Evaluation<'_> {
3153 fn skipped(&self, r: &str) -> bool {
3154 self.skipped.contains_key(
3155 crate::expr::base_ref_v2(r)
3156 .strip_prefix("step:")
3157 .unwrap_or(r),
3158 )
3159 }
3160 fn base(&self, r: &str) -> String {
3161 if self.compiled.info.v2 {
3162 crate::expr::base_ref_v2(r)
3163 } else {
3164 crate::expr::base_ref(r)
3165 }
3166 }
3167
3168 fn provenance(&self, r: &str) -> Result<Vec<ProvenanceClass>, Unresolved> {
3169 let base = View::base(self, r);
3170 self.status(&base)?;
3171 if let Some(n) = base.strip_prefix("input:") {
3172 return match self.input_evidence.get(n) {
3173 Some(InputStatus::Resolved { provenance, .. }) => Ok(vec![*provenance]),
3174 _ => Err(Unresolved::missing([n.to_string()])),
3175 };
3176 }
3177 Ok(self
3179 .metas
3180 .get(&base)
3181 .map(|m| m.whole.provenance.clone())
3182 .unwrap_or_default())
3183 }
3184
3185 fn get(&self, r: &str) -> Result<Seen<'_>, Unresolved> {
3186 if self.compiled.info.v2 {
3187 if let Some(u) = self.policy_failures.get(&self.base(r)) {
3188 return Err(u.clone());
3189 }
3190 let base = self.base(r);
3193 if base != r && base.starts_with("step:") {
3194 self.status(&base)?;
3195 let (id, path) = crate::expr::split_step(r.trim_start_matches("step:"));
3196 let mut v = self
3197 .exact_values
3198 .get(&id)
3199 .ok_or_else(|| Unresolved::invalid([], format!("{r} has no value")))?;
3200 for f in &path {
3201 v = match v {
3202 Value::Record(m) => m
3203 .get(f)
3204 .ok_or_else(|| Unresolved::invalid([], format!("no field {f:?}")))?,
3205 _ => return Err(Unresolved::invalid([], format!("no field {f:?}"))),
3206 };
3207 if matches!(v, Value::Null | Value::Absent(_)) {
3208 return Err(Unresolved::missing([r
3209 .trim_start_matches("step:")
3210 .to_string()]));
3211 }
3212 }
3213 return Ok(Seen::Exact(v));
3214 }
3215 }
3216 if let Some(rest) = r.strip_prefix("input:") {
3217 let mut parts = rest.split('/');
3218 let n = parts.next().unwrap_or_default();
3219 self.status(&format!("input:{n}"))?;
3220 let mut v = self
3221 .input_values
3222 .get(n)
3223 .ok_or_else(|| Unresolved::missing([n.to_string()]))?;
3224 for f in parts {
3225 v = match v {
3226 Value::Record(m) => m.get(f).ok_or_else(|| {
3227 Unresolved::invalid([n.to_string()], format!("no field {f:?}"))
3228 })?,
3229 _ => {
3230 return Err(Unresolved::invalid(
3231 [n.to_string()],
3232 format!("no field {f:?}"),
3233 ));
3234 }
3235 };
3236 if matches!(v, Value::Null | Value::Absent(_)) {
3238 return Err(Unresolved::missing([rest.to_string()]));
3239 }
3240 }
3241 return Ok(Seen::Exact(v));
3242 }
3243 let s = r.strip_prefix("step:").unwrap_or(r);
3244 self.status(r)?;
3245 if let Some(v) = self.exact_values.get(s) {
3246 return Ok(Seen::Exact(v));
3247 }
3248 match self.semantic.get(s) {
3249 Some(SemState::Instances { values, .. }) => match values.get(&Vec::new()) {
3250 Some(Ok(v)) => Ok(Seen::Semantic(v)),
3251 Some(Err(u)) => Err(u.clone()),
3252 None => Err(Unresolved::invalid([], format!("{r} fans out"))),
3253 },
3254 Some(SemState::Unresolved(u)) => Err(u.clone()),
3255 None => Err(Unresolved::invalid([], format!("{r} has no value"))),
3256 }
3257 }
3258
3259 fn scope(&self) -> Scope<'_> {
3260 Scope {
3261 constructed: Some(&self.constructed),
3262 now: self.now,
3263 inputs: &self.input_values,
3264 steps: &self.exact_values,
3265 present: &self.present,
3266 item: None,
3267 v2: self.compiled.info.v2,
3268 }
3269 }
3270}
3271
3272#[cfg(test)]
3273mod screening_hardening_tests {
3274 use super::*;
3275 use rustev_contract::definition::{Definition, StepBody, StepDecl};
3276 use rustev_contract::descriptor::{
3277 BackendDescriptor, InputExcess, InputLimit, OperationSupport, ScreenProfile,
3278 };
3279 use rustev_contract::output::ScreenUnits;
3280 use rustev_contract::{Document, schema};
3281 use serde_json::json;
3282
3283 fn fixture() -> (Compiled, Snapshot, Timestamp) {
3286 let mut d =
3287 Definition::parse(include_bytes!("testdata/screening.definition.json")).unwrap();
3288 let backend = BackendDescriptor {
3289 schema: schema::BACKEND.into(),
3290 backend_id: "screen".into(),
3291 artifact: rustev_contract::ids::ArtifactId::parse(&format!(
3292 "sha256:{}",
3293 "b".repeat(64)
3294 ))
3295 .unwrap(),
3296 operations: vec![
3297 OperationSupport {
3298 operation: Operation::Proposition,
3299 output: OutputKind::Screen,
3300 max_options: 2,
3301 isolation: None,
3302 screen: Some(ScreenProfile {
3303 policy_version: "synthetic/1".into(),
3304 categories: vec![],
3305 max_text_bytes: 48,
3306 text_unit_bytes: 16,
3307 }),
3308 },
3309 OperationSupport {
3310 operation: Operation::Classify,
3311 output: OutputKind::Label,
3312 max_options: 2,
3313 isolation: None,
3314 screen: None,
3315 },
3316 ],
3317 input_limit: InputLimit {
3318 max_bytes: 65536,
3319 on_excess: InputExcess::Refuse,
3320 },
3321 determinism: rustev_contract::definition::Determinism::Bitwise,
3322 };
3323 let c = crate::compile(&d, std::slice::from_ref(&backend), &[]).unwrap();
3324 let StepInfo::Semantic { decl, .. } = c.info.steps.get("screen:caller_text:0").unwrap()
3325 else {
3326 panic!("screen step")
3327 };
3328 let mut model = decl.clone();
3329 model.operation = Operation::Classify;
3330 model.requires = RequiredKind::Label;
3331 model.project = vec!["input:request".into()];
3332 d.steps.push(StepDecl {
3333 id: "model".into(),
3334 body: StepBody::Semantic(*model),
3335 });
3336 let c = crate::compile(&d, &[backend], &[]).unwrap();
3337 let now = Timestamp::from_ms(1_790_164_800_000).unwrap();
3338 let snapshot = Snapshot {
3339 schema: schema::SNAPSHOT.into(),
3340 entries: vec![
3341 Entry {
3342 field: "request".into(),
3343 value: json!({"text":"unsafe text","behaviour_text":""}),
3344 provenance: ProvenanceClass::UserSupplied,
3345 as_of_ms: now,
3346 source: "synthetic".into(),
3347 grade: None,
3348 producer: None,
3349 },
3350 Entry {
3351 field: "candidates".into(),
3352 value: json!([]),
3353 provenance: ProvenanceClass::ThirdParty,
3354 as_of_ms: now,
3355 source: "synthetic".into(),
3356 grade: None,
3357 producer: None,
3358 },
3359 ],
3360 };
3361 (c, snapshot, now)
3362 }
3363
3364 fn assert_unavailable(ev: Evaluation<'_>) {
3365 assert!(ev.pending().is_empty());
3366 assert_eq!(
3367 ev.status_of("step:model"),
3368 Err(Unresolved::ScreenUnavailable {
3369 fields: vec!["input:request/text".into()]
3370 })
3371 );
3372 let (judgment, evidence) = ev.finish("corrupted-screen").unwrap();
3373 assert!(!matches!(judgment.outcome, Outcome::Refused { .. }));
3374 let entry = evidence
3375 .screening
3376 .unwrap()
3377 .into_iter()
3378 .find(|e| e.entry == "caller_text")
3379 .unwrap();
3380 assert!(matches!(
3381 entry.verdict,
3382 ScreenVerdict::Unavailable {
3383 reason: Unresolved::InvalidBackendOutput { .. }
3384 }
3385 ));
3386 assert_eq!(entry.consequence, ScreenConsequence::FailClosed);
3387 }
3388
3389 fn blocked() -> Supplied {
3390 Supplied::Output(RawOutput::Screen {
3391 blocked: true,
3392 categories: vec![],
3393 units: ScreenUnits::NotReported,
3394 })
3395 }
3396
3397 #[test]
3398 fn legacy_screen_projection_preserves_original_inputs() {
3399 let (c, snapshot, now) = fixture();
3400 let mut ev = Evaluation::start(&c, &snapshot, now).unwrap();
3401 let StepInfo::Semantic { decl, .. } = c.info.steps.get("screen:caller_text:0").unwrap()
3402 else {
3403 panic!("screen step")
3404 };
3405 let original = ev.legacy_projection(decl, &[], &[]).unwrap();
3406 ev.input_values.remove("request");
3407 assert_eq!(ev.legacy_projection(decl, &[], &[]).unwrap(), original);
3408 }
3409
3410 #[test]
3411 fn missing_profile_is_invalid_output_without_panicking() {
3412 let (mut c, snapshot, now) = fixture();
3413 c.info.screen_categories.clear();
3414 let mut ev = Evaluation::start(&c, &snapshot, now).unwrap();
3415 ev.supply("screen:caller_text:0", &[], blocked()).unwrap();
3416 assert_unavailable(ev);
3417 }
3418
3419 #[test]
3420 fn corrupted_projection_cannot_bypass_refusing_entry() {
3421 for non_string in [false, true] {
3422 let (c, snapshot, now) = fixture();
3423 let mut ev = Evaluation::start(&c, &snapshot, now).unwrap();
3424 let Some(Value::Record(fields)) = ev.screen_inputs.get_mut("request") else {
3425 panic!("request record")
3426 };
3427 if non_string {
3428 fields.insert("text".into(), Value::List(vec![]));
3429 } else {
3430 fields.remove("text");
3431 }
3432 ev.supply("screen:caller_text:0", &[], blocked()).unwrap();
3433 assert_unavailable(ev);
3434 }
3435 }
3436}