evalengine

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 23, 2026 License: MIT Imports: 15 Imported by: 1

README

evalengine

A Go evaluation engine that compiles YAML-defined rules into CEL expressions, resolves their dependencies automatically, and executes them against a protobuf message.

Install

go get github.com/laenen-partners/evalengine

Usage

Define evaluations in YAML
evaluations:
  - name: score_eval
    description: Score meets minimum threshold
    expression: "input.score >= 100"
    writes: score_sufficient
    resolution_workflow: ScoreBoostWorkflow
    resolution: "Boost score to meet threshold"
    severity: blocking
    category: score
    failure_mode: "soft"
    cache_ttl: "10m"

  - name: active_eval
    description: Account is active
    expression: "input.is_active == true"
    writes: is_active
    resolution: "Activate account"
    severity: blocking
    category: status

  - name: eligible_eval
    description: Eligible when score sufficient and active
    expression: "score_sufficient == true && is_active == true"
    writes: eligible
    resolution: "Meet all criteria"
    severity: blocking
    category: combined
    cache_ttl: "1m"

All dependencies are auto-derived from the CEL expression:

  • input.* field accesses are extracted from select chains (e.g. input.score)
  • Bare identifiers matching another evaluator's writes create dependency edges (e.g. score_sufficient)

Explicit reads can still be provided and will be merged with auto-derived ones.

Create and run the engine
cfg, _ := evalengine.LoadDefinitionsFromFile("evals.yaml")
eng, _ := evalengine.NewEngine(cfg, &mypb.MyMessage{})

input := &mypb.MyMessage{Score: 150, IsActive: true}
results := eng.Run(input)

for _, r := range results {
    fmt.Printf("%s (%s): passed=%v\n", r.DisplayName, r.Name, r.Passed)
}

Result.Name is the writes field (canonical key). Result.DisplayName is the YAML name field (human-readable).

Derive status
status := eng.DeriveStatus(results)

switch status {
case evalengine.StatusAllPassed:
    // all evaluations passed
case evalengine.StatusWorkflowActive:
    // a resolution workflow is running
case evalengine.StatusActionRequired:
    // manual action needed
case evalengine.StatusPending:
    // preconditions not met (data incomplete)
case evalengine.StatusBlocked:
    // upstream dependencies not met
}

Priority: AllPassed > WorkflowActive > ActionRequired > Pending > Blocked.

Preconditions

Preconditions let you distinguish "data not yet provided" from "data fails the check":

- name: adult_party_check
  preconditions:
    - expression: 'size(input.parties) > 0'
      description: "At least one party must be provided"
  expression: 'input.parties.exists(p, p.age >= 18)'
  writes: has_adult

When preconditions fail, the result is Pending: true with PendingPreconditions listing the descriptions of the failing checks. The main expression is not evaluated.

r := results["has_adult"]
if r.Pending {
    fmt.Println("Waiting for:", r.PendingPreconditions)
}
Caching

Some input data may be expensive to retrieve. The cache lets you skip re-evaluation when results are still fresh.

// First run — build cache with fingerprints.
results := eng.Run(input)
cache := eng.ToCachedResults(results, input, time.Now())
// Persist cache (Redis, database, etc.)

// Later — reuse cached results when possible.
results, reused := eng.RunWithCache(input, cache, time.Now())
// reused["score_sufficient"] == true means it was served from cache.

Two tiers:

  1. TTL — if the cached result is within its cache_ttl, reuse it directly.
  2. Fingerprint — if TTL expired but the proto input fields haven't changed (SHA256 match), reuse the result anyway. Only applies to evaluators that read exclusively from input.* fields.

The *Engine is stateless and safe to reuse across requests. Cache it by config content hash to avoid recompilation:

var engineCache sync.Map

func getEngine(yamlBytes []byte, input proto.Message) (*evalengine.Engine, error) {
    key := sha256.Sum256(yamlBytes)
    if cached, ok := engineCache.Load(key); ok {
        return cached.(*evalengine.Engine), nil
    }
    eng, err := evalengine.NewEngineFromBytes(yamlBytes, input)
    if err != nil {
        return nil, err
    }
    engineCache.Store(key, eng)
    return eng, nil
}
YAML config reference
Field Required Description
name yes Evaluator identifier, exposed as Result.DisplayName
description no Human-readable description
expression yes CEL expression, must return bool
reads no Auto-derived from CEL AST. Explicit values merged and deduplicated
writes yes Output name — used as result key, CEL variable, and graph node
resolution_workflow no Workflow ID to trigger on failure
resolution no Human description of how to resolve failure
severity no Severity level (e.g., blocking). Passed through to Result
category no Grouping category. Passed through to Result
failure_mode no Opaque string (e.g., soft, hard). Passed through to Result
preconditions no List of {expression, description} — CEL bool guards before main expression
cache_ttl no Go duration (e.g., 10m, 1h). Default: no caching
Validation

evalengine provides multiple ways to validate YAML configuration files.

JSON Schema

The repository includes a JSON Schema that defines the YAML format. Use it for editor autocomplete and CI validation.

VS Code — add to your YAML file or workspace settings:

# yaml-language-server: $schema=https://raw.githubusercontent.com/laenen-partners/evalengine/main/evalengine.schema.json
evaluations:
  - name: ...

CLI with ajv-cli:

npm install -g ajv-cli ajv-formats
ajv validate -s evalengine.schema.json -d evals.yaml
CLI tool

Install the evalvalidate command to validate YAML files structurally (required fields, duplicate writes, cache_ttl format, precondition expressions):

go install github.com/laenen-partners/evalengine/cmd/evalvalidate@latest
evalvalidate evals.yaml

Accepts multiple files:

evalvalidate evals/*.yaml
Go API

Structural validation — no proto message needed, suitable for CI pipelines:

cfg, err := evalengine.LoadDefinitionsFromFile("evals.yaml")
if err != nil {
    log.Fatal(err)
}
if err := evalengine.ValidateConfig(cfg); err != nil {
    log.Fatal(err) // *evalengine.ValidationError with all issues
}

Full validation — includes CEL compilation and dependency graph checks:

if err := evalengine.Validate(cfg, &mypb.MyMessage{}); err != nil {
    log.Fatal(err)
}
Dependency graph

The graph is auto-derived from CEL expressions and validated at engine creation:

graph := eng.Graph()
graph.ExecutionOrder()               // topologically sorted evaluator names
graph.DependenciesMet(name, results) // true if all upstream deps passed
graph.BlockedBy(name, results)       // which upstream deps failed
graph.Blocks(name)                   // which evaluators depend on this one (reverse lookup)
graph.Issues()                       // validation issues (circular deps, missing producers, etc.)
Input field extraction

Query what proto fields an evaluator's expression accesses:

eng.InputFields("score_sufficient") // ["input.score"]
eng.InputFields("is_active")        // ["input.nested_object.is_active"]
eng.InputFields("eligible")         // [] (only reads upstream outputs)

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ComputeFingerprint

func ComputeFingerprint(reads []FieldRef, msg proto.Message) string

ComputeFingerprint hashes the values of the declared input fields from the given proto message. If the hash matches a cached value, the previous evaluation result can be reused.

func ToCachedResults added in v0.2.0

func ToCachedResults(results []Result, evaluatedAt time.Time) map[string]CachedResult

ToCachedResults converts a slice of results into a cache map keyed by name, stamped with the given evaluation time. No fingerprints are computed; use Engine.ToCachedResults for fingerprint-aware caching.

func Validate added in v0.5.0

func Validate(cfg *EvalConfig, input proto.Message) error

Validate performs full validation: structural checks followed by CEL compilation and dependency graph construction. This catches everything ValidateConfig catches plus invalid CEL expressions, type mismatches, circular dependencies, and missing producers.

func ValidateConfig added in v0.5.0

func ValidateConfig(cfg *EvalConfig) error

ValidateConfig performs structural validation on an EvalConfig without requiring a proto message. It checks required fields, duplicate writes, cache_ttl format, and precondition expressions.

Types

type CELEvaluator

type CELEvaluator struct {
	// contains filtered or unexported fields
}

CELEvaluator implements Evaluator using a compiled CEL program.

func NewCELEvaluator

func NewCELEvaluator(env *cel.Env, def EvalDefinition) (*CELEvaluator, error)

NewCELEvaluator compiles a CEL expression and returns an evaluator.

func (*CELEvaluator) CacheTTL

func (e *CELEvaluator) CacheTTL() time.Duration

func (*CELEvaluator) Category added in v0.4.0

func (e *CELEvaluator) Category() string

func (*CELEvaluator) DisplayName added in v0.4.0

func (e *CELEvaluator) DisplayName() string

func (*CELEvaluator) Evaluate

func (e *CELEvaluator) Evaluate(activation map[string]any) Result

func (*CELEvaluator) EvaluatePreconditions added in v0.4.0

func (e *CELEvaluator) EvaluatePreconditions(activation map[string]any) []string

EvaluatePreconditions runs all precondition programs against the activation. Returns the descriptions of preconditions that evaluated to false (or errored). If a precondition has no description, the expression is returned instead.

func (*CELEvaluator) FailureMode added in v0.4.0

func (e *CELEvaluator) FailureMode() string

func (*CELEvaluator) HasPreconditions added in v0.4.0

func (e *CELEvaluator) HasPreconditions() bool

func (*CELEvaluator) Name

func (e *CELEvaluator) Name() string

Interface accessors — these expose definition metadata through the Evaluator interface so that execute() never needs to type-assert to *CELEvaluator.

func (*CELEvaluator) Reads

func (e *CELEvaluator) Reads() []FieldRef

func (*CELEvaluator) Resolution added in v0.4.0

func (e *CELEvaluator) Resolution() string

func (*CELEvaluator) ResolutionWorkflow

func (e *CELEvaluator) ResolutionWorkflow() string

func (*CELEvaluator) Severity added in v0.4.0

func (e *CELEvaluator) Severity() string

func (*CELEvaluator) Writes

func (e *CELEvaluator) Writes() FieldRef

type CachedResult added in v0.2.0

type CachedResult struct {
	Result      Result
	EvaluatedAt time.Time
	Fingerprint string // hash of proto input fields; empty if not computed
}

CachedResult wraps a Result with the time it was evaluated and an optional fingerprint of the input fields used to produce it. The caller owns persistence — the engine is stateless.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine loads evaluation definitions, compiles CEL expressions, builds the dependency graph, and runs all evaluators against a proto input.

func NewEngine

func NewEngine(cfg *EvalConfig, input proto.Message, opts ...cel.EnvOption) (*Engine, error)

NewEngine creates an evaluation engine from a config and a proto message that serves as the input type. The proto is registered in the CEL environment as the variable "input" — YAML expressions reference fields as "input.<field>". Extra opts are forwarded to NewCELEnvironment for additional declarations.

func NewEngineFromBytes

func NewEngineFromBytes(data []byte, input proto.Message, opts ...cel.EnvOption) (*Engine, error)

NewEngineFromBytes loads evaluation definitions from raw YAML bytes.

func NewEngineFromFile

func NewEngineFromFile(path string, input proto.Message, opts ...cel.EnvOption) (*Engine, error)

NewEngineFromFile loads evaluation definitions from a YAML file.

func (*Engine) DeriveStatus

func (e *Engine) DeriveStatus(results []Result) Status

DeriveStatus derives the overall status from evaluation results.

func (*Engine) Evaluators

func (e *Engine) Evaluators() []Evaluator

Evaluators returns all registered evaluators.

func (*Engine) Graph

func (e *Engine) Graph() *EvalGraph

Graph returns the dependency graph.

func (*Engine) InputFields added in v0.4.0

func (e *Engine) InputFields(name string) []string

InputFields returns the input field paths referenced in the evaluator's CEL expression, extracted from the compiled AST. Only returns paths rooted at "input." (e.g. "input.score", "input.nested_object.is_active").

func (*Engine) Run

func (e *Engine) Run(input proto.Message) []Result

Run executes all evaluators in dependency order against the given input. The proto is bound to the "input" CEL variable. Upstream evaluator results are injected by their writes-field name for downstream expressions.

func (*Engine) RunMap

func (e *Engine) RunMap(input proto.Message) map[string]Result

RunMap executes all evaluators and returns results indexed by name.

func (*Engine) RunWithCache added in v0.2.0

func (e *Engine) RunWithCache(input proto.Message, cache map[string]CachedResult, now time.Time) ([]Result, map[string]bool)

RunWithCache executes evaluators, reusing cached results that are still within their CacheTTL. The caller owns the cache — the engine is stateless. Pass time.Now() as now; a zero now disables caching (equivalent to Run). Returns the full result set and a map indicating which evaluators were served from cache (true = reused, absent = re-evaluated).

func (*Engine) RunWithCacheMap added in v0.2.0

func (e *Engine) RunWithCacheMap(input proto.Message, cache map[string]CachedResult, now time.Time) (map[string]Result, map[string]bool)

RunWithCacheMap is like RunWithCache but returns results indexed by name.

func (*Engine) ToCachedResults added in v0.2.0

func (e *Engine) ToCachedResults(results []Result, input proto.Message, evaluatedAt time.Time) map[string]CachedResult

ToCachedResults converts results into a cache map with fingerprints computed from the evaluator's input reads and the proto message.

type EvalConfig

type EvalConfig struct {
	Evaluations []EvalDefinition `yaml:"evaluations"`
}

EvalConfig is the top-level YAML structure.

func LoadDefinitions

func LoadDefinitions(r io.Reader) (*EvalConfig, error)

LoadDefinitions parses evaluation definitions from a reader.

func LoadDefinitionsFromFile

func LoadDefinitionsFromFile(path string) (*EvalConfig, error)

LoadDefinitionsFromFile loads evaluation definitions from a YAML file.

type EvalDefinition

type EvalDefinition struct {
	Name               string         `yaml:"name"`
	Description        string         `yaml:"description"`
	Expression         string         `yaml:"expression"`
	Reads              []FieldRef     `yaml:"reads"`
	Writes             FieldRef       `yaml:"writes"`
	ResolutionWorkflow string         `yaml:"resolution_workflow"`
	Resolution         string         `yaml:"resolution"`
	Severity           string         `yaml:"severity"`
	Category           string         `yaml:"category"`
	FailureMode        string         `yaml:"failure_mode"`
	Preconditions      []Precondition `yaml:"preconditions"`
	CacheTTL           string         `yaml:"cache_ttl"`
	CacheTTLDuration   time.Duration  `yaml:"-"`
}

EvalDefinition is a single evaluator loaded from YAML.

type EvalGraph

type EvalGraph struct {
	// contains filtered or unexported fields
}

EvalGraph holds the auto-calculated dependency graph derived from evaluator reads/writes declarations.

func BuildGraph

func BuildGraph(evaluators []Evaluator) (*EvalGraph, error)

BuildGraph derives the dependency graph from evaluator declarations. Reads prefixed with "input." are treated as raw proto field references — no producer dependency is created for them.

func (*EvalGraph) BlockedBy

func (g *EvalGraph) BlockedBy(name string, results map[string]Result) []string

BlockedBy returns the names of upstream evaluators that have not passed.

func (*EvalGraph) Blocks added in v0.4.0

func (g *EvalGraph) Blocks(name string) []string

Blocks returns the names of evaluators that directly depend on the given evaluator (reverse dependency lookup).

func (*EvalGraph) DependenciesMet

func (g *EvalGraph) DependenciesMet(name string, results map[string]Result) bool

DependenciesMet returns true if all upstream dependencies of the given evaluator have passed.

func (*EvalGraph) ExecutionOrder

func (g *EvalGraph) ExecutionOrder() []string

ExecutionOrder returns the topologically sorted evaluator names.

func (*EvalGraph) Issues

func (g *EvalGraph) Issues() []Issue

Issues returns all validation issues found during graph construction.

func (*EvalGraph) MaxDepth

func (g *EvalGraph) MaxDepth() int

MaxDepth returns the longest dependency chain length.

type Evaluator

type Evaluator interface {
	Name() string
	DisplayName() string
	Reads() []FieldRef
	Writes() FieldRef
	CacheTTL() time.Duration
	Resolution() string
	ResolutionWorkflow() string
	Severity() string
	Category() string
	FailureMode() string
	HasPreconditions() bool
	EvaluatePreconditions(activation map[string]any) []string
	Evaluate(activation map[string]any) Result
}

Evaluator is the interface for all evaluators.

type FieldRef

type FieldRef string

FieldRef is a dependency reference — either an evaluator output (bare name like "score_sufficient") or an input field path (like "input.email_verified"). Reads prefixed with "input." refer to the proto passed to Engine.Run.

func (FieldRef) String

func (f FieldRef) String() string

String returns the string representation.

type Issue

type Issue struct {
	Type     string // "circular_dependency", "missing_producer", "duplicate_producer", "orphan_output"
	Severity string // "error", "warning", "info"
	Message  string
}

Issue represents a validation issue found in the dependency graph.

type Precondition added in v0.4.0

type Precondition struct {
	Expression  string `yaml:"expression"`
	Description string `yaml:"description"`
}

Precondition is a CEL expression that must evaluate to true before the main expression runs. If it fails, the evaluator is marked Pending rather than Failed.

type Result

type Result struct {
	Name                 string
	DisplayName          string
	Passed               bool
	Pending              bool
	Error                string
	Resolution           string
	ResolutionWorkflow   string
	Severity             string
	Category             string
	FailureMode          string
	PendingPreconditions []string
}

Result represents the outcome of a single evaluator run.

type Status

type Status string

Status represents the logical outcome of evaluating all results.

const (
	StatusAllPassed      Status = "StatusAllPassed"      // every evaluation passed
	StatusWorkflowActive Status = "StatusWorkflowActive" // a resolution workflow is running
	StatusActionRequired Status = "StatusActionRequired" // a failing eval needs manual action
	StatusPending        Status = "StatusPending"        // a failing eval is pending preconditions
	StatusBlocked        Status = "StatusBlocked"        // a failing eval's dependencies aren't met
)

func DeriveStatus

func DeriveStatus(results []Result, graph *EvalGraph) Status

DeriveStatus determines the overall status from evaluation results. Status is derived, never stored directly — it reflects the current state of all evaluations.

type ValidationError added in v0.5.0

type ValidationError struct {
	Errors []string
}

ValidationError collects multiple validation issues.

func (*ValidationError) Error added in v0.5.0

func (e *ValidationError) Error() string

Directories

Path Synopsis
cmd
evalvalidate command
evalvalidate validates evalengine YAML configuration files.
evalvalidate validates evalengine YAML configuration files.
proto

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL