Skip to main content

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}