use crate::store_types::{ForkGroupInfo, RecallParams, SearchHit};
use crate::store_types::VersionEntry;
use areev_core::error::{Hash, Result};
use areev_core::format::deserialize::DeserializedGrain;
#[derive(Debug, Clone, serde::Serialize)]
pub struct AssemblyManifest {
pub query_sha256: String,
pub included_hashes: Vec<String>,
pub rendered_sha256: String,
pub budget: serde_json::Value,
pub dropped_hashes: Vec<String>,
pub sources: serde_json::Value,
}
#[derive(Debug, Clone)]
pub enum RerankType {
CrossEncoder,
Llm,
}
pub trait CalStoreFacade: Send + Sync {
fn recall(&self, params: &RecallParams) -> Result<Vec<SearchHit>>;
fn exists(&self, hash: &Hash) -> Result<bool>;
fn get(&self, hash: &Hash) -> Result<DeserializedGrain>;
fn count(&self) -> Result<usize>;
fn get_history(
&self,
namespace: &str,
subject: &str,
relation: &str,
) -> Result<Vec<VersionEntry>>;
fn default_namespace(&self) -> Option<&str>;
fn active_user(&self) -> Option<&str>;
fn cal_add(
&self,
grain_type: &str,
fields: &serde_json::Map<String, serde_json::Value>,
) -> Result<Hash>;
fn cal_add_with_options(
&self,
grain_type: &str,
fields: &serde_json::Map<String, serde_json::Value>,
options: crate::store_types::AddOptions,
) -> Result<crate::store_types::AddResult> {
let _ = options;
self.cal_add(grain_type, fields)
.map(crate::store_types::AddResult::plain)
}
fn cal_supersede(
&self,
old_hash: &Hash,
grain_type: &str,
fields: &serde_json::Map<String, serde_json::Value>,
) -> Result<Hash>;
fn cal_accumulate(
&self,
grain_type: &str,
target: &super::ast::AccumulateTarget,
add_ops: &[(String, f64)],
set_ops: &serde_json::Map<String, serde_json::Value>,
reason: &str,
) -> Result<AccumulateResult> {
let _ = (grain_type, target, add_ops, set_ops, reason);
Err(areev_core::error::AreevError::Internal(
"accumulate not available".into(),
))
}
fn describe_capabilities(&self) -> CalCapabilities {
CalCapabilities::default()
}
fn describe_grain_types(&self) -> Vec<GrainTypeInfo> {
Vec::new()
}
fn describe_fields(&self, _grain_type: Option<areev_core::types::GrainType>) -> Vec<FieldInfo> {
Vec::new()
}
fn note_assembly_budget(&self, _overflow: bool) {}
fn anon_egress_report(&self) -> Option<serde_json::Value> {
None
}
fn note_assembly_manifest(&self, _manifest: &AssemblyManifest) {}
fn define_template(
&self,
_name: &str,
_source: &str,
_description: Option<&str>,
_parent: Option<&str>,
_grain_types: &[String],
) -> Result<()> {
Err(areev_core::error::AreevError::Internal(
"template management not available".into(),
))
}
fn drop_template(&self, _name: &str) -> Result<()> {
Err(areev_core::error::AreevError::Internal(
"template management not available".into(),
))
}
fn open_forks(&self) -> Result<Vec<ForkGroupInfo>> {
Ok(Vec::new())
}
fn rerank_passages(
&self,
_query: &str,
_passages: &[&str],
_rerank_type: RerankType,
_model: Option<&str>,
_user_id: Option<&str>,
) -> Result<Vec<usize>> {
Ok((0.._passages.len()).collect())
}
fn cal_delete(&self, _hash: &Hash, _because: Option<&str>) -> Result<()> {
Err(areev_core::error::AreevError::Internal(
"destructive operations not available".into(),
))
}
fn cal_forget_user(
&self,
_user_id: &str,
_text_mentions: bool,
_because: &str,
) -> Result<crate::store_types::ErasureProof> {
Err(areev_core::error::AreevError::Internal(
"destructive operations not available".into(),
))
}
fn cal_forget_scope(&self, _scope: &str) -> Result<crate::store_types::ErasureProof> {
Err(areev_core::error::AreevError::Internal(
"destructive operations not available".into(),
))
}
fn cal_subject_report(
&self,
_subject_id: &str,
_text_mentions: bool,
) -> Result<crate::store_types::SubjectReportResult> {
Err(areev_core::error::AreevError::Internal(
"subject report not available on this facade".into(),
))
}
fn as_any(&self) -> Option<&dyn std::any::Any> {
None
}
fn cal_merge(
&self,
_subject: &str,
_relation: &str,
_object: &str,
_confidence: f64,
_because: &str,
) -> Result<Hash> {
Err(areev_core::error::AreevError::Internal(
"merge not available".into(),
))
}
fn cal_related(
&self,
_start: &str,
_relations: &[&str],
_direction: &str,
_depth: usize,
_limit: usize,
) -> Result<Vec<String>> {
Err(areev_core::error::AreevError::Internal(
"graph walks not available".into(),
))
}
fn cal_novelty(
&self,
_text: &str,
_subject: Option<&str>,
_relation: Option<&str>,
_k: usize,
) -> Result<Vec<(String, f64)>> {
Err(areev_core::error::AreevError::Internal(
"novelty checks not available".into(),
))
}
fn cal_entity_at(
&self,
_subject: &str,
_relation: &str,
_at_ms: i64,
_axis: &str,
) -> Result<Option<serde_json::Value>> {
Err(areev_core::error::AreevError::Internal(
"as-of reads not available".into(),
))
}
fn cal_run_trace(&self, _run_id: &str, _limit: usize) -> Result<serde_json::Value> {
Err(areev_core::error::AreevError::Internal(
"run traces not available".into(),
))
}
fn cal_runs_touching(&self, _hash: &Hash, _depth: usize) -> Result<Vec<String>> {
Err(areev_core::error::AreevError::Internal(
"run joins not available".into(),
))
}
fn cal_derived_from(&self, _hash: &Hash) -> Result<Vec<serde_json::Value>> {
Err(areev_core::error::AreevError::Internal(
"provenance reads not available".into(),
))
}
fn cal_stats(&self) -> Result<serde_json::Value> {
Err(areev_core::error::AreevError::Internal(
"stats not available".into(),
))
}
fn cal_verify(&self) -> Result<serde_json::Value> {
Err(areev_core::error::AreevError::Internal(
"integrity checks not available".into(),
))
}
fn cal_remember(
&self,
_content: &str,
_session_id: Option<&str>,
_role: Option<&str>,
_run_id: Option<&str>,
) -> Result<Hash> {
Err(areev_core::error::AreevError::Internal(
"remember not available".into(),
))
}
fn cal_grant(
&self,
_principal: &str,
_verbs: &[String],
_namespaces: &[String],
_because: Option<&str>,
) -> Result<Hash> {
Err(areev_core::error::AreevError::Internal(
"control operations not available".into(),
))
}
fn cal_revoke(
&self,
_principal: &str,
_verbs: &[String],
_namespaces: &[String],
_because: Option<&str>,
) -> Result<usize> {
Err(areev_core::error::AreevError::Internal(
"control operations not available".into(),
))
}
fn cal_show_grants(&self, _principal: Option<&str>) -> Result<Vec<GrantRow>> {
Ok(Vec::new())
}
fn cal_purge_stale(
&self,
_min_age_days: f64,
_namespace: Option<&str>,
_batch_limit: usize,
_grain_type: Option<&str>,
_because: &str,
) -> Result<usize> {
Err(areev_core::error::AreevError::Internal(
"destructive operations not available".into(),
))
}
fn list_templates(&self) -> Vec<TemplateInfo> {
Vec::new()
}
fn get_template(&self, _name: &str) -> Option<TemplateInfo> {
None
}
fn record_template_run(&self, _name: &str) {}
fn define_query(
&self,
_name: &str,
_body: &str,
_description: Option<&str>,
_params: &[crate::ast::QueryParam],
) -> Result<()> {
Err(areev_core::error::AreevError::Internal(
"saved query management not available".into(),
))
}
fn drop_query(&self, _name: &str) -> Result<()> {
Err(areev_core::error::AreevError::Internal(
"saved query management not available".into(),
))
}
fn list_queries(&self) -> Vec<crate::queries::QueryListEntry> {
Vec::new()
}
fn get_query(&self, _name: &str) -> Option<crate::queries::QueryEntry> {
None
}
fn update_query_last_run(&self, _name: &str) -> Result<()> {
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct AccumulateResult {
pub old_hash: Hash,
pub new_hash: Hash,
pub applied_deltas: Vec<(String, f64, f64)>,
}
pub use super::templates::TemplateListEntry as TemplateInfo;
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct GrantRow {
pub principal: String,
pub object: String,
pub hash: String,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct CalCapabilities {
pub cal_version: u8,
pub conformance_level: u8,
pub supported_statements: Vec<String>,
pub max_sources: u8,
pub max_let_bindings: u8,
pub max_budget_tokens: u32,
}
impl Default for CalCapabilities {
fn default() -> Self {
Self {
cal_version: 1,
conformance_level: 2,
supported_statements: vec![
"RECALL".into(),
"EXISTS".into(),
"ASSEMBLE".into(),
"HISTORY".into(),
"EXPLAIN".into(),
"DESCRIBE".into(),
"BATCH".into(),
"COALESCE".into(),
"ADD".into(),
"SUPERSEDE".into(),
"ACCUMULATE".into(),
"REVERT".into(),
"FORGET".into(),
"PURGE".into(),
"REPORT".into(),
"DROP".into(),
"DEFINE".into(),
"RUN".into(),
"GRANT".into(),
"REVOKE".into(),
"SHOW".into(),
"REMEMBER".into(),
"MERGE".into(),
"ENTITY".into(),
"RUNS".into(),
"DERIVED".into(),
"RELATED".into(),
"NOVELTY".into(),
"APPROVE".into(),
"REJECT".into(),
"APPLY".into(),
"ROLLBACK".into(),
],
max_sources: 8,
max_let_bindings: 5,
max_budget_tokens: 100_000,
}
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct GrainTypeInfo {
pub name: String,
pub plural: String,
pub specific_fields: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct FieldInfo {
pub name: String,
pub field_type: String,
pub filterable: bool,
pub sortable: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use areev_core::error::AreevError;
struct MockStore {
grains: Vec<(Hash, DeserializedGrain)>,
default_ns: Option<String>,
active_user: Option<String>,
}
impl MockStore {
fn empty() -> Self {
Self {
grains: Vec::new(),
default_ns: None,
active_user: None,
}
}
fn with_namespace(ns: &str) -> Self {
Self {
grains: Vec::new(),
default_ns: Some(ns.to_string()),
active_user: None,
}
}
fn with_user(user: &str) -> Self {
Self {
grains: Vec::new(),
default_ns: None,
active_user: Some(user.to_string()),
}
}
}
fn make_grain(subject: &str) -> DeserializedGrain {
use areev_core::format::header::MgHeader;
use areev_core::types::GrainType;
use std::collections::HashMap;
let mut fields = HashMap::new();
fields.insert(
"subject".to_string(),
serde_json::Value::String(subject.to_string()),
);
fields.insert(
"grain_type".to_string(),
serde_json::Value::String("fact".to_string()),
);
let mut hash_bytes = [0u8; 32];
for (i, b) in subject.as_bytes().iter().enumerate().take(32) {
hash_bytes[i] = *b;
}
let hash = Hash::from_bytes(&hash_bytes);
DeserializedGrain {
header: MgHeader {
version: 1,
flags: 0,
grain_type: 0x01, ns_hash: 0,
created_at_sec: 0,
},
grain_type: GrainType::Fact,
fields,
hash,
}
}
impl CalStoreFacade for MockStore {
fn recall(&self, params: &RecallParams) -> Result<Vec<SearchHit>> {
let mut hits: Vec<SearchHit> = self
.grains
.iter()
.filter(|(_, g)| {
if let Some(ref s) = params.subject {
if g.get_str("subject") != Some(s.as_str()) {
return false;
}
}
true
})
.map(|(hash, grain)| SearchHit {
grain: grain.clone(),
score: 1.0,
hash: *hash,
score_breakdown: None,
explanation: None,
scope_depth: None,
source_namespace: None,
#[cfg(feature = "rerank")]
rerank_score: None,
#[cfg(feature = "llm-rerank")]
llm_rerank_score: None,
relative_time: None,
conflict_status: None,
supersession_status: None,
superseded_by_hash: None,
recall_source: None,
})
.collect();
if let Some(limit) = params.limit {
hits.truncate(limit);
}
Ok(hits)
}
fn exists(&self, hash: &Hash) -> Result<bool> {
Ok(self.grains.iter().any(|(h, _)| h == hash))
}
fn get(&self, hash: &Hash) -> Result<DeserializedGrain> {
self.grains
.iter()
.find(|(h, _)| h == hash)
.map(|(_, g)| g.clone())
.ok_or(AreevError::NotFound(*hash))
}
fn count(&self) -> Result<usize> {
Ok(self.grains.len())
}
fn get_history(
&self,
_namespace: &str,
_subject: &str,
_relation: &str,
) -> Result<Vec<VersionEntry>> {
Ok(Vec::new())
}
fn cal_add(
&self,
_grain_type: &str,
_fields: &serde_json::Map<String, serde_json::Value>,
) -> Result<Hash> {
Err(AreevError::Validation(
"mock: cal_add not implemented".into(),
))
}
fn cal_supersede(
&self,
_old_hash: &Hash,
_grain_type: &str,
_fields: &serde_json::Map<String, serde_json::Value>,
) -> Result<Hash> {
Err(AreevError::Validation(
"mock: cal_supersede not implemented".into(),
))
}
fn default_namespace(&self) -> Option<&str> {
self.default_ns.as_deref()
}
fn active_user(&self) -> Option<&str> {
self.active_user.as_deref()
}
fn list_templates(&self) -> Vec<TemplateInfo> {
let registry = crate::templates::TemplateRegistry::new();
registry.list()
}
}
#[test]
fn test_trait_is_object_safe() {
let store = MockStore::empty();
let _facade: &dyn CalStoreFacade = &store;
}
#[test]
fn test_mock_count_empty() {
let store = MockStore::empty();
let facade: &dyn CalStoreFacade = &store;
assert_eq!(facade.count().unwrap(), 0);
}
#[test]
fn test_mock_exists_not_found() {
let store = MockStore::empty();
let facade: &dyn CalStoreFacade = &store;
let hash = Hash::from_bytes(&[0u8; 32]);
assert!(!facade.exists(&hash).unwrap());
}
#[test]
fn test_mock_get_not_found() {
let store = MockStore::empty();
let facade: &dyn CalStoreFacade = &store;
let hash = Hash::from_bytes(&[0u8; 32]);
let result = facade.get(&hash);
assert!(result.is_err());
match result.unwrap_err() {
AreevError::NotFound(_) => {}
other => panic!("expected NotFound, got {:?}", other),
}
}
#[test]
fn test_mock_recall_empty_store() {
let store = MockStore::empty();
let facade: &dyn CalStoreFacade = &store;
let params = RecallParams::default();
let hits = facade.recall(¶ms).unwrap();
assert!(hits.is_empty());
}
#[test]
fn test_mock_default_namespace_none() {
let store = MockStore::empty();
let facade: &dyn CalStoreFacade = &store;
assert_eq!(facade.default_namespace(), None);
}
#[test]
fn test_mock_default_namespace_some() {
let store = MockStore::with_namespace("acme");
let facade: &dyn CalStoreFacade = &store;
assert_eq!(facade.default_namespace(), Some("acme"));
}
#[test]
fn test_mock_active_user_none() {
let store = MockStore::empty();
let facade: &dyn CalStoreFacade = &store;
assert_eq!(facade.active_user(), None);
}
#[test]
fn test_mock_active_user_some() {
let store = MockStore::with_user("john");
let facade: &dyn CalStoreFacade = &store;
assert_eq!(facade.active_user(), Some("john"));
}
#[test]
fn test_mock_get_history_empty() {
let store = MockStore::empty();
let facade: &dyn CalStoreFacade = &store;
let history = facade.get_history("ns", "john", "likes").unwrap();
assert!(history.is_empty());
}
#[test]
fn test_facade_can_be_boxed() {
let store = MockStore::empty();
let _boxed: Box<dyn CalStoreFacade> = Box::new(store);
}
#[test]
fn test_facade_recall_with_subject_filter() {
let grain = make_grain("john");
let hash = grain.hash;
let mut store = MockStore::empty();
store.grains.push((hash, grain));
let facade: &dyn CalStoreFacade = &store;
let params = RecallParams {
subject: Some("john".to_string()),
..Default::default()
};
let hits = facade.recall(¶ms).unwrap();
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].hash, hash);
}
#[test]
fn test_facade_recall_subject_filter_no_match() {
let grain = make_grain("john");
let hash = grain.hash;
let mut store = MockStore::empty();
store.grains.push((hash, grain));
let facade: &dyn CalStoreFacade = &store;
let params = RecallParams {
subject: Some("bob".to_string()),
..Default::default()
};
let hits = facade.recall(¶ms).unwrap();
assert!(hits.is_empty());
}
#[test]
fn test_facade_exists_known_hash() {
let grain = make_grain("john");
let hash = grain.hash;
let mut store = MockStore::empty();
store.grains.push((hash, grain));
let facade: &dyn CalStoreFacade = &store;
assert!(facade.exists(&hash).unwrap());
}
#[test]
fn test_facade_get_known_hash() {
let grain = make_grain("john");
let hash = grain.hash;
let mut store = MockStore::empty();
store.grains.push((hash, grain));
let facade: &dyn CalStoreFacade = &store;
let retrieved = facade.get(&hash).unwrap();
assert_eq!(retrieved.get_str("subject"), Some("john"));
}
#[test]
fn test_facade_describe_capabilities_default() {
let store = MockStore::empty();
let facade: &dyn CalStoreFacade = &store;
let caps = facade.describe_capabilities();
assert_eq!(caps.cal_version, 1);
assert_eq!(caps.conformance_level, 2);
assert!(!caps.supported_statements.is_empty());
assert!(caps.supported_statements.contains(&"RECALL".to_string()));
assert!(caps.supported_statements.contains(&"ASSEMBLE".to_string()));
assert!(caps.supported_statements.contains(&"COALESCE".to_string()));
assert_eq!(caps.max_sources, 8);
assert_eq!(caps.max_let_bindings, 5);
assert_eq!(caps.max_budget_tokens, 100_000);
}
#[test]
fn test_facade_describe_grain_types_default_is_empty() {
let store = MockStore::empty();
let facade: &dyn CalStoreFacade = &store;
let types = facade.describe_grain_types();
assert!(types.is_empty(), "default impl should return empty");
}
#[test]
fn test_facade_describe_fields_default_is_empty() {
let store = MockStore::empty();
let facade: &dyn CalStoreFacade = &store;
let fields = facade.describe_fields(None);
assert!(fields.is_empty(), "default impl should return empty");
}
#[test]
fn test_cal_capabilities_default_values() {
let caps = CalCapabilities::default();
assert_eq!(caps.cal_version, 1);
assert_eq!(caps.conformance_level, 2);
assert_eq!(caps.supported_statements.len(), 32);
for kw in [
"GRANT", "REVOKE", "SHOW", "REMEMBER", "MERGE", "ENTITY", "RUNS", "DERIVED",
"RELATED", "NOVELTY", "APPROVE", "REJECT", "APPLY", "ROLLBACK",
] {
assert!(
caps.supported_statements.contains(&kw.to_string()),
"CAL 1.3 statement {kw} parses but is not advertised"
);
}
assert!(caps.supported_statements.contains(&"REPORT".to_string()));
assert!(caps.supported_statements.contains(&"RECALL".to_string()));
assert!(caps.supported_statements.contains(&"DEFINE".to_string()));
assert!(caps.supported_statements.contains(&"RUN".to_string()));
assert!(caps.supported_statements.contains(&"EXISTS".to_string()));
assert!(caps.supported_statements.contains(&"ASSEMBLE".to_string()));
assert!(caps.supported_statements.contains(&"HISTORY".to_string()));
assert!(caps.supported_statements.contains(&"EXPLAIN".to_string()));
assert!(caps.supported_statements.contains(&"DESCRIBE".to_string()));
assert!(caps.supported_statements.contains(&"BATCH".to_string()));
assert!(caps.supported_statements.contains(&"COALESCE".to_string()));
assert!(caps.supported_statements.contains(&"ADD".to_string()));
assert!(caps.supported_statements.contains(&"SUPERSEDE".to_string()));
assert!(caps.supported_statements.contains(&"REVERT".to_string()));
assert!(caps.supported_statements.contains(&"FORGET".to_string()));
assert!(caps.supported_statements.contains(&"PURGE".to_string()));
assert!(caps.supported_statements.contains(&"DROP".to_string()));
assert_eq!(caps.max_sources, 8);
assert_eq!(caps.max_let_bindings, 5);
assert_eq!(caps.max_budget_tokens, 100_000);
}
#[test]
fn test_grain_type_info_struct() {
let info = GrainTypeInfo {
name: "fact".to_string(),
plural: "facts".to_string(),
specific_fields: vec!["subject".to_string(), "relation".to_string()],
};
assert_eq!(info.name, "fact");
assert_eq!(info.plural, "facts");
assert_eq!(info.specific_fields.len(), 2);
}
#[test]
fn test_field_info_struct() {
let info = FieldInfo {
name: "subject".to_string(),
field_type: "string".to_string(),
filterable: true,
sortable: true,
};
assert_eq!(info.name, "subject");
assert!(info.filterable);
assert!(info.sortable);
}
}