## Keyboard shortcuts

Press `←` or `→` to navigate between chapters

Press `S` or `/` to search in the book

Press `?` to show this help

Press `Esc` to hide this help

# ADR-001: Nickel vs TOML for Configuration

Status: Accepted

Date: 2026-01-17

Deciders: Architecture Team

Context: Configuration Strategy for Knowledge Base System

## Context

The KOGRAL requires a flexible, type-safe configuration format that supports:

1. Complex nested structures (graph settings, storage configs, embedding providers)
2. Type validation (prevent runtime errors from config mistakes)
3. Composition and inheritance (shared configs, environment-specific overrides)
4. Documentation (self-documenting schemas)
5. Validation before runtime (catch errors early)

We evaluated two primary options:

### Option 1: TOML (Traditional Config Format)

Pros:

- Widely adopted in Rust ecosystem (`Cargo.toml`)
- Simple, human-readable syntax
- Native `serde` support
- IDE support (syntax highlighting, completion)

Cons:

- No type system (validation only at runtime)
- Limited composition (no imports, no functions)
- No schema validation (errors discovered during execution)
- Verbose for complex nested structures
- No documentation in config files

Example TOML:

    [graph]
    name = "my-project"
    version = "1.0.0"

    [storage]
    primary = "filesystem"  # String, not validated as enum

    [storage.secondary]
    enabled = true
    type = "surrealdb"  # Typo would fail at runtime
    url = "ws://localhost:8000"

    [embeddings]
    enabled = true
    provider = "openai"  # No validation of valid providers
    model = "text-embedding-3-small"

Problems:

- Typos in enum values (`"surrealdb"` vs `"surealdb"`) fail at runtime
- No validation that `provider = "openai"` requires `api_key_env`
- No documentation of valid options
- No way to compose configs (e.g., base config + environment override)

### Option 2: Nickel (Functional Configuration Language)

Pros:

- Type system with contracts (validate before runtime)
- Composition via imports and merging
- Documentation in schemas (self-documenting)
- Validation at export time (catch errors early)
- Functions for conditional logic
- Default values in schema definitions

Cons:

- Less familiar to Rust developers
- Requires separate `nickel` CLI tool
- Smaller ecosystem
- Steeper learning curve

Example Nickel:

    # schemas/kb-config.ncl
    {
      KbConfig = {
        graph | GraphConfig,
        storage | StorageConfig,
        embeddings | EmbeddingConfig,
      },

      StorageConfig = {
        primary | [| 'filesystem, 'memory |],  # Enum validated at export

        secondary | {
          enabled | Bool,
          type | [| 'surrealdb, 'sqlite |],  # Typos caught immediately
          url | String,
        } | optional,
      },

      EmbeddingConfig = {
        enabled | Bool,
        provider | [| 'openai, 'claude, 'fastembed |],  # Valid providers enforced
        model | String,
        api_key_env | String | doc "Environment variable for API key",
      },
    }

Benefits:

- Typos in enum values caught at `nickel export` time
- Schema enforces required fields based on provider
- Documentation embedded in schema
- Config can be composed: `import "base.ncl" & { /* overrides */ }`

## Decision

We will use Nickel for configuration.

Implementation:

1. Define schemas in `schemas/*.ncl` with type contracts
2. Users write configs in `.kogral/config.ncl`
3. Export to JSON via CLI: `nickel export --format json config.ncl`
4. Load JSON in Rust via `serde_json` into typed structs

Pattern (double validation):

    Nickel Config (.ncl)
        ↓ [nickel export]
    JSON (validated by Nickel contracts)
        ↓ [serde_json::from_str]
    Rust Struct (validated by serde)
        ↓
    Runtime (guaranteed valid config)

Bridge Code (`kb-core/src/config/nickel.rs`):

    #![allow(unused)]
    fn main() {
    pub fn load_config<P: AsRef<Path>>(path: P) -> Result<KbConfig> {
        // Export Nickel to JSON
        let json = export_nickel_to_json(path)?;

        // Deserialize to Rust struct
        let config: KbConfig = serde_json::from_str(&json)?;

        Ok(config)
    }

    fn export_nickel_to_json<P: AsRef<Path>>(path: P) -> Result<String> {
        let output = Command::new("nickel")
            .arg("export")
            .arg("--format").arg("json")
            .arg(path.as_ref())
            .output()?;

        Ok(String::from_utf8(output.stdout)?)
    }
    }

## Consequences

### Positive

✅ Type Safety: Config errors caught before runtime

- Invalid enum values fail at export: `'filesystm` → error
- Missing required fields detected: no `graph.name` → error
- Type mismatches prevented: `enabled = "yes"` → error (expects Bool)

✅ Self-Documenting: Schemas serve as documentation

- `| doc "Environment variable for API key"` describes fields
- Enum options visible in schema: `[| 'openai, 'claude, 'fastembed |]`
- Default values explicit: `| default = 'filesystem`

✅ Composition: Config reuse and overrides

    # base.ncl
    { graph = { version = "1.0.0" } }

    # project.ncl
    import "base.ncl" & { graph = { name = "my-project" } }

✅ Validation Before Deployment: Catch errors in CI

    # CI pipeline
    nickel typecheck config.ncl
    nickel export --format json config.ncl > /dev/null

✅ Conditional Logic: Environment-specific configs

    let is_prod = std.string.is_match "prod" (std.env.get "ENV") in
    {
      embeddings = {
        provider = if is_prod then 'openai else 'fastembed,
      },
    }

### Negative

❌ Learning Curve: Team must learn Nickel syntax

- Mitigation: Provide comprehensive examples in `config/` directory
- Mitigation: Document common patterns in `docs/config/`

❌ Tool Dependency: Requires `nickel` CLI installed

- Mitigation: Document installation in setup guide
- Mitigation: Check `nickel` availability in `kb init` command

❌ IDE Support: Limited compared to TOML

- Mitigation: Use LSP (nickel-lang-lsp) for VSCode/Neovim
- Mitigation: Syntax highlighting available for major editors

❌ Ecosystem Size: Smaller than TOML

- Mitigation: Nickel actively developed by Tweag
- Mitigation: Stable language specification (v1.0+)

### Neutral

⚪ Two-Stage Loading: Nickel → JSON → Rust

- Not a performance concern (config loaded once at startup)
- Adds resilience (double validation)
- Allows runtime config inspection (read JSON directly)

## Alternatives Considered

### JSON Schema

Rejected: Not ergonomic for humans to write

- No comments
- Verbose syntax (`{"key": "value"}` vs `key = value`)
- JSON Schema separate from config (duplication)

### YAML

Rejected: No type system, ambiguous parsing

- Boolean confusion: `yes`/`no`/`on`/`off`/`true`/`false`
- Indentation-sensitive (error-prone)
- No validation without external tools

### Dhall

Rejected: More complex than needed

- Turing-incomplete by design (limits use cases)
- Smaller ecosystem than Nickel
- Steeper learning curve

### KCL (KusionStack Configuration Language)

Rejected: Kubernetes-focused, less general-purpose

- Designed for K8s manifests
- Less mature than Nickel for general config

## Implementation Timeline

1. ✅ Define base schemas (`schemas/kb-config.ncl`)
2. ✅ Implement Nickel loader (`kb-core/src/config/nickel.rs`)
3. ✅ Create example configs (`config/defaults.ncl`, `config/production.ncl`)
4. ✅ Document Nickel usage (`docs/config/nickel-schemas.md`)
5. ⏳ Add LSP recommendations to setup guide
6. ⏳ Create Nickel → TOML migration tool (for existing users)

## Monitoring

Success Criteria:

- Config errors caught at export time (not runtime)
- Users can compose configs for different environments
- Team comfortable with Nickel syntax within 2 weeks

Metrics:

- Number of config validation errors caught before runtime
- Time to diagnose config issues (should decrease)
- User feedback on config complexity

## References

- Nickel Language
- Nickel User Manual
- platform-config pattern (reference implementation)
- TOML Specification

## Revision History

Date | Author | Change
--- | --- | ---
2026-01-17 | Architecture Team | Initial decision

Next ADR: ADR-002: FastEmbed via AI Providers