Skip to main content

vasari_core/schema/
intent.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use serde_json::json;
4
5use super::NodeId;
6use crate::hash::node_id;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Intent {
10    pub id: NodeId,
11    /// Ticket URL, prompt text, or file reference — what triggered this intent.
12    pub source: String,
13    /// Human-readable description of the intent.
14    pub text: String,
15    pub created_at: DateTime<Utc>,
16    /// Empty for root intents; one prior Intent ID for amendments.
17    pub parent_ids: Vec<NodeId>,
18    /// Schema version. Stored in JSON but excluded from content hash (annotation).
19    #[serde(default = "default_schema_version")]
20    pub schema_version: String,
21}
22
23fn default_schema_version() -> String {
24    "1".to_string()
25}
26
27impl Intent {
28    pub fn new(source: String, text: String, parent_ids: Vec<NodeId>) -> Self {
29        Self::new_at(source, text, Utc::now(), parent_ids)
30    }
31
32    /// Create an Intent with an explicit timestamp — use this during ingest so the
33    /// content hash is stable relative to the session's actual start time.
34    pub fn new_at(
35        source: String,
36        text: String,
37        created_at: DateTime<Utc>,
38        parent_ids: Vec<NodeId>,
39    ) -> Self {
40        let id = NodeId(node_id(&Self::hash_input(
41            &source,
42            &text,
43            &created_at,
44            &parent_ids,
45        )));
46        Self {
47            id,
48            source,
49            text,
50            created_at,
51            parent_ids,
52            schema_version: default_schema_version(),
53        }
54    }
55
56    fn hash_input(
57        source: &str,
58        text: &str,
59        created_at: &DateTime<Utc>,
60        parent_ids: &[NodeId],
61    ) -> serde_json::Value {
62        json!({
63            "type": "intent",
64            "source": source,
65            "text": text,
66            "created_at": created_at.to_rfc3339(),
67            "parent_ids": parent_ids.iter().map(|id| id.as_str()).collect::<Vec<_>>(),
68        })
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn id_is_deterministic_for_same_timestamp() {
78        let ts = Utc::now();
79        let parents: Vec<NodeId> = vec![];
80        let a = Intent {
81            id: NodeId(node_id(&Intent::hash_input(
82                "ACME-411",
83                "Add JWT verification",
84                &ts,
85                &parents,
86            ))),
87            source: "ACME-411".into(),
88            text: "Add JWT verification".into(),
89            created_at: ts,
90            parent_ids: parents.clone(),
91            schema_version: "1".into(),
92        };
93        let b = Intent {
94            id: NodeId(node_id(&Intent::hash_input(
95                "ACME-411",
96                "Add JWT verification",
97                &ts,
98                &parents,
99            ))),
100            source: "ACME-411".into(),
101            text: "Add JWT verification".into(),
102            created_at: ts,
103            parent_ids: parents,
104            schema_version: "1".into(),
105        };
106        assert_eq!(a.id, b.id);
107    }
108}