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