chrs_slurp/
lib.rs

1//! # chrs-slurp
2//!
3//! **Intelligence Crate** – Provides the *curation* layer for the CHORUS system.
4//!
5//! The purpose of this crate is to take **Decision Records** generated by autonomous
6//! agents, validate them, and persist them into the graph database.  It isolates the
7//! validation and storage concerns so that other components (e.g. provenance, security)
8//! can work with a clean, audited data model.
9//!
10//! ## Architectural Rationale
11//!
12//! * **Separation of concerns** – Agents produce raw decisions; this crate is the
13//!   single source of truth for how those decisions are stored.
14//! * **Auditability** – By persisting to a Dolt‑backed graph each decision is versioned
15//!   and can be replay‑backed, satisfying CHORUS’s requirement for reproducible
16//!   reasoning.
17//! * **Extensibility** – The `CurationEngine` can be extended with additional validation
18//!   steps (e.g. policy checks) without touching the agents themselves.
19//!
20//! The crate depends on:
21//! * `chrs-graph` – a thin wrapper around a Dolt‑backed graph implementation.
22//! * `ucxl` – for addressing external knowledge artefacts.
23//! * `chrono`, `serde`, `uuid` – standard utilities for timestamps, (de)serialization
24//!   and unique identifiers.
25//!
26//! ---
27//!
28//! # Public API
29//!
30//! The public surface consists of three items:
31//!
32//! * `DecisionRecord` – data structure representing a curated decision.
33//! * `SlurpError` – enumeration of possible errors while curating.
34//! * `CurationEngine` – the engine that validates and persists `DecisionRecord`s.
35//!
36//! Each item is documented in‑line below.
37
38use chrono::{DateTime, Utc};
39use chrs_graph::{DoltGraph, GraphError};
40use serde::{Deserialize, Serialize};
41use thiserror::Error;
42use ucxl::UCXLAddress;
43use uuid::Uuid;
44
45/// A record representing a curated decision within the CHORUS system.
46///
47/// # What
48///
49/// This struct captures the essential metadata of a decision made by an
50/// autonomous agent, including who authored it, the reasoning behind it, any
51/// citations to external knowledge, and a timestamp.
52///
53/// # Why
54///
55/// Decision records are persisted in the graph database so that downstream
56/// components (e.g., provenance analysis) can reason about the provenance and
57/// justification of actions. Storing them as a dedicated table enables
58/// reproducibility and auditability across the CHORUS architecture.
59#[derive(Debug, Serialize, Deserialize, Clone)]
60pub struct DecisionRecord {
61    /// Unique identifier for the decision.
62    pub id: Uuid,
63    /// Identifier of the agent or human that authored the decision.
64    pub author: String,
65    /// Free‑form textual reasoning explaining the decision.
66    pub reasoning: String,
67    /// Serialized UCXL addresses that serve as citations for the decision.
68    /// Each entry should be a valid `UCXLAddress` string.
69    pub citations: Vec<String>,
70    /// The moment the decision was created.
71    pub timestamp: DateTime<Utc>,
72}
73
74/// Errors that can arise while slurping (curating) a decision record.
75///
76/// * `Graph` – underlying graph database operation failed.
77/// * `Serde` – (de)serialization of the decision data failed.
78/// * `ValidationError` – a supplied citation could not be parsed as a
79///   `UCXLAddress`.
80#[derive(Debug, Error)]
81pub enum SlurpError {
82    #[error("Graph error: {0}")]
83    Graph(#[from] GraphError),
84    #[error("Serialization error: {0}")]
85    Serde(#[from] serde_json::Error),
86    #[error("Validation error: {0}")]
87    ValidationError(String),
88}
89
90/// Core engine that validates and persists `DecisionRecord`s into the
91/// Dolt‑backed graph.
92///
93/// # Why
94///
95/// Centralising curation logic ensures a single place for validation and
96/// storage semantics, keeping the rest of the codebase agnostic of the graph
97/// implementation details.
98pub struct CurationEngine {
99    graph: DoltGraph,
100}
101
102impl CurationEngine {
103    /// Creates a new `CurationEngine` bound to the supplied `DoltGraph`.
104    ///
105    /// The engine holds a reference to the graph for the lifetime of the
106    /// instance; callers are responsible for providing a correctly initialised
107    /// graph.
108    pub fn new(graph: DoltGraph) -> Self {
109        Self { graph }
110    }
111
112    /// Validates the citations in `dr` and persists the decision into the
113    /// graph.
114    ///
115    /// The method performs three steps:
116    /// 1. **Citation validation** – each citation string is parsed into a
117    ///    `UCXLAddress`. Invalid citations produce a `ValidationError`.
118    /// 2. **Table assurance** – attempts to create the `curated_decisions`
119    ///    table if it does not already exist. Errors are ignored because the
120    ///    table may already be present.
121    /// 3. **Insertion & commit** – the decision is serialised to JSON and
122    ///    inserted as a node, then the graph transaction is committed.
123    ///
124    /// # Errors
125    /// Propagates any `GraphError`, `serde_json::Error`, or custom
126    /// validation failures.
127    pub fn curate_decision(&self, dr: DecisionRecord) -> Result<(), SlurpError> {
128        // 1. Validate Citations
129        for citation in &dr.citations {
130            use std::str::FromStr;
131            UCXLAddress::from_str(citation).map_err(|e| {
132                SlurpError::ValidationError(format!("Invalid citation {}: {}", citation, e))
133            })?;
134        }
135
136        // 2. Ensure the table exists; ignore error if it already does.
137        let _ = self.graph.create_table(
138            "curated_decisions",
139            "id VARCHAR(255) PRIMARY KEY, author TEXT, reasoning TEXT, citations TEXT, curated_at TEXT",
140        );
141
142        // 3. Serialize the record and insert it.
143        let data = serde_json::json!({
144            "id": dr.id.to_string(),
145            "author": dr.author,
146            "reasoning": dr.reasoning,
147            "citations": serde_json::to_string(&dr.citations)?,
148            "curated_at": dr.timestamp.to_rfc3339(),
149        });
150
151        self.graph.insert_node("curated_decisions", data)?;
152        self.graph
153            .commit(&format!("Curation complete for DR: {}", dr.id))?;
154        Ok(())
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use tempfile::TempDir;
162
163    /// Integration test that exercises the full curation flow on a temporary
164    /// Dolt graph.
165    #[test]
166    fn test_curation_flow() {
167        let dir = TempDir::new().unwrap();
168        let graph = DoltGraph::init(dir.path()).expect("graph init failed");
169        let engine = CurationEngine::new(graph);
170
171        let dr = DecisionRecord {
172            id: Uuid::new_v4(),
173            author: "agent-001".into(),
174            reasoning: "Tested the implementation of SLURP.".into(),
175            citations: vec!["ucxl://system:watcher@local:filesystem/#/UCXL/src/lib.rs".into()],
176            timestamp: Utc::now(),
177        };
178
179        engine.curate_decision(dr).expect("curation failed");
180    }
181}