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
41pub(crate) mod 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::Ranking => false,
614 R::ScreenVerdict => output == O::Screen,
615 R::TextRecord | R::TypedRecord => false,
616 R::Label => output == O::Label,
617 R::Distribution | R::CalibratedProbability | R::OrdinalDistribution => {
618 matches!(output, O::Distribution | O::Logits)
619 }
620 R::Scores => matches!(output, O::Scores | O::Logits),
621 }
622}
623
624fn sem_kind(op: Operation, required: RequiredKind) -> SemKind {
625 match (op, required) {
626 (_, RequiredKind::Ranking) => SemKind::Label,
631 (Operation::Rubric, RequiredKind::Label) => SemKind::Ordinal(false),
632 (_, RequiredKind::ScreenVerdict | RequiredKind::TextRecord | RequiredKind::TypedRecord) => {
636 SemKind::Label
637 }
638 (_, RequiredKind::Label) => SemKind::Label,
639 (_, RequiredKind::Distribution) => SemKind::Distribution,
640 (_, RequiredKind::CalibratedProbability) => SemKind::Calibrated,
641 (_, RequiredKind::OrdinalDistribution) => SemKind::Ordinal(true),
642 (_, RequiredKind::Scores) => SemKind::Scores,
643 }
644}
645
646pub(crate) fn check_candidate(
650 d: &SemanticDecl,
651 b: &BackendDescriptor,
652 required: RequiredKind,
653 options_needed: u64,
654 bound: u64,
655) -> Result<OutputKind, Vec<ShortfallReason>> {
656 let mut reasons = Vec::new();
657 let supports: Vec<_> = b
658 .operations
659 .iter()
660 .filter(|o| o.operation == d.operation)
661 .collect();
662 if supports.is_empty() {
663 reasons.push(ShortfallReason::Operation(d.operation));
664 }
665 let chosen = supports.iter().find(|o| {
666 satisfies(o.output.clone(), required)
667 && (required != RequiredKind::ScreenVerdict || d.operation == Operation::Proposition)
668 });
669 match (chosen, supports.first()) {
670 (None, Some(o)) => reasons.push(ShortfallReason::OutputKind {
671 offered: o.output.clone(),
672 required,
673 }),
674 (Some(o), _) if o.max_options < options_needed => reasons.push(ShortfallReason::Options {
675 max: o.max_options,
676 needed: options_needed,
677 }),
678 _ => {}
679 }
680 if bound > b.input_limit.max_bytes && b.input_limit.on_excess == InputExcess::Refuse {
681 reasons.push(ShortfallReason::InputLimit {
682 max: b.input_limit.max_bytes,
683 needed: bound,
684 });
685 }
686 if b.determinism < d.backend.min_determinism {
687 reasons.push(ShortfallReason::Determinism {
688 offered: format!("{:?}", b.determinism),
689 required: format!("{:?}", d.backend.min_determinism),
690 });
691 }
692 if let ArtifactPin::Pinned(a) = &d.backend.artifact
693 && a != &b.artifact
694 {
695 reasons.push(ShortfallReason::ArtifactPin {
696 pinned: a.to_string(),
697 offered: b.artifact.to_string(),
698 });
699 }
700 match chosen {
701 Some(o) if reasons.is_empty() => Ok(o.output.clone()),
702 _ => Err(reasons),
703 }
704}
705
706pub(crate) fn condition_refs(c: &Cond, out: &mut Vec<String>) {
707 match c {
708 Cond::Always => {}
709 Cond::TopLabel { step, .. }
710 | Cond::MassAtLeast { step, .. }
711 | Cond::TopMassBelow { step, .. }
712 | Cond::ExpectationAtLeast { step, .. } => out.push(format!("step:{step}")),
713 Cond::Exact(p) => predicate_refs(p, out),
714 Cond::Empty(r) | Cond::Nonempty(r) => out.push(r.clone()),
715 Cond::All(cs) | Cond::Any(cs) => cs.iter().for_each(|c| condition_refs(c, out)),
716 Cond::Not(c) => condition_refs(c, out),
717 Cond::ProvenanceWithin { reference, .. } => out.push(reference.clone()),
718 }
719}
720
721pub(crate) fn outcome_refs(o: &OutcomeDecl, out: &mut Vec<String>) {
722 match o {
723 OutcomeDecl::Propose { params, .. } => {
724 for p in params {
725 if let ParamSource::Ref(r) = &p.value {
726 out.push(r.clone());
727 }
728 }
729 }
730 OutcomeDecl::Escalate { .. } => {}
731 OutcomeDecl::MissingEvidenceFrom { step } => out.push(format!("step:{step}")),
732 OutcomeDecl::NeedsInput { .. } => {}
737 }
738}
739
740pub fn compile(
744 def: &Definition,
745 descriptors: &[BackendDescriptor],
746 calibrations: &[CalibrationArtifact],
747) -> Result<Compiled, Refusal> {
748 compile_inner(def, descriptors, calibrations, &[], None)
749}
750
751pub fn compile_with_sources(
756 def: &Definition,
757 descriptors: &[BackendDescriptor],
758 calibrations: &[CalibrationArtifact],
759 sources: &[SourceDescriptor],
760 execution: Option<&ExecutionPolicy>,
761) -> Result<Compiled, Refusal> {
762 compile_inner(def, descriptors, calibrations, sources, execution)
763}
764
765pub fn compile_with(
769 def: &Definition,
770 descriptors: &[BackendDescriptor],
771 calibrations: &[CalibrationArtifact],
772 execution: &ExecutionPolicy,
773) -> Result<Compiled, Refusal> {
774 compile_inner(def, descriptors, calibrations, &[], Some(execution))
775}
776
777fn compile_inner(
778 def: &Definition,
779 descriptors: &[BackendDescriptor],
780 calibrations: &[CalibrationArtifact],
781 sources: &[SourceDescriptor],
782 execution: Option<&ExecutionPolicy>,
783) -> Result<Compiled, Refusal> {
784 compile_instantiated(def, descriptors, calibrations, sources, execution, false)
785}
786
787pub(crate) fn compile_instantiated(
789 def: &Definition,
790 descriptors: &[BackendDescriptor],
791 calibrations: &[CalibrationArtifact],
792 sources: &[SourceDescriptor],
793 execution: Option<&ExecutionPolicy>,
794 #[cfg_attr(not(feature = "parameterized"), allow(unused_variables))] instantiated: bool,
795) -> Result<Compiled, Refusal> {
796 compile_generation(
797 def,
798 descriptors,
799 calibrations,
800 sources,
801 execution,
802 instantiated,
803 (&[], &[]),
804 )
805}
806
807pub fn compile_with_templates(
809 def: &Definition,
810 descriptors: &[BackendDescriptor],
811 calibrations: &[CalibrationArtifact],
812 sources: &[SourceDescriptor],
813 execution: Option<&ExecutionPolicy>,
814 templates: &[rustev_contract::generation::PromptTemplate],
815) -> Result<Compiled, Refusal> {
816 compile_generation(
817 def,
818 descriptors,
819 calibrations,
820 sources,
821 execution,
822 false,
823 (templates, &[]),
824 )
825}
826
827pub fn compile_with_extraction_documents(
830 def: &Definition,
831 descriptors: &[BackendDescriptor],
832 calibrations: &[CalibrationArtifact],
833 sources: &[SourceDescriptor],
834 execution: Option<&ExecutionPolicy>,
835 templates: &[rustev_contract::generation::PromptTemplate],
836 programs: &[rustev_contract::extraction_documents::ExtractRules],
837) -> Result<Compiled, Refusal> {
838 compile_generation(
839 def,
840 descriptors,
841 calibrations,
842 sources,
843 execution,
844 false,
845 (templates, programs),
846 )
847}
848
849fn compile_generation(
850 def: &Definition,
851 descriptors: &[BackendDescriptor],
852 calibrations: &[CalibrationArtifact],
853 sources: &[SourceDescriptor],
854 execution: Option<&ExecutionPolicy>,
855 instantiated: bool,
856 documents: (
857 &[rustev_contract::generation::PromptTemplate],
858 &[rustev_contract::extraction_documents::ExtractRules],
859 ),
860) -> Result<Compiled, Refusal> {
861 #[cfg(not(feature = "parameterized"))]
862 let _ = instantiated;
863 use Category as C;
864 let (templates, programs) = documents;
865
866 #[cfg(not(feature = "parameterized"))]
870 crate::unparameterized::typed_definition(def)?;
871 let v2 = def.schema == schema::DEFINITION_V2;
872 if def.schema != schema::DEFINITION && !v2 {
873 return refuse(
874 C::Parse,
875 "definition",
876 format!(
877 "schema {:?} is neither {} nor {}",
878 def.schema,
879 schema::DEFINITION,
880 schema::DEFINITION_V2
881 ),
882 );
883 }
884 let decode = if v2 { ops::decode_v2 } else { ops::decode };
885 let mut decoded: BTreeMap<String, OpArgs> = BTreeMap::new();
886 for s in &def.steps {
887 if let StepBody::Exact(e) = &s.body {
888 match decode(&e.op, e.version, &e.args) {
889 Some(Ok(a)) => {
890 decoded.insert(s.id.clone(), a);
891 }
892 Some(Err(msg)) => {
893 return refuse(
894 C::Parse,
895 format!("step:{}", s.id),
896 format!("arguments of {}@{}: {msg}", e.op, e.version),
897 );
898 }
899 None => {}
900 }
901 }
902 }
903 for d in descriptors {
904 if d.schema != schema::BACKEND {
905 return refuse(
906 C::Parse,
907 format!("backend:{}", d.backend_id),
908 "descriptor schema is not rustev.backend/1",
909 );
910 }
911 }
912 for c in calibrations {
913 if c.schema != schema::CALIBRATION {
914 return refuse(
915 C::Parse,
916 "calibration",
917 "calibration schema is not rustev.calibration/1",
918 );
919 }
920 }
921
922 if def
923 .limits
924 .acquisition
925 .as_ref()
926 .is_some_and(|b| !(1..=4096).contains(&b.max_calls))
927 {
928 return refuse(C::Parse, "limits", "acquisition.max_calls outside 1..=4096");
929 }
930 if !v2 && def.limits.acquisition.is_some() {
931 return refuse(
932 C::InvalidDefinition,
933 "limits",
934 "requires-definition-2: acquisition budget",
935 );
936 }
937 if !v2 && def.screening.is_some() {
939 return refuse(
940 C::InvalidDefinition,
941 "screening",
942 "requires-definition-2: screening needs rustev.definition/2",
943 );
944 }
945 for s in &def.steps {
947 if let StepBody::Semantic(d) = &s.body
948 && d.requires == RequiredKind::ScreenVerdict
949 {
950 return refuse(
951 C::InvalidDefinition,
952 format!("step:{}", s.id),
953 "screen_verdict is reserved for lowered screening steps",
954 );
955 }
956 }
957 for s in &def.steps {
962 if !v2
963 && matches!(
964 s.body,
965 StepBody::Acquire(_)
966 | StepBody::Widen(_)
967 | StepBody::Generate(_)
968 | StepBody::Extract(_)
969 | StepBody::AcceptModelDerived(_)
970 | StepBody::Rank(_)
971 | StepBody::Select(_)
972 )
973 {
974 return refuse(
975 C::InvalidDefinition,
976 format!("step:{}", s.id),
977 "requires-definition-2",
978 );
979 }
980 }
981 ranking::structure(def, &decoded)?;
982 for s in &def.steps {
983 crate::acquisition::structure(&s.id, &s.body)?;
984 }
985 if !v2
986 && def.policy.on_unresolved.iter().any(|h| {
987 matches!(&h.reasons,
988 ReasonSet::Only(rs) if rs.contains(&ReasonKind::Skipped))
989 })
990 {
991 return refuse(
992 C::InvalidDefinition,
993 "policy",
994 "requires-definition-2: skipped",
995 );
996 }
997 if !v2 && let Some((subject, construct)) = crate::actionable::first_construct(def) {
1000 return refuse(
1001 C::InvalidDefinition,
1002 subject,
1003 format!(
1004 "requires-definition-2: {construct} needs {}",
1005 schema::DEFINITION_V2
1006 ),
1007 );
1008 }
1009 if v2 {
1013 let decision_cache = execution
1017 .and_then(|e| e.optimization.as_ref())
1018 .is_some_and(|o| o.decision_cache.is_some());
1019 #[cfg(feature = "parameterized")]
1020 let construct_elsewhere = instantiated || decision_cache;
1021 #[cfg(not(feature = "parameterized"))]
1022 let construct_elsewhere = decision_cache;
1023 quality::structure_with_origin(def, &decoded, construct_elsewhere)?;
1024 } else if let Some((subject, construct)) = first_v2_construct(def, &decoded) {
1025 return refuse(
1026 C::InvalidDefinition,
1027 subject,
1028 format!(
1029 "requires_definition_2: {construct} needs {}",
1030 schema::DEFINITION_V2
1031 ),
1032 );
1033 }
1034 crate::generation::isolation(def, execution)?;
1035 crate::generation::structure(def)?;
1036 crate::extraction_compile::structure(def)?;
1037 let mut backend_ids = BTreeSet::new();
1039 for d in descriptors {
1040 if !backend_ids.insert(d.backend_id.as_str()) {
1041 return refuse(
1042 C::InvalidDefinition,
1043 format!("backend:{}", d.backend_id),
1044 "two descriptors share a backend id",
1045 );
1046 }
1047 }
1048 let inv = |subject: String, detail: String| -> Result<(), Refusal> {
1049 refuse(C::InvalidDefinition, subject, detail)
1050 };
1051 if def.name.is_empty() || def.package.is_empty() {
1052 inv(
1053 "definition".into(),
1054 "name and package must be non-empty".into(),
1055 )?;
1056 }
1057 if !is_semver(&def.version) {
1058 inv(
1059 "definition".into(),
1060 format!("version {:?} is not MAJOR.MINOR.PATCH", def.version),
1061 )?;
1062 }
1063 let mut inputs: BTreeMap<String, (Ty, InputDecl)> = BTreeMap::new();
1064 for i in &def.inputs {
1065 let subject = format!("input:{}", i.name);
1066 if !valid_name(&i.name) || inputs.contains_key(&i.name) {
1067 inv(
1068 subject.clone(),
1069 "input names must be unique, non-empty, without ':' or whitespace".into(),
1070 )?;
1071 }
1072 let classes: BTreeSet<_> = i.provenance.iter().collect();
1073 if i.provenance.is_empty() || classes.len() != i.provenance.len() {
1074 inv(
1075 subject.clone(),
1076 "accepted provenance classes must be non-empty and unique".into(),
1077 )?;
1078 }
1079 check_type_decl(&i.ty).or_else(|e| inv(subject, e))?;
1080 inputs.insert(i.name.clone(), (Ty::from_decl(&i.ty), i.clone()));
1081 }
1082 let ids: Vec<String> = def.steps.iter().map(|s| s.id.clone()).collect();
1083 let known = |r: &str| -> Result<(), String> {
1084 match Ref::parse(r)? {
1085 Ref::Input(n, _) if !inputs.contains_key(&n) => Err(format!("unknown input {n:?}")),
1086 Ref::Step(n) if v2 && !ids.contains(&crate::expr::split_step(&n).0) => {
1088 Err(format!("unknown step {n:?}"))
1089 }
1090 Ref::Step(n) if !v2 && !ids.contains(&n) => Err(format!("unknown step {n:?}")),
1091 _ => Ok(()),
1092 }
1093 };
1094 let ranking_kinds: BTreeSet<&str> = def
1095 .steps
1096 .iter()
1097 .filter(|s| matches!(s.body, StepBody::Rank(_) | StepBody::Select(_)))
1098 .map(|s| s.id.as_str())
1099 .collect();
1100 let mut options_of: BTreeMap<String, Vec<String>> = def
1101 .steps
1102 .iter()
1103 .filter_map(|s| {
1104 if let StepBody::Semantic(d) = &s.body {
1105 Some((s.id.clone(), d.options.clone()))
1106 } else {
1107 None
1108 }
1109 })
1110 .collect();
1111 options_of.extend(crate::extraction_compile::distribution_options(def));
1112 for (i, s) in def.steps.iter().enumerate() {
1113 let subject = format!("step:{}", s.id);
1114 if !valid_name(&s.id) || ids[..i].contains(&s.id) {
1115 inv(
1116 subject.clone(),
1117 "step ids must be unique, non-empty, without ':' or whitespace".into(),
1118 )?;
1119 }
1120 match &s.body {
1121 StepBody::Extract(_) | StepBody::AcceptModelDerived(_) => {
1122 }
1124 StepBody::Rank(_) | StepBody::Select(_) => {
1125 for r in ranking::refs(&s.body) {
1126 known(&r).or_else(|e| inv(subject.clone(), e))?;
1127 }
1128 }
1129 StepBody::Generate(_) => {}
1130 StepBody::Acquire(_) | StepBody::Widen(_) => {
1133 for r in crate::acquisition::refs(&s.body) {
1134 known(&r).or_else(|e| inv(subject.clone(), e))?;
1135 }
1136 }
1137 StepBody::Corroborate(c) => {
1138 let mut refs = vec![c.claim.clone()];
1139 c.by.iter()
1140 .for_each(|k| predicate_refs(&k.predicate, &mut refs));
1141 for r in refs {
1142 known(&r).or_else(|e| inv(subject.clone(), e))?;
1143 }
1144 }
1145 StepBody::Exact(_) => {
1146 if let Some(a) = decoded.get(&s.id) {
1147 a.structure().or_else(|e| inv(subject.clone(), e))?;
1148 for r in a.all_ref_strings() {
1149 known(&r).or_else(|e| inv(subject.clone(), e))?;
1150 }
1151 }
1152 }
1153 StepBody::Semantic(d) => {
1154 if d.task.is_empty() || d.question.is_empty() {
1155 inv(
1156 subject.clone(),
1157 "task and question must be non-empty".into(),
1158 )?;
1159 }
1160 let set: BTreeSet<_> = d.options.iter().collect();
1161 if set.len() != d.options.len() || d.options.iter().any(String::is_empty) {
1162 inv(
1163 subject.clone(),
1164 "options must be unique and non-empty".into(),
1165 )?;
1166 }
1167 match d.operation {
1168 Operation::Classify if d.options.len() < 2 => inv(
1169 subject.clone(),
1170 "classify needs at least two options".into(),
1171 )?,
1172 Operation::Proposition if d.options != ["false", "true"] => inv(
1173 subject.clone(),
1174 "proposition options must be [\"false\", \"true\"]".into(),
1175 )?,
1176 Operation::Rubric if d.options.len() < 2 => {
1177 inv(subject.clone(), "rubric needs at least two levels".into())?
1178 }
1179 Operation::Rank if !d.options.is_empty() => {
1180 inv(subject.clone(), "rank takes candidates, not options".into())?
1181 }
1182 _ => {}
1183 }
1184 match (&d.candidates, d.operation) {
1185 (Candidates::From(r), Operation::Rank) => {
1186 if step_ref(r).is_none() {
1187 inv(subject.clone(), format!("candidates {r:?} must be a step"))?;
1188 }
1189 known(r).or_else(|e| inv(subject.clone(), e))?;
1190 }
1191 (Candidates::None, Operation::Rank) => {
1192 inv(subject.clone(), "rank needs candidates".into())?
1193 }
1194 (Candidates::From(_), _) => {
1195 inv(subject.clone(), "only rank takes candidates".into())?
1196 }
1197 (Candidates::None, _) => {}
1198 }
1199 if !kind_valid_for(d.operation, d.requires) {
1200 inv(
1201 subject.clone(),
1202 format!("{:?} cannot require {:?}", d.operation, d.requires),
1203 )?;
1204 }
1205 if d.for_each.len() > 2 {
1206 inv(subject.clone(), "at most two fan-out indices".into())?;
1207 }
1208 let mut binds = BTreeSet::new();
1209 for f in &d.for_each {
1210 if !valid_name(&f.bind) || !binds.insert(f.bind.clone()) {
1211 inv(
1212 subject.clone(),
1213 format!("bind {:?} must be unique and valid", f.bind),
1214 )?;
1215 }
1216 if step_ref(&f.over).is_none() {
1217 inv(
1218 subject.clone(),
1219 format!("for_each over {:?} must be a step", f.over),
1220 )?;
1221 }
1222 known(&f.over).or_else(|e| inv(subject.clone(), e))?;
1223 if let ItemSource::List { list, .. } = &f.items {
1224 known(list).or_else(|e| inv(subject.clone(), e))?;
1225 }
1226 }
1227 for r in &d.project {
1228 match Ref::parse(r) {
1229 Ok(Ref::Bind(b)) if !binds.contains(&b) => {
1230 inv(subject.clone(), format!("unknown bind {b:?}"))?
1231 }
1232 Ok(Ref::Bind(_)) => {}
1233 Ok(Ref::Input(_, p)) if p.is_empty() => {
1234 known(r).or_else(|e| inv(subject.clone(), e))?
1235 }
1236 Ok(Ref::Step(_)) => known(r).or_else(|e| inv(subject.clone(), e))?,
1237 Ok(_) => inv(subject.clone(), format!("{r:?} cannot be projected"))?,
1238 Err(e) => inv(subject.clone(), e)?,
1239 }
1240 }
1241 if matches!(d.calibration, CalibrationRef::Id(_))
1242 && d.requires != RequiredKind::CalibratedProbability
1243 {
1244 inv(
1245 subject.clone(),
1246 "a calibration is bound only to a calibrated_probability step".into(),
1247 )?;
1248 }
1249 if let BindingChoice::Backend(b) = &d.backend.binding
1250 && b.is_empty()
1251 {
1252 inv(subject.clone(), "binding names no backend".into())?;
1253 }
1254 if let Fallback::Kind { reason, .. } = &d.fallback
1255 && reason.is_empty()
1256 {
1257 inv(subject.clone(), "a fallback states its reason".into())?;
1258 }
1259 }
1260 }
1261 }
1262 if def.limits.distribution_tolerance <= Decimal::ZERO
1263 || def.limits.distribution_tolerance > Decimal::parse("0.1").unwrap_or(Decimal::ZERO)
1264 {
1265 inv(
1266 "limits".into(),
1267 "distribution_tolerance must be in (0, 0.1]".into(),
1268 )?;
1269 }
1270 let policy = &def.policy;
1272 for (i, h) in policy.on_unresolved.iter().enumerate() {
1273 let subject = format!("policy:on_unresolved[{i}]");
1274 match Ref::parse(&h.target) {
1275 Ok(Ref::Input(_, p)) if p.is_empty() => {
1276 known(&h.target).or_else(|e| inv(subject.clone(), e))?
1277 }
1278 Ok(Ref::Step(_)) => known(&h.target).or_else(|e| inv(subject.clone(), e))?,
1279 _ => inv(
1280 subject.clone(),
1281 format!("handler target {:?} must be an input or step", h.target),
1282 )?,
1283 }
1284 if matches!(&h.reasons, ReasonSet::Only(v) if v.is_empty()) {
1285 inv(subject.clone(), "handler covers no reason".into())?;
1286 }
1287 if matches!(&h.action, HandlerAction::Escalate { reason } if reason.is_empty()) {
1288 inv(subject, "an escalation states its reason".into())?;
1289 }
1290 }
1291 let mut rule_ids = BTreeSet::new();
1292 let mut proposed_params: BTreeSet<String> = BTreeSet::new();
1293 for r in &policy.rules {
1294 let subject = format!("policy:rule:{}", r.id);
1295 if !valid_name(&r.id) || !rule_ids.insert(r.id.clone()) {
1296 inv(subject.clone(), "rule ids must be unique and valid".into())?;
1297 }
1298 cond_structure(&r.when, &options_of, &known, &ranking_kinds)
1299 .or_else(|e| inv(subject.clone(), e))?;
1300 let mut refs = Vec::new();
1301 outcome_refs(&r.then, &mut refs);
1302 for x in &refs {
1303 known(x).or_else(|e| inv(subject.clone(), e))?;
1304 }
1305 match &r.then {
1306 OutcomeDecl::Propose { action, params } => {
1307 let names: BTreeSet<_> = params.iter().map(|p| &p.name).collect();
1308 if action.is_empty()
1309 || names.len() != params.len()
1310 || params.iter().any(|p| p.name.is_empty())
1311 {
1312 inv(
1313 subject.clone(),
1314 "a proposal has an action and unique parameter names".into(),
1315 )?;
1316 }
1317 proposed_params.extend(params.iter().map(|p| p.name.clone()));
1318 }
1319 OutcomeDecl::Escalate { reason } if reason.is_empty() => {
1320 inv(subject.clone(), "an escalation states its reason".into())?
1321 }
1322 _ => {}
1323 }
1324 }
1325 match policy.rules.last() {
1326 Some(r) if r.when == Cond::Always => {}
1327 _ => inv(
1328 "policy".into(),
1329 "the last rule's condition must be `always`".into(),
1330 )?,
1331 }
1332 let mut adj_ids = BTreeSet::new();
1333 for a in &policy.adjustments {
1334 let subject = format!("policy:adjustment:{}", a.id);
1335 if !valid_name(&a.id) || !adj_ids.insert(a.id.clone()) {
1336 inv(
1337 subject.clone(),
1338 "adjustment ids must be unique and valid".into(),
1339 )?;
1340 }
1341 let scale: BTreeSet<_> = a.scale.iter().collect();
1342 if a.scale.len() < 2 || scale.len() != a.scale.len() {
1343 inv(
1344 subject.clone(),
1345 "a scale has at least two unique positions".into(),
1346 )?;
1347 }
1348 if !proposed_params.contains(&a.param) {
1349 inv(
1350 subject.clone(),
1351 format!("no proposal has parameter {:?}", a.param),
1352 )?;
1353 }
1354 cond_structure(&a.when, &options_of, &known, &ranking_kinds)
1355 .or_else(|e| inv(subject, e))?;
1356 }
1357 if v2 {
1360 let mut taken: BTreeSet<String> = ids.iter().cloned().collect();
1361 taken.extend(rule_ids.iter().cloned());
1362 taken.extend(adj_ids.iter().cloned());
1363 crate::actionable::structure(def, &inputs, &taken)?;
1364 }
1365
1366 let authored = def;
1367 let screens = screening::validate(def)?;
1368 let lowered = screening::lower(def, &screens, &mut decoded);
1369 let def = &lowered;
1370 let ids: Vec<String> = def.steps.iter().map(|s| s.id.clone()).collect();
1371
1372 let calibration_ids: Vec<(CalibrationId, &CalibrationArtifact)> = calibrations
1374 .iter()
1375 .map(|c| c.id().map(|id| (id, c)))
1376 .collect::<Result<_, _>>()
1377 .map_err(|e| Refusal {
1378 category: C::Parse,
1379 subject: "calibration".into(),
1380 detail: e.to_string(),
1381 shortfalls: vec![],
1382 })?;
1383 let mut resolved_cal: BTreeMap<String, (CalibrationArtifact, CalibrationId)> = BTreeMap::new();
1384 for s in &def.steps {
1385 let subject = format!("step:{}", s.id);
1386 match &s.body {
1387 StepBody::Extract(_) | StepBody::AcceptModelDerived(_) => {
1388 }
1390 StepBody::Rank(_) | StepBody::Select(_) => {}
1391 StepBody::Generate(_) => {}
1392 StepBody::Acquire(_) | StepBody::Widen(_) => {
1393 crate::acquisition::resolve(&s.id, &s.body, sources)?;
1394 }
1395 StepBody::Corroborate(_) => {}
1396 StepBody::Exact(e) => {
1397 let (registered, registry) = if v2 {
1398 (
1399 ops::is_registered_v2(&e.op, e.version),
1400 schema::EXACT_REGISTRY_V2,
1401 )
1402 } else {
1403 (ops::is_registered(&e.op, e.version), schema::EXACT_REGISTRY)
1404 };
1405 if !registered {
1406 return refuse(
1407 C::UnknownOperator,
1408 subject,
1409 format!("{}@{} is not in {registry}", e.op, e.version),
1410 );
1411 }
1412 }
1413 StepBody::Semantic(d) => {
1414 if d.requires != RequiredKind::CalibratedProbability {
1415 continue;
1416 }
1417 let CalibrationRef::Id(cid) = &d.calibration else {
1418 return refuse(
1419 C::CalibrationBinding,
1420 subject,
1421 "a calibrated_probability step names no calibration",
1422 );
1423 };
1424 let Some((_, art)) = calibration_ids.iter().find(|(id, _)| id == cid) else {
1425 return refuse(
1426 C::CalibrationBinding,
1427 subject,
1428 format!("calibration {cid} was not supplied"),
1429 );
1430 };
1431 calibrate::check_static(art, &d.task, &s.id, &d.options).map_err(|e| Refusal {
1432 category: C::CalibrationBinding,
1433 subject: subject.clone(),
1434 detail: e.to_string(),
1435 shortfalls: vec![],
1436 })?;
1437 resolved_cal.insert(s.id.clone(), ((*art).clone(), cid.clone()));
1438 }
1439 }
1440 }
1441
1442 let prompts = crate::generation::resolve(def, templates)?;
1443 crate::extraction_compile::resolve(def, programs)?;
1444
1445 let mut bound = if v2 {
1448 quality::bind_sources(def, sources)?
1449 } else {
1450 quality::Bound::default()
1451 };
1452
1453 let mut deps: BTreeMap<String, Vec<String>> = BTreeMap::new();
1455 for s in &def.steps {
1456 let mut refs: Vec<String> = match &s.body {
1457 StepBody::Extract(e) => e
1458 .projection
1459 .iter()
1460 .cloned()
1461 .chain(e.template.params.values().filter_map(|b| match b {
1462 rustev_contract::generation::TemplateBinding::Reference(r) => Some(r.clone()),
1463 _ => None,
1464 }))
1465 .collect(),
1466 StepBody::AcceptModelDerived(a) => crate::extraction_compile::accept_refs(a)
1467 .into_iter()
1468 .filter(|r| {
1469 let base = base_ref_v2(r);
1472 base.strip_prefix("step:")
1473 .is_none_or(|id| ids.iter().any(|s| s == id))
1474 })
1475 .collect(),
1476 StepBody::Rank(_) | StepBody::Select(_) => ranking::refs(&s.body),
1477 StepBody::Generate(g) => crate::generation::dependencies(g, def),
1478 StepBody::Acquire(_) | StepBody::Widen(_) => crate::acquisition::refs(&s.body),
1479 StepBody::Corroborate(c) => corroborate_refs(c),
1480 StepBody::Exact(_) => decoded[&s.id].refs(),
1481 StepBody::Semantic(d) => {
1482 let mut v = Vec::new();
1483 if let Candidates::From(r) = &d.candidates {
1484 v.push(r.clone());
1485 }
1486 for f in &d.for_each {
1487 v.push(f.over.clone());
1488 if let ItemSource::List { list, .. } = &f.items {
1489 v.push(list.clone());
1490 }
1491 }
1492 v.extend(
1493 d.project
1494 .iter()
1495 .filter(|r| !r.starts_with("bind:"))
1496 .cloned(),
1497 );
1498 v
1499 }
1500 };
1501 if v2 {
1502 refs = refs.iter().map(|r| base_ref_v2(r)).collect();
1504 }
1505 let mut seen = BTreeSet::new();
1506 refs.retain(|r| seen.insert(r.clone()));
1507 deps.insert(s.id.clone(), refs);
1508 }
1509 screening::dependencies(authored, &screens, &decoded, &mut deps);
1510 let order = topo_order(&ids, &deps)?;
1511
1512 let input_tys: BTreeMap<String, Ty> = inputs
1514 .iter()
1515 .map(|(k, (t, _))| (k.clone(), t.clone()))
1516 .collect();
1517 crate::extraction_compile::question_selections(def)?;
1518 crate::extraction_compile::model_gate(def)?;
1519 let mut step_tys: BTreeMap<String, StepTy> = BTreeMap::new();
1520 for (field, _) in crate::extraction_compile::distribution_options(def) {
1526 step_tys.insert(field, StepTy::Semantic(SemKind::Distribution, vec![]));
1527 }
1528 let mut infos: BTreeMap<String, StepInfo> = BTreeMap::new();
1529 let mut projection_bounds: BTreeMap<String, u64> = BTreeMap::new();
1530 let mut max_requests: BTreeMap<String, u64> = BTreeMap::new();
1531 let by_id: BTreeMap<&str, &rustev_contract::definition::StepDecl> =
1532 def.steps.iter().map(|s| (s.id.as_str(), s)).collect();
1533 let mut acquisition_derivations = BTreeMap::new();
1534 for id in &order {
1535 let s = by_id[id.as_str()];
1536 let subject = format!("step:{id}");
1537 let env = TyEnv {
1538 inputs: &input_tys,
1539 steps: &step_tys,
1540 item: None,
1541 v2,
1542 };
1543 let km = |e: String| Refusal {
1544 category: C::KindMismatch,
1545 subject: subject.clone(),
1546 detail: e,
1547 shortfalls: vec![],
1548 };
1549 match &s.body {
1550 StepBody::Extract(e) => {
1551 step_tys.insert(
1552 id.clone(),
1553 StepTy::Exact(crate::extraction_compile::record_ty(e, &input_tys)),
1554 );
1555 infos.insert(id.clone(), StepInfo::Extract);
1556 }
1557 StepBody::AcceptModelDerived(a) => {
1558 let ty = crate::extraction_compile::accept(
1559 id,
1560 a,
1561 def,
1562 &env,
1563 &acquisition_derivations,
1564 calibrations,
1565 )?;
1566 step_tys.insert(id.clone(), StepTy::Exact(ty));
1567 infos.insert(id.clone(), StepInfo::Accept(a.clone()));
1568 }
1569 StepBody::Rank(_) | StepBody::Select(_) => {
1570 let (ty, bytes) = ranking::types(id, &s.body, &env, &acquisition_derivations)?;
1571 projection_bounds.insert(id.clone(), bytes);
1572 step_tys.insert(id.clone(), ty);
1573 infos.insert(id.clone(), StepInfo::Ranking);
1574 }
1575 StepBody::Generate(_) => {
1576 infos.insert(id.clone(), StepInfo::Generate);
1577 }
1578 StepBody::Acquire(_) | StepBody::Widen(_) => {
1579 let a = crate::acquisition::compile(
1580 id,
1581 &s.body,
1582 sources,
1583 &env,
1584 &acquisition_derivations,
1585 def,
1586 )?;
1587 step_tys.insert(id.clone(), StepTy::Exact(a.ty.clone()));
1588 infos.insert(id.clone(), StepInfo::Acquisition(Box::new(a)));
1589 }
1590 StepBody::Corroborate(c) => {
1591 let ty = crate::expr::ref_ty(&c.claim, &env).map_err(km)?;
1593 for check in &c.by {
1594 type_predicate(&check.predicate, &env).map_err(km)?;
1595 }
1596 step_tys.insert(id.clone(), StepTy::Exact(ty.clone()));
1597 infos.insert(
1598 id.clone(),
1599 StepInfo::Corroborate {
1600 decl: Box::new(c.clone()),
1601 ty,
1602 },
1603 );
1604 }
1605 StepBody::Exact(_) => {
1606 let args = decoded[id].clone();
1607 if let OpArgs::MergeFields(m) = &args {
1608 let overlay = m
1610 .overlay
1611 .strip_prefix("step:")
1612 .map(|s| crate::expr::split_step(s).0);
1613 if let Some(OpArgs::JoinKeyed(j)) =
1614 overlay.as_deref().and_then(|o| decoded.get(o))
1615 && j.left != m.base
1616 {
1617 return refuse(
1618 C::InvalidDefinition,
1619 subject,
1620 format!("the overlay joins {}, not the base list {}", j.left, m.base),
1621 );
1622 }
1623 if let Some(e) = args.merge_rule_error(&env) {
1624 return refuse(C::InvalidDefinition, subject, e);
1625 }
1626 }
1627 let ty = args.type_check(&env).map_err(km)?;
1628 step_tys.insert(id.clone(), StepTy::Exact(ty.clone()));
1629 infos.insert(id.clone(), StepInfo::Exact { args, ty });
1630 }
1631 StepBody::Semantic(d) => {
1632 let mut requests: u64 = 1;
1633 let mut instance_bytes: u64 = 0;
1634 let mut bind_items = BTreeMap::new();
1635 for f in &d.for_each {
1636 let t = crate::expr::ref_ty(&f.over, &env).map_err(km)?;
1637 let (max, id_bytes) = Value::id_bounds(&t)
1638 .ok_or_else(|| km(format!("{} yields {}, not ids", f.over, t.name())))?;
1639 requests = requests.saturating_mul(max);
1640 instance_bytes = instance_bytes
1641 .saturating_add(2 + 6 * f.bind.len() as u64)
1642 .saturating_add(2 + 6 * id_bytes)
1643 .saturating_add(2);
1644 if let ItemSource::List { list, id_field } = &f.items {
1645 match crate::expr::ref_ty(list, &env).map_err(km)? {
1646 Ty::List { item, .. } => match item.as_ref() {
1647 Ty::Record(fields)
1648 if fields.iter().any(|(n, t)| {
1649 n == id_field && matches!(t, Ty::Text { .. } | Ty::Enum(_))
1650 }) =>
1651 {
1652 bind_items.insert(f.bind.clone(), (*item).clone());
1653 }
1654 _ => {
1655 return Err(km(format!(
1656 "{list} has no text id field {id_field:?}"
1657 )));
1658 }
1659 },
1660 t => return Err(km(format!("{list} is {}, not a list", t.name()))),
1661 }
1662 }
1663 }
1664 let mut candidate_bytes = 0u64;
1665 if let Candidates::From(r) = &d.candidates {
1666 let t = crate::expr::ref_ty(r, &env).map_err(km)?;
1667 let (max, id_bytes) = Value::id_bounds(&t)
1668 .ok_or_else(|| km(format!("{r} yields {}, not ids", t.name())))?;
1669 candidate_bytes = max.saturating_mul(2 + 6 * id_bytes + 1);
1670 }
1671 let mut value_bytes = 0u64;
1672 for r in &d.project {
1673 let t = match Ref::parse(r).map_err(km)? {
1674 Ref::Bind(b) if d.requires == RequiredKind::ScreenVerdict => {
1675 let (bind, field) = b
1676 .split_once('/')
1677 .ok_or_else(|| km("invalid lowered field".into()))?;
1678 match bind_items.get(bind) {
1679 Some(Ty::Record(fields)) => fields
1680 .iter()
1681 .find(|(name, _)| name == field)
1682 .map(|(_, ty)| ty.clone())
1683 .ok_or_else(|| km("unknown lowered field".into()))?,
1684 _ => return Err(km("invalid lowered item".into())),
1685 }
1686 }
1687 Ref::Bind(b) => bind_items.get(&b).cloned().ok_or_else(|| {
1688 km(format!("bind {b:?} has no item source to project"))
1689 })?,
1690 Ref::Input(n, path) => crate::expr::ref_ty(
1691 &format!("input:{}", [vec![n], path].concat().join("/")),
1692 &env,
1693 )
1694 .map_err(km)?,
1695 Ref::Step(n) => {
1696 let base = if v2 { crate::expr::split_step(&n).0 } else { n };
1697 match step_tys.get(&base) {
1698 Some(StepTy::ModelRanking(_)) => {
1699 return Err(km(format!(
1700 "{r} is a model ranking; only exact values are projected"
1701 )));
1702 }
1703 Some(StepTy::Semantic(..)) => {
1704 return Err(km(format!(
1705 "{r} is semantic; only exact values are projected"
1706 )));
1707 }
1708 Some(StepTy::Selection(_)) | Some(StepTy::Exact(_)) => {}
1709 None => return Err(km(format!("{r} is not a declared step"))),
1710 }
1711 let t = crate::expr::ref_ty(r, &env).map_err(km)?;
1712 if matches!(
1713 t,
1714 Ty::Filtered { .. } | Ty::Shortlist { .. } | Ty::Ranking { .. }
1715 ) {
1716 return Err(km(format!(
1717 "{r} is a filter, shortlist or ranking summary; project its ids through for_each instead"
1718 )));
1719 }
1720 t
1721 }
1722 _ => return Err(km(format!("{r} cannot be projected"))),
1723 };
1724 value_bytes = value_bytes
1725 .saturating_add(2 + 6 * r.len() as u64 + 1)
1726 .saturating_add(t.max_json_bytes())
1727 .saturating_add(1);
1728 }
1729 let envelope = json!({
1730 "candidates": [], "instance": {}, "operation": d.operation, "options": d.options,
1731 "question": d.question, "task": d.task, "values": {},
1732 });
1733 let base = canonical_value_bytes(&envelope)
1734 .map(|b| b.len() as u64)
1735 .unwrap_or(u64::MAX);
1736 let bound = base
1737 .saturating_add(candidate_bytes)
1738 .saturating_add(instance_bytes)
1739 .saturating_add(value_bytes);
1740 let bound = screens
1741 .iter()
1742 .find(|f| &f.step == id)
1743 .map_or(bound, |f| screening::projection_bound(f.max_bytes));
1744 projection_bounds.insert(id.clone(), bound);
1745 max_requests.insert(id.clone(), requests);
1746 let kind = sem_kind(d.operation, d.requires);
1747 let binds: Vec<String> = d.for_each.iter().map(|f| f.bind.clone()).collect();
1748 step_tys.insert(id.clone(), StepTy::Semantic(kind, binds));
1749 infos.insert(
1750 id.clone(),
1751 StepInfo::Semantic {
1752 decl: Box::new(d.clone()),
1753 kind,
1754 binding: None,
1755 calibration: resolved_cal.get(id).cloned(),
1756 },
1757 );
1758 }
1759 }
1760 acquisition_derivations = static_derivations(
1761 def,
1762 &inputs,
1763 &order[..=order.iter().position(|x| x == id).unwrap()],
1764 &deps,
1765 &infos,
1766 );
1767 }
1768
1769 crate::extraction_compile::types(def, &input_tys, &step_tys)?;
1770 crate::extraction_compile::templates(
1771 def,
1772 &prompts,
1773 &input_tys,
1774 &step_tys,
1775 &acquisition_derivations,
1776 )?;
1777 crate::extraction_compile::budget(def, &input_tys, &step_tys, execution)?;
1778 ranking::consumers(def, &deps, &step_tys, &decoded)?;
1779 screening::coverage(authored, &screens, &decoded)?;
1780 let mut generated = crate::generation::type_check(
1781 def,
1782 &prompts,
1783 &input_tys,
1784 &step_tys,
1785 &acquisition_derivations,
1786 &deps,
1787 )?;
1788
1789 crate::actionable::types(def, &input_tys, v2)?;
1791
1792 let mut trees: BTreeMap<String, quality::Ceil> = BTreeMap::new();
1796 let mut grade_table: Vec<StaticGrade> = Vec::new();
1797 if v2 {
1798 for (name, (_, decl)) in &inputs {
1799 trees.insert(
1800 format!("input:{name}"),
1801 quality::input_tree(&decl.ty, bound.inputs.get(name), None),
1802 );
1803 }
1804 for id in &order {
1805 let tree = match &infos[id] {
1806 StepInfo::Ranking | StepInfo::Generate | StepInfo::Extract => {
1807 quality::Ceil::leaf(rustev_contract::grade::ValueGrade::Ungraded, false)
1808 }
1809 StepInfo::Accept(a) => quality::tree_at(&trees, &a.field, &[]),
1810 StepInfo::Acquisition(a) => a.tree.clone(),
1811 StepInfo::Exact { args, .. } => quality::step_tree(args, &trees),
1812 StepInfo::Semantic { decl, .. } => quality::semantic_tree(decl, &trees),
1813 StepInfo::Corroborate { decl, .. } => quality::corroborate_tree(decl, &trees),
1814 };
1815 trees.insert(format!("step:{id}"), tree);
1816 }
1817 for s in &def.steps {
1818 let subject = format!("step:{}", s.id);
1819 let found = match &infos[&s.id] {
1820 StepInfo::Acquisition(a) => a
1821 .reads
1822 .iter()
1823 .find_map(|r| quality::requirement_refusal(&subject, r, &trees)),
1824 StepInfo::Exact { args, .. } => quality::op_reads(args)
1825 .iter()
1826 .filter(|r| r.gate || r.require.is_some())
1827 .find_map(|r| quality::requirement_refusal(&subject, r, &trees)),
1828 StepInfo::Corroborate { decl, .. } => {
1829 quality::corroborate_refusal(&subject, decl, &trees)
1830 }
1831 StepInfo::Ranking
1832 | StepInfo::Generate
1833 | StepInfo::Extract
1834 | StepInfo::Accept(_)
1835 | StepInfo::Semantic { .. } => None,
1836 };
1837 if let Some(r) = found {
1838 return Err(r);
1839 }
1840 }
1841 for (subject, c) in quality::policy_conds(def) {
1842 let mut reads = Vec::new();
1843 quality::cond_reads(c, &mut reads);
1844 if let Some(r) = reads
1845 .iter()
1846 .find_map(|r| quality::requirement_refusal(&subject, r, &trees))
1847 {
1848 return Err(r);
1849 }
1850 }
1851 if !bound.inputs.is_empty()
1852 || infos
1853 .values()
1854 .any(|i| matches!(i, StepInfo::Acquisition(_)))
1855 {
1856 for name in inputs.keys() {
1857 let key = format!("input:{name}");
1858 trees[&key].walk(&key, &mut grade_table);
1859 }
1860 for s in &def.steps {
1861 let key = format!("step:{}", s.id);
1862 trees[&key].walk(&key, &mut grade_table);
1863 }
1864 }
1865 }
1866
1867 let mut sorted: Vec<&BackendDescriptor> = descriptors.iter().collect();
1869 sorted.sort_by(|a, b| a.backend_id.as_bytes().cmp(b.backend_id.as_bytes()));
1870
1871 let mut notices: Vec<Notice> = Vec::new();
1872 let mut details: BTreeMap<String, PlanStepDetail> = BTreeMap::new();
1873 let mut determinism = BTreeMap::new();
1874 let mut bind_ctx: BTreeMap<String, (u64, u64)> = BTreeMap::new();
1875 for s in &def.steps {
1876 let subject = format!("step:{}", s.id);
1877 let d = match &s.body {
1878 StepBody::Extract(e) => {
1879 let binding = crate::extraction_compile::bind(
1880 &s.id,
1881 e,
1882 crate::extraction_compile::needs_logprobs(def, &s.id),
1883 &prompts[&s.id],
1884 programs,
1885 &TyEnv {
1886 inputs: &input_tys,
1887 steps: &step_tys,
1888 item: None,
1889 v2,
1890 },
1891 &sorted,
1892 )?;
1893 details.insert(s.id.clone(), PlanStepDetail::Extract(Box::new(binding)));
1894 continue;
1895 }
1896 StepBody::AcceptModelDerived(a) => {
1897 details.insert(s.id.clone(), PlanStepDetail::AcceptModelDerived(a.clone()));
1898 continue;
1899 }
1900 StepBody::Rank(r) => {
1901 let StepTy::ModelRanking(pool) = step_tys[&s.id] else {
1902 unreachable!()
1903 };
1904 bind_ctx.insert(s.id.clone(), (pool, projection_bounds[&s.id]));
1905 let detail = ranking::bind(&s.id, r, pool, projection_bounds[&s.id], &sorted)?;
1906 if let PlanStepDetail::Unsupported { capability } = &detail {
1907 notices.push(Notice {
1908 screening: None,
1909 kind: NoticeKind::Fallback,
1910 subject: subject.clone(),
1911 reason: format!("unsupported: {capability}"),
1912 action: None,
1913 });
1914 }
1915 if let PlanStepDetail::Rank(binding) = &detail
1916 && let Some(backend) =
1917 sorted.iter().find(|b| b.backend_id == binding.backend_id)
1918 {
1919 determinism.insert(s.id.clone(), backend.determinism);
1920 }
1921 details.insert(s.id.clone(), detail);
1922 continue;
1923 }
1924 StepBody::Select(select) => {
1925 details.insert(s.id.clone(), PlanStepDetail::Select(select.clone()));
1926 continue;
1927 }
1928 StepBody::Generate(g) => {
1929 let binding = crate::generation::bind(&s.id, g, &generated[&s.id], &sorted)?;
1930 if let Some(checked) = generated.get_mut(&s.id) {
1931 checked.binding = binding.clone();
1932 }
1933 details.insert(s.id.clone(), PlanStepDetail::Generate(Box::new(binding)));
1934 continue;
1935 }
1936 StepBody::Semantic(d) => d,
1937 StepBody::Exact(e) => {
1938 details.insert(
1939 s.id.clone(),
1940 PlanStepDetail::Exact {
1941 op: e.op.clone(),
1942 version: e.version,
1943 },
1944 );
1945 continue;
1946 }
1947 StepBody::Acquire(_) | StepBody::Widen(_) => {
1948 details.insert(
1949 s.id.clone(),
1950 if matches!(s.body, StepBody::Acquire(_)) {
1951 PlanStepDetail::Acquire
1952 } else {
1953 PlanStepDetail::Widen
1954 },
1955 );
1956 continue;
1957 }
1958 StepBody::Corroborate(_) => {
1959 details.insert(s.id.clone(), PlanStepDetail::Corroborate);
1960 continue;
1961 }
1962 };
1963 let bound = projection_bounds[&s.id];
1964 let options_needed = match &d.candidates {
1965 Candidates::From(r) => match &step_tys[step_ref(r).unwrap_or_default()] {
1966 StepTy::Exact(t) => Value::id_bounds(t).map(|(m, _)| m).unwrap_or(0),
1967 StepTy::ModelRanking(_) | StepTy::Semantic(..) => 0,
1968 StepTy::Selection(ty) => Value::id_bounds(ty).map_or(0, |(n, _)| n),
1969 },
1970 Candidates::None => d.options.len() as u64,
1971 };
1972 bind_ctx.insert(s.id.clone(), (options_needed, bound));
1973 let candidates: Vec<&BackendDescriptor> = match &d.backend.binding {
1974 BindingChoice::Backend(b) => match sorted.iter().find(|x| &x.backend_id == b) {
1975 Some(x) => vec![*x],
1976 None => {
1977 return Err(Refusal {
1978 category: C::NoCapableBackend,
1979 subject,
1980 detail: format!("bound backend {b:?} was not supplied"),
1981 shortfalls: vec![Shortfall {
1982 backend_id: b.clone(),
1983 reasons: vec![ShortfallReason::UnknownBackend],
1984 }],
1985 });
1986 }
1987 },
1988 BindingChoice::Auto => sorted.clone(),
1989 };
1990 let consumers_ok = |k: SemKind| consumers_accept(def, &s.id, k);
1991 if let Fallback::Kind { requires, .. } = &d.fallback {
1992 if !kind_valid_for(d.operation, *requires) {
1993 return refuse(
1994 C::InvalidFallback,
1995 subject,
1996 format!("{:?} cannot fall back to {:?}", d.operation, requires),
1997 );
1998 }
1999 if *requires == RequiredKind::CalibratedProbability {
2000 return refuse(
2001 C::InvalidFallback,
2002 subject,
2003 "a fallback cannot require calibration",
2004 );
2005 }
2006 if let Err(e) = consumers_ok(sem_kind(d.operation, *requires)) {
2007 return refuse(
2008 C::InvalidFallback,
2009 subject,
2010 format!("consumers cannot accept the fallback kind: {e}"),
2011 );
2012 }
2013 }
2014 let evaluate =
2015 |required: RequiredKind| -> (Option<(&BackendDescriptor, OutputKind)>, Vec<Shortfall>) {
2016 let mut shortfalls = Vec::new();
2017 for b in &candidates {
2018 match screening::candidate(
2019 screens.iter().find(|f| f.step == s.id),
2020 d,
2021 b,
2022 required,
2023 options_needed,
2024 bound,
2025 ) {
2026 Ok(o) => return (Some((*b, o)), vec![]),
2027 Err(reasons) => shortfalls.push(Shortfall {
2028 backend_id: b.backend_id.clone(),
2029 reasons,
2030 }),
2031 }
2032 }
2033 (None, shortfalls)
2034 };
2035 let (found, shortfalls) = evaluate(d.requires);
2036 let (backend, output, requires, fallback_taken) = match (found, &d.fallback) {
2037 (Some((b, o)), _) => (b, o, d.requires, false),
2038 (None, Fallback::None) => {
2039 return Err(Refusal {
2040 category: C::NoCapableBackend,
2041 subject,
2042 detail: format!(
2043 "no backend satisfies {:?} {:?}, and no fallback is declared",
2044 d.operation, d.requires
2045 ),
2046 shortfalls,
2047 });
2048 }
2049 (None, Fallback::Unsupported) => {
2050 let capability = format!("{:?}:{:?}", d.operation, d.requires).to_lowercase();
2051 notices.push(Notice {
2052 screening: None,
2053 kind: NoticeKind::Fallback,
2054 subject: subject.clone(),
2055 reason: format!("unsupported: {capability}"),
2056 action: None,
2057 });
2058 details.insert(s.id.clone(), PlanStepDetail::Unsupported { capability });
2059 continue;
2060 }
2061 (None, Fallback::Kind { requires, reason }) => match evaluate(*requires) {
2062 (Some((b, o)), _) => {
2063 notices.push(Notice {
2064 screening: None,
2065 kind: NoticeKind::Fallback,
2066 subject: subject.clone(),
2067 reason: reason.clone(),
2068 action: None,
2069 });
2070 (b, o, *requires, true)
2071 }
2072 (None, fb_shortfalls) => {
2073 return Err(Refusal {
2074 category: C::InvalidFallback,
2075 subject,
2076 detail: format!(
2077 "neither {:?} nor the fallback {:?} is satisfied",
2078 d.requires, requires
2079 ),
2080 shortfalls: fb_shortfalls,
2081 });
2082 }
2083 },
2084 };
2085 determinism.insert(s.id.clone(), backend.determinism);
2086 let calibration = match (requires, resolved_cal.get(&s.id)) {
2087 (RequiredKind::CalibratedProbability, Some((art, cid))) => {
2088 if art.binding.artifact != backend.artifact {
2089 return refuse(
2090 C::CalibrationBinding,
2091 subject,
2092 format!(
2093 "calibration is bound to {}, the backend is {}",
2094 art.binding.artifact, backend.artifact
2095 ),
2096 );
2097 }
2098 BoundCalibration::Bound {
2099 id: cid.clone(),
2100 binding: art.binding.clone(),
2101 }
2102 }
2103 _ => BoundCalibration::None,
2104 };
2105 let normalization = match (&output, requires) {
2106 (
2107 OutputKind::Logits,
2108 RequiredKind::Distribution | RequiredKind::OrdinalDistribution,
2109 ) => Normalization::Softmax1,
2110 _ => Normalization::None,
2111 };
2112 let descriptor_id = backend.id().map_err(|e| Refusal {
2113 category: C::Parse,
2114 subject: subject.clone(),
2115 detail: e.to_string(),
2116 shortfalls: vec![],
2117 })?;
2118 let binding = SemanticBinding {
2119 backend_id: backend.backend_id.clone(),
2120 artifact: backend.artifact.clone(),
2121 descriptor: descriptor_id,
2122 output,
2123 requires,
2124 normalization,
2125 calibration,
2126 fallback_taken,
2127 max_requests: max_requests[&s.id],
2128 max_projection_bytes: bound,
2129 };
2130 if let Some(StepInfo::Semantic {
2131 binding: b,
2132 kind,
2133 calibration: cal,
2134 ..
2135 }) = infos.get_mut(&s.id)
2136 {
2137 *kind = sem_kind(d.operation, requires);
2138 if requires != RequiredKind::CalibratedProbability {
2139 *cal = None;
2140 }
2141 *b = Some(Box::new(binding.clone()));
2142 step_tys.insert(
2143 s.id.clone(),
2144 StepTy::Semantic(*kind, d.for_each.iter().map(|f| f.bind.clone()).collect()),
2145 );
2146 }
2147 details.insert(s.id.clone(), PlanStepDetail::Semantic(Box::new(binding)));
2148 }
2149
2150 crate::extraction_compile::accept_bindings(def, calibrations, &details, &mut notices)?;
2151
2152 let mut plan_execution = match execution {
2155 None => PlanExecution::None,
2156 Some(policy) => crate::execution::validate(policy, def, &sorted, &details, &bind_ctx)?,
2157 };
2158 if let PlanExecution::Declared(d) = &mut plan_execution {
2161 d.decision_cache = crate::execution::decision_cache(&d.policy, def, &order, &deps, v2)?;
2162 }
2163 crate::execution::available(&plan_execution, def)?;
2164
2165 screening::budget(authored, &screens, descriptors)?;
2167 crate::acquisition::budget(def, sources, execution)?;
2168 let mut total: u64 = crate::generation::budget(def, &details)?;
2169 total += def
2170 .steps
2171 .iter()
2172 .filter(|s| matches!(s.body, StepBody::Extract(_)))
2173 .count() as u64;
2174 for s in &def.steps {
2175 if let Some(PlanStepDetail::Extract(b)) = details.get(&s.id)
2176 && b.worst_case_request_bytes > def.limits.max_projection_bytes
2177 {
2178 return refuse(
2179 C::Budget,
2180 format!("step:{}", s.id),
2181 "extraction request exceeds max_projection_bytes",
2182 );
2183 }
2184 if let StepBody::Rank(r) = &s.body {
2185 let StepTy::ModelRanking(pool) = step_tys[&s.id] else {
2186 unreachable!()
2187 };
2188 let count = ranking::budget(&s.id, r, pool)?;
2189 if matches!(details[&s.id], PlanStepDetail::Rank(_)) {
2190 total = total.saturating_add(count);
2191 }
2192 if projection_bounds[&s.id] > def.limits.max_projection_bytes {
2193 return refuse(
2194 C::Budget,
2195 format!("step:{}", s.id),
2196 "rank projection exceeds max_projection_bytes",
2197 );
2198 }
2199 }
2200 if let Some(PlanStepDetail::Semantic(b)) = details.get(&s.id) {
2201 total = total.saturating_add(b.max_requests);
2202 if b.max_projection_bytes > def.limits.max_projection_bytes {
2203 return refuse(
2204 C::Budget,
2205 format!("step:{}", s.id),
2206 format!(
2207 "worst-case projection {} bytes exceeds max_projection_bytes {}",
2208 b.max_projection_bytes, def.limits.max_projection_bytes
2209 ),
2210 );
2211 }
2212 }
2213 }
2214 if total > def.limits.max_semantic_requests {
2215 match def.limits.on_excess {
2216 ExcessPolicy::Refuse => {
2217 return refuse(
2218 C::Budget,
2219 "limits",
2220 format!(
2221 "worst-case {total} semantic requests exceed max_semantic_requests {} without truncate_visible",
2222 def.limits.max_semantic_requests
2223 ),
2224 );
2225 }
2226 ExcessPolicy::TruncateVisible => notices.push(Notice {
2227 screening: None,
2228 kind: NoticeKind::Truncation,
2229 subject: "limits".into(),
2230 reason: format!(
2231 "worst case {total} requests; at most {} are issued",
2232 def.limits.max_semantic_requests
2233 ),
2234 action: None,
2235 }),
2236 }
2237 }
2238
2239 if v2 && let Err(detail) = quality::evidence_bound(def, &infos) {
2242 return refuse(C::Budget, "limits", format!("evidence-bound: {detail}"));
2243 }
2244
2245 let mut reasons = possible_reasons(def, &inputs, &order, &infos, &details);
2247 if v2 {
2248 extend_reasons_v2(
2249 def,
2250 &inputs,
2251 &order,
2252 &infos,
2253 &deps,
2254 &trees,
2255 &details,
2256 &mut reasons,
2257 );
2258 }
2259 check_policy(def, &input_tys, &step_tys, &reasons, &mut notices, v2)?;
2260
2261 let actionability = crate::actionable::phase_h2(def, &inputs, &deps, &reasons)?;
2264
2265 let derivations = static_derivations(def, &inputs, &order, &deps, &infos);
2267 let registry = if v2
2270 && (uses_exact_v2(def, &decoded)
2271 || def
2272 .steps
2273 .iter()
2274 .any(|s| matches!(s.body, StepBody::AcceptModelDerived(_))))
2275 {
2276 schema::EXACT_REGISTRY_V2
2277 } else {
2278 schema::EXACT_REGISTRY
2279 };
2280 let definition_id = authored.id().map_err(|e| Refusal {
2281 category: C::Parse,
2282 subject: "definition".into(),
2283 detail: e.to_string(),
2284 shortfalls: vec![],
2285 })?;
2286 let acquisitions: Vec<_> = def
2287 .steps
2288 .iter()
2289 .filter_map(|s| match &infos[&s.id] {
2290 StepInfo::Acquisition(a) => {
2291 use rustev_contract::acquisition_record::AcquisitionPossibleOutcome as O;
2292 let mut plan = a.plan.clone();
2293 for (reason, outcome) in [
2294 (ReasonKind::Skipped, O::Skipped),
2295 (ReasonKind::InsufficientGrade, O::InsufficientGrade),
2296 ] {
2297 if reasons[&format!("step:{}", s.id)].contains(&reason)
2298 && !plan.outcomes.contains(&outcome)
2299 {
2300 plan.outcomes.push(outcome);
2301 }
2302 }
2303 Some(plan)
2304 }
2305 _ => None,
2306 })
2307 .collect();
2308 for a in &acquisitions {
2309 for b in &a.sources {
2310 if !bound.sources.iter().any(|s| s.name == b.source.name) {
2311 bound.sources.push(b.source.clone());
2312 }
2313 }
2314 }
2315 bound.sources.sort_by(|a, b| a.name.cmp(&b.name));
2316 let plan = Plan {
2317 acquisitions: (!acquisitions.is_empty()).then_some(acquisitions),
2318 schema: if v2 { schema::PLAN_V3 } else { schema::PLAN }.into(),
2319 compiler: COMPILER.into(),
2320 registry: registry.into(),
2321 definition_id,
2322 definition: authored.clone(),
2323 order: order.clone(),
2324 steps: def
2325 .steps
2326 .iter()
2327 .map(|s| PlanStep {
2328 id: s.id.clone(),
2329 derivation: derivations[&s.id],
2330 detail: details[&s.id].clone(),
2331 })
2332 .collect(),
2333 notices,
2334 execution: plan_execution,
2335 sources: (!bound.sources.is_empty()).then(|| bound.sources.clone()),
2336 grades: (!grade_table.is_empty()).then_some(grade_table),
2337 actionability,
2338 instantiation: None,
2339 screening: screening::plan(authored, &screens, &details),
2340 };
2341 let id = plan.id().map_err(|e| Refusal {
2342 category: C::Parse,
2343 subject: "plan".into(),
2344 detail: e.to_string(),
2345 shortfalls: vec![],
2346 })?;
2347 Ok(Compiled {
2348 plan,
2349 id,
2350 info: Info {
2351 extraction_calibrations: calibrations.to_vec(),
2352 screen_categories: descriptors
2353 .iter()
2354 .filter_map(|d| {
2355 d.operations
2356 .iter()
2357 .find_map(|op| op.screen.as_ref())
2358 .map(|p| (d.backend_id.clone(), p.categories.clone()))
2359 })
2360 .collect(),
2361 generation: generated,
2362 inputs,
2363 step_ids: ids,
2364 steps: infos,
2365 deps,
2366 order,
2367 v2,
2368 bound,
2369 trees,
2370 determinism,
2371 },
2372 })
2373}
2374
2375fn uses_exact_v2(def: &Definition, decoded: &BTreeMap<String, OpArgs>) -> bool {
2378 decoded
2379 .values()
2380 .any(|a| matches!(a, OpArgs::JoinKeyed(_) | OpArgs::MergeFields(_)) || a.uses_v2())
2381 || def.steps.iter().any(|s| match &s.body {
2382 StepBody::Acquire(_) | StepBody::Widen(_) => true,
2383 StepBody::Corroborate(c) => {
2384 c.by.iter()
2385 .any(|k| crate::expr::predicate_uses_v2(&k.predicate))
2386 }
2387 _ => false,
2388 })
2389 || quality::policy_conds(def).any(|(_, c)| first_cond_v2(c))
2390}
2391
2392fn first_cond_v2(c: &Cond) -> bool {
2393 match c {
2394 Cond::Exact(p) => crate::expr::predicate_uses_v2(p),
2395 Cond::All(cs) | Cond::Any(cs) => cs.iter().any(first_cond_v2),
2396 Cond::Not(c) => first_cond_v2(c),
2397 _ => false,
2398 }
2399}
2400
2401fn base_ref_v2(r: &str) -> String {
2402 crate::expr::base_ref_v2(r)
2403}
2404
2405fn may_be_absent(t: &TypeDecl) -> bool {
2407 match t {
2408 TypeDecl::Nullable { .. } => true,
2409 TypeDecl::Record {
2410 present_when: Some(_),
2411 ..
2412 } => true,
2413 TypeDecl::Record { fields, .. } => fields.iter().any(|f| may_be_absent(&f.ty)),
2414 TypeDecl::List { item, .. } => may_be_absent(item),
2415 TypeDecl::Map { value, .. } => may_be_absent(value),
2416 _ => false,
2417 }
2418}
2419
2420#[allow(clippy::too_many_arguments)]
2425fn extend_reasons_v2(
2426 def: &Definition,
2427 inputs: &BTreeMap<String, (Ty, InputDecl)>,
2428 order: &[String],
2429 infos: &BTreeMap<String, StepInfo>,
2430 deps: &BTreeMap<String, Vec<String>>,
2431 trees: &BTreeMap<String, quality::Ceil>,
2432 details: &BTreeMap<String, PlanStepDetail>,
2433 reasons: &mut BTreeMap<String, BTreeSet<ReasonKind>>,
2434) {
2435 use ReasonKind as K;
2436 let graded_gate = |r: &quality::Read| {
2437 r.require.is_some() || (r.gate && quality::read_ceiling(r, trees).own.graded().is_some())
2438 };
2439 let mut absent: BTreeSet<String> = BTreeSet::new();
2440 for (name, (_, decl)) in inputs {
2441 let key = format!("input:{name}");
2442 if may_be_absent(&decl.ty) {
2443 absent.insert(key.clone());
2444 }
2445 if matches!(
2446 decl.on_disagreement,
2447 Some(rustev_contract::definition::Disagreement::Abstain)
2448 ) {
2449 reasons.entry(key).or_default().insert(K::Abstained);
2450 }
2451 }
2452 for id in order {
2453 let key = format!("step:{id}");
2454 let mut add: BTreeSet<ReasonKind> = BTreeSet::new();
2455 match &infos[id] {
2456 StepInfo::Exact { args, .. } => {
2457 let reads = quality::op_reads(args);
2458 let reads_absent = reads.iter().any(|r| absent.contains(&r.base));
2459 match args {
2460 OpArgs::JoinKeyed(_) | OpArgs::MergeFields(_) => {
2461 absent.insert(key.clone());
2462 }
2463 OpArgs::FilterWithReasons(_) | OpArgs::TopK(_) | OpArgs::FieldPresence(_) => {}
2465 _ if reads_absent => {
2466 add.insert(K::MissingEvidence);
2467 }
2468 _ => {}
2469 }
2470 if reads.iter().any(graded_gate) {
2471 add.insert(K::InsufficientGrade);
2472 }
2473 if !matches!(args, OpArgs::FieldPresence(_)) {
2474 for d in &deps[id] {
2475 let mut inherited = reasons.get(d).cloned().unwrap_or_default();
2476 if probe_only(args, d) {
2477 inherited.remove(&K::Skipped);
2478 }
2479 add.extend(inherited);
2480 }
2481 }
2482 }
2483 StepInfo::Corroborate { decl, .. } => {
2484 let (claim, _) = quality::claim_path(decl);
2485 if absent.contains(&claim) {
2486 absent.insert(key.clone());
2487 }
2488 add.extend(reasons.get(&claim).cloned().unwrap_or_default());
2489 match decl.on_contradiction {
2494 rustev_contract::definition::OnContradiction::Conflict => {
2495 add.insert(K::Conflict);
2496 }
2497 rustev_contract::definition::OnContradiction::Abstain => {
2498 add.insert(K::Abstained);
2499 }
2500 rustev_contract::definition::OnContradiction::TreatAsHint => {}
2501 }
2502 }
2503 StepInfo::Accept(a) => {
2504 add.extend(
2505 reasons
2506 .get(&base_ref_v2(&a.field))
2507 .cloned()
2508 .unwrap_or_default(),
2509 );
2510 if matches!(
2511 a.on_fail,
2512 rustev_contract::extraction::OnFail::DropField { .. }
2513 ) {
2514 absent.insert(key.clone());
2515 }
2516 }
2517 StepInfo::Ranking => {
2518 let body = &def.steps.iter().find(|s| &s.id == id).unwrap().body;
2519 let updated = ranking::reasons(body, &details[id], reasons);
2520 reasons.insert(key, updated);
2521 continue;
2522 }
2523 StepInfo::Generate | StepInfo::Extract => {}
2524 StepInfo::Acquisition(_) | StepInfo::Semantic { .. } => {
2525 for d in &deps[id] {
2526 add.extend(reasons.get(d).cloned().unwrap_or_default());
2527 }
2528 }
2529 }
2530 reasons.entry(key).or_default().extend(add);
2531 }
2532 for (_, c) in quality::policy_conds(def) {
2533 let mut reads = Vec::new();
2534 quality::cond_reads(c, &mut reads);
2535 for r in &reads {
2536 if graded_gate(r) {
2537 reasons
2538 .entry(r.base.clone())
2539 .or_default()
2540 .insert(K::InsufficientGrade);
2541 }
2542 if absent.contains(&r.base) {
2543 reasons
2544 .entry(r.base.clone())
2545 .or_default()
2546 .insert(K::MissingEvidence);
2547 }
2548 }
2549 }
2550}
2551
2552fn first_v2_construct(
2556 def: &Definition,
2557 decoded: &BTreeMap<String, OpArgs>,
2558) -> Option<(String, &'static str)> {
2559 fn type_v2(t: &TypeDecl) -> Option<&'static str> {
2560 match t {
2561 TypeDecl::Nullable { .. } => Some("a nullable type"),
2562 TypeDecl::Map { .. } => Some("a map type"),
2563 TypeDecl::Record {
2564 present_when: Some(_),
2565 ..
2566 } => Some("a presence predicate"),
2567 TypeDecl::Record { fields, .. } => fields.iter().find_map(|f| type_v2(&f.ty)),
2568 TypeDecl::List { item, .. } => type_v2(item),
2569 _ => None,
2570 }
2571 }
2572 fn cond_v2(c: &Cond) -> bool {
2573 match c {
2574 Cond::Exact(p) => crate::expr::predicate_uses_v2(p),
2575 Cond::All(cs) | Cond::Any(cs) => cs.iter().any(cond_v2),
2576 Cond::Not(c) => cond_v2(c),
2577 _ => false,
2578 }
2579 }
2580 for i in &def.inputs {
2581 let subject = format!("input:{}", i.name);
2582 if i.evidence.is_some() {
2583 return Some((subject, "an evidence declaration"));
2584 }
2585 if i.on_disagreement.is_some() {
2586 return Some((subject, "a disagreement rule"));
2587 }
2588 if let Some(c) = type_v2(&i.ty) {
2589 return Some((subject, c));
2590 }
2591 }
2592 for s in &def.steps {
2593 let subject = format!("step:{}", s.id);
2594 match &s.body {
2595 StepBody::Extract(_) => return Some((subject, "an extraction step")),
2596 StepBody::AcceptModelDerived(_) => {
2597 return Some((subject, "an accept_model_derived step"));
2598 }
2599 StepBody::Rank(_) | StepBody::Select(_) => return Some((subject, "a ranking step")),
2600 StepBody::Generate(_) => return Some((subject, "a generate step")),
2601 StepBody::Acquire(_) | StepBody::Widen(_) => {
2602 return Some((subject, "an acquisition step"));
2603 }
2604 StepBody::Corroborate(_) => return Some((subject, "a corroborate step")),
2605 StepBody::Exact(_) => {
2606 if decoded.get(&s.id).is_some_and(OpArgs::uses_v2) {
2607 return Some((subject, "a rustev.exact/2 predicate or graded reference"));
2608 }
2609 }
2610 StepBody::Semantic(_) => {}
2611 }
2612 }
2613 let policy = &def.policy;
2614 for r in &policy.rules {
2615 if cond_v2(&r.when) {
2616 return Some((
2617 format!("policy:rule:{}", r.id),
2618 "a rustev.exact/2 predicate or graded reference",
2619 ));
2620 }
2621 }
2622 for a in &policy.adjustments {
2623 if cond_v2(&a.when) {
2624 return Some((
2625 format!("policy:adjustment:{}", a.id),
2626 "a rustev.exact/2 predicate or graded reference",
2627 ));
2628 }
2629 }
2630 None
2631}
2632
2633fn corroborate_refs(c: &rustev_contract::definition::CorroborateDecl) -> Vec<String> {
2635 let mut out = vec![c.claim.clone()];
2636 for check in &c.by {
2637 predicate_refs(&check.predicate, &mut out);
2638 }
2639 out.into_iter()
2640 .filter(|r| r.starts_with("input:") || r.starts_with("step:"))
2641 .map(|r| crate::expr::base_ref(&r))
2642 .collect()
2643}
2644
2645fn cond_structure(
2646 c: &Cond,
2647 options_of: &BTreeMap<String, Vec<String>>,
2648 known: &dyn Fn(&str) -> Result<(), String>,
2649 deferred: &BTreeSet<&str>,
2650) -> Result<(), String> {
2651 let unit = Decimal::from_i64(1);
2652 if matches!(c, Cond::TopLabel { step, .. } | Cond::MassAtLeast { step, .. } | Cond::TopMassBelow { step, .. } | Cond::ExpectationAtLeast { step, .. } if deferred.contains(step.as_str()))
2653 {
2654 return Ok(());
2655 }
2656 let semantic_opts = |step: &str| -> Result<&Vec<String>, String> {
2657 known(&format!("step:{step}"))?;
2658 options_of
2659 .get(step)
2660 .ok_or_else(|| format!("{step:?} is not a semantic step"))
2661 };
2662 match c {
2663 Cond::Always => Ok(()),
2664 Cond::ProvenanceWithin { reference, .. } => known(reference),
2666 Cond::TopLabel { step, label } => {
2667 if !semantic_opts(step)?.contains(label) {
2668 return Err(format!("{label:?} is not an option of {step:?}"));
2669 }
2670 Ok(())
2671 }
2672 Cond::MassAtLeast {
2673 step,
2674 option,
2675 threshold,
2676 uncalibrated_threshold,
2677 } => {
2678 if !semantic_opts(step)?.contains(option) {
2679 return Err(format!("{option:?} is not an option of {step:?}"));
2680 }
2681 if *threshold < Decimal::ZERO || *threshold > unit {
2682 return Err("a probability threshold is in [0, 1]".into());
2683 }
2684 if matches!(uncalibrated_threshold, UncalibratedThreshold::Declared { reason } if reason.is_empty())
2685 {
2686 return Err("an uncalibrated threshold states its reason".into());
2687 }
2688 Ok(())
2689 }
2690 Cond::TopMassBelow {
2691 step,
2692 threshold,
2693 uncalibrated_threshold,
2694 } => {
2695 semantic_opts(step)?;
2696 if *threshold < Decimal::ZERO || *threshold > unit {
2697 return Err("a probability threshold is in [0, 1]".into());
2698 }
2699 if matches!(uncalibrated_threshold, UncalibratedThreshold::Declared { reason } if reason.is_empty())
2700 {
2701 return Err("an uncalibrated threshold states its reason".into());
2702 }
2703 Ok(())
2704 }
2705 Cond::ExpectationAtLeast { step, .. } => semantic_opts(step).map(|_| ()),
2706 Cond::Exact(p) => {
2707 predicate_structure(p)?;
2708 let mut refs = Vec::new();
2709 predicate_refs(p, &mut refs);
2710 refs.iter().try_for_each(|r| known(r))
2711 }
2712 Cond::Empty(r) | Cond::Nonempty(r) => known(r),
2713 Cond::All(cs) | Cond::Any(cs) => cs
2714 .iter()
2715 .try_for_each(|c| cond_structure(c, options_of, known, deferred)),
2716 Cond::Not(c) => cond_structure(c, options_of, known, deferred),
2717 }
2718}
2719
2720fn topo_order(
2723 ids: &[String],
2724 deps: &BTreeMap<String, Vec<String>>,
2725) -> Result<Vec<String>, Refusal> {
2726 let step_deps: BTreeMap<&str, BTreeSet<&str>> = ids
2727 .iter()
2728 .map(|id| {
2729 (
2730 id.as_str(),
2731 deps[id].iter().filter_map(|r| step_ref(r)).collect(),
2732 )
2733 })
2734 .collect();
2735 let mut done: BTreeSet<&str> = BTreeSet::new();
2736 let mut order = Vec::with_capacity(ids.len());
2737 loop {
2738 let next = ids.iter().find(|id| {
2739 !done.contains(id.as_str()) && step_deps[id.as_str()].iter().all(|d| done.contains(d))
2740 });
2741 match next {
2742 Some(id) => {
2743 done.insert(id);
2744 order.push(id.clone());
2745 }
2746 None => break,
2747 }
2748 }
2749 if order.len() == ids.len() {
2750 return Ok(order);
2751 }
2752 let stuck: Vec<String> = ids
2753 .iter()
2754 .filter(|id| !done.contains(id.as_str()))
2755 .cloned()
2756 .collect();
2757 refuse(
2758 Category::Cycle,
2759 format!("step:{}", stuck[0]),
2760 format!("the step graph has a cycle among {}", stuck.join(", ")),
2761 )
2762}
2763
2764fn consumers_accept(def: &Definition, step: &str, kind: SemKind) -> Result<(), String> {
2766 fn walk(c: &Cond, step: &str, kind: SemKind) -> Result<(), String> {
2767 match c {
2768 Cond::TopLabel { step: s, .. } if s == step => {
2769 if kind == SemKind::Scores {
2770 return Err("top_label needs labels".into());
2771 }
2772 Ok(())
2773 }
2774 Cond::MassAtLeast { step: s, .. } | Cond::TopMassBelow { step: s, .. } if s == step => {
2775 if kind.has_mass() {
2776 Ok(())
2777 } else {
2778 Err(format!(
2779 "a probability threshold cannot read {}",
2780 kind.name()
2781 ))
2782 }
2783 }
2784 Cond::ExpectationAtLeast { step: s, .. } if s == step => {
2785 if matches!(kind, SemKind::Ordinal(_)) {
2786 Ok(())
2787 } else {
2788 Err(format!("an expectation cannot read {}", kind.name()))
2789 }
2790 }
2791 Cond::All(cs) | Cond::Any(cs) => cs.iter().try_for_each(|c| walk(c, step, kind)),
2792 Cond::Not(c) => walk(c, step, kind),
2793 _ => Ok(()),
2794 }
2795 }
2796 for r in &def.policy.rules {
2797 walk(&r.when, step, kind)?;
2798 }
2799 for a in &def.policy.adjustments {
2800 walk(&a.when, step, kind)?;
2801 }
2802 for s in &def.steps {
2803 if let StepBody::Exact(e) = &s.body
2804 && let Some(Ok(OpArgs::WeightedRank(w))) = ops::decode(&e.op, e.version, &e.args)
2805 {
2806 for c in &w.components {
2807 match &c.source {
2808 ops::Source::PropositionMean { step: s2, .. }
2809 if step_ref(s2) == Some(step) && !kind.has_mass() =>
2810 {
2811 return Err(format!("proposition_mean cannot read {}", kind.name()));
2812 }
2813 ops::Source::RubricExpectation { step: s2 }
2814 if step_ref(s2) == Some(step) && !matches!(kind, SemKind::Ordinal(_)) =>
2815 {
2816 return Err(format!("rubric_expectation cannot read {}", kind.name()));
2817 }
2818 _ => {}
2819 }
2820 }
2821 }
2822 }
2823 Ok(())
2824}
2825
2826fn possible_reasons(
2828 def: &Definition,
2829 inputs: &BTreeMap<String, (Ty, InputDecl)>,
2830 order: &[String],
2831 infos: &BTreeMap<String, StepInfo>,
2832 details: &BTreeMap<String, PlanStepDetail>,
2833) -> BTreeMap<String, BTreeSet<ReasonKind>> {
2834 use ReasonKind as K;
2835 let mut out: BTreeMap<String, BTreeSet<ReasonKind>> = BTreeMap::new();
2836 for (name, (_, decl)) in inputs {
2837 let mut r: BTreeSet<_> = [K::MissingEvidence, K::InvalidInput, K::Conflict].into();
2838 if matches!(decl.freshness, Freshness::MaxAgeMs(_)) {
2839 r.insert(K::StaleEvidence);
2840 }
2841 out.insert(format!("input:{name}"), r);
2842 }
2843 crate::actionable::extend_reasons(def, &mut out);
2846 for id in order {
2847 let mut r = BTreeSet::new();
2848 match (&infos[id], &details[id]) {
2849 (StepInfo::Accept(a), _) => {
2850 r.extend(out.get(&base_ref_v2(&a.field)).cloned().unwrap_or_default());
2851 if matches!(a.on_fail, rustev_contract::extraction::OnFail::Unresolved) {
2852 r.extend([K::InvalidInput, K::MissingEvidence]);
2853 }
2854 }
2855 (StepInfo::Ranking, detail) => {
2856 let body = &def.steps.iter().find(|s| &s.id == id).unwrap().body;
2857 r = ranking::reasons(body, detail, &out);
2858 }
2859 (StepInfo::Generate, _) => {}
2860 (StepInfo::Extract, _) => {
2861 r.insert(K::MissingEvidence);
2862 }
2863 (StepInfo::Acquisition(a), _) => {
2864 r.extend([
2865 K::MissingEvidence,
2866 K::StaleEvidence,
2867 K::InvalidInput,
2868 K::BackendUnavailable,
2869 K::InvalidBackendOutput,
2870 K::BudgetExhausted,
2871 K::DeadlineExceeded,
2872 ]);
2873 if a.plan.outcomes.contains(
2874 &rustev_contract::acquisition_record::AcquisitionPossibleOutcome::Skipped,
2875 ) {
2876 r.insert(K::Skipped);
2877 }
2878 if a.plan.outcomes.contains(&rustev_contract::acquisition_record::AcquisitionPossibleOutcome::InsufficientGrade) { r.insert(K::InsufficientGrade); }
2879 }
2880 (_, PlanStepDetail::Unsupported { .. }) => {
2881 r.insert(K::Unsupported);
2882 }
2883 (StepInfo::Corroborate { decl, .. }, _) => {
2886 let (claim, _) = quality::claim_path(decl);
2887 r.extend(out.get(&claim).cloned().unwrap_or_default());
2888 }
2889 (StepInfo::Exact { args, .. }, _) => {
2890 if let OpArgs::FieldPresence(fp) = args {
2891 for i in &fp.inputs {
2892 let mut s = out.get(i).cloned().unwrap_or_default();
2893 s.remove(&K::MissingEvidence);
2894 r.extend(s);
2895 }
2896 } else {
2897 for d in args.refs() {
2898 let mut inherited = out.get(&d).cloned().unwrap_or_default();
2899 if probe_only(args, &d) {
2900 inherited.remove(&K::Skipped);
2901 }
2902 r.extend(inherited);
2903 }
2904 }
2905 r.extend(args.own_reasons());
2906 }
2907 (StepInfo::Semantic { decl, .. }, _) => {
2908 let mut refs: Vec<String> = decl
2909 .project
2910 .iter()
2911 .filter(|p| !p.starts_with("bind:"))
2912 .cloned()
2913 .collect();
2914 for f in &decl.for_each {
2915 refs.push(f.over.clone());
2916 if let ItemSource::List { list, .. } = &f.items {
2917 refs.push(list.clone());
2918 }
2919 }
2920 if let Candidates::From(c) = &decl.candidates {
2921 refs.push(c.clone());
2922 }
2923 for d in refs {
2924 r.extend(out.get(&d).cloned().unwrap_or_default());
2925 }
2926 if decl.for_each.is_empty() {
2927 r.extend([
2928 K::BackendUnavailable,
2929 K::InvalidBackendOutput,
2930 K::BudgetExhausted,
2931 K::DeadlineExceeded,
2932 ]);
2933 } else {
2934 r.insert(K::InvalidInput);
2936 }
2937 }
2938 }
2939 out.insert(format!("step:{id}"), r);
2940 }
2941 out
2942}
2943
2944pub(crate) fn predicate_value_refs(
2946 p: &rustev_contract::definition::Predicate,
2947 out: &mut Vec<String>,
2948) {
2949 use rustev_contract::definition::Predicate as P;
2950 match p {
2951 P::Present(_) | P::IsConstructed { .. } => {}
2952 P::All(ps) | P::Any(ps) => ps.iter().for_each(|p| predicate_value_refs(p, out)),
2953 P::Not(p) => predicate_value_refs(p, out),
2954 p => crate::expr::predicate_refs(p, out),
2955 }
2956}
2957fn condition_value_refs(c: &Cond, out: &mut Vec<String>) {
2958 match c {
2959 Cond::Exact(p) => predicate_value_refs(p, out),
2960 Cond::All(cs) | Cond::Any(cs) => cs.iter().for_each(|c| condition_value_refs(c, out)),
2961 Cond::Not(c) => condition_value_refs(c, out),
2962 c => condition_refs(c, out),
2963 }
2964}
2965pub(crate) fn probe_only(args: &OpArgs, target: &str) -> bool {
2966 if let OpArgs::Compare(c) = args {
2967 let mut values = vec![];
2968 predicate_value_refs(&c.predicate, &mut values);
2969 return !values.iter().any(|r| crate::expr::base_ref_v2(r) == target);
2970 }
2971 false
2972}
2973
2974fn cond_reads(c: &Cond, negated: bool, out: &mut Vec<(String, bool)>) {
2976 match c {
2977 Cond::Not(inner) => cond_reads(inner, !negated, out),
2978 Cond::All(cs) | Cond::Any(cs) => cs.iter().for_each(|c| cond_reads(c, negated, out)),
2979 other => {
2980 let mut refs = Vec::new();
2981 condition_refs(other, &mut refs);
2982 out.extend(refs.into_iter().map(|r| (r, negated)));
2983 }
2984 }
2985}
2986
2987fn check_policy(
2988 def: &Definition,
2989 input_tys: &BTreeMap<String, Ty>,
2990 step_tys: &BTreeMap<String, StepTy>,
2991 reasons: &BTreeMap<String, BTreeSet<ReasonKind>>,
2992 notices: &mut Vec<Notice>,
2993 v2: bool,
2994) -> Result<(), Refusal> {
2995 use Category as C;
2996 let base = |r: &str| {
2997 if v2 {
2998 crate::expr::base_ref_v2(r)
2999 } else {
3000 crate::expr::base_ref(r)
3001 }
3002 };
3003 let env = TyEnv {
3004 inputs: input_tys,
3005 steps: step_tys,
3006 item: None,
3007 v2,
3008 };
3009 let policy = &def.policy;
3010
3011 let items: Vec<(String, &Cond, Option<&OutcomeDecl>)> = policy
3013 .rules
3014 .iter()
3015 .map(|r| (format!("policy:rule:{}", r.id), &r.when, Some(&r.then)))
3016 .chain(
3017 policy
3018 .adjustments
3019 .iter()
3020 .map(|a| (format!("policy:adjustment:{}", a.id), &a.when, None)),
3021 )
3022 .collect();
3023 for (subject, cond, outcome) in &items {
3024 check_cond(cond, &env, subject, notices)?;
3025 if let Some(o) = outcome {
3026 match o {
3027 OutcomeDecl::Propose { params, .. } => {
3028 for p in params {
3029 if let ParamSource::Ref(r) = &p.value {
3030 match crate::expr::ref_ty(r, &env) {
3031 Ok(_) => {}
3032 Err(e) => {
3033 return refuse(
3034 C::KindMismatch,
3035 subject.clone(),
3036 format!("parameter {:?}: {e}", p.name),
3037 );
3038 }
3039 }
3040 }
3041 }
3042 }
3043 OutcomeDecl::MissingEvidenceFrom { step } => match step_tys.get(step) {
3044 Some(StepTy::Exact(Ty::List { item, .. }))
3045 if matches!(item.as_ref(), Ty::Text { .. }) => {}
3046 _ => {
3047 return refuse(
3048 C::KindMismatch,
3049 subject.clone(),
3050 format!("{step:?} does not list field names"),
3051 );
3052 }
3053 },
3054 OutcomeDecl::Escalate { .. } | OutcomeDecl::NeedsInput { .. } => {}
3055 }
3056 }
3057 }
3058
3059 let mut covered: BTreeMap<&str, BTreeSet<ReasonKind>> = BTreeMap::new();
3061 let mut unmet: BTreeSet<&str> = BTreeSet::new();
3062 for h in &policy.on_unresolved {
3063 let set: BTreeSet<ReasonKind> = match &h.reasons {
3064 ReasonSet::Any => ReasonKind::ALL.into_iter().collect(),
3065 ReasonSet::Only(v) => v.iter().copied().collect(),
3066 };
3067 if !matches!(
3068 h.action,
3069 HandlerAction::AsUnmet | HandlerAction::Escalate { .. }
3070 ) && set.contains(&ReasonKind::Skipped)
3071 && reasons
3072 .get(&h.target)
3073 .is_some_and(|r| r.contains(&ReasonKind::Skipped))
3074 {
3075 return refuse(
3076 C::UnhandledUnresolved,
3077 &h.target,
3078 "skipped_not_propagatable",
3079 );
3080 }
3081 if h.action == HandlerAction::AsUnmet {
3082 unmet.insert(&h.target);
3083 }
3084 covered.entry(&h.target).or_default().extend(set);
3085 }
3086 let mut reads: Vec<(String, String, bool, bool)> = Vec::new();
3087 for (subject, cond, outcome) in &items {
3088 let mut r = Vec::new();
3089 cond_reads(cond, false, &mut r);
3090 let mut values = vec![];
3091 condition_value_refs(cond, &mut values);
3092 reads.extend(
3093 r.into_iter()
3094 .filter(|(x, _)| x.starts_with("input:") || x.starts_with("step:"))
3095 .map(|(x, neg)| {
3096 let target = base(&x);
3097 let probe = !values.iter().any(|v| base(v) == target);
3098 (subject.clone(), target, neg, probe)
3099 }),
3100 );
3101 if let Some(o) = outcome {
3102 let mut refs = Vec::new();
3103 outcome_refs(o, &mut refs);
3104 reads.extend(
3105 refs.into_iter()
3106 .map(|x| (subject.clone(), base(&x), false, false)),
3107 );
3108 }
3109 }
3110 for (subject, target, negated, probe) in &reads {
3111 let mut possible = reasons.get(target).cloned().unwrap_or_default();
3112 if *probe {
3113 possible.remove(&ReasonKind::Skipped);
3114 }
3115 let have = covered.get(target.as_str()).cloned().unwrap_or_default();
3116 let missing: Vec<&str> = possible.difference(&have).map(|k| k.name()).collect();
3117 if !missing.is_empty() {
3118 return refuse(
3119 C::UnhandledUnresolved,
3120 subject.clone(),
3121 format!(
3122 "{target} can be unresolved with {} and no handler covers it",
3123 missing.join(", ")
3124 ),
3125 );
3126 }
3127 if *negated && unmet.contains(target.as_str()) {
3128 return refuse(
3129 C::UnhandledUnresolved,
3130 subject.clone(),
3131 format!("`not` over {target}, which is handled as_unmet"),
3132 );
3133 }
3134 }
3135 if let Some(last) = policy.rules.last() {
3136 let mut refs = Vec::new();
3137 outcome_refs(&last.then, &mut refs);
3138 if let Some(r) = refs
3139 .iter()
3140 .map(|r| base(r))
3141 .find(|r| unmet.contains(r.as_str()))
3142 {
3143 return refuse(
3144 C::UnhandledUnresolved,
3145 format!("policy:rule:{}", last.id),
3146 format!("the last rule reads {r}, which is handled as_unmet"),
3147 );
3148 }
3149 }
3150 Ok(())
3151}
3152
3153fn check_cond(
3154 c: &Cond,
3155 env: &TyEnv<'_>,
3156 subject: &str,
3157 notices: &mut Vec<Notice>,
3158) -> Result<(), Refusal> {
3159 use Category as C;
3160 let sem = |step: &str| -> Result<SemKind, Refusal> {
3161 match env.steps.get(step) {
3162 Some(StepTy::Semantic(k, binds)) if binds.is_empty() => Ok(*k),
3163 Some(StepTy::Semantic(..)) => refuse(
3164 C::KindMismatch,
3165 subject,
3166 format!("{step:?} fans out; the policy reads single values"),
3167 ),
3168 _ => refuse(
3169 C::KindMismatch,
3170 subject,
3171 format!("{step:?} is not a semantic step"),
3172 ),
3173 }
3174 };
3175 let threshold = |step: &str,
3176 u: &UncalibratedThreshold,
3177 notices: &mut Vec<Notice>|
3178 -> Result<(), Refusal> {
3179 match sem(step)? {
3180 SemKind::Calibrated => Ok(()),
3181 SemKind::Distribution => match u {
3182 UncalibratedThreshold::Declared { reason } => {
3183 notices.push(Notice {
3184 screening: None,
3185 kind: NoticeKind::UncalibratedThreshold,
3186 subject: subject.to_string(),
3187 reason: reason.clone(),
3188 action: None,
3189 });
3190 Ok(())
3191 }
3192 UncalibratedThreshold::NotDeclared => refuse(
3193 C::UncalibratedThreshold,
3194 subject,
3195 format!(
3196 "a probability threshold reads the uncalibrated distribution {step:?} without uncalibrated_threshold"
3197 ),
3198 ),
3199 },
3200 k => refuse(
3201 C::KindMismatch,
3202 subject,
3203 format!(
3204 "a probability threshold cannot read {} ({step:?})",
3205 k.name()
3206 ),
3207 ),
3208 }
3209 };
3210 match c {
3211 Cond::Always | Cond::ProvenanceWithin { .. } => Ok(()),
3212 Cond::TopLabel { step, .. } => match sem(step)? {
3213 SemKind::Scores => refuse(
3214 C::KindMismatch,
3215 subject,
3216 format!("top_label cannot read scores ({step:?})"),
3217 ),
3218 _ => Ok(()),
3219 },
3220 Cond::MassAtLeast {
3221 step,
3222 uncalibrated_threshold,
3223 ..
3224 }
3225 | Cond::TopMassBelow {
3226 step,
3227 uncalibrated_threshold,
3228 ..
3229 } => threshold(step, uncalibrated_threshold, notices),
3230 Cond::ExpectationAtLeast { step, .. } => match sem(step)? {
3231 SemKind::Ordinal(_) => Ok(()),
3232 k => refuse(
3233 C::KindMismatch,
3234 subject,
3235 format!("an expectation cannot read {} ({step:?})", k.name()),
3236 ),
3237 },
3238 Cond::Exact(p) => type_predicate(p, env).or_else(|e| refuse(C::KindMismatch, subject, e)),
3239 Cond::Empty(r) | Cond::Nonempty(r) => match crate::expr::ref_ty(r, env) {
3240 Ok(
3241 Ty::List { .. } | Ty::Filtered { .. } | Ty::Shortlist { .. } | Ty::Ranking { .. },
3242 ) => Ok(()),
3243 Ok(t) => refuse(C::KindMismatch, subject, format!("empty() of {}", t.name())),
3244 Err(e) => refuse(C::KindMismatch, subject, e),
3245 },
3246 Cond::All(cs) | Cond::Any(cs) => cs
3247 .iter()
3248 .try_for_each(|c| check_cond(c, env, subject, notices)),
3249 Cond::Not(c) => check_cond(c, env, subject, notices),
3250 }
3251}
3252
3253fn static_derivations(
3255 def: &Definition,
3256 inputs: &BTreeMap<String, (Ty, InputDecl)>,
3257 order: &[String],
3258 deps: &BTreeMap<String, Vec<String>>,
3259 infos: &BTreeMap<String, StepInfo>,
3260) -> BTreeMap<String, Derivation> {
3261 let semantic: BTreeSet<&str> = def
3262 .steps
3263 .iter()
3264 .filter(|s| {
3265 matches!(
3266 s.body,
3267 StepBody::Semantic(_)
3268 | StepBody::Generate(_)
3269 | StepBody::Extract(_)
3270 | StepBody::Rank(_)
3271 )
3272 })
3273 .map(|s| s.id.as_str())
3274 .collect();
3275 let mut out: BTreeMap<String, Derivation> = BTreeMap::new();
3276 for id in order {
3277 if def
3278 .steps
3279 .iter()
3280 .any(|s| &s.id == id && matches!(s.body, StepBody::Select(_)))
3281 {
3282 out.insert(id.clone(), Derivation::MixedDerived);
3283 continue;
3284 }
3285 if let Some(StepInfo::Acquisition(a)) = infos.get(id) {
3286 out.insert(id.clone(), a.plan.derivation);
3287 continue;
3288 }
3289 if let Some(StepInfo::Accept(a)) = infos.get(id) {
3290 let source = base_ref_v2(&a.field);
3291 let class = source
3292 .strip_prefix("step:")
3293 .and_then(|s| out.get(s))
3294 .copied()
3295 .unwrap_or(Derivation::ModelDerived);
3296 out.insert(id.clone(), class);
3297 continue;
3298 }
3299 if semantic.contains(id.as_str()) {
3300 out.insert(id.clone(), Derivation::ModelDerived);
3301 continue;
3302 }
3303 let claim_only;
3306 let reads: &[String] = match infos.get(id) {
3307 Some(StepInfo::Corroborate { decl, .. }) => {
3308 claim_only = vec![quality::claim_path(decl).0];
3309 &claim_only
3310 }
3311 _ => &deps[id],
3312 };
3313 let all_exact = reads.iter().all(|r| {
3314 if let Some(n) = r.strip_prefix("input:") {
3315 !inputs
3316 .get(n)
3317 .is_some_and(|(_, d)| d.provenance.contains(&ProvenanceClass::ModelDerived))
3318 } else if let Some(s) = step_ref(r) {
3319 out.get(s) == Some(&Derivation::ExactDerived)
3320 } else {
3321 true
3322 }
3323 });
3324 out.insert(
3325 id.clone(),
3326 if all_exact {
3327 Derivation::ExactDerived
3328 } else {
3329 Derivation::MixedDerived
3330 },
3331 );
3332 }
3333 out
3334}
3335
3336#[cfg(test)]
3337mod tests {
3338 use super::*;
3339
3340 #[test]
3341 fn v2_construct_describes_extraction_and_acceptance_separately() {
3342 let mut def: Definition =
3343 serde_json::from_str(include_str!("testdata/support-routing.definition.json")).unwrap();
3344 for (fixture, description) in [
3345 (
3346 include_str!("testdata/extract-step.json"),
3347 "an extraction step",
3348 ),
3349 (
3350 include_str!("testdata/accept-step.json"),
3351 "an accept_model_derived step",
3352 ),
3353 ] {
3354 def.steps = vec![serde_json::from_str(fixture).unwrap()];
3355 assert_eq!(
3356 first_v2_construct(&def, &BTreeMap::new()),
3357 Some((format!("step:{}", def.steps[0].id), description)),
3358 );
3359 }
3360 }
3361}