Author and ship a rule change
From an edited ruleset to production - pull request, CI checks and compile, version bump, immutable bundle, rollout and rollback.
The running example makes the transfer memo mandatory in retail: the organisation rule
org.transfer.memo-recommended is raised from warning to error in
examples/contracts/payments-transfer.ruleset.yaml. Substitute your own ruleset and rule.
Before you start
- The command
rule-cascade(build it as in the quickstart), or Python 3.10 withpip install -r tools/requirements.txtandpython tools/rulecheck.pyin its place. - Write access to the repository that holds your rulesets, and CI that runs on pull requests.
Steps
Branch from main
git checkout -b feat/transfer-memo-required origin/mainBranch names are <type>/<short-description>; commits follow Conventional Commits
(contributing).
Change the rule
The parent marks the memo rule tighten-only, so the feature ruleset may raise its severity in
overrides:
overrides:
rules:
- rule: org.transfer.memo-recommended
set: { severity: error }
reason: Retail operations needs a memo on every transfer for dispute handling.For a new rule, give it an id, a finding code and a message key in the style of the
naming conventions, a message under messages.<locale>, and an
enforcement. Anything a user must not read is server
(guideline section 8).
Raise the version
Choose the bump by what callers can observe (guideline 1.5):
| Change | Version |
|---|---|
Adds an info rule, a message or a test | patch |
| Adds a warning without acknowledgement, a state or compute rule, or a parameter | minor |
| Adds an error rule or a warning that must be acknowledged, raises a severity, tightens a parameter, removes or renames anything | major |
Raising a severity is major: metadata.version: 2.0.0.
Add golden tests
One test that the change triggers, under tests::
- name: a transfer without a memo is denied
entity: Transfer
operation: create
given:
data: { id: t-20, type: domestic, amount: 50, currency: USD,
beneficiary: { name: Jo Lee, country: US } }
expect:
decision: deny
findings:
- { rule: org.transfer.memo-recommended, blocking: true }Check, and fix what it reports
rule-cascade check examples/contracts/*.ruleset.yamlWith only the steps above done, check refuses the change twice, and both are right:
acme.payments.transfer@2.0.0 sha256:b1faf3a8db6c... 14 rules (9 client-safe), 4 params
BINDING_MISMATCH: ./payments.openapi.yaml wants version ^1.0.0
FAIL valid domestic transfer is allowed, memo missing is a non-blocking warning
decision "deny" != "allow"
no finding matches {"rule":"org.transfer.memo-recommended","blocking":false}
15 golden tests, 1 failed- The API description binds the ruleset with
x-rule-cascade: { version: ^1.0.0 }. A major version is a contract change for the API too: update the binding to^2.0.0inpayments.openapi.yaml. - An existing golden test asserted the old behaviour. Update it (add a
memo, expect no findings) rather than deleting it.
Run check again until every ruleset ends with 0 failed and the exit status is 0:
acme.payments.transfer@2.0.0 sha256:b1faf3a8db6c... 14 rules (9 client-safe), 4 params
15 golden tests, 0 failedOther problems check reports: LOAD FAILED with a code from
specification section 5, YAML_NOT_PORTABLE (quote no,
012, 2026-10-03), NUMBER_NOT_PORTABLE. Check every ruleset that extends the one you changed:
their checksums change with it.
Open the pull request; CI checks and compiles
CI runs check on every ruleset, fails on a non-zero status, compiles the bundle and keeps it as an
artefact:
- name: Check the rulesets and run their golden tests
run: rule-cascade check rules/*.ruleset.yaml
- name: Compile
run: rule-cascade compile rules/payments-transfer.ruleset.yaml -o build/acme.payments.transfer.bundle.json
- uses: actions/upload-artifact@v4
with:
name: rule-bundles
path: build/*.bundle.jsonThis repository's own job is contract in
.github/workflows/ci.yml.
.github/CODEOWNERS routes rulesets to their owners for review.
Store the bundle immutably
After the merge, store the bundle CI compiled under its id, version and checksum, and never overwrite one. The checksum identifies the resolved rules, parents included.
Roll out
| Where the rules run | How the new bundle arrives |
|---|---|
| A service that embeds a runtime | A rolling deployment with the new bundle. A broken bundle stops start-up, so the rollout stops at the first instance |
| The rule server | A new ConfigMap and a rolling restart (Kubernetes playbook), or SIGHUP to reload; a failed reload keeps the previous rules |
| Browsers | Nothing to deploy: the next manifest fetch revalidates the ETag and receives the new client manifest |
A browser can hold an older manifest than the server enforces for a while. That is safe: the server decides, and its responses name the new checksum so the UI refetches (enforcement guide step 8).
Verify, or roll back
Every decision log line carries the ruleset id, version and checksum. Confirm the new checksum appears in production. To roll back, deploy the previous bundle from the store.
Done when check exits 0 in CI, the deployed bundle is the one CI compiled, and production
decision logs show acme.payments.transfer 2.0.0 with its new checksum, with no change to UI or
API code.
Playbooks
Task-oriented procedures. Numbered steps, commands to copy, and a check that tells you when you are done.
Add rule enforcement to a React form
Fetch the client manifest, evaluate on every change with useRuleEvaluation, draw field state, computed values and findings, and send resolutions with the submit.