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
scopeof the ruleset it is written in: an ordered list of levels, for example enterprise, organization, business unit, application, feature. A rulesetextendsexactly 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.
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.
Steps
Write the baseline at the highest level that owns the rule
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 it | A child may |
|---|---|
locked | Change 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
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:
rule-cascade check examples/contracts/*.ruleset.yamlLoosening 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 lowerSo 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.0Read 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.
Source: site/content/docs/playbooks/multi-level-inheritance.mdx
AI agent tools
Give an LLM agent list_rules, evaluate_rules and explain_rule so it checks before it acts - without letting it decide, name its own roles, or accept its own risks.
Deploy the rule server on Kubernetes
Build the image, load the rules into a ConfigMap, create the token Secret, apply the manifests, verify, publish new rules and rotate the token.