Skip to main content

rustev_core/
extraction_eval.rs

1//! Decision-stage extraction and acceptance. Screening resolves before dispatch.
2use super::*;
3use crate::extraction_validate::{self, ValidatedField};
4use crate::template as t;
5use rustev_contract::extraction::*;
6use rustev_contract::extraction_documents::ExtractRequest;
7use rustev_contract::extraction_results::*;
8use rustev_contract::generation::TemplateBinding;
9
10#[derive(Default)]
11pub(super) struct State {
12    pub requests: BTreeMap<String, ExtractRequest>,
13    pub evidence: BTreeMap<String, ExtractionEvidence>,
14    pub acceptance: BTreeMap<String, AcceptanceEvidence>,
15    pub producers: Producers,
16    origins: BTreeMap<String, AcceptedOrigin>,
17    distributions: BTreeMap<String, FieldDistribution>,
18}
19
20impl Evaluation<'_> {
21    fn extract_binding(&self, id: &str) -> &ExtractBinding {
22        self.compiled
23            .plan
24            .steps
25            .iter()
26            .find_map(|s| match &s.detail {
27                PlanStepDetail::Extract(b) if s.id == id => Some(b.as_ref()),
28                _ => None,
29            })
30            .expect("compiled extraction binding")
31    }
32
33    fn extraction_vocabularies(&self, id: &str) -> BTreeMap<String, Vec<String>> {
34        self.extract_binding(id)
35            .vocabularies
36            .iter()
37            .filter_map(|(name, v)| {
38                let options = match v {
39                    ResolvedVocabulary::Options(v) => v.clone(),
40                    ResolvedVocabulary::Input(r) => match self.value_and_meta(r).ok()?.0 {
41                        Value::List(v) => v
42                            .into_iter()
43                            .map(|v| match v {
44                                Value::Text(s) => Some(s),
45                                _ => None,
46                            })
47                            .collect::<Option<Vec<_>>>()?,
48                        Value::Enum(_) => {
49                            let (ty, _) =
50                                self.compiled.info.inputs.get(r.strip_prefix("input:")?)?;
51                            let crate::value::Ty::Enum(v) = ty else {
52                                return None;
53                            };
54                            v.clone()
55                        }
56                        _ => return None,
57                    },
58                };
59                (options.len() <= 4096
60                    && !options.is_empty()
61                    && options.iter().all(|s| !s.is_empty() && s.len() <= 64)
62                    && options.iter().collect::<BTreeSet<_>>().len() == options.len())
63                .then(|| (name.clone(), options))
64            })
65            .collect()
66    }
67
68    fn make_extract_request(&self, id: &str) -> Option<ExtractRequest> {
69        let b = self.extract_binding(id);
70        let projection = b
71            .projection
72            .iter()
73            .map(|r| {
74                self.value_and_meta(r).ok()?;
75                Some((r.clone(), self.model_node(r).render(&mut BTreeSet::new())))
76            })
77            .collect::<Option<_>>()?;
78        let params = b
79            .template_binding
80            .params
81            .iter()
82            .map(|(name, binding)| {
83                let value = match binding {
84                    TemplateBinding::Literal { literal } => Value::from_literal(literal),
85                    TemplateBinding::Reference(r) => self.value_and_meta(r).ok()?.0,
86                    TemplateBinding::Parameter(_) => return None,
87                };
88                Some((
89                    name.clone(),
90                    crate::generation_eval::template_value(&value)?,
91                ))
92            })
93            .collect::<Option<_>>()?;
94        let schema = b
95            .template
96            .params
97            .iter()
98            .map(|p| {
99                (
100                    p.name.clone(),
101                    t::Field::exact(crate::generation::param_type(&p.ty)),
102                )
103            })
104            .collect();
105        let prompt = t::Template::compile(
106            &b.template.instructions,
107            BTreeMap::from([("param".into(), t::Field::exact(t::Type::Record(schema)))]),
108            t::Options::prompt(),
109        )
110        .ok()?;
111        let instructions = prompt
112            .render(&BTreeMap::from([(
113                "param".into(),
114                t::Value::Record(params),
115            )]))
116            .ok()?
117            .text;
118        Some(ExtractRequest {
119            schema: schema::EXTRACT_REQUEST.into(),
120            step: id.into(),
121            template: b.template.id.clone(),
122            template_id: b.template_id.clone(),
123            instructions,
124            projection,
125            output: b.schema.clone(),
126            schema_digest: b.schema_digest.clone(),
127            vocabularies: self.extraction_vocabularies(id),
128        })
129    }
130
131    pub(super) fn start_extract(&mut self, id: &str) {
132        let mut unavailable = BTreeSet::new();
133        let mut blocked = false;
134        for r in &self.extract_binding(id).projection {
135            let node = self.model_node(r);
136            blocked |= node.withheld();
137            node.render(&mut unavailable);
138        }
139        if blocked || !unavailable.is_empty() {
140            self.complete_extract(
141                id,
142                None,
143                ExtractionRequestOutcome::Withheld {
144                    // A block is a definite verdict on a read value, so it
145                    // names the outcome even when another source's screener
146                    // is unavailable (035 3.17.2, 038 3.11.4).
147                    reason: if blocked {
148                        AbsenceReason::ScreenBlocked
149                    } else {
150                        AbsenceReason::ScreenUnavailable
151                    },
152                },
153            );
154            return;
155        }
156        let Some(request) = self.make_extract_request(id) else {
157            self.complete_extract(
158                id,
159                None,
160                ExtractionRequestOutcome::Withheld {
161                    reason: AbsenceReason::SourceUnavailable,
162                },
163            );
164            return;
165        };
166        let envelope =
167            json!({"operation":"extract", "plan_id":self.compiled.id, "request":request});
168        let projection =
169            canonical_value_bytes(&envelope).expect("compiled extraction request is canonical");
170        let limits = &self.compiled.plan.definition.limits;
171        if self.issued >= limits.max_semantic_requests
172            || projection.len() as u64 > limits.max_projection_bytes
173        {
174            self.complete_extract(
175                id,
176                None,
177                ExtractionRequestOutcome::Failed {
178                    reason: RequestFailure::BudgetExhausted,
179                },
180            );
181            return;
182        }
183        let backend_id = self.extract_binding(id).backend_id.clone();
184        self.extraction.requests.insert(id.into(), request);
185        self.issued += 1;
186        self.pending.insert(
187            (id.into(), vec![]),
188            SemanticRequest {
189                step: id.into(),
190                instance: vec![],
191                backend_id,
192                projection,
193            },
194        );
195    }
196
197    /// The structured request behind a pending extraction request.
198    pub fn extraction_request(&self, step: &str) -> Option<&ExtractRequest> {
199        self.extraction.requests.get(step)
200    }
201
202    /// Runtime reports screening before dispatch. Exact fallback sources remain
203    /// readable unless named in `withheld_from_exact`.
204    pub fn withhold_extraction(
205        &mut self,
206        step: &str,
207        unavailable: bool,
208        withheld_from_exact: &[String],
209    ) -> Result<(), SupplyError> {
210        if !self.extraction.requests.contains_key(step) {
211            return Err(SupplyError::NotPending {
212                step: step.into(),
213                instance: vec![],
214            });
215        }
216        self.take(step, &[])?;
217        let reason = if unavailable {
218            AbsenceReason::ScreenUnavailable
219        } else {
220            AbsenceReason::ScreenBlocked
221        };
222        self.complete_extract_with_sources(
223            step,
224            None,
225            ExtractionRequestOutcome::Withheld { reason },
226            withheld_from_exact,
227        );
228        self.advance();
229        Ok(())
230    }
231
232    /// Cancellation is a request outcome even though it is not an Unresolved kind.
233    pub fn cancel_extraction(&mut self, step: &str) -> Result<(), SupplyError> {
234        if !self.extraction.requests.contains_key(step) {
235            return Err(SupplyError::NotPending {
236                step: step.into(),
237                instance: vec![],
238            });
239        }
240        self.take(step, &[])?;
241        self.complete_extract(
242            step,
243            None,
244            ExtractionRequestOutcome::Failed {
245                reason: RequestFailure::Cancelled,
246            },
247        );
248        self.advance();
249        Ok(())
250    }
251
252    pub(super) fn extract_request_document(
253        &self,
254        step: &str,
255        target: u32,
256        scope: &ReplayScope,
257        req: &SemanticRequest,
258    ) -> Result<RequestDoc, RequestIdentityError> {
259        let b = self.extract_binding(step);
260        let (backend_id, artifact, descriptor) = if target == 0 {
261            (&b.backend_id, &b.artifact, &b.descriptor)
262        } else {
263            let PlanExecution::Declared(e) = &self.compiled.plan.execution else {
264                return Err(RequestIdentityError::UnknownTarget {
265                    step: step.into(),
266                    target,
267                });
268            };
269            let f = e
270                .fallbacks
271                .iter()
272                .find(|f| f.step == step && f.target == target)
273                .ok_or_else(|| RequestIdentityError::UnknownTarget {
274                    step: step.into(),
275                    target,
276                })?;
277            (&f.backend_id, &f.artifact, &f.descriptor)
278        };
279        Ok(RequestDoc {
280            schema: schema::REQUEST.into(),
281            scope: scope.clone(),
282            backend_id: backend_id.clone(),
283            artifact: artifact.clone(),
284            descriptor: descriptor.clone(),
285            projection: String::from_utf8(req.projection.clone())
286                .map_err(|_| RequestIdentityError::ProjectionNotUtf8 { step: step.into() })?,
287            output: b.output.clone(),
288            requires: RequiredKind::TypedRecord,
289            normalization: rustev_contract::plan::Normalization::None,
290        })
291    }
292
293    pub(super) fn validate_extract(
294        &self,
295        id: &str,
296        raw: &RawOutput,
297    ) -> Result<Vec<ValidatedField>, Unresolved> {
298        let b = self.extract_binding(id);
299        let option_logprobs = matches!(
300            b.output,
301            OutputKind::TypedRecord {
302                option_logprobs: true,
303                ..
304            }
305        );
306        extraction_validate::validate(
307            &b.schema,
308            &self.extraction.requests[id].vocabularies,
309            raw,
310            b.distributions.clone(),
311            option_logprobs,
312        )
313        .map_err(|e| Unresolved::InvalidBackendOutput {
314            detail: e.to_string(),
315        })
316    }
317
318    pub(super) fn supply_extract(&mut self, id: &str, supplied: Supplied) {
319        let result = match supplied {
320            Supplied::Output(raw) => self.validate_extract(id, &raw),
321            Supplied::Failed(u) => Err(u),
322        };
323        match result {
324            Ok(fields) => {
325                self.complete_extract(id, Some(fields), ExtractionRequestOutcome::Succeeded)
326            }
327            Err(u) => {
328                let reason = match u.kind() {
329                    ReasonKind::BackendUnavailable => RequestFailure::BackendUnavailable,
330                    ReasonKind::DeadlineExceeded => RequestFailure::DeadlineExceeded,
331                    ReasonKind::BudgetExhausted => RequestFailure::BudgetExhausted,
332                    _ => RequestFailure::InvalidBackendOutput,
333                };
334                self.complete_extract(id, None, ExtractionRequestOutcome::Failed { reason });
335            }
336        }
337    }
338
339    fn complete_extract(
340        &mut self,
341        id: &str,
342        fields: Option<Vec<ValidatedField>>,
343        outcome: ExtractionRequestOutcome,
344    ) {
345        self.complete_extract_with_sources(id, fields, outcome, &[]);
346    }
347
348    fn complete_extract_with_sources(
349        &mut self,
350        id: &str,
351        fields: Option<Vec<ValidatedField>>,
352        outcome: ExtractionRequestOutcome,
353        withheld: &[String],
354    ) {
355        let b = self.extract_binding(id).clone();
356        let produced = fields.is_some();
357        let mut fields = fields.unwrap_or_else(|| {
358            let reason = match &outcome {
359                ExtractionRequestOutcome::Failed { reason } => {
360                    AbsenceReason::RequestFailed { reason: *reason }
361                }
362                ExtractionRequestOutcome::Withheld { reason } => *reason,
363                ExtractionRequestOutcome::Succeeded => unreachable!(),
364            };
365            b.schema
366                .fields
367                .iter()
368                .map(|f| ValidatedField {
369                    value: None,
370                    evidence: ExtractionFieldEvidence::new(
371                        f.name.clone(),
372                        &f.ty,
373                        ExtractionOrigin::Absent { reason },
374                        None,
375                        None,
376                        None,
377                        vec![],
378                    ),
379                })
380                .collect()
381        });
382        let mut fallback_reads = BTreeMap::new();
383        let fallback = match &b.fallback {
384            BoundExtractionFallback::None => FallbackIdentity::None,
385            BoundExtractionFallback::Bound {
386                identity,
387                program,
388                fill,
389            } => {
390                let decl = self
391                    .compiled
392                    .plan
393                    .definition
394                    .steps
395                    .iter()
396                    .find(|s| s.id == id)
397                    .expect("extract declaration");
398                let rustev_contract::definition::StepBody::Extract(e) = &decl.body else {
399                    unreachable!()
400                };
401                let bound =
402                    crate::extraction_fallback::bind(program, e).expect("compiled fallback");
403                let owned: BTreeMap<_, _> = program
404                    .sources
405                    .iter()
406                    .filter_map(|s| {
407                        if withheld.iter().any(|r| {
408                            crate::expr::base_ref_v2(r) == crate::expr::base_ref_v2(&s.reference)
409                        }) {
410                            return None;
411                        }
412                        let Value::Text(text) = self.value_and_meta(&s.reference).ok()?.0 else {
413                            return None;
414                        };
415                        Some((s.reference.clone(), text))
416                    })
417                    .collect();
418                let sources = owned
419                    .iter()
420                    .map(|(r, text)| {
421                        (
422                            r.clone(),
423                            crate::extraction_fallback::Source::Readable {
424                                bytes: text.as_bytes(),
425                                derivation: self
426                                    .derivation
427                                    .get(&crate::expr::base_ref_v2(r))
428                                    .copied()
429                                    .unwrap_or(Derivation::ExactDerived),
430                            },
431                        )
432                    })
433                    .collect();
434                let results = bound.evaluate(
435                    &sources,
436                    &self.extraction_vocabularies(id),
437                    self.compiled.plan.definition.limits.max_projection_bytes,
438                );
439                fallback_reads = crate::extraction_fallback::merge(
440                    &mut fields,
441                    &results,
442                    fill.clone(),
443                    produced,
444                );
445                FallbackIdentity::Identity(identity.clone())
446            }
447        };
448        let mut values = BTreeMap::new();
449        for field in &fields {
450            let path = format!("step:{id}/{}", field.evidence.name);
451            self.extraction
452                .distributions
453                .insert(path.clone(), field.evidence.distribution.clone());
454            let reads = fallback_reads
455                .get(&field.evidence.name)
456                .unwrap_or(&b.projection)
457                .iter()
458                .map(|r| crate::expr::base_ref_v2(r))
459                .collect::<Vec<_>>();
460            let mut lineage = self.lineage_of(&reads);
461            lineage.steps.push(id.into());
462            lineage.steps.sort();
463            lineage.steps.dedup();
464            self.lineage.insert(path.clone(), lineage);
465            if let Some(d) = field.evidence.derivation {
466                self.derivation.insert(path, d);
467            }
468            if field.evidence.origin == ExtractionOrigin::Fallback {
469                self.notices.push(Notice {
470                    screening: None,
471                    kind: NoticeKind::Fallback,
472                    subject: format!("{id}/{}", field.evidence.name),
473                    reason: "extraction_fallback".into(),
474                    action: None,
475                });
476            }
477            values.insert(
478                field.evidence.name.clone(),
479                field.value.clone().unwrap_or(Value::Null),
480            );
481        }
482        let value = Value::Record(values);
483        self.exact_values.insert(id.into(), value.clone());
484        self.exact.insert(id.into(), Ok(value));
485        self.derivation
486            .insert(format!("step:{id}"), Derivation::ModelDerived);
487        self.extraction.requests.remove(id);
488        self.extraction.evidence.insert(
489            id.into(),
490            ExtractionEvidence {
491                step: id.into(),
492                template_id: b.template_id,
493                schema_digest: b.schema_digest,
494                artifact: b.artifact,
495                fallback,
496                request_outcome: outcome,
497                fields: fields.into_iter().map(|f| f.evidence).collect(),
498            },
499        );
500    }
501
502    pub(super) fn eval_accept(&mut self, id: &str, a: &AcceptModelDerived) {
503        let base = crate::expr::base_ref_v2(&a.field);
504        let field = if let Some(input) = base.strip_prefix("input:") {
505            input
506        } else {
507            a.field.trim_start_matches("step:")
508        };
509        let source = self.status(&base);
510        let read = self.value_and_meta(&a.field);
511        let extracted = a
512            .field
513            .strip_prefix("step:")
514            .and_then(|p| p.split_once('/'))
515            .and_then(|(step, name)| {
516                self.extraction
517                    .evidence
518                    .get(step)?
519                    .fields
520                    .iter()
521                    .find(|f| f.name == name)
522            })
523            .cloned();
524        let derivation = self
525            .derivation
526            .get(&a.field)
527            .or_else(|| self.derivation.get(&base))
528            .copied()
529            .unwrap_or(Derivation::ExactDerived);
530        let origin = extracted
531            .as_ref()
532            .and_then(|f| match f.origin {
533                ExtractionOrigin::Extracted => Some(AcceptedOrigin::Extracted),
534                ExtractionOrigin::Fallback => Some(AcceptedOrigin::Fallback),
535                ExtractionOrigin::Absent { .. } => None,
536            })
537            .or_else(|| {
538                a.field.starts_with("input:").then(|| {
539                    if self
540                        .observed
541                        .get(base.trim_start_matches("input:"))
542                        .is_some_and(|p| p == &[ProvenanceClass::ModelDerived])
543                    {
544                        AcceptedOrigin::HostExtracted {
545                            producer: self
546                                .extraction
547                                .producers
548                                .get(base.trim_start_matches("input:"))
549                                .cloned(),
550                        }
551                    } else {
552                        AcceptedOrigin::Supplied
553                    }
554                })
555            });
556        let passed = origin == Some(AcceptedOrigin::Supplied);
557        let mut validation = AcceptanceValidation::NotApplied;
558        let mut cause = extracted.as_ref().and_then(|f| match &f.origin {
559            ExtractionOrigin::Absent { reason } => Some(AcceptanceFailure::Absence(*reason)),
560            _ => None,
561        });
562        if read.is_err() && cause.is_none() {
563            cause = Some(AcceptanceFailure::Absence(AbsenceReason::Missing));
564        }
565        if source.is_ok() && read.is_ok() && !passed {
566            if let Validation::Predicate(p) = &a.validate {
567                match crate::expr::eval_predicate(p, &self.scope()) {
568                    Ok(true) => validation = AcceptanceValidation::Passed,
569                    Ok(false) => {
570                        validation = AcceptanceValidation::Failed;
571                        cause = Some(AcceptanceFailure::ValidationFailed);
572                    }
573                    Err(_) => {
574                        validation = AcceptanceValidation::Fault(Unresolved::invalid(
575                            [field.into()],
576                            "acceptance validation fault",
577                        ));
578                        cause = Some(AcceptanceFailure::ValidationFault);
579                    }
580                }
581            }
582            if cause.is_none()
583                && let MinMass::Threshold(m) = &a.min_mass
584            {
585                cause = match self.accept_mass(a, m) {
586                    Some(mass) if mass >= m.at_least.to_f64_nearest() => None,
587                    Some(_) => Some(AcceptanceFailure::MinMassNotMet),
588                    None => Some(AcceptanceFailure::DistributionAbsent),
589                };
590            }
591        }
592        let value_read = read.as_ref().ok().map(|(v, _)| {
593            let digest = rustev_contract::generation_result::generation_value_digest(&v.to_json())
594                .expect("validated core value");
595            // Text and composite input reads are digest-only. Extraction fields
596            // reuse the validator's already retention-filtered representation.
597            let ty = a
598                .field
599                .strip_prefix("step:")
600                .and_then(|p| p.split_once('/'))
601                .and_then(|(s, n)| {
602                    self.extract_binding(s)
603                        .schema
604                        .fields
605                        .iter()
606                        .find(|f| f.name == n)
607                        .map(|f| f.ty.clone())
608                })
609                .unwrap_or_else(|| match v {
610                    Value::Bool(_) => ExtractionType::Bool {},
611                    Value::Integer(n) => ExtractionType::Integer { bounds: [*n, *n] },
612                    Value::Decimal(n) => ExtractionType::Decimal(DecimalBounds {
613                        bounds: [n.to_string(), n.to_string()],
614                        scale: 9,
615                    }),
616                    Value::Enum(_) => ExtractionType::Enum {
617                        vocabulary: String::new(),
618                    },
619                    _ => ExtractionType::Text { max_bytes: 4096 },
620                });
621            let value = extracted
622                .as_ref()
623                .and_then(|f| f.value().cloned())
624                .or_else(|| Some(v.to_json()));
625            AcceptanceRead::new(&ty, digest, value)
626        });
627        let (result, outcome) = if let Err(u) = source {
628            (Err(u), AcceptanceOutcome::Unresolved)
629        } else if let Some(cause) = cause {
630            match &a.on_fail {
631                OnFail::DropField { notice } => {
632                    use rustev_contract::actionability::{
633                        Address, Cause, CauseProvenance, Remediation,
634                    };
635                    self.notices.push(Notice {
636                        screening: None,
637                        kind: NoticeKind::FieldDropped,
638                        subject: id.into(),
639                        reason: notice.clone(),
640                        action: Some(crate::actionable::action(
641                            vec![Cause::Input {
642                                field: field.into(),
643                                provenance: CauseProvenance::Observed {
644                                    class: ProvenanceClass::ModelDerived,
645                                },
646                            }],
647                            crate::actionable::ActionInputs {
648                                caller_controlled: &self.caller_controlled(),
649                                address: &Address::Computed,
650                                declared: None,
651                                computed: Remediation::None,
652                                transient: false,
653                            },
654                        )),
655                    });
656                    (
657                        Ok(Value::Absent(crate::value::Absence {
658                            reason: rustev_contract::grade::AbsentReason::AcceptanceFailed {
659                                cause,
660                            },
661                            predicate: None,
662                        })),
663                        AcceptanceOutcome::Dropped { cause },
664                    )
665                }
666                OnFail::Unresolved => {
667                    let u = if matches!(cause, AcceptanceFailure::Absence(_)) {
668                        Unresolved::missing([field.into()])
669                    } else {
670                        Unresolved::invalid(
671                            [field.into()],
672                            serde_json::to_string(&cause).expect("acceptance cause"),
673                        )
674                    };
675                    (Err(u), AcceptanceOutcome::Unresolved)
676                }
677            }
678        } else {
679            (
680                read.as_ref().map(|(v, _)| v.clone()).map_err(Clone::clone),
681                if passed {
682                    AcceptanceOutcome::PassedThrough
683                } else {
684                    AcceptanceOutcome::Accepted
685                },
686            )
687        };
688        let key = format!("step:{id}");
689        if let Ok(v) = &result {
690            self.exact_values.insert(id.into(), v.clone());
691            if !matches!(v, Value::Null | Value::Absent(_))
692                && let Some(origin) = &origin
693            {
694                self.extraction.origins.insert(key.clone(), origin.clone());
695            }
696        }
697        if let Ok((_, meta)) = read {
698            self.metas.insert(
699                key.clone(),
700                Meta {
701                    node: meta.grade,
702                    whole: meta,
703                },
704            );
705        }
706        self.derivation.insert(key, derivation);
707        self.exact.insert(id.into(), result);
708        self.extraction.acceptance.insert(
709            id.into(),
710            AcceptanceEvidence {
711                step: id.into(),
712                field: a.field.clone(),
713                value_read,
714                origin,
715                outcome,
716                validation,
717            },
718        );
719    }
720
721    fn accept_mass(&mut self, a: &AcceptModelDerived, threshold: &MassThreshold) -> Option<f64> {
722        let (step, name) = a.field.strip_prefix("step:")?.split_once('/')?;
723        let binding = self.extract_binding(step).clone();
724        let ty = &binding.schema.fields.iter().find(|f| f.name == name)?.ty;
725        let options = match ty {
726            ExtractionType::Bool {} => vec!["false".into(), "true".into()],
727            ExtractionType::Enum { vocabulary } => {
728                self.extraction_vocabularies(step).get(vocabulary)?.clone()
729            }
730            _ => return None,
731        };
732        let field = self
733            .extraction
734            .evidence
735            .get_mut(step)?
736            .fields
737            .iter_mut()
738            .find(|f| f.name == name)?;
739        let FieldDistribution::Masses(masses) = self.extraction.distributions.get(&a.field)? else {
740            return None;
741        };
742        let index = options.iter().position(|o| *o == threshold.option)?;
743        match &threshold.basis {
744            MassBasis::UncalibratedThreshold(_) => masses.get(index).copied(),
745            MassBasis::Calibration(cid) => {
746                let artifact = self
747                    .compiled
748                    .info
749                    .extraction_calibrations
750                    .iter()
751                    .find(|c| c.id().is_ok_and(|id| id.to_string() == *cid))?;
752                let id = artifact.id().ok()?;
753                let map = options
754                    .iter()
755                    .cloned()
756                    .zip(masses.iter().copied())
757                    .collect();
758                let calibrated = calibrate::apply(
759                    artifact,
760                    &id,
761                    &BindingCheck {
762                        artifact: &binding.artifact,
763                        task: &format!("{}@{}", binding.template.id, binding.template_id),
764                        question: &format!("{step}/{name}"),
765                        options: &options,
766                    },
767                    CalibrationInput::Distribution(&map),
768                    self.compiled.plan.definition.limits.distribution_tolerance,
769                )
770                .ok()?;
771                let mass = calibrated.mass(&threshold.option);
772                field.distribution = FieldDistribution::Masses(calibrated.masses().to_vec());
773                field.calibration = ExtractionCalibration::Applied(id);
774                mass
775            }
776        }
777    }
778
779    pub(super) fn attribute_outcome(&self, mut outcome: Outcome, trace: &[String]) -> Outcome {
780        use rustev_contract::definition::{OutcomeDecl, ParamSource};
781        if let Outcome::Propose { params, .. } = &mut outcome {
782            for rule in &self.compiled.plan.definition.policy.rules {
783                if !trace.contains(&format!("rule:{}", rule.id)) {
784                    continue;
785                }
786                let OutcomeDecl::Propose {
787                    params: declared, ..
788                } = &rule.then
789                else {
790                    continue;
791                };
792                for p in declared {
793                    let ParamSource::Ref(r) = &p.value else {
794                        continue;
795                    };
796                    let base = crate::expr::base_ref_v2(r);
797                    let origin = self.extraction.origins.get(&base).cloned().or_else(|| {
798                        let name = base.strip_prefix("input:")?;
799                        if self.extraction.evidence.is_empty()
800                            && self.extraction.acceptance.is_empty()
801                        {
802                            return None;
803                        }
804                        self.checks
805                            .records
806                            .iter()
807                            .any(|c| {
808                                c.subject == name
809                                    && matches!(
810                                        c.status,
811                                        rustev_contract::actionability::CheckStatus::Passed
812                                    )
813                            })
814                            .then(|| {
815                                if self
816                                    .observed
817                                    .get(name)
818                                    .is_some_and(|p| p == &[ProvenanceClass::ModelDerived])
819                                {
820                                    AcceptedOrigin::HostExtracted {
821                                        producer: self.extraction.producers.get(name).cloned(),
822                                    }
823                                } else {
824                                    AcceptedOrigin::Supplied
825                                }
826                            })
827                    });
828                    let Some(origin) = origin else {
829                        continue;
830                    };
831                    if let Some(value) = params.get_mut(&p.name) {
832                        *value = OutValue::Attributed {
833                            value: Box::new(value.clone()),
834                            derivation: self.derivation[&base],
835                            origin: origin.clone(),
836                        };
837                    }
838                }
839            }
840        }
841        outcome
842    }
843}