Skip to main content

agent_office/services/kb/
mod.rs

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/// Simplified Knowledge Base Service - shared across all agents, uses only Luhmann IDs
30#[async_trait]
31pub trait KnowledgeBaseService: Send + Sync {
32    // Core note operations (all use LuhmannId)
33    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    // Search
58    async fn search_notes(&self, query: &str) -> Result<Vec<Note>>;
59    
60    // Link operations
61    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    // Note relationships
71    async fn mark_continuation(&self, from_id: &LuhmannId, to_id: &LuhmannId) -> Result<()>;
72    
73    // Index operations
74    async fn create_index(&self, parent_id: &LuhmannId) -> Result<Note>;
75    
76    // Get full context of a note
77    async fn get_context(&self, note_id: &LuhmannId) -> Result<NoteContext>;
78}
79
80/// Full context of a note including all relationships
81#[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
92/// Convert a LuhmannId to a NodeId for storage
93fn 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    /// Convert LuhmannId to storage NodeId
107    fn to_node_id(&self, luhmann_id: &LuhmannId) -> NodeId {
108        luhmann_to_node_id(luhmann_id)
109    }
110
111    /// Get or initialize the note counter
112    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                // Create new counter
121                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    /// Update the counter
131    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    /// Generate next available top-level ID
138    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    /// Find the next available child ID under a parent
146    async fn next_child_id(&self, parent_id: &LuhmannId) -> Result<LuhmannId> {
147        let all_notes = self.list_notes().await?;
148        
149        // Collect existing children
150        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            // First child
158            Ok(parent_id.first_child())
159        } else {
160            // Find the next sibling after the last child
161            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        // Generate next available top-level Luhmann ID
177        let luhmann_id = self.next_main_id().await?;
178        
179        // Check if note already exists
180        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(_)) => (), // Good, doesn't exist
184            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        // Check if note already exists
201        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(_)) => (), // Good, doesn't exist
205            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        // Verify parent exists
222        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        // Generate child ID
230        let child_id = self.next_child_id(parent_id).await?;
231        
232        // Check if child already exists
233        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(_)) => (), // Good, doesn't exist
237            Err(e) => return Err(KbError::Storage(e)),
238        }
239        
240        // Create the note
241        let note = Note::new(child_id.clone(), title, content);
242        let node = note.to_node();
243        self.storage.create_node(&node).await?;
244        
245        // Create link to parent
246        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        // Query all nodes of type "note"
274        let query = SearchQuery {
275            node_types: vec!["note".to_string()],
276            limit: 10000, // Large limit to get all notes
277            ..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        // Sort by LuhmannId for consistent ordering
288        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        // Verify both notes exist
332        self.get_note(from_id).await?;
333        self.get_note(to_id).await?;
334        
335        // Create link edge
336        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        // Get outgoing edges
356        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            // Get the target note by looking up the node and converting it
361            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, // Skip notes that can't be found
380            }
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        // Verify both notes exist
392        self.get_note(from_id).await?;
393        self.get_note(to_id).await?;
394        
395        // Create "continues" edge
396        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        // Verify parent exists
412        let parent_note = self.get_note(parent_id).await?;
413        
414        // Find all direct children (notes that are immediate descendants)
415        let all_notes = self.list_notes().await?;
416        
417        let children: Vec<&Note> = all_notes
418            .iter()
419            .filter(|note| {
420                // Check if note's parent is the parent_id
421                note.id.parent().as_ref() == Some(parent_id)
422            })
423            .collect();
424        
425        // Create index note ID: {parent_id}0 (e.g., 1a -> 1a0)
426        let index_id = LuhmannId::parse(&format!("{}0", parent_id))
427            .ok_or_else(|| KbError::InvalidLuhmannId(format!("{}0", parent_id)))?;
428        
429        // Check if index already exists
430        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(_)) => (), // Good, doesn't exist
434            Err(e) => return Err(KbError::Storage(e)),
435        }
436        
437        // Build index content
438        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        // Create the index note
451        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        // Create "child_of" relationship to parent
461        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        // Get the note itself
477        let note = self.get_note(note_id).await?;
478        
479        // Get parent (if any)
480        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        // Get all children (direct descendants)
487        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        // Get links (notes this note links TO)
494        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        // Get backlinks (notes that link TO this note via "references" edges)
503        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        // Get continuations (notes this note "continues" to)
515        // Need to query for "continues" edges
516        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        // Get notes that continue FROM this note (reverse of continues)
533        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        // First note should get ID "1"
571        let note1 = kb.create_note("First Note", "Content 1").await.unwrap();
572        assert_eq!(note1.id.to_string(), "1");
573        
574        // Second note should get ID "2"
575        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        // Create parent note "1"
595        let parent_id = LuhmannId::parse("1").unwrap();
596        kb.create_note_with_id(parent_id.clone(), "Parent", "Parent content").await.unwrap();
597        
598        // Create branch - should get ID "1a"
599        let child = kb.create_branch(&parent_id, "Child", "Child content").await.unwrap();
600        assert_eq!(child.id.to_string(), "1a");
601        
602        // Create another branch - should get ID "1b"
603        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        // Creating with same ID should fail
616        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(&note.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        // Should be sorted by Luhmann ID
644        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); // 1a and 1a1
665        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(&note1.id, &note2.id, Some("See also".to_string())).await.unwrap();
691        
692        let links = kb.get_links(&note1.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(&note.id, &note.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        // Mark note 1 as continuing to note 2
720        kb.mark_continuation(&id1, &id2).await.unwrap();
721        
722        // Self-continuation should fail
723        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        // Create parent and children
733        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        // Create grandchild (should not appear in index)
743        let grandchild_id = LuhmannId::parse("1a1").unwrap();
744        kb.create_note_with_id(grandchild_id.clone(), "Grandchild", "Grandchild content").await.unwrap();
745        
746        // Create index
747        let index = kb.create_index(&parent_id).await.unwrap();
748        
749        // Index ID should be {parent_id}0
750        assert_eq!(index.id.to_string(), "10");
751        // Should contain references to children
752        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        // Should NOT contain grandchild
757        assert!(!index.content.contains("1a1"));
758    }
759}