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 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 async fn search_notes(&self, query: &str) -> Result<Vec<Note>>;
61
62 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 async fn mark_continuation(&self, from_id: &LuhmannId, to_id: &LuhmannId) -> Result<()>;
74
75 async fn create_index(&self, parent_id: &LuhmannId) -> Result<Note>;
77
78 async fn get_context(&self, note_id: &LuhmannId) -> Result<NoteContext>;
80}
81
82#[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#[derive(Debug, Clone)]
96pub struct NoteGraph {
97 pub center_note_id: NoteId,
98 pub notes: Vec<Note>,
99 pub links: Vec<NoteLink>,
100}
101
102fn 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 fn to_node_id(&self, luhmann_id: &LuhmannId) -> NodeId {
118 luhmann_to_node_id(luhmann_id)
119 }
120
121 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 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 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 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 async fn next_child_id(&self, parent_id: &LuhmannId) -> Result<LuhmannId> {
157 let all_notes = self.list_notes().await?;
158
159 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 Ok(parent_id.first_child())
169 } else {
170 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 let luhmann_id = self.next_main_id().await?;
188
189 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(_)) => (), 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 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(_)) => (), 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 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 let child_id = self.next_child_id(parent_id).await?;
241
242 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(_)) => (), Err(e) => return Err(KbError::Storage(e)),
248 }
249
250 let note = Note::new(child_id.clone(), title, content);
252 let node = note.to_node();
253 self.storage.create_node(&node).await?;
254
255 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 self.get_note(¬e.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 self.get_note(note_id).await?;
294
295 let node_id = self.to_node_id(note_id);
296 self.storage.delete_node(node_id).await?;
298 Ok(())
299 }
300
301 async fn list_notes(&self) -> Result<Vec<Note>> {
302 let query = SearchQuery {
304 node_types: vec!["note".to_string()],
305 limit: 10000, ..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 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 self.get_note(from_id).await?;
362 self.get_note(to_id).await?;
363
364 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 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 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, }
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 self.get_note(from_id).await?;
422 self.get_note(to_id).await?;
423
424 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 let parent_note = self.get_note(parent_id).await?;
442
443 let all_notes = self.list_notes().await?;
445
446 let children: Vec<&Note> = all_notes
447 .iter()
448 .filter(|note| {
449 note.id.parent().as_ref() == Some(parent_id)
451 })
452 .collect();
453
454 let index_id = LuhmannId::parse(&format!("{}0", parent_id))
456 .ok_or_else(|| KbError::InvalidLuhmannId(format!("{}0", parent_id)))?;
457
458 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(_)) => (), Err(e) => return Err(KbError::Storage(e)),
464 }
465
466 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 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 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 let note = self.get_note(note_id).await?;
507
508 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 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 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 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 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 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 let note1 = kb.create_note("First Note", "Content 1").await.unwrap();
601 assert_eq!(note1.id.to_string(), "1");
602
603 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 let parent_id = LuhmannId::parse("1").unwrap();
625 kb.create_note_with_id(parent_id.clone(), "Parent", "Parent content").await.unwrap();
626
627 let child = kb.create_branch(&parent_id, "Child", "Child content").await.unwrap();
629 assert_eq!(child.id.to_string(), "1a");
630
631 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 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(¬e.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 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); 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(¬e1.id, ¬e2.id, Some("See also".to_string())).await.unwrap();
720
721 let links = kb.get_links(¬e1.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(¬e.id, ¬e.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 kb.mark_continuation(&id1, &id2).await.unwrap();
750
751 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 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 let grandchild_id = LuhmannId::parse("1a1").unwrap();
773 kb.create_note_with_id(grandchild_id.clone(), "Grandchild", "Grandchild content").await.unwrap();
774
775 let index = kb.create_index(&parent_id).await.unwrap();
777
778 assert_eq!(index.id.to_string(), "10");
780 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 assert!(!index.content.contains("1a1"));
787 }
788}