1#![allow(missing_docs)]
2use std::sync::Arc;
8
9use chrono::{DateTime, Utc};
10
11use crate::{
12 application::ingest_claim::build_latest_disposition_map,
13 config::EngineConfig,
14 engine::{projection, truth_engine},
15 error::MemError,
16 ports::{PersistencePort, VectorPort},
17};
18
19use super::dto::{QueryMemoryRequest, QueryMemoryResponse};
20
21pub struct QueryMemoryUseCase<P, V>
25where
26 P: PersistencePort + Send + Sync + 'static,
27 V: VectorPort + Send + Sync + 'static,
28{
29 persistence: Arc<P>,
30 #[allow(dead_code)]
31 vector: Option<Arc<V>>, config: EngineConfig,
33}
34
35impl<P, V> QueryMemoryUseCase<P, V>
36where
37 P: PersistencePort + Send + Sync + 'static,
38 V: VectorPort + Send + Sync + 'static,
39{
40 pub fn new(persistence: Arc<P>, vector: Option<Arc<V>>, config: EngineConfig) -> Self {
41 Self { persistence, vector, config }
42 }
43
44 pub fn execute_with_time(
48 &self,
49 req: QueryMemoryRequest,
50 now: DateTime<Utc>,
51 ) -> Result<QueryMemoryResponse, MemError> {
52 let as_of = req.as_of_tx_time.unwrap_or(now);
55
56 let claims = self.persistence
61 .load_subject_line(&req.agent_id, &req.subject, &req.predicate, Some(as_of))
62 .map_err(|e| MemError::Persistence { source: Box::new(e) })?;
63
64 let claim_refs: Vec<_> = claims.iter().map(|c| c.claim_ref().clone()).collect();
68 let all_ledger = self.persistence
71 .load_ledger_for_claims(&req.agent_id, &claim_refs, Some(as_of))
72 .map_err(|e| MemError::Persistence { source: Box::new(e) })?;
73 let latest_disposition = build_latest_disposition_map(&all_ledger);
74
75 let fold = truth_engine::fold(
83 claims.clone(),
84 |cref| {
85 self.persistence
86 .load_validity_assertions_for(&req.agent_id, cref)
87 .unwrap_or_default()
88 },
89 as_of,
90 req.valid_at, &self.config,
92 &latest_disposition,
93 );
94
95 let ledger_entries: Vec<_> = claims.iter().flat_map(|c| {
98 all_ledger.iter()
99 .filter(|e| &e.claim_ref == c.claim_ref())
100 .cloned()
101 }).collect();
102
103 let contested = fold.live_claims.iter().any(|cs| {
105 cs.last_disposition
106 .as_ref()
107 .map(|d| *d == mempill_types::Disposition::Contested)
108 .unwrap_or(false)
109 });
110
111 let belief = projection::project(&fold, &ledger_entries, now, &self.config, contested);
113
114 Ok(QueryMemoryResponse { belief })
115 }
116
117 pub fn execute(&self, req: QueryMemoryRequest) -> Result<QueryMemoryResponse, MemError> {
119 self.execute_with_time(req, Utc::now())
120 }
121}
122
123#[cfg(test)]
126mod tests {
127 use super::*;
128 use crate::noop::NoOpVector;
129 use crate::ports::persistence::Txn;
130 use chrono::TimeZone;
131 use mempill_types::{
132 AgentId, BeliefStatus, Cardinality, Claim, ClaimEdge, ClaimRef, Confidence, Criticality,
133 ExternalAnchor, ExternalKind, Fact, LedgerEntry, ProvenanceLabel, TransactionTime,
134 ValidTime, ValidityAssertion,
135 };
136 use std::sync::Mutex;
137
138 struct MockTxn(AgentId);
139 impl Txn for MockTxn {
140 fn agent_id(&self) -> &AgentId { &self.0 }
141 }
142
143 #[derive(Debug, thiserror::Error)]
144 #[error("mock")]
145 struct MockErr;
146
147 #[derive(Default)]
148 struct MockStore {
149 claims: Mutex<Vec<Claim>>,
150 }
151
152 impl PersistencePort for MockStore {
153 type Transaction = MockTxn;
154 type Error = MockErr;
155 fn begin_atomic(&self, aid: &AgentId) -> Result<MockTxn, MockErr> { Ok(MockTxn(aid.clone())) }
156 fn append_claim(&self, _t: &mut MockTxn, c: &Claim) -> Result<ClaimRef, MockErr> {
157 self.claims.lock().unwrap().push(c.clone());
158 Ok(c.claim_ref().clone())
159 }
160 fn append_validity_assertion(&self, _t: &mut MockTxn, _a: &ValidityAssertion) -> Result<(), MockErr> { Ok(()) }
161 fn append_ledger_entry(&self, _t: &mut MockTxn, _e: &LedgerEntry) -> Result<(), MockErr> { Ok(()) }
162 fn append_claim_edge(&self, _t: &mut MockTxn, _e: &ClaimEdge) -> Result<(), MockErr> { Ok(()) }
163 fn commit(&self, _t: MockTxn) -> Result<(), MockErr> { Ok(()) }
164 fn rollback(&self, _t: MockTxn) -> Result<(), MockErr> { Ok(()) }
165 fn load_subject_line(&self, _aid: &AgentId, subject: &str, predicate: &str, _as_of_tx_time: Option<chrono::DateTime<chrono::Utc>>) -> Result<Vec<Claim>, MockErr> {
166 let claims = self.claims.lock().unwrap();
167 Ok(claims.iter()
168 .filter(|c| c.fact().subject == subject && c.fact().predicate == predicate)
169 .cloned()
170 .collect())
171 }
172 fn load_claim(&self, _aid: &AgentId, _r: &ClaimRef) -> Result<Option<Claim>, MockErr> { Ok(None) }
173 fn load_validity_assertions_for(&self, _aid: &AgentId, _r: &ClaimRef) -> Result<Vec<ValidityAssertion>, MockErr> { Ok(vec![]) }
174 fn load_ledger(&self, _aid: &AgentId, _from: Option<&mempill_types::TransactionTime>, _lim: usize) -> Result<Vec<LedgerEntry>, MockErr> { Ok(vec![]) }
175 fn load_ledger_for_claims(&self, _aid: &AgentId, _refs: &[ClaimRef], _as_of: Option<chrono::DateTime<chrono::Utc>>) -> Result<Vec<LedgerEntry>, MockErr> { Ok(vec![]) }
176 fn load_edges_for(&self, _aid: &AgentId, _r: &ClaimRef) -> Result<Vec<ClaimEdge>, MockErr> { Ok(vec![]) }
177 fn load_injected_claims(&self, _aid: &AgentId) -> Result<Vec<ClaimRef>, MockErr> { Ok(vec![]) }
178 fn load_lineage(&self, _aid: &AgentId, _r: &ClaimRef) -> Result<Vec<ClaimEdge>, MockErr> { Ok(vec![]) }
179 fn list_predicates_for_subject(&self, _aid: &AgentId, subject: &str, _as_of: Option<chrono::DateTime<chrono::Utc>>) -> Result<Vec<String>, MockErr> {
180 let claims = self.claims.lock().unwrap();
181 let mut preds: Vec<String> = claims.iter()
182 .filter(|c| c.fact().subject == subject)
183 .map(|c| c.fact().predicate.clone())
184 .collect::<std::collections::HashSet<_>>()
185 .into_iter()
186 .collect();
187 preds.sort();
188 Ok(preds)
189 }
190 }
191
192 fn make_claim(subject: &str, predicate: &str, value: serde_json::Value, tx: DateTime<Utc>) -> Claim {
193 Claim::new(
194 ClaimRef::new_random(),
195 AgentId("agent".into()),
196 Fact { subject: subject.into(), predicate: predicate.into(), value },
197 Cardinality::Functional,
198 ProvenanceLabel::External(ExternalKind::UserAsserted),
199 ExternalAnchor { nearest_external_anchor: None, derivation_depth: 0 },
200 TransactionTime(tx),
201 ValidTime { start: None, end: None, valid_time_confidence: 0.0 , start_granularity: None, end_granularity: None},
202 Confidence { value_confidence: 0.9, valid_time_confidence: 0.0 },
203 Criticality::Medium,
204 vec![],
205 None,
206 None,
207 )
208 }
209
210 #[test]
211 fn query_no_claims_returns_no_belief() {
212 let store = Arc::new(MockStore::default());
213 let uc = QueryMemoryUseCase::new(
214 Arc::clone(&store),
215 None::<Arc<NoOpVector>>,
216 EngineConfig::default(),
217 );
218 let now = Utc::now();
219 let req = QueryMemoryRequest {
220 agent_id: AgentId("agent".into()),
221 subject: "user".into(),
222 predicate: "city".into(),
223 as_of_tx_time: None,
224 valid_at: None,
225 };
226 let resp = uc.execute_with_time(req, now).unwrap();
227 assert_eq!(resp.belief.status, BeliefStatus::NoBelief);
228 }
229
230 #[test]
231 fn query_with_one_claim_returns_resolved() {
232 let store = Arc::new(MockStore::default());
233 let tx = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
234 let claim = make_claim("user", "city", serde_json::json!("Paris"), tx);
235 store.claims.lock().unwrap().push(claim);
236
237 let uc = QueryMemoryUseCase::new(
238 Arc::clone(&store),
239 None::<Arc<NoOpVector>>,
240 EngineConfig::default(),
241 );
242 let now = Utc.with_ymd_and_hms(2026, 1, 2, 0, 0, 0).unwrap();
243 let req = QueryMemoryRequest {
244 agent_id: AgentId("agent".into()),
245 subject: "user".into(),
246 predicate: "city".into(),
247 as_of_tx_time: None,
248 valid_at: None,
249 };
250 let resp = uc.execute_with_time(req, now).unwrap();
251 assert!(
253 matches!(resp.belief.status, BeliefStatus::TimingUncertain | BeliefStatus::Resolved),
254 "expected Resolved or TimingUncertain, got {:?}",
255 resp.belief.status
256 );
257 assert!(resp.belief.primary.is_some(), "primary belief must be present");
258 }
259
260 #[test]
274 fn valid_at_succession_two_windows_selects_correct_ceo_via_public_api() {
275 use crate::application::ingest_claim::IngestClaimUseCase;
276 use crate::noop::NoOpOracle;
277 use chrono::TimeZone;
278 use mempill_types::BeliefStatus;
279
280 #[derive(Default)]
284 struct FullMockStore {
285 claims: Mutex<Vec<Claim>>,
286 ledger: Mutex<Vec<LedgerEntry>>,
287 assertions: Mutex<Vec<ValidityAssertion>>,
288 }
289
290 impl PersistencePort for FullMockStore {
291 type Transaction = MockTxn;
292 type Error = MockErr;
293
294 fn begin_atomic(&self, aid: &AgentId) -> Result<MockTxn, MockErr> {
295 Ok(MockTxn(aid.clone()))
296 }
297 fn append_claim(&self, _t: &mut MockTxn, c: &Claim) -> Result<ClaimRef, MockErr> {
298 self.claims.lock().unwrap().push(c.clone());
299 Ok(c.claim_ref().clone())
300 }
301 fn append_validity_assertion(&self, _t: &mut MockTxn, a: &ValidityAssertion) -> Result<(), MockErr> {
302 self.assertions.lock().unwrap().push(a.clone());
303 Ok(())
304 }
305 fn append_ledger_entry(&self, _t: &mut MockTxn, e: &LedgerEntry) -> Result<(), MockErr> {
306 self.ledger.lock().unwrap().push(e.clone());
307 Ok(())
308 }
309 fn append_claim_edge(&self, _t: &mut MockTxn, _e: &ClaimEdge) -> Result<(), MockErr> { Ok(()) }
310 fn commit(&self, _t: MockTxn) -> Result<(), MockErr> { Ok(()) }
311 fn rollback(&self, _t: MockTxn) -> Result<(), MockErr> { Ok(()) }
312 fn load_subject_line(&self, _aid: &AgentId, subject: &str, predicate: &str, _as_of_tx_time: Option<chrono::DateTime<chrono::Utc>>) -> Result<Vec<Claim>, MockErr> {
313 let claims = self.claims.lock().unwrap();
314 Ok(claims.iter()
315 .filter(|c| c.fact().subject == subject && c.fact().predicate == predicate)
316 .cloned()
317 .collect())
318 }
319 fn load_claim(&self, _aid: &AgentId, r: &ClaimRef) -> Result<Option<Claim>, MockErr> {
320 let claims = self.claims.lock().unwrap();
321 Ok(claims.iter().find(|c| c.claim_ref() == r).cloned())
322 }
323 fn load_validity_assertions_for(&self, _aid: &AgentId, r: &ClaimRef) -> Result<Vec<ValidityAssertion>, MockErr> {
324 let assertions = self.assertions.lock().unwrap();
325 Ok(assertions.iter().filter(|a| &a.target_claim == r).cloned().collect())
326 }
327 fn load_ledger(&self, _aid: &AgentId, _from: Option<&mempill_types::TransactionTime>, _lim: usize) -> Result<Vec<LedgerEntry>, MockErr> {
328 Ok(self.ledger.lock().unwrap().clone())
329 }
330 fn load_ledger_for_claims(&self, _aid: &AgentId, refs: &[ClaimRef], _as_of: Option<chrono::DateTime<chrono::Utc>>) -> Result<Vec<LedgerEntry>, MockErr> {
331 let ledger = self.ledger.lock().unwrap();
332 Ok(ledger.iter().filter(|e| refs.contains(&e.claim_ref)).cloned().collect())
333 }
334 fn load_edges_for(&self, _aid: &AgentId, _r: &ClaimRef) -> Result<Vec<ClaimEdge>, MockErr> { Ok(vec![]) }
335 fn load_injected_claims(&self, _aid: &AgentId) -> Result<Vec<ClaimRef>, MockErr> { Ok(vec![]) }
336 fn load_lineage(&self, _aid: &AgentId, _r: &ClaimRef) -> Result<Vec<ClaimEdge>, MockErr> { Ok(vec![]) }
337 fn list_predicates_for_subject(&self, _aid: &AgentId, subject: &str, _as_of: Option<chrono::DateTime<chrono::Utc>>) -> Result<Vec<String>, MockErr> {
338 let claims = self.claims.lock().unwrap();
339 let mut preds: Vec<String> = claims.iter()
340 .filter(|c| c.fact().subject == subject)
341 .map(|c| c.fact().predicate.clone())
342 .collect::<std::collections::HashSet<_>>()
343 .into_iter()
344 .collect();
345 preds.sort();
346 Ok(preds)
347 }
348 }
349
350 let store = Arc::new(FullMockStore::default());
351
352 let tx_now = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
355
356 let agent = AgentId("diag-agent".into());
357
358 let ingest_uc = IngestClaimUseCase::new(
359 Arc::clone(&store),
360 None::<Arc<NoOpOracle>>,
361 None,
362 EngineConfig::default(),
363 );
364
365 let alice_req = crate::application::dto::IngestClaimRequest {
367 agent_id: agent.clone(),
368 subject: "acme".into(),
369 predicate: "ceo".into(),
370 value: serde_json::json!("alice"),
371 provenance: mempill_types::ProvenanceLabel::External(mempill_types::ExternalKind::UserAsserted),
372 cardinality: Cardinality::Functional,
373 valid_time: Some(ValidTime {
374 start: Some(Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap()),
375 end: Some(Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap()),
376 valid_time_confidence: 0.9,
377 start_granularity: None, end_granularity: None,
378 }),
379 confidence: Confidence { value_confidence: 0.9, valid_time_confidence: 0.9 },
380 criticality: Criticality::Medium,
381 derived_from: vec![],
382 };
383 let alice_resp = ingest_uc.execute_with_time(alice_req, tx_now).unwrap();
384
385 assert_eq!(
387 alice_resp.disposition,
388 mempill_types::Disposition::CommittedCheap,
389 "alice (first write) must be CommittedCheap"
390 );
391
392 let bob_req = crate::application::dto::IngestClaimRequest {
394 agent_id: agent.clone(),
395 subject: "acme".into(),
396 predicate: "ceo".into(),
397 value: serde_json::json!("bob"),
398 provenance: mempill_types::ProvenanceLabel::External(mempill_types::ExternalKind::UserAsserted),
399 cardinality: Cardinality::Functional,
400 valid_time: Some(ValidTime {
401 start: Some(Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap()),
402 end: Some(Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap()),
403 valid_time_confidence: 0.9,
404 start_granularity: None, end_granularity: None,
405 }),
406 confidence: Confidence { value_confidence: 0.9, valid_time_confidence: 0.9 },
407 criticality: Criticality::Medium,
408 derived_from: vec![],
409 };
410 let bob_resp = ingest_uc.execute_with_time(bob_req, tx_now).unwrap();
411
412 assert_eq!(
415 bob_resp.disposition,
416 mempill_types::Disposition::CommittedCheap,
417 "bob (successor, non-overlapping windows) must route to CommittedCheap via Succession gate (not Contested)"
418 );
419
420 {
423 let ledger = store.ledger.lock().unwrap();
424 let alice_dispositions: Vec<_> = ledger.iter()
425 .filter(|e| e.claim_ref == alice_resp.claim_ref)
426 .map(|e| e.disposition.clone())
427 .collect();
428 let latest_alice = alice_dispositions.last().cloned();
431 assert_eq!(
432 latest_alice,
433 Some(mempill_types::Disposition::CommittedCheap),
434 "alice's latest ledger disposition must be CommittedCheap (not Superseded) — \
435 ingest-time supersession was removed; predecessor stays live until oracle affirms"
436 );
437 }
438
439 let query_uc = QueryMemoryUseCase::new(
442 Arc::clone(&store),
443 None::<Arc<NoOpVector>>,
444 EngineConfig::default(),
445 );
446
447 let query_now = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
448
449 let q_alice = QueryMemoryRequest {
450 agent_id: agent.clone(),
451 subject: "acme".into(),
452 predicate: "ceo".into(),
453 as_of_tx_time: None,
454 valid_at: Some(Utc.with_ymd_and_hms(2021, 6, 1, 0, 0, 0).unwrap()),
455 };
456 let resp_alice = query_uc.execute_with_time(q_alice, query_now).unwrap();
457
458 let alice_primary = resp_alice.belief.primary.as_ref();
460 assert_eq!(
461 resp_alice.belief.status,
462 BeliefStatus::Resolved,
463 "valid_at=2021-06-01 (alice's window [2020,2022)) must be Resolved, got {:?}; primary={:?}",
464 resp_alice.belief.status,
465 alice_primary.map(|b| &b.fact.value)
466 );
467 assert_eq!(
468 alice_primary.map(|b| b.fact.value.clone()),
469 Some(serde_json::json!("alice")),
470 "valid_at=2021-06-01 must return alice, got {:?}",
471 alice_primary.map(|b| &b.fact.value)
472 );
473
474 let q_bob = QueryMemoryRequest {
476 agent_id: agent.clone(),
477 subject: "acme".into(),
478 predicate: "ceo".into(),
479 as_of_tx_time: None,
480 valid_at: Some(Utc.with_ymd_and_hms(2023, 6, 1, 0, 0, 0).unwrap()),
481 };
482 let resp_bob = query_uc.execute_with_time(q_bob, query_now).unwrap();
483
484 let bob_primary = resp_bob.belief.primary.as_ref();
485 assert_eq!(
486 resp_bob.belief.status,
487 BeliefStatus::Resolved,
488 "valid_at=2023-06-01 (bob's window [2022,2024)) must be Resolved, got {:?}; primary={:?}",
489 resp_bob.belief.status,
490 bob_primary.map(|b| &b.fact.value)
491 );
492 assert_eq!(
493 bob_primary.map(|b| b.fact.value.clone()),
494 Some(serde_json::json!("bob")),
495 "valid_at=2023-06-01 must return bob, got {:?}",
496 bob_primary.map(|b| &b.fact.value)
497 );
498 }
499
500 #[test]
519 fn oracle_affirm_surfaces_winner_and_excludes_loser_via_public_api() {
520 use crate::application::ingest_claim::IngestClaimUseCase;
521 use crate::application::submit_adjudication::SubmitAdjudicationUseCase;
522 use crate::engine_handle::{ErasedPendingStore, ErasedPendingStoreAdapter};
523 use crate::ports::{PendingAdjudicationPort, PendingAdjudicationRow};
524 use chrono::TimeZone;
525 use mempill_types::{BeliefStatus, Disposition};
526
527 struct TestOracle {
529 fixed_uuid: uuid::Uuid,
530 }
531 impl crate::ports::OraclePort for TestOracle {
532 type Error = crate::noop::NoOpError;
533 type Handle = uuid::Uuid;
534 fn request_adjudication(
535 &self,
536 _aid: &AgentId,
537 _req: mempill_types::AdjudicationRequest,
538 ) -> Result<uuid::Uuid, crate::noop::NoOpError> {
539 Ok(self.fixed_uuid)
540 }
541 fn handle_to_uuid(h: &uuid::Uuid) -> uuid::Uuid { *h }
542 }
543
544 #[derive(Default)]
546 struct MockPending {
547 rows: Mutex<Vec<PendingAdjudicationRow>>,
548 }
549 impl PendingAdjudicationPort for MockPending {
550 type Error = MockErr;
551 fn insert_pending(&self, r: &PendingAdjudicationRow) -> Result<(), MockErr> {
552 self.rows.lock().unwrap().push(r.clone()); Ok(())
553 }
554 fn get_pending(&self, id: uuid::Uuid) -> Result<Option<PendingAdjudicationRow>, MockErr> {
555 Ok(self.rows.lock().unwrap().iter().find(|r| r.handle_id == id).cloned())
556 }
557 fn list_pending(&self, _: Option<&AgentId>) -> Result<Vec<PendingAdjudicationRow>, MockErr> {
558 Ok(self.rows.lock().unwrap().clone())
559 }
560 fn list_expired(&self, _: chrono::DateTime<Utc>) -> Result<Vec<PendingAdjudicationRow>, MockErr> {
561 Ok(vec![])
562 }
563 fn mark_resolved(&self, id: uuid::Uuid) -> Result<(), MockErr> {
564 for r in self.rows.lock().unwrap().iter_mut() {
565 if r.handle_id == id { r.status = "resolved".to_string(); }
566 }
567 Ok(())
568 }
569 fn mark_expired(&self, _: uuid::Uuid) -> Result<(), MockErr> { Ok(()) }
570 fn list_queued_orphan_claims(&self) -> Result<Vec<crate::ports::pending_adjudication::OrphanedQueuedClaim>, MockErr> {
571 Ok(vec![])
572 }
573 }
574
575 #[derive(Default)]
578 struct FullMockStore2 {
579 claims: Mutex<Vec<Claim>>,
580 ledger: Mutex<Vec<LedgerEntry>>,
581 assertions: Mutex<Vec<ValidityAssertion>>,
582 }
583 impl PersistencePort for FullMockStore2 {
584 type Transaction = MockTxn;
585 type Error = MockErr;
586 fn begin_atomic(&self, aid: &AgentId) -> Result<MockTxn, MockErr> { Ok(MockTxn(aid.clone())) }
587 fn append_claim(&self, _t: &mut MockTxn, c: &Claim) -> Result<ClaimRef, MockErr> {
588 self.claims.lock().unwrap().push(c.clone());
589 Ok(c.claim_ref().clone())
590 }
591 fn append_validity_assertion(&self, _t: &mut MockTxn, a: &ValidityAssertion) -> Result<(), MockErr> {
592 self.assertions.lock().unwrap().push(a.clone()); Ok(())
593 }
594 fn append_ledger_entry(&self, _t: &mut MockTxn, e: &LedgerEntry) -> Result<(), MockErr> {
595 self.ledger.lock().unwrap().push(e.clone()); Ok(())
596 }
597 fn append_claim_edge(&self, _t: &mut MockTxn, _e: &ClaimEdge) -> Result<(), MockErr> { Ok(()) }
598 fn commit(&self, _t: MockTxn) -> Result<(), MockErr> { Ok(()) }
599 fn rollback(&self, _t: MockTxn) -> Result<(), MockErr> { Ok(()) }
600 fn load_subject_line(&self, _aid: &AgentId, subject: &str, predicate: &str, _as_of_tx_time: Option<chrono::DateTime<chrono::Utc>>) -> Result<Vec<Claim>, MockErr> {
601 Ok(self.claims.lock().unwrap().iter()
602 .filter(|c| c.fact().subject == subject && c.fact().predicate == predicate)
603 .cloned().collect())
604 }
605 fn load_claim(&self, _aid: &AgentId, r: &ClaimRef) -> Result<Option<Claim>, MockErr> {
606 Ok(self.claims.lock().unwrap().iter().find(|c| c.claim_ref() == r).cloned())
607 }
608 fn load_validity_assertions_for(&self, _aid: &AgentId, r: &ClaimRef) -> Result<Vec<ValidityAssertion>, MockErr> {
609 Ok(self.assertions.lock().unwrap().iter().filter(|a| &a.target_claim == r).cloned().collect())
610 }
611 fn load_ledger(&self, _aid: &AgentId, _from: Option<&mempill_types::TransactionTime>, _lim: usize) -> Result<Vec<LedgerEntry>, MockErr> {
612 Ok(self.ledger.lock().unwrap().clone())
613 }
614 fn load_ledger_for_claims(&self, _aid: &AgentId, refs: &[ClaimRef], as_of: Option<chrono::DateTime<chrono::Utc>>) -> Result<Vec<LedgerEntry>, MockErr> {
615 Ok(self.ledger.lock().unwrap().iter()
616 .filter(|e| refs.contains(&e.claim_ref))
617 .filter(|e| as_of.is_none_or(|t| e.recorded_at.0 <= t))
618 .cloned()
619 .collect())
620 }
621 fn load_edges_for(&self, _aid: &AgentId, _r: &ClaimRef) -> Result<Vec<ClaimEdge>, MockErr> { Ok(vec![]) }
622 fn load_injected_claims(&self, _aid: &AgentId) -> Result<Vec<ClaimRef>, MockErr> { Ok(vec![]) }
623 fn load_lineage(&self, _aid: &AgentId, _r: &ClaimRef) -> Result<Vec<ClaimEdge>, MockErr> { Ok(vec![]) }
624 fn list_predicates_for_subject(&self, _aid: &AgentId, subject: &str, as_of: Option<chrono::DateTime<chrono::Utc>>) -> Result<Vec<String>, MockErr> {
625 let claims = self.claims.lock().unwrap();
626 let mut preds: Vec<String> = claims.iter()
627 .filter(|c| c.fact().subject == subject)
628 .filter(|c| as_of.is_none_or(|t| c.transaction_time().0 <= t))
629 .map(|c| c.fact().predicate.clone())
630 .collect::<std::collections::HashSet<_>>()
631 .into_iter()
632 .collect();
633 preds.sort();
634 Ok(preds)
635 }
636 }
637
638 let store = Arc::new(FullMockStore2::default());
639 let pending = Arc::new(MockPending::default());
640 let handle_uuid = uuid::Uuid::new_v4();
641 let oracle = Arc::new(TestOracle { fixed_uuid: handle_uuid });
642
643 let erased_pending: Arc<dyn ErasedPendingStore> = {
645 struct Delegate(Arc<MockPending>);
646 impl PendingAdjudicationPort for Delegate {
647 type Error = MockErr;
648 fn insert_pending(&self, r: &PendingAdjudicationRow) -> Result<(), MockErr> { self.0.insert_pending(r) }
649 fn get_pending(&self, id: uuid::Uuid) -> Result<Option<PendingAdjudicationRow>, MockErr> { self.0.get_pending(id) }
650 fn list_pending(&self, a: Option<&AgentId>) -> Result<Vec<PendingAdjudicationRow>, MockErr> { self.0.list_pending(a) }
651 fn list_expired(&self, n: chrono::DateTime<Utc>) -> Result<Vec<PendingAdjudicationRow>, MockErr> { self.0.list_expired(n) }
652 fn mark_resolved(&self, id: uuid::Uuid) -> Result<(), MockErr> { self.0.mark_resolved(id) }
653 fn mark_expired(&self, id: uuid::Uuid) -> Result<(), MockErr> { self.0.mark_expired(id) }
654 fn list_queued_orphan_claims(&self) -> Result<Vec<crate::ports::pending_adjudication::OrphanedQueuedClaim>, MockErr> { self.0.list_queued_orphan_claims() }
655 }
656 Arc::new(ErasedPendingStoreAdapter::new(Delegate(Arc::clone(&pending))))
657 };
658
659 let agent = AgentId("oracle-vt-agent".into());
660 let ingest_now = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
661
662 let ingest_uc = IngestClaimUseCase::new(
663 Arc::clone(&store),
664 Some(Arc::clone(&oracle)),
665 Some(Arc::clone(&erased_pending)),
666 EngineConfig::default(),
667 );
668
669 let alice_req = crate::application::dto::IngestClaimRequest {
671 agent_id: agent.clone(),
672 subject: "acme".into(),
673 predicate: "ceo".into(),
674 value: serde_json::json!("alice"),
675 provenance: mempill_types::ProvenanceLabel::External(mempill_types::ExternalKind::UserAsserted),
676 cardinality: Cardinality::Functional,
677 valid_time: None, confidence: Confidence { value_confidence: 0.9, valid_time_confidence: 0.0 },
679 criticality: Criticality::Medium,
680 derived_from: vec![],
681 };
682 let alice_resp = ingest_uc.execute_with_time(alice_req, ingest_now).unwrap();
683 assert_eq!(alice_resp.disposition, Disposition::CommittedCheap,
684 "alice (first write, no conflict) must be CommittedCheap");
685 let alice_ref = alice_resp.claim_ref.clone();
686
687 let bob_req = crate::application::dto::IngestClaimRequest {
689 agent_id: agent.clone(),
690 subject: "acme".into(),
691 predicate: "ceo".into(),
692 value: serde_json::json!("bob"),
693 provenance: mempill_types::ProvenanceLabel::External(mempill_types::ExternalKind::UserAsserted),
694 cardinality: Cardinality::Functional,
695 valid_time: None,
696 confidence: Confidence { value_confidence: 0.9, valid_time_confidence: 0.0 },
697 criticality: Criticality::Medium,
698 derived_from: vec![],
699 };
700 let bob_resp = ingest_uc.execute_with_time(bob_req, ingest_now).unwrap();
701 assert_eq!(bob_resp.disposition, Disposition::QueuedForAdjudication,
702 "bob (conflict, oracle present) must be QueuedForAdjudication");
703 let bob_ref = bob_resp.claim_ref.clone();
704
705 let affirm_now = Utc.with_ymd_and_hms(2025, 6, 1, 0, 0, 0).unwrap();
708 let submit_uc = SubmitAdjudicationUseCase::new(
709 Arc::clone(&store),
710 Arc::clone(&erased_pending),
711 );
712 let adj_response = mempill_types::AdjudicationResponse {
713 handle_id: handle_uuid,
714 verdict: mempill_types::AdjudicationVerdict::Affirm,
715 evidence_provenance: mempill_types::ProvenanceLabel::External(
716 mempill_types::ExternalKind::ExternalFirstHand,
717 ),
718 };
719 let outcome = submit_uc.execute(handle_uuid, adj_response, affirm_now).unwrap();
720 assert_eq!(outcome.disposition, Disposition::CommittedCheap,
721 "Affirm outcome must be CommittedCheap for the winner (bob)");
722 assert_eq!(outcome.claim_ref, bob_ref,
723 "Affirm outcome claim_ref must be bob (the challenger/winner)");
724
725 {
727 let ledger = store.ledger.lock().unwrap();
728 let alice_latest = ledger.iter()
729 .filter(|e| e.claim_ref == alice_ref)
730 .max_by_key(|e| e.recorded_at.0)
731 .map(|e| e.disposition.clone());
732 assert_eq!(alice_latest, Some(Disposition::Superseded),
733 "alice's latest ledger entry must be Superseded after Affirm");
734 }
735
736 let query_uc = QueryMemoryUseCase::new(
741 Arc::clone(&store),
742 None::<Arc<crate::noop::NoOpVector>>,
743 EngineConfig::default(),
744 );
745 let query_now = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
746 let q_now = crate::application::dto::QueryMemoryRequest {
747 agent_id: agent.clone(),
748 subject: "acme".into(),
749 predicate: "ceo".into(),
750 as_of_tx_time: None,
751 valid_at: None,
752 };
753 let resp_now = query_uc.execute_with_time(q_now, query_now).unwrap();
754 let now_primary = resp_now.belief.primary.as_ref()
755 .map(|b| b.fact.value.clone());
756 assert!(
760 matches!(resp_now.belief.status, BeliefStatus::TimingUncertain | BeliefStatus::Resolved),
761 "as_of=now after Affirm must surface bob (TimingUncertain or Resolved); got {:?}",
762 resp_now.belief.status
763 );
764 assert_eq!(
765 now_primary,
766 Some(serde_json::json!("bob")),
767 "as_of=now after Affirm must return bob (CommittedCheap winner); got {now_primary:?}"
768 );
769
770 let before_affirm = Utc.with_ymd_and_hms(2025, 3, 1, 0, 0, 0).unwrap(); let q_before_affirm = crate::application::dto::QueryMemoryRequest {
783 agent_id: agent.clone(),
784 subject: "acme".into(),
785 predicate: "ceo".into(),
786 as_of_tx_time: Some(before_affirm),
787 valid_at: None,
788 };
789 let resp_before = query_uc.execute_with_time(q_before_affirm, query_now).unwrap();
790 assert_eq!(
793 resp_before.belief.status,
794 BeliefStatus::Contested,
795 "as_of=before_affirm: bi-temporal tx-time rewind must return Contested \
796 (both alice and bob live before affirm). Got {:?}",
797 resp_before.belief.status
798 );
799 }
800
801 #[test]
802 fn query_now_injected_not_read_from_clock() {
803 let store = Arc::new(MockStore::default());
807 let old_tx = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
809 let claim = make_claim("user", "job", serde_json::json!("Engineer"), old_tx);
810 store.claims.lock().unwrap().push(claim);
811
812 let uc = QueryMemoryUseCase::new(
813 Arc::clone(&store),
814 None::<Arc<NoOpVector>>,
815 EngineConfig::default(),
816 );
817
818 let near_now = Utc.with_ymd_and_hms(2020, 1, 2, 0, 0, 0).unwrap(); let req = QueryMemoryRequest {
821 agent_id: AgentId("agent".into()),
822 subject: "user".into(),
823 predicate: "job".into(),
824 as_of_tx_time: None,
825 valid_at: None,
826 };
827 let resp_near = uc.execute_with_time(req.clone(), near_now).unwrap();
828 assert!(resp_near.belief.primary.is_some());
829 assert_eq!(
830 resp_near.belief.primary.unwrap().currency_signal.state,
831 mempill_types::CurrencyState::Fresh,
832 "1 day after claim, currency must be Fresh (injected now, not system clock)"
833 );
834
835 let far_now = Utc.with_ymd_and_hms(2020, 7, 20, 0, 0, 0).unwrap(); let resp_far = uc.execute_with_time(req, far_now).unwrap();
838 assert!(resp_far.belief.primary.is_some());
839 assert_eq!(
840 resp_far.belief.primary.unwrap().currency_signal.state,
841 mempill_types::CurrencyState::Decayed,
842 "200 days after claim, currency must be Decayed (injected now, not system clock)"
843 );
844 }
845}