Skip to main content

khive_types/
note.rs

1//! Note substrate — temporal-referential records used throughout khive.
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    /// Return the canonical lowercase string for this status, as stored on the wire.
25    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/// A note record — temporal-referential content plus free-form properties.
57///
58/// When present, `salience` must be finite and in `[0.0, 1.0]`, and
59/// `decay_factor` must be finite and non-negative. When the `serde` feature
60/// is enabled, deserialization rejects out-of-range values.
61#[derive(Clone, Debug)]
62#[cfg_attr(feature = "serde", derive(serde::Serialize))]
63pub struct Note {
64    /// Identity and namespace metadata shared by all substrate records.
65    #[cfg_attr(feature = "serde", serde(flatten))]
66    pub header: Header,
67    /// Pack-declared kind string (e.g. `"observation"`, `"task"`, `"memory"`).
68    pub kind: String,
69    /// Cross-cutting lifecycle status.
70    pub status: NoteStatus,
71    /// Main textual body of the note.
72    pub content: String,
73    /// Arbitrary structured metadata as key-value pairs.
74    pub properties: BTreeMap<String, PropertyValue>,
75    /// Categorical labels for filtering and retrieval.
76    pub tags: Vec<String>,
77    /// Retrieval priority weight in [0.0, 1.0]; higher values surface the note sooner.
78    pub salience: Option<f64>,
79    /// Exponential decay rate applied to salience over time; 0.0 means no decay.
80    pub decay_factor: Option<f64>,
81    /// Optional expiry timestamp after which the note is treated as inactive.
82    pub expires_at: Option<Timestamp>,
83    /// Set when the note is soft-deleted; absent means active.
84    pub deleted_at: Option<Timestamp>,
85}
86
87impl Note {
88    /// Return `true` if all numeric fields carry finite, domain-valid values.
89    ///
90    /// - `salience`, if present, must be finite and in `[0.0, 1.0]`.
91    /// - `decay_factor`, if present, must be finite and non-negative.
92    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)]
169#[path = "note_tests.rs"]
170mod tests;