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