mempill_core/ports/persistence.rs
1#![allow(missing_docs)]
2//! PersistencePort — INSERT-only, agent_id-first persistence abstraction.
3//!
4//! All methods take `agent_id` as the primary parameter (not a filter).
5//! Must enforce: single-writer per agent_id; append-only; atomic commit unit.
6
7use mempill_types::{
8 AgentId, Claim, ClaimEdge, ClaimRef, LedgerEntry, TransactionTime, ValidityAssertion,
9};
10
11/// An opaque transaction handle scoped to exactly one agent_id.
12/// No cross-agent transaction is possible (atomic commit unit is per-agent_id).
13pub trait Txn: Send + 'static {
14 fn agent_id(&self) -> &AgentId;
15}
16
17/// The persistence port — INSERT-only, agent_id-first.
18/// All methods take `agent_id` as the primary parameter (not a filter).
19/// Must enforce: single-writer per agent_id; append-only; atomic commit unit.
20pub trait PersistencePort: Send + Sync + 'static {
21 type Transaction: Txn;
22 type Error: std::error::Error + Send + Sync + 'static;
23
24 /// Begin an atomic unit scoped to one agent_id. No cross-agent transaction allowed.
25 fn begin_atomic(&self, agent_id: &AgentId) -> Result<Self::Transaction, Self::Error>;
26
27 fn append_claim(
28 &self,
29 txn: &mut Self::Transaction,
30 claim: &Claim,
31 ) -> Result<ClaimRef, Self::Error>;
32
33 fn append_validity_assertion(
34 &self,
35 txn: &mut Self::Transaction,
36 assertion: &ValidityAssertion,
37 ) -> Result<(), Self::Error>;
38
39 fn append_ledger_entry(
40 &self,
41 txn: &mut Self::Transaction,
42 entry: &LedgerEntry,
43 ) -> Result<(), Self::Error>;
44
45 fn append_claim_edge(
46 &self,
47 txn: &mut Self::Transaction,
48 edge: &ClaimEdge,
49 ) -> Result<(), Self::Error>;
50
51 fn commit(&self, txn: Self::Transaction) -> Result<(), Self::Error>;
52 fn rollback(&self, txn: Self::Transaction) -> Result<(), Self::Error>;
53
54 // ── Read operations (non-mutating w.r.t. belief and history — I1, I3) ──
55
56 fn load_subject_line(
57 &self,
58 agent_id: &AgentId,
59 subject: &str,
60 predicate: &str,
61 ) -> Result<Vec<Claim>, Self::Error>;
62
63 fn load_claim(
64 &self,
65 agent_id: &AgentId,
66 claim_ref: &ClaimRef,
67 ) -> Result<Option<Claim>, Self::Error>;
68
69 fn load_validity_assertions_for(
70 &self,
71 agent_id: &AgentId,
72 claim_ref: &ClaimRef,
73 ) -> Result<Vec<ValidityAssertion>, Self::Error>;
74
75 fn load_ledger(
76 &self,
77 agent_id: &AgentId,
78 from: Option<&TransactionTime>,
79 limit: usize,
80 ) -> Result<Vec<LedgerEntry>, Self::Error>;
81
82 /// Load ALL ledger entries for the given claim refs, with no row cap.
83 ///
84 /// Intended for the read path (query_memory / query_history): builds the
85 /// disposition map scoped to exactly the claims on a subject-line, avoiding
86 /// the agent-wide capped scan that caused silent wrong-belief at scale.
87 ///
88 /// # Empty input
89 ///
90 /// When `claim_refs` is empty this method MUST return `Ok(vec![])` immediately
91 /// without issuing any SQL (an empty `IN ()` clause is a syntax error on most
92 /// backends).
93 ///
94 /// # No row cap
95 ///
96 /// Unlike `load_ledger`, this method applies no `LIMIT`. Subject-lines are
97 /// small (typically 1–100 claims), so the result set is bounded naturally.
98 fn load_ledger_for_claims(
99 &self,
100 agent_id: &AgentId,
101 claim_refs: &[ClaimRef],
102 ) -> Result<Vec<LedgerEntry>, Self::Error>;
103
104 fn load_edges_for(
105 &self,
106 agent_id: &AgentId,
107 claim_ref: &ClaimRef,
108 ) -> Result<Vec<ClaimEdge>, Self::Error>;
109
110 /// Load the set of claims this agent served as injected context in the current session (for Amplification Guard entailment check).
111 fn load_injected_claims(
112 &self,
113 agent_id: &AgentId,
114 ) -> Result<Vec<ClaimRef>, Self::Error>;
115
116 /// Recursive CTE lineage traversal — returns the full `DerivedFrom` ancestry for a claim.
117 fn load_lineage(
118 &self,
119 agent_id: &AgentId,
120 claim_ref: &ClaimRef,
121 ) -> Result<Vec<ClaimEdge>, Self::Error>;
122
123 /// Whether the store requires a global write serialization lock across all agent_ids.
124 ///
125 /// SQLite: true (single connection, no concurrent transactions possible).
126 /// Postgres: false (pool provides concurrent transactions; advisory lock per agent_id).
127 ///
128 /// EngineHandle consults this at write-path entry to decide whether to acquire
129 /// `store_write_lock`. Default = true (safe fallback for unknown adapters).
130 fn requires_global_write_serialization(&self) -> bool {
131 true
132 }
133}