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 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 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#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
182#[serde(deny_unknown_fields)]
183pub struct StaticGrade {
184 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 pub fallbacks: Vec<FallbackBinding>,
203}
204
205#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
207#[serde(deny_unknown_fields)]
208pub struct FallbackBinding {
209 pub step: String,
210 pub target: u32,
212 pub backend_id: String,
213 pub artifact: ArtifactId,
214 pub descriptor: DescriptorId,
215 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 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 Acquire,
233 Widen,
235 Exact {
236 op: String,
237 version: u32,
238 },
239 Semantic(Box<SemanticBinding>),
240 Unsupported {
242 capability: String,
243 },
244 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 pub requires: RequiredKind,
262 pub normalization: Normalization,
263 pub calibration: BoundCalibration,
264 pub fallback_taken: bool,
265 pub max_requests: u64,
267 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 #[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
293#[serde(deny_unknown_fields)]
294pub struct Instantiation {
295 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}