Skip to main content

semantic_memory/
types.rs

1#![allow(deprecated)]
2
3use crate::error::MemoryError;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use stack_ids::{
7    ClaimId, ClaimVersionId, EntityId, EnvelopeId, EpisodeId, RelationVersionId, ScopeKey,
8};
9
10/// Stable trace identifier used for cross-crate correlation and auditability.
11///
12/// ## Phase status: compatibility / migration-only
13///
14/// This is a crate-local `TraceId` retained for backward compatibility.
15/// The canonical replacement is `stack_ids::TraceCtx`. Use
16/// `TraceCtx::from_legacy_trace_id()` to convert.
17///
18/// **Removal condition**: removed when all internal usage migrates to `TraceCtx`.
19#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[serde(transparent)]
21pub struct CompatTraceId(pub String);
22
23#[deprecated(since = "0.5.0", note = "Use stack_ids::TraceCtx instead")]
24pub type TraceId = CompatTraceId;
25
26impl CompatTraceId {
27    /// Create a trace ID from any owned string-like input.
28    pub fn new(value: impl Into<String>) -> Self {
29        Self(value.into())
30    }
31
32    /// Borrow the trace ID as a string slice.
33    pub fn as_str(&self) -> &str {
34        &self.0
35    }
36}
37
38impl std::fmt::Display for CompatTraceId {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.write_str(&self.0)
41    }
42}
43
44impl From<String> for CompatTraceId {
45    fn from(value: String) -> Self {
46        Self(value)
47    }
48}
49
50impl From<&str> for CompatTraceId {
51    fn from(value: &str) -> Self {
52        Self(value.to_string())
53    }
54}
55
56/// Role of a message in a conversation.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "lowercase")]
59pub enum Role {
60    /// System prompt / instructions.
61    System,
62    /// User message.
63    User,
64    /// Assistant (LLM) response.
65    Assistant,
66    /// Tool call result.
67    Tool,
68}
69
70impl Role {
71    /// Convert to the string stored in SQLite.
72    pub fn as_str(&self) -> &'static str {
73        match self {
74            Role::System => "system",
75            Role::User => "user",
76            Role::Assistant => "assistant",
77            Role::Tool => "tool",
78        }
79    }
80
81    /// Parse from the string stored in SQLite.
82    pub fn from_str_value(s: &str) -> Option<Self> {
83        match s {
84            "system" => Some(Role::System),
85            "user" => Some(Role::User),
86            "assistant" => Some(Role::Assistant),
87            "tool" => Some(Role::Tool),
88            _ => None,
89        }
90    }
91}
92
93impl std::fmt::Display for Role {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        f.write_str(self.as_str())
96    }
97}
98
99impl std::str::FromStr for Role {
100    type Err = MemoryError;
101
102    fn from_str(s: &str) -> Result<Self, Self::Err> {
103        Self::from_str_value(s).ok_or_else(|| MemoryError::Other(format!("Unknown role: '{}'", s)))
104    }
105}
106
107/// Indicates whether a search result came from a fact, document chunk, message, or episode.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum SearchSourceType {
110    /// Result is from the facts table.
111    Facts,
112    /// Result is from the chunks table.
113    Chunks,
114    /// Result is from the messages table.
115    Messages,
116    /// Result is from the episodes table.
117    Episodes,
118}
119
120/// Controls whether search receipt metadata is produced.
121#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(rename_all = "snake_case")]
123pub enum ReceiptMode {
124    /// Do not produce receipt metadata.
125    #[default]
126    Disabled,
127    /// Produce receipt-ready metadata for explain/audit paths.
128    ExplainOnly,
129    /// Return receipt metadata to the caller.
130    ReturnReceipt,
131}
132
133/// Controls whether search should prefer exact reference scoring or allow approximate backends.
134#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
135#[serde(rename_all = "snake_case")]
136pub enum ExactnessProfile {
137    /// Use the configured default backend policy.
138    #[default]
139    Default,
140    /// Prefer exact brute-force f32 vector scoring over approximate sidecars.
141    PreferExact,
142    /// Permit approximate candidate generation, with exact rerank when configured.
143    AllowApproximate,
144}
145
146/// Explicit search execution context for deterministic replay and receipt generation.
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct SearchContext {
149    /// Timestamp used for time-sensitive scoring such as recency.
150    pub evaluation_time: DateTime<Utc>,
151    /// Receipt metadata mode.
152    pub receipt_mode: ReceiptMode,
153    /// Exactness policy for vector candidate generation.
154    pub exactness_profile: ExactnessProfile,
155    /// Optional caller-provided request/receipt correlation ID.
156    pub request_id: Option<String>,
157    /// Optional distributed trace identifier supplied by the caller.
158    #[serde(default, skip_serializing_if = "Option::is_none")]
159    pub trace_id: Option<String>,
160    /// Optional family ID tying retries/attempts for the same logical request.
161    #[serde(default, skip_serializing_if = "Option::is_none")]
162    pub attempt_family_id: Option<String>,
163    /// Optional retry/attempt identifier supplied by the caller.
164    #[serde(default, skip_serializing_if = "Option::is_none")]
165    pub attempt_id: Option<String>,
166    /// Receipt ID this search is replaying, when applicable.
167    #[serde(default, skip_serializing_if = "Option::is_none")]
168    pub replay_of: Option<String>,
169    /// Digest of raw query text when the caller provides one.
170    #[serde(default, skip_serializing_if = "Option::is_none")]
171    pub query_text_digest: Option<String>,
172    /// Digest of raw or structured query input when supplied by the caller.
173    #[serde(default, skip_serializing_if = "Option::is_none")]
174    pub query_input_digest: Option<String>,
175    /// Digest of structured filters when the caller provides one.
176    #[serde(default, skip_serializing_if = "Option::is_none")]
177    pub filter_digest: Option<String>,
178    /// Redaction state label for explain/replay surfaces.
179    #[serde(default, skip_serializing_if = "Option::is_none")]
180    pub redaction_state: Option<String>,
181    /// Optional budget identity associated with the search.
182    #[serde(default, skip_serializing_if = "Option::is_none")]
183    pub budget_id: Option<String>,
184    /// Optional caller deadline associated with the search.
185    #[serde(default, skip_serializing_if = "Option::is_none")]
186    pub deadline_at: Option<DateTime<Utc>>,
187}
188
189impl SearchContext {
190    /// Build a context using the current wall clock at the API boundary.
191    pub fn default_now() -> Self {
192        Self {
193            evaluation_time: Utc::now(),
194            receipt_mode: ReceiptMode::Disabled,
195            exactness_profile: ExactnessProfile::Default,
196            request_id: None,
197            trace_id: None,
198            attempt_family_id: None,
199            attempt_id: None,
200            replay_of: None,
201            query_text_digest: None,
202            query_input_digest: None,
203            filter_digest: None,
204            redaction_state: None,
205            budget_id: None,
206            deadline_at: None,
207        }
208    }
209
210    /// Build a replay context with an explicit evaluation timestamp.
211    pub fn at(evaluation_time: DateTime<Utc>) -> Self {
212        Self {
213            evaluation_time,
214            ..Self::default_now()
215        }
216    }
217
218    /// Whether a receipt should be produced for this context.
219    pub fn receipts_enabled(&self) -> bool {
220        self.receipt_mode != ReceiptMode::Disabled
221    }
222}
223
224impl Default for SearchContext {
225    fn default() -> Self {
226        Self::default_now()
227    }
228}
229
230/// Receipt-ready vector/search execution metadata.
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct VectorSearchReceiptV1 {
233    /// Receipt schema version.
234    #[serde(default = "default_vector_search_receipt_schema")]
235    pub schema_version: String,
236    /// Digest of the canonical stored receipt payload, when persisted.
237    #[serde(default, skip_serializing_if = "Option::is_none")]
238    pub receipt_digest: Option<String>,
239    /// Receipt or request correlation ID.
240    pub receipt_id: String,
241    /// Timestamp used for deterministic scoring.
242    pub evaluation_time: DateTime<Utc>,
243    /// Optional distributed trace identifier supplied by the caller.
244    #[serde(default, skip_serializing_if = "Option::is_none")]
245    pub trace_id: Option<String>,
246    /// Optional family ID tying retries/attempts for the same logical request.
247    #[serde(default, skip_serializing_if = "Option::is_none")]
248    pub attempt_family_id: Option<String>,
249    /// Optional retry/attempt identifier supplied by the caller.
250    #[serde(default, skip_serializing_if = "Option::is_none")]
251    pub attempt_id: Option<String>,
252    /// Receipt ID this receipt replays, when applicable.
253    #[serde(default, skip_serializing_if = "Option::is_none")]
254    pub replay_of: Option<String>,
255    /// Stable BLAKE3 digest of the query embedding bytes, when available.
256    pub query_embedding_digest: Option<String>,
257    /// Digest of raw query text when supplied by the caller.
258    #[serde(default, skip_serializing_if = "Option::is_none")]
259    pub query_text_digest: Option<String>,
260    /// Digest of raw or structured query input when supplied by the caller.
261    #[serde(default, skip_serializing_if = "Option::is_none")]
262    pub query_input_digest: Option<String>,
263    /// Digest of structured filters when supplied by the caller.
264    #[serde(default, skip_serializing_if = "Option::is_none")]
265    pub filter_digest: Option<String>,
266    /// Redaction state label for explain/replay surfaces.
267    #[serde(default, skip_serializing_if = "Option::is_none")]
268    pub redaction_state: Option<String>,
269    /// Optional budget identity associated with the search.
270    #[serde(default, skip_serializing_if = "Option::is_none")]
271    pub budget_id: Option<String>,
272    /// Optional caller deadline associated with the search.
273    #[serde(default, skip_serializing_if = "Option::is_none")]
274    pub deadline_at: Option<DateTime<Utc>>,
275    /// Human-readable search profile.
276    pub search_profile: String,
277    /// Candidate backend used for vector retrieval.
278    pub candidate_backend: String,
279    /// Codec family used for derived vector artifacts, when applicable.
280    pub codec_family: Option<String>,
281    /// Codec profile digest used for derived vector artifacts, when applicable.
282    pub codec_profile_digest: Option<String>,
283    /// Alias for derived artifact profile digest used by v11-compatible hooks.
284    #[serde(default, skip_serializing_if = "Option::is_none")]
285    pub artifact_profile_digest: Option<String>,
286    /// Number of derived artifacts considered by the vector path.
287    #[serde(default, skip_serializing_if = "Option::is_none")]
288    pub artifact_count: Option<usize>,
289    /// Number of corrupt derived artifacts encountered by the vector path.
290    #[serde(default, skip_serializing_if = "Option::is_none")]
291    pub artifact_corruption_count: Option<usize>,
292    /// Number of missing derived artifacts encountered by the vector path.
293    #[serde(default, skip_serializing_if = "Option::is_none")]
294    pub artifact_missing_count: Option<usize>,
295    /// Manifest digest for the derived vector artifacts considered by the search.
296    #[serde(default, skip_serializing_if = "Option::is_none")]
297    pub vector_artifact_manifest_digest: Option<String>,
298    /// Active generation ID for derived vector artifacts, when used.
299    #[serde(default, skip_serializing_if = "Option::is_none")]
300    pub artifact_generation_id: Option<String>,
301    /// Number of derived artifacts scanned by approximate candidate generation.
302    #[serde(default, skip_serializing_if = "Option::is_none")]
303    pub approximate_scanned_count: Option<usize>,
304    /// Number of approximate candidates returned for exact f32 reranking.
305    #[serde(default, skip_serializing_if = "Option::is_none")]
306    pub approximate_returned_count: Option<usize>,
307    /// Number of authoritative raw f32 rows loaded during exact rerank.
308    #[serde(default, skip_serializing_if = "Option::is_none")]
309    pub raw_rows_loaded_count: Option<usize>,
310    /// Filter strategy used by approximate candidate generation.
311    #[serde(default, skip_serializing_if = "Option::is_none")]
312    pub filter_strategy: Option<String>,
313    /// Number of derived vector artifacts considered by the vector path.
314    #[serde(default, skip_serializing_if = "Option::is_none")]
315    pub vector_artifact_count: Option<usize>,
316    /// Number of missing derived vector artifacts encountered by the vector path.
317    #[serde(default, skip_serializing_if = "Option::is_none")]
318    pub vector_artifact_missing_count: Option<usize>,
319    /// Number of stale derived vector artifacts encountered by the vector path.
320    #[serde(default, skip_serializing_if = "Option::is_none")]
321    pub vector_artifact_stale_count: Option<usize>,
322    /// Number of candidates exact-reranked against authoritative f32 embeddings.
323    #[serde(default, skip_serializing_if = "Option::is_none")]
324    pub exact_rerank_count: Option<usize>,
325    /// Number of approximate candidates produced by the candidate backend.
326    #[serde(default, skip_serializing_if = "Option::is_none")]
327    pub approximate_candidate_count: Option<usize>,
328    /// Explicit fallback reason, mirrored from fallback for evidence readers.
329    #[serde(default, skip_serializing_if = "Option::is_none")]
330    pub fallback_reason: Option<String>,
331    /// Whether approximate codec/index scoring contributed to candidate generation.
332    pub approximate: bool,
333    /// Number of vector candidates requested from the backend.
334    pub requested_candidates: usize,
335    /// Number of candidates returned by the backend before SQL post-filtering.
336    pub returned_candidates: usize,
337    /// Number of vector candidates remaining after SQL filters and exact rerank.
338    pub post_filter_candidates: usize,
339    /// Fallback path, if approximate retrieval degraded or was bypassed.
340    pub fallback: Option<String>,
341    /// Whether exact f32 rerank/reference scoring was used.
342    pub exact_rerank: bool,
343    /// Result IDs returned to the caller.
344    pub result_ids: Vec<String>,
345    /// Degradation notes visible to explain/audit paths.
346    pub degradations: Vec<String>,
347}
348
349/// Stable generation-level manifest for derived vector acceleration artifacts.
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct DerivedVectorArtifactGenerationV1 {
352    /// Stable schema marker.
353    pub schema_version: String,
354    /// Generation UUID.
355    pub generation_id: String,
356    /// Derived codec family.
357    pub codec_family: String,
358    /// Digest of the codec profile.
359    pub codec_profile_digest: String,
360    /// Digest over authoritative source rows used to build the generation.
361    pub source_snapshot_digest: String,
362    /// Number of authoritative source rows scanned.
363    pub source_row_count: usize,
364    /// Number of artifacts produced.
365    pub artifact_count: usize,
366    /// Authoritative source tables included in the build.
367    pub source_tables: Vec<String>,
368    /// Embedding dimension.
369    pub dim: usize,
370    /// Artifact wire encoding.
371    pub encoding: String,
372    /// Build timestamp.
373    pub created_at: DateTime<Utc>,
374    /// Optional build receipt ID.
375    pub build_receipt_id: Option<String>,
376    /// Digest of the artifact manifest for this generation.
377    pub artifact_manifest_digest: String,
378    /// Generation state.
379    pub status: String,
380    /// Structured or human-readable degradation markers.
381    pub degradations: Vec<String>,
382}
383
384/// Receipt-like summary for rebuilding derived vector acceleration artifacts.
385#[derive(Debug, Clone, Serialize, Deserialize)]
386pub struct VectorArtifactBuildReceiptV1 {
387    /// Stable schema marker.
388    pub schema_version: String,
389    /// Derived codec family.
390    pub codec_family: String,
391    /// Digest of the codec profile used for all artifacts in the build.
392    pub codec_profile_digest: String,
393    /// Number of authoritative embedding rows scanned.
394    pub source_row_count: usize,
395    /// Number of artifacts written.
396    pub artifact_count: usize,
397    /// Active generation ID produced by the rebuild.
398    #[serde(default, skip_serializing_if = "Option::is_none")]
399    pub generation_id: Option<String>,
400    /// Source snapshot digest used by the generation manifest.
401    #[serde(default, skip_serializing_if = "Option::is_none")]
402    pub source_snapshot_digest: Option<String>,
403    /// Artifact manifest digest for this generation.
404    #[serde(default, skip_serializing_if = "Option::is_none")]
405    pub artifact_manifest_digest: Option<String>,
406    /// ID of the build receipt itself (same value stored in the generation manifest).
407    #[serde(default, skip_serializing_if = "Option::is_none")]
408    pub build_receipt_id: Option<String>,
409    /// Number of rows skipped because authoritative embeddings were invalid.
410    pub skipped_row_count: usize,
411    /// Wall-clock build duration in milliseconds.
412    pub elapsed_ms: u128,
413    /// Build timestamp.
414    pub created_at: DateTime<Utc>,
415    /// Non-fatal build notes.
416    pub degradations: Vec<String>,
417}
418
419fn default_vector_search_receipt_schema() -> String {
420    "vector_search_receipt_v1".to_string()
421}
422
423/// Product-facing answers derived from a search receipt.
424#[derive(Debug, Clone, Serialize, Deserialize)]
425pub struct SearchReceiptAnswersV1 {
426    /// Receipt or request correlation ID.
427    pub receipt_id: String,
428    /// Stable ID to attach to replay/audit logs.
429    pub replay_receipt_id: String,
430    /// Timestamp used for deterministic scoring.
431    pub evaluation_time: DateTime<Utc>,
432    /// Human-readable search profile.
433    pub search_profile: String,
434    /// Candidate backend used for retrieval.
435    pub candidate_backend: String,
436    /// Codec family used for derived vector artifacts, when applicable.
437    pub codec_family: Option<String>,
438    /// Codec profile digest used for derived vector artifacts, when applicable.
439    pub codec_profile_digest: Option<String>,
440    /// Exactness label suitable for UI/API surfaces.
441    pub exactness: String,
442    /// Whether approximate codec/index scoring contributed to candidate generation.
443    pub approximate: bool,
444    /// Whether exact f32 rerank/reference scoring was used.
445    pub exact_rerank: bool,
446    /// Fallback path, if approximate retrieval degraded or was bypassed.
447    pub fallback: Option<String>,
448    /// Whether degradations or fallback occurred.
449    pub degraded: bool,
450    /// Whether the receipt carries enough deterministic context for replay with the original query.
451    pub replay_ready: bool,
452    /// Whether derived vector/index artifacts can be rebuilt from authoritative rows and profiles.
453    pub rebuild_ready: bool,
454    /// Result IDs returned to the caller.
455    pub result_ids: Vec<String>,
456    /// Number of returned results.
457    pub result_count: usize,
458    /// Degradation notes visible to explain/audit paths.
459    pub degradations: Vec<String>,
460    /// Plain-language reasons results appeared.
461    pub why_results_appeared: Vec<String>,
462}
463
464impl VectorSearchReceiptV1 {
465    /// Convert low-level receipt metadata into answers for explain/replay UX.
466    pub fn answers(&self) -> SearchReceiptAnswersV1 {
467        let exactness = match (self.approximate, self.exact_rerank) {
468            (true, true) => "approximate_candidate_generation_with_exact_rerank",
469            (true, false) => "approximate",
470            (false, true) => "exact_reference_with_rerank",
471            (false, false) => "exact_reference",
472        }
473        .to_string();
474
475        let mut why_results_appeared = Vec::new();
476        why_results_appeared.push(format!(
477            "retrieval used candidate backend '{}'",
478            self.candidate_backend
479        ));
480        if self.exact_rerank {
481            why_results_appeared.push("final vector ordering used exact f32 scoring".to_string());
482        }
483        if let Some(fallback) = &self.fallback {
484            why_results_appeared.push(format!("fallback path '{}' was used", fallback));
485        }
486        if let Some(codec_profile_digest) = &self.codec_profile_digest {
487            why_results_appeared.push(format!(
488                "derived vector artifacts used codec profile '{}'",
489                codec_profile_digest
490            ));
491        } else {
492            why_results_appeared.push("no derived codec profile was used".to_string());
493        }
494        if let Some(query_embedding_digest) = &self.query_embedding_digest {
495            why_results_appeared.push(format!(
496                "query embedding digest '{}' is recorded for replay checks",
497                query_embedding_digest
498            ));
499        }
500
501        SearchReceiptAnswersV1 {
502            receipt_id: self.receipt_id.clone(),
503            replay_receipt_id: self.receipt_id.clone(),
504            evaluation_time: self.evaluation_time,
505            search_profile: self.search_profile.clone(),
506            candidate_backend: self.candidate_backend.clone(),
507            codec_family: self.codec_family.clone(),
508            codec_profile_digest: self.codec_profile_digest.clone(),
509            exactness,
510            approximate: self.approximate,
511            exact_rerank: self.exact_rerank,
512            fallback: self.fallback.clone(),
513            degraded: self.fallback.is_some() || !self.degradations.is_empty(),
514            replay_ready: self.query_embedding_digest.is_some(),
515            rebuild_ready: self.query_embedding_digest.is_some()
516                && self.exact_rerank
517                && self.fallback.is_none()
518                && (self
519                    .vector_artifact_count
520                    .or(self.artifact_count)
521                    .is_some_and(|count| count > 0)
522                    || (self.codec_family.is_none()
523                        && self.candidate_backend.contains("brute_force_f32")
524                        && !self.result_ids.is_empty())),
525            result_ids: self.result_ids.clone(),
526            result_count: self.result_ids.len(),
527            degradations: self.degradations.clone(),
528            why_results_appeared,
529        }
530    }
531}
532
533/// Search response shape for context-aware APIs.
534#[derive(Debug, Clone, Serialize, Deserialize)]
535pub struct SearchResponse {
536    /// Search results.
537    pub results: Vec<SearchResult>,
538    /// Optional receipt metadata.
539    pub receipt: Option<VectorSearchReceiptV1>,
540}
541
542/// Caller-supplied chunk for manifest ingestion.
543///
544/// The external chunk ID is returned in the ingest mapping, but semantic-memory still
545/// owns the durable chunk primary key and generates its own `sm_chunk_id`.
546#[derive(Debug, Clone, Serialize, Deserialize)]
547pub struct ChunkManifestEntry {
548    /// Caller-owned chunk identifier.
549    pub external_chunk_id: String,
550    /// Already chunked content to embed and store.
551    pub content: String,
552    /// Optional caller-estimated token count.
553    #[serde(default, skip_serializing_if = "Option::is_none")]
554    pub token_count_estimate: Option<usize>,
555    /// Optional caller-computed content digest for verification by adapters.
556    #[serde(default, skip_serializing_if = "Option::is_none")]
557    pub content_digest: Option<String>,
558    /// Optional per-chunk metadata kept in the receipt mapping.
559    #[serde(default, skip_serializing_if = "Option::is_none")]
560    pub metadata: Option<serde_json::Value>,
561}
562
563/// Document-level options for chunk manifest ingestion.
564#[derive(Debug, Clone, Serialize, Deserialize)]
565pub struct ChunkManifestIngestOptions {
566    /// Document title.
567    pub title: String,
568    /// Namespace/notebook scope.
569    pub namespace: String,
570    /// Optional file path, URL, or caller source identifier.
571    #[serde(default, skip_serializing_if = "Option::is_none")]
572    pub source_path: Option<String>,
573    /// Optional document metadata stored with the semantic-memory document.
574    #[serde(default, skip_serializing_if = "Option::is_none")]
575    pub metadata: Option<serde_json::Value>,
576}
577
578/// Exact mapping returned for a single manifest chunk after a successful transaction.
579#[derive(Debug, Clone, Serialize, Deserialize)]
580pub struct ChunkManifestChunkMapping {
581    /// Caller-owned chunk identifier supplied in the manifest.
582    pub external_chunk_id: String,
583    /// semantic-memory document id that owns the chunk.
584    pub sm_document_id: String,
585    /// semantic-memory chunk id generated and stored in `chunks.id`.
586    pub sm_chunk_id: String,
587    /// Position in the supplied manifest.
588    pub chunk_index: usize,
589    /// Stored chunk content digest, when supplied by caller.
590    #[serde(default, skip_serializing_if = "Option::is_none")]
591    pub content_digest: Option<String>,
592    /// Optional caller metadata echoed for adapter receipt/audit use.
593    #[serde(default, skip_serializing_if = "Option::is_none")]
594    pub metadata: Option<serde_json::Value>,
595}
596
597/// Successful chunk-manifest ingest receipt.
598#[derive(Debug, Clone, Serialize, Deserialize)]
599pub struct ChunkManifestIngestResult {
600    /// semantic-memory document id generated for this manifest.
601    pub sm_document_id: String,
602    /// Namespace/notebook scope used for ingest.
603    pub namespace: String,
604    /// Receipt/request correlation id for adapters.
605    pub receipt_id: String,
606    /// Ordered external chunk to semantic-memory chunk mappings.
607    pub chunks: Vec<ChunkManifestChunkMapping>,
608}
609
610/// Explained search response shape for context-aware APIs.
611#[derive(Debug, Clone, Serialize, Deserialize)]
612pub struct ExplainedSearchResponse {
613    /// Search results with scoring breakdowns.
614    pub results: Vec<ExplainedResult>,
615    /// Optional receipt metadata.
616    pub receipt: Option<VectorSearchReceiptV1>,
617}
618
619/// Replay comparison for a durable search receipt.
620#[derive(Debug, Clone, Serialize, Deserialize)]
621pub struct SearchReplayReportV1 {
622    /// Durable receipt ID that was replayed.
623    pub receipt_id: String,
624    /// Newly generated receipt ID for the replay attempt.
625    pub replay_receipt_id: String,
626    /// Original durable receipt metadata.
627    pub original_receipt: VectorSearchReceiptV1,
628    /// Receipt produced by the replay attempt.
629    pub replay_receipt: VectorSearchReceiptV1,
630    /// Whether the caller-supplied query produced the same embedding digest.
631    pub query_embedding_digest_matches: bool,
632    /// Whether replay returned the same result IDs in the same order.
633    pub result_ids_match: bool,
634    /// Original result IDs missing from replay output.
635    pub missing_result_ids: Vec<String>,
636    /// Replay result IDs not present in the original receipt.
637    pub added_result_ids: Vec<String>,
638    /// Whether replay used the vector-only API family.
639    pub vector_only: bool,
640}
641
642/// Common filter surface for imported projection queries.
643#[derive(Debug, Clone, Serialize, Deserialize)]
644pub struct ProjectionQuery {
645    /// Full scope to enforce.
646    pub scope: ScopeKey,
647    /// Optional free-text query applied to the projection's searchable fields.
648    #[serde(default, skip_serializing_if = "Option::is_none")]
649    pub text_query: Option<String>,
650    /// Valid-time as-of filter for versioned projection rows.
651    #[serde(default, skip_serializing_if = "Option::is_none")]
652    pub valid_at: Option<String>,
653    /// Transaction-time cutoff for imported rows.
654    #[serde(default, skip_serializing_if = "Option::is_none")]
655    pub recorded_at_or_before: Option<String>,
656    /// Optional subject-entity filter for claim/relation queries.
657    #[serde(default, skip_serializing_if = "Option::is_none")]
658    pub subject_entity_id: Option<EntityId>,
659    /// Optional canonical-entity filter for alias queries.
660    #[serde(default, skip_serializing_if = "Option::is_none")]
661    pub canonical_entity_id: Option<EntityId>,
662    /// Optional claim-state filter for claim-version queries.
663    #[serde(default, skip_serializing_if = "Option::is_none")]
664    pub claim_state: Option<String>,
665    /// Optional claim filter for claim/evidence queries.
666    #[serde(default, skip_serializing_if = "Option::is_none")]
667    pub claim_id: Option<ClaimId>,
668    /// Optional claim-version filter for evidence queries.
669    #[serde(default, skip_serializing_if = "Option::is_none")]
670    pub claim_version_id: Option<ClaimVersionId>,
671    /// Final result limit.
672    pub limit: usize,
673}
674
675impl ProjectionQuery {
676    pub fn new(scope: ScopeKey) -> Self {
677        Self {
678            scope,
679            text_query: None,
680            valid_at: None,
681            recorded_at_or_before: None,
682            subject_entity_id: None,
683            canonical_entity_id: None,
684            claim_state: None,
685            claim_id: None,
686            claim_version_id: None,
687            limit: 10,
688        }
689    }
690}
691
692/// Public read shape for imported claim projection rows.
693#[derive(Debug, Clone, Serialize, Deserialize)]
694pub struct ProjectionClaimVersion {
695    pub claim_version_id: ClaimVersionId,
696    pub claim_id: ClaimId,
697    pub claim_state: String,
698    pub projection_family: String,
699    pub subject_entity_id: EntityId,
700    pub predicate: String,
701    pub object_anchor: serde_json::Value,
702    pub scope_key: ScopeKey,
703    pub valid_from: Option<String>,
704    pub valid_to: Option<String>,
705    pub recorded_at: String,
706    pub preferred_open: bool,
707    pub source_envelope_id: EnvelopeId,
708    pub source_authority: String,
709    pub trace_id: Option<String>,
710    pub freshness: String,
711    pub contradiction_status: String,
712    pub supersedes_claim_version_id: Option<ClaimVersionId>,
713    pub content: String,
714    pub confidence: f32,
715    pub metadata: Option<serde_json::Value>,
716    pub source_exported_at: Option<String>,
717    pub transformed_at: Option<String>,
718}
719
720/// Public read shape for imported relation projection rows.
721#[derive(Debug, Clone, Serialize, Deserialize)]
722pub struct ProjectionRelationVersion {
723    pub relation_version_id: RelationVersionId,
724    pub subject_entity_id: EntityId,
725    pub predicate: String,
726    pub object_anchor: serde_json::Value,
727    pub scope_key: ScopeKey,
728    pub claim_id: Option<ClaimId>,
729    pub source_episode_id: Option<EpisodeId>,
730    pub valid_from: Option<String>,
731    pub valid_to: Option<String>,
732    pub recorded_at: String,
733    pub preferred_open: bool,
734    pub supersedes_relation_version_id: Option<RelationVersionId>,
735    pub contradiction_status: String,
736    pub source_confidence: f32,
737    pub projection_family: String,
738    pub source_envelope_id: EnvelopeId,
739    pub source_authority: String,
740    pub trace_id: Option<String>,
741    pub freshness: String,
742    pub metadata: Option<serde_json::Value>,
743    pub source_exported_at: Option<String>,
744    pub transformed_at: Option<String>,
745}
746
747/// Public read shape for imported episode projection rows.
748#[derive(Debug, Clone, Serialize, Deserialize)]
749pub struct ProjectionEpisode {
750    pub episode_id: EpisodeId,
751    pub document_id: String,
752    pub cause_ids: Vec<String>,
753    pub effect_type: String,
754    pub outcome: String,
755    pub confidence: f32,
756    pub experiment_id: Option<String>,
757    pub scope_key: ScopeKey,
758    pub source_envelope_id: EnvelopeId,
759    pub source_authority: String,
760    pub trace_id: Option<String>,
761    pub recorded_at: String,
762    pub metadata: Option<serde_json::Value>,
763    pub source_exported_at: Option<String>,
764    pub transformed_at: Option<String>,
765}
766
767/// Public read shape for imported entity-alias rows.
768#[derive(Debug, Clone, Serialize, Deserialize)]
769pub struct ProjectionEntityAlias {
770    pub canonical_entity_id: EntityId,
771    pub alias_text: String,
772    pub alias_source: String,
773    pub match_evidence: Option<serde_json::Value>,
774    pub confidence: f32,
775    pub merge_decision: String,
776    pub scope_key: ScopeKey,
777    pub review_state: String,
778    pub is_human_confirmed: bool,
779    pub is_human_confirmed_final: bool,
780    pub superseded_by_entity_id: Option<EntityId>,
781    pub split_from_entity_id: Option<EntityId>,
782    pub source_envelope_id: EnvelopeId,
783    pub recorded_at: String,
784    pub source_exported_at: Option<String>,
785    pub transformed_at: Option<String>,
786}
787
788/// Public read shape for imported evidence-reference rows.
789#[derive(Debug, Clone, Serialize, Deserialize)]
790pub struct ProjectionEvidenceRef {
791    pub claim_id: ClaimId,
792    pub claim_version_id: Option<ClaimVersionId>,
793    pub fetch_handle: String,
794    pub source_authority: String,
795    pub source_envelope_id: EnvelopeId,
796    pub scope_key: ScopeKey,
797    pub recorded_at: String,
798    pub metadata: Option<serde_json::Value>,
799    pub source_exported_at: Option<String>,
800    pub transformed_at: Option<String>,
801}
802
803/// A conversation session.
804#[derive(Debug, Clone, Serialize, Deserialize)]
805pub struct Session {
806    /// UUID v4.
807    pub id: String,
808    /// Channel identifier (e.g. "repl", "telegram").
809    pub channel: String,
810    /// ISO 8601 timestamp.
811    pub created_at: String,
812    /// ISO 8601 timestamp.
813    pub updated_at: String,
814    /// Optional JSON metadata.
815    pub metadata: Option<serde_json::Value>,
816    /// Number of messages (populated on list queries).
817    pub message_count: u32,
818}
819
820/// A single message within a session.
821#[derive(Debug, Clone, Serialize, Deserialize)]
822pub struct Message {
823    /// Auto-increment ID.
824    pub id: i64,
825    /// Session this message belongs to.
826    pub session_id: String,
827    /// Role of the speaker.
828    pub role: Role,
829    /// Message text.
830    pub content: String,
831    /// Estimated token count (caller-provided).
832    pub token_count: Option<u32>,
833    /// ISO 8601 timestamp.
834    pub created_at: String,
835    /// Optional JSON metadata.
836    pub metadata: Option<serde_json::Value>,
837}
838
839/// A discrete fact in the knowledge store.
840#[derive(Debug, Clone, Serialize, Deserialize)]
841pub struct Fact {
842    /// UUID v4.
843    pub id: String,
844    /// Categorization namespace.
845    pub namespace: String,
846    /// The fact text.
847    pub content: String,
848    /// Where this fact came from.
849    pub source: Option<String>,
850    /// ISO 8601 timestamp.
851    pub created_at: String,
852    /// ISO 8601 timestamp.
853    pub updated_at: String,
854    /// Optional JSON metadata.
855    pub metadata: Option<serde_json::Value>,
856}
857
858/// A source document that has been chunked and embedded.
859#[derive(Debug, Clone, Serialize, Deserialize)]
860pub struct Document {
861    /// UUID v4.
862    pub id: String,
863    /// Document title.
864    pub title: String,
865    /// File path, URL, or identifier.
866    pub source_path: Option<String>,
867    /// Categorization namespace.
868    pub namespace: String,
869    /// ISO 8601 timestamp.
870    pub created_at: String,
871    /// Optional JSON metadata.
872    pub metadata: Option<serde_json::Value>,
873    /// Number of chunks (populated on list queries).
874    pub chunk_count: u32,
875}
876
877/// A chunk produced by the text splitter.
878#[derive(Debug, Clone, Serialize, Deserialize)]
879pub struct TextChunk {
880    /// Position in the original document (0-based).
881    pub index: usize,
882    /// The chunk text.
883    pub content: String,
884    /// Rough token estimate (chars / 4).
885    pub token_count_estimate: usize,
886}
887
888/// A single search result.
889#[derive(Debug, Clone, Serialize, Deserialize)]
890pub struct SearchResult {
891    /// The matched text content.
892    pub content: String,
893
894    /// Where this result came from.
895    pub source: SearchSource,
896
897    /// Combined RRF score. Higher = more relevant.
898    pub score: f64,
899
900    /// BM25 rank (1-based) if this result appeared in BM25 results.
901    pub bm25_rank: Option<usize>,
902
903    /// Vector rank (1-based) if this result appeared in vector results.
904    pub vector_rank: Option<usize>,
905
906    /// Cosine similarity score if computed.
907    pub cosine_similarity: Option<f64>,
908}
909
910/// Source information for a search result.
911#[derive(Debug, Clone, Serialize, Deserialize)]
912#[serde(rename_all = "snake_case")]
913pub enum SearchSource {
914    /// Result came from the facts table.
915    Fact {
916        /// Fact UUID.
917        fact_id: String,
918        /// Fact namespace.
919        namespace: String,
920    },
921    /// Result came from a document chunk.
922    Chunk {
923        /// Chunk UUID.
924        chunk_id: String,
925        /// Parent document UUID.
926        document_id: String,
927        /// Parent document title.
928        document_title: String,
929        /// Position within the document (0-based).
930        chunk_index: usize,
931    },
932    /// Result came from a conversation message.
933    Message {
934        /// Message auto-increment ID.
935        message_id: i64,
936        /// Session UUID.
937        session_id: String,
938        /// Message role (user, assistant, etc.).
939        role: String,
940    },
941    /// Result came from an episode (causal record). SearchSource::Episode variant.
942    Episode {
943        /// First-class episode identity (V9+). Falls back to `document_id + "-ep0"`
944        /// for legacy data.
945        episode_id: String,
946        /// Document ID the episode is attached to.
947        document_id: String,
948        /// Type of effect (e.g. "test_failure", "regression").
949        effect_type: String,
950        /// Current outcome.
951        outcome: String,
952    },
953    /// Result came from an imported projection row.
954    Projection {
955        /// Projection row family, such as `claim_version` or `relation_version`.
956        projection_kind: String,
957        /// Stable projection-row identity.
958        projection_id: String,
959        /// Full scope carried by the imported row.
960        scope_key: ScopeKey,
961        /// Validity start for versioned projections, if any.
962        valid_from: Option<String>,
963        /// Validity end for versioned projections, if any.
964        valid_to: Option<String>,
965        /// Authoritative importer-assigned recorded_at.
966        recorded_at: String,
967        /// Source envelope provenance.
968        source_envelope_id: String,
969        /// Source authority provenance.
970        source_authority: String,
971    },
972}
973
974impl SearchSource {
975    /// Stable result ID used in receipts and replay logs.
976    pub fn result_id(&self) -> String {
977        match self {
978            Self::Fact { fact_id, .. } => format!("fact:{fact_id}"),
979            Self::Chunk { chunk_id, .. } => format!("chunk:{chunk_id}"),
980            Self::Message { message_id, .. } => format!("msg:{message_id}"),
981            Self::Episode { episode_id, .. } => format!("episode:{episode_id}"),
982            Self::Projection { projection_id, .. } => format!("projection:{projection_id}"),
983        }
984    }
985
986    /// Source family label used by explain/receipt surfaces.
987    pub fn source_kind(&self) -> &'static str {
988        match self {
989            Self::Fact { .. } => "fact",
990            Self::Chunk { .. } => "chunk",
991            Self::Message { .. } => "message",
992            Self::Episode { .. } => "episode",
993            Self::Projection { .. } => "projection",
994        }
995    }
996
997    /// Authoritative source row key without the receipt result prefix.
998    pub fn source_id(&self) -> String {
999        match self {
1000            Self::Fact { fact_id, .. } => fact_id.clone(),
1001            Self::Chunk { chunk_id, .. } => chunk_id.clone(),
1002            Self::Message { message_id, .. } => message_id.to_string(),
1003            Self::Episode { episode_id, .. } => episode_id.clone(),
1004            Self::Projection { projection_id, .. } => projection_id.clone(),
1005        }
1006    }
1007}
1008
1009// ─── Episode Types ─────────────────────────────────────────────
1010
1011/// Metadata for a causal episode (PRIMITIVES_CONTRACT §4).
1012#[derive(Debug, Clone, Serialize, Deserialize)]
1013pub struct EpisodeMeta {
1014    /// IDs of the facts/chunks/messages that caused this episode.
1015    pub cause_ids: Vec<String>,
1016    /// Type of effect (e.g. "test_failure", "regression", "improvement").
1017    pub effect_type: String,
1018    /// Current outcome assessment.
1019    pub outcome: EpisodeOutcome,
1020    /// Confidence in the causal link (0.0 to 1.0).
1021    pub confidence: f32,
1022    /// Verification status.
1023    pub verification_status: VerificationStatus,
1024    /// Links to an EvidenceBundle.run_id (if experimentally verified).
1025    pub experiment_id: Option<String>,
1026    /// Bitemporal valid time — when this episode fact was true in the domain.
1027    pub valid_time: Option<chrono::DateTime<chrono::Utc>>,
1028    /// Content-addressed digest of the episode fact payload (for supersession chain).
1029    pub fact_digest: Option<String>,
1030}
1031
1032/// Receipt for an as-of bitemporal episode query.
1033#[derive(Debug, Clone, Serialize, Deserialize)]
1034pub struct EpisodeAsOfReceiptV1 {
1035    pub query_id: String,
1036    pub as_of_valid: chrono::DateTime<chrono::Utc>,
1037    pub as_of_recorded: chrono::DateTime<chrono::Utc>,
1038    pub episode_count: usize,
1039    pub episode_ids: Vec<String>,
1040    pub excluded_superseded: usize,
1041}
1042
1043/// Outcome of an episode's causal hypothesis.
1044#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1045#[serde(rename_all = "lowercase")]
1046pub enum EpisodeOutcome {
1047    /// Causal link confirmed by experiment.
1048    Confirmed,
1049    /// Causal link refuted by experiment.
1050    Refuted,
1051    /// Evidence is inconclusive.
1052    Inconclusive,
1053    /// Not yet tested.
1054    Pending,
1055}
1056
1057impl EpisodeOutcome {
1058    /// Convert to the string stored in SQLite.
1059    pub fn as_str(&self) -> &'static str {
1060        match self {
1061            Self::Confirmed => "confirmed",
1062            Self::Refuted => "refuted",
1063            Self::Inconclusive => "inconclusive",
1064            Self::Pending => "pending",
1065        }
1066    }
1067
1068    /// Parse from the string stored in SQLite.
1069    pub fn from_str_value(s: &str) -> Option<Self> {
1070        match s {
1071            "confirmed" => Some(Self::Confirmed),
1072            "refuted" => Some(Self::Refuted),
1073            "inconclusive" => Some(Self::Inconclusive),
1074            "pending" => Some(Self::Pending),
1075            _ => None,
1076        }
1077    }
1078}
1079
1080impl std::fmt::Display for EpisodeOutcome {
1081    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1082        f.write_str(self.as_str())
1083    }
1084}
1085
1086/// Verification status for an episode.
1087#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1088#[serde(tag = "status", rename_all = "lowercase")]
1089pub enum VerificationStatus {
1090    /// Not yet verified.
1091    Unverified,
1092    /// Successfully verified.
1093    Verified {
1094        /// Method used for verification.
1095        method: String,
1096        /// When verification occurred (ISO 8601).
1097        at: String,
1098    },
1099    /// Verification attempt failed.
1100    Failed {
1101        /// Reason for failure.
1102        reason: String,
1103        /// When verification was attempted (ISO 8601).
1104        at: String,
1105    },
1106}
1107
1108// ─── Score Breakdown ───────────────────────────────────────────
1109
1110/// Detailed score breakdown for explainable search results.
1111#[derive(Debug, Clone, Serialize, Deserialize)]
1112pub struct ScoreBreakdown {
1113    /// Final fused RRF score.
1114    pub rrf_score: f64,
1115    /// Raw BM25 score reported by SQLite FTS5 (lower is better).
1116    pub bm25_score: Option<f64>,
1117    /// Raw vector similarity used for the final vector ordering.
1118    pub vector_score: Option<f64>,
1119    /// Recency contribution added during fusion.
1120    pub recency_score: Option<f64>,
1121    /// BM25 rank (1-based).
1122    pub bm25_rank: Option<usize>,
1123    /// Vector rank (1-based).
1124    pub vector_rank: Option<usize>,
1125    /// Rank from the underlying vector retrieval source before any exact rerank.
1126    pub vector_source_rank: Option<usize>,
1127    /// Similarity score from the underlying vector retrieval source before rerank.
1128    pub vector_source_score: Option<f64>,
1129    /// BM25 RRF contribution to the final score.
1130    pub bm25_contribution: Option<f64>,
1131    /// Vector RRF contribution to the final score.
1132    pub vector_contribution: Option<f64>,
1133    /// Whether the vector ordering was reranked with exact f32 cosine similarity.
1134    pub vector_reranked_from_f32: bool,
1135    /// Configured BM25 fusion weight.
1136    pub bm25_weight: f64,
1137    /// Configured vector fusion weight.
1138    pub vector_weight: f64,
1139    /// Configured recency weight when recency is enabled.
1140    pub recency_weight: Option<f64>,
1141    /// Configured RRF decay constant.
1142    pub rrf_k: f64,
1143}
1144
1145/// Search result with full score explanation.
1146#[derive(Debug, Clone, Serialize, Deserialize)]
1147pub struct ExplainedResult {
1148    /// The search result.
1149    pub result: SearchResult,
1150    /// Score breakdown.
1151    pub breakdown: ScoreBreakdown,
1152}
1153
1154/// Product-facing answer for one explained result.
1155#[derive(Debug, Clone, Serialize, Deserialize)]
1156pub struct ExplainedResultAnswerV1 {
1157    /// Stable result ID used in receipts and replay logs.
1158    pub result_id: String,
1159    /// Source family label.
1160    pub source_kind: String,
1161    /// Authoritative source row key without the receipt result prefix.
1162    pub source_id: String,
1163    /// Plain-language reasons this result appeared.
1164    pub why_this_result: Vec<String>,
1165    /// Whether the result matched the text/BM25 lane.
1166    pub text_match: bool,
1167    /// Whether the result matched the vector lane.
1168    pub vector_match: bool,
1169    /// Whether recency contributed to the score.
1170    pub recency_applied: bool,
1171    /// Whether exact f32 rerank/reference scoring was used for the vector lane.
1172    pub exact_vector_rerank: bool,
1173    /// Final fused score.
1174    pub final_score: f64,
1175}
1176
1177impl ExplainedResult {
1178    /// Convert a detailed score breakdown into a practical "why this result" answer.
1179    pub fn answer(&self) -> ExplainedResultAnswerV1 {
1180        let text_match = self.breakdown.bm25_rank.is_some();
1181        let vector_match = self.breakdown.vector_rank.is_some();
1182        let recency_applied = self.breakdown.recency_score.is_some();
1183        let mut why_this_result = Vec::new();
1184
1185        if let Some(rank) = self.breakdown.bm25_rank {
1186            why_this_result.push(format!("text match rank {rank} contributed to fusion"));
1187        }
1188        if let Some(rank) = self.breakdown.vector_rank {
1189            why_this_result.push(format!("vector match rank {rank} contributed to fusion"));
1190        }
1191        if recency_applied {
1192            why_this_result.push("recency contributed to the fused score".to_string());
1193        }
1194        if self.breakdown.vector_reranked_from_f32 {
1195            why_this_result.push("vector score was checked with exact f32 rerank".to_string());
1196        }
1197        if why_this_result.is_empty() {
1198            why_this_result.push("result survived filtering and deterministic ranking".to_string());
1199        }
1200
1201        ExplainedResultAnswerV1 {
1202            result_id: self.result.source.result_id(),
1203            source_kind: self.result.source.source_kind().to_string(),
1204            source_id: self.result.source.source_id(),
1205            why_this_result,
1206            text_match,
1207            vector_match,
1208            recency_applied,
1209            exact_vector_rerank: self.breakdown.vector_reranked_from_f32,
1210            final_score: self.result.score,
1211        }
1212    }
1213}
1214
1215// ─── Graph Types (PRIMITIVES_CONTRACT §8) ──────────────────────
1216
1217/// Trait for querying the memory store as a graph.
1218pub trait GraphView: Send + Sync {
1219    /// Find neighboring nodes up to `max_depth` hops away.
1220    fn neighbors(
1221        &self,
1222        node_id: &str,
1223        direction: GraphDirection,
1224        max_depth: usize,
1225    ) -> Result<Vec<GraphEdge>, MemoryError>;
1226
1227    /// Find a path between two nodes (BFS, max depth).
1228    fn path(
1229        &self,
1230        from: &str,
1231        to: &str,
1232        max_depth: usize,
1233    ) -> Result<Option<Vec<String>>, MemoryError>;
1234}
1235
1236/// Direction for graph traversal.
1237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1238pub enum GraphDirection {
1239    /// Follow outgoing edges.
1240    Outgoing,
1241    /// Follow incoming edges.
1242    Incoming,
1243    /// Follow edges in both directions.
1244    Both,
1245}
1246
1247/// An edge in the memory graph.
1248#[derive(Debug, Clone, Serialize, Deserialize)]
1249pub struct GraphEdge {
1250    /// Source node ID.
1251    pub source: String,
1252    /// Target node ID.
1253    pub target: String,
1254    /// Type of relationship.
1255    pub edge_type: GraphEdgeType,
1256    /// Edge weight (interpretation depends on edge_type).
1257    pub weight: f64,
1258    /// Optional metadata.
1259    pub metadata: Option<serde_json::Value>,
1260}
1261
1262/// Type of relationship between graph nodes.
1263#[derive(Debug, Clone, Serialize, Deserialize)]
1264#[serde(rename_all = "snake_case")]
1265pub enum GraphEdgeType {
1266    /// Semantic similarity. GraphEdgeType::Semantic variant.
1267    Semantic {
1268        /// Cosine similarity between embeddings.
1269        cosine_similarity: f32,
1270    },
1271    /// Temporal proximity. GraphEdgeType::Temporal variant.
1272    Temporal {
1273        /// Time delta in seconds.
1274        delta_secs: u64,
1275    },
1276    /// Causal relationship. GraphEdgeType::Causal variant.
1277    Causal {
1278        /// Confidence in the causal link.
1279        confidence: f32,
1280        /// EvidenceBundle run_ids supporting this link.
1281        evidence_ids: Vec<String>,
1282    },
1283    /// Entity co-occurrence. GraphEdgeType::Entity variant.
1284    Entity {
1285        /// Relationship type (e.g. "mentions", "modifies").
1286        relation: String,
1287    },
1288}
1289
1290/// Embedding displacement between two text embeddings.
1291#[derive(Debug, Clone, Serialize, Deserialize)]
1292pub struct EmbeddingDisplacement {
1293    /// Cosine similarity between the two embeddings.
1294    pub cosine_similarity: f32,
1295    /// Euclidean distance between the two embeddings.
1296    pub euclidean_distance: f32,
1297    /// Magnitude of the first embedding.
1298    pub magnitude_a: f32,
1299    /// Magnitude of the second embedding.
1300    pub magnitude_b: f32,
1301}
1302
1303/// Database statistics.
1304#[derive(Debug, Clone, Serialize, Deserialize)]
1305pub struct MemoryStats {
1306    /// Total number of facts.
1307    pub total_facts: u64,
1308    /// Total number of documents.
1309    pub total_documents: u64,
1310    /// Total number of chunks across all documents.
1311    pub total_chunks: u64,
1312    /// Total number of conversation sessions.
1313    pub total_sessions: u64,
1314    /// Total number of messages across all sessions.
1315    pub total_messages: u64,
1316    /// Database file size in bytes.
1317    pub database_size_bytes: u64,
1318    /// Currently configured embedding model.
1319    pub embedding_model: Option<String>,
1320    /// Currently configured embedding dimensions.
1321    pub embedding_dimensions: Option<usize>,
1322}
1323
1324/// Per-surface deletion counts for namespace removal.
1325#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1326pub struct NamespaceDeleteReport {
1327    /// Facts deleted from the namespace.
1328    pub facts: usize,
1329    /// Documents deleted from the namespace.
1330    pub documents: usize,
1331    /// Document chunks deleted from the namespace.
1332    pub chunks: usize,
1333    /// Messages deleted through namespaced sessions.
1334    pub messages: usize,
1335    /// Sessions deleted for the namespace.
1336    pub sessions: usize,
1337    /// Episodes deleted with namespaced documents.
1338    pub episodes: usize,
1339    /// Projection/import rows deleted or invalidated.
1340    pub projection_rows: usize,
1341    /// HNSW pending operations queued by the deletion.
1342    pub hnsw_ops: usize,
1343}