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