Skip to main content

a3s_memory/repository/
change.rs

1use super::validation::{
2    validate_count, validate_required_text, MAX_CHANGE_OPERATIONS, MAX_IDENTIFIER_BYTES,
3};
4use super::{
5    EvidenceRef, MemoryNamespace, MemoryNode, MemoryNodeDraft, MemoryRelation,
6    MemoryRepositoryError, MemoryStatus, RevisionMode,
7};
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10
11/// One bounded mutation inside an atomic change set.
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13#[serde(tag = "operation", rename_all = "snake_case")]
14pub enum MemoryOperation {
15    Create {
16        node: MemoryNodeDraft,
17    },
18    Activate {
19        node_id: String,
20        expected_revision: u64,
21        /// New evidence for the explicit activation decision.
22        evidence: Vec<EvidenceRef>,
23    },
24    Corroborate {
25        node_id: String,
26        expected_revision: u64,
27        evidence: Vec<EvidenceRef>,
28    },
29    Revise {
30        node_id: String,
31        expected_revision: u64,
32        content: String,
33        mode: RevisionMode,
34        evidence: Vec<EvidenceRef>,
35        confidence: Option<f32>,
36        importance: Option<f32>,
37    },
38    AddRelation {
39        node_id: String,
40        expected_revision: u64,
41        relation: MemoryRelation,
42    },
43    RemoveRelation {
44        node_id: String,
45        expected_revision: u64,
46        relation: MemoryRelation,
47    },
48    SetStatus {
49        node_id: String,
50        expected_revision: u64,
51        status: MemoryStatus,
52    },
53}
54
55/// Caller-timestamped, idempotent unit of atomic mutation.
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct MemoryChangeSet {
59    pub idempotency_key: String,
60    pub namespace: MemoryNamespace,
61    pub occurred_at: DateTime<Utc>,
62    pub operations: Vec<MemoryOperation>,
63}
64
65impl MemoryChangeSet {
66    pub fn new(
67        idempotency_key: impl Into<String>,
68        namespace: MemoryNamespace,
69        occurred_at: DateTime<Utc>,
70        operations: Vec<MemoryOperation>,
71    ) -> Self {
72        Self {
73            idempotency_key: idempotency_key.into(),
74            namespace,
75            occurred_at,
76            operations,
77        }
78    }
79
80    pub(crate) fn validate_shape(&self) -> Result<(), MemoryRepositoryError> {
81        self.namespace.validate()?;
82        validate_required_text(
83            "changeSet.idempotencyKey",
84            &self.idempotency_key,
85            MAX_IDENTIFIER_BYTES,
86        )?;
87        if self.operations.is_empty() {
88            return Err(MemoryRepositoryError::invalid(
89                "changeSet.operations",
90                "must contain at least one operation",
91            ));
92        }
93        validate_count(
94            "change-set operations",
95            self.operations.len(),
96            MAX_CHANGE_OPERATIONS,
97        )
98    }
99}
100
101/// Deterministic result retained for idempotent replay.
102#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct MemoryChangeResult {
105    pub idempotency_key: String,
106    pub occurred_at: DateTime<Utc>,
107    pub nodes: Vec<MemoryNode>,
108}