Rule Cascade
Playbooks

Rules from existing code

An engineering playbook for moving business rules out of application code into rulesets, safely, in any language, with or without an AI agent.

Outcome: the decisions your services and front ends make (what is valid, what needs approval, what is computed, what happens next) live in reviewed, tested rulesets, evaluated identically in every language, and the copies in code are gone.

Prerequisites: rcas installed, and the project initialized from its code (rcas init --from .).

1. Find the decisions

Start from the inventory (rcas analyze, or .rcas/analysis.md). For each hit, read enough code to write one sentence: on entity, during operation, field or relation, what is allowed, what happens otherwise. Then sort it:

What you foundBelongs inWhy
A constraint the API schema states (required, enum, length, pattern, range)the derived ruleset (rcas derive)Generated from the schema, so it never drifts from it
A rule across fields, on a state transition, against a limit, or with a warning or an approvala hand-written rulesetThis is what rulesets are for
A computed value (a fee, a due date, a risk score from facts)a compute ruleSame answer in the form and the API
A follow-up (an event, a notification) that depends on the decisionan action rule with commandsThe host executes; the rule decides
Shape and transport: types, formats, sizes, authentication, rate limitsstays in the API layerRules assume valid shapes; see enforcement guide step 7
A check of the program itself (a file exists, a connection is open)stays in codeIt is not a business decision

The rcas-analyst agent does this sorting and cites file:line for every decision.

2. Write each rule to be reviewed

These are the rules a reviewer checks (authoring guidelines has the full list with the codes check reports):

  • One rule, one concern. One condition on one field or one relation, one finding code. "Positive and below the limit" is two rules.
  • Names are permanent. Rule ids <entity>.<field or aspect>.<constraint> (transfer.amount.positive), finding codes <DOMAIN>-<GROUP>-<NNN> (PAY-TRF-002). Never reuse or repurpose one; retire it and add a new one (naming conventions).
  • Guard what may be missing. A rule that reads an optional field has a when that checks it exists. Otherwise a missing value is an evaluation error, not a finding the user can act on.
  • Limits are parameters. Thresholds, lists and amounts are params with an override policy: locked for law and integrity, tighten-only for limits, open for defaults. A business unit can then tighten a limit in a child ruleset without copying the rule.
  • Severity is a decision. error blocks, warning warns and can require an acknowledgement, info informs. Risk acceptance names the roles that may accept; integrity and legal rules are never acceptable.
  • The server decides. Every rule is enforced on the server. Mark a rule enforcement: both only when everything it reads may be public: the client manifest is readable by anyone.
  • Messages are keys, with arguments, translated per locale in messages. No text in code.
  • Portable expressions. Operators from the specification, patterns from the portable subset (anchored, character classes, bounded counts).
  • Version like an API (guideline 1.5): adding an error rule, raising a severity, tightening a parameter, or removing or renaming anything is major; a warning without acknowledgement or a compute rule is minor; an info rule or a reworded message is a patch.

3. Test every rule

Golden tests are the specification of the rule; reviewers read them first. For each rule:

CaseExample
passes3 items: allow, no finding
fails11 items: deny, the finding, its field and message
boundaryexactly 10 items: allow
missing optional datano quantity: the guard skips the rule
client channelwhen the rule runs in the browser, the same case with channel: client

rcas check runs them in CI. A rule without tests is reported by the MCP tools, and the reviewer should not accept it.

4. Propose, review, accept

Changes reach the rules through review, never directly:

rcas derive api/openapi.yaml --schema Transfer --id acme.payments.transfer-generated --propose
rcas proposals show derive-acme-payments-transfer-generated --diff
rcas proposals accept derive-acme-payments-transfer-generated
rcas check && git add rules/ && git commit

An agent submits with propose_ruleset; the proposal records its rationale and the code it came from. The reviewer uses rcas-reviewer, or the checklist in authoring guidelines section 12.

5. Replace the code, one rule at a time

  1. Shadow. Evaluate the ruleset next to the existing check and log disagreements. Change no behaviour.
  2. Parity test. A test in the project's own framework feeds the same inputs to the old code path and to the engine, and compares decisions. Keep it until step 4.
  3. Switch. Enforce the engine's decision on the backend; show its findings in the form.
  4. Delete the old check and the parity test. One source of truth remains.

Do this per rule or per entity, not for the whole system at once. A rollback is a revert of the ruleset (or of the bundle version a service loads); the code check is still there until step 4.

6. Keep it language-agnostic

  • Rules are data. Nothing in a ruleset depends on the language, framework or operating system of a service: the same bundle is evaluated by TypeScript, Python, Java and Go, by the command and by WebAssembly, identically (the conformance suite proves it on Linux, macOS and Windows).
  • Compile once in CI (rcas compile --all), ship bundles as build artefacts, load them at start-up.
  • A language without a runtime uses rcas engine or rcas.wasm: one child process or one module, no port needed.
  • Front ends get the client manifest, never the bundle.

Done when

  • every decision in the inventory is either a rule, or noted as staying in code with a reason;
  • rcas check passes with golden tests for every rule, and CI runs it;
  • each replaced check in code is deleted, with its parity test.

On this page