1use serde::Serialize;
2
3use super::{HirnDB, episodic, namespace, procedural, semantic};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
7#[serde(rename_all = "snake_case")]
8pub enum MutationWriteGuarantee {
9 StorageAtomic,
11 RecoverableEnvelope,
13 DurableLog,
15 BestEffort,
17 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#[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}