Skip to main content

locus_core_rs/domain/
contracts.rs

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