System Architecture
Comprehensive overview of the KOGRAL architecture.
High-Level Architecture
The KOGRAL consists of three main layers:
- User Interfaces: kb-cli (terminal), kb-mcp (AI integration), NuShell scripts (automation)
- Core Library (kb-core): Rust library with graph engine, storage abstraction, embeddings, query engine
- Storage Backends: Filesystem (git-friendly), SurrealDB (scalable), In-Memory (cache/testing)
Component Details
kb-cli (Command-Line Interface)
Purpose: Primary user interface for local knowledge management.
Commands (13 total):
init: Initialize.kogral/directoryadd: Create nodes (note, decision, guideline, pattern, journal)search: Text and semantic searchlink: Create relationships between nodeslist: List all nodesshow: Display node detailsdelete: Remove nodesgraph: Visualize knowledge graphsync: Sync filesystem ↔ SurrealDBserve: Start MCP serverimport: Import from Logseqexport: Export to Logseq/JSONconfig: Manage configuration
Technology: Rust + clap (derive API)
Features:
- Colored terminal output
- Interactive prompts
- Dry-run modes
- Validation before operations
kb-mcp (MCP Server)
Purpose: AI integration via Model Context Protocol.
Protocol: JSON-RPC 2.0 over stdio
Components:
-
Tools (7):
kogral/search: Query knowledge basekb/add_note: Create noteskb/add_decision: Create ADRskb/link: Create relationshipskb/get_guidelines: Retrieve guidelines with inheritancekb/list_graphs: List available graphskb/export: Export to formats
-
Resources (6 URIs):
kogral://project/noteskogral://project/decisionskogral://project/guidelineskogral://project/patternskogral://shared/guidelineskogral://shared/patterns
-
Prompts (2):
kb/summarize_project: Generate project summarykb/find_related: Find related nodes
Integration: Claude Code via ~/.config/claude/config.json
NuShell Scripts
Purpose: Automation and maintenance tasks.
Scripts (6):
kb-sync.nu: Filesystem ↔ SurrealDB synckb-backup.nu: Archive knowledge basekb-reindex.nu: Rebuild embeddingskb-import-logseq.nu: Import from Logseqkb-export-logseq.nu: Export to Logseqkb-stats.nu: Graph statistics
Features:
- Colored output
- Dry-run modes
- Progress indicators
- Error handling
Core Library (kb-core)
Models
Graph:
#![allow(unused)] fn main() { pub struct Graph { pub name: String, pub version: String, pub nodes: HashMap<String, Node>, // ID → Node pub edges: Vec<Edge>, pub metadata: HashMap<String, Value>, } }
Node:
#![allow(unused)] fn main() { pub struct Node { pub id: String, pub node_type: NodeType, pub title: String, pub content: String, pub tags: Vec<String>, pub status: NodeStatus, pub created: DateTime<Utc>, pub modified: DateTime<Utc>, // ... relationships, metadata } }
Edge:
#![allow(unused)] fn main() { pub struct Edge { pub from: String, pub to: String, pub relation: EdgeType, pub strength: f32, pub created: DateTime<Utc>, } }
Storage Trait
#![allow(unused)] fn main() { #[async_trait] pub trait Storage: Send + Sync { async fn save_graph(&self, graph: &Graph) -> Result<()>; async fn load_graph(&self, name: &str) -> Result<Graph>; async fn delete_graph(&self, name: &str) -> Result<()>; async fn list_graphs(&self) -> Result<Vec<String>>; } }
Implementations:
FilesystemStorage: Git-friendly markdown filesMemoryStorage: In-memory with DashMapSurrealDbStorage: Scalable graph database
Embedding Provider Trait
#![allow(unused)] fn main() { #[async_trait] pub trait EmbeddingProvider: Send + Sync { async fn embed(&self, texts: Vec<String>) -> Result<Vec<Vec<f32>>>; fn dimensions(&self) -> usize; fn model_name(&self) -> &str; } }
Implementations:
FastEmbedProvider: Local fastembedRigEmbeddingProvider: OpenAI, Claude, Ollama (via rig-core)
Parser
Input: Markdown file with YAML frontmatter
Output: Node struct
Features:
- YAML frontmatter extraction
- Markdown body parsing
- Wikilink detection (
[[linked-note]]) - Code reference parsing (
@file.rs:42)
Example:
---
id: note-123
type: note
title: My Note
tags: [rust, async]
---
# My Note
Content with [[other-note]] and @src/main.rs:10
→
#![allow(unused)] fn main() { Node { id: "note-123", node_type: NodeType::Note, title: "My Note", content: "Content with [[other-note]] and @src/main.rs:10", tags: vec!["rust", "async"], // ... parsed wikilinks, code refs } }
Configuration System
Nickel Schema
# schemas/kb-config.ncl
{
KbConfig = {
graph | GraphConfig,
storage | StorageConfig,
embeddings | EmbeddingConfig,
templates | TemplateConfig,
query | QueryConfig,
mcp | McpConfig,
sync | SyncConfig,
},
}
Loading Process
User writes: .kogral/config.ncl
↓ [nickel export --format json]
JSON intermediate
↓ [serde_json::from_str]
KbConfig struct (Rust)
↓
Runtime behavior
Double Validation:
- Nickel contracts: Type-safe, enum validation
- Serde deserialization: Rust type checking
Benefits:
- Errors caught at export time
- Runtime guaranteed valid config
- Self-documenting schemas
Storage Architecture
Hybrid Strategy
Local Graph (per project):
- Storage: Filesystem (
.kogral/directory) - Format: Markdown + YAML frontmatter
- Version control: Git
- Scope: Project-specific knowledge
Shared Graph (organization):
- Storage: SurrealDB (or synced filesystem)
- Format: Same markdown (for compatibility)
- Version control: Optional
- Scope: Organization-wide guidelines
Sync:
Filesystem (.kogral/)
↕ [bidirectional sync]
SurrealDB (central)
File Layout
.kogral/
├── config.toml # Graph metadata
├── notes/
│ ├── async-patterns.md # Individual note
│ └── error-handling.md
├── decisions/
│ ├── 0001-use-rust.md # ADR format
│ └── 0002-surrealdb.md
├── guidelines/
│ ├── rust-errors.md # Project guideline
│ └── testing.md
├── patterns/
│ └── repository.md
└── journal/
├── 2026-01-17.md # Daily journal
└── 2026-01-18.md
Query Engine
Text Search
#![allow(unused)] fn main() { let results = graph.nodes.values() .filter(|node| { node.title.contains(&query) || node.content.contains(&query) || node.tags.iter().any(|tag| tag.contains(&query)) }) .collect(); }
Semantic Search
#![allow(unused)] fn main() { let query_embedding = embeddings.embed(vec![query]).await?; let mut scored: Vec<_> = graph.nodes.values() .filter_map(|node| { let node_embedding = node.embedding.as_ref()?; let similarity = cosine_similarity(&query_embedding[0], node_embedding); (similarity >= threshold).then_some((node, similarity)) }) .collect(); scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); }
Cross-Graph Query
#![allow(unused)] fn main() { // Query both project and shared graphs let project_results = project_graph.search(&query).await?; let shared_results = shared_graph.search(&query).await?; // Merge with deduplication let combined = merge_results(project_results, shared_results); }
MCP Protocol Flow
Claude Code kb-mcp kb-core
│ │ │
├─ JSON-RPC request ───→ │ │
│ kb/search │ │
│ {"query": "rust"} │ │
│ ├─ search() ──────────→ │
│ │ │
│ │ Query engine
│ │ Text + semantic
│ │ │
│ │ ←──── results ─────────┤
│ │ │
│ ←─ JSON-RPC response ──┤ │
│ {"results": [...]} │ │
Template System
Engine: Tera (Jinja2-like)
Templates:
-
Document Templates (6):
note.md.teradecision.md.teraguideline.md.terapattern.md.terajournal.md.teraexecution.md.tera
-
Export Templates (4):
logseq-page.md.teralogseq-journal.md.terasummary.md.teragraph.json.tera
Usage:
#![allow(unused)] fn main() { let mut tera = Tera::new("templates/**/*.tera")?; let rendered = tera.render("note.md.tera", &context)?; }
Error Handling
Strategy: thiserror for structured errors
#![allow(unused)] fn main() { #[derive(Error, Debug)] pub enum KbError { #[error("Storage error: {0}")] Storage(String), #[error("Node not found: {0}")] NodeNotFound(String), #[error("Configuration error: {0}")] Config(String), #[error("Parse error: {0}")] Parse(String), #[error("Embedding error: {0}")] Embedding(String), } }
Propagation: ? operator throughout
Testing Strategy
Unit Tests: Per module (models, parser, storage)
Integration Tests: Full workflow (add → save → load → query)
Test Coverage:
- kb-core: 48 tests
- kb-mcp: 5 tests
- Total: 56 tests
Test Data: Fixtures in tests/fixtures/
Performance Considerations
Node Lookup: O(1) via HashMap
Semantic Search: O(n) with early termination (threshold filter)
Storage:
- Filesystem: Lazy loading (load on demand)
- Memory: Full graph in RAM
- SurrealDB: Query optimization (indexes)
Embeddings:
- Cache embeddings in node metadata
- Batch processing (configurable batch size)
- Async generation (non-blocking)
Security
No unsafe code: #![forbid(unsafe_code)]
Input validation:
- Nickel contracts validate config
- serde validates JSON
- Custom validation for user input
File operations:
- Path sanitization (no
../traversal) - Permissions checking
- Atomic writes (temp file + rename)
Scalability
Small Projects (< 1000 nodes):
- Filesystem storage
- In-memory search
- Local embeddings (fastembed)
Medium Projects (1000-10,000 nodes):
- Filesystem + SurrealDB sync
- Semantic search with caching
- Cloud embeddings (OpenAI/Claude)
Large Organizations (> 10,000 nodes):
- SurrealDB primary
- Distributed embeddings
- Multi-graph federation
Next Steps
- Graph Model Details: Graph Model
- Storage Deep Dive: Storage Architecture
- ADRs: Architectural Decisions
- Implementation: Development Guide