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