Skip to content

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 optional actor (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, or INCONCLUSIVE.

The three statuses

The overall decision status is always one of three values:

StatusMeaning
PASSThe score met the pass threshold and no hard rule failed or was inconclusive.
WARNThe score is between the warn and pass thresholds, or a hard rule failed or was inconclusive, capping a would-be PASS.
FAILThe 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 soft or hard. A hard rule that fails (or is inconclusive) prevents the decision from passing: a would-be PASS is capped to WARN. 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 to FAIL, 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 ≥ passPASS; ≥ warnWARN; 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