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#[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 pub fn new(value: impl Into<String>) -> Self {
29 Self(value.into())
30 }
31
32 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "lowercase")]
59pub enum Role {
60 System,
62 User,
64 Assistant,
66 Tool,
68}
69
70impl Role {
71 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum SearchSourceType {
110 Facts,
112 Chunks,
114 Messages,
116 Episodes,
118}
119
120#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(rename_all = "snake_case")]
123pub enum ReceiptMode {
124 #[default]
126 Disabled,
127 ExplainOnly,
129 ReturnReceipt,
131}
132
133#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
135#[serde(rename_all = "snake_case")]
136pub enum ExactnessProfile {
137 #[default]
139 Default,
140 PreferExact,
142 AllowApproximate,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct SearchContext {
149 pub evaluation_time: DateTime<Utc>,
151 pub receipt_mode: ReceiptMode,
153 pub exactness_profile: ExactnessProfile,
155 pub request_id: Option<String>,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub trace_id: Option<String>,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub attempt_family_id: Option<String>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub attempt_id: Option<String>,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub replay_of: Option<String>,
169 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub query_text_digest: Option<String>,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub query_input_digest: Option<String>,
175 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub filter_digest: Option<String>,
178 #[serde(default, skip_serializing_if = "Option::is_none")]
180 pub redaction_state: Option<String>,
181 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub budget_id: Option<String>,
184 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub deadline_at: Option<DateTime<Utc>>,
187}
188
189impl SearchContext {
190 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 pub fn at(evaluation_time: DateTime<Utc>) -> Self {
212 Self {
213 evaluation_time,
214 ..Self::default_now()
215 }
216 }
217
218 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#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct VectorSearchReceiptV1 {
233 #[serde(default = "default_vector_search_receipt_schema")]
235 pub schema_version: String,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub receipt_digest: Option<String>,
239 pub receipt_id: String,
241 pub evaluation_time: DateTime<Utc>,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub trace_id: Option<String>,
246 #[serde(default, skip_serializing_if = "Option::is_none")]
248 pub attempt_family_id: Option<String>,
249 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub attempt_id: Option<String>,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
254 pub replay_of: Option<String>,
255 pub query_embedding_digest: Option<String>,
257 #[serde(default, skip_serializing_if = "Option::is_none")]
259 pub query_text_digest: Option<String>,
260 #[serde(default, skip_serializing_if = "Option::is_none")]
262 pub query_input_digest: Option<String>,
263 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub filter_digest: Option<String>,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
268 pub redaction_state: Option<String>,
269 #[serde(default, skip_serializing_if = "Option::is_none")]
271 pub budget_id: Option<String>,
272 #[serde(default, skip_serializing_if = "Option::is_none")]
274 pub deadline_at: Option<DateTime<Utc>>,
275 pub search_profile: String,
277 pub candidate_backend: String,
279 pub codec_family: Option<String>,
281 pub codec_profile_digest: Option<String>,
283 #[serde(default, skip_serializing_if = "Option::is_none")]
285 pub artifact_profile_digest: Option<String>,
286 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub artifact_count: Option<usize>,
289 #[serde(default, skip_serializing_if = "Option::is_none")]
291 pub artifact_corruption_count: Option<usize>,
292 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub artifact_missing_count: Option<usize>,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
297 pub vector_artifact_manifest_digest: Option<String>,
298 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub artifact_generation_id: Option<String>,
301 #[serde(default, skip_serializing_if = "Option::is_none")]
303 pub approximate_scanned_count: Option<usize>,
304 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub approximate_returned_count: Option<usize>,
307 #[serde(default, skip_serializing_if = "Option::is_none")]
309 pub raw_rows_loaded_count: Option<usize>,
310 #[serde(default, skip_serializing_if = "Option::is_none")]
312 pub filter_strategy: Option<String>,
313 #[serde(default, skip_serializing_if = "Option::is_none")]
315 pub vector_artifact_count: Option<usize>,
316 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub vector_artifact_missing_count: Option<usize>,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
321 pub vector_artifact_stale_count: Option<usize>,
322 #[serde(default, skip_serializing_if = "Option::is_none")]
324 pub exact_rerank_count: Option<usize>,
325 #[serde(default, skip_serializing_if = "Option::is_none")]
327 pub approximate_candidate_count: Option<usize>,
328 #[serde(default, skip_serializing_if = "Option::is_none")]
330 pub fallback_reason: Option<String>,
331 #[serde(default, skip_serializing_if = "Option::is_none")]
333 pub derived_candidate: Option<DerivedCandidateReceiptV1>,
334 pub approximate: bool,
336 pub requested_candidates: usize,
338 pub returned_candidates: usize,
340 pub post_filter_candidates: usize,
342 pub fallback: Option<String>,
344 pub exact_rerank: bool,
346 pub result_ids: Vec<String>,
348 pub degradations: Vec<String>,
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize)]
354pub struct DerivedVectorArtifactGenerationV1 {
355 pub schema_version: String,
357 pub generation_id: String,
359 pub codec_family: String,
361 pub codec_profile_digest: String,
363 pub source_snapshot_digest: String,
365 pub source_row_count: usize,
367 pub artifact_count: usize,
369 pub source_tables: Vec<String>,
371 pub dim: usize,
373 pub encoding: String,
375 pub created_at: DateTime<Utc>,
377 pub build_receipt_id: Option<String>,
379 pub artifact_manifest_digest: String,
381 pub status: String,
383 pub degradations: Vec<String>,
385}
386
387#[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#[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#[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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
472pub struct VectorArtifactBuildReceiptV1 {
473 pub schema_version: String,
475 pub codec_family: String,
477 pub codec_profile_digest: String,
479 pub source_row_count: usize,
481 pub artifact_count: usize,
483 #[serde(default, skip_serializing_if = "Option::is_none")]
485 pub generation_id: Option<String>,
486 #[serde(default, skip_serializing_if = "Option::is_none")]
488 pub source_snapshot_digest: Option<String>,
489 #[serde(default, skip_serializing_if = "Option::is_none")]
491 pub artifact_manifest_digest: Option<String>,
492 #[serde(default, skip_serializing_if = "Option::is_none")]
494 pub build_receipt_id: Option<String>,
495 pub skipped_row_count: usize,
497 pub elapsed_ms: u128,
499 pub created_at: DateTime<Utc>,
501 pub degradations: Vec<String>,
503}
504
505fn default_vector_search_receipt_schema() -> String {
506 "vector_search_receipt_v1".to_string()
507}
508
509#[derive(Debug, Clone, Serialize, Deserialize)]
511pub struct SearchReceiptAnswersV1 {
512 pub receipt_id: String,
514 pub replay_receipt_id: String,
516 pub evaluation_time: DateTime<Utc>,
518 pub search_profile: String,
520 pub candidate_backend: String,
522 pub codec_family: Option<String>,
524 pub codec_profile_digest: Option<String>,
526 pub exactness: String,
528 pub approximate: bool,
530 pub exact_rerank: bool,
532 pub fallback: Option<String>,
534 pub degraded: bool,
536 pub replay_ready: bool,
538 pub rebuild_ready: bool,
540 pub result_ids: Vec<String>,
542 pub result_count: usize,
544 pub degradations: Vec<String>,
546 pub why_results_appeared: Vec<String>,
548}
549
550impl VectorSearchReceiptV1 {
551 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#[derive(Debug, Clone, Serialize, Deserialize)]
621pub struct SearchResponse {
622 pub results: Vec<SearchResult>,
624 pub receipt: Option<VectorSearchReceiptV1>,
626}
627
628#[derive(Debug, Clone, Serialize, Deserialize)]
633pub struct ChunkManifestEntry {
634 pub external_chunk_id: String,
636 pub content: String,
638 #[serde(default, skip_serializing_if = "Option::is_none")]
640 pub token_count_estimate: Option<usize>,
641 #[serde(default, skip_serializing_if = "Option::is_none")]
643 pub content_digest: Option<String>,
644 #[serde(default, skip_serializing_if = "Option::is_none")]
646 pub metadata: Option<serde_json::Value>,
647}
648
649#[derive(Debug, Clone, Serialize, Deserialize)]
651pub struct ChunkManifestIngestOptions {
652 pub title: String,
654 pub namespace: String,
656 #[serde(default, skip_serializing_if = "Option::is_none")]
658 pub source_path: Option<String>,
659 #[serde(default, skip_serializing_if = "Option::is_none")]
661 pub metadata: Option<serde_json::Value>,
662}
663
664#[derive(Debug, Clone, Serialize, Deserialize)]
666pub struct ChunkManifestChunkMapping {
667 pub external_chunk_id: String,
669 pub sm_document_id: String,
671 pub sm_chunk_id: String,
673 pub chunk_index: usize,
675 #[serde(default, skip_serializing_if = "Option::is_none")]
677 pub content_digest: Option<String>,
678 #[serde(default, skip_serializing_if = "Option::is_none")]
680 pub metadata: Option<serde_json::Value>,
681}
682
683#[derive(Debug, Clone, Serialize, Deserialize)]
685pub struct ChunkManifestIngestResult {
686 pub sm_document_id: String,
688 pub namespace: String,
690 pub receipt_id: String,
692 pub chunks: Vec<ChunkManifestChunkMapping>,
694}
695
696#[derive(Debug, Clone, Serialize, Deserialize)]
698pub struct ExplainedSearchResponse {
699 pub results: Vec<ExplainedResult>,
701 pub receipt: Option<VectorSearchReceiptV1>,
703}
704
705#[derive(Debug, Clone, Serialize, Deserialize)]
707pub struct SearchReplayReportV1 {
708 pub receipt_id: String,
710 pub replay_receipt_id: String,
712 pub original_receipt: VectorSearchReceiptV1,
714 pub replay_receipt: VectorSearchReceiptV1,
716 pub query_embedding_digest_matches: bool,
718 pub result_ids_match: bool,
720 pub missing_result_ids: Vec<String>,
722 pub added_result_ids: Vec<String>,
724 pub vector_only: bool,
726}
727
728#[derive(Debug, Clone, Serialize, Deserialize)]
730pub struct ProjectionQuery {
731 pub scope: ScopeKey,
733 #[serde(default, skip_serializing_if = "Option::is_none")]
735 pub text_query: Option<String>,
736 #[serde(default, skip_serializing_if = "Option::is_none")]
738 pub valid_at: Option<String>,
739 #[serde(default, skip_serializing_if = "Option::is_none")]
741 pub recorded_at_or_before: Option<String>,
742 #[serde(default, skip_serializing_if = "Option::is_none")]
744 pub subject_entity_id: Option<EntityId>,
745 #[serde(default, skip_serializing_if = "Option::is_none")]
747 pub canonical_entity_id: Option<EntityId>,
748 #[serde(default, skip_serializing_if = "Option::is_none")]
750 pub claim_state: Option<String>,
751 #[serde(default, skip_serializing_if = "Option::is_none")]
753 pub claim_id: Option<ClaimId>,
754 #[serde(default, skip_serializing_if = "Option::is_none")]
756 pub claim_version_id: Option<ClaimVersionId>,
757 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#[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#[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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
891pub struct Session {
892 pub id: String,
894 pub channel: String,
896 pub created_at: String,
898 pub updated_at: String,
900 pub metadata: Option<serde_json::Value>,
902 pub message_count: u32,
904}
905
906#[derive(Debug, Clone, Serialize, Deserialize)]
908pub struct Message {
909 pub id: i64,
911 pub session_id: String,
913 pub role: Role,
915 pub content: String,
917 pub token_count: Option<u32>,
919 pub created_at: String,
921 pub metadata: Option<serde_json::Value>,
923}
924
925#[derive(Debug, Clone, Serialize, Deserialize)]
927pub struct Fact {
928 pub id: String,
930 pub namespace: String,
932 pub content: String,
934 pub source: Option<String>,
936 pub created_at: String,
938 pub updated_at: String,
940 pub metadata: Option<serde_json::Value>,
942}
943
944#[derive(Debug, Clone, Serialize, Deserialize)]
946pub struct Document {
947 pub id: String,
949 pub title: String,
951 pub source_path: Option<String>,
953 pub namespace: String,
955 pub created_at: String,
957 pub metadata: Option<serde_json::Value>,
959 pub chunk_count: u32,
961}
962
963#[derive(Debug, Clone, Serialize, Deserialize)]
965pub struct TextChunk {
966 pub index: usize,
968 pub content: String,
970 pub token_count_estimate: usize,
972}
973
974#[derive(Debug, Clone, Serialize, Deserialize)]
976pub struct SearchResult {
977 pub content: String,
979
980 pub source: SearchSource,
982
983 pub score: f64,
985
986 pub bm25_rank: Option<usize>,
988
989 pub vector_rank: Option<usize>,
991
992 pub cosine_similarity: Option<f64>,
994}
995
996#[derive(Debug, Clone, Serialize, Deserialize)]
998#[serde(rename_all = "snake_case")]
999pub enum SearchSource {
1000 Fact {
1002 fact_id: String,
1004 namespace: String,
1006 },
1007 Chunk {
1009 chunk_id: String,
1011 document_id: String,
1013 document_title: String,
1015 chunk_index: usize,
1017 },
1018 Message {
1020 message_id: i64,
1022 session_id: String,
1024 role: String,
1026 },
1027 Episode {
1029 episode_id: String,
1032 document_id: String,
1034 effect_type: String,
1036 outcome: String,
1038 },
1039 Projection {
1041 projection_kind: String,
1043 projection_id: String,
1045 scope_key: ScopeKey,
1047 valid_from: Option<String>,
1049 valid_to: Option<String>,
1051 recorded_at: String,
1053 source_envelope_id: String,
1055 source_authority: String,
1057 },
1058}
1059
1060impl SearchSource {
1061 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
1099pub struct EpisodeMeta {
1100 pub cause_ids: Vec<String>,
1102 pub effect_type: String,
1104 pub outcome: EpisodeOutcome,
1106 pub confidence: f32,
1108 pub verification_status: VerificationStatus,
1110 pub experiment_id: Option<String>,
1112 pub valid_time: Option<chrono::DateTime<chrono::Utc>>,
1114 pub fact_digest: Option<String>,
1116}
1117
1118#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1131#[serde(rename_all = "lowercase")]
1132pub enum EpisodeOutcome {
1133 Confirmed,
1135 Refuted,
1137 Inconclusive,
1139 Pending,
1141}
1142
1143impl EpisodeOutcome {
1144 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 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1174#[serde(tag = "status", rename_all = "lowercase")]
1175pub enum VerificationStatus {
1176 Unverified,
1178 Verified {
1180 method: String,
1182 at: String,
1184 },
1185 Failed {
1187 reason: String,
1189 at: String,
1191 },
1192}
1193
1194#[derive(Debug, Clone, Serialize, Deserialize)]
1198pub struct ScoreBreakdown {
1199 pub rrf_score: f64,
1201 pub bm25_score: Option<f64>,
1203 pub vector_score: Option<f64>,
1205 pub recency_score: Option<f64>,
1207 pub bm25_rank: Option<usize>,
1209 pub vector_rank: Option<usize>,
1211 pub vector_source_rank: Option<usize>,
1213 pub vector_source_score: Option<f64>,
1215 pub bm25_contribution: Option<f64>,
1217 pub vector_contribution: Option<f64>,
1219 pub vector_reranked_from_f32: bool,
1221 pub bm25_weight: f64,
1223 pub vector_weight: f64,
1225 pub recency_weight: Option<f64>,
1227 pub rrf_k: f64,
1229}
1230
1231#[derive(Debug, Clone, Serialize, Deserialize)]
1233pub struct ExplainedResult {
1234 pub result: SearchResult,
1236 pub breakdown: ScoreBreakdown,
1238}
1239
1240#[derive(Debug, Clone, Serialize, Deserialize)]
1242pub struct ExplainedResultAnswerV1 {
1243 pub result_id: String,
1245 pub source_kind: String,
1247 pub source_id: String,
1249 pub why_this_result: Vec<String>,
1251 pub text_match: bool,
1253 pub vector_match: bool,
1255 pub recency_applied: bool,
1257 pub exact_vector_rerank: bool,
1259 pub final_score: f64,
1261}
1262
1263impl ExplainedResult {
1264 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
1301pub trait GraphView: Send + Sync {
1305 fn neighbors(
1307 &self,
1308 node_id: &str,
1309 direction: GraphDirection,
1310 max_depth: usize,
1311 ) -> Result<Vec<GraphEdge>, MemoryError>;
1312
1313 fn path(
1315 &self,
1316 from: &str,
1317 to: &str,
1318 max_depth: usize,
1319 ) -> Result<Option<Vec<String>>, MemoryError>;
1320}
1321
1322#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1324pub enum GraphDirection {
1325 Outgoing,
1327 Incoming,
1329 Both,
1331}
1332
1333#[derive(Debug, Clone, Serialize, Deserialize)]
1335pub struct GraphEdge {
1336 pub source: String,
1338 pub target: String,
1340 pub edge_type: GraphEdgeType,
1342 pub weight: f64,
1344 pub metadata: Option<serde_json::Value>,
1346}
1347
1348#[derive(Debug, Clone, Serialize, Deserialize)]
1350#[serde(rename_all = "snake_case")]
1351pub enum GraphEdgeType {
1352 Semantic {
1354 cosine_similarity: f32,
1356 },
1357 Temporal {
1359 delta_secs: u64,
1361 },
1362 Causal {
1364 confidence: f32,
1366 evidence_ids: Vec<String>,
1368 },
1369 Entity {
1371 relation: String,
1373 },
1374}
1375
1376#[derive(Debug, Clone, Serialize, Deserialize)]
1378pub struct EmbeddingDisplacement {
1379 pub cosine_similarity: f32,
1381 pub euclidean_distance: f32,
1383 pub magnitude_a: f32,
1385 pub magnitude_b: f32,
1387}
1388
1389#[derive(Debug, Clone, Serialize, Deserialize)]
1391pub struct MemoryStats {
1392 pub total_facts: u64,
1394 pub total_documents: u64,
1396 pub total_chunks: u64,
1398 pub total_sessions: u64,
1400 pub total_messages: u64,
1402 pub database_size_bytes: u64,
1404 pub embedding_model: Option<String>,
1406 pub embedding_dimensions: Option<usize>,
1408}
1409
1410#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1412pub struct NamespaceDeleteReport {
1413 pub facts: usize,
1415 pub documents: usize,
1417 pub chunks: usize,
1419 pub messages: usize,
1421 pub sessions: usize,
1423 pub episodes: usize,
1425 pub projection_rows: usize,
1427 pub hnsw_ops: usize,
1429}