Skip to main content

mati_core/store/
durability.rs

1//! Write durability levels for the SurrealKV store.
2//!
3//! **Never mix these.** The split is load-bearing for performance:
4//! - `Immediate`: fsync on every write — slow but crash-safe.
5//! - `Eventual`: OS write buffer — fast but may lose the last ~10ms on crash.
6//!
7//! Assignment by key prefix (from ARCHITECTURE.md §4):
8//! ```text
9//! Immediate  gotcha:*   decision:*   file:*   stage:*   dev_note:*
10//! Eventual   session:*  analytics:*  hook_event:*  compliance:*  graph:edge:*
11//! ```
12//!
13//! `graph:edge:*` is Eventual because edges are derived data (re-computed from
14//! source on `mati init`) — they are not irreplaceable like user-authored records.
15//! Losing a few edges on an OS crash costs one `mati init` re-run, not lost knowledge.
16
17/// Controls whether a `Store::put` call fsyncs before returning.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Durability {
20    /// fsync after write. Use for all user-visible knowledge records.
21    /// Correct for: `gotcha:*`, `decision:*`, `file:*`, `stage:*`, `dev_note:*`.
22    Immediate,
23    /// OS write buffer only. Fast path for high-frequency internal writes.
24    /// Correct for: `session:*`, `analytics:*`, `hook_event:*`, `compliance:*`, `graph:edge:*`.
25    Eventual,
26}
27
28impl Durability {
29    /// Infer durability from a record key prefix.
30    ///
31    /// Unknown prefixes default to `Immediate` (safe over sorry).
32    pub fn for_key(key: &str) -> Self {
33        if key.starts_with("session:")
34            || key.starts_with("analytics:")
35            || key.starts_with("hook_event:")
36            || key.starts_with("compliance:")
37            || key.starts_with("graph:edge:")
38            || key.starts_with("health:") // derived/computed data, fully recomputable
39            || key.starts_with("parse:")  // file content hashes — recomputable on re-init
40            || key.starts_with("audit:session:")
41        // session-side audit — co-located with session mutations
42        {
43            Self::Eventual
44        } else {
45            Self::Immediate
46        }
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn immediate_keys() {
56        assert_eq!(
57            Durability::for_key("gotcha:inference-async"),
58            Durability::Immediate
59        );
60        assert_eq!(
61            Durability::for_key("decision:storage-engine"),
62            Durability::Immediate
63        );
64        assert_eq!(
65            Durability::for_key("file:src/main.rs"),
66            Durability::Immediate
67        );
68        assert_eq!(Durability::for_key("stage:current"), Durability::Immediate);
69        assert_eq!(
70            Durability::for_key("dev_note:dont-refactor"),
71            Durability::Immediate
72        );
73    }
74
75    #[test]
76    fn eventual_keys() {
77        assert_eq!(
78            Durability::for_key("session:1710520800"),
79            Durability::Eventual
80        );
81        assert_eq!(
82            Durability::for_key("analytics:tokens_saved_total"),
83            Durability::Eventual
84        );
85        assert_eq!(
86            Durability::for_key("hook_event:pre_read"),
87            Durability::Eventual
88        );
89        assert_eq!(
90            Durability::for_key("compliance:2026-03-15"),
91            Durability::Eventual
92        );
93    }
94
95    #[test]
96    fn unknown_prefix_defaults_to_immediate() {
97        assert_eq!(
98            Durability::for_key("unknown:something"),
99            Durability::Immediate
100        );
101    }
102
103    // ── Key shape edge cases ─────────────────────────────────────────────────
104
105    #[test]
106    fn graph_edge_key_is_eventual() {
107        // Edges are derived data — Eventual so bulk init avoids per-edge fsyncs.
108        assert_eq!(
109            Durability::for_key("graph:edge:src/main.rs:HasGotcha:gotcha:inference-async"),
110            Durability::Eventual
111        );
112    }
113
114    #[test]
115    fn empty_key_defaults_to_immediate() {
116        // Empty string matches no eventual prefix → safe fallback.
117        assert_eq!(Durability::for_key(""), Durability::Immediate);
118    }
119
120    #[test]
121    fn key_without_colon_defaults_to_immediate() {
122        // A bare word with no colon cannot match any "prefix:" pattern.
123        assert_eq!(Durability::for_key("gotcha"), Durability::Immediate);
124        assert_eq!(Durability::for_key("session"), Durability::Immediate);
125    }
126
127    #[test]
128    fn prefix_only_eventual_keys_are_eventual() {
129        // The bare prefix (no timestamp/slug suffix) still routes correctly.
130        assert_eq!(Durability::for_key("session:"), Durability::Eventual);
131        assert_eq!(Durability::for_key("analytics:"), Durability::Eventual);
132        assert_eq!(Durability::for_key("hook_event:"), Durability::Eventual);
133        assert_eq!(Durability::for_key("compliance:"), Durability::Eventual);
134    }
135
136    #[test]
137    fn all_immediate_prefixes_from_architecture_doc() {
138        // Every Immediate prefix listed in ARCHITECTURE.md §4 must route correctly.
139        let cases = [
140            "gotcha:inference-async",
141            "decision:storage-engine",
142            "file:src/store/db.rs",
143            "stage:current",
144            "dev_note:no-refactor",
145            "dep:cargo:tokio",
146        ];
147        for key in cases {
148            assert_eq!(
149                Durability::for_key(key),
150                Durability::Immediate,
151                "expected Immediate for '{key}'"
152            );
153        }
154    }
155
156    #[test]
157    fn eventual_prefix_requires_exact_colon_boundary() {
158        // "session_v2:x" starts with "session" but NOT "session:" → Immediate.
159        // This guards against accidental prefix collision with future namespaces.
160        assert_eq!(
161            Durability::for_key("session_v2:something"),
162            Durability::Immediate
163        );
164        assert_eq!(
165            Durability::for_key("analytics_v2:something"),
166            Durability::Immediate
167        );
168        assert_eq!(
169            Durability::for_key("hook_event_extra:x"),
170            Durability::Immediate
171        );
172    }
173
174    #[test]
175    fn all_eventual_prefixes_from_architecture_doc() {
176        let cases = [
177            "session:1710520800",
178            "analytics:tokens_saved_total",
179            "hook_event:pre_read",
180            "compliance:2026-03-15",
181            "graph:edge:file:src/main.rs:imports:file:src/lib.rs",
182        ];
183        for key in cases {
184            assert_eq!(
185                Durability::for_key(key),
186                Durability::Eventual,
187                "expected Eventual for '{key}'"
188            );
189        }
190    }
191
192    #[test]
193    fn key_containing_eventual_prefix_as_embedded_substring_is_immediate() {
194        // "gotcha:session:something" contains "session:" but does NOT start_with it.
195        // Must route to Immediate (knowledge tree), not Eventual (sessions tree).
196        assert_eq!(
197            Durability::for_key("gotcha:session:something"),
198            Durability::Immediate,
199            "embedded 'session:' must not trigger Eventual routing"
200        );
201        assert_eq!(
202            Durability::for_key("file:analytics:performance.rs"),
203            Durability::Immediate,
204            "embedded 'analytics:' must not trigger Eventual routing"
205        );
206        assert_eq!(
207            Durability::for_key("decision:hook_event:design"),
208            Durability::Immediate,
209            "embedded 'hook_event:' must not trigger Eventual routing"
210        );
211    }
212
213    // ── Audit routing tests ─────────────────────────────────────────────
214
215    #[test]
216    fn audit_session_is_eventual() {
217        // Session-side audit co-locates with session mutations.
218        assert_eq!(
219            Durability::for_key("audit:session:1234567890"),
220            Durability::Eventual,
221            "audit:session:* must route to sessions tree"
222        );
223    }
224
225    #[test]
226    fn audit_knowledge_is_immediate() {
227        // Knowledge-side audit co-locates with knowledge mutations.
228        // "audit:knowledge:*" does NOT match any Eventual prefix → Immediate.
229        assert_eq!(
230            Durability::for_key("audit:knowledge:1234567890"),
231            Durability::Immediate,
232            "audit:knowledge:* must route to knowledge tree"
233        );
234    }
235
236    #[test]
237    fn audit_bare_prefix_is_immediate() {
238        // "audit:" alone doesn't match "audit:session:" → Immediate.
239        assert_eq!(
240            Durability::for_key("audit:something"),
241            Durability::Immediate,
242            "unknown audit prefix must default to Immediate"
243        );
244    }
245}