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
96                .steps
97                .iter()
98                .any(|s| matches!(s.detail, PlanStepDetail::Generate(_)))
99        {
100            return Err(DocumentError::Parse(ParseError::Schema {
101                message: "generate requires rustev.plan/3".into(),
102            }));
103        }
104        if doc.schema != schema::PLAN_V3 && doc.acquisitions.is_some() {
105            return Err(DocumentError::Parse(ParseError::Schema {
106                message: "acquisitions require rustev.plan/3".into(),
107            }));
108        }
109        if doc.schema != schema::PLAN_V3 && doc.screening.is_some() {
110            return Err(DocumentError::Parse(ParseError::Schema {
111                message: "screening is a rustev.plan/3 member".into(),
112            }));
113        }
114        // Check presence separately because Option erases explicit null (spec
115        // 034, 3.10.1). Ignore values instead of deserializing Plan through Value:
116        // that would instantiate a second deserializer for its entire type tree.
117        if doc.schema != schema::PLAN_V3
118            && serde_json::from_slice::<PlanMemberPresence>(bytes)
119                .map_err(|e| {
120                    DocumentError::Parse(ParseError::Schema {
121                        message: e.to_string(),
122                    })
123                })?
124                .instantiation
125        {
126            return Err(DocumentError::Parse(ParseError::Schema {
127                message: "instantiation is a rustev.plan/3 member".into(),
128            }));
129        }
130        Ok(doc)
131    }
132
133    fn record_digest(&self) -> Result<crate::ids::ContentDigest, crate::canonical::CanonicalError> {
134        Ok(crate::ids::ContentDigest::from_digest(
135            crate::canonical::tagged_digest(&self.schema, &self.record_canonical()?),
136        ))
137    }
138}
139
140#[derive(Deserialize)]
141struct PlanMemberPresence {
142    #[serde(default, deserialize_with = "member_present")]
143    instantiation: bool,
144}
145
146fn member_present<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<bool, D::Error> {
147    serde::de::IgnoredAny::deserialize(deserializer).map(|_| true)
148}
149
150impl crate::Identified for Plan {
151    type Id = PlanId;
152
153    fn wrap(digest: String) -> PlanId {
154        PlanId::from_digest(digest)
155    }
156
157    fn id(&self) -> Result<PlanId, crate::canonical::CanonicalError> {
158        Ok(Self::wrap(crate::canonical::tagged_digest(
159            &self.schema,
160            &crate::Document::canonical(self)?,
161        )))
162    }
163}
164
165/// A source descriptor bound at compile time.
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
167#[serde(deny_unknown_fields)]
168pub struct BoundSource {
169    pub name: String,
170    pub version: String,
171    pub id: SourceId,
172}
173
174/// The highest grade an input path or step value can have.
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
176#[serde(deny_unknown_fields)]
177pub struct StaticGrade {
178    /// `input:<name>`, `input:<name>/<path>` or `step:<id>`.
179    pub subject: String,
180    pub ceiling: ValueGrade,
181}
182
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
184#[serde(rename_all = "snake_case", deny_unknown_fields)]
185pub enum PlanExecution {
186    None,
187    Declared(Box<DeclaredExecution>),
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
191#[serde(deny_unknown_fields)]
192pub struct DeclaredExecution {
193    pub id: ExecutionPolicyId,
194    pub policy: ExecutionPolicy,
195    /// Every runtime fallback target, by step in policy order, then position.
196    pub fallbacks: Vec<FallbackBinding>,
197}
198
199/// A runtime fallback target bound at compile time.
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
201#[serde(deny_unknown_fields)]
202pub struct FallbackBinding {
203    pub step: String,
204    /// 1 for the first fallback; the bound backend is target 0.
205    pub target: u32,
206    pub backend_id: String,
207    pub artifact: ArtifactId,
208    pub descriptor: DescriptorId,
209    /// Always the primary binding's output kind.
210    pub output: OutputKind,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
214#[serde(deny_unknown_fields)]
215pub struct PlanStep {
216    pub id: String,
217    /// Static worst case from declared provenance (spec 002, 3.12.3).
218    pub derivation: Derivation,
219    pub detail: PlanStepDetail,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
223#[serde(rename_all = "snake_case", deny_unknown_fields)]
224pub enum PlanStepDetail {
225    /// Bound acquisition details are in the plan acquisitions table.
226    Acquire,
227    /// Bound widening details are in the plan acquisitions table.
228    Widen,
229    Exact {
230        op: String,
231        version: u32,
232    },
233    Semantic(Box<SemanticBinding>),
234    /// Fallback `unsupported` was taken: always `unsupported{capability}`.
235    Unsupported {
236        capability: String,
237    },
238    /// Plan 3: a corroboration step (spec 032, 3.7).
239    Corroborate,
240    Generate(Box<crate::generation::GenerateBinding>),
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
244#[serde(deny_unknown_fields)]
245pub struct SemanticBinding {
246    pub backend_id: String,
247    pub artifact: ArtifactId,
248    pub descriptor: DescriptorId,
249    pub output: OutputKind,
250    /// The kind the step produces after normalization and calibration.
251    pub requires: RequiredKind,
252    pub normalization: Normalization,
253    pub calibration: BoundCalibration,
254    pub fallback_taken: bool,
255    /// Worst-case requests this step can issue.
256    pub max_requests: u64,
257    /// Worst-case canonical projection bytes of one request.
258    pub max_projection_bytes: u64,
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
262#[serde(rename_all = "snake_case")]
263pub enum Normalization {
264    None,
265    /// Softmax with max subtraction in declared option order.
266    #[serde(rename = "softmax/1")]
267    Softmax1,
268}
269
270#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
271#[serde(rename_all = "snake_case", deny_unknown_fields)]
272pub enum BoundCalibration {
273    None,
274    Bound {
275        id: CalibrationId,
276        binding: CalibrationBinding,
277    },
278}
279
280/// Instantiation evidence (spec 034, 3.10.1). Present only in a
281/// `rustev.plan/3` document; absent from every earlier plan.
282#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
283#[serde(deny_unknown_fields)]
284pub struct Instantiation {
285    /// `rustev.definition/1` or `rustev.definition/2`.
286    pub template_schema: String,
287    pub template: serde_json::Value,
288    pub template_id: DefinitionId,
289    pub variant: Variant,
290    pub profile: ProfileChain,
291    pub parameters: Vec<ResolvedParameter>,
292}
293#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
294#[serde(rename_all = "snake_case", deny_unknown_fields)]
295pub enum Variant {
296    None,
297    Named(String),
298}
299#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
300#[serde(rename_all = "snake_case", deny_unknown_fields)]
301pub enum ProfileChain {
302    None,
303    Chain(Vec<ProfileEntry>),
304}
305#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
306#[serde(deny_unknown_fields)]
307pub struct ProfileEntry {
308    pub profile: String,
309    pub version: String,
310    pub id: crate::ids::ProfileId,
311}
312#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
313#[serde(deny_unknown_fields)]
314pub struct ResolvedParameter {
315    pub name: String,
316    pub value: serde_json::Value,
317    pub source: ParameterSource,
318    pub sites: Vec<String>,
319}
320#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
321#[serde(rename_all = "snake_case", deny_unknown_fields)]
322pub enum ParameterSource {
323    Default,
324    Profile(String),
325}