Skip to main content

rustev_contract/
definition.rs

1//! Decision definitions, `rustev.definition/1` (spec 002, 3.7 to 3.11) and
2//! `rustev.definition/2` (spec 032, 3.1.1).
3//!
4//! These are the wire types. Their validation, typing and compilation live
5//! in `rustev-core`; the typed builder there produces these same values, so
6//! the builder and the JSON form enter one validated representation (R-02).
7//! Every field is required: absence is always an explicit variant. The
8//! exception is spec 032's optional members, which version 1 never carries
9//! and which are omitted from canonical bytes when unused (spec 032, 3.1.2);
10//! the compiler refuses them in a version 1 definition. Spec 033 adds
11//! `actionability` and its policy constructs on the same terms.
12
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16use crate::actionability::{Actionability, Address, RemediationUse};
17use crate::decimal::Decimal;
18use crate::grade::{Grade, PresentWhen};
19use crate::ids::{ArtifactId, CalibrationId, DefinitionId};
20use crate::limits::DEFINITION_V1;
21use crate::schema;
22use crate::time::Timestamp;
23
24/// A versioned, authored description of a recurring decision.
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
26#[serde(deny_unknown_fields)]
27pub struct Definition {
28    pub schema: String,
29    pub name: String,
30    /// `MAJOR.MINOR.PATCH`, each without leading zeros.
31    pub version: String,
32    pub package: String,
33    pub inputs: Vec<InputDecl>,
34    #[serde(deserialize_with = "crate::generation::steps")]
35    pub steps: Vec<StepDecl>,
36    pub policy: PolicyDecl,
37    pub limits: LimitsDecl,
38    /// Version 2: provenance-conditional checks and actionable outcomes
39    /// (spec 033, 3.1.1); omitted when unused.
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub actionability: Option<Actionability>,
42    /// Optional screening contract (spec 035); omitted when unused.
43    #[serde(
44        default,
45        skip_serializing_if = "Option::is_none",
46        deserialize_with = "crate::screening::present"
47    )]
48    #[schemars(with = "crate::screening::Screening")]
49    pub screening: Option<crate::screening::Screening>,
50}
51
52crate::document::versioned_document!(
53    Definition,
54    [schema::DEFINITION, schema::DEFINITION_V2],
55    "rustev.definition/1 or rustev.definition/2",
56    DEFINITION_V1,
57    DefinitionId
58);
59
60/// One declared input field (spec 002, 3.7.1).
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
62#[serde(deny_unknown_fields)]
63pub struct InputDecl {
64    pub name: String,
65    #[serde(rename = "type")]
66    pub ty: TypeDecl,
67    /// The provenance classes this input accepts; non-empty.
68    pub provenance: Vec<ProvenanceClass>,
69    pub freshness: Freshness,
70    /// Version 2: the sources that grade this input (spec 032, 3.3.3);
71    /// an input without it is ungraded.
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub evidence: Option<EvidenceDecl>,
74    /// Version 2: how disagreeing entries are resolved (spec 032, 3.8);
75    /// omitted means `require_agreement`.
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub on_disagreement: Option<Disagreement>,
78}
79
80/// How an input is graded (spec 032, 3.3.3).
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
82#[serde(rename_all = "snake_case", deny_unknown_fields)]
83pub enum EvidenceDecl {
84    Graded {
85        /// Non-empty, sorted, duplicate-free source names.
86        sources: Vec<String>,
87        /// Lower the ceiling of a path and its descendants; never raise.
88        caps: Vec<Cap>,
89    },
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
93#[serde(deny_unknown_fields)]
94pub struct Cap {
95    pub path: String,
96    pub max_grade: Grade,
97}
98
99/// The conflict policy of a graded input (spec 032, 3.8). There is no
100/// recency rule: entry order never chooses a value.
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
102#[serde(rename_all = "snake_case", deny_unknown_fields)]
103pub enum Disagreement {
104    RequireAgreement,
105    Prefer { sources: Vec<String> },
106    TreatAsHint { sources: Vec<String> },
107    Abstain,
108}
109
110/// The closed set of provenance classes (spec 002, 3.7.1).
111#[derive(
112    Debug,
113    Clone,
114    Copy,
115    PartialEq,
116    Eq,
117    PartialOrd,
118    Ord,
119    Hash,
120    Serialize,
121    Deserialize,
122    schemars::JsonSchema,
123)]
124#[serde(rename_all = "kebab-case")]
125pub enum ProvenanceClass {
126    AuthenticatedAppField,
127    SystemOfRecord,
128    UserSupplied,
129    ThirdParty,
130    AttributedClaim,
131    ModelDerived,
132}
133
134/// How old an input may be at evaluation time.
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
136#[serde(rename_all = "snake_case", deny_unknown_fields)]
137pub enum Freshness {
138    MaxAgeMs(u64),
139    NotRequired,
140}
141
142/// Input value types (spec 002, 3.7.2). Every record field is required.
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
144#[serde(rename_all = "snake_case", deny_unknown_fields)]
145pub enum TypeDecl {
146    Bool,
147    Integer,
148    Decimal,
149    Text {
150        max_bytes: u64,
151    },
152    Timestamp,
153    Enum {
154        options: Vec<String>,
155    },
156    List {
157        item: Box<TypeDecl>,
158        max_items: u64,
159    },
160    Record {
161        fields: Vec<FieldDecl>,
162        /// Version 2: when a decoded record is present (spec 032, 3.4.2).
163        #[serde(default, skip_serializing_if = "Option::is_none")]
164        present_when: Option<PresentWhen>,
165    },
166    /// Version 2, record fields only: JSON `null` or a missing key, stored
167    /// as `null` (spec 032, 3.4.1).
168    Nullable {
169        #[serde(rename = "type")]
170        ty: Box<TypeDecl>,
171    },
172    /// Version 2: a JSON object with text keys (spec 032, 3.4.1).
173    Map {
174        key_max_bytes: u64,
175        value: Box<TypeDecl>,
176        max_entries: u64,
177    },
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
181#[serde(deny_unknown_fields)]
182pub struct FieldDecl {
183    pub name: String,
184    #[serde(rename = "type")]
185    pub ty: TypeDecl,
186}
187
188/// One step of the plan graph.
189#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
190#[serde(deny_unknown_fields)]
191pub struct StepDecl {
192    pub id: String,
193    pub body: StepBody,
194}
195
196#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
197#[serde(rename_all = "snake_case", deny_unknown_fields)]
198pub enum StepBody {
199    Acquire(crate::acquisition_step::Acquire),
200    Widen(crate::acquisition_step::Widen),
201    Exact(ExactDecl),
202    Semantic(SemanticDecl),
203    /// Version 2: the only way a grade rises (spec 032, 3.7).
204    Corroborate(CorroborateDecl),
205    Generate(Box<crate::generation::Generate>),
206    Extract(Box<crate::extraction::Extract>),
207    AcceptModelDerived(Box<crate::extraction::AcceptModelDerived>),
208    Rank(Box<crate::ranking::Rank>),
209    Select(Box<crate::ranking::Select>),
210}
211
212/// A corroboration step (spec 032, 3.7.1).
213#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
214#[serde(deny_unknown_fields)]
215pub struct CorroborateDecl {
216    /// An input path or step value.
217    pub claim: String,
218    /// A grade above `hint`.
219    pub upgrade_to: Grade,
220    /// Non-empty.
221    pub by: Vec<Check>,
222    pub mode: CorroborationMode,
223    pub on_contradiction: OnContradiction,
224}
225
226#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
227#[serde(deny_unknown_fields)]
228pub struct Check {
229    pub predicate: Predicate,
230    pub on_false: OnFalse,
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
234#[serde(rename_all = "snake_case")]
235pub enum OnFalse {
236    Contradicts,
237    NoEvidence,
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
241#[serde(rename_all = "snake_case")]
242pub enum CorroborationMode {
243    All,
244    Any,
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
248#[serde(rename_all = "snake_case")]
249pub enum OnContradiction {
250    Conflict,
251    TreatAsHint,
252    Abstain,
253}
254
255/// An exact step: a registry operator, its version and its arguments. The
256/// arguments are decoded against the operator's own schema by the compiler,
257/// so an unknown operator is refused as `unknown_operator`, not as a parse
258/// failure (spec 002, 3.9.5).
259#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
260#[serde(deny_unknown_fields)]
261pub struct ExactDecl {
262    pub op: String,
263    pub version: u32,
264    pub args: Value,
265}
266
267/// The four semantic operations (R-05).
268#[derive(
269    Debug,
270    Clone,
271    Copy,
272    PartialEq,
273    Eq,
274    PartialOrd,
275    Ord,
276    Hash,
277    Serialize,
278    Deserialize,
279    schemars::JsonSchema,
280)]
281#[serde(rename_all = "snake_case")]
282pub enum Operation {
283    Classify,
284    Proposition,
285    Rubric,
286    Rank,
287    Generate,
288    Extract,
289}
290
291/// The value kind a semantic step requires (spec 002, 3.9.3).
292#[derive(
293    Debug,
294    Clone,
295    Copy,
296    PartialEq,
297    Eq,
298    PartialOrd,
299    Ord,
300    Hash,
301    Serialize,
302    Deserialize,
303    schemars::JsonSchema,
304)]
305#[serde(rename_all = "snake_case")]
306pub enum RequiredKind {
307    Ranking,
308    Label,
309    Distribution,
310    CalibratedProbability,
311    OrdinalDistribution,
312    Scores,
313    ScreenVerdict,
314    TextRecord,
315    TypedRecord,
316}
317
318/// A question answered by a decision backend.
319#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
320#[serde(deny_unknown_fields)]
321pub struct SemanticDecl {
322    pub operation: Operation,
323    /// The task identity a calibration binds to.
324    pub task: String,
325    /// The question or statement put to the backend.
326    pub question: String,
327    /// Labels (classify), `["false", "true"]` (proposition), levels from low to
328    /// high (rubric); empty for rank.
329    pub options: Vec<String>,
330    /// For rank: the step whose id list is ranked.
331    pub candidates: Candidates,
332    pub requires: RequiredKind,
333    /// References (`input:<name>`, `step:<id>`, `bind:<name>`) the backend sees.
334    pub project: Vec<String>,
335    /// Zero, one or two id lists this step fans out over.
336    pub for_each: Vec<ForEach>,
337    pub backend: BackendReq,
338    pub calibration: CalibrationRef,
339    pub fallback: Fallback,
340}
341
342#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
343#[serde(rename_all = "snake_case", deny_unknown_fields)]
344pub enum Candidates {
345    None,
346    From(String),
347}
348
349/// Fan-out over the ids produced by a step.
350#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
351#[serde(deny_unknown_fields)]
352pub struct ForEach {
353    /// `step:<id>` whose result is an id list, a filter result or a shortlist.
354    pub over: String,
355    /// The name the instance key carries; `bind:<name>` projects its item.
356    pub bind: String,
357    /// Where the item record for each id comes from, if it is projected.
358    pub items: ItemSource,
359}
360
361#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
362#[serde(rename_all = "snake_case", deny_unknown_fields)]
363pub enum ItemSource {
364    None,
365    /// A list of records (`input:<name>`) and the text field holding the id.
366    List {
367        list: String,
368        id_field: String,
369    },
370}
371
372/// Declared backend requirements beyond the operation and value kind.
373#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
374#[serde(deny_unknown_fields)]
375pub struct BackendReq {
376    pub min_determinism: Determinism,
377    pub artifact: ArtifactPin,
378    pub binding: BindingChoice,
379}
380
381/// Declared determinism of a backend, weakest first.
382#[derive(
383    Debug,
384    Clone,
385    Copy,
386    PartialEq,
387    Eq,
388    PartialOrd,
389    Ord,
390    Hash,
391    Serialize,
392    Deserialize,
393    schemars::JsonSchema,
394)]
395#[serde(rename_all = "snake_case")]
396pub enum Determinism {
397    Unspecified,
398    Tolerance,
399    Bitwise,
400}
401
402#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
403#[serde(rename_all = "snake_case", deny_unknown_fields)]
404pub enum ArtifactPin {
405    Any,
406    Pinned(ArtifactId),
407}
408
409/// An explicit binding makes that backend the only candidate (3.9.4).
410#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
411#[serde(rename_all = "snake_case", deny_unknown_fields)]
412pub enum BindingChoice {
413    Auto,
414    Backend(String),
415}
416
417#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
418#[serde(rename_all = "snake_case", deny_unknown_fields)]
419pub enum CalibrationRef {
420    None,
421    Id(CalibrationId),
422}
423
424/// What happens when no candidate satisfies the step (3.9.6).
425#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
426#[serde(rename_all = "snake_case", deny_unknown_fields)]
427pub enum Fallback {
428    None,
429    /// The step is always `unsupported{capability}`.
430    Unsupported,
431    /// Bind with this alternative required kind instead.
432    Kind {
433        requires: RequiredKind,
434        reason: String,
435    },
436}
437
438/// Declared ceilings (spec 002, 3.9.5 category 7, and 3.10.3).
439#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
440#[serde(deny_unknown_fields)]
441pub struct LimitsDecl {
442    /// Spec 036 acquisition ceilings, omitted when unused.
443    #[serde(
444        default,
445        skip_serializing_if = "Option::is_none",
446        deserialize_with = "crate::screening::present"
447    )]
448    #[schemars(with = "crate::acquisition_step::AcquisitionBudget")]
449    pub acquisition: Option<crate::acquisition_step::AcquisitionBudget>,
450    pub max_semantic_requests: u64,
451    pub on_excess: ExcessPolicy,
452    pub max_projection_bytes: u64,
453    /// Allowed `|sum - 1|` for a supplied distribution.
454    pub distribution_tolerance: Decimal,
455    /// Recorded for the runtime; not enforced by the pure core.
456    pub deadline_ms: u64,
457}
458
459#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
460#[serde(rename_all = "snake_case")]
461pub enum ExcessPolicy {
462    Refuse,
463    TruncateVisible,
464}
465
466/// A typed literal.
467#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
468#[serde(rename_all = "snake_case", deny_unknown_fields)]
469pub enum Literal {
470    Bool(bool),
471    Integer(i64),
472    Decimal(Decimal),
473    Text(String),
474    Timestamp(Timestamp),
475    DurationMs(u64),
476    Enum(String),
477}
478
479/// Expressions inside exact-operator arguments and policy predicates
480/// (registry `rustev.exact/1`). References are `input:<name>`,
481/// `step:<id>`, `item:<field>[.<field>...]` (inside a per-item operator) and
482/// `now`.
483#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
484#[serde(rename_all = "snake_case", deny_unknown_fields)]
485pub enum Expr {
486    Ref(String),
487    Lit(Literal),
488    Add {
489        left: Box<Expr>,
490        right: Box<Expr>,
491    },
492    Sub {
493        left: Box<Expr>,
494        right: Box<Expr>,
495    },
496    /// Decimal product rounded half-even to 10^-9; integers are exact.
497    Mul {
498        left: Box<Expr>,
499        right: Box<Expr>,
500    },
501    /// Decimal quotient rounded half-even to 10^-9.
502    Div {
503        left: Box<Expr>,
504        right: Box<Expr>,
505    },
506    /// Rescale a decimal half-even to `scale` digits.
507    Round {
508        value: Box<Expr>,
509        scale: u32,
510    },
511    /// Currency conversion through a rate table (`fx_convert@1` semantics).
512    Fx {
513        amount: Box<Expr>,
514        from: Box<Expr>,
515        to: Box<Expr>,
516        rates: String,
517        scale: u32,
518    },
519    /// Length of a list.
520    Len(Box<Expr>),
521    /// Integer to decimal, the only numeric conversion.
522    ToDecimal(Box<Expr>),
523    /// Registry `rustev.exact/2`: a reference with a grade requirement
524    /// (spec 032, 3.9.1).
525    Graded {
526        #[serde(rename = "ref")]
527        reference: String,
528        require: GradeRequirement,
529    },
530}
531
532/// What a graded read requires (spec 032, 3.9.1).
533#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
534#[serde(deny_unknown_fields)]
535pub struct GradeRequirement {
536    pub min: Grade,
537    pub max_age_ms: AgeLimit,
538}
539
540#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
541#[serde(rename_all = "snake_case", deny_unknown_fields)]
542pub enum AgeLimit {
543    Ms(u64),
544    NotRequired,
545}
546
547#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
548#[serde(rename_all = "snake_case")]
549pub enum CmpOp {
550    Eq,
551    Ne,
552    Lt,
553    Le,
554    Gt,
555    Ge,
556}
557
558/// Boolean predicates. `all([])` is true; `any([])` is false.
559#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
560#[serde(rename_all = "snake_case", deny_unknown_fields)]
561pub enum Predicate {
562    Cmp {
563        left: Expr,
564        op: CmpOp,
565        right: Expr,
566    },
567    All(Vec<Predicate>),
568    Any(Vec<Predicate>),
569    Not(Box<Predicate>),
570    Member {
571        value: Expr,
572        set: Vec<Literal>,
573    },
574    /// True when the named input resolved (`input:<name>`).
575    Present(String),
576    /// Registry 2: whether the referenced value was constructed (spec 036).
577    IsConstructed {
578        #[serde(rename = "ref")]
579        reference: String,
580    },
581    /// Registry `rustev.exact/2`: true when the value is a member of the
582    /// list, or the keys of the map, `set` references (spec 032, 3.7.2).
583    MemberRef {
584        value: Expr,
585        set: String,
586    },
587}
588
589/// The selection policy (spec 002, 3.11).
590#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
591#[serde(deny_unknown_fields)]
592pub struct PolicyDecl {
593    pub on_unresolved: Vec<Handler>,
594    pub rules: Vec<Rule>,
595    pub adjustments: Vec<Adjustment>,
596}
597
598/// Handles one input or step being unresolved.
599#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
600#[serde(deny_unknown_fields)]
601pub struct Handler {
602    /// `input:<name>` or `step:<id>`.
603    pub target: String,
604    pub reasons: ReasonSet,
605    pub action: HandlerAction,
606}
607
608#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
609#[serde(rename_all = "snake_case", deny_unknown_fields)]
610pub enum ReasonSet {
611    Any,
612    Only(Vec<ReasonKind>),
613}
614
615/// The names of the closed `Unresolved` reasons.
616#[derive(
617    Debug,
618    Clone,
619    Copy,
620    PartialEq,
621    Eq,
622    PartialOrd,
623    Ord,
624    Hash,
625    Serialize,
626    Deserialize,
627    schemars::JsonSchema,
628)]
629#[serde(rename_all = "snake_case")]
630pub enum ReasonKind {
631    MissingEvidence,
632    StaleEvidence,
633    InvalidInput,
634    Abstained,
635    BackendUnavailable,
636    InvalidBackendOutput,
637    BudgetExhausted,
638    DeadlineExceeded,
639    Conflict,
640    Unsupported,
641    /// Present but below a required grade (spec 032, 3.9.4).
642    InsufficientGrade,
643    ScreenUnavailable,
644    /// A declared skipped path, distinct from an unresolved failure.
645    Skipped,
646}
647
648impl ReasonKind {
649    pub const ALL: [ReasonKind; 13] = [
650        ReasonKind::MissingEvidence,
651        ReasonKind::StaleEvidence,
652        ReasonKind::InvalidInput,
653        ReasonKind::Abstained,
654        ReasonKind::BackendUnavailable,
655        ReasonKind::InvalidBackendOutput,
656        ReasonKind::BudgetExhausted,
657        ReasonKind::DeadlineExceeded,
658        ReasonKind::Conflict,
659        ReasonKind::Unsupported,
660        ReasonKind::InsufficientGrade,
661        ReasonKind::ScreenUnavailable,
662        ReasonKind::Skipped,
663    ];
664}
665
666#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
667#[serde(rename_all = "snake_case", deny_unknown_fields)]
668pub enum HandlerAction {
669    /// The judgment is the target's unresolved reason.
670    Propagate,
671    Escalate {
672        reason: String,
673    },
674    /// Atomic conditions reading the target are false; a rule whose outcome
675    /// reads it does not match.
676    AsUnmet,
677    /// `propagate` with a declared remediation or address override (spec
678    /// 033, 3.5.5).
679    PropagateWith {
680        remediation: Option<RemediationUse>,
681        #[serde(default, skip_serializing_if = "Address::is_computed")]
682        address: Address,
683    },
684    /// The caller is asked for these fields (spec 033, 3.7).
685    NeedsInput {
686        fields: Vec<String>,
687        questions: QuestionSelection,
688    },
689}
690
691#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
692#[serde(deny_unknown_fields)]
693pub struct Rule {
694    pub id: String,
695    pub when: Cond,
696    pub then: OutcomeDecl,
697}
698
699/// Whether a probability threshold may read an uncalibrated distribution.
700#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
701#[serde(rename_all = "snake_case", deny_unknown_fields)]
702pub enum UncalibratedThreshold {
703    NotDeclared,
704    Declared { reason: String },
705}
706
707/// Policy conditions (spec 002, 3.11.3).
708#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
709#[serde(rename_all = "snake_case", deny_unknown_fields)]
710pub enum Cond {
711    Always,
712    TopLabel {
713        step: String,
714        label: String,
715    },
716    MassAtLeast {
717        step: String,
718        option: String,
719        threshold: Decimal,
720        uncalibrated_threshold: UncalibratedThreshold,
721    },
722    TopMassBelow {
723        step: String,
724        threshold: Decimal,
725        uncalibrated_threshold: UncalibratedThreshold,
726    },
727    ExpectationAtLeast {
728        step: String,
729        value: Decimal,
730    },
731    Exact(Predicate),
732    Empty(String),
733    Nonempty(String),
734    All(Vec<Cond>),
735    Any(Vec<Cond>),
736    Not(Box<Cond>),
737    /// True when the referenced value's provenance is within `classes`
738    /// (spec 033, 3.4.7).
739    ProvenanceWithin {
740        #[serde(rename = "ref")]
741        reference: String,
742        classes: Vec<ProvenanceClass>,
743    },
744}
745
746/// Declared questions, optionally selected by an accepted extraction (spec 038, 3.13).
747/// The list encoding preserves existing definition and plan bytes.
748#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
749#[serde(untagged, deny_unknown_fields)]
750pub enum QuestionSelection {
751    Declared(Vec<String>),
752    Proposed {
753        proposed_by: String,
754        allowed: Vec<String>,
755    },
756}
757
758impl QuestionSelection {
759    pub fn allowed(&self) -> &[String] {
760        match self {
761            Self::Declared(ids) => ids,
762            Self::Proposed { allowed, .. } => allowed,
763        }
764    }
765}
766
767/// What a matching rule proposes. Never a grant (spec 001, 3.2).
768#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
769#[serde(rename_all = "snake_case", deny_unknown_fields)]
770pub enum OutcomeDecl {
771    Propose {
772        action: String,
773        params: Vec<ParamDecl>,
774    },
775    Escalate {
776        reason: String,
777    },
778    /// `missing_evidence` naming the fields listed by a step.
779    MissingEvidenceFrom {
780        step: String,
781    },
782    /// The caller is asked for these fields (spec 033, 3.7).
783    NeedsInput {
784        fields: Vec<String>,
785        questions: QuestionSelection,
786    },
787}
788
789#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
790#[serde(deny_unknown_fields)]
791pub struct ParamDecl {
792    pub name: String,
793    pub value: ParamSource,
794}
795
796#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
797#[serde(rename_all = "snake_case", deny_unknown_fields)]
798pub enum ParamSource {
799    Ref(String),
800    Lit(Literal),
801}
802
803/// Raises a proposal parameter one position on an ordered scale, saturating.
804#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
805#[serde(deny_unknown_fields)]
806pub struct Adjustment {
807    pub id: String,
808    pub when: Cond,
809    pub param: String,
810    pub scale: Vec<String>,
811}