Skip to main content

artifactize_core/store/
definitions.rs

1//! Typed schema-5 saved graph snapshots, distinct from strict live declarations.
2//! Unknown saved fields survive round-trips; owner schemas and payload extensions stay JSON.
3mod extensions;
4#[cfg(test)]
5mod tests;
6use crate::config::{self, Field, StoredPayload};
7pub use extensions::{Profile, SavedSelection};
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use serde_json::{Map, Value};
10use std::{collections::BTreeMap, path::PathBuf};
11
12fn missing<T>(field: &Field<T>) -> bool {
13    matches!(field, Field::Missing)
14}
15
16#[derive(Debug, Clone, Default)]
17pub struct Definitions(pub Option<Graph>);
18impl Definitions {
19    pub fn graph(&self) -> Option<&Graph> {
20        self.0.as_ref()
21    }
22    pub fn from_view(view: &crate::query::GraphView<'_>) -> Result<Self, serde_json::Error> {
23        serde_json::from_value(serde_json::to_value(view)?)
24    }
25}
26impl Serialize for Definitions {
27    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
28        self.0.serialize(serializer)
29    }
30}
31impl<'de> Deserialize<'de> for Definitions {
32    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
33        Option::<Graph>::deserialize(deserializer).map(Self)
34    }
35}
36
37#[derive(Debug, Clone, Default, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct Graph {
40    #[serde(default, skip_serializing_if = "missing")]
41    pub version: Field<u32>,
42    #[serde(default, skip_serializing_if = "missing", with = "path_field")]
43    pub repo_path: Field<PathBuf>,
44    #[serde(default, skip_serializing_if = "missing")]
45    pub selection: Field<SavedSelection>,
46    #[serde(default, skip_serializing_if = "Field::missing")]
47    pub artifacts: Field<BTreeMap<crate::types::ArtifactName, Artifact>>,
48    #[serde(default, skip_serializing_if = "missing")]
49    pub evals: Field<Vec<Eval>>,
50    #[serde(default, skip_serializing_if = "missing")]
51    pub relations: Field<Vec<Relation>>,
52    #[serde(default, skip_serializing_if = "missing")]
53    pub components: Field<Vec<Component>>,
54    #[serde(flatten)]
55    pub extra: Map<String, Value>,
56}
57impl Graph {
58    pub fn artifacts(&self) -> impl Iterator<Item = (&crate::types::ArtifactName, &Artifact)> {
59        match &self.artifacts {
60            Field::Value(map) => Some(map),
61            _ => None,
62        }
63        .into_iter()
64        .flatten()
65    }
66    pub fn artifact(&self, id: &str) -> Option<&Artifact> {
67        match &self.artifacts {
68            Field::Value(map) => map.get(id),
69            _ => None,
70        }
71    }
72    pub fn evals(&self) -> &[Eval] {
73        match &self.evals {
74            Field::Value(evals) => evals,
75            _ => &[],
76        }
77    }
78    pub fn relations(&self) -> &[Relation] {
79        match &self.relations {
80            Field::Value(relations) => relations,
81            _ => &[],
82        }
83    }
84    pub fn components(&self) -> &[Component] {
85        match &self.components {
86            Field::Value(components) => components,
87            _ => &[],
88        }
89    }
90}
91
92#[derive(Debug, Clone, Default, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct Artifact {
95    #[serde(default, skip_serializing_if = "missing")]
96    pub kind: Field<config::ArtifactKind>,
97    #[serde(default, skip_serializing_if = "missing", with = "path_field")]
98    pub path: Field<PathBuf>,
99    #[serde(default, skip_serializing_if = "missing")]
100    pub children: Field<BTreeMap<config::ChildPrefix, crate::types::ArtifactName>>,
101    #[serde(default, skip_serializing_if = "missing")]
102    pub name: Field<crate::types::ArtifactName>,
103    #[serde(default, skip_serializing_if = "missing")]
104    pub tags: Field<Vec<String>>,
105    #[serde(default, skip_serializing_if = "missing")]
106    pub views: Field<Views>,
107    #[serde(default, skip_serializing_if = "missing")]
108    pub mounts: Field<BTreeMap<config::MountAlias, crate::types::ArtifactName>>,
109    #[serde(default, skip_serializing_if = "missing")]
110    pub basis: Field<bool>,
111    #[serde(default, skip_serializing_if = "missing")]
112    pub fingerprint: Field<Fingerprint>,
113    #[serde(default, skip_serializing_if = "missing")]
114    pub review_policy: Field<Policy>,
115    #[serde(flatten)]
116    pub extra: Map<String, Value>,
117}
118#[derive(Debug, Clone, Default, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct Views {
121    #[serde(default, skip_serializing_if = "missing")]
122    pub agent_tools: Field<BTreeMap<config::ToolOperationName, AgentTool>>,
123    #[serde(default, skip_serializing_if = "missing")]
124    pub human_tools: Field<BTreeMap<config::ToolOperationName, HumanTool>>,
125    #[serde(flatten)]
126    pub extra: Map<String, Value>,
127}
128#[derive(Debug, Clone, Serialize, Deserialize)]
129#[serde(untagged)]
130pub enum AgentTool {
131    Command(CommandTool),
132    Builtin(BuiltinTool),
133}
134#[derive(Debug, Clone, Serialize, Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct CommandTool {
137    pub description: String,
138    pub input_schema: Value,
139    pub protocol: config::ToolProtocol,
140    pub command: String,
141    pub args: Vec<String>,
142    #[serde(default, skip_serializing_if = "missing", with = "duration_field")]
143    pub timeout_ms: Field<std::time::Duration>,
144    #[serde(default, skip_serializing_if = "missing")]
145    pub execution_paths: Field<Vec<String>>,
146    #[serde(flatten)]
147    pub extra: Map<String, Value>,
148}
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct BuiltinTool {
151    pub builtin: config::Builtin,
152    #[serde(default, skip_serializing_if = "missing")]
153    pub args: Field<Vec<String>>,
154    #[serde(default, skip_serializing_if = "missing")]
155    pub description: Field<String>,
156    #[serde(flatten)]
157    pub extra: Map<String, Value>,
158}
159#[derive(Debug, Clone, Serialize, Deserialize)]
160#[serde(untagged)]
161pub enum HumanTool {
162    Command(HumanCommandTool),
163    Builtin(HumanBuiltinTool),
164}
165#[derive(Debug, Clone, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase")]
167pub struct HumanCommandTool {
168    pub description: String,
169    pub kind: config::HumanToolKind,
170    pub command: String,
171    pub args: Vec<String>,
172    #[serde(flatten)]
173    pub extra: Map<String, Value>,
174}
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct HumanBuiltinTool {
177    pub builtin: config::Builtin,
178    pub description: String,
179    pub kind: config::HumanToolKind,
180    pub args: Vec<String>,
181    #[serde(flatten)]
182    pub extra: Map<String, Value>,
183}
184#[derive(Debug, Clone, Default, Serialize, Deserialize)]
185#[serde(rename_all = "camelCase")]
186pub struct Fingerprint {
187    #[serde(default, skip_serializing_if = "missing")]
188    pub script: Field<Script>,
189    #[serde(default, skip_serializing_if = "missing")]
190    pub files: Field<Vec<String>>,
191    #[serde(default, skip_serializing_if = "missing")]
192    pub ignore: Field<Vec<String>>,
193    #[serde(flatten)]
194    pub extra: Map<String, Value>,
195}
196#[derive(Debug, Clone, Serialize, Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub struct Script {
199    pub command: String,
200    pub args: Vec<String>,
201    #[serde(default, skip_serializing_if = "missing")]
202    pub files: Field<Vec<String>>,
203    #[serde(default, skip_serializing_if = "missing", with = "duration_field")]
204    pub timeout_ms: Field<std::time::Duration>,
205    #[serde(flatten)]
206    pub extra: Map<String, Value>,
207}
208#[derive(Debug, Clone, Default, Serialize, Deserialize)]
209#[serde(rename_all = "camelCase")]
210pub struct Policy {
211    #[serde(default, skip_serializing_if = "missing")]
212    pub dependency_gates: Field<config::DependencyGates>,
213    #[serde(flatten)]
214    pub extra: Map<String, Value>,
215}
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct Eval {
218    pub id: crate::types::EvalId,
219    pub target: crate::types::ArtifactName,
220    #[serde(default, skip_serializing_if = "missing")]
221    pub references: Field<BTreeMap<crate::config::ReferenceName, crate::types::ArtifactName>>,
222    #[serde(default, skip_serializing_if = "missing")]
223    pub deps: Field<Vec<crate::types::ArtifactName>>,
224    #[serde(default, skip_serializing_if = "missing")]
225    pub declaration: Field<Declaration>,
226    #[serde(flatten)]
227    pub extra: Map<String, Value>,
228}
229#[derive(Debug, Clone, Default, Serialize, Deserialize)]
230#[serde(rename_all = "camelCase")]
231pub struct Declaration {
232    #[serde(default, skip_serializing_if = "missing")]
233    pub id: Field<crate::config::LocalEvalId>,
234    #[serde(default, skip_serializing_if = "missing")]
235    pub title: Field<String>,
236    #[serde(default, skip_serializing_if = "missing")]
237    pub profile: Field<Profile>,
238    #[serde(default, skip_serializing_if = "missing")]
239    pub profile_variants: Field<BTreeMap<config::ProfileVariantName, Profile>>,
240    #[serde(default, skip_serializing_if = "missing")]
241    pub payload: Field<StoredPayload>,
242    #[serde(default, skip_serializing_if = "missing")]
243    pub pass_schema: Field<Map<String, Value>>,
244    #[serde(default, skip_serializing_if = "missing")]
245    pub fail_schema: Field<Map<String, Value>>,
246    #[serde(flatten)]
247    pub extra: Map<String, Value>,
248}
249#[derive(Debug, Clone, Default, Serialize, Deserialize)]
250pub struct Component {
251    #[serde(default, skip_serializing_if = "missing")]
252    pub id: Field<crate::graph::ComponentId>,
253    #[serde(default, skip_serializing_if = "missing")]
254    pub artifacts: Field<Vec<crate::types::ArtifactName>>,
255    #[serde(default, skip_serializing_if = "missing")]
256    pub dependencies: Field<Vec<crate::graph::ComponentId>>,
257    #[serde(default, skip_serializing_if = "missing")]
258    pub gates: Field<Vec<crate::types::EvalId>>,
259    #[serde(default, skip_serializing_if = "missing")]
260    pub cyclic: Field<bool>,
261    #[serde(flatten)]
262    pub extra: Map<String, Value>,
263}
264#[derive(Debug, Clone, Serialize)]
265pub struct Relation {
266    pub source: crate::types::ArtifactName,
267    pub target: crate::types::ArtifactName,
268    #[serde(flatten)]
269    pub kind: RelationKind,
270    #[serde(default, skip_serializing_if = "missing")]
271    pub cyclic: Field<bool>,
272    #[serde(flatten)]
273    pub extra: Map<String, Value>,
274}
275impl<'de> Deserialize<'de> for Relation {
276    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
277        // Flattened internally-tagged enums do not consume their fields from a second
278        // flatten map. Split at the saved boundary so reserialization never duplicates kind.
279        let mut fields = Map::<String, Value>::deserialize(deserializer)?;
280        let mut take = |name: &str| {
281            fields
282                .remove(name)
283                .ok_or_else(|| serde::de::Error::custom(format!("missing relation {name}")))
284        };
285        let source = serde_json::from_value(take("source")?).map_err(serde::de::Error::custom)?;
286        let target = serde_json::from_value(take("target")?).map_err(serde::de::Error::custom)?;
287        let kind_name: String =
288            serde_json::from_value(take("kind")?).map_err(serde::de::Error::custom)?;
289        let names: &[&str] = match kind_name.as_str() {
290            "child" => &["path"],
291            "mount" => &["alias"],
292            "instruction" | "dependency" => &["evalId", "name"],
293            "argv" => &["evalId", "index", "name", "path"],
294            _ => &[],
295        };
296        let unknown = names.is_empty();
297        let mut kind = Map::from_iter([("kind".into(), Value::String(kind_name.clone()))]);
298        for name in names {
299            if let Some(value) = fields.remove(*name) {
300                kind.insert((*name).into(), value);
301            }
302        }
303        let kind = if unknown {
304            RelationKind::Unknown { kind: kind_name }
305        } else {
306            let parsed =
307                serde_json::from_value(Value::Object(kind)).map_err(serde::de::Error::custom)?;
308            if matches!(parsed, RelationKind::Unknown { .. }) {
309                return Err(serde::de::Error::custom(format!(
310                    "Invalid saved {kind_name} relation."
311                )));
312            }
313            parsed
314        };
315        let cyclic = match fields.remove("cyclic") {
316            Some(value) => serde_json::from_value(value).map_err(serde::de::Error::custom)?,
317            None => Field::Missing,
318        };
319        Ok(Self {
320            source,
321            target,
322            kind,
323            cyclic,
324            extra: fields,
325        })
326    }
327}
328#[derive(Debug, Clone, Serialize, Deserialize)]
329#[serde(tag = "kind", rename_all = "lowercase")]
330pub enum RelationKind {
331    Child {
332        #[serde(default, skip_serializing_if = "missing")]
333        path: Field<config::ChildPrefix>,
334    },
335    Mount {
336        #[serde(default, skip_serializing_if = "missing")]
337        alias: Field<config::MountAlias>,
338    },
339    Dependency {
340        #[serde(rename = "evalId", default, skip_serializing_if = "missing")]
341        eval_id: Field<crate::types::EvalId>,
342        #[serde(default, skip_serializing_if = "missing")]
343        name: Field<config::ReferenceName>,
344    },
345    Instruction {
346        #[serde(rename = "evalId")]
347        eval_id: crate::types::EvalId,
348        name: config::ReferenceName,
349    },
350    #[serde(rename = "argv")]
351    Argument {
352        #[serde(rename = "evalId")]
353        eval_id: crate::types::EvalId,
354        index: usize,
355        name: config::ReferenceName,
356        path: String,
357    },
358    #[serde(untagged)]
359    Unknown { kind: String },
360}
361
362mod duration_field {
363    use crate::config::Field;
364    use serde::{Deserializer, Serializer};
365    use std::time::Duration;
366    pub fn serialize<S: Serializer>(
367        value: &Field<Duration>,
368        serializer: S,
369    ) -> Result<S::Ok, S::Error> {
370        match value {
371            Field::Value(value) => {
372                let milliseconds = value.as_millis();
373                if !(1..=u128::from(crate::config::validation::MAX_TIMEOUT_MS))
374                    .contains(&milliseconds)
375                    || Duration::from_millis(milliseconds as u64) != *value
376                {
377                    return Err(serde::ser::Error::custom(
378                        "Saved definition timeoutMs must be whole milliseconds from 1 through 2147483647.",
379                    ));
380                }
381                serializer.serialize_u128(milliseconds)
382            }
383            _ => serializer.serialize_none(),
384        }
385    }
386    pub fn deserialize<'de, D: Deserializer<'de>>(
387        deserializer: D,
388    ) -> Result<Field<Duration>, D::Error> {
389        crate::config::validation::milliseconds::deserialize(deserializer)
390            .map(|value| value.map_or(Field::Null, Field::Value))
391    }
392}
393
394pub(super) mod path_field {
395    use crate::config::Field;
396    use serde::{Deserializer, Serializer};
397    use std::path::PathBuf;
398    pub fn serialize<S: Serializer>(
399        value: &Field<PathBuf>,
400        serializer: S,
401    ) -> Result<S::Ok, S::Error> {
402        match value {
403            Field::Value(path) => crate::platform::path_serde::serialize(path, serializer),
404            _ => serializer.serialize_none(),
405        }
406    }
407    pub fn deserialize<'de, D: Deserializer<'de>>(
408        deserializer: D,
409    ) -> Result<Field<PathBuf>, D::Error> {
410        crate::platform::path_serde::option::deserialize(deserializer)
411            .map(|value| value.map_or(Field::Null, Field::Value))
412    }
413}