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