12 KiB
System Architecture
Comprehensive overview of the KOGRAL architecture.
High-Level Architecture
The KOGRAL consists of three main layers:
- User Interfaces: kogral-cli (terminal), kogral-mcp (AI integration), NuShell scripts (automation)
- Core Library (kogral-core): Rust library with graph engine, storage abstraction, embeddings, query engine
- Storage Backends: Filesystem (git-friendly), SurrealDB (scalable), In-Memory (cache/testing)
Component Details
kogral-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
kogral-mcp (MCP Server)
Purpose: AI integration via Model Context Protocol.
Protocol: JSON-RPC 2.0 over stdio
Components:
-
Tools (7):
kogral/search: Query knowledge basekogral/add_note: Create noteskogral/add_decision: Create ADRskogral/link: Create relationshipskogral/get_guidelines: Retrieve guidelines with inheritancekb/list_graphs: List available graphskogral/export: Export to formats
-
Resources (6 URIs):
kogral://project/noteskogral://project/decisionskogral://project/guidelineskogral://project/patternskogral://shared/guidelineskogral://shared/patterns
-
Prompts (2):
kogral/summarize_project: Generate project summarykogral/find_related: Find related nodes
Integration: Claude Code via ~/.config/claude/config.json
NuShell Scripts
Purpose: Automation and maintenance tasks.
Scripts (6):
kogral-sync.nu: Filesystem ↔ SurrealDB synckogral-backup.nu: Archive knowledge basekogral-reindex.nu: Rebuild embeddingskogral-import-logseq.nu: Import from Logseqkogral-export-logseq.nu: Export to Logseqkogral-stats.nu: Graph statistics
Features:
- Colored output
- Dry-run modes
- Progress indicators
- Error handling
Core Library (kogral-core)
Models
Graph:
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:
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:
pub struct Edge {
pub from: String,
pub to: String,
pub relation: EdgeType,
pub strength: f32,
pub created: DateTime<Utc>,
}
Storage Trait
#[async_trait]
pub trait Storage: Send + Sync {
/// Save a complete graph to storage
async fn save_graph(&mut self, graph: &Graph) -> Result<()>;
/// Load a graph from storage
async fn load_graph(&self, name: &str) -> Result<Graph>;
/// Save a single node to storage
async fn save_node(&mut self, node: &Node) -> Result<()>;
/// Load a node by ID
async fn load_node(&self, graph_name: &str, node_id: &str) -> Result<Node>;
/// Delete a node
async fn delete_node(&mut self, graph_name: &str, node_id: &str) -> Result<()>;
/// List all graphs
async fn list_graphs(&self) -> Result<Vec<String>>;
/// List nodes in a graph, optionally filtered by type
async fn list_nodes(&self, graph_name: &str, node_type: Option<&str>) -> Result<Vec<Node>>;
}
Implementations:
FilesystemStorage: Git-friendly markdown filesMemoryStorage: In-memory with DashMapSurrealDbStorage: Scalable graph database
Embedding Provider Trait
#[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
→
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/kogral-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
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
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
// 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 kogral-mcp kogral-core
│ │ │
├─ JSON-RPC request ───→ │ │
│ kogral/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:
let mut tera = Tera::new("templates/**/*.tera")?;
let rendered = tera.render("note.md.tera", &context)?;
Error Handling
Strategy: thiserror for structured errors
#[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:
- kogral-core: 48 tests
- kogral-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