Skip to main content

mempill_core/
noop.rs

1//! NoOp stub implementations for OraclePort and VectorPort.
2//!
3//! These stubs satisfy their respective trait bounds with do-nothing / empty behavior.
4//! They are provided for:
5//!   - v0.1 testing without a real oracle or vector store.
6//!   - The `DefaultEngine` type alias which wires NoOpOracle + NoOpVector
7//!     as the defaults for host-optional ports.
8//!
9//! DO NOT use these stubs in production. They are clearly doc-commented as test/default stubs.
10
11use mempill_types::{AgentId, AdjudicationRequest, ClaimRef};
12use crate::ports::{OraclePort, VectorPort};
13
14// ── ERROR TYPES ───────────────────────────────────────────────────────────────
15
16/// Infallible error type for NoOp stubs — the stubs never fail.
17#[derive(Debug)]
18pub enum NoOpError {}
19
20impl std::fmt::Display for NoOpError {
21    fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        // Infallible — this variant is unreachable.
23        unreachable!()
24    }
25}
26
27impl std::error::Error for NoOpError {}
28
29// ── NOOP ORACLE ───────────────────────────────────────────────────────────────
30
31/// A no-op oracle that accepts adjudication requests and immediately returns a unit handle.
32///
33/// # Test / Default Stub
34///
35/// - Does NOT surface requests to any external system.
36/// - The returned `()` handle cannot be used to correlate an `AdjudicationResponse`.
37/// - With this oracle present, `oracle_present = true` in the gate Proposal; therefore
38///   conflicting claims route to `QueuedForAdjudication` rather than `Contested`.
39///   No verdict ever arrives, leaving them in `QueuedForAdjudication` indefinitely.
40/// - Suitable for unit tests that don't care about oracle resolution and for the
41///   `DefaultEngine` alias.
42#[derive(Debug, Clone)]
43pub struct NoOpOracle;
44
45impl OraclePort for NoOpOracle {
46    type Error = NoOpError;
47    type Handle = ();
48
49    fn request_adjudication(
50        &self,
51        _agent_id: &AgentId,
52        _request: AdjudicationRequest,
53    ) -> Result<Self::Handle, Self::Error> {
54        // No-op: accept and discard the request.
55        Ok(())
56    }
57
58    /// NoOpOracle never correlates responses, so return a fresh UUID.
59    /// The pending row will be persisted but never resolved (no verdict arrives).
60    fn handle_to_uuid(_handle: &Self::Handle) -> uuid::Uuid {
61        uuid::Uuid::new_v4()
62    }
63}
64
65// ── NOOP VECTOR ───────────────────────────────────────────────────────────────
66
67/// A no-op vector store that discards all embeddings and returns empty search results.
68///
69/// # Test / Default Stub
70///
71/// - `store_embedding` silently discards the vector (no-op).
72/// - `search` always returns an empty `Vec<ClaimRef>`.
73/// - Engine operates in structural-only mode when this stub is used (no fuzzy candidate coverage).
74/// - Matches v0.1 intent: VectorPort is a compile-time seam only; sqlite-vec integration is v0.2.
75#[derive(Debug, Clone)]
76pub struct NoOpVector;
77
78impl VectorPort for NoOpVector {
79    type Error = NoOpError;
80
81    fn store_embedding(
82        &self,
83        _agent_id: &AgentId,
84        _claim_ref: &ClaimRef,
85        _vector: &[f32],
86        _embedding_model_id: &str,
87    ) -> Result<(), Self::Error> {
88        // No-op: discard the embedding.
89        Ok(())
90    }
91
92    fn search(
93        &self,
94        _agent_id: &AgentId,
95        _query_vector: &[f32],
96        _k: usize,
97        _embedding_model_id: &str,
98    ) -> Result<Vec<ClaimRef>, Self::Error> {
99        // No-op: always empty result set.
100        Ok(vec![])
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use mempill_types::{
108        AgentId, AdjudicationRequest, Belief, Claim, ClaimRef, Cardinality, Criticality,
109        Confidence, CurrencySignal, CurrencyState, ExternalAnchor, ExternalKind,
110        Fact, OverturnReason, ProvenanceLabel, SubjectLineRef, TransactionTime, ValidTime,
111    };
112
113    fn make_agent() -> AgentId {
114        AgentId("test-agent".into())
115    }
116
117    fn make_belief() -> Belief {
118        Belief {
119            claim_ref: ClaimRef::new_random(),
120            fact: Fact {
121                subject: "alice".into(),
122                predicate: "age".into(),
123                value: serde_json::json!(30),
124            },
125            provenance: ProvenanceLabel::External(ExternalKind::UserAsserted),
126            valid_time: ValidTime { start: None, end: None, valid_time_confidence: 0.0 },
127            transaction_time: TransactionTime(chrono::Utc::now()),
128            confidence: Confidence { value_confidence: 0.9, valid_time_confidence: 0.0 },
129            currency_signal: CurrencySignal {
130                last_refreshed_at: TransactionTime(chrono::Utc::now()),
131                state: CurrencyState::Fresh,
132                corroboration_count: 0,
133            },
134            criticality: Criticality::Low,
135        }
136    }
137
138    fn make_challenger(agent: &AgentId) -> Claim {
139        Claim::new(
140            ClaimRef::new_random(),
141            agent.clone(),
142            Fact {
143                subject: "alice".into(),
144                predicate: "age".into(),
145                value: serde_json::json!(31),
146            },
147            Cardinality::Functional,
148            ProvenanceLabel::External(ExternalKind::UserAsserted),
149            ExternalAnchor { nearest_external_anchor: None, derivation_depth: 0 },
150            TransactionTime(chrono::Utc::now()),
151            ValidTime { start: None, end: None, valid_time_confidence: 0.0 },
152            Confidence { value_confidence: 0.9, valid_time_confidence: 0.0 },
153            Criticality::Low,
154            vec![],
155            None,
156            None,
157        )
158    }
159
160    #[test]
161    fn noop_oracle_implements_oracle_port_and_returns_ok() {
162        let oracle = NoOpOracle;
163        let agent = make_agent();
164        let request = AdjudicationRequest {
165            subject_line: SubjectLineRef {
166                agent_id: agent.clone(),
167                subject: "alice".into(),
168                predicate: "age".into(),
169            },
170            incumbent: make_belief(),
171            challenger: make_challenger(&agent),
172            criticality: Criticality::Low,
173            reason: OverturnReason::ExternalContradiction,
174        };
175
176        let result = oracle.request_adjudication(&agent, request);
177        assert!(result.is_ok());
178        let _handle: () = result.unwrap();
179    }
180
181    #[test]
182    fn noop_vector_store_embedding_is_noop() {
183        let vector = NoOpVector;
184        let agent = make_agent();
185        let claim_ref = ClaimRef::new_random();
186        let embedding = vec![0.1f32, 0.2, 0.3];
187        let result = vector.store_embedding(&agent, &claim_ref, &embedding, "text-embedding-3-small");
188        assert!(result.is_ok());
189    }
190
191    #[test]
192    fn noop_vector_search_returns_empty() {
193        let vector = NoOpVector;
194        let agent = make_agent();
195        let query = vec![0.1f32, 0.2, 0.3];
196        let result = vector.search(&agent, &query, 10, "text-embedding-3-small");
197        assert!(result.is_ok());
198        assert!(result.unwrap().is_empty());
199    }
200
201    #[test]
202    fn noop_oracle_is_clone_and_debug() {
203        let oracle = NoOpOracle;
204        let _cloned = oracle.clone();
205        let s = format!("{oracle:?}");
206        assert!(s.contains("NoOpOracle"));
207    }
208
209    #[test]
210    fn noop_vector_is_clone_and_debug() {
211        let vector = NoOpVector;
212        let _cloned = vector.clone();
213        let s = format!("{vector:?}");
214        assert!(s.contains("NoOpVector"));
215    }
216
217    #[test]
218    fn noop_oracle_satisfies_trait_bounds() {
219        fn assert_oracle_bounds<T: OraclePort + Send + Sync + 'static>() {}
220        assert_oracle_bounds::<NoOpOracle>();
221    }
222
223    #[test]
224    fn noop_vector_satisfies_trait_bounds() {
225        fn assert_vector_bounds<T: VectorPort + Send + Sync + 'static>() {}
226        assert_vector_bounds::<NoOpVector>();
227    }
228
229    /// NoOpOracle::handle_to_uuid returns a non-nil UUID for the unit handle.
230    /// Two calls must return distinct UUIDs (because each call generates a fresh UUID v4).
231    #[test]
232    fn noop_oracle_handle_to_uuid_returns_non_nil_uuid() {
233        let uuid1 = NoOpOracle::handle_to_uuid(&());
234        let uuid2 = NoOpOracle::handle_to_uuid(&());
235        assert!(!uuid1.is_nil(), "handle_to_uuid must return a non-nil UUID");
236        assert_ne!(uuid1, uuid2, "successive handle_to_uuid calls must return distinct UUIDs");
237    }
238}