Skip to main content

memnite_core/
event.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
4pub struct Event {
5    pub event_id: String,
6    pub lamport: u64,
7    pub ts: String,
8    pub engine: String,
9    pub machine: String,
10    pub memory_id: String,
11    #[serde(flatten)]
12    pub kind: EventKind,
13}
14
15#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
16#[serde(tag = "kind", content = "payload")]
17pub enum EventKind {
18    MemoryAdded(MemoryData),
19    MemoryUpdated(MemoryData),
20    MemoryPatched(MemoryPatch),
21    MemoryDeleted,
22    MemoryStabilized,
23    MemoryMarkedStale {
24        reason: StaleReason,
25    },
26    MemoryReviewed,
27    RelationAsserted {
28        to_id: String,
29        relation: Relation,
30        confidence: f32,
31        reason: String,
32        judged_by: String,
33    },
34}
35
36#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
37#[serde(rename_all = "snake_case")]
38pub enum StaleReason {
39    FileGone,
40    SymbolDeleted,
41    ContentChanged,
42}
43
44#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
45#[serde(rename_all = "lowercase")]
46pub enum Scope {
47    Agent,
48    Repo,
49    Global,
50}
51
52#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
53#[serde(rename_all = "snake_case")]
54pub enum Relation {
55    ConflictsWith,
56    Supersedes,
57    Scoped,
58    Related,
59    Compatible,
60    NotConflict,
61}
62
63impl Relation {
64    /// Snake-case label as stored/serialized (matches serde `rename_all`).
65    pub fn as_str(&self) -> &'static str {
66        match self {
67            Relation::ConflictsWith => "conflicts_with",
68            Relation::Supersedes => "supersedes",
69            Relation::Scoped => "scoped",
70            Relation::Related => "related",
71            Relation::Compatible => "compatible",
72            Relation::NotConflict => "not_conflict",
73        }
74    }
75
76    /// Parse a snake-case label. `None` for unknown input (boundary validation).
77    pub fn from_label(s: &str) -> Option<Relation> {
78        match s {
79            "conflicts_with" => Some(Relation::ConflictsWith),
80            "supersedes" => Some(Relation::Supersedes),
81            "scoped" => Some(Relation::Scoped),
82            "related" => Some(Relation::Related),
83            "compatible" => Some(Relation::Compatible),
84            "not_conflict" => Some(Relation::NotConflict),
85            _ => None,
86        }
87    }
88}
89
90#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
91pub struct MemoryData {
92    pub title: String,
93    pub body: String,
94    #[serde(rename = "type")]
95    pub mem_type: String,
96    pub scope: Scope,
97    pub project: String,
98    #[serde(default)]
99    pub topic_key: Option<String>,
100    #[serde(default)]
101    pub anchors: Vec<Anchor>,
102    #[serde(default)]
103    pub tags: Vec<String>,
104}
105
106/// Sparse update: only the fields the writer actually touched are `Some`.
107/// Presence is the "touched" signal — untouched fields are `None` and skipped
108/// on the wire. Enables field-level LWW merge (a whole-snapshot update cannot
109/// distinguish "cleared" from "carried a stale copy"). `topic_key` cannot be
110/// cleared via a patch (matches `App::update`); `Some` sets it, absent leaves it.
111#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
112pub struct MemoryPatch {
113    #[serde(skip_serializing_if = "Option::is_none", default)]
114    pub title: Option<String>,
115    #[serde(skip_serializing_if = "Option::is_none", default)]
116    pub body: Option<String>,
117    #[serde(rename = "type", skip_serializing_if = "Option::is_none", default)]
118    pub mem_type: Option<String>,
119    #[serde(skip_serializing_if = "Option::is_none", default)]
120    pub scope: Option<Scope>,
121    #[serde(skip_serializing_if = "Option::is_none", default)]
122    pub project: Option<String>,
123    #[serde(skip_serializing_if = "Option::is_none", default)]
124    pub topic_key: Option<String>,
125    #[serde(skip_serializing_if = "Option::is_none", default)]
126    pub anchors: Option<Vec<Anchor>>,
127    #[serde(skip_serializing_if = "Option::is_none", default)]
128    pub tags: Option<Vec<String>>,
129}
130
131#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
132pub struct Anchor {
133    pub path: String,
134    #[serde(default)]
135    pub symbol: Option<String>,
136    pub line_start: u32,
137    pub line_end: u32,
138    pub content_hash: String,
139}