1use std::collections::{BTreeMap, BTreeSet};
11use std::fmt;
12
13use rustev_contract::calibration::CalibrationArtifact;
14use rustev_contract::canonical::canonical_value_bytes;
15use rustev_contract::decimal::Decimal;
16use rustev_contract::definition::{
17 ArtifactPin, BindingChoice, CalibrationRef, Candidates, Cond, Definition, ExcessPolicy,
18 Fallback, Freshness, HandlerAction, InputDecl, ItemSource, Operation, OutcomeDecl, ParamSource,
19 ProvenanceClass, ReasonKind, ReasonSet, RequiredKind, SemanticDecl, StepBody, TypeDecl,
20 UncalibratedThreshold,
21};
22use rustev_contract::descriptor::{BackendDescriptor, InputExcess, OutputKind};
23use rustev_contract::execution::ExecutionPolicy;
24use rustev_contract::ids::{CalibrationId, DescriptorId, PlanId};
25use rustev_contract::judgment::{Derivation, Notice, NoticeKind};
26use rustev_contract::plan::{
27 BoundCalibration, Normalization, Plan, PlanExecution, PlanStep, PlanStepDetail,
28 SemanticBinding, StaticGrade,
29};
30use rustev_contract::source::SourceDescriptor;
31use rustev_contract::{Document, Identified, schema};
32use serde_json::json;
33
34use crate::calibrate;
35use crate::expr::{Ref, StepTy, TyEnv, predicate_refs, predicate_structure, type_predicate};
36use crate::ops::{self, OpArgs};
37use crate::quality;
38use crate::sem::SemKind;
39use crate::value::{Ty, Value};
40
41mod ranking;
42pub(crate) mod screening;
43
44pub const COMPILER: &str = concat!("rustev-core/", env!("CARGO_PKG_VERSION"));
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
49pub enum Category {
50 Parse = 1,
51 UnknownOperator = 2,
52 KindMismatch = 3,
53 Cycle = 4,
54 NoCapableBackend = 5,
55 UncalibratedThreshold = 6,
56 Budget = 7,
57 UnhandledUnresolved = 8,
58 InvalidFallback = 9,
59 CalibrationBinding = 10,
60 InvalidDefinition = 11,
61 InvalidExecution = 12,
63 GradeRequirement = 13,
66 UncorroboratedHint = 14,
69 SourceBinding = 15,
71 UncoveredProvenance = 16,
74 CallerCannotAct = 17,
77 InvalidScreening = 18,
79 UnscreenedText = 19,
81 InvalidAcquisition = 20,
82 SourceMapping = 21,
83 InvalidGeneration = 22,
84 Template = 23,
85 ModelOutputFlow = 24,
86 InvalidExtraction = 25,
87 InvalidAcceptance = 26,
88 UnacceptedModelValue = 27,
89 ExactOrderSource = 28,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
94pub enum ShortfallReason {
95 UnknownBackend,
96 OptionLogprobs,
97 RankMode,
98 MaxCandidates {
99 max: u64,
100 needed: u64,
101 },
102 ListwiseScores,
103 ScreenProfile,
104 ScreenTextLimit {
105 max: u64,
106 needed: u64,
107 },
108 ScreenInputExcess,
109 Operation(Operation),
110 OutputKind {
111 offered: OutputKind,
112 required: RequiredKind,
113 },
114 Options {
115 max: u64,
116 needed: u64,
117 },
118 InputLimit {
119 max: u64,
120 needed: u64,
121 },
122 Determinism {
123 offered: String,
124 required: String,
125 },
126 ArtifactPin {
127 pinned: String,
128 offered: String,
129 },
130 OutputLimit {
131 max: u64,
132 needed: u64,
133 },
134 Language {
135 required: String,
136 },
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct Shortfall {
141 pub backend_id: String,
142 pub reasons: Vec<ShortfallReason>,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct Refusal {
148 pub category: Category,
149 pub subject: String,
151 pub detail: String,
152 pub shortfalls: Vec<Shortfall>,
154}
155
156impl fmt::Display for Refusal {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 write!(
159 f,
160 "refused ({:?}, category {}) at {}: {}",
161 self.category, self.category as u8, self.subject, self.detail
162 )?;
163 for s in &self.shortfalls {
164 write!(f, "; {}: {:?}", s.backend_id, s.reasons)?;
165 }
166 Ok(())
167 }
168}
169
170impl std::error::Error for Refusal {}
171
172fn refuse<T>(
173 category: Category,
174 subject: impl Into<String>,
175 detail: impl Into<String>,
176) -> Result<T, Refusal> {
177 Err(Refusal {
178 category,
179 subject: subject.into(),
180 detail: detail.into(),
181 shortfalls: vec![],
182 })
183}
184
185#[derive(Debug, Clone)]
187pub(crate) enum StepInfo {
188 Ranking,
189 Generate,
190 Extract,
191 Accept(Box<rustev_contract::extraction::AcceptModelDerived>),
192 Acquisition(Box<crate::acquisition::Acquisition>),
193 Exact {
194 args: OpArgs,
195 ty: Ty,
196 },
197 Semantic {
198 decl: Box<SemanticDecl>,
199 kind: SemKind,
200 binding: Option<Box<SemanticBinding>>,
202 calibration: Option<(CalibrationArtifact, CalibrationId)>,
203 },
204 Corroborate {
206 decl: Box<rustev_contract::definition::CorroborateDecl>,
207 ty: Ty,
208 },
209}
210
211#[derive(Debug, Clone)]
212pub(crate) struct Info {
213 pub extraction_calibrations: Vec<CalibrationArtifact>,
214 pub screen_categories: BTreeMap<String, Vec<String>>,
215 pub generation: BTreeMap<String, crate::generation::Checked>,
216 pub inputs: BTreeMap<String, (Ty, InputDecl)>,
217 pub step_ids: Vec<String>,
219 pub steps: BTreeMap<String, StepInfo>,
220 pub deps: BTreeMap<String, Vec<String>>,
223 pub order: Vec<String>,
224 pub v2: bool,
226 #[allow(dead_code)] pub bound: quality::Bound,
229 #[allow(dead_code)] pub trees: BTreeMap<String, quality::Ceil>,
232 pub determinism: BTreeMap<String, rustev_contract::definition::Determinism>,
235}
236
237#[derive(Debug, Clone)]
239pub struct Compiled {
240 pub plan: Plan,
241 pub id: PlanId,
242 pub(crate) info: Info,
243}
244
245impl Compiled {
246 pub fn canonical(&self) -> Vec<u8> {
248 self.plan.canonical().unwrap_or_default()
250 }
251
252 pub fn load(
257 plan: &Plan,
258 descriptors: &[BackendDescriptor],
259 calibrations: &[CalibrationArtifact],
260 ) -> Result<Compiled, Refusal> {
261 Self::load_with_sources(plan, descriptors, calibrations, &[])
262 }
263
264 pub fn load_with_sources(
267 plan: &Plan,
268 descriptors: &[BackendDescriptor],
269 calibrations: &[CalibrationArtifact],
270 sources: &[SourceDescriptor],
271 ) -> Result<Compiled, Refusal> {
272 recompile(plan, descriptors, calibrations, sources).map_err(|e| match e {
273 Recompiled::Refused(r) => r,
274 Recompiled::Differs => Refusal {
275 category: Category::Parse,
276 subject: "plan".into(),
277 detail: "the plan document does not match its recompiled definition".into(),
278 shortfalls: vec![],
279 },
280 })
281 }
282
283 pub fn load_checked(
292 plan: &Plan,
293 descriptors: &[BackendDescriptor],
294 calibrations: &[CalibrationArtifact],
295 ) -> Result<Compiled, LoadError> {
296 Self::load_checked_with_sources(plan, descriptors, calibrations, &[])
297 }
298
299 pub fn load_checked_with_sources(
303 plan: &Plan,
304 descriptors: &[BackendDescriptor],
305 calibrations: &[CalibrationArtifact],
306 sources: &[SourceDescriptor],
307 ) -> Result<Compiled, LoadError> {
308 #[cfg(not(feature = "parameterized"))]
309 crate::unparameterized::plan(plan).map_err(|refusal| LoadError::PlanMismatch {
310 refusal: Some(refusal),
311 })?;
312 let registry_known = plan.registry == schema::EXACT_REGISTRY
315 || (plan.schema == schema::PLAN_V3 && plan.registry == schema::EXACT_REGISTRY_V2);
316 if plan.compiler != COMPILER || !registry_known {
317 return Err(LoadError::CompilerChanged {
318 recorded_compiler: plan.compiler.clone(),
319 current_compiler: COMPILER,
320 recorded_registry: plan.registry.clone(),
321 current_registry: schema::EXACT_REGISTRY,
322 });
323 }
324 let have_d: BTreeSet<DescriptorId> =
325 descriptors.iter().filter_map(|d| d.id().ok()).collect();
326 let have_c: BTreeSet<CalibrationId> =
327 calibrations.iter().filter_map(|c| c.id().ok()).collect();
328 let mut needed: Vec<(String, &DescriptorId)> = vec![];
329 for step in &plan.steps {
330 if let PlanStepDetail::Extract(b) = &step.detail {
331 needed.push((b.backend_id.clone(), &b.descriptor));
332 }
333 if let PlanStepDetail::Rank(b) = &step.detail {
334 needed.push((b.backend_id.clone(), &b.descriptor));
335 }
336 if let PlanStepDetail::Generate(b) = &step.detail {
337 needed.push((b.backend_id.clone(), &b.descriptor));
338 }
339 if let PlanStepDetail::Semantic(b) = &step.detail {
340 needed.push((b.backend_id.clone(), &b.descriptor));
341 if let BoundCalibration::Bound { id, .. } = &b.calibration
342 && !have_c.contains(id)
343 {
344 return Err(LoadError::MissingCalibration {
345 step: step.id.clone(),
346 calibration: id.clone(),
347 });
348 }
349 }
350 }
351 if let PlanExecution::Declared(d) = &plan.execution {
352 needed.extend(
353 d.fallbacks
354 .iter()
355 .map(|f| (f.backend_id.clone(), &f.descriptor)),
356 );
357 }
358 for (backend_id, descriptor) in needed {
359 if !have_d.contains(descriptor) {
360 return Err(LoadError::MissingDescriptor {
361 backend_id,
362 descriptor: descriptor.clone(),
363 });
364 }
365 }
366 recompile(plan, descriptors, calibrations, sources).map_err(|e| LoadError::PlanMismatch {
367 refusal: match e {
368 Recompiled::Refused(r) => Some(r),
369 Recompiled::Differs => None,
370 },
371 })
372 }
373}
374
375#[derive(Debug, Clone, PartialEq, Eq)]
377pub enum LoadError {
378 CompilerChanged {
380 recorded_compiler: String,
381 current_compiler: &'static str,
382 recorded_registry: String,
383 current_registry: &'static str,
384 },
385 MissingDescriptor {
387 backend_id: String,
388 descriptor: DescriptorId,
389 },
390 MissingCalibration {
392 step: String,
393 calibration: CalibrationId,
394 },
395 PlanMismatch { refusal: Option<Refusal> },
398}
399
400impl fmt::Display for LoadError {
401 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402 match self {
403 LoadError::CompilerChanged {
404 recorded_compiler,
405 current_compiler,
406 recorded_registry,
407 current_registry,
408 } => write!(
409 f,
410 "compiler changed: recorded {recorded_compiler} ({recorded_registry}), \
411 current {current_compiler} ({current_registry})"
412 ),
413 LoadError::MissingDescriptor {
414 backend_id,
415 descriptor,
416 } => write!(f, "descriptor {descriptor} of {backend_id} not supplied"),
417 LoadError::MissingCalibration { step, calibration } => {
418 write!(f, "calibration {calibration} of step {step} not supplied")
419 }
420 LoadError::PlanMismatch { refusal: Some(r) } => {
421 write!(f, "plan does not recompile: {r}")
422 }
423 LoadError::PlanMismatch { refusal: None } => {
424 f.write_str("plan does not match its recompiled definition")
425 }
426 }
427 }
428}
429
430impl std::error::Error for LoadError {}
431
432enum Recompiled {
433 Refused(Refusal),
434 Differs,
435}
436
437fn recompile(
439 plan: &Plan,
440 descriptors: &[BackendDescriptor],
441 calibrations: &[CalibrationArtifact],
442 sources: &[SourceDescriptor],
443) -> Result<Compiled, Recompiled> {
444 let execution = match &plan.execution {
445 PlanExecution::None => None,
446 PlanExecution::Declared(d) => Some(&d.policy),
447 };
448 #[cfg(not(feature = "parameterized"))]
449 crate::unparameterized::plan(plan).map_err(Recompiled::Refused)?;
450 #[cfg(feature = "parameterized")]
451 if let Some(record) = &plan.instantiation {
452 crate::instantiate::validate_record(plan, record).map_err(|detail| {
453 Recompiled::Refused(Refusal {
454 category: Category::Parse,
455 subject: "plan".into(),
456 detail,
457 shortfalls: vec![],
458 })
459 })?;
460 }
461 let templates: Vec<_> = plan
462 .steps
463 .iter()
464 .filter_map(|s| match &s.detail {
465 PlanStepDetail::Generate(g) => Some(g.template.clone()),
466 PlanStepDetail::Extract(e) => Some(e.template.clone()),
467 _ => None,
468 })
469 .collect();
470 let programs: Vec<_> = plan
471 .steps
472 .iter()
473 .filter_map(|s| match &s.detail {
474 PlanStepDetail::Extract(e) => match &e.fallback {
475 rustev_contract::extraction_results::BoundExtractionFallback::Bound {
476 program,
477 ..
478 } => Some(program.clone()),
479 _ => None,
480 },
481 _ => None,
482 })
483 .collect();
484 let mut c = compile_generation(
485 &plan.definition,
486 descriptors,
487 calibrations,
488 sources,
489 execution,
490 plan.instantiation.is_some(),
491 (&templates, &programs),
492 )
493 .map_err(Recompiled::Refused)?;
494 #[cfg(feature = "parameterized")]
495 {
496 c.plan.instantiation = plan.instantiation.clone();
497 }
498 c.id = c.plan.id().map_err(|e| {
499 Recompiled::Refused(Refusal {
500 category: Category::Parse,
501 subject: "plan".into(),
502 detail: e.to_string(),
503 shortfalls: vec![],
504 })
505 })?;
506 if plan.canonical().ok() != Some(c.canonical()) {
507 #[cfg(feature = "parameterized")]
508 if plan.instantiation.is_some() {
509 return Err(Recompiled::Refused(Refusal {
510 category: Category::Parse,
511 subject: "plan".into(),
512 detail: "instantiated plan differs from its recompilation".into(),
513 shortfalls: vec![],
514 }));
515 }
516 return Err(Recompiled::Differs);
517 }
518 Ok(c)
519}
520
521pub fn compile_bytes(
523 bytes: &[u8],
524 descriptors: &[BackendDescriptor],
525 calibrations: &[CalibrationArtifact],
526) -> Result<Compiled, Refusal> {
527 #[cfg(not(feature = "parameterized"))]
528 crate::unparameterized::definition(bytes)?;
529 let def = Definition::parse(bytes).map_err(|e| {
530 ranking::parse_refusal(bytes, &e).unwrap_or_else(|| Refusal {
531 category: Category::Parse,
532 subject: "definition".into(),
533 detail: e.to_string(),
534 shortfalls: vec![],
535 })
536 })?;
537 compile(&def, descriptors, calibrations)
538}
539
540fn step_ref(r: &str) -> Option<&str> {
541 r.strip_prefix("step:")
542}
543
544fn is_semver(v: &str) -> bool {
545 let parts: Vec<&str> = v.split('.').collect();
546 parts.len() == 3
547 && parts.iter().all(|p| {
548 !p.is_empty()
549 && p.bytes().all(|b| b.is_ascii_digit())
550 && (p.len() == 1 || !p.starts_with('0'))
551 })
552}
553
554fn valid_name(s: &str) -> bool {
555 !s.is_empty() && !s.contains(':') && !s.chars().any(char::is_whitespace)
556}
557
558fn check_type_decl(t: &TypeDecl) -> Result<(), String> {
559 match t {
560 TypeDecl::Text { max_bytes } if *max_bytes == 0 => {
561 Err("text max_bytes must be positive".into())
562 }
563 TypeDecl::Enum { options } => {
564 let set: BTreeSet<_> = options.iter().collect();
565 if options.is_empty()
566 || set.len() != options.len()
567 || options.iter().any(String::is_empty)
568 {
569 Err("enum options must be non-empty and unique".into())
570 } else {
571 Ok(())
572 }
573 }
574 TypeDecl::List { item, .. } => check_type_decl(item),
575 TypeDecl::Nullable { ty } => check_type_decl(ty),
576 TypeDecl::Map { value, .. } => check_type_decl(value),
577 TypeDecl::Record { fields, .. } => {
578 let set: BTreeSet<_> = fields.iter().map(|f| &f.name).collect();
579 if fields.is_empty()
580 || set.len() != fields.len()
581 || fields.iter().any(|f| f.name.is_empty())
582 {
583 return Err("record fields must be non-empty and unique".into());
584 }
585 fields.iter().try_for_each(|f| check_type_decl(&f.ty))
586 }
587 _ => Ok(()),
588 }
589}
590
591fn kind_valid_for(op: Operation, required: RequiredKind) -> bool {
593 use RequiredKind as R;
594 match op {
595 Operation::Classify => matches!(
596 required,
597 R::Label | R::Distribution | R::CalibratedProbability
598 ),
599 Operation::Proposition => matches!(
600 required,
601 R::Label | R::Distribution | R::CalibratedProbability
602 ),
603 Operation::Rubric => matches!(required, R::Label | R::OrdinalDistribution),
604 Operation::Rank => matches!(required, R::Scores),
605 Operation::Generate | Operation::Extract => false,
606 }
607}
608
609fn satisfies(output: OutputKind, required: RequiredKind) -> bool {
610 use OutputKind as O;
611 use RequiredKind as R;
612 match required {
613 R::ScreenVerdict => output == O::Screen,
614 R::TextRecord | R::TypedRecord => false,
615 R::Label => output == O::Label,
616 R::Distribution | R::CalibratedProbability | R::OrdinalDistribution => {
617 matches!(output, O::Distribution | O::Logits)
618 }
619 R::Scores => matches!(output, O::Scores | O::Logits),
620 }
621}
622
623fn sem_kind(op: Operation, required: RequiredKind) -> SemKind {
624 match (op, required) {
625 (Operation::Rubric, RequiredKind::Label) => SemKind::Ordinal(false),
626 (_, RequiredKind::ScreenVerdict | RequiredKind::TextRecord | RequiredKind::TypedRecord) => {
630 SemKind::Label
631 }
632 (_, RequiredKind::Label) => SemKind::Label,
633 (_, RequiredKind::Distribution) => SemKind::Distribution,
634 (_, RequiredKind::CalibratedProbability) => SemKind::Calibrated,
635 (_, RequiredKind::OrdinalDistribution) => SemKind::Ordinal(true),
636 (_, RequiredKind::Scores) => SemKind::Scores,
637 }
638}
639
640pub(crate) fn check_candidate(
644 d: &SemanticDecl,
645 b: &BackendDescriptor,
646 required: RequiredKind,
647 options_needed: u64,
648 bound: u64,
649) -> Result<OutputKind, Vec<ShortfallReason>> {
650 let mut reasons = Vec::new();
651 let supports: Vec<_> = b
652 .operations
653 .iter()
654 .filter(|o| o.operation == d.operation)
655 .collect();
656 if supports.is_empty() {
657 reasons.push(ShortfallReason::Operation(d.operation));
658 }
659 let chosen = supports.iter().find(|o| {
660 satisfies(o.output.clone(), required)
661 && (required != RequiredKind::ScreenVerdict || d.operation == Operation::Proposition)
662 });
663 match (chosen, supports.first()) {
664 (None, Some(o)) => reasons.push(ShortfallReason::OutputKind {
665 offered: o.output.clone(),
666 required,
667 }),
668 (Some(o), _) if o.max_options < options_needed => reasons.push(ShortfallReason::Options {
669 max: o.max_options,
670 needed: options_needed,
671 }),
672 _ => {}
673 }
674 if bound > b.input_limit.max_bytes && b.input_limit.on_excess == InputExcess::Refuse {
675 reasons.push(ShortfallReason::InputLimit {
676 max: b.input_limit.max_bytes,
677 needed: bound,
678 });
679 }
680 if b.determinism < d.backend.min_determinism {
681 reasons.push(ShortfallReason::Determinism {
682 offered: format!("{:?}", b.determinism),
683 required: format!("{:?}", d.backend.min_determinism),
684 });
685 }
686 if let ArtifactPin::Pinned(a) = &d.backend.artifact
687 && a != &b.artifact
688 {
689 reasons.push(ShortfallReason::ArtifactPin {
690 pinned: a.to_string(),
691 offered: b.artifact.to_string(),
692 });
693 }
694 match chosen {
695 Some(o) if reasons.is_empty() => Ok(o.output.clone()),
696 _ => Err(reasons),
697 }
698}
699
700pub(crate) fn condition_refs(c: &Cond, out: &mut Vec<String>) {
701 match c {
702 Cond::Always => {}
703 Cond::TopLabel { step, .. }
704 | Cond::MassAtLeast { step, .. }
705 | Cond::TopMassBelow { step, .. }
706 | Cond::ExpectationAtLeast { step, .. } => out.push(format!("step:{step}")),
707 Cond::Exact(p) => predicate_refs(p, out),
708 Cond::Empty(r) | Cond::Nonempty(r) => out.push(r.clone()),
709 Cond::All(cs) | Cond::Any(cs) => cs.iter().for_each(|c| condition_refs(c, out)),
710 Cond::Not(c) => condition_refs(c, out),
711 Cond::ProvenanceWithin { reference, .. } => out.push(reference.clone()),
712 }
713}
714
715pub(crate) fn outcome_refs(o: &OutcomeDecl, out: &mut Vec<String>) {
716 match o {
717 OutcomeDecl::Propose { params, .. } => {
718 for p in params {
719 if let ParamSource::Ref(r) = &p.value {
720 out.push(r.clone());
721 }
722 }
723 }
724 OutcomeDecl::Escalate { .. } => {}
725 OutcomeDecl::MissingEvidenceFrom { step } => out.push(format!("step:{step}")),
726 OutcomeDecl::NeedsInput { .. } => {}
731 }
732}
733
734pub fn compile(
738 def: &Definition,
739 descriptors: &[BackendDescriptor],
740 calibrations: &[CalibrationArtifact],
741) -> Result<Compiled, Refusal> {
742 compile_inner(def, descriptors, calibrations, &[], None)
743}
744
745pub fn compile_with_sources(
750 def: &Definition,
751 descriptors: &[BackendDescriptor],
752 calibrations: &[CalibrationArtifact],
753 sources: &[SourceDescriptor],
754 execution: Option<&ExecutionPolicy>,
755) -> Result<Compiled, Refusal> {
756 compile_inner(def, descriptors, calibrations, sources, execution)
757}
758
759pub fn compile_with(
763 def: &Definition,
764 descriptors: &[BackendDescriptor],
765 calibrations: &[CalibrationArtifact],
766 execution: &ExecutionPolicy,
767) -> Result<Compiled, Refusal> {
768 compile_inner(def, descriptors, calibrations, &[], Some(execution))
769}
770
771fn compile_inner(
772 def: &Definition,
773 descriptors: &[BackendDescriptor],
774 calibrations: &[CalibrationArtifact],
775 sources: &[SourceDescriptor],
776 execution: Option<&ExecutionPolicy>,
777) -> Result<Compiled, Refusal> {
778 compile_instantiated(def, descriptors, calibrations, sources, execution, false)
779}
780
781pub(crate) fn compile_instantiated(
783 def: &Definition,
784 descriptors: &[BackendDescriptor],
785 calibrations: &[CalibrationArtifact],
786 sources: &[SourceDescriptor],
787 execution: Option<&ExecutionPolicy>,
788 #[cfg_attr(not(feature = "parameterized"), allow(unused_variables))] instantiated: bool,
789) -> Result<Compiled, Refusal> {
790 compile_generation(
791 def,
792 descriptors,
793 calibrations,
794 sources,
795 execution,
796 instantiated,
797 (&[], &[]),
798 )
799}
800
801pub fn compile_with_templates(
803 def: &Definition,
804 descriptors: &[BackendDescriptor],
805 calibrations: &[CalibrationArtifact],
806 sources: &[SourceDescriptor],
807 execution: Option<&ExecutionPolicy>,
808 templates: &[rustev_contract::generation::PromptTemplate],
809) -> Result<Compiled, Refusal> {
810 compile_generation(
811 def,
812 descriptors,
813 calibrations,
814 sources,
815 execution,
816 false,
817 (templates, &[]),
818 )
819}
820
821pub fn compile_with_extraction_documents(
824 def: &Definition,
825 descriptors: &[BackendDescriptor],
826 calibrations: &[CalibrationArtifact],
827 sources: &[SourceDescriptor],
828 execution: Option<&ExecutionPolicy>,
829 templates: &[rustev_contract::generation::PromptTemplate],
830 programs: &[rustev_contract::extraction_documents::ExtractRules],
831) -> Result<Compiled, Refusal> {
832 compile_generation(
833 def,
834 descriptors,
835 calibrations,
836 sources,
837 execution,
838 false,
839 (templates, programs),
840 )
841}
842
843fn compile_generation(
844 def: &Definition,
845 descriptors: &[BackendDescriptor],
846 calibrations: &[CalibrationArtifact],
847 sources: &[SourceDescriptor],
848 execution: Option<&ExecutionPolicy>,
849 instantiated: bool,
850 documents: (
851 &[rustev_contract::generation::PromptTemplate],
852 &[rustev_contract::extraction_documents::ExtractRules],
853 ),
854) -> Result<Compiled, Refusal> {
855 #[cfg(not(feature = "parameterized"))]
856 let _ = instantiated;
857 use Category as C;
858 let (templates, programs) = documents;
859
860 #[cfg(not(feature = "parameterized"))]
864 crate::unparameterized::typed_definition(def)?;
865 let v2 = def.schema == schema::DEFINITION_V2;
866 if def.schema != schema::DEFINITION && !v2 {
867 return refuse(
868 C::Parse,
869 "definition",
870 format!(
871 "schema {:?} is neither {} nor {}",
872 def.schema,
873 schema::DEFINITION,
874 schema::DEFINITION_V2
875 ),
876 );
877 }
878 let decode = if v2 { ops::decode_v2 } else { ops::decode };
879 let mut decoded: BTreeMap<String, OpArgs> = BTreeMap::new();
880 for s in &def.steps {
881 if let StepBody::Exact(e) = &s.body {
882 match decode(&e.op, e.version, &e.args) {
883 Some(Ok(a)) => {
884 decoded.insert(s.id.clone(), a);
885 }
886 Some(Err(msg)) => {
887 return refuse(
888 C::Parse,
889 format!("step:{}", s.id),
890 format!("arguments of {}@{}: {msg}", e.op, e.version),
891 );
892 }
893 None => {}
894 }
895 }
896 }
897 for d in descriptors {
898 if d.schema != schema::BACKEND {
899 return refuse(
900 C::Parse,
901 format!("backend:{}", d.backend_id),
902 "descriptor schema is not rustev.backend/1",
903 );
904 }
905 }
906 for c in calibrations {
907 if c.schema != schema::CALIBRATION {
908 return refuse(
909 C::Parse,
910 "calibration",
911 "calibration schema is not rustev.calibration/1",
912 );
913 }
914 }
915
916 if def
917 .limits
918 .acquisition
919 .as_ref()
920 .is_some_and(|b| !(1..=4096).contains(&b.max_calls))
921 {
922 return refuse(C::Parse, "limits", "acquisition.max_calls outside 1..=4096");
923 }
924 if !v2 && def.limits.acquisition.is_some() {
925 return refuse(
926 C::InvalidDefinition,
927 "limits",
928 "requires-definition-2: acquisition budget",
929 );
930 }
931 if !v2 && def.screening.is_some() {
933 return refuse(
934 C::InvalidDefinition,
935 "screening",
936 "requires-definition-2: screening needs rustev.definition/2",
937 );
938 }
939 for s in &def.steps {
941 if let StepBody::Semantic(d) = &s.body
942 && d.requires == RequiredKind::ScreenVerdict
943 {
944 return refuse(
945 C::InvalidDefinition,
946 format!("step:{}", s.id),
947 "screen_verdict is reserved for lowered screening steps",
948 );
949 }
950 }
951 for s in &def.steps {
956 if !v2
957 && matches!(
958 s.body,
959 StepBody::Acquire(_)
960 | StepBody::Widen(_)
961 | StepBody::Generate(_)
962 | StepBody::Extract(_)
963 | StepBody::AcceptModelDerived(_)
964 | StepBody::Rank(_)
965 | StepBody::Select(_)
966 )
967 {
968 return refuse(
969 C::InvalidDefinition,
970 format!("step:{}", s.id),
971 "requires-definition-2",
972 );
973 }
974 }
975 ranking::structure(def, &decoded)?;
976 for s in &def.steps {
977 crate::acquisition::structure(&s.id, &s.body)?;
978 }
979 if !v2
980 && def.policy.on_unresolved.iter().any(|h| {
981 matches!(&h.reasons,
982 ReasonSet::Only(rs) if rs.contains(&ReasonKind::Skipped))
983 })
984 {
985 return refuse(
986 C::InvalidDefinition,
987 "policy",
988 "requires-definition-2: skipped",
989 );
990 }
991 if !v2 && let Some((subject, construct)) = crate::actionable::first_construct(def) {
994 return refuse(
995 C::InvalidDefinition,
996 subject,
997 format!(
998 "requires-definition-2: {construct} needs {}",
999 schema::DEFINITION_V2
1000 ),
1001 );
1002 }
1003 if v2 {
1007 #[cfg(feature = "parameterized")]
1008 if instantiated {
1009 quality::structure_with_origin(def, &decoded, true)?;
1010 } else {
1011 quality::structure(def, &decoded)?;
1012 }
1013 #[cfg(not(feature = "parameterized"))]
1014 quality::structure(def, &decoded)?;
1015 } else if let Some((subject, construct)) = first_v2_construct(def, &decoded) {
1016 return refuse(
1017 C::InvalidDefinition,
1018 subject,
1019 format!(
1020 "requires_definition_2: {construct} needs {}",
1021 schema::DEFINITION_V2
1022 ),
1023 );
1024 }
1025 crate::generation::isolation(def, execution)?;
1026 crate::generation::structure(def)?;
1027 crate::extraction_compile::structure(def)?;
1028 let mut backend_ids = BTreeSet::new();
1030 for d in descriptors {
1031 if !backend_ids.insert(d.backend_id.as_str()) {
1032 return refuse(
1033 C::InvalidDefinition,
1034 format!("backend:{}", d.backend_id),
1035 "two descriptors share a backend id",
1036 );
1037 }
1038 }
1039 let inv = |subject: String, detail: String| -> Result<(), Refusal> {
1040 refuse(C::InvalidDefinition, subject, detail)
1041 };
1042 if def.name.is_empty() || def.package.is_empty() {
1043 inv(
1044 "definition".into(),
1045 "name and package must be non-empty".into(),
1046 )?;
1047 }
1048 if !is_semver(&def.version) {
1049 inv(
1050 "definition".into(),
1051 format!("version {:?} is not MAJOR.MINOR.PATCH", def.version),
1052 )?;
1053 }
1054 let mut inputs: BTreeMap<String, (Ty, InputDecl)> = BTreeMap::new();
1055 for i in &def.inputs {
1056 let subject = format!("input:{}", i.name);
1057 if !valid_name(&i.name) || inputs.contains_key(&i.name) {
1058 inv(
1059 subject.clone(),
1060 "input names must be unique, non-empty, without ':' or whitespace".into(),
1061 )?;
1062 }
1063 let classes: BTreeSet<_> = i.provenance.iter().collect();
1064 if i.provenance.is_empty() || classes.len() != i.provenance.len() {
1065 inv(
1066 subject.clone(),
1067 "accepted provenance classes must be non-empty and unique".into(),
1068 )?;
1069 }
1070 check_type_decl(&i.ty).or_else(|e| inv(subject, e))?;
1071 inputs.insert(i.name.clone(), (Ty::from_decl(&i.ty), i.clone()));
1072 }
1073 let ids: Vec<String> = def.steps.iter().map(|s| s.id.clone()).collect();
1074 let known = |r: &str| -> Result<(), String> {
1075 match Ref::parse(r)? {
1076 Ref::Input(n, _) if !inputs.contains_key(&n) => Err(format!("unknown input {n:?}")),
1077 Ref::Step(n) if v2 && !ids.contains(&crate::expr::split_step(&n).0) => {
1079 Err(format!("unknown step {n:?}"))
1080 }
1081 Ref::Step(n) if !v2 && !ids.contains(&n) => Err(format!("unknown step {n:?}")),
1082 _ => Ok(()),
1083 }
1084 };
1085 let ranking_kinds: BTreeSet<&str> = def
1086 .steps
1087 .iter()
1088 .filter(|s| matches!(s.body, StepBody::Rank(_) | StepBody::Select(_)))
1089 .map(|s| s.id.as_str())
1090 .collect();
1091 let mut options_of: BTreeMap<String, Vec<String>> = def
1092 .steps
1093 .iter()
1094 .filter_map(|s| {
1095 if let StepBody::Semantic(d) = &s.body {
1096 Some((s.id.clone(), d.options.clone()))
1097 } else {
1098 None
1099 }
1100 })
1101 .collect();
1102 options_of.extend(crate::extraction_compile::distribution_options(def));
1103 for (i, s) in def.steps.iter().enumerate() {
1104 let subject = format!("step:{}", s.id);
1105 if !valid_name(&s.id) || ids[..i].contains(&s.id) {
1106 inv(
1107 subject.clone(),
1108 "step ids must be unique, non-empty, without ':' or whitespace".into(),
1109 )?;
1110 }
1111 match &s.body {
1112 StepBody::Extract(_) | StepBody::AcceptModelDerived(_) => {
1113 }
1115 StepBody::Rank(_) | StepBody::Select(_) => {
1116 for r in ranking::refs(&s.body) {
1117 known(&r).or_else(|e| inv(subject.clone(), e))?;
1118 }
1119 }
1120 StepBody::Generate(_) => {}
1121 StepBody::Acquire(_) | StepBody::Widen(_) => {
1124 for r in crate::acquisition::refs(&s.body) {
1125 known(&r).or_else(|e| inv(subject.clone(), e))?;
1126 }
1127 }
1128 StepBody::Corroborate(c) => {
1129 let mut refs = vec![c.claim.clone()];
1130 c.by.iter()
1131 .for_each(|k| predicate_refs(&k.predicate, &mut refs));
1132 for r in refs {
1133 known(&r).or_else(|e| inv(subject.clone(), e))?;
1134 }
1135 }
1136 StepBody::Exact(_) => {
1137 if let Some(a) = decoded.get(&s.id) {
1138 a.structure().or_else(|e| inv(subject.clone(), e))?;
1139 for r in a.all_ref_strings() {
1140 known(&r).or_else(|e| inv(subject.clone(), e))?;
1141 }
1142 }
1143 }
1144 StepBody::Semantic(d) => {
1145 if d.task.is_empty() || d.question.is_empty() {
1146 inv(
1147 subject.clone(),
1148 "task and question must be non-empty".into(),
1149 )?;
1150 }
1151 let set: BTreeSet<_> = d.options.iter().collect();
1152 if set.len() != d.options.len() || d.options.iter().any(String::is_empty) {
1153 inv(
1154 subject.clone(),
1155 "options must be unique and non-empty".into(),
1156 )?;
1157 }
1158 match d.operation {
1159 Operation::Classify if d.options.len() < 2 => inv(
1160 subject.clone(),
1161 "classify needs at least two options".into(),
1162 )?,
1163 Operation::Proposition if d.options != ["false", "true"] => inv(
1164 subject.clone(),
1165 "proposition options must be [\"false\", \"true\"]".into(),
1166 )?,
1167 Operation::Rubric if d.options.len() < 2 => {
1168 inv(subject.clone(), "rubric needs at least two levels".into())?
1169 }
1170 Operation::Rank if !d.options.is_empty() => {
1171 inv(subject.clone(), "rank takes candidates, not options".into())?
1172 }
1173 _ => {}
1174 }
1175 match (&d.candidates, d.operation) {
1176 (Candidates::From(r), Operation::Rank) => {
1177 if step_ref(r).is_none() {
1178 inv(subject.clone(), format!("candidates {r:?} must be a step"))?;
1179 }
1180 known(r).or_else(|e| inv(subject.clone(), e))?;
1181 }
1182 (Candidates::None, Operation::Rank) => {
1183 inv(subject.clone(), "rank needs candidates".into())?
1184 }
1185 (Candidates::From(_), _) => {
1186 inv(subject.clone(), "only rank takes candidates".into())?
1187 }
1188 (Candidates::None, _) => {}
1189 }
1190 if !kind_valid_for(d.operation, d.requires) {
1191 inv(
1192 subject.clone(),
1193 format!("{:?} cannot require {:?}", d.operation, d.requires),
1194 )?;
1195 }
1196 if d.for_each.len() > 2 {
1197 inv(subject.clone(), "at most two fan-out indices".into())?;
1198 }
1199 let mut binds = BTreeSet::new();
1200 for f in &d.for_each {
1201 if !valid_name(&f.bind) || !binds.insert(f.bind.clone()) {
1202 inv(
1203 subject.clone(),
1204 format!("bind {:?} must be unique and valid", f.bind),
1205 )?;
1206 }
1207 if step_ref(&f.over).is_none() {
1208 inv(
1209 subject.clone(),
1210 format!("for_each over {:?} must be a step", f.over),
1211 )?;
1212 }
1213 known(&f.over).or_else(|e| inv(subject.clone(), e))?;
1214 if let ItemSource::List { list, .. } = &f.items {
1215 known(list).or_else(|e| inv(subject.clone(), e))?;
1216 }
1217 }
1218 for r in &d.project {
1219 match Ref::parse(r) {
1220 Ok(Ref::Bind(b)) if !binds.contains(&b) => {
1221 inv(subject.clone(), format!("unknown bind {b:?}"))?
1222 }
1223 Ok(Ref::Bind(_)) => {}
1224 Ok(Ref::Input(_, p)) if p.is_empty() => {
1225 known(r).or_else(|e| inv(subject.clone(), e))?
1226 }
1227 Ok(Ref::Step(_)) => known(r).or_else(|e| inv(subject.clone(), e))?,
1228 Ok(_) => inv(subject.clone(), format!("{r:?} cannot be projected"))?,
1229 Err(e) => inv(subject.clone(), e)?,
1230 }
1231 }
1232 if matches!(d.calibration, CalibrationRef::Id(_))
1233 && d.requires != RequiredKind::CalibratedProbability
1234 {
1235 inv(
1236 subject.clone(),
1237 "a calibration is bound only to a calibrated_probability step".into(),
1238 )?;
1239 }
1240 if let BindingChoice::Backend(b) = &d.backend.binding
1241 && b.is_empty()
1242 {
1243 inv(subject.clone(), "binding names no backend".into())?;
1244 }
1245 if let Fallback::Kind { reason, .. } = &d.fallback
1246 && reason.is_empty()
1247 {
1248 inv(subject.clone(), "a fallback states its reason".into())?;
1249 }
1250 }
1251 }
1252 }
1253 if def.limits.distribution_tolerance <= Decimal::ZERO
1254 || def.limits.distribution_tolerance > Decimal::parse("0.1").unwrap_or(Decimal::ZERO)
1255 {
1256 inv(
1257 "limits".into(),
1258 "distribution_tolerance must be in (0, 0.1]".into(),
1259 )?;
1260 }
1261 let policy = &def.policy;
1263 for (i, h) in policy.on_unresolved.iter().enumerate() {
1264 let subject = format!("policy:on_unresolved[{i}]");
1265 match Ref::parse(&h.target) {
1266 Ok(Ref::Input(_, p)) if p.is_empty() => {
1267 known(&h.target).or_else(|e| inv(subject.clone(), e))?
1268 }
1269 Ok(Ref::Step(_)) => known(&h.target).or_else(|e| inv(subject.clone(), e))?,
1270 _ => inv(
1271 subject.clone(),
1272 format!("handler target {:?} must be an input or step", h.target),
1273 )?,
1274 }
1275 if matches!(&h.reasons, ReasonSet::Only(v) if v.is_empty()) {
1276 inv(subject.clone(), "handler covers no reason".into())?;
1277 }
1278 if matches!(&h.action, HandlerAction::Escalate { reason } if reason.is_empty()) {
1279 inv(subject, "an escalation states its reason".into())?;
1280 }
1281 }
1282 let mut rule_ids = BTreeSet::new();
1283 let mut proposed_params: BTreeSet<String> = BTreeSet::new();
1284 for r in &policy.rules {
1285 let subject = format!("policy:rule:{}", r.id);
1286 if !valid_name(&r.id) || !rule_ids.insert(r.id.clone()) {
1287 inv(subject.clone(), "rule ids must be unique and valid".into())?;
1288 }
1289 cond_structure(&r.when, &options_of, &known, &ranking_kinds)
1290 .or_else(|e| inv(subject.clone(), e))?;
1291 let mut refs = Vec::new();
1292 outcome_refs(&r.then, &mut refs);
1293 for x in &refs {
1294 known(x).or_else(|e| inv(subject.clone(), e))?;
1295 }
1296 match &r.then {
1297 OutcomeDecl::Propose { action, params } => {
1298 let names: BTreeSet<_> = params.iter().map(|p| &p.name).collect();
1299 if action.is_empty()
1300 || names.len() != params.len()
1301 || params.iter().any(|p| p.name.is_empty())
1302 {
1303 inv(
1304 subject.clone(),
1305 "a proposal has an action and unique parameter names".into(),
1306 )?;
1307 }
1308 proposed_params.extend(params.iter().map(|p| p.name.clone()));
1309 }
1310 OutcomeDecl::Escalate { reason } if reason.is_empty() => {
1311 inv(subject.clone(), "an escalation states its reason".into())?
1312 }
1313 _ => {}
1314 }
1315 }
1316 match policy.rules.last() {
1317 Some(r) if r.when == Cond::Always => {}
1318 _ => inv(
1319 "policy".into(),
1320 "the last rule's condition must be `always`".into(),
1321 )?,
1322 }
1323 let mut adj_ids = BTreeSet::new();
1324 for a in &policy.adjustments {
1325 let subject = format!("policy:adjustment:{}", a.id);
1326 if !valid_name(&a.id) || !adj_ids.insert(a.id.clone()) {
1327 inv(
1328 subject.clone(),
1329 "adjustment ids must be unique and valid".into(),
1330 )?;
1331 }
1332 let scale: BTreeSet<_> = a.scale.iter().collect();
1333 if a.scale.len() < 2 || scale.len() != a.scale.len() {
1334 inv(
1335 subject.clone(),
1336 "a scale has at least two unique positions".into(),
1337 )?;
1338 }
1339 if !proposed_params.contains(&a.param) {
1340 inv(
1341 subject.clone(),
1342 format!("no proposal has parameter {:?}", a.param),
1343 )?;
1344 }
1345 cond_structure(&a.when, &options_of, &known, &ranking_kinds)
1346 .or_else(|e| inv(subject, e))?;
1347 }
1348 if v2 {
1351 let mut taken: BTreeSet<String> = ids.iter().cloned().collect();
1352 taken.extend(rule_ids.iter().cloned());
1353 taken.extend(adj_ids.iter().cloned());
1354 crate::actionable::structure(def, &inputs, &taken)?;
1355 }
1356
1357 let authored = def;
1358 let screens = screening::validate(def)?;
1359 let lowered = screening::lower(def, &screens, &mut decoded);
1360 let def = &lowered;
1361 let ids: Vec<String> = def.steps.iter().map(|s| s.id.clone()).collect();
1362
1363 let calibration_ids: Vec<(CalibrationId, &CalibrationArtifact)> = calibrations
1365 .iter()
1366 .map(|c| c.id().map(|id| (id, c)))
1367 .collect::<Result<_, _>>()
1368 .map_err(|e| Refusal {
1369 category: C::Parse,
1370 subject: "calibration".into(),
1371 detail: e.to_string(),
1372 shortfalls: vec![],
1373 })?;
1374 let mut resolved_cal: BTreeMap<String, (CalibrationArtifact, CalibrationId)> = BTreeMap::new();
1375 for s in &def.steps {
1376 let subject = format!("step:{}", s.id);
1377 match &s.body {
1378 StepBody::Extract(_) | StepBody::AcceptModelDerived(_) => {
1379 }
1381 StepBody::Rank(_) | StepBody::Select(_) => {}
1382 StepBody::Generate(_) => {}
1383 StepBody::Acquire(_) | StepBody::Widen(_) => {
1384 crate::acquisition::resolve(&s.id, &s.body, sources)?;
1385 }
1386 StepBody::Corroborate(_) => {}
1387 StepBody::Exact(e) => {
1388 let (registered, registry) = if v2 {
1389 (
1390 ops::is_registered_v2(&e.op, e.version),
1391 schema::EXACT_REGISTRY_V2,
1392 )
1393 } else {
1394 (ops::is_registered(&e.op, e.version), schema::EXACT_REGISTRY)
1395 };
1396 if !registered {
1397 return refuse(
1398 C::UnknownOperator,
1399 subject,
1400 format!("{}@{} is not in {registry}", e.op, e.version),
1401 );
1402 }
1403 }
1404 StepBody::Semantic(d) => {
1405 if d.requires != RequiredKind::CalibratedProbability {
1406 continue;
1407 }
1408 let CalibrationRef::Id(cid) = &d.calibration else {
1409 return refuse(
1410 C::CalibrationBinding,
1411 subject,
1412 "a calibrated_probability step names no calibration",
1413 );
1414 };
1415 let Some((_, art)) = calibration_ids.iter().find(|(id, _)| id == cid) else {
1416 return refuse(
1417 C::CalibrationBinding,
1418 subject,
1419 format!("calibration {cid} was not supplied"),
1420 );
1421 };
1422 calibrate::check_static(art, &d.task, &s.id, &d.options).map_err(|e| Refusal {
1423 category: C::CalibrationBinding,
1424 subject: subject.clone(),
1425 detail: e.to_string(),
1426 shortfalls: vec![],
1427 })?;
1428 resolved_cal.insert(s.id.clone(), ((*art).clone(), cid.clone()));
1429 }
1430 }
1431 }
1432
1433 let prompts = crate::generation::resolve(def, templates)?;
1434 crate::extraction_compile::resolve(def, programs)?;
1435
1436 let mut bound = if v2 {
1439 quality::bind_sources(def, sources)?
1440 } else {
1441 quality::Bound::default()
1442 };
1443
1444 let mut deps: BTreeMap<String, Vec<String>> = BTreeMap::new();
1446 for s in &def.steps {
1447 let mut refs: Vec<String> = match &s.body {
1448 StepBody::Extract(e) => e
1449 .projection
1450 .iter()
1451 .cloned()
1452 .chain(e.template.params.values().filter_map(|b| match b {
1453 rustev_contract::generation::TemplateBinding::Reference(r) => Some(r.clone()),
1454 _ => None,
1455 }))
1456 .collect(),
1457 StepBody::AcceptModelDerived(a) => crate::extraction_compile::accept_refs(a)
1458 .into_iter()
1459 .filter(|r| {
1460 let base = base_ref_v2(r);
1463 base.strip_prefix("step:")
1464 .is_none_or(|id| ids.iter().any(|s| s == id))
1465 })
1466 .collect(),
1467 StepBody::Rank(_) | StepBody::Select(_) => ranking::refs(&s.body),
1468 StepBody::Generate(g) => crate::generation::dependencies(g, def),
1469 StepBody::Acquire(_) | StepBody::Widen(_) => crate::acquisition::refs(&s.body),
1470 StepBody::Corroborate(c) => corroborate_refs(c),
1471 StepBody::Exact(_) => decoded[&s.id].refs(),
1472 StepBody::Semantic(d) => {
1473 let mut v = Vec::new();
1474 if let Candidates::From(r) = &d.candidates {
1475 v.push(r.clone());
1476 }
1477 for f in &d.for_each {
1478 v.push(f.over.clone());
1479 if let ItemSource::List { list, .. } = &f.items {
1480 v.push(list.clone());
1481 }
1482 }
1483 v.extend(
1484 d.project
1485 .iter()
1486 .filter(|r| !r.starts_with("bind:"))
1487 .cloned(),
1488 );
1489 v
1490 }
1491 };
1492 if v2 {
1493 refs = refs.iter().map(|r| base_ref_v2(r)).collect();
1495 }
1496 let mut seen = BTreeSet::new();
1497 refs.retain(|r| seen.insert(r.clone()));
1498 deps.insert(s.id.clone(), refs);
1499 }
1500 screening::dependencies(authored, &screens, &decoded, &mut deps);
1501 let order = topo_order(&ids, &deps)?;
1502
1503 let input_tys: BTreeMap<String, Ty> = inputs
1505 .iter()
1506 .map(|(k, (t, _))| (k.clone(), t.clone()))
1507 .collect();
1508 crate::extraction_compile::question_selections(def)?;
1509 crate::extraction_compile::model_gate(def)?;
1510 let mut step_tys: BTreeMap<String, StepTy> = BTreeMap::new();
1511 for (field, _) in crate::extraction_compile::distribution_options(def) {
1517 step_tys.insert(field, StepTy::Semantic(SemKind::Distribution, vec![]));
1518 }
1519 let mut infos: BTreeMap<String, StepInfo> = BTreeMap::new();
1520 let mut projection_bounds: BTreeMap<String, u64> = BTreeMap::new();
1521 let mut max_requests: BTreeMap<String, u64> = BTreeMap::new();
1522 let by_id: BTreeMap<&str, &rustev_contract::definition::StepDecl> =
1523 def.steps.iter().map(|s| (s.id.as_str(), s)).collect();
1524 let mut acquisition_derivations = BTreeMap::new();
1525 for id in &order {
1526 let s = by_id[id.as_str()];
1527 let subject = format!("step:{id}");
1528 let env = TyEnv {
1529 inputs: &input_tys,
1530 steps: &step_tys,
1531 item: None,
1532 v2,
1533 };
1534 let km = |e: String| Refusal {
1535 category: C::KindMismatch,
1536 subject: subject.clone(),
1537 detail: e,
1538 shortfalls: vec![],
1539 };
1540 match &s.body {
1541 StepBody::Extract(e) => {
1542 step_tys.insert(
1543 id.clone(),
1544 StepTy::Exact(crate::extraction_compile::record_ty(e, &input_tys)),
1545 );
1546 infos.insert(id.clone(), StepInfo::Extract);
1547 }
1548 StepBody::AcceptModelDerived(a) => {
1549 let ty = crate::extraction_compile::accept(
1550 id,
1551 a,
1552 def,
1553 &env,
1554 &acquisition_derivations,
1555 calibrations,
1556 )?;
1557 step_tys.insert(id.clone(), StepTy::Exact(ty));
1558 infos.insert(id.clone(), StepInfo::Accept(a.clone()));
1559 }
1560 StepBody::Rank(_) | StepBody::Select(_) => {
1561 let (ty, bytes) = ranking::types(id, &s.body, &env, &acquisition_derivations)?;
1562 projection_bounds.insert(id.clone(), bytes);
1563 step_tys.insert(id.clone(), ty);
1564 infos.insert(id.clone(), StepInfo::Ranking);
1565 }
1566 StepBody::Generate(_) => {
1567 infos.insert(id.clone(), StepInfo::Generate);
1568 }
1569 StepBody::Acquire(_) | StepBody::Widen(_) => {
1570 let a = crate::acquisition::compile(
1571 id,
1572 &s.body,
1573 sources,
1574 &env,
1575 &acquisition_derivations,
1576 def,
1577 )?;
1578 step_tys.insert(id.clone(), StepTy::Exact(a.ty.clone()));
1579 infos.insert(id.clone(), StepInfo::Acquisition(Box::new(a)));
1580 }
1581 StepBody::Corroborate(c) => {
1582 let ty = crate::expr::ref_ty(&c.claim, &env).map_err(km)?;
1584 for check in &c.by {
1585 type_predicate(&check.predicate, &env).map_err(km)?;
1586 }
1587 step_tys.insert(id.clone(), StepTy::Exact(ty.clone()));
1588 infos.insert(
1589 id.clone(),
1590 StepInfo::Corroborate {
1591 decl: Box::new(c.clone()),
1592 ty,
1593 },
1594 );
1595 }
1596 StepBody::Exact(_) => {
1597 let args = decoded[id].clone();
1598 if let OpArgs::MergeFields(m) = &args {
1599 let overlay = m
1601 .overlay
1602 .strip_prefix("step:")
1603 .map(|s| crate::expr::split_step(s).0);
1604 if let Some(OpArgs::JoinKeyed(j)) =
1605 overlay.as_deref().and_then(|o| decoded.get(o))
1606 && j.left != m.base
1607 {
1608 return refuse(
1609 C::InvalidDefinition,
1610 subject,
1611 format!("the overlay joins {}, not the base list {}", j.left, m.base),
1612 );
1613 }
1614 if let Some(e) = args.merge_rule_error(&env) {
1615 return refuse(C::InvalidDefinition, subject, e);
1616 }
1617 }
1618 let ty = args.type_check(&env).map_err(km)?;
1619 step_tys.insert(id.clone(), StepTy::Exact(ty.clone()));
1620 infos.insert(id.clone(), StepInfo::Exact { args, ty });
1621 }
1622 StepBody::Semantic(d) => {
1623 let mut requests: u64 = 1;
1624 let mut instance_bytes: u64 = 0;
1625 let mut bind_items = BTreeMap::new();
1626 for f in &d.for_each {
1627 let t = crate::expr::ref_ty(&f.over, &env).map_err(km)?;
1628 let (max, id_bytes) = Value::id_bounds(&t)
1629 .ok_or_else(|| km(format!("{} yields {}, not ids", f.over, t.name())))?;
1630 requests = requests.saturating_mul(max);
1631 instance_bytes = instance_bytes
1632 .saturating_add(2 + 6 * f.bind.len() as u64)
1633 .saturating_add(2 + 6 * id_bytes)
1634 .saturating_add(2);
1635 if let ItemSource::List { list, id_field } = &f.items {
1636 match crate::expr::ref_ty(list, &env).map_err(km)? {
1637 Ty::List { item, .. } => match item.as_ref() {
1638 Ty::Record(fields)
1639 if fields.iter().any(|(n, t)| {
1640 n == id_field && matches!(t, Ty::Text { .. } | Ty::Enum(_))
1641 }) =>
1642 {
1643 bind_items.insert(f.bind.clone(), (*item).clone());
1644 }
1645 _ => {
1646 return Err(km(format!(
1647 "{list} has no text id field {id_field:?}"
1648 )));
1649 }
1650 },
1651 t => return Err(km(format!("{list} is {}, not a list", t.name()))),
1652 }
1653 }
1654 }
1655 let mut candidate_bytes = 0u64;
1656 if let Candidates::From(r) = &d.candidates {
1657 let t = crate::expr::ref_ty(r, &env).map_err(km)?;
1658 let (max, id_bytes) = Value::id_bounds(&t)
1659 .ok_or_else(|| km(format!("{r} yields {}, not ids", t.name())))?;
1660 candidate_bytes = max.saturating_mul(2 + 6 * id_bytes + 1);
1661 }
1662 let mut value_bytes = 0u64;
1663 for r in &d.project {
1664 let t = match Ref::parse(r).map_err(km)? {
1665 Ref::Bind(b) if d.requires == RequiredKind::ScreenVerdict => {
1666 let (bind, field) = b
1667 .split_once('/')
1668 .ok_or_else(|| km("invalid lowered field".into()))?;
1669 match bind_items.get(bind) {
1670 Some(Ty::Record(fields)) => fields
1671 .iter()
1672 .find(|(name, _)| name == field)
1673 .map(|(_, ty)| ty.clone())
1674 .ok_or_else(|| km("unknown lowered field".into()))?,
1675 _ => return Err(km("invalid lowered item".into())),
1676 }
1677 }
1678 Ref::Bind(b) => bind_items.get(&b).cloned().ok_or_else(|| {
1679 km(format!("bind {b:?} has no item source to project"))
1680 })?,
1681 Ref::Input(n, path) => crate::expr::ref_ty(
1682 &format!("input:{}", [vec![n], path].concat().join("/")),
1683 &env,
1684 )
1685 .map_err(km)?,
1686 Ref::Step(n) => {
1687 let base = if v2 { crate::expr::split_step(&n).0 } else { n };
1688 match step_tys.get(&base) {
1689 Some(StepTy::ModelRanking(_)) => {
1690 return Err(km(format!(
1691 "{r} is a model ranking; only exact values are projected"
1692 )));
1693 }
1694 Some(StepTy::Semantic(..)) => {
1695 return Err(km(format!(
1696 "{r} is semantic; only exact values are projected"
1697 )));
1698 }
1699 Some(StepTy::Selection(_)) | Some(StepTy::Exact(_)) => {}
1700 None => return Err(km(format!("{r} is not a declared step"))),
1701 }
1702 let t = crate::expr::ref_ty(r, &env).map_err(km)?;
1703 if matches!(
1704 t,
1705 Ty::Filtered { .. } | Ty::Shortlist { .. } | Ty::Ranking { .. }
1706 ) {
1707 return Err(km(format!(
1708 "{r} is a filter, shortlist or ranking summary; project its ids through for_each instead"
1709 )));
1710 }
1711 t
1712 }
1713 _ => return Err(km(format!("{r} cannot be projected"))),
1714 };
1715 value_bytes = value_bytes
1716 .saturating_add(2 + 6 * r.len() as u64 + 1)
1717 .saturating_add(t.max_json_bytes())
1718 .saturating_add(1);
1719 }
1720 let envelope = json!({
1721 "candidates": [], "instance": {}, "operation": d.operation, "options": d.options,
1722 "question": d.question, "task": d.task, "values": {},
1723 });
1724 let base = canonical_value_bytes(&envelope)
1725 .map(|b| b.len() as u64)
1726 .unwrap_or(u64::MAX);
1727 let bound = base
1728 .saturating_add(candidate_bytes)
1729 .saturating_add(instance_bytes)
1730 .saturating_add(value_bytes);
1731 let bound = screens
1732 .iter()
1733 .find(|f| &f.step == id)
1734 .map_or(bound, |f| screening::projection_bound(f.max_bytes));
1735 projection_bounds.insert(id.clone(), bound);
1736 max_requests.insert(id.clone(), requests);
1737 let kind = sem_kind(d.operation, d.requires);
1738 let binds: Vec<String> = d.for_each.iter().map(|f| f.bind.clone()).collect();
1739 step_tys.insert(id.clone(), StepTy::Semantic(kind, binds));
1740 infos.insert(
1741 id.clone(),
1742 StepInfo::Semantic {
1743 decl: Box::new(d.clone()),
1744 kind,
1745 binding: None,
1746 calibration: resolved_cal.get(id).cloned(),
1747 },
1748 );
1749 }
1750 }
1751 acquisition_derivations = static_derivations(
1752 def,
1753 &inputs,
1754 &order[..=order.iter().position(|x| x == id).unwrap()],
1755 &deps,
1756 &infos,
1757 );
1758 }
1759
1760 crate::extraction_compile::types(def, &input_tys, &step_tys)?;
1761 crate::extraction_compile::templates(
1762 def,
1763 &prompts,
1764 &input_tys,
1765 &step_tys,
1766 &acquisition_derivations,
1767 )?;
1768 crate::extraction_compile::budget(def, &input_tys, &step_tys, execution)?;
1769 ranking::consumers(def, &deps, &step_tys, &decoded)?;
1770 screening::coverage(authored, &screens, &decoded)?;
1771 let mut generated = crate::generation::type_check(
1772 def,
1773 &prompts,
1774 &input_tys,
1775 &step_tys,
1776 &acquisition_derivations,
1777 &deps,
1778 )?;
1779
1780 crate::actionable::types(def, &input_tys, v2)?;
1782
1783 let mut trees: BTreeMap<String, quality::Ceil> = BTreeMap::new();
1787 let mut grade_table: Vec<StaticGrade> = Vec::new();
1788 if v2 {
1789 for (name, (_, decl)) in &inputs {
1790 trees.insert(
1791 format!("input:{name}"),
1792 quality::input_tree(&decl.ty, bound.inputs.get(name), None),
1793 );
1794 }
1795 for id in &order {
1796 let tree = match &infos[id] {
1797 StepInfo::Ranking | StepInfo::Generate | StepInfo::Extract => {
1798 quality::Ceil::leaf(rustev_contract::grade::ValueGrade::Ungraded, false)
1799 }
1800 StepInfo::Accept(a) => quality::tree_at(&trees, &a.field, &[]),
1801 StepInfo::Acquisition(a) => a.tree.clone(),
1802 StepInfo::Exact { args, .. } => quality::step_tree(args, &trees),
1803 StepInfo::Semantic { decl, .. } => quality::semantic_tree(decl, &trees),
1804 StepInfo::Corroborate { decl, .. } => quality::corroborate_tree(decl, &trees),
1805 };
1806 trees.insert(format!("step:{id}"), tree);
1807 }
1808 for s in &def.steps {
1809 let subject = format!("step:{}", s.id);
1810 let found = match &infos[&s.id] {
1811 StepInfo::Acquisition(a) => a
1812 .reads
1813 .iter()
1814 .find_map(|r| quality::requirement_refusal(&subject, r, &trees)),
1815 StepInfo::Exact { args, .. } => quality::op_reads(args)
1816 .iter()
1817 .filter(|r| r.gate || r.require.is_some())
1818 .find_map(|r| quality::requirement_refusal(&subject, r, &trees)),
1819 StepInfo::Corroborate { decl, .. } => {
1820 quality::corroborate_refusal(&subject, decl, &trees)
1821 }
1822 StepInfo::Ranking
1823 | StepInfo::Generate
1824 | StepInfo::Extract
1825 | StepInfo::Accept(_)
1826 | StepInfo::Semantic { .. } => None,
1827 };
1828 if let Some(r) = found {
1829 return Err(r);
1830 }
1831 }
1832 for (subject, c) in quality::policy_conds(def) {
1833 let mut reads = Vec::new();
1834 quality::cond_reads(c, &mut reads);
1835 if let Some(r) = reads
1836 .iter()
1837 .find_map(|r| quality::requirement_refusal(&subject, r, &trees))
1838 {
1839 return Err(r);
1840 }
1841 }
1842 if !bound.inputs.is_empty()
1843 || infos
1844 .values()
1845 .any(|i| matches!(i, StepInfo::Acquisition(_)))
1846 {
1847 for name in inputs.keys() {
1848 let key = format!("input:{name}");
1849 trees[&key].walk(&key, &mut grade_table);
1850 }
1851 for s in &def.steps {
1852 let key = format!("step:{}", s.id);
1853 trees[&key].walk(&key, &mut grade_table);
1854 }
1855 }
1856 }
1857
1858 let mut sorted: Vec<&BackendDescriptor> = descriptors.iter().collect();
1860 sorted.sort_by(|a, b| a.backend_id.as_bytes().cmp(b.backend_id.as_bytes()));
1861
1862 let mut notices: Vec<Notice> = Vec::new();
1863 let mut details: BTreeMap<String, PlanStepDetail> = BTreeMap::new();
1864 let mut determinism = BTreeMap::new();
1865 let mut bind_ctx: BTreeMap<String, (u64, u64)> = BTreeMap::new();
1866 for s in &def.steps {
1867 let subject = format!("step:{}", s.id);
1868 let d = match &s.body {
1869 StepBody::Extract(e) => {
1870 let binding = crate::extraction_compile::bind(
1871 &s.id,
1872 e,
1873 crate::extraction_compile::needs_logprobs(def, &s.id),
1874 &prompts[&s.id],
1875 programs,
1876 &TyEnv {
1877 inputs: &input_tys,
1878 steps: &step_tys,
1879 item: None,
1880 v2,
1881 },
1882 &sorted,
1883 )?;
1884 details.insert(s.id.clone(), PlanStepDetail::Extract(Box::new(binding)));
1885 continue;
1886 }
1887 StepBody::AcceptModelDerived(a) => {
1888 details.insert(s.id.clone(), PlanStepDetail::AcceptModelDerived(a.clone()));
1889 continue;
1890 }
1891 StepBody::Rank(r) => {
1892 let StepTy::ModelRanking(pool) = step_tys[&s.id] else {
1893 unreachable!()
1894 };
1895 let detail = ranking::bind(&s.id, r, pool, projection_bounds[&s.id], &sorted)?;
1896 if let PlanStepDetail::Unsupported { capability } = &detail {
1897 notices.push(Notice {
1898 screening: None,
1899 kind: NoticeKind::Fallback,
1900 subject: subject.clone(),
1901 reason: format!("unsupported: {capability}"),
1902 action: None,
1903 });
1904 }
1905 if let PlanStepDetail::Rank(binding) = &detail
1906 && let Some(backend) =
1907 sorted.iter().find(|b| b.backend_id == binding.backend_id)
1908 {
1909 determinism.insert(s.id.clone(), backend.determinism);
1910 }
1911 details.insert(s.id.clone(), detail);
1912 continue;
1913 }
1914 StepBody::Select(select) => {
1915 details.insert(s.id.clone(), PlanStepDetail::Select(select.clone()));
1916 continue;
1917 }
1918 StepBody::Generate(g) => {
1919 let binding = crate::generation::bind(&s.id, g, &generated[&s.id], &sorted)?;
1920 if let Some(checked) = generated.get_mut(&s.id) {
1921 checked.binding = binding.clone();
1922 }
1923 details.insert(s.id.clone(), PlanStepDetail::Generate(Box::new(binding)));
1924 continue;
1925 }
1926 StepBody::Semantic(d) => d,
1927 StepBody::Exact(e) => {
1928 details.insert(
1929 s.id.clone(),
1930 PlanStepDetail::Exact {
1931 op: e.op.clone(),
1932 version: e.version,
1933 },
1934 );
1935 continue;
1936 }
1937 StepBody::Acquire(_) | StepBody::Widen(_) => {
1938 details.insert(
1939 s.id.clone(),
1940 if matches!(s.body, StepBody::Acquire(_)) {
1941 PlanStepDetail::Acquire
1942 } else {
1943 PlanStepDetail::Widen
1944 },
1945 );
1946 continue;
1947 }
1948 StepBody::Corroborate(_) => {
1949 details.insert(s.id.clone(), PlanStepDetail::Corroborate);
1950 continue;
1951 }
1952 };
1953 let bound = projection_bounds[&s.id];
1954 let options_needed = match &d.candidates {
1955 Candidates::From(r) => match &step_tys[step_ref(r).unwrap_or_default()] {
1956 StepTy::Exact(t) => Value::id_bounds(t).map(|(m, _)| m).unwrap_or(0),
1957 StepTy::ModelRanking(_) | StepTy::Semantic(..) => 0,
1958 StepTy::Selection(ty) => Value::id_bounds(ty).map_or(0, |(n, _)| n),
1959 },
1960 Candidates::None => d.options.len() as u64,
1961 };
1962 bind_ctx.insert(s.id.clone(), (options_needed, bound));
1963 let candidates: Vec<&BackendDescriptor> = match &d.backend.binding {
1964 BindingChoice::Backend(b) => match sorted.iter().find(|x| &x.backend_id == b) {
1965 Some(x) => vec![*x],
1966 None => {
1967 return Err(Refusal {
1968 category: C::NoCapableBackend,
1969 subject,
1970 detail: format!("bound backend {b:?} was not supplied"),
1971 shortfalls: vec![Shortfall {
1972 backend_id: b.clone(),
1973 reasons: vec![ShortfallReason::UnknownBackend],
1974 }],
1975 });
1976 }
1977 },
1978 BindingChoice::Auto => sorted.clone(),
1979 };
1980 let consumers_ok = |k: SemKind| consumers_accept(def, &s.id, k);
1981 if let Fallback::Kind { requires, .. } = &d.fallback {
1982 if !kind_valid_for(d.operation, *requires) {
1983 return refuse(
1984 C::InvalidFallback,
1985 subject,
1986 format!("{:?} cannot fall back to {:?}", d.operation, requires),
1987 );
1988 }
1989 if *requires == RequiredKind::CalibratedProbability {
1990 return refuse(
1991 C::InvalidFallback,
1992 subject,
1993 "a fallback cannot require calibration",
1994 );
1995 }
1996 if let Err(e) = consumers_ok(sem_kind(d.operation, *requires)) {
1997 return refuse(
1998 C::InvalidFallback,
1999 subject,
2000 format!("consumers cannot accept the fallback kind: {e}"),
2001 );
2002 }
2003 }
2004 let evaluate =
2005 |required: RequiredKind| -> (Option<(&BackendDescriptor, OutputKind)>, Vec<Shortfall>) {
2006 let mut shortfalls = Vec::new();
2007 for b in &candidates {
2008 match screening::candidate(
2009 screens.iter().find(|f| f.step == s.id),
2010 d,
2011 b,
2012 required,
2013 options_needed,
2014 bound,
2015 ) {
2016 Ok(o) => return (Some((*b, o)), vec![]),
2017 Err(reasons) => shortfalls.push(Shortfall {
2018 backend_id: b.backend_id.clone(),
2019 reasons,
2020 }),
2021 }
2022 }
2023 (None, shortfalls)
2024 };
2025 let (found, shortfalls) = evaluate(d.requires);
2026 let (backend, output, requires, fallback_taken) = match (found, &d.fallback) {
2027 (Some((b, o)), _) => (b, o, d.requires, false),
2028 (None, Fallback::None) => {
2029 return Err(Refusal {
2030 category: C::NoCapableBackend,
2031 subject,
2032 detail: format!(
2033 "no backend satisfies {:?} {:?}, and no fallback is declared",
2034 d.operation, d.requires
2035 ),
2036 shortfalls,
2037 });
2038 }
2039 (None, Fallback::Unsupported) => {
2040 let capability = format!("{:?}:{:?}", d.operation, d.requires).to_lowercase();
2041 notices.push(Notice {
2042 screening: None,
2043 kind: NoticeKind::Fallback,
2044 subject: subject.clone(),
2045 reason: format!("unsupported: {capability}"),
2046 action: None,
2047 });
2048 details.insert(s.id.clone(), PlanStepDetail::Unsupported { capability });
2049 continue;
2050 }
2051 (None, Fallback::Kind { requires, reason }) => match evaluate(*requires) {
2052 (Some((b, o)), _) => {
2053 notices.push(Notice {
2054 screening: None,
2055 kind: NoticeKind::Fallback,
2056 subject: subject.clone(),
2057 reason: reason.clone(),
2058 action: None,
2059 });
2060 (b, o, *requires, true)
2061 }
2062 (None, fb_shortfalls) => {
2063 return Err(Refusal {
2064 category: C::InvalidFallback,
2065 subject,
2066 detail: format!(
2067 "neither {:?} nor the fallback {:?} is satisfied",
2068 d.requires, requires
2069 ),
2070 shortfalls: fb_shortfalls,
2071 });
2072 }
2073 },
2074 };
2075 determinism.insert(s.id.clone(), backend.determinism);
2076 let calibration = match (requires, resolved_cal.get(&s.id)) {
2077 (RequiredKind::CalibratedProbability, Some((art, cid))) => {
2078 if art.binding.artifact != backend.artifact {
2079 return refuse(
2080 C::CalibrationBinding,
2081 subject,
2082 format!(
2083 "calibration is bound to {}, the backend is {}",
2084 art.binding.artifact, backend.artifact
2085 ),
2086 );
2087 }
2088 BoundCalibration::Bound {
2089 id: cid.clone(),
2090 binding: art.binding.clone(),
2091 }
2092 }
2093 _ => BoundCalibration::None,
2094 };
2095 let normalization = match (&output, requires) {
2096 (
2097 OutputKind::Logits,
2098 RequiredKind::Distribution | RequiredKind::OrdinalDistribution,
2099 ) => Normalization::Softmax1,
2100 _ => Normalization::None,
2101 };
2102 let descriptor_id = backend.id().map_err(|e| Refusal {
2103 category: C::Parse,
2104 subject: subject.clone(),
2105 detail: e.to_string(),
2106 shortfalls: vec![],
2107 })?;
2108 let binding = SemanticBinding {
2109 backend_id: backend.backend_id.clone(),
2110 artifact: backend.artifact.clone(),
2111 descriptor: descriptor_id,
2112 output,
2113 requires,
2114 normalization,
2115 calibration,
2116 fallback_taken,
2117 max_requests: max_requests[&s.id],
2118 max_projection_bytes: bound,
2119 };
2120 if let Some(StepInfo::Semantic {
2121 binding: b,
2122 kind,
2123 calibration: cal,
2124 ..
2125 }) = infos.get_mut(&s.id)
2126 {
2127 *kind = sem_kind(d.operation, requires);
2128 if requires != RequiredKind::CalibratedProbability {
2129 *cal = None;
2130 }
2131 *b = Some(Box::new(binding.clone()));
2132 step_tys.insert(
2133 s.id.clone(),
2134 StepTy::Semantic(*kind, d.for_each.iter().map(|f| f.bind.clone()).collect()),
2135 );
2136 }
2137 details.insert(s.id.clone(), PlanStepDetail::Semantic(Box::new(binding)));
2138 }
2139
2140 crate::extraction_compile::accept_bindings(def, calibrations, &details, &mut notices)?;
2141
2142 let plan_execution = match execution {
2145 None => PlanExecution::None,
2146 Some(policy) => crate::execution::validate(policy, def, &sorted, &details, &bind_ctx)?,
2147 };
2148
2149 screening::budget(authored, &screens, descriptors)?;
2151 crate::acquisition::budget(def, sources, execution)?;
2152 let mut total: u64 = crate::generation::budget(def, &details)?;
2153 total += def
2154 .steps
2155 .iter()
2156 .filter(|s| matches!(s.body, StepBody::Extract(_)))
2157 .count() as u64;
2158 for s in &def.steps {
2159 if let Some(PlanStepDetail::Extract(b)) = details.get(&s.id)
2160 && b.worst_case_request_bytes > def.limits.max_projection_bytes
2161 {
2162 return refuse(
2163 C::Budget,
2164 format!("step:{}", s.id),
2165 "extraction request exceeds max_projection_bytes",
2166 );
2167 }
2168 if let StepBody::Rank(r) = &s.body {
2169 let StepTy::ModelRanking(pool) = step_tys[&s.id] else {
2170 unreachable!()
2171 };
2172 let count = ranking::budget(&s.id, r, pool)?;
2173 if matches!(details[&s.id], PlanStepDetail::Rank(_)) {
2174 total = total.saturating_add(count);
2175 }
2176 if projection_bounds[&s.id] > def.limits.max_projection_bytes {
2177 return refuse(
2178 C::Budget,
2179 format!("step:{}", s.id),
2180 "rank projection exceeds max_projection_bytes",
2181 );
2182 }
2183 }
2184 if let Some(PlanStepDetail::Semantic(b)) = details.get(&s.id) {
2185 total = total.saturating_add(b.max_requests);
2186 if b.max_projection_bytes > def.limits.max_projection_bytes {
2187 return refuse(
2188 C::Budget,
2189 format!("step:{}", s.id),
2190 format!(
2191 "worst-case projection {} bytes exceeds max_projection_bytes {}",
2192 b.max_projection_bytes, def.limits.max_projection_bytes
2193 ),
2194 );
2195 }
2196 }
2197 }
2198 if total > def.limits.max_semantic_requests {
2199 match def.limits.on_excess {
2200 ExcessPolicy::Refuse => {
2201 return refuse(
2202 C::Budget,
2203 "limits",
2204 format!(
2205 "worst-case {total} semantic requests exceed max_semantic_requests {} without truncate_visible",
2206 def.limits.max_semantic_requests
2207 ),
2208 );
2209 }
2210 ExcessPolicy::TruncateVisible => notices.push(Notice {
2211 screening: None,
2212 kind: NoticeKind::Truncation,
2213 subject: "limits".into(),
2214 reason: format!(
2215 "worst case {total} requests; at most {} are issued",
2216 def.limits.max_semantic_requests
2217 ),
2218 action: None,
2219 }),
2220 }
2221 }
2222
2223 if v2 && let Err(detail) = quality::evidence_bound(def, &infos) {
2226 return refuse(C::Budget, "limits", format!("evidence-bound: {detail}"));
2227 }
2228
2229 let mut reasons = possible_reasons(def, &inputs, &order, &infos, &details);
2231 if v2 {
2232 extend_reasons_v2(
2233 def,
2234 &inputs,
2235 &order,
2236 &infos,
2237 &deps,
2238 &trees,
2239 &details,
2240 &mut reasons,
2241 );
2242 }
2243 check_policy(def, &input_tys, &step_tys, &reasons, &mut notices, v2)?;
2244
2245 let actionability = crate::actionable::phase_h2(def, &inputs, &deps, &reasons)?;
2248
2249 let derivations = static_derivations(def, &inputs, &order, &deps, &infos);
2251 let registry = if v2
2254 && (uses_exact_v2(def, &decoded)
2255 || def
2256 .steps
2257 .iter()
2258 .any(|s| matches!(s.body, StepBody::AcceptModelDerived(_))))
2259 {
2260 schema::EXACT_REGISTRY_V2
2261 } else {
2262 schema::EXACT_REGISTRY
2263 };
2264 let definition_id = authored.id().map_err(|e| Refusal {
2265 category: C::Parse,
2266 subject: "definition".into(),
2267 detail: e.to_string(),
2268 shortfalls: vec![],
2269 })?;
2270 let acquisitions: Vec<_> = def
2271 .steps
2272 .iter()
2273 .filter_map(|s| match &infos[&s.id] {
2274 StepInfo::Acquisition(a) => {
2275 use rustev_contract::acquisition_record::AcquisitionPossibleOutcome as O;
2276 let mut plan = a.plan.clone();
2277 for (reason, outcome) in [
2278 (ReasonKind::Skipped, O::Skipped),
2279 (ReasonKind::InsufficientGrade, O::InsufficientGrade),
2280 ] {
2281 if reasons[&format!("step:{}", s.id)].contains(&reason)
2282 && !plan.outcomes.contains(&outcome)
2283 {
2284 plan.outcomes.push(outcome);
2285 }
2286 }
2287 Some(plan)
2288 }
2289 _ => None,
2290 })
2291 .collect();
2292 for a in &acquisitions {
2293 for b in &a.sources {
2294 if !bound.sources.iter().any(|s| s.name == b.source.name) {
2295 bound.sources.push(b.source.clone());
2296 }
2297 }
2298 }
2299 bound.sources.sort_by(|a, b| a.name.cmp(&b.name));
2300 let plan = Plan {
2301 acquisitions: (!acquisitions.is_empty()).then_some(acquisitions),
2302 schema: if v2 { schema::PLAN_V3 } else { schema::PLAN }.into(),
2303 compiler: COMPILER.into(),
2304 registry: registry.into(),
2305 definition_id,
2306 definition: authored.clone(),
2307 order: order.clone(),
2308 steps: def
2309 .steps
2310 .iter()
2311 .map(|s| PlanStep {
2312 id: s.id.clone(),
2313 derivation: derivations[&s.id],
2314 detail: details[&s.id].clone(),
2315 })
2316 .collect(),
2317 notices,
2318 execution: plan_execution,
2319 sources: (!bound.sources.is_empty()).then(|| bound.sources.clone()),
2320 grades: (!grade_table.is_empty()).then_some(grade_table),
2321 actionability,
2322 instantiation: None,
2323 screening: screening::plan(authored, &screens, &details),
2324 };
2325 let id = plan.id().map_err(|e| Refusal {
2326 category: C::Parse,
2327 subject: "plan".into(),
2328 detail: e.to_string(),
2329 shortfalls: vec![],
2330 })?;
2331 Ok(Compiled {
2332 plan,
2333 id,
2334 info: Info {
2335 extraction_calibrations: calibrations.to_vec(),
2336 screen_categories: descriptors
2337 .iter()
2338 .filter_map(|d| {
2339 d.operations
2340 .iter()
2341 .find_map(|op| op.screen.as_ref())
2342 .map(|p| (d.backend_id.clone(), p.categories.clone()))
2343 })
2344 .collect(),
2345 generation: generated,
2346 inputs,
2347 step_ids: ids,
2348 steps: infos,
2349 deps,
2350 order,
2351 v2,
2352 bound,
2353 trees,
2354 determinism,
2355 },
2356 })
2357}
2358
2359fn uses_exact_v2(def: &Definition, decoded: &BTreeMap<String, OpArgs>) -> bool {
2362 decoded
2363 .values()
2364 .any(|a| matches!(a, OpArgs::JoinKeyed(_) | OpArgs::MergeFields(_)) || a.uses_v2())
2365 || def.steps.iter().any(|s| match &s.body {
2366 StepBody::Acquire(_) | StepBody::Widen(_) => true,
2367 StepBody::Corroborate(c) => {
2368 c.by.iter()
2369 .any(|k| crate::expr::predicate_uses_v2(&k.predicate))
2370 }
2371 _ => false,
2372 })
2373 || quality::policy_conds(def).any(|(_, c)| first_cond_v2(c))
2374}
2375
2376fn first_cond_v2(c: &Cond) -> bool {
2377 match c {
2378 Cond::Exact(p) => crate::expr::predicate_uses_v2(p),
2379 Cond::All(cs) | Cond::Any(cs) => cs.iter().any(first_cond_v2),
2380 Cond::Not(c) => first_cond_v2(c),
2381 _ => false,
2382 }
2383}
2384
2385fn base_ref_v2(r: &str) -> String {
2386 crate::expr::base_ref_v2(r)
2387}
2388
2389fn may_be_absent(t: &TypeDecl) -> bool {
2391 match t {
2392 TypeDecl::Nullable { .. } => true,
2393 TypeDecl::Record {
2394 present_when: Some(_),
2395 ..
2396 } => true,
2397 TypeDecl::Record { fields, .. } => fields.iter().any(|f| may_be_absent(&f.ty)),
2398 TypeDecl::List { item, .. } => may_be_absent(item),
2399 TypeDecl::Map { value, .. } => may_be_absent(value),
2400 _ => false,
2401 }
2402}
2403
2404#[allow(clippy::too_many_arguments)]
2409fn extend_reasons_v2(
2410 def: &Definition,
2411 inputs: &BTreeMap<String, (Ty, InputDecl)>,
2412 order: &[String],
2413 infos: &BTreeMap<String, StepInfo>,
2414 deps: &BTreeMap<String, Vec<String>>,
2415 trees: &BTreeMap<String, quality::Ceil>,
2416 details: &BTreeMap<String, PlanStepDetail>,
2417 reasons: &mut BTreeMap<String, BTreeSet<ReasonKind>>,
2418) {
2419 use ReasonKind as K;
2420 let graded_gate = |r: &quality::Read| {
2421 r.require.is_some() || (r.gate && quality::read_ceiling(r, trees).own.graded().is_some())
2422 };
2423 let mut absent: BTreeSet<String> = BTreeSet::new();
2424 for (name, (_, decl)) in inputs {
2425 let key = format!("input:{name}");
2426 if may_be_absent(&decl.ty) {
2427 absent.insert(key.clone());
2428 }
2429 if matches!(
2430 decl.on_disagreement,
2431 Some(rustev_contract::definition::Disagreement::Abstain)
2432 ) {
2433 reasons.entry(key).or_default().insert(K::Abstained);
2434 }
2435 }
2436 for id in order {
2437 let key = format!("step:{id}");
2438 let mut add: BTreeSet<ReasonKind> = BTreeSet::new();
2439 match &infos[id] {
2440 StepInfo::Exact { args, .. } => {
2441 let reads = quality::op_reads(args);
2442 let reads_absent = reads.iter().any(|r| absent.contains(&r.base));
2443 match args {
2444 OpArgs::JoinKeyed(_) | OpArgs::MergeFields(_) => {
2445 absent.insert(key.clone());
2446 }
2447 OpArgs::FilterWithReasons(_) | OpArgs::TopK(_) | OpArgs::FieldPresence(_) => {}
2449 _ if reads_absent => {
2450 add.insert(K::MissingEvidence);
2451 }
2452 _ => {}
2453 }
2454 if reads.iter().any(graded_gate) {
2455 add.insert(K::InsufficientGrade);
2456 }
2457 if !matches!(args, OpArgs::FieldPresence(_)) {
2458 for d in &deps[id] {
2459 let mut inherited = reasons.get(d).cloned().unwrap_or_default();
2460 if probe_only(args, d) {
2461 inherited.remove(&K::Skipped);
2462 }
2463 add.extend(inherited);
2464 }
2465 }
2466 }
2467 StepInfo::Corroborate { decl, .. } => {
2468 let (claim, _) = quality::claim_path(decl);
2469 if absent.contains(&claim) {
2470 absent.insert(key.clone());
2471 }
2472 add.extend(reasons.get(&claim).cloned().unwrap_or_default());
2473 match decl.on_contradiction {
2478 rustev_contract::definition::OnContradiction::Conflict => {
2479 add.insert(K::Conflict);
2480 }
2481 rustev_contract::definition::OnContradiction::Abstain => {
2482 add.insert(K::Abstained);
2483 }
2484 rustev_contract::definition::OnContradiction::TreatAsHint => {}
2485 }
2486 }
2487 StepInfo::Accept(a) => {
2488 add.extend(
2489 reasons
2490 .get(&base_ref_v2(&a.field))
2491 .cloned()
2492 .unwrap_or_default(),
2493 );
2494 if matches!(
2495 a.on_fail,
2496 rustev_contract::extraction::OnFail::DropField { .. }
2497 ) {
2498 absent.insert(key.clone());
2499 }
2500 }
2501 StepInfo::Ranking => {
2502 let body = &def.steps.iter().find(|s| &s.id == id).unwrap().body;
2503 let updated = ranking::reasons(body, &details[id], reasons);
2504 reasons.insert(key, updated);
2505 continue;
2506 }
2507 StepInfo::Generate | StepInfo::Extract => {}
2508 StepInfo::Acquisition(_) | StepInfo::Semantic { .. } => {
2509 for d in &deps[id] {
2510 add.extend(reasons.get(d).cloned().unwrap_or_default());
2511 }
2512 }
2513 }
2514 reasons.entry(key).or_default().extend(add);
2515 }
2516 for (_, c) in quality::policy_conds(def) {
2517 let mut reads = Vec::new();
2518 quality::cond_reads(c, &mut reads);
2519 for r in &reads {
2520 if graded_gate(r) {
2521 reasons
2522 .entry(r.base.clone())
2523 .or_default()
2524 .insert(K::InsufficientGrade);
2525 }
2526 if absent.contains(&r.base) {
2527 reasons
2528 .entry(r.base.clone())
2529 .or_default()
2530 .insert(K::MissingEvidence);
2531 }
2532 }
2533 }
2534}
2535
2536fn first_v2_construct(
2540 def: &Definition,
2541 decoded: &BTreeMap<String, OpArgs>,
2542) -> Option<(String, &'static str)> {
2543 fn type_v2(t: &TypeDecl) -> Option<&'static str> {
2544 match t {
2545 TypeDecl::Nullable { .. } => Some("a nullable type"),
2546 TypeDecl::Map { .. } => Some("a map type"),
2547 TypeDecl::Record {
2548 present_when: Some(_),
2549 ..
2550 } => Some("a presence predicate"),
2551 TypeDecl::Record { fields, .. } => fields.iter().find_map(|f| type_v2(&f.ty)),
2552 TypeDecl::List { item, .. } => type_v2(item),
2553 _ => None,
2554 }
2555 }
2556 fn cond_v2(c: &Cond) -> bool {
2557 match c {
2558 Cond::Exact(p) => crate::expr::predicate_uses_v2(p),
2559 Cond::All(cs) | Cond::Any(cs) => cs.iter().any(cond_v2),
2560 Cond::Not(c) => cond_v2(c),
2561 _ => false,
2562 }
2563 }
2564 for i in &def.inputs {
2565 let subject = format!("input:{}", i.name);
2566 if i.evidence.is_some() {
2567 return Some((subject, "an evidence declaration"));
2568 }
2569 if i.on_disagreement.is_some() {
2570 return Some((subject, "a disagreement rule"));
2571 }
2572 if let Some(c) = type_v2(&i.ty) {
2573 return Some((subject, c));
2574 }
2575 }
2576 for s in &def.steps {
2577 let subject = format!("step:{}", s.id);
2578 match &s.body {
2579 StepBody::Extract(_) => return Some((subject, "an extraction step")),
2580 StepBody::AcceptModelDerived(_) => {
2581 return Some((subject, "an accept_model_derived step"));
2582 }
2583 StepBody::Rank(_) | StepBody::Select(_) => return Some((subject, "a ranking step")),
2584 StepBody::Generate(_) => return Some((subject, "a generate step")),
2585 StepBody::Acquire(_) | StepBody::Widen(_) => {
2586 return Some((subject, "an acquisition step"));
2587 }
2588 StepBody::Corroborate(_) => return Some((subject, "a corroborate step")),
2589 StepBody::Exact(_) => {
2590 if decoded.get(&s.id).is_some_and(OpArgs::uses_v2) {
2591 return Some((subject, "a rustev.exact/2 predicate or graded reference"));
2592 }
2593 }
2594 StepBody::Semantic(_) => {}
2595 }
2596 }
2597 let policy = &def.policy;
2598 for r in &policy.rules {
2599 if cond_v2(&r.when) {
2600 return Some((
2601 format!("policy:rule:{}", r.id),
2602 "a rustev.exact/2 predicate or graded reference",
2603 ));
2604 }
2605 }
2606 for a in &policy.adjustments {
2607 if cond_v2(&a.when) {
2608 return Some((
2609 format!("policy:adjustment:{}", a.id),
2610 "a rustev.exact/2 predicate or graded reference",
2611 ));
2612 }
2613 }
2614 None
2615}
2616
2617fn corroborate_refs(c: &rustev_contract::definition::CorroborateDecl) -> Vec<String> {
2619 let mut out = vec![c.claim.clone()];
2620 for check in &c.by {
2621 predicate_refs(&check.predicate, &mut out);
2622 }
2623 out.into_iter()
2624 .filter(|r| r.starts_with("input:") || r.starts_with("step:"))
2625 .map(|r| crate::expr::base_ref(&r))
2626 .collect()
2627}
2628
2629fn cond_structure(
2630 c: &Cond,
2631 options_of: &BTreeMap<String, Vec<String>>,
2632 known: &dyn Fn(&str) -> Result<(), String>,
2633 deferred: &BTreeSet<&str>,
2634) -> Result<(), String> {
2635 let unit = Decimal::from_i64(1);
2636 if matches!(c, Cond::TopLabel { step, .. } | Cond::MassAtLeast { step, .. } | Cond::TopMassBelow { step, .. } | Cond::ExpectationAtLeast { step, .. } if deferred.contains(step.as_str()))
2637 {
2638 return Ok(());
2639 }
2640 let semantic_opts = |step: &str| -> Result<&Vec<String>, String> {
2641 known(&format!("step:{step}"))?;
2642 options_of
2643 .get(step)
2644 .ok_or_else(|| format!("{step:?} is not a semantic step"))
2645 };
2646 match c {
2647 Cond::Always => Ok(()),
2648 Cond::ProvenanceWithin { reference, .. } => known(reference),
2650 Cond::TopLabel { step, label } => {
2651 if !semantic_opts(step)?.contains(label) {
2652 return Err(format!("{label:?} is not an option of {step:?}"));
2653 }
2654 Ok(())
2655 }
2656 Cond::MassAtLeast {
2657 step,
2658 option,
2659 threshold,
2660 uncalibrated_threshold,
2661 } => {
2662 if !semantic_opts(step)?.contains(option) {
2663 return Err(format!("{option:?} is not an option of {step:?}"));
2664 }
2665 if *threshold < Decimal::ZERO || *threshold > unit {
2666 return Err("a probability threshold is in [0, 1]".into());
2667 }
2668 if matches!(uncalibrated_threshold, UncalibratedThreshold::Declared { reason } if reason.is_empty())
2669 {
2670 return Err("an uncalibrated threshold states its reason".into());
2671 }
2672 Ok(())
2673 }
2674 Cond::TopMassBelow {
2675 step,
2676 threshold,
2677 uncalibrated_threshold,
2678 } => {
2679 semantic_opts(step)?;
2680 if *threshold < Decimal::ZERO || *threshold > unit {
2681 return Err("a probability threshold is in [0, 1]".into());
2682 }
2683 if matches!(uncalibrated_threshold, UncalibratedThreshold::Declared { reason } if reason.is_empty())
2684 {
2685 return Err("an uncalibrated threshold states its reason".into());
2686 }
2687 Ok(())
2688 }
2689 Cond::ExpectationAtLeast { step, .. } => semantic_opts(step).map(|_| ()),
2690 Cond::Exact(p) => {
2691 predicate_structure(p)?;
2692 let mut refs = Vec::new();
2693 predicate_refs(p, &mut refs);
2694 refs.iter().try_for_each(|r| known(r))
2695 }
2696 Cond::Empty(r) | Cond::Nonempty(r) => known(r),
2697 Cond::All(cs) | Cond::Any(cs) => cs
2698 .iter()
2699 .try_for_each(|c| cond_structure(c, options_of, known, deferred)),
2700 Cond::Not(c) => cond_structure(c, options_of, known, deferred),
2701 }
2702}
2703
2704fn topo_order(
2707 ids: &[String],
2708 deps: &BTreeMap<String, Vec<String>>,
2709) -> Result<Vec<String>, Refusal> {
2710 let step_deps: BTreeMap<&str, BTreeSet<&str>> = ids
2711 .iter()
2712 .map(|id| {
2713 (
2714 id.as_str(),
2715 deps[id].iter().filter_map(|r| step_ref(r)).collect(),
2716 )
2717 })
2718 .collect();
2719 let mut done: BTreeSet<&str> = BTreeSet::new();
2720 let mut order = Vec::with_capacity(ids.len());
2721 loop {
2722 let next = ids.iter().find(|id| {
2723 !done.contains(id.as_str()) && step_deps[id.as_str()].iter().all(|d| done.contains(d))
2724 });
2725 match next {
2726 Some(id) => {
2727 done.insert(id);
2728 order.push(id.clone());
2729 }
2730 None => break,
2731 }
2732 }
2733 if order.len() == ids.len() {
2734 return Ok(order);
2735 }
2736 let stuck: Vec<String> = ids
2737 .iter()
2738 .filter(|id| !done.contains(id.as_str()))
2739 .cloned()
2740 .collect();
2741 refuse(
2742 Category::Cycle,
2743 format!("step:{}", stuck[0]),
2744 format!("the step graph has a cycle among {}", stuck.join(", ")),
2745 )
2746}
2747
2748fn consumers_accept(def: &Definition, step: &str, kind: SemKind) -> Result<(), String> {
2750 fn walk(c: &Cond, step: &str, kind: SemKind) -> Result<(), String> {
2751 match c {
2752 Cond::TopLabel { step: s, .. } if s == step => {
2753 if kind == SemKind::Scores {
2754 return Err("top_label needs labels".into());
2755 }
2756 Ok(())
2757 }
2758 Cond::MassAtLeast { step: s, .. } | Cond::TopMassBelow { step: s, .. } if s == step => {
2759 if kind.has_mass() {
2760 Ok(())
2761 } else {
2762 Err(format!(
2763 "a probability threshold cannot read {}",
2764 kind.name()
2765 ))
2766 }
2767 }
2768 Cond::ExpectationAtLeast { step: s, .. } if s == step => {
2769 if matches!(kind, SemKind::Ordinal(_)) {
2770 Ok(())
2771 } else {
2772 Err(format!("an expectation cannot read {}", kind.name()))
2773 }
2774 }
2775 Cond::All(cs) | Cond::Any(cs) => cs.iter().try_for_each(|c| walk(c, step, kind)),
2776 Cond::Not(c) => walk(c, step, kind),
2777 _ => Ok(()),
2778 }
2779 }
2780 for r in &def.policy.rules {
2781 walk(&r.when, step, kind)?;
2782 }
2783 for a in &def.policy.adjustments {
2784 walk(&a.when, step, kind)?;
2785 }
2786 for s in &def.steps {
2787 if let StepBody::Exact(e) = &s.body
2788 && let Some(Ok(OpArgs::WeightedRank(w))) = ops::decode(&e.op, e.version, &e.args)
2789 {
2790 for c in &w.components {
2791 match &c.source {
2792 ops::Source::PropositionMean { step: s2, .. }
2793 if step_ref(s2) == Some(step) && !kind.has_mass() =>
2794 {
2795 return Err(format!("proposition_mean cannot read {}", kind.name()));
2796 }
2797 ops::Source::RubricExpectation { step: s2 }
2798 if step_ref(s2) == Some(step) && !matches!(kind, SemKind::Ordinal(_)) =>
2799 {
2800 return Err(format!("rubric_expectation cannot read {}", kind.name()));
2801 }
2802 _ => {}
2803 }
2804 }
2805 }
2806 }
2807 Ok(())
2808}
2809
2810fn possible_reasons(
2812 def: &Definition,
2813 inputs: &BTreeMap<String, (Ty, InputDecl)>,
2814 order: &[String],
2815 infos: &BTreeMap<String, StepInfo>,
2816 details: &BTreeMap<String, PlanStepDetail>,
2817) -> BTreeMap<String, BTreeSet<ReasonKind>> {
2818 use ReasonKind as K;
2819 let mut out: BTreeMap<String, BTreeSet<ReasonKind>> = BTreeMap::new();
2820 for (name, (_, decl)) in inputs {
2821 let mut r: BTreeSet<_> = [K::MissingEvidence, K::InvalidInput, K::Conflict].into();
2822 if matches!(decl.freshness, Freshness::MaxAgeMs(_)) {
2823 r.insert(K::StaleEvidence);
2824 }
2825 out.insert(format!("input:{name}"), r);
2826 }
2827 crate::actionable::extend_reasons(def, &mut out);
2830 for id in order {
2831 let mut r = BTreeSet::new();
2832 match (&infos[id], &details[id]) {
2833 (StepInfo::Accept(a), _) => {
2834 r.extend(out.get(&base_ref_v2(&a.field)).cloned().unwrap_or_default());
2835 if matches!(a.on_fail, rustev_contract::extraction::OnFail::Unresolved) {
2836 r.extend([K::InvalidInput, K::MissingEvidence]);
2837 }
2838 }
2839 (StepInfo::Ranking, detail) => {
2840 let body = &def.steps.iter().find(|s| &s.id == id).unwrap().body;
2841 r = ranking::reasons(body, detail, &out);
2842 }
2843 (StepInfo::Generate, _) => {}
2844 (StepInfo::Extract, _) => {
2845 r.insert(K::MissingEvidence);
2846 }
2847 (StepInfo::Acquisition(a), _) => {
2848 r.extend([
2849 K::MissingEvidence,
2850 K::StaleEvidence,
2851 K::InvalidInput,
2852 K::BackendUnavailable,
2853 K::InvalidBackendOutput,
2854 K::BudgetExhausted,
2855 K::DeadlineExceeded,
2856 ]);
2857 if a.plan.outcomes.contains(
2858 &rustev_contract::acquisition_record::AcquisitionPossibleOutcome::Skipped,
2859 ) {
2860 r.insert(K::Skipped);
2861 }
2862 if a.plan.outcomes.contains(&rustev_contract::acquisition_record::AcquisitionPossibleOutcome::InsufficientGrade) { r.insert(K::InsufficientGrade); }
2863 }
2864 (_, PlanStepDetail::Unsupported { .. }) => {
2865 r.insert(K::Unsupported);
2866 }
2867 (StepInfo::Corroborate { decl, .. }, _) => {
2870 let (claim, _) = quality::claim_path(decl);
2871 r.extend(out.get(&claim).cloned().unwrap_or_default());
2872 }
2873 (StepInfo::Exact { args, .. }, _) => {
2874 if let OpArgs::FieldPresence(fp) = args {
2875 for i in &fp.inputs {
2876 let mut s = out.get(i).cloned().unwrap_or_default();
2877 s.remove(&K::MissingEvidence);
2878 r.extend(s);
2879 }
2880 } else {
2881 for d in args.refs() {
2882 let mut inherited = out.get(&d).cloned().unwrap_or_default();
2883 if probe_only(args, &d) {
2884 inherited.remove(&K::Skipped);
2885 }
2886 r.extend(inherited);
2887 }
2888 }
2889 r.extend(args.own_reasons());
2890 }
2891 (StepInfo::Semantic { decl, .. }, _) => {
2892 let mut refs: Vec<String> = decl
2893 .project
2894 .iter()
2895 .filter(|p| !p.starts_with("bind:"))
2896 .cloned()
2897 .collect();
2898 for f in &decl.for_each {
2899 refs.push(f.over.clone());
2900 if let ItemSource::List { list, .. } = &f.items {
2901 refs.push(list.clone());
2902 }
2903 }
2904 if let Candidates::From(c) = &decl.candidates {
2905 refs.push(c.clone());
2906 }
2907 for d in refs {
2908 r.extend(out.get(&d).cloned().unwrap_or_default());
2909 }
2910 if decl.for_each.is_empty() {
2911 r.extend([
2912 K::BackendUnavailable,
2913 K::InvalidBackendOutput,
2914 K::BudgetExhausted,
2915 K::DeadlineExceeded,
2916 ]);
2917 } else {
2918 r.insert(K::InvalidInput);
2920 }
2921 }
2922 }
2923 out.insert(format!("step:{id}"), r);
2924 }
2925 out
2926}
2927
2928pub(crate) fn predicate_value_refs(
2930 p: &rustev_contract::definition::Predicate,
2931 out: &mut Vec<String>,
2932) {
2933 use rustev_contract::definition::Predicate as P;
2934 match p {
2935 P::Present(_) | P::IsConstructed { .. } => {}
2936 P::All(ps) | P::Any(ps) => ps.iter().for_each(|p| predicate_value_refs(p, out)),
2937 P::Not(p) => predicate_value_refs(p, out),
2938 p => crate::expr::predicate_refs(p, out),
2939 }
2940}
2941fn condition_value_refs(c: &Cond, out: &mut Vec<String>) {
2942 match c {
2943 Cond::Exact(p) => predicate_value_refs(p, out),
2944 Cond::All(cs) | Cond::Any(cs) => cs.iter().for_each(|c| condition_value_refs(c, out)),
2945 Cond::Not(c) => condition_value_refs(c, out),
2946 c => condition_refs(c, out),
2947 }
2948}
2949pub(crate) fn probe_only(args: &OpArgs, target: &str) -> bool {
2950 if let OpArgs::Compare(c) = args {
2951 let mut values = vec![];
2952 predicate_value_refs(&c.predicate, &mut values);
2953 return !values.iter().any(|r| crate::expr::base_ref_v2(r) == target);
2954 }
2955 false
2956}
2957
2958fn cond_reads(c: &Cond, negated: bool, out: &mut Vec<(String, bool)>) {
2960 match c {
2961 Cond::Not(inner) => cond_reads(inner, !negated, out),
2962 Cond::All(cs) | Cond::Any(cs) => cs.iter().for_each(|c| cond_reads(c, negated, out)),
2963 other => {
2964 let mut refs = Vec::new();
2965 condition_refs(other, &mut refs);
2966 out.extend(refs.into_iter().map(|r| (r, negated)));
2967 }
2968 }
2969}
2970
2971fn check_policy(
2972 def: &Definition,
2973 input_tys: &BTreeMap<String, Ty>,
2974 step_tys: &BTreeMap<String, StepTy>,
2975 reasons: &BTreeMap<String, BTreeSet<ReasonKind>>,
2976 notices: &mut Vec<Notice>,
2977 v2: bool,
2978) -> Result<(), Refusal> {
2979 use Category as C;
2980 let base = |r: &str| {
2981 if v2 {
2982 crate::expr::base_ref_v2(r)
2983 } else {
2984 crate::expr::base_ref(r)
2985 }
2986 };
2987 let env = TyEnv {
2988 inputs: input_tys,
2989 steps: step_tys,
2990 item: None,
2991 v2,
2992 };
2993 let policy = &def.policy;
2994
2995 let items: Vec<(String, &Cond, Option<&OutcomeDecl>)> = policy
2997 .rules
2998 .iter()
2999 .map(|r| (format!("policy:rule:{}", r.id), &r.when, Some(&r.then)))
3000 .chain(
3001 policy
3002 .adjustments
3003 .iter()
3004 .map(|a| (format!("policy:adjustment:{}", a.id), &a.when, None)),
3005 )
3006 .collect();
3007 for (subject, cond, outcome) in &items {
3008 check_cond(cond, &env, subject, notices)?;
3009 if let Some(o) = outcome {
3010 match o {
3011 OutcomeDecl::Propose { params, .. } => {
3012 for p in params {
3013 if let ParamSource::Ref(r) = &p.value {
3014 match crate::expr::ref_ty(r, &env) {
3015 Ok(_) => {}
3016 Err(e) => {
3017 return refuse(
3018 C::KindMismatch,
3019 subject.clone(),
3020 format!("parameter {:?}: {e}", p.name),
3021 );
3022 }
3023 }
3024 }
3025 }
3026 }
3027 OutcomeDecl::MissingEvidenceFrom { step } => match step_tys.get(step) {
3028 Some(StepTy::Exact(Ty::List { item, .. }))
3029 if matches!(item.as_ref(), Ty::Text { .. }) => {}
3030 _ => {
3031 return refuse(
3032 C::KindMismatch,
3033 subject.clone(),
3034 format!("{step:?} does not list field names"),
3035 );
3036 }
3037 },
3038 OutcomeDecl::Escalate { .. } | OutcomeDecl::NeedsInput { .. } => {}
3039 }
3040 }
3041 }
3042
3043 let mut covered: BTreeMap<&str, BTreeSet<ReasonKind>> = BTreeMap::new();
3045 let mut unmet: BTreeSet<&str> = BTreeSet::new();
3046 for h in &policy.on_unresolved {
3047 let set: BTreeSet<ReasonKind> = match &h.reasons {
3048 ReasonSet::Any => ReasonKind::ALL.into_iter().collect(),
3049 ReasonSet::Only(v) => v.iter().copied().collect(),
3050 };
3051 if !matches!(
3052 h.action,
3053 HandlerAction::AsUnmet | HandlerAction::Escalate { .. }
3054 ) && set.contains(&ReasonKind::Skipped)
3055 && reasons
3056 .get(&h.target)
3057 .is_some_and(|r| r.contains(&ReasonKind::Skipped))
3058 {
3059 return refuse(
3060 C::UnhandledUnresolved,
3061 &h.target,
3062 "skipped_not_propagatable",
3063 );
3064 }
3065 if h.action == HandlerAction::AsUnmet {
3066 unmet.insert(&h.target);
3067 }
3068 covered.entry(&h.target).or_default().extend(set);
3069 }
3070 let mut reads: Vec<(String, String, bool, bool)> = Vec::new();
3071 for (subject, cond, outcome) in &items {
3072 let mut r = Vec::new();
3073 cond_reads(cond, false, &mut r);
3074 let mut values = vec![];
3075 condition_value_refs(cond, &mut values);
3076 reads.extend(
3077 r.into_iter()
3078 .filter(|(x, _)| x.starts_with("input:") || x.starts_with("step:"))
3079 .map(|(x, neg)| {
3080 let target = base(&x);
3081 let probe = !values.iter().any(|v| base(v) == target);
3082 (subject.clone(), target, neg, probe)
3083 }),
3084 );
3085 if let Some(o) = outcome {
3086 let mut refs = Vec::new();
3087 outcome_refs(o, &mut refs);
3088 reads.extend(
3089 refs.into_iter()
3090 .map(|x| (subject.clone(), base(&x), false, false)),
3091 );
3092 }
3093 }
3094 for (subject, target, negated, probe) in &reads {
3095 let mut possible = reasons.get(target).cloned().unwrap_or_default();
3096 if *probe {
3097 possible.remove(&ReasonKind::Skipped);
3098 }
3099 let have = covered.get(target.as_str()).cloned().unwrap_or_default();
3100 let missing: Vec<&str> = possible.difference(&have).map(|k| k.name()).collect();
3101 if !missing.is_empty() {
3102 return refuse(
3103 C::UnhandledUnresolved,
3104 subject.clone(),
3105 format!(
3106 "{target} can be unresolved with {} and no handler covers it",
3107 missing.join(", ")
3108 ),
3109 );
3110 }
3111 if *negated && unmet.contains(target.as_str()) {
3112 return refuse(
3113 C::UnhandledUnresolved,
3114 subject.clone(),
3115 format!("`not` over {target}, which is handled as_unmet"),
3116 );
3117 }
3118 }
3119 if let Some(last) = policy.rules.last() {
3120 let mut refs = Vec::new();
3121 outcome_refs(&last.then, &mut refs);
3122 if let Some(r) = refs
3123 .iter()
3124 .map(|r| base(r))
3125 .find(|r| unmet.contains(r.as_str()))
3126 {
3127 return refuse(
3128 C::UnhandledUnresolved,
3129 format!("policy:rule:{}", last.id),
3130 format!("the last rule reads {r}, which is handled as_unmet"),
3131 );
3132 }
3133 }
3134 Ok(())
3135}
3136
3137fn check_cond(
3138 c: &Cond,
3139 env: &TyEnv<'_>,
3140 subject: &str,
3141 notices: &mut Vec<Notice>,
3142) -> Result<(), Refusal> {
3143 use Category as C;
3144 let sem = |step: &str| -> Result<SemKind, Refusal> {
3145 match env.steps.get(step) {
3146 Some(StepTy::Semantic(k, binds)) if binds.is_empty() => Ok(*k),
3147 Some(StepTy::Semantic(..)) => refuse(
3148 C::KindMismatch,
3149 subject,
3150 format!("{step:?} fans out; the policy reads single values"),
3151 ),
3152 _ => refuse(
3153 C::KindMismatch,
3154 subject,
3155 format!("{step:?} is not a semantic step"),
3156 ),
3157 }
3158 };
3159 let threshold = |step: &str,
3160 u: &UncalibratedThreshold,
3161 notices: &mut Vec<Notice>|
3162 -> Result<(), Refusal> {
3163 match sem(step)? {
3164 SemKind::Calibrated => Ok(()),
3165 SemKind::Distribution => match u {
3166 UncalibratedThreshold::Declared { reason } => {
3167 notices.push(Notice {
3168 screening: None,
3169 kind: NoticeKind::UncalibratedThreshold,
3170 subject: subject.to_string(),
3171 reason: reason.clone(),
3172 action: None,
3173 });
3174 Ok(())
3175 }
3176 UncalibratedThreshold::NotDeclared => refuse(
3177 C::UncalibratedThreshold,
3178 subject,
3179 format!(
3180 "a probability threshold reads the uncalibrated distribution {step:?} without uncalibrated_threshold"
3181 ),
3182 ),
3183 },
3184 k => refuse(
3185 C::KindMismatch,
3186 subject,
3187 format!(
3188 "a probability threshold cannot read {} ({step:?})",
3189 k.name()
3190 ),
3191 ),
3192 }
3193 };
3194 match c {
3195 Cond::Always | Cond::ProvenanceWithin { .. } => Ok(()),
3196 Cond::TopLabel { step, .. } => match sem(step)? {
3197 SemKind::Scores => refuse(
3198 C::KindMismatch,
3199 subject,
3200 format!("top_label cannot read scores ({step:?})"),
3201 ),
3202 _ => Ok(()),
3203 },
3204 Cond::MassAtLeast {
3205 step,
3206 uncalibrated_threshold,
3207 ..
3208 }
3209 | Cond::TopMassBelow {
3210 step,
3211 uncalibrated_threshold,
3212 ..
3213 } => threshold(step, uncalibrated_threshold, notices),
3214 Cond::ExpectationAtLeast { step, .. } => match sem(step)? {
3215 SemKind::Ordinal(_) => Ok(()),
3216 k => refuse(
3217 C::KindMismatch,
3218 subject,
3219 format!("an expectation cannot read {} ({step:?})", k.name()),
3220 ),
3221 },
3222 Cond::Exact(p) => type_predicate(p, env).or_else(|e| refuse(C::KindMismatch, subject, e)),
3223 Cond::Empty(r) | Cond::Nonempty(r) => match crate::expr::ref_ty(r, env) {
3224 Ok(
3225 Ty::List { .. } | Ty::Filtered { .. } | Ty::Shortlist { .. } | Ty::Ranking { .. },
3226 ) => Ok(()),
3227 Ok(t) => refuse(C::KindMismatch, subject, format!("empty() of {}", t.name())),
3228 Err(e) => refuse(C::KindMismatch, subject, e),
3229 },
3230 Cond::All(cs) | Cond::Any(cs) => cs
3231 .iter()
3232 .try_for_each(|c| check_cond(c, env, subject, notices)),
3233 Cond::Not(c) => check_cond(c, env, subject, notices),
3234 }
3235}
3236
3237fn static_derivations(
3239 def: &Definition,
3240 inputs: &BTreeMap<String, (Ty, InputDecl)>,
3241 order: &[String],
3242 deps: &BTreeMap<String, Vec<String>>,
3243 infos: &BTreeMap<String, StepInfo>,
3244) -> BTreeMap<String, Derivation> {
3245 let semantic: BTreeSet<&str> = def
3246 .steps
3247 .iter()
3248 .filter(|s| {
3249 matches!(
3250 s.body,
3251 StepBody::Semantic(_)
3252 | StepBody::Generate(_)
3253 | StepBody::Extract(_)
3254 | StepBody::Rank(_)
3255 )
3256 })
3257 .map(|s| s.id.as_str())
3258 .collect();
3259 let mut out: BTreeMap<String, Derivation> = BTreeMap::new();
3260 for id in order {
3261 if def
3262 .steps
3263 .iter()
3264 .any(|s| &s.id == id && matches!(s.body, StepBody::Select(_)))
3265 {
3266 out.insert(id.clone(), Derivation::MixedDerived);
3267 continue;
3268 }
3269 if let Some(StepInfo::Acquisition(a)) = infos.get(id) {
3270 out.insert(id.clone(), a.plan.derivation);
3271 continue;
3272 }
3273 if let Some(StepInfo::Accept(a)) = infos.get(id) {
3274 let source = base_ref_v2(&a.field);
3275 let class = source
3276 .strip_prefix("step:")
3277 .and_then(|s| out.get(s))
3278 .copied()
3279 .unwrap_or(Derivation::ModelDerived);
3280 out.insert(id.clone(), class);
3281 continue;
3282 }
3283 if semantic.contains(id.as_str()) {
3284 out.insert(id.clone(), Derivation::ModelDerived);
3285 continue;
3286 }
3287 let claim_only;
3290 let reads: &[String] = match infos.get(id) {
3291 Some(StepInfo::Corroborate { decl, .. }) => {
3292 claim_only = vec![quality::claim_path(decl).0];
3293 &claim_only
3294 }
3295 _ => &deps[id],
3296 };
3297 let all_exact = reads.iter().all(|r| {
3298 if let Some(n) = r.strip_prefix("input:") {
3299 !inputs
3300 .get(n)
3301 .is_some_and(|(_, d)| d.provenance.contains(&ProvenanceClass::ModelDerived))
3302 } else if let Some(s) = step_ref(r) {
3303 out.get(s) == Some(&Derivation::ExactDerived)
3304 } else {
3305 true
3306 }
3307 });
3308 out.insert(
3309 id.clone(),
3310 if all_exact {
3311 Derivation::ExactDerived
3312 } else {
3313 Derivation::MixedDerived
3314 },
3315 );
3316 }
3317 out
3318}
3319
3320#[cfg(test)]
3321mod tests {
3322 use super::*;
3323
3324 #[test]
3325 fn v2_construct_describes_extraction_and_acceptance_separately() {
3326 let mut def: Definition =
3327 serde_json::from_str(include_str!("testdata/support-routing.definition.json")).unwrap();
3328 for (fixture, description) in [
3329 (
3330 include_str!("testdata/extract-step.json"),
3331 "an extraction step",
3332 ),
3333 (
3334 include_str!("testdata/accept-step.json"),
3335 "an accept_model_derived step",
3336 ),
3337 ] {
3338 def.steps = vec![serde_json::from_str(fixture).unwrap()];
3339 assert_eq!(
3340 first_v2_construct(&def, &BTreeMap::new()),
3341 Some((format!("step:{}", def.steps[0].id), description)),
3342 );
3343 }
3344 }
3345}