Skip to main content

rustev_core/
evaluate.rs

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