1#![allow(missing_docs)]
2use std::sync::Arc;
21
22use chrono::{DateTime, Utc};
23use mempill_types::{Claim, ClaimRef, HistoryEntryStatus, ProvenanceLabel, ExternalKind};
24
25use crate::{
26 application::ingest_claim::build_latest_disposition_map,
27 config::EngineConfig,
28 engine::truth_engine,
29 error::MemError,
30 ports::{PersistencePort, VectorPort},
31};
32
33use super::dto::{HistoryEntry, QueryHistoryRequest, QueryHistoryResponse};
34
35fn ordering_key_dt(claim: &Claim, config: &EngineConfig) -> DateTime<Utc> {
40 if claim.valid_time().valid_time_confidence >= config.valid_time_confidence_threshold {
41 claim.valid_time().start.unwrap_or(claim.transaction_time().0)
42 } else {
43 claim.transaction_time().0
44 }
45}
46
47fn format_provenance(p: &ProvenanceLabel) -> String {
52 match p {
53 ProvenanceLabel::External(ExternalKind::UserAsserted) => {
54 "External/UserAsserted".to_owned()
55 }
56 ProvenanceLabel::External(ExternalKind::ExternalFirstHand) => {
57 "External/ExternalFirstHand".to_owned()
58 }
59 ProvenanceLabel::RecallReEntry => "RecallReEntry".to_owned(),
60 ProvenanceLabel::ModelDerived => "ModelDerived".to_owned(),
61 _ => format!("{p:?}"),
62 }
63}
64
65pub fn compute_effective_windows(
76 sorted: &[&Claim],
77 config: &EngineConfig,
78) -> Vec<Option<DateTime<Utc>>> {
79 let n = sorted.len();
80 let mut windows = Vec::with_capacity(n);
81 for i in 0..n {
82 if i + 1 < n {
83 windows.push(Some(ordering_key_dt(sorted[i + 1], config)));
85 } else {
86 windows.push(None);
88 }
89 }
90 windows
91}
92
93pub struct QueryHistoryUseCase<P, V>
99where
100 P: PersistencePort + Send + Sync + 'static,
101 V: VectorPort + Send + Sync + 'static,
102{
103 persistence: Arc<P>,
104 #[allow(dead_code)]
105 vector: Option<Arc<V>>,
106 config: EngineConfig,
107}
108
109impl<P, V> QueryHistoryUseCase<P, V>
110where
111 P: PersistencePort + Send + Sync + 'static,
112 V: VectorPort + Send + Sync + 'static,
113{
114 pub fn new(persistence: Arc<P>, vector: Option<Arc<V>>, config: EngineConfig) -> Self {
115 Self { persistence, vector, config }
116 }
117
118 pub fn execute_with_time(
122 &self,
123 req: QueryHistoryRequest,
124 now: DateTime<Utc>,
125 ) -> Result<QueryHistoryResponse, MemError> {
126 let claims = self.persistence
131 .load_subject_line(&req.agent_id, &req.subject, &req.predicate, None)
132 .map_err(|e| MemError::Persistence { source: Box::new(e) })?;
133
134 if claims.is_empty() {
135 return Ok(QueryHistoryResponse { entries: vec![] });
136 }
137
138 let claim_refs: Vec<_> = claims.iter().map(|c| c.claim_ref().clone()).collect();
141 let all_ledger = self.persistence
145 .load_ledger_for_claims(&req.agent_id, &claim_refs, None)
146 .map_err(|e| MemError::Persistence { source: Box::new(e) })?;
147 let latest_disposition = build_latest_disposition_map(&all_ledger);
148
149 let fold = truth_engine::fold(
151 claims.clone(),
152 |cref| {
153 self.persistence
154 .load_validity_assertions_for(&req.agent_id, cref)
155 .unwrap_or_default()
156 },
157 now,
158 None, &self.config,
160 &latest_disposition,
161 );
162
163 let live_refs: std::collections::HashSet<&ClaimRef> = fold
165 .live_claims
166 .iter()
167 .map(|cs| cs.claim.claim_ref())
168 .collect();
169
170 let mut sorted_claims = claims;
172 sorted_claims.sort_by(|a, b| {
173 let ka = ordering_key_dt(a, &self.config);
174 let kb = ordering_key_dt(b, &self.config);
175 ka.cmp(&kb)
176 .then(a.transaction_time().0.cmp(&b.transaction_time().0))
177 .then(a.claim_ref().0.as_u128().cmp(&b.claim_ref().0.as_u128()))
178 });
179
180 let refs: Vec<&Claim> = sorted_claims.iter().collect();
182 let windows = compute_effective_windows(&refs, &self.config);
183
184 let entries: Vec<HistoryEntry> = sorted_claims
186 .iter()
187 .zip(windows)
188 .map(|(claim, valid_until)| {
189 let status = if live_refs.contains(claim.claim_ref()) {
190 HistoryEntryStatus::Current
191 } else {
192 HistoryEntryStatus::Superseded
193 };
194 HistoryEntry {
195 claim_ref: claim.claim_ref().clone(),
196 value: claim.fact().value.clone(),
197 valid_from: claim.valid_time().start,
198 valid_until,
199 status,
200 provenance: format_provenance(claim.provenance()),
201 value_confidence: claim.confidence().value_confidence,
202 }
203 })
204 .collect();
205
206 Ok(QueryHistoryResponse { entries })
207 }
208
209 pub fn execute(&self, req: QueryHistoryRequest) -> Result<QueryHistoryResponse, MemError> {
211 self.execute_with_time(req, Utc::now())
212 }
213}
214
215#[cfg(test)]
218mod tests {
219 use super::*;
220 use crate::config::EngineConfig;
221 use crate::noop::NoOpVector;
222 use crate::ports::persistence::Txn;
223 use chrono::TimeZone;
224 use mempill_types::{
225 AgentId, Cardinality, Claim, ClaimEdge, ClaimRef, Confidence, Criticality,
226 ExternalAnchor, ExternalKind, Fact, LedgerEntry, ProvenanceLabel, TransactionTime,
227 ValidTime, ValidityAssertion,
228 };
229 use std::sync::Mutex;
230
231 struct MockTxn(AgentId);
234 impl Txn for MockTxn {
235 fn agent_id(&self) -> &AgentId { &self.0 }
236 }
237
238 #[derive(Debug, thiserror::Error)]
239 #[error("mock")]
240 struct MockErr;
241
242 #[derive(Default)]
243 struct MockStore {
244 claims: Mutex<Vec<Claim>>,
245 assertions: Mutex<Vec<ValidityAssertion>>,
246 }
247
248 impl PersistencePort for MockStore {
249 type Transaction = MockTxn;
250 type Error = MockErr;
251 fn begin_atomic(&self, aid: &AgentId) -> Result<MockTxn, MockErr> {
252 Ok(MockTxn(aid.clone()))
253 }
254 fn append_claim(&self, _t: &mut MockTxn, c: &Claim) -> Result<ClaimRef, MockErr> {
255 self.claims.lock().unwrap().push(c.clone());
256 Ok(c.claim_ref().clone())
257 }
258 fn append_validity_assertion(
259 &self,
260 _t: &mut MockTxn,
261 a: &ValidityAssertion,
262 ) -> Result<(), MockErr> {
263 self.assertions.lock().unwrap().push(a.clone());
264 Ok(())
265 }
266 fn append_ledger_entry(
267 &self,
268 _t: &mut MockTxn,
269 _e: &LedgerEntry,
270 ) -> Result<(), MockErr> {
271 Ok(())
272 }
273 fn append_claim_edge(
274 &self,
275 _t: &mut MockTxn,
276 _e: &ClaimEdge,
277 ) -> Result<(), MockErr> {
278 Ok(())
279 }
280 fn commit(&self, _t: MockTxn) -> Result<(), MockErr> { Ok(()) }
281 fn rollback(&self, _t: MockTxn) -> Result<(), MockErr> { Ok(()) }
282 fn load_subject_line(
283 &self,
284 _aid: &AgentId,
285 subject: &str,
286 predicate: &str,
287 _as_of_tx_time: Option<chrono::DateTime<chrono::Utc>>,
288 ) -> Result<Vec<Claim>, MockErr> {
289 let claims = self.claims.lock().unwrap();
290 Ok(claims
291 .iter()
292 .filter(|c| {
293 c.fact().subject == subject && c.fact().predicate == predicate
294 })
295 .cloned()
296 .collect())
297 }
298 fn load_claim(
299 &self,
300 _aid: &AgentId,
301 r: &ClaimRef,
302 ) -> Result<Option<Claim>, MockErr> {
303 let claims = self.claims.lock().unwrap();
304 Ok(claims.iter().find(|c| c.claim_ref() == r).cloned())
305 }
306 fn load_validity_assertions_for(
307 &self,
308 _aid: &AgentId,
309 r: &ClaimRef,
310 ) -> Result<Vec<ValidityAssertion>, MockErr> {
311 let assertions = self.assertions.lock().unwrap();
312 Ok(assertions
313 .iter()
314 .filter(|a| &a.target_claim == r)
315 .cloned()
316 .collect())
317 }
318 fn load_ledger(
319 &self,
320 _aid: &AgentId,
321 _from: Option<&mempill_types::TransactionTime>,
322 _lim: usize,
323 ) -> Result<Vec<LedgerEntry>, MockErr> {
324 Ok(vec![])
325 }
326 fn load_ledger_for_claims(
327 &self,
328 _aid: &AgentId,
329 _refs: &[ClaimRef],
330 _as_of: Option<chrono::DateTime<chrono::Utc>>,
331 ) -> Result<Vec<LedgerEntry>, MockErr> {
332 Ok(vec![])
333 }
334 fn load_edges_for(
335 &self,
336 _aid: &AgentId,
337 _r: &ClaimRef,
338 ) -> Result<Vec<ClaimEdge>, MockErr> {
339 Ok(vec![])
340 }
341 fn load_injected_claims(
342 &self,
343 _aid: &AgentId,
344 ) -> Result<Vec<ClaimRef>, MockErr> {
345 Ok(vec![])
346 }
347 fn load_lineage(
348 &self,
349 _aid: &AgentId,
350 _r: &ClaimRef,
351 ) -> Result<Vec<ClaimEdge>, MockErr> {
352 Ok(vec![])
353 }
354 fn list_predicates_for_subject(&self, _aid: &AgentId, _s: &str, _as_of: Option<chrono::DateTime<chrono::Utc>>) -> Result<Vec<String>, MockErr> { Ok(vec![]) }
355 }
356
357 fn agent() -> AgentId {
360 AgentId("test-agent".into())
361 }
362
363 #[allow(clippy::too_many_arguments)]
364 fn make_claim(
366 agent_id: &AgentId,
367 subject: &str,
368 predicate: &str,
369 value: serde_json::Value,
370 tx: DateTime<Utc>,
371 vt_start: Option<DateTime<Utc>>,
372 vt_end: Option<DateTime<Utc>>,
373 vt_confidence: f32,
374 ) -> Claim {
375 Claim::new(
376 ClaimRef::new_random(),
377 agent_id.clone(),
378 Fact { subject: subject.into(), predicate: predicate.into(), value },
379 Cardinality::Functional,
380 ProvenanceLabel::External(ExternalKind::UserAsserted),
381 ExternalAnchor { nearest_external_anchor: None, derivation_depth: 0 },
382 TransactionTime(tx),
383 ValidTime { start: vt_start, end: vt_end, valid_time_confidence: vt_confidence , start_granularity: None, end_granularity: None},
384 Confidence { value_confidence: 0.9, valid_time_confidence: vt_confidence },
385 Criticality::Medium,
386 vec![],
387 None,
388 None,
389 )
390 }
391
392 fn uc(store: Arc<MockStore>) -> QueryHistoryUseCase<MockStore, NoOpVector> {
393 QueryHistoryUseCase::new(store, None::<Arc<NoOpVector>>, EngineConfig::default())
394 }
395
396 #[test]
399 fn empty_subject_line_returns_empty_entries() {
400 let store = Arc::new(MockStore::default());
401 let uc = uc(Arc::clone(&store));
402 let now = Utc::now();
403 let req = QueryHistoryRequest {
404 agent_id: agent(),
405 subject: "nobody".into(),
406 predicate: "nothing".into(),
407 };
408 let resp = uc.execute_with_time(req, now).unwrap();
409 assert!(resp.entries.is_empty(), "no claims → empty history");
410 assert!(resp.current().is_none(), "no current entry");
411 }
412
413 #[test]
416 fn single_claim_returns_one_current_entry() {
417 let store = Arc::new(MockStore::default());
418 let agent = agent();
419 let tx = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
420 let claim = make_claim(&agent, "acme", "ceo", serde_json::json!("Alice"), tx, None, None, 0.0);
421 store.claims.lock().unwrap().push(claim.clone());
422
423 let uc = uc(Arc::clone(&store));
424 let now = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
425 let resp = uc.execute_with_time(
426 QueryHistoryRequest { agent_id: agent, subject: "acme".into(), predicate: "ceo".into() },
427 now,
428 ).unwrap();
429
430 assert_eq!(resp.entries.len(), 1, "one claim → one entry");
431 assert_eq!(resp.entries[0].status, HistoryEntryStatus::Current);
432 assert_eq!(resp.entries[0].value, serde_json::json!("Alice"));
433 assert!(resp.entries[0].valid_until.is_none(), "single entry has no successor → open-ended");
434 }
435
436 #[test]
439 fn succession_ordering_oldest_first() {
440 let store = Arc::new(MockStore::default());
441 let agent = agent();
442 let t1 = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
443 let t2 = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
444
445 let claim2 = make_claim(&agent, "acme", "ceo", serde_json::json!("Bob"), t2, None, None, 0.0);
447 let claim1 = make_claim(&agent, "acme", "ceo", serde_json::json!("Alice"), t1, None, None, 0.0);
448 store.claims.lock().unwrap().push(claim2);
449 store.claims.lock().unwrap().push(claim1);
450
451 let uc = uc(Arc::clone(&store));
452 let now = Utc.with_ymd_and_hms(2025, 6, 1, 0, 0, 0).unwrap();
453 let resp = uc.execute_with_time(
454 QueryHistoryRequest { agent_id: agent, subject: "acme".into(), predicate: "ceo".into() },
455 now,
456 ).unwrap();
457
458 assert_eq!(resp.entries.len(), 2);
459 assert_eq!(resp.entries[0].value, serde_json::json!("Alice"), "oldest first");
460 assert_eq!(resp.entries[1].value, serde_json::json!("Bob"), "newer second");
461 }
462
463 #[test]
466 fn effective_window_successor_closes_prior_entry() {
467 let config = EngineConfig::default();
468 let agent = agent();
469 let t1 = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
470 let t2 = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
471
472 let c1 = make_claim(&agent, "a", "b", serde_json::json!("v1"), t1, None, None, 0.0);
473 let c2 = make_claim(&agent, "a", "b", serde_json::json!("v2"), t2, None, None, 0.0);
474
475 let sorted: Vec<&Claim> = vec![&c1, &c2];
476 let windows = compute_effective_windows(&sorted, &config);
477
478 assert_eq!(windows[0], Some(t2), "c1 closed by c2's ordering key");
480 assert_eq!(windows[1], None, "last entry is open-ended");
482 }
483
484 #[test]
487 fn current_entry_value_matches_recall_primary() {
488 let store = Arc::new(MockStore::default());
489 let agent = agent();
490 let t1 = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
491 let t2 = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
492
493 let c1 = make_claim(&agent, "acme", "ceo", serde_json::json!("Alice"), t1, None, None, 0.0);
494 let c2 = make_claim(&agent, "acme", "ceo", serde_json::json!("Bob"), t2, None, None, 0.0);
495 store.claims.lock().unwrap().push(c1);
496 store.claims.lock().unwrap().push(c2);
497
498 let uc = uc(Arc::clone(&store));
499 let now = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
500 let resp = uc.execute_with_time(
501 QueryHistoryRequest { agent_id: agent, subject: "acme".into(), predicate: "ceo".into() },
502 now,
503 ).unwrap();
504
505 let current_entries: Vec<_> = resp.entries.iter().filter(|e| e.status == HistoryEntryStatus::Current).collect();
508 assert!(!current_entries.is_empty(), "at least one Current entry must exist");
509 }
510
511 #[test]
514 fn high_confidence_ordering_key_uses_valid_time_start() {
515 let config = EngineConfig::default(); let agent = agent();
517
518 let tx_late = Utc.with_ymd_and_hms(2024, 6, 1, 0, 0, 0).unwrap();
520 let vt_early = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
521 let claim_a = make_claim(&agent, "x", "y", serde_json::json!("A"), tx_late, Some(vt_early), None, 0.9);
522
523 let tx_early = Utc.with_ymd_and_hms(2023, 1, 1, 0, 0, 0).unwrap();
525 let claim_b = make_claim(&agent, "x", "y", serde_json::json!("B"), tx_early, None, None, 0.0);
526
527 let key_a = ordering_key_dt(&claim_a, &config);
529 let key_b = ordering_key_dt(&claim_b, &config);
530 assert!(key_a < key_b, "high-confidence A (vt=2020) must precede B (tx=2023)");
531
532 let sorted: Vec<&Claim> = vec![&claim_a, &claim_b];
534 let windows = compute_effective_windows(&sorted, &config);
535 assert_eq!(windows[0], Some(key_b), "A's valid_until = B's ordering key");
536 assert_eq!(windows[1], None, "B is last → open-ended");
537 }
538
539 #[test]
542 fn all_claims_bounded_returns_all_superseded() {
543 use mempill_types::{AssertionKind, ValidityAssertion};
544 use uuid::Uuid;
545
546 let store = Arc::new(MockStore::default());
547 let agent = agent();
548 let tx = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
549 let bound_at = Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap();
550
551 let claim = make_claim(&agent, "acme", "ceo", serde_json::json!("Alice"), tx, None, None, 0.0);
552 let claim_ref = claim.claim_ref().clone();
553
554 let assertion = ValidityAssertion {
555 assertion_ref: Uuid::new_v4(),
556 agent_id: agent.clone(),
557 target_claim: claim_ref.clone(),
558 kind: AssertionKind::Bound { bound_at },
559 provenance: ProvenanceLabel::External(ExternalKind::UserAsserted),
560 confidence: Confidence { value_confidence: 1.0, valid_time_confidence: 1.0 },
561 asserted_at: TransactionTime(bound_at),
562 };
563
564 store.claims.lock().unwrap().push(claim);
565 store.assertions.lock().unwrap().push(assertion);
566
567 let uc = uc(Arc::clone(&store));
568 let now = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
570 let resp = uc.execute_with_time(
571 QueryHistoryRequest { agent_id: agent, subject: "acme".into(), predicate: "ceo".into() },
572 now,
573 ).unwrap();
574
575 assert_eq!(resp.entries.len(), 1, "one claim in history");
576 assert_eq!(
577 resp.entries[0].status,
578 HistoryEntryStatus::Superseded,
579 "bounded claim must be Superseded"
580 );
581 assert!(resp.current().is_none(), "no current entry when all claims are bounded");
582 }
583
584 #[test]
587 fn compute_effective_windows_empty() {
588 let config = EngineConfig::default();
589 let windows = compute_effective_windows(&[], &config);
590 assert!(windows.is_empty());
591 }
592
593 #[test]
594 fn compute_effective_windows_single() {
595 let config = EngineConfig::default();
596 let agent = agent();
597 let tx = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
598 let c = make_claim(&agent, "a", "b", serde_json::json!("v"), tx, None, None, 0.0);
599 let sorted = vec![&c];
600 let windows = compute_effective_windows(&sorted, &config);
601 assert_eq!(windows.len(), 1);
602 assert_eq!(windows[0], None, "single claim → open-ended");
603 }
604
605 #[test]
606 fn compute_effective_windows_three_entries() {
607 let config = EngineConfig::default();
608 let agent = agent();
609 let t1 = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
610 let t2 = Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap();
611 let t3 = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
612
613 let c1 = make_claim(&agent, "a", "b", serde_json::json!("v1"), t1, None, None, 0.0);
614 let c2 = make_claim(&agent, "a", "b", serde_json::json!("v2"), t2, None, None, 0.0);
615 let c3 = make_claim(&agent, "a", "b", serde_json::json!("v3"), t3, None, None, 0.0);
616
617 let sorted = vec![&c1, &c2, &c3];
618 let windows = compute_effective_windows(&sorted, &config);
619
620 assert_eq!(windows.len(), 3);
621 assert_eq!(windows[0], Some(t2));
623 assert_eq!(windows[1], Some(t3));
624 assert_eq!(windows[2], None);
625 }
626
627 #[test]
630 fn provenance_formatted_correctly_in_entry() {
631 let store = Arc::new(MockStore::default());
632 let agent = agent();
633 let tx = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
634 let claim = make_claim(&agent, "acme", "ceo", serde_json::json!("Alice"), tx, None, None, 0.0);
635 store.claims.lock().unwrap().push(claim);
636
637 let uc = uc(Arc::clone(&store));
638 let now = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
639 let resp = uc.execute_with_time(
640 QueryHistoryRequest { agent_id: agent, subject: "acme".into(), predicate: "ceo".into() },
641 now,
642 ).unwrap();
643
644 assert_eq!(resp.entries[0].provenance, "External/UserAsserted");
645 }
646}