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 update_note(&self, note: &Note) -> Result<Note>;
55    async fn delete_note(&self, note_id: &LuhmannId) -> Result<()>;
56    async fn list_notes(&self) -> Result<Vec<Note>>;
57    async fn list_notes_by_prefix(&self, prefix: &LuhmannId) -> Result<Vec<Note>>;
58    
59    // Search
60    async fn search_notes(&self, query: &str) -> Result<Vec<Note>>;
61    
62    // Link operations
63    async fn link_notes(
64        &self,
65        from_id: &LuhmannId,
66        to_id: &LuhmannId,
67        context: Option<String>,
68    ) -> Result<()>;
69    
70    async fn get_links(&self, note_id: &LuhmannId) -> Result<Vec<NoteLink>>;
71    
72    // Note relationships
73    async fn mark_continuation(&self, from_id: &LuhmannId, to_id: &LuhmannId) -> Result<()>;
74    
75    // Index operations
76    async fn create_index(&self, parent_id: &LuhmannId) -> Result<Note>;
77    
78    // Get full context of a note
79    async fn get_context(&self, note_id: &LuhmannId) -> Result<NoteContext>;
80}
81
82/// Full context of a note including all relationships
83#[derive(Debug, Clone)]
84pub struct NoteContext {
85    pub note: Note,
86    pub parent: Option<Note>,
87    pub children: Vec<Note>,
88    pub links_to: Vec<Note>,
89    pub backlinks: Vec<Note>,
90    pub continues_to: Vec<Note>,
91    pub continued_from: Vec<Note>,
92}
93
94/// Represents a subgraph of related notes
95#[derive(Debug, Clone)]
96pub struct NoteGraph {
97    pub center_note_id: NoteId,
98    pub notes: Vec<Note>,
99    pub links: Vec<NoteLink>,
100}
101
102/// Convert a LuhmannId to a NodeId for storage
103fn luhmann_to_node_id(luhmann_id: &LuhmannId) -> NodeId {
104    string_to_node_id(&luhmann_id.to_string())
105}
106
107pub struct KnowledgeBaseServiceImpl<S: GraphStorage> {
108    storage: S,
109}
110
111impl<S: GraphStorage> KnowledgeBaseServiceImpl<S> {
112    pub fn new(storage: S) -> Self {
113        Self { storage }
114    }
115
116    /// Convert LuhmannId to storage NodeId
117    fn to_node_id(&self, luhmann_id: &LuhmannId) -> NodeId {
118        luhmann_to_node_id(luhmann_id)
119    }
120
121    /// Get or initialize the note counter
122    async fn get_or_init_counter(&self) -> Result<NoteCounter> {
123        let counter_id = string_to_node_id("__kb_counter__");
124        match self.storage.get_node(counter_id).await {
125            Ok(node) => {
126                NoteCounter::from_node(&node)
127                    .ok_or_else(|| KbError::Storage(StorageError::ConstraintViolation("Invalid counter node".to_string())))
128            }
129            Err(StorageError::NodeNotFound(_)) => {
130                // Create new counter
131                let counter = NoteCounter::new();
132                let node = counter.to_node();
133                self.storage.create_node(&node).await?;
134                Ok(counter)
135            }
136            Err(e) => Err(KbError::Storage(e)),
137        }
138    }
139
140    /// Update the counter
141    async fn update_counter(&self, counter: &NoteCounter) -> Result<()> {
142        let node = counter.to_node();
143        self.storage.update_node(&node).await?;
144        Ok(())
145    }
146
147    /// Generate next available top-level ID
148    async fn next_main_id(&self) -> Result<LuhmannId> {
149        let mut counter = self.get_or_init_counter().await?;
150        let id = counter.next_main_topic_id();
151        self.update_counter(&counter).await?;
152        Ok(id)
153    }
154
155    /// Find the next available child ID under a parent
156    async fn next_child_id(&self, parent_id: &LuhmannId) -> Result<LuhmannId> {
157        let all_notes = self.list_notes().await?;
158        
159        // Collect existing children
160        let mut children: Vec<LuhmannId> = all_notes
161            .into_iter()
162            .map(|n| n.id)
163            .filter(|id| id.parent().as_ref() == Some(parent_id))
164            .collect();
165        
166        if children.is_empty() {
167            // First child
168            Ok(parent_id.first_child())
169        } else {
170            // Find the next sibling after the last child
171            children.sort();
172            let last = children.last().unwrap();
173            Ok(last.next_sibling()
174                .unwrap_or_else(|| last.first_child()))
175        }
176    }
177}
178
179#[async_trait]
180impl<S: GraphStorage> KnowledgeBaseService for KnowledgeBaseServiceImpl<S> {
181    async fn create_note(
182        &self,
183        title: impl Into<String> + Send,
184        content: impl Into<String> + Send,
185    ) -> Result<Note> {
186        // Generate next available top-level Luhmann ID
187        let luhmann_id = self.next_main_id().await?;
188        
189        // Check if note already exists
190        let node_id = self.to_node_id(&luhmann_id);
191        match self.storage.get_node(node_id).await {
192            Ok(_) => return Err(KbError::NoteAlreadyExists(luhmann_id)),
193            Err(StorageError::NodeNotFound(_)) => (), // Good, doesn't exist
194            Err(e) => return Err(KbError::Storage(e)),
195        }
196        
197        let note = Note::new(luhmann_id, title, content);
198        let node = note.to_node();
199        self.storage.create_node(&node).await?;
200        
201        Ok(note)
202    }
203
204    async fn create_note_with_id(
205        &self,
206        id: LuhmannId,
207        title: impl Into<String> + Send,
208        content: impl Into<String> + Send,
209    ) -> Result<Note> {
210        // Check if note already exists
211        let node_id = self.to_node_id(&id);
212        match self.storage.get_node(node_id).await {
213            Ok(_) => return Err(KbError::NoteAlreadyExists(id)),
214            Err(StorageError::NodeNotFound(_)) => (), // Good, doesn't exist
215            Err(e) => return Err(KbError::Storage(e)),
216        }
217        
218        let note = Note::new(id, title, content);
219        let node = note.to_node();
220        self.storage.create_node(&node).await?;
221        
222        Ok(note)
223    }
224
225    async fn create_branch(
226        &self,
227        parent_id: &LuhmannId,
228        title: impl Into<String> + Send,
229        content: impl Into<String> + Send,
230    ) -> Result<Note> {
231        // Verify parent exists
232        let parent_node_id = self.to_node_id(parent_id);
233        self.storage.get_node(parent_node_id).await
234            .map_err(|e| match e {
235                StorageError::NodeNotFound(_) => KbError::NoteNotFound(parent_id.clone()),
236                _ => KbError::Storage(e),
237            })?;
238        
239        // Generate child ID
240        let child_id = self.next_child_id(parent_id).await?;
241        
242        // Check if child already exists
243        let child_node_id = self.to_node_id(&child_id);
244        match self.storage.get_node(child_node_id).await {
245            Ok(_) => return Err(KbError::NoteAlreadyExists(child_id)),
246            Err(StorageError::NodeNotFound(_)) => (), // Good, doesn't exist
247            Err(e) => return Err(KbError::Storage(e)),
248        }
249        
250        // Create the note
251        let note = Note::new(child_id.clone(), title, content);
252        let node = note.to_node();
253        self.storage.create_node(&node).await?;
254        
255        // Create link to parent
256        let mut props = Properties::new();
257        props.insert("context".to_string(), PropertyValue::String(format!("Branch of {}", parent_id)));
258        
259        let edge = Edge::new(
260            "references",
261            self.to_node_id(&child_id),
262            parent_node_id,
263            props,
264        );
265        self.storage.create_edge(&edge).await?;
266        
267        Ok(note)
268    }
269
270    async fn get_note(&self, note_id: &LuhmannId) -> Result<Note> {
271        let node_id = self.to_node_id(note_id);
272        let node = self.storage.get_node(node_id).await
273            .map_err(|e| match e {
274                StorageError::NodeNotFound(_) => KbError::NoteNotFound(note_id.clone()),
275                _ => KbError::Storage(e),
276            })?;
277        
278        Note::from_node(&node)
279            .ok_or_else(|| KbError::NoteNotFound(note_id.clone()))
280    }
281
282    async fn update_note(&self, note: &Note) -> Result<Note> {
283        // Verify note exists
284        self.get_note(&note.id).await?;
285        
286        let node = note.to_node();
287        self.storage.update_node(&node).await?;
288        Ok(note.clone())
289    }
290
291    async fn delete_note(&self, note_id: &LuhmannId) -> Result<()> {
292        // Verify note exists
293        self.get_note(note_id).await?;
294        
295        let node_id = self.to_node_id(note_id);
296        // Delete the note (edges will be cascade deleted)
297        self.storage.delete_node(node_id).await?;
298        Ok(())
299    }
300
301    async fn list_notes(&self) -> Result<Vec<Note>> {
302        // Query all nodes of type "note"
303        let query = SearchQuery {
304            node_types: vec!["note".to_string()],
305            limit: 10000, // Large limit to get all notes
306            ..SearchQuery::default()
307        };
308        
309        let results = self.storage.search_nodes(&query).await?;
310        
311        let mut notes: Vec<Note> = results.items
312            .into_iter()
313            .filter_map(|node| Note::from_node(&node))
314            .collect();
315        
316        // Sort by LuhmannId for consistent ordering
317        notes.sort_by(|a, b| a.id.cmp(&b.id));
318        
319        Ok(notes)
320    }
321
322    async fn list_notes_by_prefix(&self, prefix: &LuhmannId) -> Result<Vec<Note>> {
323        let all_notes = self.list_notes().await?;
324        
325        let filtered: Vec<Note> = all_notes
326            .into_iter()
327            .filter(|note| {
328                note.id == *prefix || note.id.is_descendant_of(prefix)
329            })
330            .collect();
331        
332        Ok(filtered)
333    }
334
335    async fn search_notes(&self, query: &str) -> Result<Vec<Note>> {
336        let all_notes = self.list_notes().await?;
337        let query_lower = query.to_lowercase();
338        
339        let filtered: Vec<Note> = all_notes.into_iter()
340            .filter(|note| {
341                note.title.to_lowercase().contains(&query_lower) ||
342                note.content.to_lowercase().contains(&query_lower) ||
343                note.tags.iter().any(|tag| tag.to_lowercase().contains(&query_lower))
344            })
345            .collect();
346        
347        Ok(filtered)
348    }
349
350    async fn link_notes(
351        &self,
352        from_id: &LuhmannId,
353        to_id: &LuhmannId,
354        context: Option<String>,
355    ) -> Result<()> {
356        if from_id == to_id {
357            return Err(KbError::SelfLink);
358        }
359        
360        // Verify both notes exist
361        self.get_note(from_id).await?;
362        self.get_note(to_id).await?;
363        
364        // Create link edge
365        let mut props = Properties::new();
366        if let Some(ctx) = context {
367            props.insert("context".to_string(), PropertyValue::String(ctx));
368        }
369        
370        let edge = Edge::new(
371            "references",
372            self.to_node_id(from_id),
373            self.to_node_id(to_id),
374            props,
375        );
376        
377        self.storage.create_edge(&edge).await?;
378        Ok(())
379    }
380
381    async fn get_links(&self, note_id: &LuhmannId) -> Result<Vec<NoteLink>> {
382        let node_id = self.to_node_id(note_id);
383        
384        // Get outgoing edges
385        let edges = self.storage.get_edges_from(node_id, Some("references")).await?;
386        
387        let mut links = Vec::new();
388        for edge in edges {
389            // Get the target note by looking up the node and converting it
390            match self.storage.get_node(edge.to_node_id).await {
391                Ok(target_node) => {
392                    if let Some(target_id) = target_node.properties.get("luhmann_id")
393                        .and_then(|v| v.as_str())
394                        .and_then(|s| LuhmannId::parse(s))
395                    {
396                        let context = edge.properties.get("context")
397                            .and_then(|v| v.as_str())
398                            .map(String::from);
399                        
400                        links.push(NoteLink::new(
401                            note_id.clone(),
402                            target_id,
403                            LinkType::References,
404                            context,
405                        ));
406                    }
407                }
408                Err(_) => continue, // Skip notes that can't be found
409            }
410        }
411        
412        Ok(links)
413    }
414
415    async fn mark_continuation(&self, from_id: &LuhmannId, to_id: &LuhmannId) -> Result<()> {
416        if from_id == to_id {
417            return Err(KbError::SelfLink);
418        }
419        
420        // Verify both notes exist
421        self.get_note(from_id).await?;
422        self.get_note(to_id).await?;
423        
424        // Create "continues" edge
425        let mut props = Properties::new();
426        props.insert("context".to_string(), PropertyValue::String("Continues on next note".to_string()));
427        
428        let edge = Edge::new(
429            "continues",
430            self.to_node_id(from_id),
431            self.to_node_id(to_id),
432            props,
433        );
434        
435        self.storage.create_edge(&edge).await?;
436        Ok(())
437    }
438
439    async fn create_index(&self, parent_id: &LuhmannId) -> Result<Note> {
440        // Verify parent exists
441        let parent_note = self.get_note(parent_id).await?;
442        
443        // Find all direct children (notes that are immediate descendants)
444        let all_notes = self.list_notes().await?;
445        
446        let children: Vec<&Note> = all_notes
447            .iter()
448            .filter(|note| {
449                // Check if note's parent is the parent_id
450                note.id.parent().as_ref() == Some(parent_id)
451            })
452            .collect();
453        
454        // Create index note ID: {parent_id}0 (e.g., 1a -> 1a0)
455        let index_id = LuhmannId::parse(&format!("{}0", parent_id))
456            .ok_or_else(|| KbError::InvalidLuhmannId(format!("{}0", parent_id)))?;
457        
458        // Check if index already exists
459        let index_node_id = self.to_node_id(&index_id);
460        match self.storage.get_node(index_node_id).await {
461            Ok(_) => return Err(KbError::NoteAlreadyExists(index_id)),
462            Err(StorageError::NodeNotFound(_)) => (), // Good, doesn't exist
463            Err(e) => return Err(KbError::Storage(e)),
464        }
465        
466        // Build index content
467        let mut content = format!("# Index: {}\n\n", parent_note.title);
468        content.push_str(&format!("Parent note: [[{}]]\n\n", parent_id));
469        content.push_str("Children:\n\n");
470        
471        if children.is_empty() {
472            content.push_str("(No children)\n");
473        } else {
474            for child in &children {
475                content.push_str(&format!("- [[{}]]: {}\n", child.id, child.title));
476            }
477        }
478        
479        // Create the index note
480        let index_note = Note::new(
481            index_id.clone(),
482            format!("Index: {}", parent_note.title),
483            content,
484        );
485        
486        let node = index_note.to_node();
487        self.storage.create_node(&node).await?;
488        
489        // Create "child_of" relationship to parent
490        let mut props = Properties::new();
491        props.insert("context".to_string(), PropertyValue::String("Index of children".to_string()));
492        
493        let edge = Edge::new(
494            "child_of",
495            index_node_id,
496            self.to_node_id(parent_id),
497            props,
498        );
499        self.storage.create_edge(&edge).await?;
500        
501        Ok(index_note)
502    }
503    
504    async fn get_context(&self, note_id: &LuhmannId) -> Result<NoteContext> {
505        // Get the note itself
506        let note = self.get_note(note_id).await?;
507        
508        // Get parent (if any)
509        let parent = if let Some(parent_id) = note.id.parent() {
510            self.get_note(&parent_id).await.ok()
511        } else {
512            None
513        };
514        
515        // Get all children (direct descendants)
516        let all_notes = self.list_notes().await?;
517        let children: Vec<Note> = all_notes
518            .into_iter()
519            .filter(|n| n.id.parent().as_ref() == Some(note_id))
520            .collect();
521        
522        // Get links (notes this note links TO)
523        let links = self.get_links(note_id).await?;
524        let mut links_to = Vec::new();
525        for link in &links {
526            if let Ok(target_note) = self.get_note(&link.to_note_id).await {
527                links_to.push(target_note);
528            }
529        }
530        
531        // Get backlinks (notes that link TO this note via "references" edges)
532        let node_id = self.to_node_id(note_id);
533        let edges = self.storage.get_edges_to(node_id, Some("references")).await?;
534        let mut backlinks = Vec::new();
535        for edge in edges {
536            if let Ok(source_node) = self.storage.get_node(edge.from_node_id).await {
537                if let Some(note) = Note::from_node(&source_node) {
538                    backlinks.push(note);
539                }
540            }
541        }
542        
543        // Get continuations (notes this note "continues" to)
544        // Need to query for "continues" edges
545        let note_node_id = self.to_node_id(note_id);
546        let neighbors = self.storage
547            .get_neighbors(note_node_id, Some("continues"), EdgeDirection::Outgoing)
548            .await?;
549        
550        let mut continues_to = Vec::new();
551        for node in neighbors {
552            if let Some(luhmann_str) = node.get_property("luhmann_id").and_then(|v| v.as_str()) {
553                if let Some(target_id) = LuhmannId::parse(luhmann_str) {
554                    if let Ok(target_note) = self.get_note(&target_id).await {
555                        continues_to.push(target_note);
556                    }
557                }
558            }
559        }
560        
561        // Get notes that continue FROM this note (reverse of continues)
562        let incoming_neighbors = self.storage
563            .get_neighbors(note_node_id, Some("continues"), EdgeDirection::Incoming)
564            .await?;
565        
566        let mut continued_from = Vec::new();
567        for node in incoming_neighbors {
568            if let Some(luhmann_str) = node.get_property("luhmann_id").and_then(|v| v.as_str()) {
569                if let Some(source_id) = LuhmannId::parse(luhmann_str) {
570                    if let Ok(source_note) = self.get_note(&source_id).await {
571                        continued_from.push(source_note);
572                    }
573                }
574            }
575        }
576        
577        Ok(NoteContext {
578            note,
579            parent,
580            children,
581            links_to,
582            backlinks,
583            continues_to,
584            continued_from,
585        })
586    }
587}
588
589#[cfg(test)]
590mod tests {
591    use super::*;
592    use crate::storage::memory::InMemoryStorage;
593
594    #[tokio::test]
595    async fn test_create_note_auto_id() {
596        let storage = InMemoryStorage::new();
597        let kb = KnowledgeBaseServiceImpl::new(storage);
598        
599        // First note should get ID "1"
600        let note1 = kb.create_note("First Note", "Content 1").await.unwrap();
601        assert_eq!(note1.id.to_string(), "1");
602        
603        // Second note should get ID "2"
604        let note2 = kb.create_note("Second Note", "Content 2").await.unwrap();
605        assert_eq!(note2.id.to_string(), "2");
606    }
607
608    #[tokio::test]
609    async fn test_create_note_with_specific_id() {
610        let storage = InMemoryStorage::new();
611        let kb = KnowledgeBaseServiceImpl::new(storage);
612        
613        let id = LuhmannId::parse("1a").unwrap();
614        let note = kb.create_note_with_id(id.clone(), "Note 1a", "Content").await.unwrap();
615        assert_eq!(note.id, id);
616    }
617
618    #[tokio::test]
619    async fn test_create_branch() {
620        let storage = InMemoryStorage::new();
621        let kb = KnowledgeBaseServiceImpl::new(storage);
622        
623        // Create parent note "1"
624        let parent_id = LuhmannId::parse("1").unwrap();
625        kb.create_note_with_id(parent_id.clone(), "Parent", "Parent content").await.unwrap();
626        
627        // Create branch - should get ID "1a"
628        let child = kb.create_branch(&parent_id, "Child", "Child content").await.unwrap();
629        assert_eq!(child.id.to_string(), "1a");
630        
631        // Create another branch - should get ID "1b"
632        let child2 = kb.create_branch(&parent_id, "Child 2", "Child content 2").await.unwrap();
633        assert_eq!(child2.id.to_string(), "1b");
634    }
635
636    #[tokio::test]
637    async fn test_duplicate_id_fails() {
638        let storage = InMemoryStorage::new();
639        let kb = KnowledgeBaseServiceImpl::new(storage);
640        
641        let id = LuhmannId::parse("1").unwrap();
642        kb.create_note_with_id(id.clone(), "First", "Content").await.unwrap();
643        
644        // Creating with same ID should fail
645        let result = kb.create_note_with_id(id, "Second", "Content").await;
646        assert!(matches!(result, Err(KbError::NoteAlreadyExists(_))));
647    }
648
649    #[tokio::test]
650    async fn test_get_note() {
651        let storage = InMemoryStorage::new();
652        let kb = KnowledgeBaseServiceImpl::new(storage);
653        
654        let note = kb.create_note("Test", "Content").await.unwrap();
655        let retrieved = kb.get_note(&note.id).await.unwrap();
656        
657        assert_eq!(retrieved.title, "Test");
658        assert_eq!(retrieved.content, "Content");
659    }
660
661    #[tokio::test]
662    async fn test_list_notes() {
663        let storage = InMemoryStorage::new();
664        let kb = KnowledgeBaseServiceImpl::new(storage);
665        
666        kb.create_note_with_id(LuhmannId::parse("2").unwrap(), "Second", "Content").await.unwrap();
667        kb.create_note_with_id(LuhmannId::parse("1").unwrap(), "First", "Content").await.unwrap();
668        kb.create_note_with_id(LuhmannId::parse("1a").unwrap(), "Child", "Content").await.unwrap();
669        
670        let notes = kb.list_notes().await.unwrap();
671        
672        // Should be sorted by Luhmann ID
673        assert_eq!(notes.len(), 3);
674        assert_eq!(notes[0].id.to_string(), "1");
675        assert_eq!(notes[1].id.to_string(), "1a");
676        assert_eq!(notes[2].id.to_string(), "2");
677    }
678
679    #[tokio::test]
680    async fn test_list_by_prefix() {
681        let storage = InMemoryStorage::new();
682        let kb = KnowledgeBaseServiceImpl::new(storage);
683        
684        kb.create_note_with_id(LuhmannId::parse("1").unwrap(), "One", "Content").await.unwrap();
685        kb.create_note_with_id(LuhmannId::parse("1a").unwrap(), "One-A", "Content").await.unwrap();
686        kb.create_note_with_id(LuhmannId::parse("1a1").unwrap(), "One-A-One", "Content").await.unwrap();
687        kb.create_note_with_id(LuhmannId::parse("1b").unwrap(), "One-B", "Content").await.unwrap();
688        kb.create_note_with_id(LuhmannId::parse("2").unwrap(), "Two", "Content").await.unwrap();
689        
690        let prefix = LuhmannId::parse("1a").unwrap();
691        let notes = kb.list_notes_by_prefix(&prefix).await.unwrap();
692        
693        assert_eq!(notes.len(), 2); // 1a and 1a1
694        assert!(notes.iter().any(|n| n.id.to_string() == "1a"));
695        assert!(notes.iter().any(|n| n.id.to_string() == "1a1"));
696    }
697
698    #[tokio::test]
699    async fn test_search_notes() {
700        let storage = InMemoryStorage::new();
701        let kb = KnowledgeBaseServiceImpl::new(storage);
702        
703        kb.create_note("Rust Programming", "A systems language").await.unwrap();
704        kb.create_note("Python Basics", "Easy to learn").await.unwrap();
705        kb.create_note("Rust vs Go", "Comparison").await.unwrap();
706        
707        let results = kb.search_notes("rust").await.unwrap();
708        assert_eq!(results.len(), 2);
709    }
710
711    #[tokio::test]
712    async fn test_link_notes() {
713        let storage = InMemoryStorage::new();
714        let kb = KnowledgeBaseServiceImpl::new(storage);
715        
716        let note1 = kb.create_note("First", "Content").await.unwrap();
717        let note2 = kb.create_note("Second", "Content").await.unwrap();
718        
719        kb.link_notes(&note1.id, &note2.id, Some("See also".to_string())).await.unwrap();
720        
721        let links = kb.get_links(&note1.id).await.unwrap();
722        assert_eq!(links.len(), 1);
723        assert_eq!(links[0].to_note_id, note2.id);
724    }
725
726    #[tokio::test]
727    async fn test_self_link_fails() {
728        let storage = InMemoryStorage::new();
729        let kb = KnowledgeBaseServiceImpl::new(storage);
730        
731        let note = kb.create_note("Note", "Content").await.unwrap();
732        
733        let result = kb.link_notes(&note.id, &note.id, None).await;
734        assert!(matches!(result, Err(KbError::SelfLink)));
735    }
736
737    #[tokio::test]
738    async fn test_mark_continuation() {
739        let storage = InMemoryStorage::new();
740        let kb = KnowledgeBaseServiceImpl::new(storage);
741        
742        let id1 = LuhmannId::parse("1").unwrap();
743        let id2 = LuhmannId::parse("2").unwrap();
744        
745        kb.create_note_with_id(id1.clone(), "First", "Content 1").await.unwrap();
746        kb.create_note_with_id(id2.clone(), "Second", "Content 2").await.unwrap();
747        
748        // Mark note 1 as continuing to note 2
749        kb.mark_continuation(&id1, &id2).await.unwrap();
750        
751        // Self-continuation should fail
752        let result = kb.mark_continuation(&id1, &id1).await;
753        assert!(matches!(result, Err(KbError::SelfLink)));
754    }
755
756    #[tokio::test]
757    async fn test_create_index() {
758        let storage = InMemoryStorage::new();
759        let kb = KnowledgeBaseServiceImpl::new(storage);
760        
761        // Create parent and children
762        let parent_id = LuhmannId::parse("1").unwrap();
763        kb.create_note_with_id(parent_id.clone(), "Parent Note", "Parent content").await.unwrap();
764        
765        let child1_id = LuhmannId::parse("1a").unwrap();
766        kb.create_note_with_id(child1_id.clone(), "First Child", "Child 1 content").await.unwrap();
767        
768        let child2_id = LuhmannId::parse("1b").unwrap();
769        kb.create_note_with_id(child2_id.clone(), "Second Child", "Child 2 content").await.unwrap();
770        
771        // Create grandchild (should not appear in index)
772        let grandchild_id = LuhmannId::parse("1a1").unwrap();
773        kb.create_note_with_id(grandchild_id.clone(), "Grandchild", "Grandchild content").await.unwrap();
774        
775        // Create index
776        let index = kb.create_index(&parent_id).await.unwrap();
777        
778        // Index ID should be {parent_id}0
779        assert_eq!(index.id.to_string(), "10");
780        // Should contain references to children
781        assert!(index.content.contains("1a"));
782        assert!(index.content.contains("1b"));
783        assert!(index.content.contains("First Child"));
784        assert!(index.content.contains("Second Child"));
785        // Should NOT contain grandchild
786        assert!(!index.content.contains("1a1"));
787    }
788}