1use crate::domain::{Edge, Properties, PropertyValue, string_to_node_id, NodeId};
2use crate::services::kb::domain::{LinkType, LuhmannId, Note, NoteId, NoteLink, NoteCounter};
3use crate::storage::{GraphStorage, StorageError, SearchQuery, EdgeDirection};
4use async_trait::async_trait;
5use thiserror::Error;
6
7pub mod domain;
8
9#[derive(Error, Debug)]
10pub enum KbError {
11 #[error("Note not found: {0}")]
12 NoteNotFound(NoteId),
13
14 #[error("Note already exists: {0}")]
15 NoteAlreadyExists(NoteId),
16
17 #[error("Invalid Luhmann ID: {0}")]
18 InvalidLuhmannId(String),
19
20 #[error("Storage error: {0}")]
21 Storage(#[from] StorageError),
22
23 #[error("Cannot link note to itself")]
24 SelfLink,
25}
26
27pub type Result<T> = std::result::Result<T, KbError>;
28
29#[async_trait]
31pub trait KnowledgeBaseService: Send + Sync {
32 async fn create_note(
34 &self,
35 title: impl Into<String> + Send,
36 content: impl Into<String> + Send,
37 ) -> Result<Note>;
38
39 async fn create_note_with_id(
40 &self,
41 id: LuhmannId,
42 title: impl Into<String> + Send,
43 content: impl Into<String> + Send,
44 ) -> Result<Note>;
45
46 async fn create_branch(
47 &self,
48 parent_id: &LuhmannId,
49 title: impl Into<String> + Send,
50 content: impl Into<String> + Send,
51 ) -> Result<Note>;
52
53 async fn get_note(&self, note_id: &LuhmannId) -> Result<Note>;
54 async fn list_notes(&self) -> Result<Vec<Note>>;
55 async fn list_notes_by_prefix(&self, prefix: &LuhmannId) -> Result<Vec<Note>>;
56
57 async fn search_notes(&self, query: &str) -> Result<Vec<Note>>;
59
60 async fn link_notes(
62 &self,
63 from_id: &LuhmannId,
64 to_id: &LuhmannId,
65 context: Option<String>,
66 ) -> Result<()>;
67
68 async fn get_links(&self, note_id: &LuhmannId) -> Result<Vec<NoteLink>>;
69
70 async fn mark_continuation(&self, from_id: &LuhmannId, to_id: &LuhmannId) -> Result<()>;
72
73 async fn create_index(&self, parent_id: &LuhmannId) -> Result<Note>;
75
76 async fn get_context(&self, note_id: &LuhmannId) -> Result<NoteContext>;
78}
79
80#[derive(Debug, Clone)]
82pub struct NoteContext {
83 pub note: Note,
84 pub parent: Option<Note>,
85 pub children: Vec<Note>,
86 pub links_to: Vec<Note>,
87 pub backlinks: Vec<Note>,
88 pub continues_to: Vec<Note>,
89 pub continued_from: Vec<Note>,
90}
91
92fn luhmann_to_node_id(luhmann_id: &LuhmannId) -> NodeId {
94 string_to_node_id(&luhmann_id.to_string())
95}
96
97pub struct KnowledgeBaseServiceImpl<S: GraphStorage> {
98 storage: S,
99}
100
101impl<S: GraphStorage> KnowledgeBaseServiceImpl<S> {
102 pub fn new(storage: S) -> Self {
103 Self { storage }
104 }
105
106 fn to_node_id(&self, luhmann_id: &LuhmannId) -> NodeId {
108 luhmann_to_node_id(luhmann_id)
109 }
110
111 async fn get_or_init_counter(&self) -> Result<NoteCounter> {
113 let counter_id = string_to_node_id("__kb_counter__");
114 match self.storage.get_node(counter_id).await {
115 Ok(node) => {
116 NoteCounter::from_node(&node)
117 .ok_or_else(|| KbError::Storage(StorageError::ConstraintViolation("Invalid counter node".to_string())))
118 }
119 Err(StorageError::NodeNotFound(_)) => {
120 let counter = NoteCounter::new();
122 let node = counter.to_node();
123 self.storage.create_node(&node).await?;
124 Ok(counter)
125 }
126 Err(e) => Err(KbError::Storage(e)),
127 }
128 }
129
130 async fn update_counter(&self, counter: &NoteCounter) -> Result<()> {
132 let node = counter.to_node();
133 self.storage.update_node(&node).await?;
134 Ok(())
135 }
136
137 async fn next_main_id(&self) -> Result<LuhmannId> {
139 let mut counter = self.get_or_init_counter().await?;
140 let id = counter.next_main_topic_id();
141 self.update_counter(&counter).await?;
142 Ok(id)
143 }
144
145 async fn next_child_id(&self, parent_id: &LuhmannId) -> Result<LuhmannId> {
147 let all_notes = self.list_notes().await?;
148
149 let mut children: Vec<LuhmannId> = all_notes
151 .into_iter()
152 .map(|n| n.id)
153 .filter(|id| id.parent().as_ref() == Some(parent_id))
154 .collect();
155
156 if children.is_empty() {
157 Ok(parent_id.first_child())
159 } else {
160 children.sort();
162 let last = children.last().unwrap();
163 Ok(last.next_sibling()
164 .unwrap_or_else(|| last.first_child()))
165 }
166 }
167}
168
169#[async_trait]
170impl<S: GraphStorage> KnowledgeBaseService for KnowledgeBaseServiceImpl<S> {
171 async fn create_note(
172 &self,
173 title: impl Into<String> + Send,
174 content: impl Into<String> + Send,
175 ) -> Result<Note> {
176 let luhmann_id = self.next_main_id().await?;
178
179 let node_id = self.to_node_id(&luhmann_id);
181 match self.storage.get_node(node_id).await {
182 Ok(_) => return Err(KbError::NoteAlreadyExists(luhmann_id)),
183 Err(StorageError::NodeNotFound(_)) => (), Err(e) => return Err(KbError::Storage(e)),
185 }
186
187 let note = Note::new(luhmann_id, title, content);
188 let node = note.to_node();
189 self.storage.create_node(&node).await?;
190
191 Ok(note)
192 }
193
194 async fn create_note_with_id(
195 &self,
196 id: LuhmannId,
197 title: impl Into<String> + Send,
198 content: impl Into<String> + Send,
199 ) -> Result<Note> {
200 let node_id = self.to_node_id(&id);
202 match self.storage.get_node(node_id).await {
203 Ok(_) => return Err(KbError::NoteAlreadyExists(id)),
204 Err(StorageError::NodeNotFound(_)) => (), Err(e) => return Err(KbError::Storage(e)),
206 }
207
208 let note = Note::new(id, title, content);
209 let node = note.to_node();
210 self.storage.create_node(&node).await?;
211
212 Ok(note)
213 }
214
215 async fn create_branch(
216 &self,
217 parent_id: &LuhmannId,
218 title: impl Into<String> + Send,
219 content: impl Into<String> + Send,
220 ) -> Result<Note> {
221 let parent_node_id = self.to_node_id(parent_id);
223 self.storage.get_node(parent_node_id).await
224 .map_err(|e| match e {
225 StorageError::NodeNotFound(_) => KbError::NoteNotFound(parent_id.clone()),
226 _ => KbError::Storage(e),
227 })?;
228
229 let child_id = self.next_child_id(parent_id).await?;
231
232 let child_node_id = self.to_node_id(&child_id);
234 match self.storage.get_node(child_node_id).await {
235 Ok(_) => return Err(KbError::NoteAlreadyExists(child_id)),
236 Err(StorageError::NodeNotFound(_)) => (), Err(e) => return Err(KbError::Storage(e)),
238 }
239
240 let note = Note::new(child_id.clone(), title, content);
242 let node = note.to_node();
243 self.storage.create_node(&node).await?;
244
245 let mut props = Properties::new();
247 props.insert("context".to_string(), PropertyValue::String(format!("Branch of {}", parent_id)));
248
249 let edge = Edge::new(
250 "references",
251 self.to_node_id(&child_id),
252 parent_node_id,
253 props,
254 );
255 self.storage.create_edge(&edge).await?;
256
257 Ok(note)
258 }
259
260 async fn get_note(&self, note_id: &LuhmannId) -> Result<Note> {
261 let node_id = self.to_node_id(note_id);
262 let node = self.storage.get_node(node_id).await
263 .map_err(|e| match e {
264 StorageError::NodeNotFound(_) => KbError::NoteNotFound(note_id.clone()),
265 _ => KbError::Storage(e),
266 })?;
267
268 Note::from_node(&node)
269 .ok_or_else(|| KbError::NoteNotFound(note_id.clone()))
270 }
271
272 async fn list_notes(&self) -> Result<Vec<Note>> {
273 let query = SearchQuery {
275 node_types: vec!["note".to_string()],
276 limit: 10000, ..SearchQuery::default()
278 };
279
280 let results = self.storage.search_nodes(&query).await?;
281
282 let mut notes: Vec<Note> = results.items
283 .into_iter()
284 .filter_map(|node| Note::from_node(&node))
285 .collect();
286
287 notes.sort_by(|a, b| a.id.cmp(&b.id));
289
290 Ok(notes)
291 }
292
293 async fn list_notes_by_prefix(&self, prefix: &LuhmannId) -> Result<Vec<Note>> {
294 let all_notes = self.list_notes().await?;
295
296 let filtered: Vec<Note> = all_notes
297 .into_iter()
298 .filter(|note| {
299 note.id == *prefix || note.id.is_descendant_of(prefix)
300 })
301 .collect();
302
303 Ok(filtered)
304 }
305
306 async fn search_notes(&self, query: &str) -> Result<Vec<Note>> {
307 let all_notes = self.list_notes().await?;
308 let query_lower = query.to_lowercase();
309
310 let filtered: Vec<Note> = all_notes.into_iter()
311 .filter(|note| {
312 note.title.to_lowercase().contains(&query_lower) ||
313 note.content.to_lowercase().contains(&query_lower) ||
314 note.tags.iter().any(|tag| tag.to_lowercase().contains(&query_lower))
315 })
316 .collect();
317
318 Ok(filtered)
319 }
320
321 async fn link_notes(
322 &self,
323 from_id: &LuhmannId,
324 to_id: &LuhmannId,
325 context: Option<String>,
326 ) -> Result<()> {
327 if from_id == to_id {
328 return Err(KbError::SelfLink);
329 }
330
331 self.get_note(from_id).await?;
333 self.get_note(to_id).await?;
334
335 let mut props = Properties::new();
337 if let Some(ctx) = context {
338 props.insert("context".to_string(), PropertyValue::String(ctx));
339 }
340
341 let edge = Edge::new(
342 "references",
343 self.to_node_id(from_id),
344 self.to_node_id(to_id),
345 props,
346 );
347
348 self.storage.create_edge(&edge).await?;
349 Ok(())
350 }
351
352 async fn get_links(&self, note_id: &LuhmannId) -> Result<Vec<NoteLink>> {
353 let node_id = self.to_node_id(note_id);
354
355 let edges = self.storage.get_edges_from(node_id, Some("references")).await?;
357
358 let mut links = Vec::new();
359 for edge in edges {
360 match self.storage.get_node(edge.to_node_id).await {
362 Ok(target_node) => {
363 if let Some(target_id) = target_node.properties.get("luhmann_id")
364 .and_then(|v| v.as_str())
365 .and_then(|s| LuhmannId::parse(s))
366 {
367 let context = edge.properties.get("context")
368 .and_then(|v| v.as_str())
369 .map(String::from);
370
371 links.push(NoteLink::new(
372 note_id.clone(),
373 target_id,
374 LinkType::References,
375 context,
376 ));
377 }
378 }
379 Err(_) => continue, }
381 }
382
383 Ok(links)
384 }
385
386 async fn mark_continuation(&self, from_id: &LuhmannId, to_id: &LuhmannId) -> Result<()> {
387 if from_id == to_id {
388 return Err(KbError::SelfLink);
389 }
390
391 self.get_note(from_id).await?;
393 self.get_note(to_id).await?;
394
395 let mut props = Properties::new();
397 props.insert("context".to_string(), PropertyValue::String("Continues on next note".to_string()));
398
399 let edge = Edge::new(
400 "continues",
401 self.to_node_id(from_id),
402 self.to_node_id(to_id),
403 props,
404 );
405
406 self.storage.create_edge(&edge).await?;
407 Ok(())
408 }
409
410 async fn create_index(&self, parent_id: &LuhmannId) -> Result<Note> {
411 let parent_note = self.get_note(parent_id).await?;
413
414 let all_notes = self.list_notes().await?;
416
417 let children: Vec<&Note> = all_notes
418 .iter()
419 .filter(|note| {
420 note.id.parent().as_ref() == Some(parent_id)
422 })
423 .collect();
424
425 let index_id = LuhmannId::parse(&format!("{}0", parent_id))
427 .ok_or_else(|| KbError::InvalidLuhmannId(format!("{}0", parent_id)))?;
428
429 let index_node_id = self.to_node_id(&index_id);
431 match self.storage.get_node(index_node_id).await {
432 Ok(_) => return Err(KbError::NoteAlreadyExists(index_id)),
433 Err(StorageError::NodeNotFound(_)) => (), Err(e) => return Err(KbError::Storage(e)),
435 }
436
437 let mut content = format!("# Index: {}\n\n", parent_note.title);
439 content.push_str(&format!("Parent note: [[{}]]\n\n", parent_id));
440 content.push_str("Children:\n\n");
441
442 if children.is_empty() {
443 content.push_str("(No children)\n");
444 } else {
445 for child in &children {
446 content.push_str(&format!("- [[{}]]: {}\n", child.id, child.title));
447 }
448 }
449
450 let index_note = Note::new(
452 index_id.clone(),
453 format!("Index: {}", parent_note.title),
454 content,
455 );
456
457 let node = index_note.to_node();
458 self.storage.create_node(&node).await?;
459
460 let mut props = Properties::new();
462 props.insert("context".to_string(), PropertyValue::String("Index of children".to_string()));
463
464 let edge = Edge::new(
465 "child_of",
466 index_node_id,
467 self.to_node_id(parent_id),
468 props,
469 );
470 self.storage.create_edge(&edge).await?;
471
472 Ok(index_note)
473 }
474
475 async fn get_context(&self, note_id: &LuhmannId) -> Result<NoteContext> {
476 let note = self.get_note(note_id).await?;
478
479 let parent = if let Some(parent_id) = note.id.parent() {
481 self.get_note(&parent_id).await.ok()
482 } else {
483 None
484 };
485
486 let all_notes = self.list_notes().await?;
488 let children: Vec<Note> = all_notes
489 .into_iter()
490 .filter(|n| n.id.parent().as_ref() == Some(note_id))
491 .collect();
492
493 let links = self.get_links(note_id).await?;
495 let mut links_to = Vec::new();
496 for link in &links {
497 if let Ok(target_note) = self.get_note(&link.to_note_id).await {
498 links_to.push(target_note);
499 }
500 }
501
502 let node_id = self.to_node_id(note_id);
504 let edges = self.storage.get_edges_to(node_id, Some("references")).await?;
505 let mut backlinks = Vec::new();
506 for edge in edges {
507 if let Ok(source_node) = self.storage.get_node(edge.from_node_id).await {
508 if let Some(note) = Note::from_node(&source_node) {
509 backlinks.push(note);
510 }
511 }
512 }
513
514 let note_node_id = self.to_node_id(note_id);
517 let neighbors = self.storage
518 .get_neighbors(note_node_id, Some("continues"), EdgeDirection::Outgoing)
519 .await?;
520
521 let mut continues_to = Vec::new();
522 for node in neighbors {
523 if let Some(luhmann_str) = node.get_property("luhmann_id").and_then(|v| v.as_str()) {
524 if let Some(target_id) = LuhmannId::parse(luhmann_str) {
525 if let Ok(target_note) = self.get_note(&target_id).await {
526 continues_to.push(target_note);
527 }
528 }
529 }
530 }
531
532 let incoming_neighbors = self.storage
534 .get_neighbors(note_node_id, Some("continues"), EdgeDirection::Incoming)
535 .await?;
536
537 let mut continued_from = Vec::new();
538 for node in incoming_neighbors {
539 if let Some(luhmann_str) = node.get_property("luhmann_id").and_then(|v| v.as_str()) {
540 if let Some(source_id) = LuhmannId::parse(luhmann_str) {
541 if let Ok(source_note) = self.get_note(&source_id).await {
542 continued_from.push(source_note);
543 }
544 }
545 }
546 }
547
548 Ok(NoteContext {
549 note,
550 parent,
551 children,
552 links_to,
553 backlinks,
554 continues_to,
555 continued_from,
556 })
557 }
558}
559
560#[cfg(test)]
561mod tests {
562 use super::*;
563 use crate::storage::memory::InMemoryStorage;
564
565 #[tokio::test]
566 async fn test_create_note_auto_id() {
567 let storage = InMemoryStorage::new();
568 let kb = KnowledgeBaseServiceImpl::new(storage);
569
570 let note1 = kb.create_note("First Note", "Content 1").await.unwrap();
572 assert_eq!(note1.id.to_string(), "1");
573
574 let note2 = kb.create_note("Second Note", "Content 2").await.unwrap();
576 assert_eq!(note2.id.to_string(), "2");
577 }
578
579 #[tokio::test]
580 async fn test_create_note_with_specific_id() {
581 let storage = InMemoryStorage::new();
582 let kb = KnowledgeBaseServiceImpl::new(storage);
583
584 let id = LuhmannId::parse("1a").unwrap();
585 let note = kb.create_note_with_id(id.clone(), "Note 1a", "Content").await.unwrap();
586 assert_eq!(note.id, id);
587 }
588
589 #[tokio::test]
590 async fn test_create_branch() {
591 let storage = InMemoryStorage::new();
592 let kb = KnowledgeBaseServiceImpl::new(storage);
593
594 let parent_id = LuhmannId::parse("1").unwrap();
596 kb.create_note_with_id(parent_id.clone(), "Parent", "Parent content").await.unwrap();
597
598 let child = kb.create_branch(&parent_id, "Child", "Child content").await.unwrap();
600 assert_eq!(child.id.to_string(), "1a");
601
602 let child2 = kb.create_branch(&parent_id, "Child 2", "Child content 2").await.unwrap();
604 assert_eq!(child2.id.to_string(), "1b");
605 }
606
607 #[tokio::test]
608 async fn test_duplicate_id_fails() {
609 let storage = InMemoryStorage::new();
610 let kb = KnowledgeBaseServiceImpl::new(storage);
611
612 let id = LuhmannId::parse("1").unwrap();
613 kb.create_note_with_id(id.clone(), "First", "Content").await.unwrap();
614
615 let result = kb.create_note_with_id(id, "Second", "Content").await;
617 assert!(matches!(result, Err(KbError::NoteAlreadyExists(_))));
618 }
619
620 #[tokio::test]
621 async fn test_get_note() {
622 let storage = InMemoryStorage::new();
623 let kb = KnowledgeBaseServiceImpl::new(storage);
624
625 let note = kb.create_note("Test", "Content").await.unwrap();
626 let retrieved = kb.get_note(¬e.id).await.unwrap();
627
628 assert_eq!(retrieved.title, "Test");
629 assert_eq!(retrieved.content, "Content");
630 }
631
632 #[tokio::test]
633 async fn test_list_notes() {
634 let storage = InMemoryStorage::new();
635 let kb = KnowledgeBaseServiceImpl::new(storage);
636
637 kb.create_note_with_id(LuhmannId::parse("2").unwrap(), "Second", "Content").await.unwrap();
638 kb.create_note_with_id(LuhmannId::parse("1").unwrap(), "First", "Content").await.unwrap();
639 kb.create_note_with_id(LuhmannId::parse("1a").unwrap(), "Child", "Content").await.unwrap();
640
641 let notes = kb.list_notes().await.unwrap();
642
643 assert_eq!(notes.len(), 3);
645 assert_eq!(notes[0].id.to_string(), "1");
646 assert_eq!(notes[1].id.to_string(), "1a");
647 assert_eq!(notes[2].id.to_string(), "2");
648 }
649
650 #[tokio::test]
651 async fn test_list_by_prefix() {
652 let storage = InMemoryStorage::new();
653 let kb = KnowledgeBaseServiceImpl::new(storage);
654
655 kb.create_note_with_id(LuhmannId::parse("1").unwrap(), "One", "Content").await.unwrap();
656 kb.create_note_with_id(LuhmannId::parse("1a").unwrap(), "One-A", "Content").await.unwrap();
657 kb.create_note_with_id(LuhmannId::parse("1a1").unwrap(), "One-A-One", "Content").await.unwrap();
658 kb.create_note_with_id(LuhmannId::parse("1b").unwrap(), "One-B", "Content").await.unwrap();
659 kb.create_note_with_id(LuhmannId::parse("2").unwrap(), "Two", "Content").await.unwrap();
660
661 let prefix = LuhmannId::parse("1a").unwrap();
662 let notes = kb.list_notes_by_prefix(&prefix).await.unwrap();
663
664 assert_eq!(notes.len(), 2); assert!(notes.iter().any(|n| n.id.to_string() == "1a"));
666 assert!(notes.iter().any(|n| n.id.to_string() == "1a1"));
667 }
668
669 #[tokio::test]
670 async fn test_search_notes() {
671 let storage = InMemoryStorage::new();
672 let kb = KnowledgeBaseServiceImpl::new(storage);
673
674 kb.create_note("Rust Programming", "A systems language").await.unwrap();
675 kb.create_note("Python Basics", "Easy to learn").await.unwrap();
676 kb.create_note("Rust vs Go", "Comparison").await.unwrap();
677
678 let results = kb.search_notes("rust").await.unwrap();
679 assert_eq!(results.len(), 2);
680 }
681
682 #[tokio::test]
683 async fn test_link_notes() {
684 let storage = InMemoryStorage::new();
685 let kb = KnowledgeBaseServiceImpl::new(storage);
686
687 let note1 = kb.create_note("First", "Content").await.unwrap();
688 let note2 = kb.create_note("Second", "Content").await.unwrap();
689
690 kb.link_notes(¬e1.id, ¬e2.id, Some("See also".to_string())).await.unwrap();
691
692 let links = kb.get_links(¬e1.id).await.unwrap();
693 assert_eq!(links.len(), 1);
694 assert_eq!(links[0].to_note_id, note2.id);
695 }
696
697 #[tokio::test]
698 async fn test_self_link_fails() {
699 let storage = InMemoryStorage::new();
700 let kb = KnowledgeBaseServiceImpl::new(storage);
701
702 let note = kb.create_note("Note", "Content").await.unwrap();
703
704 let result = kb.link_notes(¬e.id, ¬e.id, None).await;
705 assert!(matches!(result, Err(KbError::SelfLink)));
706 }
707
708 #[tokio::test]
709 async fn test_mark_continuation() {
710 let storage = InMemoryStorage::new();
711 let kb = KnowledgeBaseServiceImpl::new(storage);
712
713 let id1 = LuhmannId::parse("1").unwrap();
714 let id2 = LuhmannId::parse("2").unwrap();
715
716 kb.create_note_with_id(id1.clone(), "First", "Content 1").await.unwrap();
717 kb.create_note_with_id(id2.clone(), "Second", "Content 2").await.unwrap();
718
719 kb.mark_continuation(&id1, &id2).await.unwrap();
721
722 let result = kb.mark_continuation(&id1, &id1).await;
724 assert!(matches!(result, Err(KbError::SelfLink)));
725 }
726
727 #[tokio::test]
728 async fn test_create_index() {
729 let storage = InMemoryStorage::new();
730 let kb = KnowledgeBaseServiceImpl::new(storage);
731
732 let parent_id = LuhmannId::parse("1").unwrap();
734 kb.create_note_with_id(parent_id.clone(), "Parent Note", "Parent content").await.unwrap();
735
736 let child1_id = LuhmannId::parse("1a").unwrap();
737 kb.create_note_with_id(child1_id.clone(), "First Child", "Child 1 content").await.unwrap();
738
739 let child2_id = LuhmannId::parse("1b").unwrap();
740 kb.create_note_with_id(child2_id.clone(), "Second Child", "Child 2 content").await.unwrap();
741
742 let grandchild_id = LuhmannId::parse("1a1").unwrap();
744 kb.create_note_with_id(grandchild_id.clone(), "Grandchild", "Grandchild content").await.unwrap();
745
746 let index = kb.create_index(&parent_id).await.unwrap();
748
749 assert_eq!(index.id.to_string(), "10");
751 assert!(index.content.contains("1a"));
753 assert!(index.content.contains("1b"));
754 assert!(index.content.contains("First Child"));
755 assert!(index.content.contains("Second Child"));
756 assert!(!index.content.contains("1a1"));
758 }
759}