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