1use 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 pub compiler: String,
27 pub registry: String,
30 pub definition_id: DefinitionId,
31 pub definition: Definition,
32 pub order: Vec<String>,
34 pub steps: Vec<PlanStep>,
36 pub notices: Vec<Notice>,
37 pub execution: PlanExecution,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub sources: Option<Vec<BoundSource>>,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub grades: Option<Vec<StaticGrade>>,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub actionability: Option<PlanActionability>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub instantiation: Option<Instantiation>,
54 #[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 #[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 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#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
199#[serde(deny_unknown_fields)]
200pub struct StaticGrade {
201 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 pub fallbacks: Vec<FallbackBinding>,
220 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub decision_cache: Option<PlanDecisionCache>,
224}
225
226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
229#[serde(deny_unknown_fields)]
230pub struct PlanDecisionCache {
231 pub decision_key_schema: Vec<String>,
233 pub refresh_closure: Vec<String>,
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
240#[serde(deny_unknown_fields)]
241pub struct FallbackBinding {
242 pub step: String,
243 pub target: u32,
245 pub backend_id: String,
246 pub artifact: ArtifactId,
247 pub descriptor: DescriptorId,
248 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 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 Acquire,
266 Widen,
268 Exact {
269 op: String,
270 version: u32,
271 },
272 Semantic(Box<SemanticBinding>),
273 Unsupported {
275 capability: String,
276 },
277 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 pub requires: RequiredKind,
295 pub normalization: Normalization,
296 pub calibration: BoundCalibration,
297 pub fallback_taken: bool,
298 pub max_requests: u64,
300 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 #[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
326#[serde(deny_unknown_fields)]
327pub struct Instantiation {
328 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}