Jesús Pérez 44648e3206
chore: complete nickel migration and consolidate legacy configs
- Remove KCL ecosystem (~220 files deleted)
- Migrate all infrastructure to Nickel schema system
- Consolidate documentation: legacy docs → provisioning/docs/src/
- Add CI/CD workflows (.github/) and Rust build config (.cargo/)
- Update core system for Nickel schema parsing
- Update README.md and CHANGES.md for v5.0.0 release
- Fix pre-commit hooks: end-of-file, trailing-whitespace
- Breaking changes: KCL workspaces require migration
- Migration bridge available in docs/src/development/
2026-01-08 09:55:37 +00:00
..

Validators

Validation logic for configuration values using constraints and business rules.

Purpose

Validators provide:

  • Constraint checking - Numeric ranges, required fields
  • Business logic validation - Service-specific constraints
  • Error messages - Clear feedback on invalid values
  • Composition with configs - Validators applied during config generation

File Organization

validators/
├── README.md                           # This file
├── common-validator.ncl                # Ports, positive numbers, strings
├── network-validator.ncl               # IP addresses, bind addresses
├── path-validator.ncl                  # File paths, directories
├── resource-validator.ncl              # CPU, memory, disk
├── string-validator.ncl                # Workspace names, identifiers
├── orchestrator-validator.ncl          # Queue, workflow validation
├── control-center-validator.ncl        # RBAC, policy validation
├── mcp-server-validator.ncl            # MCP tools, capabilities
└── deployment-validator.ncl            # Resource allocation

Validation Patterns

1. Basic Range Validation

# validators/common-validator.ncl
let constraints = import "../constraints/constraints.toml" in

{
  ValidPort = fun port =>
    if port < constraints.common.server.port.min then
      std.contract.blame_with_message "Port < 1024" port
    else if port > constraints.common.server.port.max then
      std.contract.blame_with_message "Port > 65535" port
    else
      port,
}

2. Range Validator (Reusable)

# Reusable validator for any numeric range
ValidRange = fun min max value =>
  if value < min then
    std.contract.blame_with_message "Value < %{std.to_string min}" value
  else if value > max then
    std.contract.blame_with_message "Value > %{std.to_string max}" value
  else
    value,

3. Enum Validation

{
  ValidStorageBackend = fun backend =>
    if backend != 'filesystem &&
       backend != 'rocksdb &&
       backend != 'surrealdb &&
       backend != 'postgres then
      std.contract.blame_with_message "Invalid backend" backend
    else
      backend,
}

4. String Validation

{
  ValidNonEmptyString = fun s =>
    if s == "" then
      std.contract.blame_with_message "Cannot be empty" s
    else
      s,

  ValidWorkspaceName = fun name =>
    if std.string.matches "^[a-z0-9_-]+$" name then
      name
    else
      std.contract.blame_with_message "Invalid workspace name" name,
}

Common Validators

common-validator.ncl

let constraints = import "../constraints/constraints.toml" in

{
  # Port validation
  ValidPort = fun port =>
    if port < constraints.common.server.port.min then error "Port too low"
    else if port > constraints.common.server.port.max then error "Port too high"
    else port,

  # Positive integer
  ValidPositiveNumber = fun n =>
    if n <= 0 then error "Must be positive"
    else n,

  # Non-empty string
  ValidNonEmptyString = fun s =>
    if s == "" then error "Cannot be empty"
    else s,

  # Generic range validator
  ValidRange = fun min max value =>
    if value < min then error "Value below minimum"
    else if value > max then error "Value above maximum"
    else value,
}

resource-validator.ncl

let constraints = import "../constraints/constraints.toml" in
let common = import "./common-validator.ncl" in

{
  # Validate CPU cores for deployment mode
  ValidCPUCores = fun mode cores =>
    let limits = constraints.deployment.{mode} in
    common.ValidRange limits.cpu.min limits.cpu.max cores,

  # Validate memory allocation
  ValidMemory = fun mode memory_mb =>
    let limits = constraints.deployment.{mode} in
    common.ValidRange limits.memory_mb.min limits.memory_mb.max memory_mb,
}

Service-Specific Validators

orchestrator-validator.ncl

let constraints = import "../constraints/constraints.toml" in
let common = import "./common-validator.ncl" in

{
  # Validate worker count
  ValidWorkers = fun workers =>
    common.ValidRange
      constraints.orchestrator.workers.min
      constraints.orchestrator.workers.max
      workers,

  # Validate queue concurrency
  ValidConcurrentTasks = fun tasks =>
    common.ValidRange
      constraints.orchestrator.queue.concurrent_tasks.min
      constraints.orchestrator.queue.concurrent_tasks.max
      tasks,

  # Validate batch parallelism
  ValidParallelLimit = fun limit =>
    common.ValidRange
      constraints.orchestrator.batch.parallel_limit.min
      constraints.orchestrator.batch.parallel_limit.max
      limit,

  # Validate task timeout (ms)
  ValidTaskTimeout = fun timeout =>
    if timeout < 1000 then error "Timeout < 1 second"
    else if timeout > 86400000 then error "Timeout > 24 hours"
    else timeout,
}

control-center-validator.ncl

{
  # JWT token expiration
  ValidTokenExpiration = fun seconds =>
    if seconds < 300 then error "Token expiration < 5 min"
    else if seconds > 604800 then error "Token expiration > 7 days"
    else seconds,

  # Rate limit threshold
  ValidRateLimit = fun requests_per_minute =>
    if requests_per_minute < 10 then error "Rate limit too low"
    else if requests_per_minute > 10000 then error "Rate limit too high"
    else requests_per_minute,
}

mcp-server-validator.ncl

{
  # Max concurrent tool executions
  ValidConcurrentTools = fun count =>
    if count < 1 then error "Must allow >= 1 concurrent"
    else if count > 20 then error "Max 20 concurrent tools"
    else count,

  # Max resource size
  ValidMaxResourceSize = fun bytes =>
    if bytes < 1048576 then error "Min 1 MB"
    else if bytes > 1073741824 then error "Max 1 GB"
    else bytes,
}

Composition with Configs

Validators are applied in config files:

# configs/orchestrator.solo.ncl
let validators = import "../validators/orchestrator-validator.ncl" in

{
  orchestrator = {
    server.workers = validators.ValidWorkers 2,  # Validated
    queue.max_concurrent_tasks = validators.ValidConcurrentTasks 3,  # Validated
  },
}

Validation happens at:

  1. Config composition - When config is evaluated
  2. Nickel typecheck - When config is typechecked
  3. Form submission - When TypeDialog form is submitted (constraints)
  4. TOML export - When Nickel is exported to TOML

Error Handling

Validation Errors

# If validation fails during config evaluation:
# Error: Port too high

Meaningful Messages

Always provide context in error messages:

# Bad
std.contract.blame "Invalid" value

# Good
std.contract.blame_with_message "Port must be 1024-65535, got %{std.to_string value}" port

Best Practices

  1. Reuse common validators - Build from common-validator.ncl
  2. Name clearly - Prefix with "Valid" (ValidPort, ValidWorkers, etc.)
  3. Error messages - Include valid range or enum in message
  4. Test edge cases - Verify min/max boundary values
  5. Document assumptions - Why a constraint exists

Testing Validators

# Test a single validator
nickel eval -c 'import "validators/orchestrator-validator.ncl" as v in v.ValidWorkers 2'

# Test config with validators
nickel typecheck provisioning/.typedialog/provisioning/platform/configs/orchestrator.solo.ncl

# Evaluate config (runs validators)
nickel eval provisioning/.typedialog/provisioning/platform/configs/orchestrator.solo.ncl

# Export to TOML (validates during export)
nickel export --format toml provisioning/.typedialog/provisioning/platform/configs/orchestrator.solo.ncl

Adding a New Validator

  1. Create validator function in appropriate file:

    ValidMyValue = fun value =>
      if value < minimum then error "Too low"
      else if value > maximum then error "Too high"
      else value,
    
  2. Add constraint to constraints.toml if needed:

    [service.feature.my_value]
    min = 1
    max = 100
    
  3. Use in config:

    my_value = validators.ValidMyValue 50,
    
  4. Add form constraint (if interactive):

    [[elements]]
    name = "my_value"
    min = "${constraint.service.feature.my_value.min}"
    max = "${constraint.service.feature.my_value.max}"
    
  5. Test:

    nickel typecheck configs/service.mode.ncl
    

Version: 1.0.0 Last Updated: 2025-01-05