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}