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