Skip to content

Pack authoring

A pack is a versioned, immutable bundle of rules. The live JSON Schema is served at GET /v1/schemas/pack (use it for editor and agent autocomplete).

Anatomy

json
{
  "format": 1,
  "meta": { "id": "acme.bill.validity", "version": "1.2.0" },
  "nodes": [
    {
      "id": "vat_rate_check",
      "kind": "rule",
      "expr": "number(facts.vat_rate) == 0.1",
      "meta": { "severity": "hard", "citation": "REG-001 s.4", "inputs": ["vat_rate"] }
    }
  ],
  "decision": { "aggregation": "weighted_mean", "thresholds": { "pass": "0.9", "warn": "0.7" } }
}
  • meta.id grammar: ^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$; version is semver.
  • nodes: rule, table, and switch nodes. Each declares its inputs and a severity (soft / hard). Hard rules also declare a citation.
  • decision: the aggregation and thresholds as decimal strings.
  • tests (recommended): embedded vectors that gate publish.

Rules that matter

  • All comparison and aggregation is decimal. Numbers in hash-input positions serialize as strings ("0.9", not 0.9).
  • A hard rule without a citation fails lint.
  • Regime-dated rules resolve against context.transaction_date; a missing date yields INCONCLUSIVE, never a silent skip.
  • A rule/cell/field evaluation error is a hard failure (INCONCLUSIVE with a manifest), never a silent non-match.

Author in JSON or YAML

Authoring syntax doesn't affect the published artifact. YAML and JSON of the same logical pack produce a byte-identical content hash, because the hash is over the canonical form, not the bytes you typed. Use whichever you prefer.

Lint locally

opaline-cli lint runs the exact publish gate the server runs (lint, AOT compile, embedded tests) and prints the content hash:

bash
opaline-cli lint ./my-pack.yaml
# pack:         acme.bill.validity@1.2.0
# content_hash: 9f86d081…
# lint:         OK (publishable)

If it says OK (publishable), the document will publish.

Publish & iterate

Publish is immutable and gated (see Getting started). Before moving an alias, use simulate to see what changes:

bash
curl -X POST "https://api.opaline.dev/v1/simulate" \
  -H "Authorization: Bearer opl_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "pack": "acme.bill.validity",
    "versions": ["1.2.0", "1.3.0"],
    "over": { "inputs": [{ "facts": { "vat_rate": "0.1" }, "context": {} }] }
  }'

Simulate never meters and never writes the trail. It's the authoring loop for answering "what flips between v1.2 and v1.3?"