Skip to main content

locus_core_rs/domain/
contracts.rs

1use anyhow::Result;
2use async_trait::async_trait;
3use chrono::{DateTime, Utc};
4
5use std::collections::HashMap;
6
7use crate::domain::models::{
8    AvecState, BatchRekeyResult, ChangeQueryResult, ConnectorMetadata, NodeDeleteRequest,
9    NodeDeleteResult, NodeQuery, NodeUpsertResult, SemanticTagNodeRef, SemanticTagQueryFilter,
10    SemanticTagRecord, SessionPurgeRequest, SttpNode, SyncCheckpoint, SyncCursor, ValidationResult,
11};
12
13/// Storage abstraction for STTP nodes and calibration data.
14///
15/// Implementors are expected to preserve semantics across both in-memory and
16/// persistent backends.
17#[async_trait]
18pub trait NodeStore: Send + Sync {
19    /// Query nodes with optional session and time filters.
20    async fn query_nodes_async(&self, query: NodeQuery) -> Result<Vec<SttpNode>>;
21
22    /// Persist a parsed node and return its storage identifier.
23    async fn store_async(&self, node: SttpNode) -> Result<String> {
24        Ok(self.upsert_node_async(node).await?.node_id)
25    }
26
27    /// Idempotently persist a parsed node using its deterministic sync key.
28    async fn upsert_node_async(&self, node: SttpNode) -> Result<NodeUpsertResult>;
29
30    /// Retrieve nodes ordered by resonance to the provided AVEC state.
31    async fn get_by_resonance_async(
32        &self,
33        session_id: &str,
34        current_avec: AvecState,
35        from_utc: Option<DateTime<Utc>>,
36        to_utc: Option<DateTime<Utc>>,
37        tiers: Option<&[String]>,
38        limit: usize,
39    ) -> Result<Vec<SttpNode>>;
40
41    /// Retrieve nodes ordered by resonance across all sessions.
42    async fn get_by_resonance_global_async(
43        &self,
44        current_avec: AvecState,
45        from_utc: Option<DateTime<Utc>>,
46        to_utc: Option<DateTime<Utc>>,
47        tiers: Option<&[String]>,
48        limit: usize,
49    ) -> Result<Vec<SttpNode>>;
50
51    /// Retrieve nodes using blended AVEC resonance and semantic similarity.
52    ///
53    /// This is additive and backward-compatible with resonance-only callers.
54    /// Implementations should gracefully fall back to AVEC-only ranking when
55    /// embeddings are unavailable.
56    async fn get_by_hybrid_async(
57        &self,
58        session_id: &str,
59        current_avec: AvecState,
60        from_utc: Option<DateTime<Utc>>,
61        to_utc: Option<DateTime<Utc>>,
62        tiers: Option<&[String]>,
63        query_embedding: Option<&[f32]>,
64        alpha: f32,
65        beta: f32,
66        limit: usize,
67    ) -> Result<Vec<SttpNode>> {
68        let _ = (query_embedding, alpha, beta);
69        self.get_by_resonance_async(session_id, current_avec, from_utc, to_utc, tiers, limit)
70            .await
71    }
72
73    /// Retrieve nodes using blended AVEC resonance and semantic similarity across all sessions.
74    async fn get_by_hybrid_global_async(
75        &self,
76        current_avec: AvecState,
77        from_utc: Option<DateTime<Utc>>,
78        to_utc: Option<DateTime<Utc>>,
79        tiers: Option<&[String]>,
80        query_embedding: Option<&[f32]>,
81        alpha: f32,
82        beta: f32,
83        limit: usize,
84    ) -> Result<Vec<SttpNode>> {
85        let _ = (query_embedding, alpha, beta);
86        self.get_by_resonance_global_async(current_avec, from_utc, to_utc, tiers, limit)
87            .await
88    }
89
90    /// List recent nodes with an optional session filter.
91    async fn list_nodes_async(
92        &self,
93        limit: usize,
94        session_id: Option<&str>,
95    ) -> Result<Vec<SttpNode>>;
96
97    /// Read the most recent calibration AVEC for a session.
98    async fn get_last_avec_async(&self, session_id: &str) -> Result<Option<AvecState>>;
99
100    /// Read calibration trigger history for a session.
101    async fn get_trigger_history_async(&self, session_id: &str) -> Result<Vec<String>>;
102
103    /// Store a new calibration measurement for a session.
104    async fn store_calibration_async(
105        &self,
106        session_id: &str,
107        avec: AvecState,
108        trigger: &str,
109    ) -> Result<()>;
110
111    /// Query nodes that changed after the provided cursor.
112    async fn query_changes_since_async(
113        &self,
114        session_id: &str,
115        cursor: Option<SyncCursor>,
116        limit: usize,
117    ) -> Result<ChangeQueryResult>;
118
119    /// Read the last sync checkpoint for a connector within a session scope.
120    async fn get_checkpoint_async(
121        &self,
122        session_id: &str,
123        connector_id: &str,
124    ) -> Result<Option<SyncCheckpoint>>;
125
126    /// Persist the last sync checkpoint for a connector within a session scope.
127    async fn put_checkpoint_async(&self, checkpoint: SyncCheckpoint) -> Result<()>;
128
129    /// Batch-rekey one or more source scopes to a target scope using node IDs as anchors.
130    ///
131    /// Implementations should treat `node_ids` as source-scope anchors and apply scope-wide
132    /// updates across all related tables, not just the anchor records themselves.
133    async fn batch_rekey_scopes_async(
134        &self,
135        node_ids: Vec<String>,
136        target_tenant_id: &str,
137        target_session_id: &str,
138        dry_run: bool,
139        allow_merge: bool,
140    ) -> Result<BatchRekeyResult>;
141
142    /// Delete nodes by sync key and/or Surreal record id within a session scope.
143    async fn delete_nodes_async(&self, request: NodeDeleteRequest) -> Result<NodeDeleteResult>;
144
145    /// Delete all nodes (and optionally calibration/checkpoints) in a session scope.
146    async fn purge_session_async(&self, request: SessionPurgeRequest) -> Result<NodeDeleteResult>;
147}
148
149/// Per-tag embedding payload used when syncing the semantic tag index.
150#[derive(Debug, Clone)]
151pub struct TagEmbedding {
152    pub vector: Vec<f32>,
153    pub model: String,
154}
155
156/// Storage for per-tag vocabulary rows and optional tag embeddings.
157#[async_trait]
158pub trait SemanticIndexStore: Send + Sync {
159    async fn sync_node_tags_async(
160        &self,
161        node_ref: SemanticTagNodeRef,
162        tags: &[String],
163        embeddings: Option<&HashMap<String, TagEmbedding>>,
164    ) -> Result<()>;
165
166    async fn delete_node_tags_async(&self, tenant_id: &str, sync_key: &str) -> Result<()>;
167
168    async fn find_sync_keys_by_tags_async(
169        &self,
170        tenant_id: &str,
171        tags: &[String],
172        match_all: bool,
173        session_id: Option<&str>,
174        limit: usize,
175    ) -> Result<Vec<String>>;
176
177    async fn find_tags_async(
178        &self,
179        tenant_id: &str,
180        prefix: Option<&str>,
181        limit: usize,
182    ) -> Result<Vec<String>>;
183
184    async fn query_tag_records_async(
185        &self,
186        filter: SemanticTagQueryFilter,
187    ) -> Result<Vec<SemanticTagRecord>>;
188}
189
190#[async_trait]
191pub trait SemanticIndexStoreInitializer: Send + Sync {
192    async fn initialize_async(&self) -> Result<()>;
193}
194
195#[async_trait]
196pub trait EmbeddingProvider: Send + Sync {
197    fn model_name(&self) -> &str;
198    async fn embed_async(&self, text: &str) -> Result<Vec<f32>>;
199}
200
201/// One-time initializer contract for a storage backend.
202///
203/// This is typically used for schema creation and migration/backfill hooks.
204#[async_trait]
205pub trait NodeStoreInitializer: Send + Sync {
206    async fn initialize_async(&self) -> Result<()>;
207}
208
209/// Validator contract for raw STTP node payloads.
210pub trait NodeValidator: Send + Sync {
211    /// Validate structural and semantic correctness of raw STTP text.
212    fn validate(&self, raw_node: &str) -> ValidationResult;
213    /// Verify PSI coherence between fields and computed values.
214    fn verify_psi(&self, node: &SttpNode) -> bool;
215}
216
217#[async_trait]
218pub trait SyncChangeSource: Send + Sync {
219    async fn read_changes_async(
220        &self,
221        session_id: &str,
222        connector_id: &str,
223        cursor: Option<SyncCursor>,
224        limit: usize,
225    ) -> Result<ChangeQueryResult>;
226}
227
228pub trait SyncCoordinatorPolicy: Send + Sync {
229    fn should_accept_node(&self, _node: &SttpNode) -> bool {
230        true
231    }
232
233    fn checkpoint_metadata(
234        &self,
235        _session_id: &str,
236        _connector_id: &str,
237        previous: Option<&SyncCheckpoint>,
238        _last_applied_node: Option<&SttpNode>,
239        _next_cursor: Option<&SyncCursor>,
240    ) -> Option<ConnectorMetadata> {
241        previous.and_then(|checkpoint| checkpoint.metadata.clone())
242    }
243}