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 basis: Field<bool>,
109    #[serde(default, skip_serializing_if = "missing")]
110    pub fingerprint: Field<Fingerprint>,
111    #[serde(flatten)]
112    pub extra: Map<String, Value>,
113}
114#[derive(Debug, Clone, Default, Serialize, Deserialize)]
115#[serde(rename_all = "camelCase")]
116pub struct Views {
117    #[serde(default, skip_serializing_if = "missing")]
118    pub agent_tools: Field<BTreeMap<config::ToolOperationName, AgentTool>>,
119    #[serde(default, skip_serializing_if = "missing")]
120    pub human_tools: Field<BTreeMap<config::ToolOperationName, HumanTool>>,
121    #[serde(flatten)]
122    pub extra: Map<String, Value>,
123}
124#[derive(Debug, Clone, Serialize, Deserialize)]
125#[serde(untagged)]
126pub enum AgentTool {
127    Command(CommandTool),
128    Builtin(BuiltinTool),
129}
130#[derive(Debug, Clone, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct CommandTool {
133    pub description: String,
134    pub input_schema: Value,
135    pub protocol: config::ToolProtocol,
136    pub command: String,
137    pub args: Vec<String>,
138    #[serde(default, skip_serializing_if = "missing", with = "duration_field")]
139    pub timeout_ms: Field<std::time::Duration>,
140    #[serde(default, skip_serializing_if = "missing")]
141    pub execution_paths: Field<Vec<String>>,
142    #[serde(flatten)]
143    pub extra: Map<String, Value>,
144}
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct BuiltinTool {
147    pub builtin: config::Builtin,
148    #[serde(default, skip_serializing_if = "missing")]
149    pub args: Field<Vec<String>>,
150    #[serde(default, skip_serializing_if = "missing")]
151    pub description: Field<String>,
152    #[serde(flatten)]
153    pub extra: Map<String, Value>,
154}
155#[derive(Debug, Clone, Serialize, Deserialize)]
156#[serde(untagged)]
157pub enum HumanTool {
158    Command(HumanCommandTool),
159    Builtin(HumanBuiltinTool),
160}
161#[derive(Debug, Clone, Serialize, Deserialize)]
162#[serde(rename_all = "camelCase")]
163pub struct HumanCommandTool {
164    pub description: String,
165    pub kind: config::HumanToolKind,
166    pub command: String,
167    pub args: Vec<String>,
168    #[serde(flatten)]
169    pub extra: Map<String, Value>,
170}
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct HumanBuiltinTool {
173    pub builtin: config::Builtin,
174    pub description: String,
175    pub kind: config::HumanToolKind,
176    pub args: Vec<String>,
177    #[serde(flatten)]
178    pub extra: Map<String, Value>,
179}
180#[derive(Debug, Clone, Default, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct Fingerprint {
183    #[serde(default, skip_serializing_if = "missing")]
184    pub script: Field<Script>,
185    #[serde(default, skip_serializing_if = "missing")]
186    pub files: Field<Vec<String>>,
187    #[serde(default, skip_serializing_if = "missing")]
188    pub ignore: Field<Vec<String>>,
189    #[serde(flatten)]
190    pub extra: Map<String, Value>,
191}
192#[derive(Debug, Clone, Serialize, Deserialize)]
193#[serde(rename_all = "camelCase")]
194pub struct Script {
195    pub command: String,
196    pub args: Vec<String>,
197    #[serde(default, skip_serializing_if = "missing")]
198    pub files: Field<Vec<String>>,
199    #[serde(default, skip_serializing_if = "missing", with = "duration_field")]
200    pub timeout_ms: Field<std::time::Duration>,
201    #[serde(flatten)]
202    pub extra: Map<String, Value>,
203}
204#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct Eval {
206    pub id: crate::types::EvalId,
207    pub target: crate::types::ArtifactName,
208    #[serde(default, skip_serializing_if = "missing")]
209    pub references: Field<BTreeMap<crate::config::ReferenceName, crate::types::ArtifactName>>,
210    #[serde(default, skip_serializing_if = "missing")]
211    pub deps: Field<Vec<crate::types::ArtifactName>>,
212    #[serde(default, skip_serializing_if = "missing")]
213    pub declaration: Field<Declaration>,
214    #[serde(flatten)]
215    pub extra: Map<String, Value>,
216}
217#[derive(Debug, Clone, Default, Serialize, Deserialize)]
218#[serde(rename_all = "camelCase")]
219pub struct Declaration {
220    #[serde(default, skip_serializing_if = "missing")]
221    pub mutex: Field<Vec<config::MutexTag>>,
222    #[serde(default, skip_serializing_if = "missing")]
223    pub gate: Field<config::Gate>,
224    #[serde(default, skip_serializing_if = "missing")]
225    pub id: Field<crate::config::LocalEvalId>,
226    #[serde(default, skip_serializing_if = "missing")]
227    pub title: Field<String>,
228    #[serde(default, skip_serializing_if = "missing")]
229    pub profile: Field<Profile>,
230    #[serde(default, skip_serializing_if = "missing")]
231    pub profile_variants: Field<BTreeMap<config::ProfileVariantName, Profile>>,
232    #[serde(default, skip_serializing_if = "missing")]
233    pub payload: Field<StoredPayload>,
234    #[serde(default, skip_serializing_if = "missing")]
235    pub pass_schema: Field<Map<String, Value>>,
236    #[serde(default, skip_serializing_if = "missing")]
237    pub fail_schema: Field<Map<String, Value>>,
238    #[serde(flatten)]
239    pub extra: Map<String, Value>,
240}
241#[derive(Debug, Clone, Default, Serialize, Deserialize)]
242pub struct Component {
243    #[serde(default, skip_serializing_if = "missing")]
244    pub id: Field<crate::graph::ComponentId>,
245    #[serde(default, skip_serializing_if = "missing")]
246    pub artifacts: Field<Vec<crate::types::ArtifactName>>,
247    #[serde(default, skip_serializing_if = "missing")]
248    pub dependencies: Field<Vec<crate::graph::ComponentId>>,
249    #[serde(default, skip_serializing_if = "missing")]
250    pub gates: Field<Vec<crate::types::EvalId>>,
251    #[serde(default, skip_serializing_if = "missing")]
252    pub cyclic: Field<bool>,
253    #[serde(flatten)]
254    pub extra: Map<String, Value>,
255}
256#[derive(Debug, Clone, Serialize)]
257pub struct Relation {
258    pub source: crate::types::ArtifactName,
259    pub target: crate::types::ArtifactName,
260    #[serde(flatten)]
261    pub kind: RelationKind,
262    #[serde(default, skip_serializing_if = "missing")]
263    pub cyclic: Field<bool>,
264    #[serde(flatten)]
265    pub extra: Map<String, Value>,
266}
267impl<'de> Deserialize<'de> for Relation {
268    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
269        // Flattened internally-tagged enums do not consume their fields from a second
270        // flatten map. Split at the saved boundary so reserialization never duplicates kind.
271        let mut fields = Map::<String, Value>::deserialize(deserializer)?;
272        let mut take = |name: &str| {
273            fields
274                .remove(name)
275                .ok_or_else(|| serde::de::Error::custom(format!("missing relation {name}")))
276        };
277        let source = serde_json::from_value(take("source")?).map_err(serde::de::Error::custom)?;
278        let target = serde_json::from_value(take("target")?).map_err(serde::de::Error::custom)?;
279        let kind_name: String =
280            serde_json::from_value(take("kind")?).map_err(serde::de::Error::custom)?;
281        let names: &[&str] = match kind_name.as_str() {
282            "child" => &["path"],
283            "instruction" => &["evalId", "name"],
284            "argv" => &["evalId", "index", "name", "path"],
285            _ => &[],
286        };
287        let unknown = names.is_empty();
288        let mut kind = Map::from_iter([("kind".into(), Value::String(kind_name.clone()))]);
289        for name in names {
290            if let Some(value) = fields.remove(*name) {
291                kind.insert((*name).into(), value);
292            }
293        }
294        let kind = if unknown {
295            RelationKind::Unknown { kind: kind_name }
296        } else {
297            let parsed =
298                serde_json::from_value(Value::Object(kind)).map_err(serde::de::Error::custom)?;
299            if matches!(parsed, RelationKind::Unknown { .. }) {
300                return Err(serde::de::Error::custom(format!(
301                    "Invalid saved {kind_name} relation."
302                )));
303            }
304            parsed
305        };
306        let cyclic = match fields.remove("cyclic") {
307            Some(value) => serde_json::from_value(value).map_err(serde::de::Error::custom)?,
308            None => Field::Missing,
309        };
310        Ok(Self {
311            source,
312            target,
313            kind,
314            cyclic,
315            extra: fields,
316        })
317    }
318}
319#[derive(Debug, Clone, Serialize, Deserialize)]
320#[serde(tag = "kind", rename_all = "lowercase")]
321pub enum RelationKind {
322    Child {
323        #[serde(default, skip_serializing_if = "missing")]
324        path: Field<config::ChildPrefix>,
325    },
326    Instruction {
327        #[serde(rename = "evalId")]
328        eval_id: crate::types::EvalId,
329        name: config::ReferenceName,
330    },
331    #[serde(rename = "argv")]
332    Argument {
333        #[serde(rename = "evalId")]
334        eval_id: crate::types::EvalId,
335        index: usize,
336        name: config::ReferenceName,
337        path: String,
338    },
339    #[serde(untagged)]
340    Unknown { kind: String },
341}
342
343mod duration_field {
344    use crate::config::Field;
345    use serde::{Deserializer, Serializer};
346    use std::time::Duration;
347    pub fn serialize<S: Serializer>(
348        value: &Field<Duration>,
349        serializer: S,
350    ) -> Result<S::Ok, S::Error> {
351        match value {
352            Field::Value(value) => {
353                let milliseconds = value.as_millis();
354                if !(1..=u128::from(crate::config::validation::MAX_TIMEOUT_MS))
355                    .contains(&milliseconds)
356                    || Duration::from_millis(milliseconds as u64) != *value
357                {
358                    return Err(serde::ser::Error::custom(
359                        "Saved definition timeoutMs must be whole milliseconds from 1 through 2147483647.",
360                    ));
361                }
362                serializer.serialize_u128(milliseconds)
363            }
364            _ => serializer.serialize_none(),
365        }
366    }
367    pub fn deserialize<'de, D: Deserializer<'de>>(
368        deserializer: D,
369    ) -> Result<Field<Duration>, D::Error> {
370        crate::config::validation::milliseconds::deserialize(deserializer)
371            .map(|value| value.map_or(Field::Null, Field::Value))
372    }
373}
374
375pub(super) mod path_field {
376    use crate::config::Field;
377    use serde::{Deserializer, Serializer};
378    use std::path::PathBuf;
379    pub fn serialize<S: Serializer>(
380        value: &Field<PathBuf>,
381        serializer: S,
382    ) -> Result<S::Ok, S::Error> {
383        match value {
384            Field::Value(path) => crate::platform::path_serde::serialize(path, serializer),
385            _ => serializer.serialize_none(),
386        }
387    }
388    pub fn deserialize<'de, D: Deserializer<'de>>(
389        deserializer: D,
390    ) -> Result<Field<PathBuf>, D::Error> {
391        crate::platform::path_serde::option::deserialize(deserializer)
392            .map(|value| value.map_or(Field::Null, Field::Value))
393    }
394}