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}