1extern crate alloc;
4use alloc::collections::BTreeMap;
5use alloc::string::String;
6use alloc::vec::Vec;
7use core::fmt;
8
9use crate::entity::PropertyValue;
10use crate::{Header, Timestamp};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
16pub enum NoteStatus {
17 #[default]
18 Active,
19 Archived,
20 Deleted,
21}
22
23impl NoteStatus {
24 pub const fn name(self) -> &'static str {
26 match self {
27 Self::Active => "active",
28 Self::Archived => "archived",
29 Self::Deleted => "deleted",
30 }
31 }
32}
33
34impl fmt::Display for NoteStatus {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 f.write_str(self.name())
37 }
38}
39
40impl core::str::FromStr for NoteStatus {
41 type Err = crate::error::UnknownVariant;
42 fn from_str(s: &str) -> Result<Self, Self::Err> {
43 match s.trim().to_ascii_lowercase().as_str() {
44 "active" => Ok(Self::Active),
45 "archived" => Ok(Self::Archived),
46 "deleted" => Ok(Self::Deleted),
47 other => Err(crate::error::UnknownVariant::new(
48 "note_status",
49 other,
50 &["active", "archived", "deleted"],
51 )),
52 }
53 }
54}
55
56#[derive(Clone, Debug)]
62#[cfg_attr(feature = "serde", derive(serde::Serialize))]
63pub struct Note {
64 #[cfg_attr(feature = "serde", serde(flatten))]
66 pub header: Header,
67 pub kind: String,
69 pub status: NoteStatus,
71 pub content: String,
73 pub properties: BTreeMap<String, PropertyValue>,
75 pub tags: Vec<String>,
77 pub salience: Option<f64>,
79 pub decay_factor: Option<f64>,
81 pub expires_at: Option<Timestamp>,
83 pub deleted_at: Option<Timestamp>,
85}
86
87impl Note {
88 pub fn is_valid(&self) -> bool {
93 let salience_ok = self
94 .salience
95 .map(|s| s.is_finite() && (0.0..=1.0).contains(&s))
96 .unwrap_or(true);
97 let decay_ok = self
98 .decay_factor
99 .map(|d| d.is_finite() && d >= 0.0)
100 .unwrap_or(true);
101 salience_ok && decay_ok
102 }
103}
104
105#[cfg(feature = "serde")]
106impl<'de> serde::Deserialize<'de> for Note {
107 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
108 where
109 D: serde::Deserializer<'de>,
110 {
111 #[derive(serde::Deserialize)]
112 struct NoteRaw {
113 #[serde(flatten)]
114 header: Header,
115 kind: String,
116 status: NoteStatus,
117 content: String,
118 properties: BTreeMap<String, PropertyValue>,
119 tags: Vec<String>,
120 salience: Option<f64>,
121 decay_factor: Option<f64>,
122 expires_at: Option<Timestamp>,
123 deleted_at: Option<Timestamp>,
124 }
125
126 let raw = NoteRaw::deserialize(deserializer)?;
127
128 if let Some(s) = raw.salience {
129 if !s.is_finite() {
130 return Err(serde::de::Error::custom(alloc::format!(
131 "Note salience must be finite, got {s}"
132 )));
133 }
134 if !(0.0..=1.0).contains(&s) {
135 return Err(serde::de::Error::custom(alloc::format!(
136 "Note salience must be in [0.0, 1.0], got {s}"
137 )));
138 }
139 }
140 if let Some(d) = raw.decay_factor {
141 if !d.is_finite() {
142 return Err(serde::de::Error::custom(alloc::format!(
143 "Note decay_factor must be finite, got {d}"
144 )));
145 }
146 if d < 0.0 {
147 return Err(serde::de::Error::custom(alloc::format!(
148 "Note decay_factor must be non-negative, got {d}"
149 )));
150 }
151 }
152
153 Ok(Note {
154 header: raw.header,
155 kind: raw.kind,
156 status: raw.status,
157 content: raw.content,
158 properties: raw.properties,
159 tags: raw.tags,
160 salience: raw.salience,
161 decay_factor: raw.decay_factor,
162 expires_at: raw.expires_at,
163 deleted_at: raw.deleted_at,
164 })
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::{Id128, Namespace};
172 #[cfg(feature = "serde")]
173 use alloc::string::ToString;
174
175 fn test_header() -> Header {
176 Header::new(
177 Id128::from_u128(1),
178 Namespace::local(),
179 Timestamp::from_secs(1700000000),
180 )
181 }
182
183 #[test]
184 fn note_construction() {
185 let note = Note {
186 header: test_header(),
187 kind: String::from("decision"),
188 status: NoteStatus::Active,
189 content: String::from("Use BGE-base for multilingual corpus"),
190 properties: BTreeMap::new(),
191 tags: alloc::vec!["retrieval".into()],
192 salience: Some(0.8),
193 decay_factor: Some(0.01),
194 expires_at: None,
195 deleted_at: None,
196 };
197 assert_eq!(note.kind, "decision");
198 assert_eq!(note.tags.len(), 1);
199 }
200
201 #[test]
202 fn note_construction_uses_pack_owned_kind_string() {
203 let note = Note {
204 header: test_header(),
205 kind: String::from("decision"),
206 status: NoteStatus::Active,
207 content: String::from("test"),
208 properties: BTreeMap::new(),
209 tags: alloc::vec![],
210 salience: None,
211 decay_factor: None,
212 expires_at: None,
213 deleted_at: None,
214 };
215 assert_eq!(note.kind, "decision");
216 }
217
218 #[test]
219 fn note_status_deleted_roundtrip() {
220 use core::str::FromStr;
221 assert_eq!(
222 NoteStatus::from_str("deleted").unwrap(),
223 NoteStatus::Deleted
224 );
225 assert_eq!(NoteStatus::Deleted.name(), "deleted");
226 }
227
228 #[test]
229 fn note_is_valid_checks_salience_range() {
230 let mut note = Note {
231 header: test_header(),
232 kind: String::from("observation"),
233 status: NoteStatus::Active,
234 content: String::from("test"),
235 properties: BTreeMap::new(),
236 tags: alloc::vec![],
237 salience: Some(1.5),
238 decay_factor: None,
239 expires_at: None,
240 deleted_at: None,
241 };
242 assert!(!note.is_valid());
243 note.salience = Some(0.5);
244 assert!(note.is_valid());
245 }
246
247 #[test]
248 fn note_is_valid_checks_decay_non_negative() {
249 let mut note = Note {
250 header: test_header(),
251 kind: String::from("observation"),
252 status: NoteStatus::Active,
253 content: String::from("test"),
254 properties: BTreeMap::new(),
255 tags: alloc::vec![],
256 salience: None,
257 decay_factor: Some(-0.1),
258 expires_at: None,
259 deleted_at: None,
260 };
261 assert!(!note.is_valid());
262 note.decay_factor = Some(0.01);
263 assert!(note.is_valid());
264 }
265
266 #[cfg(feature = "serde")]
267 #[test]
268 fn note_serde_rejects_salience_above_one() {
269 let json = serde_json::json!({
270 "id": "00000000-0000-0000-0000-000000000001",
271 "namespace": "local",
272 "created_at": 1700000000000000_u64,
273 "updated_at": 1700000000000000_u64,
274 "kind": "observation",
275 "status": "active",
276 "content": "test",
277 "properties": {},
278 "tags": [],
279 "salience": 1.5,
280 "decay_factor": null,
281 "expires_at": null,
282 "deleted_at": null
283 });
284 let result: Result<Note, _> = serde_json::from_value(json);
285 assert!(result.is_err());
286 let err = result.unwrap_err().to_string();
287 assert!(
288 err.contains("[0.0, 1.0]"),
289 "error should mention range: {err}"
290 );
291 }
292
293 #[cfg(feature = "serde")]
294 #[test]
295 fn note_serde_rejects_negative_decay() {
296 let json = serde_json::json!({
297 "id": "00000000-0000-0000-0000-000000000001",
298 "namespace": "local",
299 "created_at": 1700000000000000_u64,
300 "updated_at": 1700000000000000_u64,
301 "kind": "observation",
302 "status": "active",
303 "content": "test",
304 "properties": {},
305 "tags": [],
306 "salience": null,
307 "decay_factor": -0.5,
308 "expires_at": null,
309 "deleted_at": null
310 });
311 let result: Result<Note, _> = serde_json::from_value(json);
312 assert!(result.is_err());
313 let err = result.unwrap_err().to_string();
314 assert!(
315 err.contains("non-negative"),
316 "error should mention non-negative: {err}"
317 );
318 }
319
320 #[cfg(feature = "serde")]
321 #[test]
322 fn note_serde_accepts_valid_values() {
323 let json = serde_json::json!({
324 "id": "00000000-0000-0000-0000-000000000001",
325 "namespace": "local",
326 "created_at": 1700000000000000_u64,
327 "updated_at": 1700000000000000_u64,
328 "kind": "decision",
329 "status": "active",
330 "content": "test content",
331 "properties": {},
332 "tags": ["tag1"],
333 "salience": 0.8,
334 "decay_factor": 0.01,
335 "expires_at": null,
336 "deleted_at": null
337 });
338 let note: Note = serde_json::from_value(json).expect("valid note should deserialize");
339 assert_eq!(note.salience, Some(0.8));
340 assert_eq!(note.decay_factor, Some(0.01));
341 }
342}