Skip to main content

hirn_engine/db/
mutation_contract.rs

1use serde::Serialize;
2
3use super::{HirnDB, episodic, namespace, procedural, semantic};
4
5/// Product-level durability class for a write surface.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
7#[serde(rename_all = "snake_case")]
8pub enum MutationWriteGuarantee {
9    /// One storage mutation is the source of truth; no grouped recovery is needed.
10    StorageAtomic,
11    /// A durable pending envelope is written before side effects and reconciled on open.
12    RecoverableEnvelope,
13    /// Append-only history is the source of truth and replay/inspection surface.
14    DurableLog,
15    /// The operation is intentionally non-critical and may be lost or duplicated.
16    BestEffort,
17    /// The owner node, provider, or caller owns the stronger end-to-end guarantee.
18    Delegated,
19}
20
21impl MutationWriteGuarantee {
22    #[must_use]
23    pub const fn as_str(self) -> &'static str {
24        match self {
25            Self::StorageAtomic => "storage_atomic",
26            Self::RecoverableEnvelope => "recoverable_envelope",
27            Self::DurableLog => "durable_log",
28            Self::BestEffort => "best_effort",
29            Self::Delegated => "delegated",
30        }
31    }
32}
33
34/// One documented write class in Hirn's mutation contract.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
36pub struct MutationWriteContract {
37    pub operation: &'static str,
38    pub guarantee: MutationWriteGuarantee,
39    pub envelope_kind: Option<&'static str>,
40    pub affected_datasets: &'static [&'static str],
41    pub recovery: &'static str,
42    pub notes: &'static str,
43}
44
45const GRAPH_DATASETS: &[&str] = &[
46    hirn_storage::datasets::graph::DATASET_NODES_NAME,
47    hirn_storage::datasets::graph::DATASET_EDGES_NAME,
48];
49
50const EPISODE_DATASETS: &[&str] = &[
51    hirn_storage::datasets::mutation_envelope::DATASET_NAME,
52    hirn_storage::datasets::episodic::DATASET_NAME,
53    hirn_storage::datasets::graph::DATASET_NODES_NAME,
54    hirn_storage::datasets::graph::DATASET_EDGES_NAME,
55    hirn_storage::datasets::events::DATASET_NAME,
56    hirn_storage::datasets::prospective_implications::DATASET_NAME,
57    hirn_storage::datasets::svo_events::DATASET_NAME,
58];
59
60const RESOURCE_DATASETS: &[&str] = &[
61    hirn_storage::datasets::mutation_envelope::DATASET_NAME,
62    hirn_storage::datasets::resource_object::DATASET_NAME,
63    hirn_storage::datasets::resource_blob::DATASET_NAME,
64    hirn_storage::datasets::derived_artifact::DATASET_NAME,
65];
66
67const SEMANTIC_DATASETS: &[&str] = &[
68    hirn_storage::datasets::mutation_envelope::DATASET_NAME,
69    hirn_storage::datasets::semantic::DATASET_NAME,
70    hirn_storage::datasets::graph::DATASET_NODES_NAME,
71    hirn_storage::datasets::graph::DATASET_EDGES_NAME,
72    hirn_storage::datasets::events::DATASET_NAME,
73];
74
75const PROCEDURAL_DATASETS: &[&str] = &[
76    hirn_storage::datasets::mutation_envelope::DATASET_NAME,
77    hirn_storage::datasets::procedural::DATASET_NAME,
78    hirn_storage::datasets::graph::DATASET_NODES_NAME,
79    hirn_storage::datasets::graph::DATASET_EDGES_NAME,
80    hirn_storage::datasets::events::DATASET_NAME,
81];
82
83const EVENT_DATASETS: &[&str] = &[hirn_storage::datasets::events::DATASET_NAME];
84const OFFLINE_DATASETS: &[&str] = &[hirn_storage::datasets::offline_jobs::DATASET_NAME];
85const NAMESPACE_DATASETS: &[&str] = &[
86    hirn_storage::datasets::mutation_envelope::DATASET_NAME,
87    hirn_storage::datasets::namespace::DATASET_NAME,
88    hirn_storage::datasets::episodic::DATASET_NAME,
89    hirn_storage::datasets::semantic::DATASET_NAME,
90    hirn_storage::datasets::procedural::DATASET_NAME,
91    hirn_storage::datasets::graph::DATASET_NODES_NAME,
92    hirn_storage::datasets::graph::DATASET_EDGES_NAME,
93    hirn_storage::datasets::audit::DATASET_NAME,
94];
95
96const NAMESPACE_CREATE_DATASETS: &[&str] = &[
97    hirn_storage::datasets::namespace::DATASET_NAME,
98    hirn_storage::datasets::audit::DATASET_NAME,
99];
100
101const AGENT_DATASETS: &[&str] = &[
102    hirn_storage::datasets::mutation_envelope::DATASET_NAME,
103    hirn_storage::datasets::agent::DATASET_NAME,
104    hirn_storage::datasets::namespace::DATASET_NAME,
105    hirn_storage::datasets::audit::DATASET_NAME,
106];
107
108const AGENT_UPDATE_DATASETS: &[&str] = &[hirn_storage::datasets::agent::DATASET_NAME];
109
110const TEAM_MEMBERSHIP_DATASETS: &[&str] = &[
111    hirn_storage::datasets::namespace::DATASET_NAME,
112    hirn_storage::datasets::audit::DATASET_NAME,
113];
114
115const WORKING_DATASETS: &[&str] = &[
116    hirn_storage::datasets::working::DATASET_NAME,
117    hirn_storage::datasets::events::DATASET_NAME,
118];
119
120pub const MUTATION_WRITE_CONTRACTS: &[MutationWriteContract] = &[
121    MutationWriteContract {
122        operation: "remember_episode",
123        guarantee: MutationWriteGuarantee::RecoverableEnvelope,
124        envelope_kind: Some(episodic::EPISODE_REMEMBER_MUTATION_KIND),
125        affected_datasets: EPISODE_DATASETS,
126        recovery: "startup reconciles the durable episode row with graph node, planned edges, captured TemporalNext edge, and EpisodeCreated event; missing durable rows fail the envelope and remove orphan graph state",
127        notes: "prospective implications and SVO rows are post-commit enrichment and intentionally do not fail the accepted episode",
128    },
129    MutationWriteContract {
130        operation: "batch_remember_episode",
131        guarantee: MutationWriteGuarantee::RecoverableEnvelope,
132        envelope_kind: Some(episodic::EPISODE_REMEMBER_MUTATION_KIND),
133        affected_datasets: EPISODE_DATASETS,
134        recovery: "each accepted row gets its own envelope before graph/storage work; startup reconciles pending rows independently",
135        notes: "the Lance append is batched for throughput, while envelope state remains per memory id",
136    },
137    MutationWriteContract {
138        operation: "semantic_create",
139        guarantee: MutationWriteGuarantee::RecoverableEnvelope,
140        envelope_kind: Some(semantic::SEMANTIC_CREATE_MUTATION_KIND),
141        affected_datasets: SEMANTIC_DATASETS,
142        recovery: "startup verifies the semantic revision row and graph node; failures are marked with graph cleanup context",
143        notes: "batch semantic creation uses the same per-record envelope kind",
144    },
145    MutationWriteContract {
146        operation: "semantic_successor",
147        guarantee: MutationWriteGuarantee::RecoverableEnvelope,
148        envelope_kind: Some(semantic::SEMANTIC_SUCCESSOR_MUTATION_KIND),
149        affected_datasets: SEMANTIC_DATASETS,
150        recovery: "startup reconciles successor visibility and graph/cache state against authoritative semantic revisions",
151        notes: "covers correct, supersede, and override-style head transitions that append a successor revision",
152    },
153    MutationWriteContract {
154        operation: "semantic_merge",
155        guarantee: MutationWriteGuarantee::RecoverableEnvelope,
156        envelope_kind: Some(semantic::SEMANTIC_MERGE_MUTATION_KIND),
157        affected_datasets: SEMANTIC_DATASETS,
158        recovery: "startup reconciles merged target/source revisions and marks incomplete merge groups failed",
159        notes: "merge is a revision operation, not an in-place overwrite",
160    },
161    MutationWriteContract {
162        operation: "semantic_contradiction_sync",
163        guarantee: MutationWriteGuarantee::RecoverableEnvelope,
164        envelope_kind: Some(semantic::SEMANTIC_CONTRADICTION_SYNC_MUTATION_KIND),
165        affected_datasets: SEMANTIC_DATASETS,
166        recovery: "startup reconciles contradiction replacement revisions after ABA/conflict processing",
167        notes: "keeps conflict-history repair separate from ordinary successor creation",
168    },
169    MutationWriteContract {
170        operation: "semantic_retract",
171        guarantee: MutationWriteGuarantee::RecoverableEnvelope,
172        envelope_kind: Some(semantic::SEMANTIC_RETRACT_MUTATION_KIND),
173        affected_datasets: SEMANTIC_DATASETS,
174        recovery: "startup verifies the tombstone revision and head collapse behavior",
175        notes: "logical memory ids remain queryable through history surfaces",
176    },
177    MutationWriteContract {
178        operation: "semantic_purge",
179        guarantee: MutationWriteGuarantee::RecoverableEnvelope,
180        envelope_kind: Some(semantic::SEMANTIC_PURGE_MUTATION_KIND),
181        affected_datasets: SEMANTIC_DATASETS,
182        recovery: "startup reconciles delete intent against remaining revision rows and graph/cache state",
183        notes: "purge is intentionally stronger than archive/retract and should stay rare",
184    },
185    MutationWriteContract {
186        operation: "procedural_create",
187        guarantee: MutationWriteGuarantee::RecoverableEnvelope,
188        envelope_kind: Some(procedural::PROCEDURAL_CREATE_MUTATION_KIND),
189        affected_datasets: PROCEDURAL_DATASETS,
190        recovery: "startup verifies the procedural row and graph node, then finalizes or fails the envelope",
191        notes: "batch procedural surfaces should keep this per-record shape",
192    },
193    MutationWriteContract {
194        operation: "procedural_successor",
195        guarantee: MutationWriteGuarantee::RecoverableEnvelope,
196        envelope_kind: Some(procedural::PROCEDURAL_SUCCESSOR_MUTATION_KIND),
197        affected_datasets: PROCEDURAL_DATASETS,
198        recovery: "startup reconciles successor rows for procedure success/failure updates",
199        notes: "procedural stats changes are modeled as revision successors",
200    },
201    MutationWriteContract {
202        operation: "resource_head_transition",
203        guarantee: MutationWriteGuarantee::RecoverableEnvelope,
204        envelope_kind: Some(hirn_storage::RESOURCE_HEAD_TRANSITION_KIND),
205        affected_datasets: RESOURCE_DATASETS,
206        recovery: "startup reconciles current/successor resource revisions and rolls back impossible head transitions",
207        notes: "blob storage_ready staging keeps hydration from exposing incomplete payloads",
208    },
209    MutationWriteContract {
210        operation: "resource_initial_persist",
211        guarantee: MutationWriteGuarantee::StorageAtomic,
212        envelope_kind: None,
213        affected_datasets: RESOURCE_DATASETS,
214        recovery: "resource rows and blobs are individually durable; attachment to an episode is governed by the episode envelope",
215        notes: "a failed later episode write can leave an unreferenced resource for retention/GC rather than rolling back source evidence",
216    },
217    MutationWriteContract {
218        operation: "explicit_graph_connect",
219        guarantee: MutationWriteGuarantee::StorageAtomic,
220        envelope_kind: None,
221        affected_datasets: GRAPH_DATASETS,
222        recovery: "cold graph persistence is the durable source of truth; hot-tier changes are rolled back on cold-tier failure and reloaded on open",
223        notes: "client retries should treat graph edges as idempotent at the source/target/relation level",
224    },
225    MutationWriteContract {
226        operation: "durable_event_append",
227        guarantee: MutationWriteGuarantee::DurableLog,
228        envelope_kind: None,
229        affected_datasets: EVENT_DATASETS,
230        recovery: "the event log is append-only and ordered by sequence for replay/inspection",
231        notes: "event consumers must be idempotent because replay and retry can deliver duplicates",
232    },
233    MutationWriteContract {
234        operation: "live_watch_fanout",
235        guarantee: MutationWriteGuarantee::BestEffort,
236        envelope_kind: None,
237        affected_datasets: &[],
238        recovery: "no replay is implied by the live broadcast channel; use durable event reads for replay semantics",
239        notes: "slow consumers can lag or disconnect without failing the underlying write",
240    },
241    MutationWriteContract {
242        operation: "offline_job_transition",
243        guarantee: MutationWriteGuarantee::DurableLog,
244        envelope_kind: None,
245        affected_datasets: OFFLINE_DATASETS,
246        recovery: "startup reloads append-only job transition history and resumes according to OfflineRecoveryPolicy",
247        notes: "generated cognition remains inactive until explicit review/promotion paths approve it",
248    },
249    MutationWriteContract {
250        operation: "namespace_create",
251        guarantee: MutationWriteGuarantee::StorageAtomic,
252        envelope_kind: None,
253        affected_datasets: NAMESPACE_CREATE_DATASETS,
254        recovery: "namespace row append is authoritative; audit append is a checked follow-up",
255        notes: "namespace bootstrap for the shared namespace is idempotent during open",
256    },
257    MutationWriteContract {
258        operation: "agent_register",
259        guarantee: MutationWriteGuarantee::RecoverableEnvelope,
260        envelope_kind: Some(namespace::AGENT_REGISTER_MUTATION_KIND),
261        affected_datasets: AGENT_DATASETS,
262        recovery: "startup reconciles the agent row, private namespace row, and durable audit entry until registration can be marked applied",
263        notes: "agent registration now records durable intent before metadata writes so partial private-namespace creation can be replayed safely",
264    },
265    MutationWriteContract {
266        operation: "agent_update",
267        guarantee: MutationWriteGuarantee::StorageAtomic,
268        envelope_kind: None,
269        affected_datasets: AGENT_UPDATE_DATASETS,
270        recovery: "the keyed agent row upsert is authoritative and preserves the prior row if the write fails",
271        notes: "cache refresh is local follow-up work after the durable row update succeeds",
272    },
273    MutationWriteContract {
274        operation: "agent_deregister",
275        guarantee: MutationWriteGuarantee::RecoverableEnvelope,
276        envelope_kind: Some(namespace::AGENT_DEREGISTER_MUTATION_KIND),
277        affected_datasets: AGENT_DATASETS,
278        recovery: "startup finishes private-namespace deletion through the namespace-delete envelope, removes the agent row, and appends the stable deregistration audit entry until the envelope can be marked applied",
279        notes: "agent deregistration composes namespace-delete replay with a dedicated agent metadata envelope for the remaining delete and audit work",
280    },
281    MutationWriteContract {
282        operation: "namespace_update",
283        guarantee: MutationWriteGuarantee::StorageAtomic,
284        envelope_kind: None,
285        affected_datasets: TEAM_MEMBERSHIP_DATASETS,
286        recovery: "the keyed namespace row upsert is authoritative; audit append is a checked follow-up when higher-level flows use it",
287        notes: "covers direct namespace record replacement without reopening a delete gap",
288    },
289    MutationWriteContract {
290        operation: "team_membership_update",
291        guarantee: MutationWriteGuarantee::StorageAtomic,
292        envelope_kind: None,
293        affected_datasets: TEAM_MEMBERSHIP_DATASETS,
294        recovery: "the namespace membership row is updated via keyed upsert and remains the source of truth if the follow-up audit append fails",
295        notes: "add/remove team member flows reuse namespace_update semantics",
296    },
297    MutationWriteContract {
298        operation: "namespace_delete",
299        guarantee: MutationWriteGuarantee::RecoverableEnvelope,
300        envelope_kind: Some(namespace::NAMESPACE_DELETE_MUTATION_KIND),
301        affected_datasets: NAMESPACE_DATASETS,
302        recovery: "startup replays the captured namespace delete plan across episodic, semantic, procedural, graph/cache cleanup, namespace row deletion, and audit intent until it can mark the envelope applied",
303        notes: "per-layer deletes remain idempotent; already-deleted rows are treated as successful replay and the envelope carries a stable audit entry id for replay-safe audit append",
304    },
305    MutationWriteContract {
306        operation: "working_memory_update",
307        guarantee: MutationWriteGuarantee::StorageAtomic,
308        envelope_kind: None,
309        affected_datasets: WORKING_DATASETS,
310        recovery: "working rows are short-lived storage records; promotion to episodic uses the episode remember contract",
311        notes: "working memory is intentionally lower durability than episodic/semantic/procedural layers",
312    },
313    MutationWriteContract {
314        operation: "daemon_forwarded_write",
315        guarantee: MutationWriteGuarantee::Delegated,
316        envelope_kind: None,
317        affected_datasets: &[],
318        recovery: "forwarding preserves caller headers and idempotency context, then delegates the write contract to the realm owner",
319        notes: "transport failure returns an error before pretending the mutation succeeded",
320    },
321];
322
323#[must_use]
324pub const fn mutation_write_contracts() -> &'static [MutationWriteContract] {
325    MUTATION_WRITE_CONTRACTS
326}
327
328impl HirnDB {
329    #[must_use]
330    pub(crate) const fn mutation_write_contracts(&self) -> &'static [MutationWriteContract] {
331        mutation_write_contracts()
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use std::collections::HashSet;
338
339    use super::*;
340
341    #[test]
342    fn mutation_contract_operations_are_unique() {
343        let mut operations = HashSet::new();
344        for contract in mutation_write_contracts() {
345            assert!(
346                operations.insert(contract.operation),
347                "duplicate mutation contract operation: {}",
348                contract.operation
349            );
350        }
351    }
352
353    #[test]
354    fn recoverable_contracts_have_envelope_kinds() {
355        for contract in mutation_write_contracts() {
356            if contract.guarantee == MutationWriteGuarantee::RecoverableEnvelope {
357                assert!(
358                    contract.envelope_kind.is_some(),
359                    "recoverable contract missing envelope kind: {}",
360                    contract.operation
361                );
362            }
363        }
364    }
365
366    #[test]
367    fn every_current_envelope_kind_is_documented() {
368        let documented = mutation_write_contracts()
369            .iter()
370            .filter_map(|contract| contract.envelope_kind)
371            .collect::<HashSet<_>>();
372        let expected = [
373            episodic::EPISODE_REMEMBER_MUTATION_KIND,
374            semantic::SEMANTIC_CREATE_MUTATION_KIND,
375            semantic::SEMANTIC_SUCCESSOR_MUTATION_KIND,
376            semantic::SEMANTIC_MERGE_MUTATION_KIND,
377            semantic::SEMANTIC_CONTRADICTION_SYNC_MUTATION_KIND,
378            semantic::SEMANTIC_RETRACT_MUTATION_KIND,
379            semantic::SEMANTIC_PURGE_MUTATION_KIND,
380            procedural::PROCEDURAL_CREATE_MUTATION_KIND,
381            procedural::PROCEDURAL_SUCCESSOR_MUTATION_KIND,
382            namespace::AGENT_REGISTER_MUTATION_KIND,
383            namespace::AGENT_DEREGISTER_MUTATION_KIND,
384            namespace::NAMESPACE_DELETE_MUTATION_KIND,
385            hirn_storage::RESOURCE_HEAD_TRANSITION_KIND,
386        ];
387
388        for envelope_kind in expected {
389            assert!(
390                documented.contains(envelope_kind),
391                "undocumented mutation envelope kind: {envelope_kind}"
392            );
393        }
394    }
395}