1use 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 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 pub fn extraction_request(&self, step: &str) -> Option<&ExtractRequest> {
199 self.extraction.requests.get(step)
200 }
201
202 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 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 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}