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 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            "instruction" => &["evalId", "name"],
292            "argv" => &["evalId", "index", "name", "path"],
293            _ => &[],
294        };
295        let unknown = names.is_empty();
296        let mut kind = Map::from_iter([("kind".into(), Value::String(kind_name.clone()))]);
297        for name in names {
298            if let Some(value) = fields.remove(*name) {
299                kind.insert((*name).into(), value);
300            }
301        }
302        let kind = if unknown {
303            RelationKind::Unknown { kind: kind_name }
304        } else {
305            let parsed =
306                serde_json::from_value(Value::Object(kind)).map_err(serde::de::Error::custom)?;
307            if matches!(parsed, RelationKind::Unknown { .. }) {
308                return Err(serde::de::Error::custom(format!(
309                    "Invalid saved {kind_name} relation."
310                )));
311            }
312            parsed
313        };
314        let cyclic = match fields.remove("cyclic") {
315            Some(value) => serde_json::from_value(value).map_err(serde::de::Error::custom)?,
316            None => Field::Missing,
317        };
318        Ok(Self {
319            source,
320            target,
321            kind,
322            cyclic,
323            extra: fields,
324        })
325    }
326}
327#[derive(Debug, Clone, Serialize, Deserialize)]
328#[serde(tag = "kind", rename_all = "lowercase")]
329pub enum RelationKind {
330    Child {
331        #[serde(default, skip_serializing_if = "missing")]
332        path: Field<config::ChildPrefix>,
333    },
334    Instruction {
335        #[serde(rename = "evalId")]
336        eval_id: crate::types::EvalId,
337        name: config::ReferenceName,
338    },
339    #[serde(rename = "argv")]
340    Argument {
341        #[serde(rename = "evalId")]
342        eval_id: crate::types::EvalId,
343        index: usize,
344        name: config::ReferenceName,
345        path: String,
346    },
347    #[serde(untagged)]
348    Unknown { kind: String },
349}
350
351mod duration_field {
352    use crate::config::Field;
353    use serde::{Deserializer, Serializer};
354    use std::time::Duration;
355    pub fn serialize<S: Serializer>(
356        value: &Field<Duration>,
357        serializer: S,
358    ) -> Result<S::Ok, S::Error> {
359        match value {
360            Field::Value(value) => {
361                let milliseconds = value.as_millis();
362                if !(1..=u128::from(crate::config::validation::MAX_TIMEOUT_MS))
363                    .contains(&milliseconds)
364                    || Duration::from_millis(milliseconds as u64) != *value
365                {
366                    return Err(serde::ser::Error::custom(
367                        "Saved definition timeoutMs must be whole milliseconds from 1 through 2147483647.",
368                    ));
369                }
370                serializer.serialize_u128(milliseconds)
371            }
372            _ => serializer.serialize_none(),
373        }
374    }
375    pub fn deserialize<'de, D: Deserializer<'de>>(
376        deserializer: D,
377    ) -> Result<Field<Duration>, D::Error> {
378        crate::config::validation::milliseconds::deserialize(deserializer)
379            .map(|value| value.map_or(Field::Null, Field::Value))
380    }
381}
382
383pub(super) mod path_field {
384    use crate::config::Field;
385    use serde::{Deserializer, Serializer};
386    use std::path::PathBuf;
387    pub fn serialize<S: Serializer>(
388        value: &Field<PathBuf>,
389        serializer: S,
390    ) -> Result<S::Ok, S::Error> {
391        match value {
392            Field::Value(path) => crate::platform::path_serde::serialize(path, serializer),
393            _ => serializer.serialize_none(),
394        }
395    }
396    pub fn deserialize<'de, D: Deserializer<'de>>(
397        deserializer: D,
398    ) -> Result<Field<PathBuf>, D::Error> {
399        crate::platform::path_serde::option::deserialize(deserializer)
400            .map(|value| value.map_or(Field::Null, Field::Value))
401    }
402}