Skip to main content

locus_core_rs/storage/surrealdb/
models.rs

1use crate::domain::models::{ConnectorMetadata, SemanticLink};
2use serde::Deserialize;
3use serde::de::{DeserializeOwned, Deserializer, Error as DeError};
4use serde_json::Value;
5
6fn deserialize_optional_string_vec<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
7where
8    D: Deserializer<'de>,
9{
10    deserialize_optional_vec(deserializer)
11}
12
13fn deserialize_optional_semantic_links<'de, D>(
14    deserializer: D,
15) -> Result<Option<Vec<SemanticLink>>, D::Error>
16where
17    D: Deserializer<'de>,
18{
19    deserialize_optional_vec(deserializer)
20}
21
22fn deserialize_optional_vec<'de, T, D>(deserializer: D) -> Result<Option<Vec<T>>, D::Error>
23where
24    T: DeserializeOwned,
25    D: Deserializer<'de>,
26{
27    let value = Option::<Value>::deserialize(deserializer)?;
28    match value {
29        None => Ok(None),
30        Some(Value::Null) => Ok(None),
31        Some(Value::String(raw)) if raw.trim().eq_ignore_ascii_case("null") => Ok(None),
32        Some(Value::Array(items)) if items.is_empty() => Ok(None),
33        Some(other) => {
34            let items: Vec<T> = serde_json::from_value(other).map_err(DeError::custom)?;
35            if items.is_empty() {
36                Ok(None)
37            } else {
38                Ok(Some(items))
39            }
40        }
41    }
42}
43
44#[derive(Debug, Clone, Deserialize)]
45pub struct SurrealNodeRecord {
46    #[serde(rename = "SessionId")]
47    pub session_id: String,
48    #[serde(rename = "Raw")]
49    pub raw: String,
50    #[serde(rename = "Tier")]
51    pub tier: String,
52    #[serde(rename = "Timestamp")]
53    pub timestamp: String,
54    #[serde(rename = "CompressionDepth")]
55    pub compression_depth: i32,
56    #[serde(rename = "ParentNodeId", default)]
57    pub parent_node_id: Option<String>,
58    #[serde(rename = "SyncKey", default)]
59    pub sync_key: Option<String>,
60    #[serde(rename = "UpdatedAt", default)]
61    pub updated_at: Option<String>,
62    #[serde(rename = "SourceMetadata", default)]
63    pub source_metadata: Option<ConnectorMetadata>,
64    #[serde(rename = "ContextSummary", default)]
65    pub context_summary: Option<String>,
66    #[serde(rename = "SemanticTags", default, deserialize_with = "deserialize_optional_string_vec")]
67    pub semantic_tags: Option<Vec<String>>,
68    #[serde(rename = "SemanticLinks", default, deserialize_with = "deserialize_optional_semantic_links")]
69    pub semantic_links: Option<Vec<SemanticLink>>,
70    #[serde(rename = "Embedding", default)]
71    pub embedding: Option<Vec<f32>>,
72    #[serde(rename = "EmbeddingModel", default)]
73    pub embedding_model: Option<String>,
74    #[serde(rename = "EmbeddingDimensions", default)]
75    pub embedding_dimensions: Option<usize>,
76    #[serde(rename = "EmbeddedAt", default)]
77    pub embedded_at: Option<String>,
78    #[serde(rename = "Psi", default)]
79    pub psi: f64,
80    #[serde(rename = "Rho", default)]
81    pub rho: f64,
82    #[serde(rename = "Kappa", default)]
83    pub kappa: f64,
84    #[serde(rename = "UserStability", default)]
85    pub user_stability: f64,
86    #[serde(rename = "UserFriction", default)]
87    pub user_friction: f64,
88    #[serde(rename = "UserLogic", default)]
89    pub user_logic: f64,
90    #[serde(rename = "UserAutonomy", default)]
91    pub user_autonomy: f64,
92    #[serde(rename = "UserPsi", default)]
93    pub user_psi: f64,
94    #[serde(rename = "ModelStability", default)]
95    pub model_stability: f64,
96    #[serde(rename = "ModelFriction", default)]
97    pub model_friction: f64,
98    #[serde(rename = "ModelLogic", default)]
99    pub model_logic: f64,
100    #[serde(rename = "ModelAutonomy", default)]
101    pub model_autonomy: f64,
102    #[serde(rename = "ModelPsi", default)]
103    pub model_psi: f64,
104    #[serde(rename = "CompStability", default)]
105    pub comp_stability: f64,
106    #[serde(rename = "CompFriction", default)]
107    pub comp_friction: f64,
108    #[serde(rename = "CompLogic", default)]
109    pub comp_logic: f64,
110    #[serde(rename = "CompAutonomy", default)]
111    pub comp_autonomy: f64,
112    #[serde(rename = "CompPsi", default)]
113    pub comp_psi: f64,
114    #[serde(rename = "ResonanceDelta", default)]
115    pub resonance_delta: f64,
116}
117
118#[derive(Debug, Clone, Deserialize)]
119pub struct SurrealSemanticTagRecord {
120    #[serde(rename = "TenantId", default)]
121    pub tenant_id: String,
122    #[serde(rename = "SessionId", default)]
123    pub session_id: String,
124    #[serde(rename = "NodeId", default)]
125    pub node_id: String,
126    #[serde(rename = "SyncKey", default)]
127    pub sync_key: String,
128    #[serde(rename = "Tag", default)]
129    pub tag: String,
130    #[serde(rename = "Embedding", default)]
131    pub embedding: Option<Vec<f32>>,
132    #[serde(rename = "EmbeddingModel", default)]
133    pub embedding_model: Option<String>,
134    #[serde(rename = "EmbeddingDimensions", default)]
135    pub embedding_dimensions: Option<usize>,
136    #[serde(rename = "EmbeddedAt", default)]
137    pub embedded_at: Option<String>,
138    #[serde(rename = "UpdatedAt", default)]
139    pub updated_at: Option<String>,
140}
141
142#[derive(Debug, Clone, Deserialize)]
143pub struct SurrealTagVocabularyRecord {
144    #[serde(rename = "Tag", default)]
145    pub tag: String,
146}
147
148#[derive(Debug, Clone, Deserialize)]
149pub struct SurrealSyncKeyRecord {
150    #[serde(rename = "SyncKey", default)]
151    pub sync_key: String,
152}
153
154#[derive(Debug, Clone, Deserialize)]
155pub struct SurrealAvecRecord {
156    #[serde(default)]
157    pub stability: f32,
158    #[serde(default)]
159    pub friction: f32,
160    #[serde(default)]
161    pub logic: f32,
162    #[serde(default)]
163    pub autonomy: f32,
164    #[serde(default)]
165    pub psi: f32,
166    #[serde(default)]
167    pub created_at: Option<String>,
168}
169
170#[derive(Debug, Clone, Deserialize)]
171pub struct SurrealTriggerRecord {
172    pub trigger: String,
173}
174
175#[derive(Debug, Clone, Deserialize)]
176pub struct SurrealExistingNodeRecord {
177    #[serde(rename = "Id", default)]
178    pub id: Value,
179    #[serde(rename = "SourceMetadata", default)]
180    pub source_metadata: Option<ConnectorMetadata>,
181    #[serde(rename = "ContextSummary", default)]
182    pub context_summary: Option<String>,
183    #[serde(rename = "Embedding", default)]
184    pub embedding: Option<Vec<f32>>,
185    #[serde(rename = "EmbeddingModel", default)]
186    pub embedding_model: Option<String>,
187    #[serde(rename = "EmbeddingDimensions", default)]
188    pub embedding_dimensions: Option<usize>,
189    #[serde(rename = "EmbeddedAt", default)]
190    pub embedded_at: Option<String>,
191}
192
193#[derive(Debug, Clone, Deserialize)]
194pub struct SurrealCheckpointRecord {
195    #[serde(rename = "SessionId")]
196    pub session_id: String,
197    #[serde(rename = "ConnectorId")]
198    pub connector_id: String,
199    #[serde(rename = "CursorUpdatedAt", default)]
200    pub cursor_updated_at: Option<String>,
201    #[serde(rename = "CursorSyncKey", default)]
202    pub cursor_sync_key: Option<String>,
203    #[serde(rename = "UpdatedAt")]
204    pub updated_at: String,
205    #[serde(rename = "Metadata", default)]
206    pub metadata: Option<ConnectorMetadata>,
207}
208
209#[cfg(test)]
210mod tests {
211    use super::SurrealNodeRecord;
212    use serde_json::json;
213
214    #[test]
215    fn surreal_node_record_treats_null_semantic_fields_as_absent() {
216        let row = json!({
217            "SessionId": "s1",
218            "Raw": "raw",
219            "Tier": "raw",
220            "Timestamp": "2026-03-05T06:30:00Z",
221            "CompressionDepth": 1,
222            "SemanticTags": null,
223            "SemanticLinks": null,
224        });
225
226        let record: SurrealNodeRecord = serde_json::from_value(row).expect("should deserialize");
227        assert_eq!(record.semantic_tags, None);
228        assert_eq!(record.semantic_links, None);
229    }
230
231    #[test]
232    fn surreal_node_record_treats_string_null_semantic_fields_as_absent() {
233        let row = json!({
234            "SessionId": "s1",
235            "Raw": "raw",
236            "Tier": "raw",
237            "Timestamp": "2026-03-05T06:30:00Z",
238            "CompressionDepth": 1,
239            "SemanticTags": "null",
240            "SemanticLinks": "null",
241        });
242
243        let record: SurrealNodeRecord = serde_json::from_value(row).expect("should deserialize");
244        assert_eq!(record.semantic_tags, None);
245        assert_eq!(record.semantic_links, None);
246    }
247}