Skip to main content

khive_types/
note.rs

1//! Note substrate — temporal-referential records (ADR-004, ADR-013).
2
3extern 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/// Lifecycle status of a note. Cross-cutting across all note kinds.
13#[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 {
25        match self {
26            Self::Active => "active",
27            Self::Archived => "archived",
28            Self::Deleted => "deleted",
29        }
30    }
31}
32
33impl fmt::Display for NoteStatus {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        f.write_str(self.name())
36    }
37}
38
39impl core::str::FromStr for NoteStatus {
40    type Err = crate::error::UnknownVariant;
41    fn from_str(s: &str) -> Result<Self, Self::Err> {
42        match s.trim().to_ascii_lowercase().as_str() {
43            "active" => Ok(Self::Active),
44            "archived" => Ok(Self::Archived),
45            "deleted" => Ok(Self::Deleted),
46            other => Err(crate::error::UnknownVariant::new(
47                "note_status",
48                other,
49                &["active", "archived", "deleted"],
50            )),
51        }
52    }
53}
54
55/// A note record — temporal-referential content plus free-form properties.
56#[derive(Clone, Debug)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58pub struct Note {
59    #[cfg_attr(feature = "serde", serde(flatten))]
60    pub header: Header,
61    pub kind: String,
62    pub status: NoteStatus,
63    pub content: String,
64    pub properties: BTreeMap<String, PropertyValue>,
65    pub tags: Vec<String>,
66    pub salience: Option<f64>,
67    pub decay_factor: Option<f64>,
68    pub expires_at: Option<Timestamp>,
69    pub deleted_at: Option<Timestamp>,
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use crate::{Id128, Namespace};
76
77    fn test_header() -> Header {
78        Header::new(
79            Id128::from_u128(1),
80            Namespace::local(),
81            Timestamp::from_secs(1700000000),
82        )
83    }
84
85    #[test]
86    fn note_construction() {
87        let note = Note {
88            header: test_header(),
89            kind: String::from("decision"),
90            status: NoteStatus::Active,
91            content: String::from("Use BGE-base for multilingual corpus"),
92            properties: BTreeMap::new(),
93            tags: alloc::vec!["retrieval".into()],
94            salience: Some(0.8),
95            decay_factor: Some(0.01),
96            expires_at: None,
97            deleted_at: None,
98        };
99        assert_eq!(note.kind, "decision");
100        assert_eq!(note.tags.len(), 1);
101    }
102
103    #[test]
104    fn note_construction_uses_pack_owned_kind_string() {
105        let note = Note {
106            header: test_header(),
107            kind: String::from("decision"),
108            status: NoteStatus::Active,
109            content: String::from("test"),
110            properties: BTreeMap::new(),
111            tags: alloc::vec![],
112            salience: None,
113            decay_factor: None,
114            expires_at: None,
115            deleted_at: None,
116        };
117        assert_eq!(note.kind, "decision");
118    }
119
120    #[test]
121    fn note_status_deleted_roundtrip() {
122        use core::str::FromStr;
123        assert_eq!(
124            NoteStatus::from_str("deleted").unwrap(),
125            NoteStatus::Deleted
126        );
127        assert_eq!(NoteStatus::Deleted.name(), "deleted");
128    }
129}