use redb::{MultimapTableDefinition, TableDefinition};
use serde::{Deserialize, Serialize};
use crate::config::EmbeddingDimension;
use crate::experience::ExperienceType;
use crate::types::{AgentId, CollectiveId, ExperienceId, TaskId, Timestamp};
pub const SCHEMA_VERSION: u32 = 3;
pub const SUBSTRATE_FORMAT_KEY: &str = "substrate_format";
pub const SUBSTRATE_MAGIC: [u8; 2] = *b"PS";
pub const CURRENT_SUBSTRATE_FORMAT: u8 = 2;
pub const LEGACY_SUBSTRATE_FORMAT: u8 = 0;
pub const SUBSTRATE_MARKER_LEN: usize = 3;
pub const MAX_CONTENT_SIZE: usize = 100 * 1024;
pub const MAX_DOMAIN_TAGS: usize = 50;
pub const MAX_TAG_LENGTH: usize = 100;
pub const MAX_SOURCE_FILES: usize = 100;
pub const MAX_FILE_PATH_LENGTH: usize = 500;
pub const MAX_SOURCE_AGENT_LENGTH: usize = 256;
pub const MAX_RELATION_METADATA_SIZE: usize = 10 * 1024;
pub const MAX_INSIGHT_CONTENT_SIZE: usize = 50 * 1024;
pub const MAX_INSIGHT_SOURCES: usize = 100;
pub const MAX_ACTIVITY_AGENT_ID_LENGTH: usize = 255;
pub const MAX_ACTIVITY_FIELD_SIZE: usize = 1024;
pub const METADATA_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("metadata");
pub const COLLECTIVES_TABLE: TableDefinition<&[u8; 16], &[u8]> =
TableDefinition::new("collectives");
pub const DECAY_CONFIGS_TABLE: TableDefinition<&[u8; 16], &[u8]> =
TableDefinition::new("decay_configs");
pub const EXPERIENCES_TABLE: TableDefinition<&[u8; 16], &[u8]> =
TableDefinition::new("experiences");
pub const EXPERIENCES_BY_COLLECTIVE_TABLE: MultimapTableDefinition<&[u8; 16], &[u8; 24]> =
MultimapTableDefinition::new("experiences_by_collective");
pub const EXPERIENCES_BY_TYPE_TABLE: MultimapTableDefinition<&[u8; 17], &[u8; 16]> =
MultimapTableDefinition::new("experiences_by_type");
pub const EMBEDDINGS_TABLE: TableDefinition<&[u8; 16], &[u8]> = TableDefinition::new("embeddings");
pub const RELATIONS_TABLE: TableDefinition<&[u8; 16], &[u8]> = TableDefinition::new("relations");
pub const RELATIONS_BY_SOURCE_TABLE: MultimapTableDefinition<&[u8; 16], &[u8; 16]> =
MultimapTableDefinition::new("relations_by_source");
pub const RELATIONS_BY_TARGET_TABLE: MultimapTableDefinition<&[u8; 16], &[u8; 16]> =
MultimapTableDefinition::new("relations_by_target");
pub const INSIGHTS_TABLE: TableDefinition<&[u8; 16], &[u8]> = TableDefinition::new("insights");
pub const INSIGHTS_BY_COLLECTIVE_TABLE: MultimapTableDefinition<&[u8; 16], &[u8; 16]> =
MultimapTableDefinition::new("insights_by_collective");
pub const ACTIVITIES_TABLE: TableDefinition<&[u8], &[u8]> = TableDefinition::new("activities");
pub const INSTANCE_ID_KEY: &str = "instance_id";
#[cfg(feature = "sync")]
pub const SYNC_CURSORS_TABLE: TableDefinition<&[u8; 16], &[u8]> =
TableDefinition::new("sync_cursors");
pub const WAL_SEQUENCE_KEY: &str = "wal_sequence";
pub const WATCH_EVENTS_TABLE: TableDefinition<&[u8; 8], &[u8]> =
TableDefinition::new("watch_events");
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct WatchEventRecord {
pub entity_id: [u8; 16],
pub collective_id: [u8; 16],
pub event_type: WatchEventTypeTag,
pub timestamp_ms: i64,
pub entity_type: EntityTypeTag,
}
#[derive(Deserialize)]
pub(crate) struct WatchEventRecordV1 {
pub experience_id: [u8; 16],
pub collective_id: [u8; 16],
pub event_type: WatchEventTypeTag,
pub timestamp_ms: i64,
}
#[derive(Deserialize)]
pub(crate) struct ExperienceV2 {
pub id: ExperienceId,
pub collective_id: CollectiveId,
pub content: String,
#[serde(skip)]
pub embedding: Vec<f32>,
pub experience_type: ExperienceType,
pub importance: f32,
pub confidence: f32,
pub applications: u32,
pub domain: Vec<String>,
pub related_files: Vec<String>,
pub source_agent: AgentId,
pub source_task: Option<TaskId>,
pub timestamp: Timestamp,
pub archived: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum WatchEventTypeTag {
Created = 0,
Updated = 1,
Archived = 2,
Deleted = 3,
}
impl WatchEventTypeTag {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(Self::Created),
1 => Some(Self::Updated),
2 => Some(Self::Archived),
3 => Some(Self::Deleted),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum EntityTypeTag {
#[default]
Experience = 0,
Relation = 1,
Insight = 2,
Collective = 3,
}
impl EntityTypeTag {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(Self::Experience),
1 => Some(Self::Relation),
2 => Some(Self::Insight),
3 => Some(Self::Collective),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum ExperienceTypeTag {
Difficulty = 0,
Solution = 1,
ErrorPattern = 2,
SuccessPattern = 3,
UserPreference = 4,
ArchitecturalDecision = 5,
TechInsight = 6,
Fact = 7,
Generic = 8,
}
impl ExperienceTypeTag {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(Self::Difficulty),
1 => Some(Self::Solution),
2 => Some(Self::ErrorPattern),
3 => Some(Self::SuccessPattern),
4 => Some(Self::UserPreference),
5 => Some(Self::ArchitecturalDecision),
6 => Some(Self::TechInsight),
7 => Some(Self::Fact),
8 => Some(Self::Generic),
_ => None,
}
}
pub fn all() -> &'static [Self] {
&[
Self::Difficulty,
Self::Solution,
Self::ErrorPattern,
Self::SuccessPattern,
Self::UserPreference,
Self::ArchitecturalDecision,
Self::TechInsight,
Self::Fact,
Self::Generic,
]
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DatabaseMetadata {
pub schema_version: u32,
pub embedding_dimension: EmbeddingDimension,
pub created_at: Timestamp,
pub last_opened_at: Timestamp,
}
impl DatabaseMetadata {
pub fn new(embedding_dimension: EmbeddingDimension) -> Self {
let now = Timestamp::now();
Self {
schema_version: SCHEMA_VERSION,
embedding_dimension,
created_at: now,
last_opened_at: now,
}
}
pub fn touch(&mut self) {
self.last_opened_at = Timestamp::now();
}
pub fn is_compatible(&self) -> bool {
self.schema_version == SCHEMA_VERSION
}
}
#[inline]
pub fn encode_collective_timestamp_key(collective_id: &[u8; 16], timestamp: Timestamp) -> [u8; 24] {
let mut key = [0u8; 24];
key[..16].copy_from_slice(collective_id);
key[16..24].copy_from_slice(×tamp.to_be_bytes());
key
}
#[inline]
pub fn decode_timestamp_from_key(key: &[u8; 24]) -> Timestamp {
let mut bytes = [0u8; 8];
bytes.copy_from_slice(&key[16..24]);
Timestamp::from_millis(i64::from_be_bytes(bytes))
}
#[inline]
pub fn collective_range_start(collective_id: &[u8; 16]) -> [u8; 24] {
encode_collective_timestamp_key(collective_id, Timestamp::from_millis(0))
}
#[inline]
pub fn collective_range_end(collective_id: &[u8; 16]) -> [u8; 24] {
encode_collective_timestamp_key(collective_id, Timestamp::from_millis(i64::MAX))
}
#[inline]
pub fn encode_type_index_key(collective_id: &[u8; 16], type_tag: ExperienceTypeTag) -> [u8; 17] {
let mut key = [0u8; 17];
key[..16].copy_from_slice(collective_id);
key[16] = type_tag as u8;
key
}
#[inline]
pub fn decode_type_tag_from_key(key: &[u8; 17]) -> Option<ExperienceTypeTag> {
ExperienceTypeTag::from_u8(key[16])
}
#[inline]
pub fn decode_collective_from_type_key(key: &[u8; 17]) -> [u8; 16] {
let mut id = [0u8; 16];
id.copy_from_slice(&key[..16]);
id
}
#[inline]
pub fn encode_activity_key(collective_id: &[u8; 16], agent_id: &str) -> Vec<u8> {
let agent_bytes = agent_id.as_bytes();
let len = agent_bytes.len() as u16;
let mut key = Vec::with_capacity(16 + 2 + agent_bytes.len());
key.extend_from_slice(collective_id);
key.extend_from_slice(&len.to_be_bytes());
key.extend_from_slice(agent_bytes);
key
}
#[inline]
pub fn decode_collective_from_activity_key(key: &[u8]) -> [u8; 16] {
let mut id = [0u8; 16];
id.copy_from_slice(&key[..16]);
id
}
#[inline]
pub fn decode_agent_id_from_activity_key(key: &[u8]) -> &str {
let len = u16::from_be_bytes([key[16], key[17]]) as usize;
std::str::from_utf8(&key[18..18 + len]).expect("activity key contains invalid UTF-8")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_schema_version() {
assert_eq!(SCHEMA_VERSION, 3);
}
#[test]
fn test_database_metadata_new() {
let meta = DatabaseMetadata::new(EmbeddingDimension::D384);
assert_eq!(meta.schema_version, SCHEMA_VERSION);
assert_eq!(meta.embedding_dimension, EmbeddingDimension::D384);
assert!(meta.is_compatible());
}
#[test]
fn test_database_metadata_touch() {
let mut meta = DatabaseMetadata::new(EmbeddingDimension::D384);
let original = meta.last_opened_at;
std::thread::sleep(std::time::Duration::from_millis(1));
meta.touch();
assert!(meta.last_opened_at > original);
}
#[test]
fn test_database_metadata_serialization() {
let meta = DatabaseMetadata::new(EmbeddingDimension::D768);
let bytes = postcard::to_stdvec(&meta).unwrap();
let restored: DatabaseMetadata = postcard::from_bytes(&bytes).unwrap();
assert_eq!(meta.schema_version, restored.schema_version);
assert_eq!(meta.embedding_dimension, restored.embedding_dimension);
}
#[test]
fn test_encode_collective_timestamp_key() {
let collective_id = [1u8; 16];
let timestamp = Timestamp::from_millis(1234567890);
let key = encode_collective_timestamp_key(&collective_id, timestamp);
assert_eq!(&key[..16], &collective_id);
assert_eq!(decode_timestamp_from_key(&key), timestamp);
}
#[test]
fn test_key_ordering() {
let collective_id = [1u8; 16];
let t1 = Timestamp::from_millis(1000);
let t2 = Timestamp::from_millis(2000);
let key1 = encode_collective_timestamp_key(&collective_id, t1);
let key2 = encode_collective_timestamp_key(&collective_id, t2);
assert!(key1 < key2);
}
#[test]
fn test_collective_range() {
let collective_id = [42u8; 16];
let start = collective_range_start(&collective_id);
let end = collective_range_end(&collective_id);
let mid = encode_collective_timestamp_key(&collective_id, Timestamp::now());
assert!(start <= mid);
assert!(mid <= end);
}
#[test]
fn test_experience_type_tag_from_u8_roundtrip() {
for tag in ExperienceTypeTag::all() {
let byte = *tag as u8;
let restored = ExperienceTypeTag::from_u8(byte).unwrap();
assert_eq!(*tag, restored);
}
}
#[test]
fn test_experience_type_tag_from_u8_invalid() {
assert!(ExperienceTypeTag::from_u8(255).is_none());
assert!(ExperienceTypeTag::from_u8(9).is_none());
}
#[test]
fn test_experience_type_tag_all_variants() {
let all = ExperienceTypeTag::all();
assert_eq!(all.len(), 9);
assert_eq!(all[0], ExperienceTypeTag::Difficulty);
assert_eq!(all[5], ExperienceTypeTag::ArchitecturalDecision);
assert_eq!(all[8], ExperienceTypeTag::Generic);
}
#[test]
fn test_experience_type_tag_postcard_roundtrip() {
for tag in ExperienceTypeTag::all() {
let bytes = postcard::to_stdvec(tag).unwrap();
let restored: ExperienceTypeTag = postcard::from_bytes(&bytes).unwrap();
assert_eq!(*tag, restored);
}
}
#[test]
fn test_encode_type_index_key_roundtrip() {
let collective_id = [7u8; 16];
let tag = ExperienceTypeTag::SuccessPattern;
let key = encode_type_index_key(&collective_id, tag);
assert_eq!(decode_collective_from_type_key(&key), collective_id);
assert_eq!(decode_type_tag_from_key(&key), Some(tag));
}
#[test]
fn test_type_index_key_different_types_produce_different_keys() {
let collective_id = [1u8; 16];
let key_obs = encode_type_index_key(&collective_id, ExperienceTypeTag::Difficulty);
let key_les = encode_type_index_key(&collective_id, ExperienceTypeTag::SuccessPattern);
assert_ne!(key_obs, key_les);
assert_eq!(&key_obs[..16], &key_les[..16]);
assert_ne!(key_obs[16], key_les[16]);
}
#[test]
fn test_type_index_key_different_collectives_produce_different_keys() {
let id_a = [1u8; 16];
let id_b = [2u8; 16];
let tag = ExperienceTypeTag::Solution;
let key_a = encode_type_index_key(&id_a, tag);
let key_b = encode_type_index_key(&id_b, tag);
assert_ne!(key_a, key_b);
assert_eq!(key_a[16], key_b[16]);
}
#[test]
fn test_activity_key_encode_decode_roundtrip() {
let collective_id = [42u8; 16];
let agent_id = "claude-opus";
let key = encode_activity_key(&collective_id, agent_id);
assert_eq!(decode_collective_from_activity_key(&key), collective_id);
assert_eq!(decode_agent_id_from_activity_key(&key), agent_id);
}
#[test]
fn test_activity_key_different_agents_produce_different_keys() {
let collective_id = [1u8; 16];
let key_a = encode_activity_key(&collective_id, "agent-alpha");
let key_b = encode_activity_key(&collective_id, "agent-beta");
assert_ne!(key_a, key_b);
assert_eq!(&key_a[..16], &key_b[..16]);
}
#[test]
fn test_activity_key_different_collectives_produce_different_keys() {
let id_a = [1u8; 16];
let id_b = [2u8; 16];
let key_a = encode_activity_key(&id_a, "same-agent");
let key_b = encode_activity_key(&id_b, "same-agent");
assert_ne!(key_a, key_b);
assert_eq!(
decode_agent_id_from_activity_key(&key_a),
decode_agent_id_from_activity_key(&key_b)
);
}
#[test]
fn test_activity_key_format() {
let collective_id = [0xAB; 16];
let agent_id = "hi";
let key = encode_activity_key(&collective_id, agent_id);
assert_eq!(key.len(), 20);
assert_eq!(&key[..16], &[0xAB; 16]);
assert_eq!(&key[16..18], &[0, 2]);
assert_eq!(&key[18..], b"hi");
}
}