dmn

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Sep 9, 2026 License: AGPL-3.0 Imports: 19 Imported by: 0

Documentation

Overview

Package dmn integrates the temis DMN decision engine (github.com/pblumer/temis) into Atlas, so a BPMN business rule task can delegate a decision and get an answer back.

The integration deliberately mirrors how service tasks reach external workers (ADR-0007), so it inherits the engine's durability guarantees without touching the hot path (ADR-0014):

  • A DMN model is compiled by temis once, at deploy time, into immutable thread-safe decisions held in a Registry (invariant I5: compile, don't interpret — no XML parsing or FEEL compilation at runtime).
  • A business rule task creates a job carrying the reserved DMN job type. The processor never evaluates a decision itself, so it stays allocation-free (invariant I1) and free of the temis dependency.
  • The in-process Handler — a job worker — pulls those jobs, evaluates the decision off the processor goroutine, and completes the job, which drives the token onward through the normal completion path. Evaluation is a post-durability side effect, exactly like any other worker (invariant I2).

Because there is no process-variable subsystem yet (Milestone 1), a business rule task feeds its decision a static input context recorded at deploy time and its outputs are surfaced through a caller-supplied sink rather than written back as variables. Wiring real input/output variable mappings is future work.

Index

Constants

This section is empty.

Variables

View Source
var ErrNotFound = errors.New("dmn: no model matches the reference")

ErrNotFound is returned by a Resolver when no model matches the handle. It is deliberately distinct from an I/O failure so a caller can tell "unresolved reference" (a user-fixable modelRef) apart from "the model source is broken" (an infrastructure error).

Functions

func BuildInputs added in v0.5.0

func BuildInputs(store state.Reader, elementKey, piKey uint64, staticJSON string, mappings []compiler.DecisionInputMapping) (map[string]any, error)

buildInputs assembles a decision's input context: the static constant inputs as a base, overlaid with the input mappings evaluated over the instance's live variables (a mapping overrides a static input of the same name). A nil result evaluates every referenced decision input to FEEL null.

The mappings resolve over the business rule task's full scope chain (elementKey up to the process root), not just the process scope, so a task nested in a subprocess or a multi-instance body reads its enclosing scope's variables — e.g. a per-row `inputElement` bound by a multi-instance loop (ADR-0068 scope-chain resolution, ADR-0077, ADR-0084). piKey binds the reserved processInstanceKey.

func DecisionHandler

func DecisionHandler(store state.Reader, lookup ProcessLookup, bind Bind, sink func(Result)) job.CompletingHandler

DecisionHandler builds a job handler that evaluates the DMN decision behind a business rule task using the Evaluator that bind resolves for it. It owns the shared input/output-mapping semantics (ADR-0039) so local and central decisions cannot drift: for each job it

  • resolves the decision, static inputs, input mappings, and result variable from the compiled process,
  • builds the decision's input context by merging the static inputs with the input mappings evaluated over the instance's live variables (a mapping wins over a static input of the same name),
  • evaluates the decision through the bound Evaluator, and
  • returns the result as the process variable named by resultVariable, which the job completion writes back into the instance so a downstream gateway can route on it.

Evaluation is a post-durability side effect off the processor goroutine (invariant I2/I4). Returning an error leaves the job pending. Alongside the output variables, the completion carries a durable decision-evaluation record — the inputs, outputs, and trace — so an operator can inspect how the decision was made live and after the fact (ADR-0066). sink, if non-nil, additionally observes each result. Handler (local) and the temis worker (central, ADR-0050) are both built on it.

func Handler

func Handler(store state.Reader, lookup ProcessLookup, reg *Registry, sink func(Result)) job.CompletingHandler

Handler builds the in-process, local DMN worker: a DecisionHandler whose Evaluator is the embedded temis library, evaluating each decision against the model deployed under the process's own key (ADR-0014). Register it with a job.Runner via HandleCompleting for the reserved DMN job type (compiler.DMNJobTypeIndex). sink, if non-nil, observes each result.

func JSONObject added in v0.5.0

func JSONObject(m map[string]any) string

jsonObject renders a decision's input or output context as canonical JSON for a durable evaluation record. An empty context is recorded as "{}" (an object with no members) rather than JSON null, so the debugging view always shows an object; a marshal failure degrades the same way rather than failing the completion.

func OutputVariable added in v0.5.0

func OutputVariable(name string, outputs map[string]any) model.VariableValue

outputVariable turns a decision's outputs into the process variable named by the task's result variable. A single-output decision stores that value directly (so a condition reads it as a scalar); a multi-output decision stores the whole output map as a structured (JSON) context. The value is canonicalized through the same expr path as any other variable, so it round-trips on replay.

Types

type Bind

Bind resolves the Evaluator for a business rule task on a compiled process — the decision engine bound to it. A returned error (e.g. an unregistered worker) leaves the job pending like any worker error.

type DecisionField

type DecisionField struct {
	Name string `json:"name"`
	Type string `json:"type,omitempty"`
}

DecisionField is one input or output of a decision, for authoring tooling: a name and its declared FEEL type (empty when the model declares none).

type DecisionInfo

type DecisionInfo struct {
	ID     string          `json:"id"`
	Name   string          `json:"name"`
	Inputs []DecisionField `json:"inputs"`
	Output DecisionField   `json:"output"`
}

DecisionInfo is a decision's self-description for the Modeler's decision picker (ADR-0050): the id to reference it by, its display name, the input data it consumes (so a business rule task's input mappings can be auto-filled), and its output (the process variable a result naturally lands in).

type DeployedDecision

type DeployedDecision struct {
	ID     string
	Name   string
	Model  string
	Inputs []DecisionField
	Output DecisionField
}

DeployedDecision is a decision available from a deployed model, described for the Modeler's decision picker (ADR-0050): its id, declared inputs and output, and the name of the model that provides it. It lets an author select — and auto-fill the inputs of — a decision that is deployed (and thus runnable) even when no separate DMN reference artifact exists for it.

type DirResolver

type DirResolver struct {
	Dir string
}

DirResolver resolves a handle against a directory of DMN files exported from temis: modelRef "risk-score" resolves to <Dir>/risk-score.dmn, falling back to <Dir>/risk-score.xml. It is the zero-config default source; a temis git or service resolver can replace it behind the Resolver interface without touching callers.

func (DirResolver) Resolve

func (r DirResolver) Resolve(_ context.Context, modelRef string) ([]byte, error)

Resolve reads the model file for a handle. A missing model yields ErrNotFound; any other read failure is returned as-is so the caller reports it as an infrastructure error, not an unresolved reference.

type Evaluation

type Evaluation struct {
	Outputs map[string]any
	Trace   []byte
}

Evaluation is what running a decision yields: its named outputs and, when the engine can produce one, the temis trace explaining how it got there — which tables ran and which rules fired (ADR-0066). Trace is canonical JSON or nil when no trace is available (a literal-expression decision, or a remote decision whose worker returns none).

type Evaluator

type Evaluator func(ctx context.Context, decisionId string, inputs map[string]any) (Evaluation, error)

Evaluator evaluates a decision by id against an input context and returns its outputs and (when available) trace. It is the seam between a business rule task's I/O semantics and the engine that runs the decision: the local embedded temis library and a remote temis worker each provide one (ADR-0050).

type GraphEdge

type GraphEdge struct {
	Type   string `json:"type"`
	Source string `json:"source"`
	Target string `json:"target"`
}

GraphEdge is one requirement, directed from the required (upstream) element to the one that requires it — matching the DMN arrow direction. Type is "informationRequirement" or "knowledgeRequirement".

type GraphNode

type GraphNode struct {
	ID       string  `json:"id"`
	Type     string  `json:"type"`
	Name     string  `json:"name"`
	DataType string  `json:"dataType,omitempty"`
	VarName  string  `json:"varName,omitempty"`
	HasTable bool    `json:"hasTable,omitempty"`
	X        float64 `json:"x,omitempty"`
	Y        float64 `json:"y,omitempty"`
	Width    float64 `json:"width,omitempty"`
	Height   float64 `json:"height,omitempty"`
}

GraphNode is one element of a DMN model's decision requirements graph, for a read-only viewer (ADR-0014's non-goal is a DMN *editor*; viewing what a referenced model contains is in scope). Type is "decision", "inputData", or "businessKnowledgeModel". X/Y/Width/Height carry the authored DMNDI bounds when the model has a diagram (all zero otherwise, so the client auto-lays-out).

type ModelGraph

type ModelGraph struct {
	Resolved  bool        `json:"resolved"`
	Valid     bool        `json:"valid"`
	ModelName string      `json:"modelName,omitempty"`
	Message   string      `json:"message,omitempty"`
	Nodes     []GraphNode `json:"nodes"`
	Edges     []GraphEdge `json:"edges"`
}

ModelGraph is a referenced DMN model's requirements graph plus its resolve/valid status, so a viewer can render the diagram or show why it can't. Nodes/Edges are empty (never null) unless the model resolved and compiled cleanly.

type ProcessLookup

type ProcessLookup func(defKey uint64) *compiler.CompiledProcess

ProcessLookup resolves a process-definition key to its compiled process. The worker uses it to find the decision, inputs, and result variable a business-rule job belongs to, so one handler serves every deployed process.

type Registry

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

Registry holds the DMN models deployed alongside process definitions. It compiles each model once with temis and keeps the immutable result, keyed by the owning process-definition key, ready for cheap repeated evaluation.

A Registry is safe for concurrent evaluation once populated. Populate it (via Deploy) before the processes that use it start running.

func NewRegistry

func NewRegistry() *Registry

NewRegistry creates an empty registry over a fresh temis engine.

func (*Registry) Deploy

func (r *Registry) Deploy(defKey uint64, dmnXML []byte) error

Deploy compiles a DMN model and registers it under the process-definition key of the process whose business rule tasks reference it. A process may bundle several models (its tasks can call decisions from different models), so Deploy appends — call it once per bundled model. It also updates the latest-version pointer for every decision the model provides (ADR-0063), so a latest-bound task resolves the newest deployed version. Compilation happens here, at deploy time, never at evaluation time (invariant I5). It returns an error if temis cannot parse or compile the model.

func (*Registry) DeployedDecisions

func (r *Registry) DeployedDecisions() []DeployedDecision

DeployedDecisions describes every decision provided by the newest deployed model that supplies it (ADR-0063 latest binding), for the Modeler's picker. It reads the registry's already-compiled models — no XML parsing or resolver I/O — so it must run on the registry's owning goroutine (the run loop), the same single-writer discipline as Deploy. Results are sorted by decision id for a stable picker.

func (*Registry) Evaluate

func (r *Registry) Evaluate(ctx context.Context, defKey uint64, decisionId string, in map[string]any) (map[string]any, error)

Evaluate runs the named decision from the model deployed under defKey against the given input context and returns its outputs (decision name → value). It is the runtime hot spot of the integration, but it runs on a worker, off the processor goroutine.

func (*Registry) EvaluateLatest

func (r *Registry) EvaluateLatest(ctx context.Context, decisionId string, in map[string]any) (map[string]any, error)

EvaluateLatest runs the named decision from the newest deployed model that provides it (ADR-0063), for a latest-bound business rule task. It is otherwise identical to Evaluate; only the model selection differs.

func (*Registry) EvaluateLatestTraced

func (r *Registry) EvaluateLatestTraced(ctx context.Context, decisionId string, in map[string]any) (map[string]any, []byte, error)

EvaluateLatestTraced is EvaluateLatest plus the temis trace (see EvaluateTraced).

func (*Registry) EvaluateTraced

func (r *Registry) EvaluateTraced(ctx context.Context, defKey uint64, decisionId string, in map[string]any) (map[string]any, []byte, error)

EvaluateTraced is Evaluate plus the temis trace explaining how the decision was made — which tables ran, which rules matched, and why (ADR-0066). The trace is canonical JSON (temis's tdmn.Trace tree) or nil for a decision with no table logic (a literal expression). The DMN worker uses it to retain a debuggable record of the evaluation. Tracing runs off the processor goroutine, so its extra allocation is not on any hot path (temis's WithTrace, ADR-0013/WP-51).

func (*Registry) Reload added in v0.4.0

func (r *Registry) Reload(defKey uint64, dmnXML []byte) (string, error)

Reload is Deploy for a model that is *already* deployed — one snapshotted into a deployment record, coming back at startup — and so it does not re-apply the deploy-time gate above (ADR-0177). Refusing the model here would not undeploy anything; it would only keep the server from starting, with every other definition and every running instance behind it, because a diagnostic that did not exist when the model was deployed exists now.

The model is registered and the error diagnostics are returned rendered for display ("" when there are none), for the caller to report. Rendered, not structured, because temis documents diagnostic messages as human-readable and explicitly not a stable API: they are for an operator to read, not for code to act on.

This is safe because of what temis guarantees about a diagnostic: malformed XML is a hard error, but per-decision problems leave the rest of the model compiled, and a decision whose logic failed to compile is "present but not executable" — so evaluating *that* decision fails the way a failing decision has always failed, as a job error on a worker, while every other decision in the model still answers. A hard compile error still returns an error here: there is no model to bring back.

type Resolver

type Resolver interface {
	Resolve(ctx context.Context, modelRef string) ([]byte, error)
}

Resolver turns a DMN reference handle — the modelRef an Atlas project stores (ADR-0034) — into the DMN model XML authored in temis. It is the seam between "this project references decision X" and "here is X's model": a filesystem folder of temis-exported models today, a temis git repo or service later. Implementations must be safe for concurrent use.

type Result

type Result struct {
	ElementInstanceKey uint64
	ProcessDefKey      uint64
	DecisionId         string
	Inputs             map[string]any
	Outputs            map[string]any
	Trace              []byte // temis trace JSON explaining the evaluation; nil if none
}

Result is one evaluated business rule task's outcome, delivered to the optional sink a Handler is built with. The decision's outputs are written back into the instance as process variables (see Handler), and the full evaluation (inputs, outputs, trace) is retained as a durable debugging record (ADR-0066); the sink is an additional observation seam for tests and diagnostics, not the primary path.

type ServiceResolver

type ServiceResolver struct {
	// BaseURL is the model source root; the handle "risk-score" resolves to
	// <BaseURL>/risk-score.dmn.
	BaseURL string
	// Client is the HTTP client to use; nil uses a client bounded by the shared
	// worker call budget (nettimeout.Default), never an unbounded one — this
	// resolver is called from the DMN worker on the run-loop goroutine.
	Client *http.Client
	// Token, if set, is sent as an "Authorization: Bearer <Token>" header — the
	// credential for a private temis service or git host.
	Token string
}

ServiceResolver resolves a DMN reference handle against a temis model source reachable over HTTP: it GETs <BaseURL>/<modelRef>.dmn and returns the body. It is the networked alternative to DirResolver behind the same Resolver interface — a temis git host (raw file URLs) or a temis model service both fit this shape, so which source Atlas reads from is a deployment choice, not a code change (ADR-0034/0014).

A 404 is reported as ErrNotFound (an unresolved, user-fixable reference); any other non-2xx response or a transport failure is returned as an infrastructure error, so a caller can tell "no such model" from "the model source is broken". A ServiceResolver is safe for concurrent use.

func (ServiceResolver) Resolve

func (r ServiceResolver) Resolve(ctx context.Context, modelRef string) ([]byte, error)

Resolve fetches the model file for a handle from the service. A missing model (404) yields ErrNotFound; any other non-2xx status or a transport error is returned as-is so the caller reports it as an infrastructure failure, not an unresolved reference. The handle is validated exactly as DirResolver validates it, so it can never escape BaseURL's path.

type ValidationResult

type ValidationResult struct {
	Resolved  bool     // the modelRef resolved to an actual model
	Valid     bool     // the resolved model compiled without errors in temis
	ModelName string   // the DMN <definitions name>, when resolved
	Decisions []string // decision names the model exposes, when valid
	Message   string   // human-readable reason when unresolved or invalid
}

ValidationResult reports the outcome of resolving a DMN reference and checking it against temis (ADR-0034 Phase 2). It is a pure preflight result: no engine or registry state is mutated by producing it.

type Validator

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

Validator resolves DMN references and validates them against temis. It owns a temis engine used only to compile (nothing is deployed) and a Resolver for fetching model XML. Safe for concurrent use once constructed.

func NewValidator

func NewValidator(resolver Resolver) *Validator

NewValidator builds a Validator over a resolver and a fresh temis engine.

func (*Validator) Describe

func (v *Validator) Describe(ctx context.Context, modelRef string) (string, []DecisionInfo, error)

Describe resolves modelRef, compiles it, and returns its model name and the self-description of each evaluable decision (inputs + output). Like Validate it returns a non-nil error only for an infrastructure failure; an unresolved handle or an invalid model yields an empty result (a best-effort catalog entry), not an error, so one broken reference does not blank the whole picker.

func (*Validator) Graph

func (v *Validator) Graph(ctx context.Context, modelRef string) (ModelGraph, error)

Graph resolves modelRef, compiles it, and returns its decision requirements graph for a read-only viewer. Like Validate it returns a non-nil error only for an infrastructure failure; an unresolved handle or an invalid model is a normal result carrying a Message (and no nodes), so the viewer can explain the state instead of erroring.

func (*Validator) Validate

func (v *Validator) Validate(ctx context.Context, modelRef string) (ValidationResult, error)

Validate resolves modelRef and compiles it with temis, reporting whether it resolved and whether it is a valid DMN model — the check a deploy runs before trusting a reference. It returns a non-nil error ONLY for an infrastructure failure (e.g. the model source is unreadable); an unresolved handle or an invalid model is a normal, reportable result, not an error, so a caller can surface it to the user rather than as a 500.

func (*Validator) ValidateXML

func (v *Validator) ValidateXML(ctx context.Context, xml []byte) ValidationResult

ValidateXML compiles a DMN model already in hand (not resolved by a handle) and reports whether it is valid, with its name and decisions — the check the upload path runs before storing a model. Resolved is always true (the bytes are present), and no infrastructure failure is possible, so it returns no error.

Jump to

Keyboard shortcuts

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