Rule Cascade
Examples

Level cascade

An organisation baseline and a retail feature ruleset that extends it, tightens a limit and raises a severity - and what the result looks like.

Files: examples/contracts/acme-org-base.ruleset.yaml (acme.org.base@1.2.0) and examples/contracts/payments-transfer.ruleset.yaml (acme.payments.transfer@1.0.0). Every listing below is read from them when the site is built.

Try it in your browserBoth rulesets in tabs: the feature tightens the organisation limit. Nothing to install.

Rules cascade down the levels; the organization locks its blocked-country list and the business unit may only tighten the transfer limit

Diagram, described in Mermaid: flowchart TB O["acme.org.base@1.2.0 (organization)<br/>blockedCountries [KP, IR]: locked<br/>maxTransferAmount 50000: tighten-only, lower"] F["acme.payments.transfer@1.0.0 (feature)<br/>extends acme.org.base ^1.2.0<br/>maxTransferAmount 25000<br/>memo-recommended: severity warning"] O -- extends --> F F --> M["Compiled manifest: one rule set, each rule's source names the level that defined it"]

The organisation sets a baseline, with policies on what may change

blockedCountries is locked: no level below may change it. maxTransferAmount is tighten-only in the lower direction: a level below may lower it, never raise it.

examples/contracts/acme-org-base.ruleset.yaml
metadata:
  id: acme.org.base
  version: 1.2.0
  title: Acme organisation-wide money movement rules
  owner: enterprise-risk
  status: active

scope:
  - { level: enterprise, id: acme-group }
  - { level: organization, id: acme }

entities:
  Transfer:
    description: Any outbound movement of money.
    schema: { $ref: "./payments.openapi.yaml#/components/schemas/Transfer" }

params:
  blockedCountries:
    type: stringList
    default: [KP, IR]
    description: ISO 3166-1 alpha-2 codes no transfer may be sent to.
    overridePolicy: locked
  maxTransferAmount:
    type: number
    default: 50000
    description: Largest single transfer without a risk acceptance.
    overridePolicy: tighten-only
    tightenDirection: lower

The feature extends it and tightens it

The retail feature names its place in the organisation, extends the baseline with a version range, lowers the limit to 25000 and raises the memo rule's severity:

examples/contracts/payments-transfer.ruleset.yaml
scope:
  - { level: enterprise, id: acme-group }
  - { level: organization, id: acme }
  - { level: businessUnit, id: retail-banking }
  - { level: application, id: payments-hub }
  - { level: feature, id: transfers }

extends:
  - { ruleset: acme.org.base, version: ^1.2.0 }
examples/contracts/payments-transfer.ruleset.yaml
overrides:
  params:
    maxTransferAmount: 25000        # retail tightens the org limit of 50000
  rules:
    - rule: org.transfer.memo-recommended
      set: { severity: warning }
      reason: Retail operations needs memos for dispute handling.
rcas check examples/contracts/*.ruleset.yaml
acme.org.base@1.2.0  sha256:4dff42ddd5da...  3 rules (2 client-safe), 2 params
  3 golden tests, 0 failed
acme.payments.transfer@1.0.0  sha256:c192dd53b5b1...  14 rules (9 client-safe), 4 params
  14 golden tests, 0 failed

Set the feature's maxTransferAmount to 60000 instead, and the policy refuses it:

payments-transfer.ruleset.yaml: LOAD FAILED
  PARAM_LOOSENED: acme.payments.transfer: param maxTransferAmount may only move lower

Done when both rulesets load with 0 failed golden tests, and loosening the limit fails to load with PARAM_LOOSENED and exit status 1.

See the cascade in a result

A teller sends 30,000. The limit applied is the tightened 25000, and source names the level that defined the rule:

echo '{"entity":"Transfer","operation":"create",
       "data":{"id":"t-5","type":"domestic","amount":30000,"currency":"USD","memo":"house",
               "beneficiary":{"name":"Sam","country":"US"}},
       "actor":{"id":"u-1","roles":["teller"]},
       "resolutions":[{"rule":"transfer.large.review-warning","type":"acknowledge"}]}' |
  rcas evaluate --bundle conformance/bundles/acme.payments.transfer.bundle.json - |
  jq '{decision, finding: (.findings[] | select(.rule == "org.transfer.amount-limit"))}'
{
  "decision": "deny",
  "finding": {
    "rule": "org.transfer.amount-limit",
    "code": "ORG-TRF-002",
    "severity": "error",
    "message": "Amount exceeds the single-transfer limit of 25000.",
    "fields": [
      "/amount"
    ],
    "blocking": true,
    "status": "open",
    "resolution": "accept-risk",
    "source": "acme.org.base@1.2.0",
    "location": {
      "component": "amount-panel"
    },
    "acceptableBy": [
      "risk-officer"
    ]
  }
}

Done when the message says 25000 (the feature's value, not the organisation's 50000) and source is acme.org.base@1.2.0. The golden tests of the feature ruleset pin this down: "over the tightened retail limit is denied for an ordinary user" and "a risk officer can accept the limit breach with a justification".

What the policies refuse, and how to build your own chain: Multi-level inheritance and overrides.

On this page