Appearance
Concepts
Everything in Opaline is built from a few nouns. This page defines them once so the rest of the docs, and the decision JSON, read cleanly.
Pack
A pack is a versioned, immutable bundle of rules plus a decision configuration. A pack is identified by id@version (e.g. acme.bill.validity@1.2.0). Publishing is the only way a pack comes into existence, and a published version never changes. Re-publishing the same id@version with different content is rejected. To change a pack you publish a new version.
An alias (e.g. stable, prod) is a movable pointer to one version. Clients decide against an alias and you move the alias when a new version is ready; the alias is resolved per request and the resolved version is recorded in the trail.
Nodes
A pack's logic is a list of nodes. There are three kinds:
- rule: a single boolean expression over the facts.
- table: a decision table, with rows of input conditions mapping to an outcome.
- switch: branches that route evaluation down one path.
Every node declares the inputs it reads, a severity, and (for hard rules) a citation (the authority the rule enforces).
Facts and context
A decision evaluates a pack against two inputs:
- facts: the data under evaluation (
{ "vat_rate": "0.1", "total": "1250.00" }). - context: the evaluation environment, not the data itself. Most importantly
transaction_date(which regime-dated rules resolve against) and an optionalactor(required when a decision carries human overrides).
Numbers in both are decimal strings ("0.1", never 0.1). See Determinism.
Decision
A decision is what decide returns:
json
{
"status": "PASS",
"score": "1",
"auto_clearable": true,
"rules": [
{ "rule_id": "vat_rate_check", "status": "PASS",
"confidence": "1", "input_confidence": "1",
"weight": "1", "severity": "soft", "outcome": "1" }
]
}- status: the overall verdict (see below).
- score: the aggregated numeric result, a decimal string.
- auto_clearable: whether the decision is safe to action without review.
- rules: the per-rule outcomes that produced the verdict. Each has its own status:
PASS,FAIL, orINCONCLUSIVE.
The three statuses
The overall decision status is always one of three values:
| Status | Meaning |
|---|---|
PASS | The score met the pass threshold and no hard rule failed or was inconclusive. |
WARN | The score is between the warn and pass thresholds, or a hard rule failed or was inconclusive, capping a would-be PASS. |
FAIL | The score fell below the warn threshold, or a rule the pack marks as fatal failed. |
Inconclusive rules
There is no overall INCONCLUSIVE status. When a required input is missing or a rule can't be evaluated, that rule is INCONCLUSIVE (in rules[].status) and the missing inputs are listed in the decision's manifests, never a silent skip. A rule, cell, or field that errors during evaluation is treated the same way, never a silent non-match. If an inconclusive rule is hard, the decision is capped at WARN and auto_clearable is false. The response is still a normal 200.
Confidence
Every rule outcome carries two confidence values, both decimal strings in [0, 1]:
- input_confidence: the confidence of the inputs the rule read, taken as the minimum over its declared inputs. Low-confidence input flows through to the outcome rather than being silently trusted.
- confidence: the rule's confidence in its own outcome.
Confidence propagates through aggregation, so the decision reflects how much it could trust what it was given.
Severity and weight
- severity is
softorhard. A hard rule that fails (or is inconclusive) prevents the decision from passing: a would-bePASSis capped toWARN. A hard rule must declare a citation, or the pack fails lint. - weight is how much a rule contributes to the aggregated score.
- A pack can additionally mark specific rules as fatal (
decision.hard_fail): if a fatal rule fails, the whole decision is forced toFAIL, regardless of the score.
Aggregation and thresholds
The pack's decision block says how per-rule outcomes combine into the score (e.g. weighted_mean) and where the cut-offs are:
json
"decision": { "aggregation": "weighted_mean", "thresholds": { "pass": "0.9", "warn": "0.7" } }Score ≥ pass → PASS; ≥ warn → WARN; below → FAIL. Hard rules then adjust the result, as described under Severity and weight.
Regime-dating
Rules can be effective only within a date range. They resolve against context.transaction_date (a civil date), so a decision made today can be evaluated under the rules that applied on the transaction's date. A regime-dated rule with no transaction_date yields INCONCLUSIVE: the engine will not guess which regime applied.
Determinism
The same pack version, the same inputs, and the same engine version always produce a byte-identical decision. All arithmetic is decimal; the evaluation clock and timezone are explicit inputs, never ambient. This is why numbers cross the wire as strings: a JSON float can't represent 0.1 exactly, so floats in hash-input positions are rejected at the boundary.
The trail
Every live decision is recorded in a per-tenant, append-only SHA-256 hash chain before the response is returned (trail-before-response). The chain is signed into periodic checkpoints and anchored externally, so a decision can be verified independently, by you, not just by the server that made it. See Trail & verification.
Namespaces
Each tenant has two isolated namespaces, selected by the key you use:
- live (
opl_live_): production decisions; metered; written to the live trail. - test (
opl_test_): experimentation; unmetered; a separate trail namespace.
Next
- Getting started: from a key to a decision.
- Pack authoring: write, lint, publish, and simulate packs.
- Trail & verification: how decisions are recorded and proven.