brainwires_knowledge/knowledge/
thought.rs1use chrono::Utc;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4use uuid::Uuid;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Thought {
13 pub id: String,
15 pub content: String,
17 pub category: ThoughtCategory,
19 pub tags: Vec<String>,
21 pub source: ThoughtSource,
23 pub importance: f32,
25 pub created_at: i64,
27 pub updated_at: i64,
29 pub deleted: bool,
31 pub confidence: f32,
33 pub evidence_chain: Vec<String>,
35 pub reinforcement_count: u32,
37 pub contradiction_count: u32,
39}
40
41impl Thought {
42 pub fn new(content: String) -> Self {
44 let now = Utc::now().timestamp();
45 Self {
46 id: Uuid::new_v4().to_string(),
47 content,
48 category: ThoughtCategory::General,
49 tags: Vec::new(),
50 source: ThoughtSource::ManualCapture,
51 importance: 0.5,
52 created_at: now,
53 updated_at: now,
54 deleted: false,
55 confidence: 0.5,
56 evidence_chain: Vec::new(),
57 reinforcement_count: 0,
58 contradiction_count: 0,
59 }
60 }
61
62 pub fn with_category(mut self, category: ThoughtCategory) -> Self {
64 self.category = category;
65 self
66 }
67
68 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
70 self.tags = tags;
71 self
72 }
73
74 pub fn with_source(mut self, source: ThoughtSource) -> Self {
76 self.source = source;
77 self
78 }
79
80 pub fn with_importance(mut self, importance: f32) -> Self {
82 self.importance = importance.clamp(0.0, 1.0);
83 self
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
89#[serde(rename_all = "snake_case")]
90pub enum ThoughtCategory {
91 Decision,
93 Person,
95 Insight,
97 MeetingNote,
99 Idea,
101 ActionItem,
103 Reference,
105 Conversation,
107 General,
109}
110
111impl ThoughtCategory {
112 pub const ALL: &[ThoughtCategory] = &[
114 Self::Decision,
115 Self::Person,
116 Self::Insight,
117 Self::MeetingNote,
118 Self::Idea,
119 Self::ActionItem,
120 Self::Reference,
121 Self::Conversation,
122 Self::General,
123 ];
124
125 pub fn as_str(&self) -> &'static str {
127 match self {
128 Self::Decision => "decision",
129 Self::Person => "person",
130 Self::Insight => "insight",
131 Self::MeetingNote => "meeting_note",
132 Self::Idea => "idea",
133 Self::ActionItem => "action_item",
134 Self::Reference => "reference",
135 Self::Conversation => "conversation",
136 Self::General => "general",
137 }
138 }
139
140 pub fn parse(s: &str) -> Self {
142 match s.to_lowercase().as_str() {
143 "decision" => Self::Decision,
144 "person" => Self::Person,
145 "insight" => Self::Insight,
146 "meeting_note" | "meetingnote" => Self::MeetingNote,
147 "idea" => Self::Idea,
148 "action_item" | "actionitem" | "todo" => Self::ActionItem,
149 "reference" | "ref" => Self::Reference,
150 "conversation" | "conversation_extract" => Self::Conversation,
151 _ => Self::General,
152 }
153 }
154}
155
156impl fmt::Display for ThoughtCategory {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 f.write_str(self.as_str())
159 }
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
164#[serde(rename_all = "snake_case")]
165pub enum ThoughtSource {
166 ManualCapture,
168 ConversationExtract,
170 Import,
172}
173
174impl ThoughtSource {
175 pub fn as_str(&self) -> &'static str {
177 match self {
178 Self::ManualCapture => "manual",
179 Self::ConversationExtract => "conversation",
180 Self::Import => "import",
181 }
182 }
183
184 pub fn parse(s: &str) -> Self {
186 match s.to_lowercase().as_str() {
187 "manual" | "manual_capture" => Self::ManualCapture,
188 "conversation" | "conversation_extract" => Self::ConversationExtract,
189 "import" => Self::Import,
190 _ => Self::ManualCapture,
191 }
192 }
193}
194
195impl fmt::Display for ThoughtSource {
196 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197 f.write_str(self.as_str())
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn test_thought_creation() {
207 let thought = Thought::new("Test thought".into())
208 .with_category(ThoughtCategory::Decision)
209 .with_tags(vec!["rust".into(), "architecture".into()])
210 .with_importance(0.8);
211
212 assert_eq!(thought.category, ThoughtCategory::Decision);
213 assert_eq!(thought.tags.len(), 2);
214 assert!((thought.importance - 0.8).abs() < f32::EPSILON);
215 assert!(!thought.deleted);
216 }
217
218 #[test]
219 fn test_category_roundtrip() {
220 for cat in ThoughtCategory::ALL {
221 let s = cat.as_str();
222 let parsed = ThoughtCategory::parse(s);
223 assert_eq!(*cat, parsed);
224 }
225 }
226
227 #[test]
228 fn test_importance_clamped() {
229 let t = Thought::new("x".into()).with_importance(1.5);
230 assert!((t.importance - 1.0).abs() < f32::EPSILON);
231
232 let t = Thought::new("x".into()).with_importance(-0.5);
233 assert!((t.importance - 0.0).abs() < f32::EPSILON);
234 }
235}