Skip to main content

yantrikdb/base/
types.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4// ── Embedder trait ──
5
6/// Trait for converting text to embedding vectors.
7/// Implementations can use any embedding model (sentence-transformers, candle, etc.).
8pub trait Embedder: Send + Sync {
9    /// Embed a single text string into a vector.
10    fn embed(&self, text: &str) -> std::result::Result<Vec<f32>, Box<dyn std::error::Error + Send + Sync>>;
11
12    /// Embed multiple texts. Default implementation calls embed() in a loop.
13    fn embed_batch(&self, texts: &[&str]) -> std::result::Result<Vec<Vec<f32>>, Box<dyn std::error::Error + Send + Sync>> {
14        texts.iter().map(|t| self.embed(t)).collect()
15    }
16
17    /// The dimensionality of produced embeddings.
18    fn dim(&self) -> usize;
19}
20
21/// A memory record returned by get() and recall().
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct Memory {
24    pub rid: String,
25    pub memory_type: String,
26    pub text: String,
27    pub created_at: f64,
28    pub importance: f64,
29    pub valence: f64,
30    pub half_life: f64,
31    pub last_access: f64,
32    pub access_count: u32,
33    pub consolidation_status: String,
34    pub storage_tier: String,
35    pub consolidated_into: Option<String>,
36    pub metadata: serde_json::Value,
37    pub namespace: String,
38    // Cognitive dimensions (V10)
39    pub certainty: f64,
40    pub domain: String,
41    pub source: String,
42    pub emotional_state: Option<String>,
43    // Session & temporal (V13)
44    pub session_id: Option<String>,
45    pub due_at: Option<f64>,
46    pub temporal_kind: Option<String>,
47}
48
49/// Score breakdown for a recall result.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ScoreBreakdown {
52    pub similarity: f64,
53    pub decay: f64,
54    pub recency: f64,
55    pub importance: f64,
56    pub graph_proximity: f64,
57    /// Weighted contribution of each signal to the final score.
58    pub contributions: ScoreContributions,
59    /// Valence multiplier applied to the raw score.
60    pub valence_multiplier: f64,
61}
62
63/// Weighted contributions of each signal (signal_value * weight).
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct ScoreContributions {
66    pub similarity: f64,
67    pub decay: f64,
68    pub recency: f64,
69    pub importance: f64,
70    pub graph_proximity: f64,
71}
72
73/// A recall result with scoring information.
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct RecallResult {
76    pub rid: String,
77    pub memory_type: String,
78    pub text: String,
79    pub created_at: f64,
80    pub importance: f64,
81    pub valence: f64,
82    pub score: f64,
83    pub scores: ScoreBreakdown,
84    pub why_retrieved: Vec<String>,
85    pub metadata: serde_json::Value,
86    pub namespace: String,
87    // Cognitive dimensions (V10)
88    pub certainty: f64,
89    pub domain: String,
90    pub source: String,
91    pub emotional_state: Option<String>,
92}
93
94/// Response from recall with confidence and hints for interactive retrieval.
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct RecallResponse {
97    pub results: Vec<RecallResult>,
98    pub confidence: f64,
99    /// Human-readable explanation of what drove the confidence score.
100    pub certainty_reasons: Vec<String>,
101    pub retrieval_summary: RetrievalSummary,
102    pub hints: Vec<RefinementHint>,
103}
104
105/// Summary of how retrieval was performed.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct RetrievalSummary {
108    pub top_similarity: f64,
109    pub score_spread: f64,
110    pub sources_used: Vec<String>,
111    pub candidate_count: usize,
112}
113
114/// A hint for refining a query when confidence is low.
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct RefinementHint {
117    pub hint_type: String,
118    pub suggestion: String,
119    pub related_entities: Vec<String>,
120}
121
122/// An edge in the entity graph.
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct Edge {
125    pub edge_id: String,
126    pub src: String,
127    pub dst: String,
128    pub rel_type: String,
129    pub weight: f64,
130}
131
132/// An entity in the knowledge graph.
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct Entity {
135    pub name: String,
136    pub entity_type: String,
137    pub first_seen: f64,
138    pub last_seen: f64,
139    pub mention_count: i64,
140}
141
142/// Engine statistics.
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct Stats {
145    pub active_memories: i64,
146    pub consolidated_memories: i64,
147    pub tombstoned_memories: i64,
148    pub archived_memories: i64,
149    pub edges: i64,
150    pub entities: i64,
151    pub operations: i64,
152    pub open_conflicts: i64,
153    pub resolved_conflicts: i64,
154    pub pending_triggers: i64,
155    pub active_patterns: i64,
156    pub scoring_cache_entries: usize,
157    pub vec_index_entries: usize,
158    pub graph_index_entities: usize,
159    pub graph_index_edges: usize,
160}
161
162/// A proactive trigger.
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct Trigger {
165    pub trigger_type: String,
166    pub reason: String,
167    pub urgency: f64,
168    pub source_rids: Vec<String>,
169    pub suggested_action: String,
170    pub context: HashMap<String, serde_json::Value>,
171}
172
173/// Consolidation result (after consolidation runs).
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct ConsolidationResult {
176    pub consolidated_rid: String,
177    pub source_rids: Vec<String>,
178    pub cluster_size: usize,
179    pub summary: String,
180    pub importance: f64,
181    pub entities_linked: usize,
182}
183
184/// Dry run consolidation preview.
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct ConsolidationPreview {
187    pub cluster_size: usize,
188    pub texts: Vec<String>,
189    pub preview_summary: String,
190    pub source_rids: Vec<String>,
191}
192
193/// Internal struct with embedding data for clustering.
194#[derive(Debug, Clone)]
195pub struct MemoryWithEmbedding {
196    pub rid: String,
197    pub memory_type: String,
198    pub text: String,
199    pub embedding: Vec<f32>,
200    pub created_at: f64,
201    pub importance: f64,
202    pub valence: f64,
203    pub half_life: f64,
204    pub last_access: f64,
205    pub metadata: serde_json::Value,
206    pub namespace: String,
207}
208
209/// A decayed memory candidate from decay().
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct DecayedMemory {
212    pub rid: String,
213    pub text: String,
214    pub memory_type: String,
215    pub original_importance: f64,
216    pub current_score: f64,
217    pub days_since_access: f64,
218}
219
220/// Lightweight scoring fields cached in memory for fast recall scoring.
221/// These are the only fields needed to compute composite_score() during recall.
222#[derive(Debug, Clone)]
223pub struct ScoringRow {
224    pub created_at: f64,
225    pub importance: f64,
226    pub half_life: f64,
227    pub last_access: f64,
228    pub access_count: u32,
229    pub valence: f64,
230    pub consolidation_status: String,
231    pub memory_type: String,
232    pub namespace: String,
233    // Cognitive dimensions (V10)
234    pub certainty: f64,
235    pub domain: String,
236    pub source: String,
237    pub emotional_state: Option<String>,
238}
239
240/// Input for batch record operations.
241#[derive(Debug, Clone)]
242pub struct RecordInput {
243    pub text: String,
244    pub memory_type: String,
245    pub importance: f64,
246    pub valence: f64,
247    pub half_life: f64,
248    pub metadata: serde_json::Value,
249    pub embedding: Vec<f32>,
250    pub namespace: String,
251    // Cognitive dimensions (V10)
252    pub certainty: f64,
253    pub domain: String,
254    pub source: String,
255    pub emotional_state: Option<String>,
256}
257
258// ── Conflict types (V2) ──
259
260/// The type of semantic conflict between two memories.
261#[derive(Debug, Clone, PartialEq)]
262pub enum ConflictType {
263    IdentityFact,
264    Preference,
265    Temporal,
266    Consolidation,
267    Minor,
268}
269
270impl ConflictType {
271    pub fn as_str(&self) -> &'static str {
272        match self {
273            ConflictType::IdentityFact => "identity_fact",
274            ConflictType::Preference => "preference",
275            ConflictType::Temporal => "temporal",
276            ConflictType::Consolidation => "consolidation",
277            ConflictType::Minor => "minor",
278        }
279    }
280
281    pub fn from_str(s: &str) -> Self {
282        match s {
283            "identity_fact" => ConflictType::IdentityFact,
284            "preference" => ConflictType::Preference,
285            "temporal" => ConflictType::Temporal,
286            "consolidation" => ConflictType::Consolidation,
287            _ => ConflictType::Minor,
288        }
289    }
290
291    pub fn default_priority(&self) -> &'static str {
292        match self {
293            ConflictType::IdentityFact => "critical",
294            ConflictType::Preference => "high",
295            ConflictType::Temporal => "high",
296            ConflictType::Consolidation => "medium",
297            ConflictType::Minor => "low",
298        }
299    }
300}
301
302/// A conflict between two memories.
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct Conflict {
305    pub conflict_id: String,
306    pub conflict_type: String,
307    pub priority: String,
308    pub status: String,
309    pub memory_a: String,
310    pub memory_b: String,
311    pub entity: Option<String>,
312    pub rel_type: Option<String>,
313    pub detected_at: f64,
314    pub detected_by: String,
315    pub detection_reason: String,
316    pub resolved_at: Option<f64>,
317    pub resolved_by: Option<String>,
318    pub strategy: Option<String>,
319    pub winner_rid: Option<String>,
320    pub resolution_note: Option<String>,
321}
322
323/// Result of a conflict resolution.
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct ConflictResolutionResult {
326    pub conflict_id: String,
327    pub strategy: String,
328    pub winner_rid: Option<String>,
329    pub loser_tombstoned: bool,
330    pub new_memory_rid: Option<String>,
331}
332
333/// Result of a user-initiated correction.
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct CorrectionResult {
336    pub original_rid: String,
337    pub corrected_rid: String,
338    pub original_tombstoned: bool,
339}
340
341// ── Cognition types (V3) ──
342
343/// Trigger type classification.
344#[derive(Debug, Clone, PartialEq)]
345pub enum TriggerType {
346    DecayReview,
347    ConsolidationReady,
348    ConflictEscalation,
349    TemporalDrift,
350    Redundancy,
351    RelationshipInsight,
352    ValenceTrend,
353    EntityAnomaly,
354    PatternDiscovered,
355}
356
357impl TriggerType {
358    pub fn as_str(&self) -> &'static str {
359        match self {
360            TriggerType::DecayReview => "decay_review",
361            TriggerType::ConsolidationReady => "consolidation_ready",
362            TriggerType::ConflictEscalation => "conflict_escalation",
363            TriggerType::TemporalDrift => "temporal_drift",
364            TriggerType::Redundancy => "redundancy",
365            TriggerType::RelationshipInsight => "relationship_insight",
366            TriggerType::ValenceTrend => "valence_trend",
367            TriggerType::EntityAnomaly => "entity_anomaly",
368            TriggerType::PatternDiscovered => "pattern_discovered",
369        }
370    }
371
372    pub fn from_str(s: &str) -> Self {
373        match s {
374            "decay_review" => TriggerType::DecayReview,
375            "consolidation_ready" => TriggerType::ConsolidationReady,
376            "conflict_escalation" => TriggerType::ConflictEscalation,
377            "temporal_drift" => TriggerType::TemporalDrift,
378            "redundancy" => TriggerType::Redundancy,
379            "relationship_insight" => TriggerType::RelationshipInsight,
380            "valence_trend" => TriggerType::ValenceTrend,
381            "entity_anomaly" => TriggerType::EntityAnomaly,
382            "pattern_discovered" => TriggerType::PatternDiscovered,
383            _ => TriggerType::DecayReview,
384        }
385    }
386
387    pub fn default_cooldown_secs(&self) -> f64 {
388        match self {
389            TriggerType::DecayReview => 86400.0 * 3.0,
390            TriggerType::ConsolidationReady => 86400.0,
391            TriggerType::ConflictEscalation => 86400.0 * 2.0,
392            TriggerType::TemporalDrift => 86400.0 * 14.0,
393            TriggerType::Redundancy => 86400.0,
394            TriggerType::RelationshipInsight => 86400.0 * 7.0,
395            TriggerType::ValenceTrend => 86400.0 * 7.0,
396            TriggerType::EntityAnomaly => 86400.0 * 7.0,
397            TriggerType::PatternDiscovered => 86400.0 * 7.0,
398        }
399    }
400
401    pub fn default_expiry_secs(&self) -> f64 {
402        match self {
403            TriggerType::DecayReview => 86400.0 * 7.0,
404            TriggerType::ConsolidationReady => 86400.0 * 3.0,
405            TriggerType::ConflictEscalation => 86400.0 * 14.0,
406            _ => 86400.0 * 7.0,
407        }
408    }
409}
410
411/// Configuration for the think() cognition loop.
412#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct ThinkConfig {
414    pub importance_threshold: f64,
415    pub decay_threshold: f64,
416    pub max_triggers: usize,
417    pub run_consolidation: bool,
418    pub run_conflict_scan: bool,
419    pub run_pattern_mining: bool,
420    pub consolidation_sim_threshold: f64,
421    pub consolidation_time_window_days: f64,
422    pub consolidation_min_cluster: usize,
423    pub min_active_memories: i64,
424    pub run_personality: bool,
425}
426
427impl Default for ThinkConfig {
428    fn default() -> Self {
429        Self {
430            importance_threshold: 0.5,
431            decay_threshold: 0.1,
432            max_triggers: 10,
433            run_consolidation: true,
434            run_conflict_scan: true,
435            run_pattern_mining: true,
436            consolidation_sim_threshold: 0.6,
437            consolidation_time_window_days: 7.0,
438            consolidation_min_cluster: 2,
439            min_active_memories: 10,
440            run_personality: true,
441        }
442    }
443}
444
445/// Result of a think() pass.
446#[derive(Debug, Clone, Serialize, Deserialize)]
447pub struct ThinkResult {
448    pub triggers: Vec<Trigger>,
449    pub consolidation_count: usize,
450    pub conflicts_found: usize,
451    pub patterns_new: usize,
452    pub patterns_updated: usize,
453    pub expired_triggers: usize,
454    pub personality_updated: bool,
455    pub duration_ms: u64,
456}
457
458/// A persisted trigger with lifecycle state.
459#[derive(Debug, Clone, Serialize, Deserialize)]
460pub struct PersistedTrigger {
461    pub trigger_id: String,
462    pub trigger_type: String,
463    pub urgency: f64,
464    pub status: String,
465    pub reason: String,
466    pub suggested_action: String,
467    pub source_rids: Vec<String>,
468    pub context: serde_json::Value,
469    pub created_at: f64,
470    pub delivered_at: Option<f64>,
471    pub acknowledged_at: Option<f64>,
472    pub acted_at: Option<f64>,
473    pub expires_at: Option<f64>,
474}
475
476/// A detected pattern across memories.
477#[derive(Debug, Clone, Serialize, Deserialize)]
478pub struct Pattern {
479    pub pattern_id: String,
480    pub pattern_type: String,
481    pub status: String,
482    pub confidence: f64,
483    pub description: String,
484    pub evidence_rids: Vec<String>,
485    pub entity_names: Vec<String>,
486    pub context: serde_json::Value,
487    pub first_seen: f64,
488    pub last_confirmed: f64,
489    pub occurrence_count: i64,
490}
491
492/// Result of pattern mining.
493#[derive(Debug, Clone, Serialize, Deserialize)]
494pub struct PatternMiningResult {
495    pub new_patterns: usize,
496    pub updated_patterns: usize,
497    pub stale_patterns: usize,
498}
499
500/// Configuration for pattern mining.
501#[derive(Debug, Clone)]
502pub struct PatternConfig {
503    pub co_occurrence_min_count: usize,
504    pub temporal_cluster_min_events: usize,
505    pub valence_trend_delta_threshold: f64,
506    pub topic_cluster_sim_threshold: f64,
507    pub topic_cluster_time_window_days: f64,
508    pub entity_hub_min_degree: usize,
509    pub max_patterns: usize,
510    // Cross-domain mining (V13)
511    pub cross_domain_candidates_per_domain: usize,
512    pub cross_domain_sim_threshold: f64,
513    pub cross_domain_max_per_pair: usize,
514    pub entity_bridge_min_domains: usize,
515    pub entity_bridge_min_mentions_per_domain: usize,
516    pub run_cross_domain: bool,
517}
518
519// ── Profiling types (feature-gated) ──
520
521/// Timing breakdown for a single recall() invocation.
522#[cfg(feature = "profiling")]
523#[derive(Debug, Clone)]
524pub struct RecallTimings {
525    pub vec_search_ms: f64,
526    pub cache_score_ms: f64,
527    pub fetch_ms: f64,
528    pub scoring_ms: f64,
529    pub graph_ms: f64,
530    pub reinforce_ms: f64,
531    pub sort_truncate_ms: f64,
532    pub total_ms: f64,
533    pub candidate_count: usize,
534    pub graph_expansion_count: usize,
535}
536
537/// Result of recall_profiled() — recall results plus timing breakdown.
538#[cfg(feature = "profiling")]
539#[derive(Debug, Clone)]
540pub struct RecallProfiledResult {
541    pub results: Vec<RecallResult>,
542    pub timings: RecallTimings,
543}
544
545/// Builder for composable recall queries.
546///
547/// ```rust,ignore
548/// let results = db.query(embedding)
549///     .top_k(10)
550///     .memory_type("episodic")
551///     .namespace("work")
552///     .expand_entities("tell me about Alice")
553///     .time_window(start, end)
554///     .execute()?;
555/// ```
556#[derive(Debug, Clone)]
557pub struct RecallQuery {
558    pub embedding: Vec<f32>,
559    pub top_k: usize,
560    pub time_window: Option<(f64, f64)>,
561    pub memory_type: Option<String>,
562    pub include_consolidated: bool,
563    pub expand_entities: bool,
564    pub query_text: Option<String>,
565    pub skip_reinforce: bool,
566    pub namespace: Option<String>,
567    // V10 filters
568    pub domain: Option<String>,
569    pub source: Option<String>,
570}
571
572impl RecallQuery {
573    /// Create a new query builder with the given embedding vector.
574    pub fn new(embedding: Vec<f32>) -> Self {
575        Self {
576            embedding,
577            top_k: 10,
578            time_window: None,
579            memory_type: None,
580            include_consolidated: false,
581            expand_entities: false,
582            query_text: None,
583            skip_reinforce: false,
584            namespace: None,
585            domain: None,
586            source: None,
587        }
588    }
589
590    /// Set maximum number of results to return.
591    pub fn top_k(mut self, k: usize) -> Self {
592        self.top_k = k;
593        self
594    }
595
596    /// Filter by memory type (e.g., "episodic", "semantic", "procedural").
597    pub fn memory_type(mut self, mt: &str) -> Self {
598        self.memory_type = Some(mt.to_string());
599        self
600    }
601
602    /// Filter by namespace.
603    pub fn namespace(mut self, ns: &str) -> Self {
604        self.namespace = Some(ns.to_string());
605        self
606    }
607
608    /// Restrict results to a time window (created_at between start and end).
609    pub fn time_window(mut self, start: f64, end: f64) -> Self {
610        self.time_window = Some((start, end));
611        self
612    }
613
614    /// Enable graph expansion with the given query text for entity extraction.
615    pub fn expand_entities(mut self, query_text: &str) -> Self {
616        self.expand_entities = true;
617        self.query_text = Some(query_text.to_string());
618        self
619    }
620
621    /// Include consolidated (merged) memories in results.
622    pub fn include_consolidated(mut self) -> Self {
623        self.include_consolidated = true;
624        self
625    }
626
627    /// Skip spaced-repetition reinforcement on accessed memories.
628    pub fn skip_reinforce(mut self) -> Self {
629        self.skip_reinforce = true;
630        self
631    }
632
633    /// Filter by domain (e.g., "work", "health", "family").
634    pub fn domain(mut self, d: &str) -> Self {
635        self.domain = Some(d.to_string());
636        self
637    }
638
639    /// Filter by source (e.g., "user", "system", "document", "inference").
640    pub fn source(mut self, s: &str) -> Self {
641        self.source = Some(s.to_string());
642        self
643    }
644}
645
646/// Learned scoring weights stored per-database for adaptive recall.
647#[derive(Debug, Clone, Serialize, Deserialize)]
648pub struct LearnedWeights {
649    pub w_sim: f64,
650    pub w_decay: f64,
651    pub w_recency: f64,
652    pub gate_tau: f64,
653    pub alpha_imp: f64,
654    pub keyword_boost: f64,
655    pub generation: i64,
656}
657
658impl Default for LearnedWeights {
659    fn default() -> Self {
660        Self {
661            w_sim: 0.50,
662            w_decay: 0.20,
663            w_recency: 0.30,
664            gate_tau: 0.25,
665            alpha_imp: 0.80,
666            keyword_boost: 0.31,
667            generation: 0,
668        }
669    }
670}
671
672// ── Personality types (V11) ──
673
674/// A single personality trait with its current score and derivation metadata.
675#[derive(Debug, Clone, Serialize, Deserialize)]
676pub struct PersonalityTrait {
677    pub trait_name: String,
678    pub score: f64,
679    pub confidence: f64,
680    pub sample_count: i64,
681    pub updated_at: f64,
682}
683
684/// Aggregated personality profile across all traits.
685#[derive(Debug, Clone, Serialize, Deserialize)]
686pub struct PersonalityProfile {
687    pub traits: Vec<PersonalityTrait>,
688    pub updated_at: f64,
689}
690
691// ── Session types (V13) ──
692
693/// A session tracks a conversation or interaction period.
694#[derive(Debug, Clone, Serialize, Deserialize)]
695pub struct Session {
696    pub session_id: String,
697    pub namespace: String,
698    pub client_id: String,
699    pub status: String,
700    pub started_at: f64,
701    pub ended_at: Option<f64>,
702    pub summary: Option<String>,
703    pub avg_valence: Option<f64>,
704    pub memory_count: i64,
705    pub topics: Vec<String>,
706    pub metadata: serde_json::Value,
707}
708
709/// Summary returned when ending a session.
710#[derive(Debug, Clone, Serialize, Deserialize)]
711pub struct SessionSummary {
712    pub session_id: String,
713    pub duration_secs: f64,
714    pub memory_count: i64,
715    pub avg_valence: f64,
716    pub topics: Vec<String>,
717}
718
719// ── Temporal & Entity Profile types (V13) ──
720
721/// Rich profile of an entity across time, domains, and sessions.
722#[derive(Debug, Clone, Serialize, Deserialize)]
723pub struct EntityProfile {
724    pub entity: String,
725    pub entity_type: String,
726    pub mention_count: i64,
727    pub session_count: i64,
728    pub domains: Vec<DomainCount>,
729    pub avg_valence: f64,
730    pub valence_trend: f64,
731    pub dominant_emotion: Option<String>,
732    pub interaction_frequency: f64,
733    pub last_mentioned_at: f64,
734    pub first_seen: f64,
735    pub window_days: f64,
736}
737
738/// Count of mentions within a domain.
739#[derive(Debug, Clone, Serialize, Deserialize)]
740pub struct DomainCount {
741    pub domain: String,
742    pub count: i64,
743}
744
745// ── Cross-domain mining types (V13) ──
746
747/// A link between memories in different domains discovered by cross-domain mining.
748#[derive(Debug, Clone, Serialize, Deserialize)]
749pub struct CrossDomainLink {
750    pub rid_a: String,
751    pub rid_b: String,
752    pub domain_a: String,
753    pub domain_b: String,
754    pub similarity: f64,
755    pub text_a: String,
756    pub text_b: String,
757    pub score: f64,
758}
759
760/// An entity that bridges multiple domains.
761#[derive(Debug, Clone, Serialize, Deserialize)]
762pub struct EntityBridge {
763    pub entity: String,
764    pub domains: Vec<DomainCount>,
765    pub bridge_score: f64,
766    pub total_mentions: i64,
767}
768
769// ── Relationship depth types (V14) ──
770
771/// Rich interaction metrics for an entity, measuring depth of knowledge.
772/// This goes beyond simple mention counts to capture how deeply the system
773/// knows about an entity across sessions, domains, and time.
774#[derive(Debug, Clone, Serialize, Deserialize)]
775pub struct RelationshipDepth {
776    /// The entity name.
777    pub entity: String,
778    /// The entity type (person, organization, tech, etc.).
779    pub entity_type: String,
780    /// Number of distinct sessions where this entity appeared.
781    pub sessions_together: i64,
782    /// Total memories mentioning this entity.
783    pub memories_mentioning: i64,
784    /// Average valence of memories involving this entity.
785    pub avg_valence: f64,
786    /// Domains this entity spans (e.g., ["work", "health", "family"]).
787    pub domains_spanning: Vec<String>,
788    /// Distinct relationship types connected to this entity.
789    pub relationship_types: Vec<String>,
790    /// Number of distinct entities this entity is connected to in the graph.
791    pub connection_count: i64,
792    /// Composite depth score (0.0-1.0): higher = deeper relationship.
793    /// Combines sessions, memories, domain breadth, connection count.
794    pub depth_score: f64,
795    /// When this entity was first seen.
796    pub first_seen: f64,
797    /// When this entity was last seen.
798    pub last_seen: f64,
799    /// Mentions per day since first seen.
800    pub interaction_frequency: f64,
801}
802
803// ── Substitution categories (V14) ──
804
805/// A substitution category (e.g., "databases", "cloud_providers").
806#[derive(Debug, Clone, Serialize, Deserialize)]
807pub struct SubstitutionCategory {
808    pub id: String,
809    pub name: String,
810    pub conflict_mode: String,
811    pub status: String,
812    pub member_count: i64,
813}
814
815/// A member of a substitution category.
816#[derive(Debug, Clone, Serialize, Deserialize)]
817pub struct SubstitutionMember {
818    pub id: String,
819    pub category_name: String,
820    pub token_normalized: String,
821    pub token_display: String,
822    pub confidence: f64,
823    pub source: String,
824    pub status: String,
825}
826
827/// Result of reclassifying a conflict (learning entry point).
828#[derive(Debug, Clone, Serialize, Deserialize)]
829pub struct ReclassifyResult {
830    pub conflict_id: String,
831    pub old_type: String,
832    pub new_type: String,
833    pub learned_members: Vec<LearnedMember>,
834    pub category_created: Option<String>,
835}
836
837/// A member learned during conflict reclassification.
838#[derive(Debug, Clone, Serialize, Deserialize)]
839pub struct LearnedMember {
840    pub token: String,
841    pub category_name: String,
842    pub is_new: bool,
843}
844
845impl Default for PatternConfig {
846    fn default() -> Self {
847        Self {
848            co_occurrence_min_count: 3,
849            temporal_cluster_min_events: 3,
850            valence_trend_delta_threshold: 0.3,
851            topic_cluster_sim_threshold: 0.55,
852            topic_cluster_time_window_days: 30.0,
853            entity_hub_min_degree: 5,
854            max_patterns: 50,
855            cross_domain_candidates_per_domain: 15,
856            cross_domain_sim_threshold: 0.50,
857            cross_domain_max_per_pair: 3,
858            entity_bridge_min_domains: 2,
859            entity_bridge_min_mentions_per_domain: 3,
860            run_cross_domain: true,
861        }
862    }
863}