Skip to main content

rustev_contract/
plan.rs

1//! Compiled plans, `rustev.plan/2` (spec 002, 3.3.4, amended by spec 003,
2//! 3.2.1) and `rustev.plan/3` (spec 032, 3.1.1). A plan embeds its canonical definition, every binding that affects
3//! behavior and its execution policy, so its identity changes whenever
4//! behavior can.
5
6use serde::{Deserialize, Serialize};
7
8use crate::actionability::PlanActionability;
9use crate::calibration::CalibrationBinding;
10use crate::definition::{Definition, RequiredKind};
11use crate::descriptor::OutputKind;
12use crate::execution::ExecutionPolicy;
13use crate::grade::ValueGrade;
14use crate::ids::{
15    ArtifactId, CalibrationId, DefinitionId, DescriptorId, ExecutionPolicyId, PlanId, SourceId,
16};
17use crate::judgment::{Derivation, Notice};
18use crate::limits::PLAN_V1;
19use crate::schema;
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
22#[serde(deny_unknown_fields)]
23pub struct Plan {
24    pub schema: String,
25    /// `rustev-core/<crate version>`.
26    pub compiler: String,
27    /// `rustev.exact/1`, or for a plan 3 the lowest registry version
28    /// containing every operator, predicate and reference form it uses.
29    pub registry: String,
30    pub definition_id: DefinitionId,
31    pub definition: Definition,
32    /// Step ids in topological order, ties by declaration order.
33    pub order: Vec<String>,
34    /// One entry per step, in declaration order.
35    pub steps: Vec<PlanStep>,
36    pub notices: Vec<Notice>,
37    /// The runtime execution policy (spec 003, 3.2.1); `none` means one
38    /// attempt per request on the bound backend, nothing else.
39    pub execution: PlanExecution,
40    /// Plan 3: every bound source, in name order (spec 032, 3.3.4).
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub sources: Option<Vec<BoundSource>>,
43    /// Plan 3: the static grade table (spec 032, 3.9.3).
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub grades: Option<Vec<StaticGrade>>,
46    /// Plan 3: present exactly when the definition has `actionability`
47    /// (spec 033, 3.1.2).
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub actionability: Option<PlanActionability>,
50    /// Instantiation evidence (spec 034, 3.10.1). Present only in a
51    /// `rustev.plan/3` document; absent from every earlier plan.
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub instantiation: Option<Instantiation>,
54    /// Optional screening contract (spec 035); omitted when unused.
55    #[serde(
56        default,
57        skip_serializing_if = "Option::is_none",
58        deserialize_with = "crate::screening::present"
59    )]
60    #[schemars(with = "crate::screening::PlanScreening")]
61    pub screening: Option<crate::screening::PlanScreening>,
62    /// Spec 036: omitted when the plan has no acquisition step.
63    #[serde(
64        default,
65        skip_serializing_if = "Option::is_none",
66        deserialize_with = "crate::acquisition_record::list"
67    )]
68    #[schemars(
69        with = "Vec<crate::acquisition_record::PlanAcquisition>",
70        length(min = 1, max = 4096)
71    )]
72    pub acquisitions: Option<Vec<crate::acquisition_record::PlanAcquisition>>,
73}
74
75impl crate::Document for Plan {
76    const SCHEMA: &'static str = schema::PLAN;
77    const LIMITS: crate::bounded::ParseLimits = PLAN_V1;
78
79    fn schema(&self) -> &str {
80        &self.schema
81    }
82
83    fn parse(bytes: &[u8]) -> Result<Self, crate::DocumentError> {
84        use crate::DocumentError;
85        use crate::bounded::{ParseError, parse_bounded};
86
87        let doc: Self = parse_bounded(bytes, &Self::LIMITS).map_err(DocumentError::Parse)?;
88        if doc.schema != schema::PLAN && doc.schema != schema::PLAN_V3 {
89            return Err(DocumentError::Schema {
90                expected: "rustev.plan/2 or rustev.plan/3",
91                found: doc.schema.chars().take(80).collect(),
92            });
93        }
94        if doc.schema != schema::PLAN_V3
95            && doc.steps.iter().any(|s| {
96                matches!(
97                    s.detail,
98                    PlanStepDetail::Generate(_)
99                        | PlanStepDetail::Extract(_)
100                        | PlanStepDetail::AcceptModelDerived(_)
101                        | PlanStepDetail::Rank(_)
102                        | PlanStepDetail::Select(_)
103                )
104            })
105        {
106            return Err(DocumentError::Parse(ParseError::Schema {
107                message: "model step details require rustev.plan/3".into(),
108            }));
109        }
110        if doc.schema != schema::PLAN_V3 && doc.acquisitions.is_some() {
111            return Err(DocumentError::Parse(ParseError::Schema {
112                message: "acquisitions require rustev.plan/3".into(),
113            }));
114        }
115        if let PlanExecution::Declared(d) = &doc.execution {
116            let declared = d
117                .policy
118                .optimization
119                .as_ref()
120                .is_some_and(|o| o.decision_cache.is_some());
121            if declared != d.decision_cache.is_some() {
122                return Err(DocumentError::Parse(ParseError::Schema {
123                    message: "decision_cache and its compiled parts appear together".into(),
124                }));
125            }
126            if declared && doc.schema != schema::PLAN_V3 {
127                return Err(DocumentError::Parse(ParseError::Schema {
128                    message: "decision_cache is a rustev.plan/3 member".into(),
129                }));
130            }
131        }
132        if doc.schema != schema::PLAN_V3 && doc.screening.is_some() {
133            return Err(DocumentError::Parse(ParseError::Schema {
134                message: "screening is a rustev.plan/3 member".into(),
135            }));
136        }
137        // Check presence separately because Option erases explicit null (spec
138        // 034, 3.10.1). Ignore values instead of deserializing Plan through Value:
139        // that would instantiate a second deserializer for its entire type tree.
140        if doc.schema != schema::PLAN_V3
141            && serde_json::from_slice::<PlanMemberPresence>(bytes)
142                .map_err(|e| {
143                    DocumentError::Parse(ParseError::Schema {
144                        message: e.to_string(),
145                    })
146                })?
147                .instantiation
148        {
149            return Err(DocumentError::Parse(ParseError::Schema {
150                message: "instantiation is a rustev.plan/3 member".into(),
151            }));
152        }
153        Ok(doc)
154    }
155
156    fn record_digest(&self) -> Result<crate::ids::ContentDigest, crate::canonical::CanonicalError> {
157        Ok(crate::ids::ContentDigest::from_digest(
158            crate::canonical::tagged_digest(&self.schema, &self.record_canonical()?),
159        ))
160    }
161}
162
163#[derive(Deserialize)]
164struct PlanMemberPresence {
165    #[serde(default, deserialize_with = "member_present")]
166    instantiation: bool,
167}
168
169fn member_present<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<bool, D::Error> {
170    serde::de::IgnoredAny::deserialize(deserializer).map(|_| true)
171}
172
173impl crate::Identified for Plan {
174    type Id = PlanId;
175
176    fn wrap(digest: String) -> PlanId {
177        PlanId::from_digest(digest)
178    }
179
180    fn id(&self) -> Result<PlanId, crate::canonical::CanonicalError> {
181        Ok(Self::wrap(crate::canonical::tagged_digest(
182            &self.schema,
183            &crate::Document::canonical(self)?,
184        )))
185    }
186}
187
188/// A source descriptor bound at compile time.
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
190#[serde(deny_unknown_fields)]
191pub struct BoundSource {
192    pub name: String,
193    pub version: String,
194    pub id: SourceId,
195}
196
197/// The highest grade an input path or step value can have.
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
199#[serde(deny_unknown_fields)]
200pub struct StaticGrade {
201    /// `input:<name>`, `input:<name>/<path>` or `step:<id>`.
202    pub subject: String,
203    pub ceiling: ValueGrade,
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
207#[serde(rename_all = "snake_case", deny_unknown_fields)]
208pub enum PlanExecution {
209    None,
210    Declared(Box<DeclaredExecution>),
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
214#[serde(deny_unknown_fields)]
215pub struct DeclaredExecution {
216    pub id: ExecutionPolicyId,
217    pub policy: ExecutionPolicy,
218    /// Every runtime fallback target, by step in policy order, then position.
219    pub fallbacks: Vec<FallbackBinding>,
220    /// Present exactly when the policy has `decision_cache`, and only in a
221    /// `rustev.plan/3` (spec 044, 3.1).
222    #[serde(default, skip_serializing_if = "Option::is_none")]
223    pub decision_cache: Option<PlanDecisionCache>,
224}
225
226/// What the compiler derives from a policy's `decision_cache` (spec 044,
227/// 3.6.1 and 3.6.2).
228#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
229#[serde(deny_unknown_fields)]
230pub struct PlanDecisionCache {
231    /// The decision-level key's members, in order (3.6.1).
232    pub decision_key_schema: Vec<String>,
233    /// Every step that reads a volatile source directly or through another
234    /// step, in plan order (3.6.2).
235    pub refresh_closure: Vec<String>,
236}
237
238/// A runtime fallback target bound at compile time.
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
240#[serde(deny_unknown_fields)]
241pub struct FallbackBinding {
242    pub step: String,
243    /// 1 for the first fallback; the bound backend is target 0.
244    pub target: u32,
245    pub backend_id: String,
246    pub artifact: ArtifactId,
247    pub descriptor: DescriptorId,
248    /// Always the primary binding's output kind.
249    pub output: OutputKind,
250}
251
252#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
253#[serde(deny_unknown_fields)]
254pub struct PlanStep {
255    pub id: String,
256    /// Static worst case from declared provenance (spec 002, 3.12.3).
257    pub derivation: Derivation,
258    pub detail: PlanStepDetail,
259}
260
261#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
262#[serde(rename_all = "snake_case", deny_unknown_fields)]
263pub enum PlanStepDetail {
264    /// Bound acquisition details are in the plan acquisitions table.
265    Acquire,
266    /// Bound widening details are in the plan acquisitions table.
267    Widen,
268    Exact {
269        op: String,
270        version: u32,
271    },
272    Semantic(Box<SemanticBinding>),
273    /// Fallback `unsupported` was taken: always `unsupported{capability}`.
274    Unsupported {
275        capability: String,
276    },
277    /// Plan 3: a corroboration step (spec 032, 3.7).
278    Corroborate,
279    Generate(Box<crate::generation::GenerateBinding>),
280    Extract(Box<crate::extraction_results::ExtractBinding>),
281    AcceptModelDerived(Box<crate::extraction::AcceptModelDerived>),
282    Rank(Box<crate::ranking_result::RankBinding>),
283    Select(Box<crate::ranking::Select>),
284}
285
286#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
287#[serde(deny_unknown_fields)]
288pub struct SemanticBinding {
289    pub backend_id: String,
290    pub artifact: ArtifactId,
291    pub descriptor: DescriptorId,
292    pub output: OutputKind,
293    /// The kind the step produces after normalization and calibration.
294    pub requires: RequiredKind,
295    pub normalization: Normalization,
296    pub calibration: BoundCalibration,
297    pub fallback_taken: bool,
298    /// Worst-case requests this step can issue.
299    pub max_requests: u64,
300    /// Worst-case canonical projection bytes of one request.
301    pub max_projection_bytes: u64,
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
305#[serde(rename_all = "snake_case")]
306pub enum Normalization {
307    None,
308    /// Softmax with max subtraction in declared option order.
309    #[serde(rename = "softmax/1")]
310    Softmax1,
311}
312
313#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
314#[serde(rename_all = "snake_case", deny_unknown_fields)]
315pub enum BoundCalibration {
316    None,
317    Bound {
318        id: CalibrationId,
319        binding: CalibrationBinding,
320    },
321}
322
323/// Instantiation evidence (spec 034, 3.10.1). Present only in a
324/// `rustev.plan/3` document; absent from every earlier plan.
325#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
326#[serde(deny_unknown_fields)]
327pub struct Instantiation {
328    /// `rustev.definition/1` or `rustev.definition/2`.
329    pub template_schema: String,
330    pub template: serde_json::Value,
331    pub template_id: DefinitionId,
332    pub variant: Variant,
333    pub profile: ProfileChain,
334    pub parameters: Vec<ResolvedParameter>,
335}
336#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
337#[serde(rename_all = "snake_case", deny_unknown_fields)]
338pub enum Variant {
339    None,
340    Named(String),
341}
342#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
343#[serde(rename_all = "snake_case", deny_unknown_fields)]
344pub enum ProfileChain {
345    None,
346    Chain(Vec<ProfileEntry>),
347}
348#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
349#[serde(deny_unknown_fields)]
350pub struct ProfileEntry {
351    pub profile: String,
352    pub version: String,
353    pub id: crate::ids::ProfileId,
354}
355#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
356#[serde(deny_unknown_fields)]
357pub struct ResolvedParameter {
358    pub name: String,
359    pub value: serde_json::Value,
360    pub source: ParameterSource,
361    pub sites: Vec<String>,
362}
363#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
364#[serde(rename_all = "snake_case", deny_unknown_fields)]
365pub enum ParameterSource {
366    Default,
367    Profile(String),
368}