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    /// Structured receipt for derived candidate backends such as proveKV pool.
332    #[serde(default, skip_serializing_if = "Option::is_none")]
333    pub derived_candidate: Option<DerivedCandidateReceiptV1>,
334    /// Whether approximate codec/index scoring contributed to candidate generation.
335    pub approximate: bool,
336    /// Number of vector candidates requested from the backend.
337    pub requested_candidates: usize,
338    /// Number of candidates returned by the backend before SQL post-filtering.
339    pub returned_candidates: usize,
340    /// Number of vector candidates remaining after SQL filters and exact rerank.
341    pub post_filter_candidates: usize,
342    /// Fallback path, if approximate retrieval degraded or was bypassed.
343    pub fallback: Option<String>,
344    /// Whether exact f32 rerank/reference scoring was used.
345    pub exact_rerank: bool,
346    /// Result IDs returned to the caller.
347    pub result_ids: Vec<String>,
348    /// Degradation notes visible to explain/audit paths.
349    pub degradations: Vec<String>,
350}
351
352/// Stable generation-level manifest for derived vector acceleration artifacts.
353#[derive(Debug, Clone, Serialize, Deserialize)]
354pub struct DerivedVectorArtifactGenerationV1 {
355    /// Stable schema marker.
356    pub schema_version: String,
357    /// Generation UUID.
358    pub generation_id: String,
359    /// Derived codec family.
360    pub codec_family: String,
361    /// Digest of the codec profile.
362    pub codec_profile_digest: String,
363    /// Digest over authoritative source rows used to build the generation.
364    pub source_snapshot_digest: String,
365    /// Number of authoritative source rows scanned.
366    pub source_row_count: usize,
367    /// Number of artifacts produced.
368    pub artifact_count: usize,
369    /// Authoritative source tables included in the build.
370    pub source_tables: Vec<String>,
371    /// Embedding dimension.
372    pub dim: usize,
373    /// Artifact wire encoding.
374    pub encoding: String,
375    /// Build timestamp.
376    pub created_at: DateTime<Utc>,
377    /// Optional build receipt ID.
378    pub build_receipt_id: Option<String>,
379    /// Digest of the artifact manifest for this generation.
380    pub artifact_manifest_digest: String,
381    /// Generation state.
382    pub status: String,
383    /// Structured or human-readable degradation markers.
384    pub degradations: Vec<String>,
385}
386
387/// Stable generation-level manifest for derived proveKV/poly-kv pool candidate artifacts.
388#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
389pub struct ProveKvPoolGenerationV1 {
390    pub schema_version: String,
391    pub generation_id: String,
392    pub embedding_snapshot_digest: String,
393    pub source_digest: String,
394    pub pool_manifest_digest: String,
395    pub codec_family: String,
396    pub codec_profile: String,
397    pub vector_dim: usize,
398    pub item_count: usize,
399    pub payload_bytes: u64,
400    pub created_at: DateTime<Utc>,
401}
402
403/// Durable item-to-pool-index mapping for a proveKV/poly-kv pool generation.
404#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
405pub struct ProveKvPoolItemMapEntryV1 {
406    pub generation_id: String,
407    pub item_id: String,
408    pub source_type: String,
409    pub pool_index: usize,
410    pub embedding_digest: String,
411}
412
413/// Lifecycle status for a proveKV/poly-kv pool generation.
414#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
415#[serde(rename_all = "snake_case")]
416pub enum ProveKvPoolGenerationStatus {
417    Disabled,
418    Missing,
419    Building,
420    Ready,
421    Stale,
422    Failed,
423}
424
425/// Current proveKV/poly-kv pool artifact status.
426#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
427pub struct ProveKvPoolArtifactStatusV1 {
428    pub status: ProveKvPoolGenerationStatus,
429    pub generation_id: Option<String>,
430    pub embedding_snapshot_digest: Option<String>,
431    pub pool_manifest_digest: Option<String>,
432    pub item_count: usize,
433    pub payload_bytes: u64,
434    pub reason: Option<String>,
435}
436
437/// Receipt-like summary for rebuilding proveKV/poly-kv pool artifacts.
438#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
439pub struct ProveKvPoolArtifactBuildReceiptV1 {
440    pub schema_version: String,
441    pub generation_id: String,
442    pub embedding_snapshot_digest: String,
443    pub source_digest: String,
444    pub pool_manifest_digest: String,
445    pub codec_family: String,
446    pub codec_profile: String,
447    pub vector_dim: usize,
448    pub item_count: usize,
449    pub payload_bytes: u64,
450    pub exact_rerank_required: bool,
451    pub created_at: DateTime<Utc>,
452}
453
454/// Structured search receipt for derived candidate generation followed by exact f32 rerank.
455#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
456pub struct DerivedCandidateReceiptV1 {
457    pub candidate_backend: String,
458    pub codec_family: Option<String>,
459    pub generation_id: Option<String>,
460    pub embedding_snapshot_digest: Option<String>,
461    pub pool_manifest_digest: Option<String>,
462    pub exact_rerank: bool,
463    pub approximate: bool,
464    pub fallback: Option<String>,
465    pub raw_candidate_count: usize,
466    pub post_filter_count: usize,
467    pub final_result_count: usize,
468}
469
470/// Receipt-like summary for rebuilding derived vector acceleration artifacts.
471#[derive(Debug, Clone, Serialize, Deserialize)]
472pub struct VectorArtifactBuildReceiptV1 {
473    /// Stable schema marker.
474    pub schema_version: String,
475    /// Derived codec family.
476    pub codec_family: String,
477    /// Digest of the codec profile used for all artifacts in the build.
478    pub codec_profile_digest: String,
479    /// Number of authoritative embedding rows scanned.
480    pub source_row_count: usize,
481    /// Number of artifacts written.
482    pub artifact_count: usize,
483    /// Active generation ID produced by the rebuild.
484    #[serde(default, skip_serializing_if = "Option::is_none")]
485    pub generation_id: Option<String>,
486    /// Source snapshot digest used by the generation manifest.
487    #[serde(default, skip_serializing_if = "Option::is_none")]
488    pub source_snapshot_digest: Option<String>,
489    /// Artifact manifest digest for this generation.
490    #[serde(default, skip_serializing_if = "Option::is_none")]
491    pub artifact_manifest_digest: Option<String>,
492    /// ID of the build receipt itself (same value stored in the generation manifest).
493    #[serde(default, skip_serializing_if = "Option::is_none")]
494    pub build_receipt_id: Option<String>,
495    /// Number of rows skipped because authoritative embeddings were invalid.
496    pub skipped_row_count: usize,
497    /// Wall-clock build duration in milliseconds.
498    pub elapsed_ms: u128,
499    /// Build timestamp.
500    pub created_at: DateTime<Utc>,
501    /// Non-fatal build notes.
502    pub degradations: Vec<String>,
503}
504
505fn default_vector_search_receipt_schema() -> String {
506    "vector_search_receipt_v1".to_string()
507}
508
509/// Product-facing answers derived from a search receipt.
510#[derive(Debug, Clone, Serialize, Deserialize)]
511pub struct SearchReceiptAnswersV1 {
512    /// Receipt or request correlation ID.
513    pub receipt_id: String,
514    /// Stable ID to attach to replay/audit logs.
515    pub replay_receipt_id: String,
516    /// Timestamp used for deterministic scoring.
517    pub evaluation_time: DateTime<Utc>,
518    /// Human-readable search profile.
519    pub search_profile: String,
520    /// Candidate backend used for retrieval.
521    pub candidate_backend: String,
522    /// Codec family used for derived vector artifacts, when applicable.
523    pub codec_family: Option<String>,
524    /// Codec profile digest used for derived vector artifacts, when applicable.
525    pub codec_profile_digest: Option<String>,
526    /// Exactness label suitable for UI/API surfaces.
527    pub exactness: String,
528    /// Whether approximate codec/index scoring contributed to candidate generation.
529    pub approximate: bool,
530    /// Whether exact f32 rerank/reference scoring was used.
531    pub exact_rerank: bool,
532    /// Fallback path, if approximate retrieval degraded or was bypassed.
533    pub fallback: Option<String>,
534    /// Whether degradations or fallback occurred.
535    pub degraded: bool,
536    /// Whether the receipt carries enough deterministic context for replay with the original query.
537    pub replay_ready: bool,
538    /// Whether derived vector/index artifacts can be rebuilt from authoritative rows and profiles.
539    pub rebuild_ready: bool,
540    /// Result IDs returned to the caller.
541    pub result_ids: Vec<String>,
542    /// Number of returned results.
543    pub result_count: usize,
544    /// Degradation notes visible to explain/audit paths.
545    pub degradations: Vec<String>,
546    /// Plain-language reasons results appeared.
547    pub why_results_appeared: Vec<String>,
548}
549
550impl VectorSearchReceiptV1 {
551    /// Convert low-level receipt metadata into answers for explain/replay UX.
552    pub fn answers(&self) -> SearchReceiptAnswersV1 {
553        let exactness = match (self.approximate, self.exact_rerank) {
554            (true, true) => "approximate_candidate_generation_with_exact_rerank",
555            (true, false) => "approximate",
556            (false, true) => "exact_reference_with_rerank",
557            (false, false) => "exact_reference",
558        }
559        .to_string();
560
561        let mut why_results_appeared = Vec::new();
562        why_results_appeared.push(format!(
563            "retrieval used candidate backend '{}'",
564            self.candidate_backend
565        ));
566        if self.exact_rerank {
567            why_results_appeared.push("final vector ordering used exact f32 scoring".to_string());
568        }
569        if let Some(fallback) = &self.fallback {
570            why_results_appeared.push(format!("fallback path '{}' was used", fallback));
571        }
572        if let Some(codec_profile_digest) = &self.codec_profile_digest {
573            why_results_appeared.push(format!(
574                "derived vector artifacts used codec profile '{}'",
575                codec_profile_digest
576            ));
577        } else {
578            why_results_appeared.push("no derived codec profile was used".to_string());
579        }
580        if let Some(query_embedding_digest) = &self.query_embedding_digest {
581            why_results_appeared.push(format!(
582                "query embedding digest '{}' is recorded for replay checks",
583                query_embedding_digest
584            ));
585        }
586
587        SearchReceiptAnswersV1 {
588            receipt_id: self.receipt_id.clone(),
589            replay_receipt_id: self.receipt_id.clone(),
590            evaluation_time: self.evaluation_time,
591            search_profile: self.search_profile.clone(),
592            candidate_backend: self.candidate_backend.clone(),
593            codec_family: self.codec_family.clone(),
594            codec_profile_digest: self.codec_profile_digest.clone(),
595            exactness,
596            approximate: self.approximate,
597            exact_rerank: self.exact_rerank,
598            fallback: self.fallback.clone(),
599            degraded: self.fallback.is_some() || !self.degradations.is_empty(),
600            replay_ready: self.query_embedding_digest.is_some(),
601            rebuild_ready: self.query_embedding_digest.is_some()
602                && self.exact_rerank
603                && self.fallback.is_none()
604                && (self
605                    .vector_artifact_count
606                    .or(self.artifact_count)
607                    .is_some_and(|count| count > 0)
608                    || (self.codec_family.is_none()
609                        && self.candidate_backend.contains("brute_force_f32")
610                        && !self.result_ids.is_empty())),
611            result_ids: self.result_ids.clone(),
612            result_count: self.result_ids.len(),
613            degradations: self.degradations.clone(),
614            why_results_appeared,
615        }
616    }
617}
618
619/// Search response shape for context-aware APIs.
620#[derive(Debug, Clone, Serialize, Deserialize)]
621pub struct SearchResponse {
622    /// Search results.
623    pub results: Vec<SearchResult>,
624    /// Optional receipt metadata.
625    pub receipt: Option<VectorSearchReceiptV1>,
626}
627
628/// Caller-supplied chunk for manifest ingestion.
629///
630/// The external chunk ID is returned in the ingest mapping, but semantic-memory still
631/// owns the durable chunk primary key and generates its own `sm_chunk_id`.
632#[derive(Debug, Clone, Serialize, Deserialize)]
633pub struct ChunkManifestEntry {
634    /// Caller-owned chunk identifier.
635    pub external_chunk_id: String,
636    /// Already chunked content to embed and store.
637    pub content: String,
638    /// Optional caller-estimated token count.
639    #[serde(default, skip_serializing_if = "Option::is_none")]
640    pub token_count_estimate: Option<usize>,
641    /// Optional caller-computed content digest for verification by adapters.
642    #[serde(default, skip_serializing_if = "Option::is_none")]
643    pub content_digest: Option<String>,
644    /// Optional per-chunk metadata kept in the receipt mapping.
645    #[serde(default, skip_serializing_if = "Option::is_none")]
646    pub metadata: Option<serde_json::Value>,
647}
648
649/// Document-level options for chunk manifest ingestion.
650#[derive(Debug, Clone, Serialize, Deserialize)]
651pub struct ChunkManifestIngestOptions {
652    /// Document title.
653    pub title: String,
654    /// Namespace/notebook scope.
655    pub namespace: String,
656    /// Optional file path, URL, or caller source identifier.
657    #[serde(default, skip_serializing_if = "Option::is_none")]
658    pub source_path: Option<String>,
659    /// Optional document metadata stored with the semantic-memory document.
660    #[serde(default, skip_serializing_if = "Option::is_none")]
661    pub metadata: Option<serde_json::Value>,
662}
663
664/// Exact mapping returned for a single manifest chunk after a successful transaction.
665#[derive(Debug, Clone, Serialize, Deserialize)]
666pub struct ChunkManifestChunkMapping {
667    /// Caller-owned chunk identifier supplied in the manifest.
668    pub external_chunk_id: String,
669    /// semantic-memory document id that owns the chunk.
670    pub sm_document_id: String,
671    /// semantic-memory chunk id generated and stored in `chunks.id`.
672    pub sm_chunk_id: String,
673    /// Position in the supplied manifest.
674    pub chunk_index: usize,
675    /// Stored chunk content digest, when supplied by caller.
676    #[serde(default, skip_serializing_if = "Option::is_none")]
677    pub content_digest: Option<String>,
678    /// Optional caller metadata echoed for adapter receipt/audit use.
679    #[serde(default, skip_serializing_if = "Option::is_none")]
680    pub metadata: Option<serde_json::Value>,
681}
682
683/// Successful chunk-manifest ingest receipt.
684#[derive(Debug, Clone, Serialize, Deserialize)]
685pub struct ChunkManifestIngestResult {
686    /// semantic-memory document id generated for this manifest.
687    pub sm_document_id: String,
688    /// Namespace/notebook scope used for ingest.
689    pub namespace: String,
690    /// Receipt/request correlation id for adapters.
691    pub receipt_id: String,
692    /// Ordered external chunk to semantic-memory chunk mappings.
693    pub chunks: Vec<ChunkManifestChunkMapping>,
694}
695
696/// Explained search response shape for context-aware APIs.
697#[derive(Debug, Clone, Serialize, Deserialize)]
698pub struct ExplainedSearchResponse {
699    /// Search results with scoring breakdowns.
700    pub results: Vec<ExplainedResult>,
701    /// Optional receipt metadata.
702    pub receipt: Option<VectorSearchReceiptV1>,
703}
704
705/// Replay comparison for a durable search receipt.
706#[derive(Debug, Clone, Serialize, Deserialize)]
707pub struct SearchReplayReportV1 {
708    /// Durable receipt ID that was replayed.
709    pub receipt_id: String,
710    /// Newly generated receipt ID for the replay attempt.
711    pub replay_receipt_id: String,
712    /// Original durable receipt metadata.
713    pub original_receipt: VectorSearchReceiptV1,
714    /// Receipt produced by the replay attempt.
715    pub replay_receipt: VectorSearchReceiptV1,
716    /// Whether the caller-supplied query produced the same embedding digest.
717    pub query_embedding_digest_matches: bool,
718    /// Whether replay returned the same result IDs in the same order.
719    pub result_ids_match: bool,
720    /// Original result IDs missing from replay output.
721    pub missing_result_ids: Vec<String>,
722    /// Replay result IDs not present in the original receipt.
723    pub added_result_ids: Vec<String>,
724    /// Whether replay used the vector-only API family.
725    pub vector_only: bool,
726}
727
728/// Common filter surface for imported projection queries.
729#[derive(Debug, Clone, Serialize, Deserialize)]
730pub struct ProjectionQuery {
731    /// Full scope to enforce.
732    pub scope: ScopeKey,
733    /// Optional free-text query applied to the projection's searchable fields.
734    #[serde(default, skip_serializing_if = "Option::is_none")]
735    pub text_query: Option<String>,
736    /// Valid-time as-of filter for versioned projection rows.
737    #[serde(default, skip_serializing_if = "Option::is_none")]
738    pub valid_at: Option<String>,
739    /// Transaction-time cutoff for imported rows.
740    #[serde(default, skip_serializing_if = "Option::is_none")]
741    pub recorded_at_or_before: Option<String>,
742    /// Optional subject-entity filter for claim/relation queries.
743    #[serde(default, skip_serializing_if = "Option::is_none")]
744    pub subject_entity_id: Option<EntityId>,
745    /// Optional canonical-entity filter for alias queries.
746    #[serde(default, skip_serializing_if = "Option::is_none")]
747    pub canonical_entity_id: Option<EntityId>,
748    /// Optional claim-state filter for claim-version queries.
749    #[serde(default, skip_serializing_if = "Option::is_none")]
750    pub claim_state: Option<String>,
751    /// Optional claim filter for claim/evidence queries.
752    #[serde(default, skip_serializing_if = "Option::is_none")]
753    pub claim_id: Option<ClaimId>,
754    /// Optional claim-version filter for evidence queries.
755    #[serde(default, skip_serializing_if = "Option::is_none")]
756    pub claim_version_id: Option<ClaimVersionId>,
757    /// Final result limit.
758    pub limit: usize,
759}
760
761impl ProjectionQuery {
762    pub fn new(scope: ScopeKey) -> Self {
763        Self {
764            scope,
765            text_query: None,
766            valid_at: None,
767            recorded_at_or_before: None,
768            subject_entity_id: None,
769            canonical_entity_id: None,
770            claim_state: None,
771            claim_id: None,
772            claim_version_id: None,
773            limit: 10,
774        }
775    }
776}
777
778/// Public read shape for imported claim projection rows.
779#[derive(Debug, Clone, Serialize, Deserialize)]
780pub struct ProjectionClaimVersion {
781    pub claim_version_id: ClaimVersionId,
782    pub claim_id: ClaimId,
783    pub claim_state: String,
784    pub projection_family: String,
785    pub subject_entity_id: EntityId,
786    pub predicate: String,
787    pub object_anchor: serde_json::Value,
788    pub scope_key: ScopeKey,
789    pub valid_from: Option<String>,
790    pub valid_to: Option<String>,
791    pub recorded_at: String,
792    pub preferred_open: bool,
793    pub source_envelope_id: EnvelopeId,
794    pub source_authority: String,
795    pub trace_id: Option<String>,
796    pub freshness: String,
797    pub contradiction_status: String,
798    pub supersedes_claim_version_id: Option<ClaimVersionId>,
799    pub content: String,
800    pub confidence: f32,
801    pub metadata: Option<serde_json::Value>,
802    pub source_exported_at: Option<String>,
803    pub transformed_at: Option<String>,
804}
805
806/// Public read shape for imported relation projection rows.
807#[derive(Debug, Clone, Serialize, Deserialize)]
808pub struct ProjectionRelationVersion {
809    pub relation_version_id: RelationVersionId,
810    pub subject_entity_id: EntityId,
811    pub predicate: String,
812    pub object_anchor: serde_json::Value,
813    pub scope_key: ScopeKey,
814    pub claim_id: Option<ClaimId>,
815    pub source_episode_id: Option<EpisodeId>,
816    pub valid_from: Option<String>,
817    pub valid_to: Option<String>,
818    pub recorded_at: String,
819    pub preferred_open: bool,
820    pub supersedes_relation_version_id: Option<RelationVersionId>,
821    pub contradiction_status: String,
822    pub source_confidence: f32,
823    pub projection_family: String,
824    pub source_envelope_id: EnvelopeId,
825    pub source_authority: String,
826    pub trace_id: Option<String>,
827    pub freshness: String,
828    pub metadata: Option<serde_json::Value>,
829    pub source_exported_at: Option<String>,
830    pub transformed_at: Option<String>,
831}
832
833/// Public read shape for imported episode projection rows.
834#[derive(Debug, Clone, Serialize, Deserialize)]
835pub struct ProjectionEpisode {
836    pub episode_id: EpisodeId,
837    pub document_id: String,
838    pub cause_ids: Vec<String>,
839    pub effect_type: String,
840    pub outcome: String,
841    pub confidence: f32,
842    pub experiment_id: Option<String>,
843    pub scope_key: ScopeKey,
844    pub source_envelope_id: EnvelopeId,
845    pub source_authority: String,
846    pub trace_id: Option<String>,
847    pub recorded_at: String,
848    pub metadata: Option<serde_json::Value>,
849    pub source_exported_at: Option<String>,
850    pub transformed_at: Option<String>,
851}
852
853/// Public read shape for imported entity-alias rows.
854#[derive(Debug, Clone, Serialize, Deserialize)]
855pub struct ProjectionEntityAlias {
856    pub canonical_entity_id: EntityId,
857    pub alias_text: String,
858    pub alias_source: String,
859    pub match_evidence: Option<serde_json::Value>,
860    pub confidence: f32,
861    pub merge_decision: String,
862    pub scope_key: ScopeKey,
863    pub review_state: String,
864    pub is_human_confirmed: bool,
865    pub is_human_confirmed_final: bool,
866    pub superseded_by_entity_id: Option<EntityId>,
867    pub split_from_entity_id: Option<EntityId>,
868    pub source_envelope_id: EnvelopeId,
869    pub recorded_at: String,
870    pub source_exported_at: Option<String>,
871    pub transformed_at: Option<String>,
872}
873
874/// Public read shape for imported evidence-reference rows.
875#[derive(Debug, Clone, Serialize, Deserialize)]
876pub struct ProjectionEvidenceRef {
877    pub claim_id: ClaimId,
878    pub claim_version_id: Option<ClaimVersionId>,
879    pub fetch_handle: String,
880    pub source_authority: String,
881    pub source_envelope_id: EnvelopeId,
882    pub scope_key: ScopeKey,
883    pub recorded_at: String,
884    pub metadata: Option<serde_json::Value>,
885    pub source_exported_at: Option<String>,
886    pub transformed_at: Option<String>,
887}
888
889/// A conversation session.
890#[derive(Debug, Clone, Serialize, Deserialize)]
891pub struct Session {
892    /// UUID v4.
893    pub id: String,
894    /// Channel identifier (e.g. "repl", "telegram").
895    pub channel: String,
896    /// ISO 8601 timestamp.
897    pub created_at: String,
898    /// ISO 8601 timestamp.
899    pub updated_at: String,
900    /// Optional JSON metadata.
901    pub metadata: Option<serde_json::Value>,
902    /// Number of messages (populated on list queries).
903    pub message_count: u32,
904}
905
906/// A single message within a session.
907#[derive(Debug, Clone, Serialize, Deserialize)]
908pub struct Message {
909    /// Auto-increment ID.
910    pub id: i64,
911    /// Session this message belongs to.
912    pub session_id: String,
913    /// Role of the speaker.
914    pub role: Role,
915    /// Message text.
916    pub content: String,
917    /// Estimated token count (caller-provided).
918    pub token_count: Option<u32>,
919    /// ISO 8601 timestamp.
920    pub created_at: String,
921    /// Optional JSON metadata.
922    pub metadata: Option<serde_json::Value>,
923}
924
925/// A discrete fact in the knowledge store.
926#[derive(Debug, Clone, Serialize, Deserialize)]
927pub struct Fact {
928    /// UUID v4.
929    pub id: String,
930    /// Categorization namespace.
931    pub namespace: String,
932    /// The fact text.
933    pub content: String,
934    /// Where this fact came from.
935    pub source: Option<String>,
936    /// ISO 8601 timestamp.
937    pub created_at: String,
938    /// ISO 8601 timestamp.
939    pub updated_at: String,
940    /// Optional JSON metadata.
941    pub metadata: Option<serde_json::Value>,
942}
943
944/// A source document that has been chunked and embedded.
945#[derive(Debug, Clone, Serialize, Deserialize)]
946pub struct Document {
947    /// UUID v4.
948    pub id: String,
949    /// Document title.
950    pub title: String,
951    /// File path, URL, or identifier.
952    pub source_path: Option<String>,
953    /// Categorization namespace.
954    pub namespace: String,
955    /// ISO 8601 timestamp.
956    pub created_at: String,
957    /// Optional JSON metadata.
958    pub metadata: Option<serde_json::Value>,
959    /// Number of chunks (populated on list queries).
960    pub chunk_count: u32,
961}
962
963/// A chunk produced by the text splitter.
964#[derive(Debug, Clone, Serialize, Deserialize)]
965pub struct TextChunk {
966    /// Position in the original document (0-based).
967    pub index: usize,
968    /// The chunk text.
969    pub content: String,
970    /// Rough token estimate (chars / 4).
971    pub token_count_estimate: usize,
972}
973
974/// A single search result.
975#[derive(Debug, Clone, Serialize, Deserialize)]
976pub struct SearchResult {
977    /// The matched text content.
978    pub content: String,
979
980    /// Where this result came from.
981    pub source: SearchSource,
982
983    /// Combined RRF score. Higher = more relevant.
984    pub score: f64,
985
986    /// BM25 rank (1-based) if this result appeared in BM25 results.
987    pub bm25_rank: Option<usize>,
988
989    /// Vector rank (1-based) if this result appeared in vector results.
990    pub vector_rank: Option<usize>,
991
992    /// Cosine similarity score if computed.
993    pub cosine_similarity: Option<f64>,
994}
995
996/// Source information for a search result.
997#[derive(Debug, Clone, Serialize, Deserialize)]
998#[serde(rename_all = "snake_case")]
999pub enum SearchSource {
1000    /// Result came from the facts table.
1001    Fact {
1002        /// Fact UUID.
1003        fact_id: String,
1004        /// Fact namespace.
1005        namespace: String,
1006    },
1007    /// Result came from a document chunk.
1008    Chunk {
1009        /// Chunk UUID.
1010        chunk_id: String,
1011        /// Parent document UUID.
1012        document_id: String,
1013        /// Parent document title.
1014        document_title: String,
1015        /// Position within the document (0-based).
1016        chunk_index: usize,
1017    },
1018    /// Result came from a conversation message.
1019    Message {
1020        /// Message auto-increment ID.
1021        message_id: i64,
1022        /// Session UUID.
1023        session_id: String,
1024        /// Message role (user, assistant, etc.).
1025        role: String,
1026    },
1027    /// Result came from an episode (causal record). SearchSource::Episode variant.
1028    Episode {
1029        /// First-class episode identity (V9+). Falls back to `document_id + "-ep0"`
1030        /// for legacy data.
1031        episode_id: String,
1032        /// Document ID the episode is attached to.
1033        document_id: String,
1034        /// Type of effect (e.g. "test_failure", "regression").
1035        effect_type: String,
1036        /// Current outcome.
1037        outcome: String,
1038    },
1039    /// Result came from an imported projection row.
1040    Projection {
1041        /// Projection row family, such as `claim_version` or `relation_version`.
1042        projection_kind: String,
1043        /// Stable projection-row identity.
1044        projection_id: String,
1045        /// Full scope carried by the imported row.
1046        scope_key: ScopeKey,
1047        /// Validity start for versioned projections, if any.
1048        valid_from: Option<String>,
1049        /// Validity end for versioned projections, if any.
1050        valid_to: Option<String>,
1051        /// Authoritative importer-assigned recorded_at.
1052        recorded_at: String,
1053        /// Source envelope provenance.
1054        source_envelope_id: String,
1055        /// Source authority provenance.
1056        source_authority: String,
1057    },
1058}
1059
1060impl SearchSource {
1061    /// Stable result ID used in receipts and replay logs.
1062    pub fn result_id(&self) -> String {
1063        match self {
1064            Self::Fact { fact_id, .. } => format!("fact:{fact_id}"),
1065            Self::Chunk { chunk_id, .. } => format!("chunk:{chunk_id}"),
1066            Self::Message { message_id, .. } => format!("msg:{message_id}"),
1067            Self::Episode { episode_id, .. } => format!("episode:{episode_id}"),
1068            Self::Projection { projection_id, .. } => format!("projection:{projection_id}"),
1069        }
1070    }
1071
1072    /// Source family label used by explain/receipt surfaces.
1073    pub fn source_kind(&self) -> &'static str {
1074        match self {
1075            Self::Fact { .. } => "fact",
1076            Self::Chunk { .. } => "chunk",
1077            Self::Message { .. } => "message",
1078            Self::Episode { .. } => "episode",
1079            Self::Projection { .. } => "projection",
1080        }
1081    }
1082
1083    /// Authoritative source row key without the receipt result prefix.
1084    pub fn source_id(&self) -> String {
1085        match self {
1086            Self::Fact { fact_id, .. } => fact_id.clone(),
1087            Self::Chunk { chunk_id, .. } => chunk_id.clone(),
1088            Self::Message { message_id, .. } => message_id.to_string(),
1089            Self::Episode { episode_id, .. } => episode_id.clone(),
1090            Self::Projection { projection_id, .. } => projection_id.clone(),
1091        }
1092    }
1093}
1094
1095// ─── Episode Types ─────────────────────────────────────────────
1096
1097/// Metadata for a causal episode (PRIMITIVES_CONTRACT §4).
1098#[derive(Debug, Clone, Serialize, Deserialize)]
1099pub struct EpisodeMeta {
1100    /// IDs of the facts/chunks/messages that caused this episode.
1101    pub cause_ids: Vec<String>,
1102    /// Type of effect (e.g. "test_failure", "regression", "improvement").
1103    pub effect_type: String,
1104    /// Current outcome assessment.
1105    pub outcome: EpisodeOutcome,
1106    /// Confidence in the causal link (0.0 to 1.0).
1107    pub confidence: f32,
1108    /// Verification status.
1109    pub verification_status: VerificationStatus,
1110    /// Links to an EvidenceBundle.run_id (if experimentally verified).
1111    pub experiment_id: Option<String>,
1112    /// Bitemporal valid time — when this episode fact was true in the domain.
1113    pub valid_time: Option<chrono::DateTime<chrono::Utc>>,
1114    /// Content-addressed digest of the episode fact payload (for supersession chain).
1115    pub fact_digest: Option<String>,
1116}
1117
1118/// Receipt for an as-of bitemporal episode query.
1119#[derive(Debug, Clone, Serialize, Deserialize)]
1120pub struct EpisodeAsOfReceiptV1 {
1121    pub query_id: String,
1122    pub as_of_valid: chrono::DateTime<chrono::Utc>,
1123    pub as_of_recorded: chrono::DateTime<chrono::Utc>,
1124    pub episode_count: usize,
1125    pub episode_ids: Vec<String>,
1126    pub excluded_superseded: usize,
1127}
1128
1129/// Outcome of an episode's causal hypothesis.
1130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1131#[serde(rename_all = "lowercase")]
1132pub enum EpisodeOutcome {
1133    /// Causal link confirmed by experiment.
1134    Confirmed,
1135    /// Causal link refuted by experiment.
1136    Refuted,
1137    /// Evidence is inconclusive.
1138    Inconclusive,
1139    /// Not yet tested.
1140    Pending,
1141}
1142
1143impl EpisodeOutcome {
1144    /// Convert to the string stored in SQLite.
1145    pub fn as_str(&self) -> &'static str {
1146        match self {
1147            Self::Confirmed => "confirmed",
1148            Self::Refuted => "refuted",
1149            Self::Inconclusive => "inconclusive",
1150            Self::Pending => "pending",
1151        }
1152    }
1153
1154    /// Parse from the string stored in SQLite.
1155    pub fn from_str_value(s: &str) -> Option<Self> {
1156        match s {
1157            "confirmed" => Some(Self::Confirmed),
1158            "refuted" => Some(Self::Refuted),
1159            "inconclusive" => Some(Self::Inconclusive),
1160            "pending" => Some(Self::Pending),
1161            _ => None,
1162        }
1163    }
1164}
1165
1166impl std::fmt::Display for EpisodeOutcome {
1167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1168        f.write_str(self.as_str())
1169    }
1170}
1171
1172/// Verification status for an episode.
1173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1174#[serde(tag = "status", rename_all = "lowercase")]
1175pub enum VerificationStatus {
1176    /// Not yet verified.
1177    Unverified,
1178    /// Successfully verified.
1179    Verified {
1180        /// Method used for verification.
1181        method: String,
1182        /// When verification occurred (ISO 8601).
1183        at: String,
1184    },
1185    /// Verification attempt failed.
1186    Failed {
1187        /// Reason for failure.
1188        reason: String,
1189        /// When verification was attempted (ISO 8601).
1190        at: String,
1191    },
1192}
1193
1194// ─── Score Breakdown ───────────────────────────────────────────
1195
1196/// Detailed score breakdown for explainable search results.
1197#[derive(Debug, Clone, Serialize, Deserialize)]
1198pub struct ScoreBreakdown {
1199    /// Final fused RRF score.
1200    pub rrf_score: f64,
1201    /// Raw BM25 score reported by SQLite FTS5 (lower is better).
1202    pub bm25_score: Option<f64>,
1203    /// Raw vector similarity used for the final vector ordering.
1204    pub vector_score: Option<f64>,
1205    /// Recency contribution added during fusion.
1206    pub recency_score: Option<f64>,
1207    /// BM25 rank (1-based).
1208    pub bm25_rank: Option<usize>,
1209    /// Vector rank (1-based).
1210    pub vector_rank: Option<usize>,
1211    /// Rank from the underlying vector retrieval source before any exact rerank.
1212    pub vector_source_rank: Option<usize>,
1213    /// Similarity score from the underlying vector retrieval source before rerank.
1214    pub vector_source_score: Option<f64>,
1215    /// BM25 RRF contribution to the final score.
1216    pub bm25_contribution: Option<f64>,
1217    /// Vector RRF contribution to the final score.
1218    pub vector_contribution: Option<f64>,
1219    /// Whether the vector ordering was reranked with exact f32 cosine similarity.
1220    pub vector_reranked_from_f32: bool,
1221    /// Configured BM25 fusion weight.
1222    pub bm25_weight: f64,
1223    /// Configured vector fusion weight.
1224    pub vector_weight: f64,
1225    /// Configured recency weight when recency is enabled.
1226    pub recency_weight: Option<f64>,
1227    /// Configured RRF decay constant.
1228    pub rrf_k: f64,
1229}
1230
1231/// Search result with full score explanation.
1232#[derive(Debug, Clone, Serialize, Deserialize)]
1233pub struct ExplainedResult {
1234    /// The search result.
1235    pub result: SearchResult,
1236    /// Score breakdown.
1237    pub breakdown: ScoreBreakdown,
1238}
1239
1240/// Product-facing answer for one explained result.
1241#[derive(Debug, Clone, Serialize, Deserialize)]
1242pub struct ExplainedResultAnswerV1 {
1243    /// Stable result ID used in receipts and replay logs.
1244    pub result_id: String,
1245    /// Source family label.
1246    pub source_kind: String,
1247    /// Authoritative source row key without the receipt result prefix.
1248    pub source_id: String,
1249    /// Plain-language reasons this result appeared.
1250    pub why_this_result: Vec<String>,
1251    /// Whether the result matched the text/BM25 lane.
1252    pub text_match: bool,
1253    /// Whether the result matched the vector lane.
1254    pub vector_match: bool,
1255    /// Whether recency contributed to the score.
1256    pub recency_applied: bool,
1257    /// Whether exact f32 rerank/reference scoring was used for the vector lane.
1258    pub exact_vector_rerank: bool,
1259    /// Final fused score.
1260    pub final_score: f64,
1261}
1262
1263impl ExplainedResult {
1264    /// Convert a detailed score breakdown into a practical "why this result" answer.
1265    pub fn answer(&self) -> ExplainedResultAnswerV1 {
1266        let text_match = self.breakdown.bm25_rank.is_some();
1267        let vector_match = self.breakdown.vector_rank.is_some();
1268        let recency_applied = self.breakdown.recency_score.is_some();
1269        let mut why_this_result = Vec::new();
1270
1271        if let Some(rank) = self.breakdown.bm25_rank {
1272            why_this_result.push(format!("text match rank {rank} contributed to fusion"));
1273        }
1274        if let Some(rank) = self.breakdown.vector_rank {
1275            why_this_result.push(format!("vector match rank {rank} contributed to fusion"));
1276        }
1277        if recency_applied {
1278            why_this_result.push("recency contributed to the fused score".to_string());
1279        }
1280        if self.breakdown.vector_reranked_from_f32 {
1281            why_this_result.push("vector score was checked with exact f32 rerank".to_string());
1282        }
1283        if why_this_result.is_empty() {
1284            why_this_result.push("result survived filtering and deterministic ranking".to_string());
1285        }
1286
1287        ExplainedResultAnswerV1 {
1288            result_id: self.result.source.result_id(),
1289            source_kind: self.result.source.source_kind().to_string(),
1290            source_id: self.result.source.source_id(),
1291            why_this_result,
1292            text_match,
1293            vector_match,
1294            recency_applied,
1295            exact_vector_rerank: self.breakdown.vector_reranked_from_f32,
1296            final_score: self.result.score,
1297        }
1298    }
1299}
1300
1301// ─── Graph Types (PRIMITIVES_CONTRACT §8) ──────────────────────
1302
1303/// Trait for querying the memory store as a graph.
1304pub trait GraphView: Send + Sync {
1305    /// Find neighboring nodes up to `max_depth` hops away.
1306    fn neighbors(
1307        &self,
1308        node_id: &str,
1309        direction: GraphDirection,
1310        max_depth: usize,
1311    ) -> Result<Vec<GraphEdge>, MemoryError>;
1312
1313    /// Find a path between two nodes (BFS, max depth).
1314    fn path(
1315        &self,
1316        from: &str,
1317        to: &str,
1318        max_depth: usize,
1319    ) -> Result<Option<Vec<String>>, MemoryError>;
1320}
1321
1322/// Direction for graph traversal.
1323#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1324pub enum GraphDirection {
1325    /// Follow outgoing edges.
1326    Outgoing,
1327    /// Follow incoming edges.
1328    Incoming,
1329    /// Follow edges in both directions.
1330    Both,
1331}
1332
1333/// An edge in the memory graph.
1334#[derive(Debug, Clone, Serialize, Deserialize)]
1335pub struct GraphEdge {
1336    /// Source node ID.
1337    pub source: String,
1338    /// Target node ID.
1339    pub target: String,
1340    /// Type of relationship.
1341    pub edge_type: GraphEdgeType,
1342    /// Edge weight (interpretation depends on edge_type).
1343    pub weight: f64,
1344    /// Optional metadata.
1345    pub metadata: Option<serde_json::Value>,
1346}
1347
1348/// Type of relationship between graph nodes.
1349#[derive(Debug, Clone, Serialize, Deserialize)]
1350#[serde(rename_all = "snake_case")]
1351pub enum GraphEdgeType {
1352    /// Semantic similarity. GraphEdgeType::Semantic variant.
1353    Semantic {
1354        /// Cosine similarity between embeddings.
1355        cosine_similarity: f32,
1356    },
1357    /// Temporal proximity. GraphEdgeType::Temporal variant.
1358    Temporal {
1359        /// Time delta in seconds.
1360        delta_secs: u64,
1361    },
1362    /// Causal relationship. GraphEdgeType::Causal variant.
1363    Causal {
1364        /// Confidence in the causal link.
1365        confidence: f32,
1366        /// EvidenceBundle run_ids supporting this link.
1367        evidence_ids: Vec<String>,
1368    },
1369    /// Entity co-occurrence. GraphEdgeType::Entity variant.
1370    Entity {
1371        /// Relationship type (e.g. "mentions", "modifies").
1372        relation: String,
1373    },
1374}
1375
1376/// Embedding displacement between two text embeddings.
1377#[derive(Debug, Clone, Serialize, Deserialize)]
1378pub struct EmbeddingDisplacement {
1379    /// Cosine similarity between the two embeddings.
1380    pub cosine_similarity: f32,
1381    /// Euclidean distance between the two embeddings.
1382    pub euclidean_distance: f32,
1383    /// Magnitude of the first embedding.
1384    pub magnitude_a: f32,
1385    /// Magnitude of the second embedding.
1386    pub magnitude_b: f32,
1387}
1388
1389/// Database statistics.
1390#[derive(Debug, Clone, Serialize, Deserialize)]
1391pub struct MemoryStats {
1392    /// Total number of facts.
1393    pub total_facts: u64,
1394    /// Total number of documents.
1395    pub total_documents: u64,
1396    /// Total number of chunks across all documents.
1397    pub total_chunks: u64,
1398    /// Total number of conversation sessions.
1399    pub total_sessions: u64,
1400    /// Total number of messages across all sessions.
1401    pub total_messages: u64,
1402    /// Database file size in bytes.
1403    pub database_size_bytes: u64,
1404    /// Currently configured embedding model.
1405    pub embedding_model: Option<String>,
1406    /// Currently configured embedding dimensions.
1407    pub embedding_dimensions: Option<usize>,
1408}
1409
1410/// Per-surface deletion counts for namespace removal.
1411#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1412pub struct NamespaceDeleteReport {
1413    /// Facts deleted from the namespace.
1414    pub facts: usize,
1415    /// Documents deleted from the namespace.
1416    pub documents: usize,
1417    /// Document chunks deleted from the namespace.
1418    pub chunks: usize,
1419    /// Messages deleted through namespaced sessions.
1420    pub messages: usize,
1421    /// Sessions deleted for the namespace.
1422    pub sessions: usize,
1423    /// Episodes deleted with namespaced documents.
1424    pub episodes: usize,
1425    /// Projection/import rows deleted or invalidated.
1426    pub projection_rows: usize,
1427    /// HNSW pending operations queued by the deletion.
1428    pub hnsw_ops: usize,
1429}