vasari-core 0.2.3

Content-addressed intent-graph library behind Vasari — intent attribution for autonomous coding agents.
Documentation
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::json;

use super::NodeId;
use crate::hash::node_id;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Intent {
    pub id: NodeId,
    /// Ticket URL, prompt text, or file reference — what triggered this intent.
    pub source: String,
    /// Human-readable description of the intent.
    pub text: String,
    pub created_at: DateTime<Utc>,
    /// Empty for root intents; one prior Intent ID for amendments.
    pub parent_ids: Vec<NodeId>,
    /// Schema version. Stored in JSON but excluded from content hash (annotation).
    #[serde(default = "default_schema_version")]
    pub schema_version: String,
}

fn default_schema_version() -> String {
    "1".to_string()
}

impl Intent {
    pub fn new(source: String, text: String, parent_ids: Vec<NodeId>) -> Self {
        Self::new_at(source, text, Utc::now(), parent_ids)
    }

    /// Create an Intent with an explicit timestamp — use this during ingest so the
    /// content hash is stable relative to the session's actual start time.
    pub fn new_at(
        source: String,
        text: String,
        created_at: DateTime<Utc>,
        parent_ids: Vec<NodeId>,
    ) -> Self {
        let id = NodeId(node_id(&Self::hash_input(
            &source,
            &text,
            &created_at,
            &parent_ids,
        )));
        Self {
            id,
            source,
            text,
            created_at,
            parent_ids,
            schema_version: default_schema_version(),
        }
    }

    fn hash_input(
        source: &str,
        text: &str,
        created_at: &DateTime<Utc>,
        parent_ids: &[NodeId],
    ) -> serde_json::Value {
        json!({
            "type": "intent",
            "source": source,
            "text": text,
            "created_at": created_at.to_rfc3339(),
            "parent_ids": parent_ids.iter().map(|id| id.as_str()).collect::<Vec<_>>(),
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn id_is_deterministic_for_same_timestamp() {
        let ts = Utc::now();
        let parents: Vec<NodeId> = vec![];
        let a = Intent {
            id: NodeId(node_id(&Intent::hash_input(
                "ACME-411",
                "Add JWT verification",
                &ts,
                &parents,
            ))),
            source: "ACME-411".into(),
            text: "Add JWT verification".into(),
            created_at: ts,
            parent_ids: parents.clone(),
            schema_version: "1".into(),
        };
        let b = Intent {
            id: NodeId(node_id(&Intent::hash_input(
                "ACME-411",
                "Add JWT verification",
                &ts,
                &parents,
            ))),
            source: "ACME-411".into(),
            text: "Add JWT verification".into(),
            created_at: ts,
            parent_ids: parents,
            schema_version: "1".into(),
        };
        assert_eq!(a.id, b.id);
    }
}