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    Label,
308    Distribution,
309    CalibratedProbability,
310    OrdinalDistribution,
311    Scores,
312    ScreenVerdict,
313    TextRecord,
314    TypedRecord,
315}
316
317/// A question answered by a decision backend.
318#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
319#[serde(deny_unknown_fields)]
320pub struct SemanticDecl {
321    pub operation: Operation,
322    /// The task identity a calibration binds to.
323    pub task: String,
324    /// The question or statement put to the backend.
325    pub question: String,
326    /// Labels (classify), `["false", "true"]` (proposition), levels from low to
327    /// high (rubric); empty for rank.
328    pub options: Vec<String>,
329    /// For rank: the step whose id list is ranked.
330    pub candidates: Candidates,
331    pub requires: RequiredKind,
332    /// References (`input:<name>`, `step:<id>`, `bind:<name>`) the backend sees.
333    pub project: Vec<String>,
334    /// Zero, one or two id lists this step fans out over.
335    pub for_each: Vec<ForEach>,
336    pub backend: BackendReq,
337    pub calibration: CalibrationRef,
338    pub fallback: Fallback,
339}
340
341#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
342#[serde(rename_all = "snake_case", deny_unknown_fields)]
343pub enum Candidates {
344    None,
345    From(String),
346}
347
348/// Fan-out over the ids produced by a step.
349#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
350#[serde(deny_unknown_fields)]
351pub struct ForEach {
352    /// `step:<id>` whose result is an id list, a filter result or a shortlist.
353    pub over: String,
354    /// The name the instance key carries; `bind:<name>` projects its item.
355    pub bind: String,
356    /// Where the item record for each id comes from, if it is projected.
357    pub items: ItemSource,
358}
359
360#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
361#[serde(rename_all = "snake_case", deny_unknown_fields)]
362pub enum ItemSource {
363    None,
364    /// A list of records (`input:<name>`) and the text field holding the id.
365    List {
366        list: String,
367        id_field: String,
368    },
369}
370
371/// Declared backend requirements beyond the operation and value kind.
372#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
373#[serde(deny_unknown_fields)]
374pub struct BackendReq {
375    pub min_determinism: Determinism,
376    pub artifact: ArtifactPin,
377    pub binding: BindingChoice,
378}
379
380/// Declared determinism of a backend, weakest first.
381#[derive(
382    Debug,
383    Clone,
384    Copy,
385    PartialEq,
386    Eq,
387    PartialOrd,
388    Ord,
389    Hash,
390    Serialize,
391    Deserialize,
392    schemars::JsonSchema,
393)]
394#[serde(rename_all = "snake_case")]
395pub enum Determinism {
396    Unspecified,
397    Tolerance,
398    Bitwise,
399}
400
401#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
402#[serde(rename_all = "snake_case", deny_unknown_fields)]
403pub enum ArtifactPin {
404    Any,
405    Pinned(ArtifactId),
406}
407
408/// An explicit binding makes that backend the only candidate (3.9.4).
409#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
410#[serde(rename_all = "snake_case", deny_unknown_fields)]
411pub enum BindingChoice {
412    Auto,
413    Backend(String),
414}
415
416#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
417#[serde(rename_all = "snake_case", deny_unknown_fields)]
418pub enum CalibrationRef {
419    None,
420    Id(CalibrationId),
421}
422
423/// What happens when no candidate satisfies the step (3.9.6).
424#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
425#[serde(rename_all = "snake_case", deny_unknown_fields)]
426pub enum Fallback {
427    None,
428    /// The step is always `unsupported{capability}`.
429    Unsupported,
430    /// Bind with this alternative required kind instead.
431    Kind {
432        requires: RequiredKind,
433        reason: String,
434    },
435}
436
437/// Declared ceilings (spec 002, 3.9.5 category 7, and 3.10.3).
438#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
439#[serde(deny_unknown_fields)]
440pub struct LimitsDecl {
441    /// Spec 036 acquisition ceilings, omitted when unused.
442    #[serde(
443        default,
444        skip_serializing_if = "Option::is_none",
445        deserialize_with = "crate::screening::present"
446    )]
447    #[schemars(with = "crate::acquisition_step::AcquisitionBudget")]
448    pub acquisition: Option<crate::acquisition_step::AcquisitionBudget>,
449    pub max_semantic_requests: u64,
450    pub on_excess: ExcessPolicy,
451    pub max_projection_bytes: u64,
452    /// Allowed `|sum - 1|` for a supplied distribution.
453    pub distribution_tolerance: Decimal,
454    /// Recorded for the runtime; not enforced by the pure core.
455    pub deadline_ms: u64,
456}
457
458#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
459#[serde(rename_all = "snake_case")]
460pub enum ExcessPolicy {
461    Refuse,
462    TruncateVisible,
463}
464
465/// A typed literal.
466#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
467#[serde(rename_all = "snake_case", deny_unknown_fields)]
468pub enum Literal {
469    Bool(bool),
470    Integer(i64),
471    Decimal(Decimal),
472    Text(String),
473    Timestamp(Timestamp),
474    DurationMs(u64),
475    Enum(String),
476}
477
478/// Expressions inside exact-operator arguments and policy predicates
479/// (registry `rustev.exact/1`). References are `input:<name>`,
480/// `step:<id>`, `item:<field>[.<field>...]` (inside a per-item operator) and
481/// `now`.
482#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
483#[serde(rename_all = "snake_case", deny_unknown_fields)]
484pub enum Expr {
485    Ref(String),
486    Lit(Literal),
487    Add {
488        left: Box<Expr>,
489        right: Box<Expr>,
490    },
491    Sub {
492        left: Box<Expr>,
493        right: Box<Expr>,
494    },
495    /// Decimal product rounded half-even to 10^-9; integers are exact.
496    Mul {
497        left: Box<Expr>,
498        right: Box<Expr>,
499    },
500    /// Decimal quotient rounded half-even to 10^-9.
501    Div {
502        left: Box<Expr>,
503        right: Box<Expr>,
504    },
505    /// Rescale a decimal half-even to `scale` digits.
506    Round {
507        value: Box<Expr>,
508        scale: u32,
509    },
510    /// Currency conversion through a rate table (`fx_convert@1` semantics).
511    Fx {
512        amount: Box<Expr>,
513        from: Box<Expr>,
514        to: Box<Expr>,
515        rates: String,
516        scale: u32,
517    },
518    /// Length of a list.
519    Len(Box<Expr>),
520    /// Integer to decimal, the only numeric conversion.
521    ToDecimal(Box<Expr>),
522    /// Registry `rustev.exact/2`: a reference with a grade requirement
523    /// (spec 032, 3.9.1).
524    Graded {
525        #[serde(rename = "ref")]
526        reference: String,
527        require: GradeRequirement,
528    },
529}
530
531/// What a graded read requires (spec 032, 3.9.1).
532#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
533#[serde(deny_unknown_fields)]
534pub struct GradeRequirement {
535    pub min: Grade,
536    pub max_age_ms: AgeLimit,
537}
538
539#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
540#[serde(rename_all = "snake_case", deny_unknown_fields)]
541pub enum AgeLimit {
542    Ms(u64),
543    NotRequired,
544}
545
546#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
547#[serde(rename_all = "snake_case")]
548pub enum CmpOp {
549    Eq,
550    Ne,
551    Lt,
552    Le,
553    Gt,
554    Ge,
555}
556
557/// Boolean predicates. `all([])` is true; `any([])` is false.
558#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
559#[serde(rename_all = "snake_case", deny_unknown_fields)]
560pub enum Predicate {
561    Cmp {
562        left: Expr,
563        op: CmpOp,
564        right: Expr,
565    },
566    All(Vec<Predicate>),
567    Any(Vec<Predicate>),
568    Not(Box<Predicate>),
569    Member {
570        value: Expr,
571        set: Vec<Literal>,
572    },
573    /// True when the named input resolved (`input:<name>`).
574    Present(String),
575    /// Registry 2: whether the referenced value was constructed (spec 036).
576    IsConstructed {
577        #[serde(rename = "ref")]
578        reference: String,
579    },
580    /// Registry `rustev.exact/2`: true when the value is a member of the
581    /// list, or the keys of the map, `set` references (spec 032, 3.7.2).
582    MemberRef {
583        value: Expr,
584        set: String,
585    },
586}
587
588/// The selection policy (spec 002, 3.11).
589#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
590#[serde(deny_unknown_fields)]
591pub struct PolicyDecl {
592    pub on_unresolved: Vec<Handler>,
593    pub rules: Vec<Rule>,
594    pub adjustments: Vec<Adjustment>,
595}
596
597/// Handles one input or step being unresolved.
598#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
599#[serde(deny_unknown_fields)]
600pub struct Handler {
601    /// `input:<name>` or `step:<id>`.
602    pub target: String,
603    pub reasons: ReasonSet,
604    pub action: HandlerAction,
605}
606
607#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
608#[serde(rename_all = "snake_case", deny_unknown_fields)]
609pub enum ReasonSet {
610    Any,
611    Only(Vec<ReasonKind>),
612}
613
614/// The names of the closed `Unresolved` reasons.
615#[derive(
616    Debug,
617    Clone,
618    Copy,
619    PartialEq,
620    Eq,
621    PartialOrd,
622    Ord,
623    Hash,
624    Serialize,
625    Deserialize,
626    schemars::JsonSchema,
627)]
628#[serde(rename_all = "snake_case")]
629pub enum ReasonKind {
630    MissingEvidence,
631    StaleEvidence,
632    InvalidInput,
633    Abstained,
634    BackendUnavailable,
635    InvalidBackendOutput,
636    BudgetExhausted,
637    DeadlineExceeded,
638    Conflict,
639    Unsupported,
640    /// Present but below a required grade (spec 032, 3.9.4).
641    InsufficientGrade,
642    ScreenUnavailable,
643    /// A declared skipped path, distinct from an unresolved failure.
644    Skipped,
645}
646
647impl ReasonKind {
648    pub const ALL: [ReasonKind; 13] = [
649        ReasonKind::MissingEvidence,
650        ReasonKind::StaleEvidence,
651        ReasonKind::InvalidInput,
652        ReasonKind::Abstained,
653        ReasonKind::BackendUnavailable,
654        ReasonKind::InvalidBackendOutput,
655        ReasonKind::BudgetExhausted,
656        ReasonKind::DeadlineExceeded,
657        ReasonKind::Conflict,
658        ReasonKind::Unsupported,
659        ReasonKind::InsufficientGrade,
660        ReasonKind::ScreenUnavailable,
661        ReasonKind::Skipped,
662    ];
663}
664
665#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
666#[serde(rename_all = "snake_case", deny_unknown_fields)]
667pub enum HandlerAction {
668    /// The judgment is the target's unresolved reason.
669    Propagate,
670    Escalate {
671        reason: String,
672    },
673    /// Atomic conditions reading the target are false; a rule whose outcome
674    /// reads it does not match.
675    AsUnmet,
676    /// `propagate` with a declared remediation or address override (spec
677    /// 033, 3.5.5).
678    PropagateWith {
679        remediation: Option<RemediationUse>,
680        #[serde(default, skip_serializing_if = "Address::is_computed")]
681        address: Address,
682    },
683    /// The caller is asked for these fields (spec 033, 3.7).
684    NeedsInput {
685        fields: Vec<String>,
686        questions: QuestionSelection,
687    },
688}
689
690#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
691#[serde(deny_unknown_fields)]
692pub struct Rule {
693    pub id: String,
694    pub when: Cond,
695    pub then: OutcomeDecl,
696}
697
698/// Whether a probability threshold may read an uncalibrated distribution.
699#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
700#[serde(rename_all = "snake_case", deny_unknown_fields)]
701pub enum UncalibratedThreshold {
702    NotDeclared,
703    Declared { reason: String },
704}
705
706/// Policy conditions (spec 002, 3.11.3).
707#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
708#[serde(rename_all = "snake_case", deny_unknown_fields)]
709pub enum Cond {
710    Always,
711    TopLabel {
712        step: String,
713        label: String,
714    },
715    MassAtLeast {
716        step: String,
717        option: String,
718        threshold: Decimal,
719        uncalibrated_threshold: UncalibratedThreshold,
720    },
721    TopMassBelow {
722        step: String,
723        threshold: Decimal,
724        uncalibrated_threshold: UncalibratedThreshold,
725    },
726    ExpectationAtLeast {
727        step: String,
728        value: Decimal,
729    },
730    Exact(Predicate),
731    Empty(String),
732    Nonempty(String),
733    All(Vec<Cond>),
734    Any(Vec<Cond>),
735    Not(Box<Cond>),
736    /// True when the referenced value's provenance is within `classes`
737    /// (spec 033, 3.4.7).
738    ProvenanceWithin {
739        #[serde(rename = "ref")]
740        reference: String,
741        classes: Vec<ProvenanceClass>,
742    },
743}
744
745/// Declared questions, optionally selected by an accepted extraction (spec 038, 3.13).
746/// The list encoding preserves existing definition and plan bytes.
747#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
748#[serde(untagged, deny_unknown_fields)]
749pub enum QuestionSelection {
750    Declared(Vec<String>),
751    Proposed {
752        proposed_by: String,
753        allowed: Vec<String>,
754    },
755}
756
757impl QuestionSelection {
758    pub fn allowed(&self) -> &[String] {
759        match self {
760            Self::Declared(ids) => ids,
761            Self::Proposed { allowed, .. } => allowed,
762        }
763    }
764}
765
766/// What a matching rule proposes. Never a grant (spec 001, 3.2).
767#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
768#[serde(rename_all = "snake_case", deny_unknown_fields)]
769pub enum OutcomeDecl {
770    Propose {
771        action: String,
772        params: Vec<ParamDecl>,
773    },
774    Escalate {
775        reason: String,
776    },
777    /// `missing_evidence` naming the fields listed by a step.
778    MissingEvidenceFrom {
779        step: String,
780    },
781    /// The caller is asked for these fields (spec 033, 3.7).
782    NeedsInput {
783        fields: Vec<String>,
784        questions: QuestionSelection,
785    },
786}
787
788#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
789#[serde(deny_unknown_fields)]
790pub struct ParamDecl {
791    pub name: String,
792    pub value: ParamSource,
793}
794
795#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
796#[serde(rename_all = "snake_case", deny_unknown_fields)]
797pub enum ParamSource {
798    Ref(String),
799    Lit(Literal),
800}
801
802/// Raises a proposal parameter one position on an ordered scale, saturating.
803#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
804#[serde(deny_unknown_fields)]
805pub struct Adjustment {
806    pub id: String,
807    pub when: Cond,
808    pub param: String,
809    pub scale: Vec<String>,
810}