Rule Cascade
Playbooks

Multi-level inheritance and overrides

An enterprise or organisation baseline, rulesets per business unit, application and feature that extend it, and rules targeted down to a single field - with the parent deciding what a child may change.

Two separate questions place a rule:

  • Who owns it is the scope of the ruleset it is written in: an ordered list of levels, for example enterprise, organization, business unit, application, feature. A ruleset extends exactly one parent one level up, so the levels form a chain.
  • What it is about is the rule's target: an entity, optionally narrowed to a page, screen, section, component and field. A field is a target, not a level: the most specific ruleset in the chain holds the field-level rules.

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

The example is the pair in examples/contracts: the organisation baseline acme.org.base@1.2.0 and the feature ruleset acme.payments.transfer that extends it.

Try it in your browserThe two rulesets of this playbook, side by side. Nothing to install.

Steps

Write the baseline at the highest level that owns the rule

acme-org-base.ruleset.yaml
metadata:
  id: acme.org.base
  version: 1.2.0

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

params:
  blockedCountries:
    type: stringList
    default: [KP, IR]
    overridePolicy: locked              # no child may change it
  maxTransferAmount:
    type: number
    default: 50000
    overridePolicy: tighten-only        # a child may only make it stricter ...
    tightenDirection: lower             # ... which for this parameter means lower

rules:
  - id: org.transfer.blocked-country
    target: { entity: Transfer, field: /beneficiary/country }
    enforcement: server
    overridePolicy: locked
    # ...
The parent marks itA child may
lockedChange nothing
tighten-only (the default for rules)Raise a rule's severity, require an acknowledgement, withdraw an acceptance; move a numeric parameter in the stricter direction
open (the default for parameters)Change it freely

Extend it one level down and state what changes

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 }

overrides:
  params:
    maxTransferAmount: 25000              # the organisation allows 50000; retail tightens it
  rules:
    - rule: org.transfer.memo-recommended
      set: { severity: warning }          # raised from info
      reason: Retail operations needs memos for dispute handling.

Every rule override needs a reason (without one the schema refuses the document, guideline 2.5). Pin extends[].checksum when an upgrade of the parent within the version range must be a deliberate decision.

Add the field-level rules in the most specific ruleset

rules:
  - id: transfer.swift.required-international
    kind: validation
    target: { entity: Transfer, component: beneficiary-panel, field: /beneficiary/swiftCode }
    operations: [create, update]
    when: { op: eq, args: [ { var: data.type }, international ] }
    # ...

A rule that should hold for every field of one meaning targets a data type instead (target: { type: Money }, with fields bound in entities.<Name>.fieldTypes); see the cookbook.

Check the chain

check resolves the parent from the *.ruleset.* files in the same directory and enforces every policy:

rcas check examples/contracts/*.ruleset.yaml

Loosening a tighten-only parameter (maxTransferAmount: 60000) fails the load:

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

So does touching a locked rule (set: { severity: warning } on org.transfer.blocked-country):

payments-transfer.ruleset.yaml: LOAD FAILED
  RULE_LOCKED: org.transfer.blocked-country acme.payments.transfer: rule org.transfer.blocked-country is locked by acme.org.base@1.2.0

Read the result: one flat ruleset, every finding says where it came from

The compiler resolves the chain into one flat ruleset; a bundle has no hierarchy left, so depth costs nothing per request. A finding's source names the level that defined the rule, and its message uses the effective parameter:

{ "rule": "org.transfer.amount-limit", "code": "ORG-TRF-002",
  "message": "Amount exceeds the single-transfer limit of 25000.",
  "source": "acme.org.base@1.2.0" }

The limit reads 25000, not the organisation's 50000, because retail tightened it one level down.

Release parents with care

A new parent version inside the child's range (^1.2.0) changes every child's checksum at its next compile. Run check on every child in the parent's pull request, and follow ship a rule change for each.

Done when each level below the top has its own ruleset that extends exactly one parent, every rule override has a reason, check passes for the whole chain, and an attempt to loosen a tighten-only parameter or change a locked rule fails with PARAM_LOOSENED or RULE_LOCKED.

The design is ADR 0004; the normative rules are in specification section 5 and guideline section 2.

On this page