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 ¶
- Variables
- func BuildInputs(store state.Reader, elementKey, piKey uint64, staticJSON string, ...) (map[string]any, error)
- func DecisionHandler(store state.Reader, lookup ProcessLookup, bind Bind, sink func(Result)) job.CompletingHandler
- func Handler(store state.Reader, lookup ProcessLookup, reg *Registry, sink func(Result)) job.CompletingHandler
- func JSONObject(m map[string]any) string
- func OutputVariable(name string, outputs map[string]any) model.VariableValue
- type Bind
- type DecisionField
- type DecisionInfo
- type DeployedDecision
- type DirResolver
- type Evaluation
- type Evaluator
- type GraphEdge
- type GraphNode
- type ModelGraph
- type ProcessLookup
- type Registry
- func (r *Registry) Deploy(defKey uint64, dmnXML []byte) error
- func (r *Registry) DeployedDecisions() []DeployedDecision
- func (r *Registry) Evaluate(ctx context.Context, defKey uint64, decisionId string, in map[string]any) (map[string]any, error)
- func (r *Registry) EvaluateLatest(ctx context.Context, decisionId string, in map[string]any) (map[string]any, error)
- func (r *Registry) EvaluateLatestTraced(ctx context.Context, decisionId string, in map[string]any) (map[string]any, []byte, error)
- func (r *Registry) EvaluateTraced(ctx context.Context, defKey uint64, decisionId string, in map[string]any) (map[string]any, []byte, error)
- func (r *Registry) Reload(defKey uint64, dmnXML []byte) (string, error)
- type Resolver
- type Result
- type ServiceResolver
- type ValidationResult
- type Validator
- func (v *Validator) Describe(ctx context.Context, modelRef string) (string, []DecisionInfo, error)
- func (v *Validator) Graph(ctx context.Context, modelRef string) (ModelGraph, error)
- func (v *Validator) Validate(ctx context.Context, modelRef string) (ValidationResult, error)
- func (v *Validator) ValidateXML(ctx context.Context, xml []byte) ValidationResult
Constants ¶
This section is empty.
Variables ¶
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
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 ¶
type Bind func(cp *compiler.CompiledProcess, detail *compiler.BusinessRuleTaskDetail) (Evaluator, error)
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 ¶
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.
type Evaluation ¶
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 ¶
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
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 ¶
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 ¶
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 ¶
NewValidator builds a Validator over a resolver and a fresh temis engine.
func (*Validator) Describe ¶
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 ¶
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 ¶
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.