# Config-Driven Architecture The KOGRAL follows a **config-driven architecture** where all behavior is defined through Nickel configuration files rather than hardcoded in Rust. ## Philosophy **"Configuration, not code, defines behavior"** Instead of hardcoding storage backends, embedding providers, or query parameters, KOGRAL uses a layered configuration system that composes settings from multiple sources: 1. **Schema contracts** (type definitions) 2. **Defaults** (base values) 3. **Mode overlays** (dev/prod/test optimizations) 4. **User customizations** (project-specific overrides) This approach provides: - ✅ **Type safety** - Nickel contracts validate configuration before runtime - ✅ **Composability** - Mix and match configurations for different environments - ✅ **Discoverability** - Self-documenting schemas with inline documentation - ✅ **Hot-reload** - Change behavior without recompiling Rust code - ✅ **Double validation** - Nickel contracts + serde ensure correctness ## Configuration Composition Flow ![Configuration Composition](../diagrams/config-composition.svg) The configuration system uses a **four-layer composition pattern**: ### Layer 1: Schema Contracts **Location**: `schemas/contracts.ncl` **Purpose**: Define types and validation rules using Nickel contracts. **Example**: ```nickel { StorageType = [| 'filesystem, 'memory, 'surrealdb |], StorageConfig = { primary | StorageType | doc "Primary storage backend" | default = 'filesystem, secondary | SecondaryStorageConfig | doc "Optional secondary storage" | default = { enabled = false }, }, } ``` **Benefits**: - Enum validation (only valid storage types accepted) - Required vs optional fields - Default values for optional fields - Documentation attached to types ### Layer 2: Defaults **Location**: `schemas/defaults.ncl` **Purpose**: Provide sensible base values for all configuration options. **Example**: ```nickel { base = { storage = { primary = 'filesystem, secondary = { enabled = false, type = 'surrealdb, url = "ws://localhost:8000", }, }, embeddings = { enabled = true, provider = 'fastembed, model = "BAAI/bge-small-en-v1.5", dimensions = 384, }, } | contracts.KbConfig, } ``` **Validated by**: `contracts.KbConfig` contract ensures defaults are valid. ### Layer 3: Mode Overlays **Location**: `schemas/modes/{dev,prod,test}.ncl` **Purpose**: Environment-specific optimizations and tuning. #### Development Mode (`dev.ncl`) Optimized for: Fast iteration, local development, debugging ```nickel { storage = { primary = 'filesystem, secondary = { enabled = false }, # No database overhead }, embeddings = { provider = 'fastembed, # Local, no API costs }, sync = { auto_index = false, # Manual control }, } ``` #### Production Mode (`prod.ncl`) Optimized for: Performance, reliability, scalability ```nickel { storage = { secondary = { enabled = true }, # SurrealDB for scale }, embeddings = { provider = 'openai, # High-quality cloud embeddings model = "text-embedding-3-small", dimensions = 1536, }, sync = { auto_index = true, debounce_ms = 300, # Fast response }, } ``` #### Test Mode (`test.ncl`) Optimized for: Fast tests, isolation, determinism ```nickel { storage = { primary = 'memory, # Ephemeral, no disk I/O }, embeddings = { enabled = false, # Disable for speed }, sync = { auto_index = false, debounce_ms = 0, # No delays in tests }, } ``` ### Layer 4: User Customizations **Location**: `.kogral-config/core/kogral.ncl` or `.kogral-config/platform/{dev,prod,test}.ncl` **Purpose**: Project-specific or deployment-specific overrides. **Example** (user project config): ```nickel let mode = import "../../schemas/modes/dev.ncl" in let user_custom = { graph = { name = "my-project", }, embeddings = { provider = 'claude, # Override to use Claude model = "claude-3-haiku-20240307", }, query = { similarity_threshold = 0.7, # Stricter threshold }, } in helpers.compose_config defaults.base mode user_custom | contracts.KbConfig ``` ## Composition Mechanism The `helpers.ncl` module provides the composition function: ```nickel { # Recursively merge with override precedence merge_with_override = fun base override => /* ... */, # Compose three layers compose_config = fun defaults mode_config user_custom => let with_mode = merge_with_override defaults mode_config in merge_with_override with_mode user_custom, } ``` **Merge behavior**: - Records are merged recursively - Override values take precedence over base values - Arrays are not merged, override replaces base - Null in override keeps base value **Example merge**: ```nickel base = { storage = { primary = 'filesystem }, embeddings = { enabled = true } } override = { storage = { primary = 'memory } } # Result: { storage = { primary = 'memory }, embeddings = { enabled = true } } ``` ## Export to JSON Once composed, the Nickel configuration is exported to JSON for Rust consumption: ```bash nickel export --format json .kogral-config/core/kogral.ncl > .kogral-config/targets/kogral-core.json ``` **Output** (`.kogral-config/targets/kogral-core.json`): ```json { "graph": { "name": "my-project", "version": "1.0.0" }, "storage": { "primary": "memory", "secondary": { "enabled": false } }, "embeddings": { "enabled": true, "provider": "claude", "model": "claude-3-haiku-20240307", "dimensions": 768 } } ``` ## Rust Integration The Rust code deserializes the JSON into typed structs using serde: ```rust use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize)] pub struct KbConfig { pub graph: GraphConfig, pub storage: StorageConfig, pub embeddings: EmbeddingConfig, pub templates: TemplateConfig, pub query: QueryConfig, pub mcp: McpConfig, pub sync: SyncConfig, } impl KbConfig { pub fn from_file(path: &Path) -> Result { let json = std::fs::read_to_string(path)?; let config: KbConfig = serde_json::from_str(&json)?; Ok(config) } } ``` **Usage in kogral-core**: ```rust let config = KbConfig::from_file(".kogral-config/targets/kogral-core.json")?; // Config drives behavior let storage: Box = match config.storage.primary { StorageType::Filesystem => Box::new(FilesystemStorage::new(&config)?), StorageType::Memory => Box::new(MemoryStorage::new()), StorageType::SurrealDb => Box::new(SurrealDbStorage::new(&config).await?), }; let embeddings: Box = match config.embeddings.provider { EmbeddingProviderType::FastEmbed => Box::new(FastEmbedProvider::new()?), EmbeddingProviderType::OpenAI => Box::new(RigEmbeddingProvider::openai(&config)?), EmbeddingProviderType::Claude => Box::new(RigEmbeddingProvider::claude(&config)?), EmbeddingProviderType::Ollama => Box::new(RigEmbeddingProvider::ollama(&config)?), }; ``` ## Double Validation Configuration is validated **twice**: ### 1. Nickel Contract Validation At export time, Nickel validates: - ✅ Types match contracts (e.g., `primary | StorageType`) - ✅ Required fields are present - ✅ Enums have valid values - ✅ Nested structure is correct **Error example**: ```text error: contract broken by a value ┌─ .kogral-config/core/kogral.ncl:15:5 │ 15│ primary = 'invalid, │ ^^^^^^^^^^^^^^^^^^^ applied to this expression │ = This value is not in the enum ['filesystem, 'memory, 'surrealdb] ``` ### 2. Serde Deserialization Validation At runtime, serde validates: - ✅ JSON structure matches Rust types - ✅ Field names match (with rename support) - ✅ Values can be converted to Rust types - ✅ Required fields are not null **Error example**: ```rust Error: missing field `graph` at line 1 column 123 ``` ## Benefits of Config-Driven Architecture ### 1. Zero Hardcoding **Bad** (hardcoded): ```rust // Hardcoded - requires recompilation to change let storage = FilesystemStorage::new("/fixed/path"); let threshold = 0.6; ``` **Good** (config-driven): ```rust // Config-driven - change via .ncl file let storage = create_storage(&config)?; let threshold = config.query.similarity_threshold; ``` ### 2. Environment Flexibility Same codebase, different behavior: ```bash # Development nickel export .kogral-config/platform/dev.ncl > targets/kogral-core.json # → Filesystem storage, fastembed, no auto-sync # Production nickel export .kogral-config/platform/prod.ncl > targets/kogral-core.json # → SurrealDB enabled, OpenAI embeddings, auto-sync # Testing nickel export .kogral-config/platform/test.ncl > targets/kogral-core.json # → In-memory storage, no embeddings, isolated ``` ### 3. Self-Documenting Nickel contracts include inline documentation: ```nickel StorageType = [| 'filesystem, 'memory, 'surrealdb |] | doc "Storage backend type: filesystem (git-tracked), memory (ephemeral), surrealdb (scalable)", ``` IDEs can show this documentation when editing `.ncl` files. ### 4. Type-Safe Evolution When adding new features: 1. Update contract in `contracts.ncl` 2. Add default in `defaults.ncl` 3. Export validates existing configs 4. Rust compilation validates deserialization Breaking changes are caught **before runtime**. ### 5. Testability Different test scenarios without code changes: ```nickel # test-semantic-search.ncl let test_config = defaults.base & { embeddings = { enabled = true, provider = 'fastembed }, query = { similarity_threshold = 0.3 }, } in test_config ``` ```rust #[test] fn test_semantic_search() { let config = KbConfig::from_file("test-semantic-search.json")?; // Config drives test behavior } ``` ## Configuration Discovery KOGRAL tools automatically discover configuration: 1. **Check `.kogral-config/targets/kogral-core.json`** (pre-exported) 2. **Check `.kogral-config/core/kogral.ncl`** (export on-demand) 3. **Check environment variable `KOGRAL_CONFIG`** 4. **Fall back to embedded defaults** ```rust impl KbConfig { pub fn discover() -> Result { if let Ok(config) = Self::from_file(".kogral-config/targets/kogral-core.json") { return Ok(config); } if Path::new(".kogral-config/core/kogral.ncl").exists() { // Export and load let output = Command::new("nickel") .args(["export", "--format", "json", ".kogral-config/core/kogral.ncl"]) .output()?; return serde_json::from_slice(&output.stdout)?; } if let Ok(path) = std::env::var("KOGRAL_CONFIG") { return Self::from_file(&path); } Ok(Self::default()) // Embedded defaults } } ``` ## Integration with justfile The `justfile` integrates configuration validation: ```just # Validate all Nickel configs nickel-validate-all: @echo "Validating Nickel schemas..." nickel typecheck schemas/contracts.ncl nickel typecheck schemas/defaults.ncl nickel typecheck schemas/helpers.ncl nickel typecheck schemas/modes/dev.ncl nickel typecheck schemas/modes/prod.ncl nickel typecheck schemas/modes/test.ncl nickel typecheck .kogral-config/core/kogral.ncl # Export all platform configs nickel-export-all: @echo "Exporting platform configs to JSON..." @mkdir -p .kogral-config/targets nickel export --format json .kogral-config/platform/dev.ncl > .kogral-config/targets/kogral-dev.json nickel export --format json .kogral-config/platform/prod.ncl > .kogral-config/targets/kogral-prod.json nickel export --format json .kogral-config/platform/test.ncl > .kogral-config/targets/kogral-test.json @echo "Exported 3 configurations to .kogral-config/targets/" ``` Usage: ```bash just nickel::validate-all # Check configs are valid just nickel::export-all # Generate JSON for all environments ``` ## Best Practices ### 1. Never Hardcode If it's behavior, it's config: - Storage paths - API endpoints - Thresholds - Timeouts - Feature flags - Provider selection ### 2. Use Modes for Environment Differences Don't put environment-specific values in user config: **Bad**: ```nickel # user config with env-specific values { storage = { url = if env == "prod" then "prod-url" else "dev-url" # Don't do this } } ``` **Good**: ```nickel # modes/prod.ncl { storage = { url = "prod-url" } } # modes/dev.ncl { storage = { url = "dev-url" } } # user config is environment-agnostic { graph = { name = "my-project" } } ``` ### 3. Document Complex Fields Use Nickel's `doc` metadata: ```nickel similarity_threshold | Number | doc "Minimum cosine similarity (0-1) for semantic search matches. Higher = stricter." | default = 0.6, ``` ### 4. Validate Early Run `nickel typecheck` in CI/CD before building Rust code. ### 5. Version Configs Track `.ncl` files in git, ignore `.kogral-config/targets/*.json` (generated). ## See Also - **Schema Reference**: [Configuration Schema](../config/schema.md) - **User Guide**: [Configuration Guide](../config/overview.md) - **ADR**: [Why Nickel vs TOML](adrs/001-nickel-vs-toml.md) - **Examples**: `.kogral-config/core/kogral.ncl`, `.kogral-config/platform/*.ncl`