Rule Cascade
Reference

Contributing

The conformance suite is the specification. A change to behaviour changes the specification, the reference implementation, the conformance cases and every runtime, in one pull request.

The one rule

The conformance suite is the specification. A change to behaviour changes the specification, the reference implementation, the conformance cases and every runtime, in one pull request. Where the prose and the suite disagree, the suite wins and the prose is a bug.

Prerequisites

ForYou need
Rulesets, specifications, reference implementation, toolsPython 3.10+, pip install -r tools/requirements.txt
TypeScript runtime, rule server, React, Node.js and agent-tool examplesNode.js 20+, npm ci at the repository root
Java runtimeJDK 17+; Maven 3.9+ only for the packaged build and the Spring Boot example
Go runtime, the rule-cascade command, the WebAssembly module, the Go exampleGo 1.22+. CI runs 1.22 with GOTOOLCHAIN=local to prove the minimum, and 1.24
Engine client examples (optional)Whichever of Ruby, PHP, Rust, jq, the .NET SDK and PowerShell you have; pip install wasmtime for the Python WebAssembly client. Missing ones are skipped

Verifying a change

make verify

runs what CI runs, except the Maven builds, the React example build, the Docker image and the other operating systems. It needs all four toolchains. Each part is a target of its own:

TargetWhat it does
make contractrulecheck check on every ruleset in examples/ and conformance/sources; tools/lint_specs.py (schema, OpenAPI descriptions, operator list in bindings/, generated files current); the unit tests of the tools
make conformanceThe conformance suite on the reference implementation
make pythonThe tests of packages/python: the suite through the engine protocol, and generated files current
make typescriptnpm ci, build, type check, the suite in process, the tests of the runtime and of the rule server
make javaCompiles with javac, all warnings as errors, and runs the suite in process. No Maven, no network
make gogo vet, gofmt, the suite in process, and the native command in packages/go/dist
make go-releaseThe command for Linux, macOS and Windows on amd64 and arm64, the WebAssembly module, and SHA256SUMS
make enginesThe suite over the engine protocol against the TypeScript, Java, Go, WebAssembly and Python engines, with the one driver. Needs typescript, java, go and go-release first
make engine-clientsRuns every example in examples/engine-clients whose language is installed and compares its output

Not part of verify:

TargetWhat it does
make java-mavenmvn verify for the Java runtime, as CI builds it
make example-frontendBuilds the React example
make syncRegenerates everything derived from the rulesets and the schema (see below)
make cleanRemoves build output

The examples in examples/backend-node, examples/backend-go, examples/batch and examples/agent-tools have their own tests, which CI runs in one job each:

npm run build && npm run test:examples                    # Node.js service and agent tools
(cd examples/backend-go && go test ./...)                  # Go service
python -m unittest discover -s examples/batch -v           # batch recipe, on every engine that is built
(cd packages/go && GOTOOLCHAIN=go1.22.12 go test ./...)    # the Go runtime on its declared minimum

A passing suite ends with conformance: 1899 cases, 0 failure(s), for every engine.

Changing the specification

Work in this order. Each step has something to run before the next.

  1. Specification and schema. Edit spec/v1/SPECIFICATION.md. For a change to the shape of a ruleset, edit spec/v1/rule-cascade.schema.json, and only that copy. For a change to the wire API, edit spec/v1/rule-evaluation.openapi.yaml.
  2. Reference implementation. Implement it in packages/python/src/rule_cascade: expressions in expressions.py, loading in ruleset.py, evaluation in evaluate.py, the protocol in engine.py.
  3. Conformance cases. Add hand-written cases that pin the behaviour down, including the errors: conformance/expressions.json, load-errors.json, protocol.json. For behaviour that needs a whole ruleset, add golden tests to an example ruleset or to a ruleset in conformance/sources.
  4. Regenerate. python tools/rulecheck.py sync copies the schema into the four packages and regenerates fixtures, bundles, expectations and the evaluation corpus. Read the diff: an unexpected change there is a behaviour change. Then make contract conformance python.
  5. Every runtime. Port the change to packages/typescript, packages/java and packages/go and run each suite in process: make typescript java go.
  6. The driver. make go-release engines certifies every engine, the WebAssembly module included, over the engine protocol.
  7. Everything that describes it. CHANGELOG.md; the package READMEs; docs/; for a new operator also the op enum in bindings/rule-draft.response-format.json, which tools/lint_specs.py compares with the schema. A significant design choice gets an ADR in docs/adr/.

Common changes:

You want toDo this
Change an example rulesetEdit the YAML under examples/contracts or examples/catalog, then make sync
Add an operatorIt belongs to a new profile: add it to the schema enum and to specification 4.3, implement it, add expression cases. Then steps 4 to 7
Add a load-time checkA new code in specification section 5, cases in conformance/load-errors.json, the check in every compiler (LoadErrorCode.java in Java)
Change the schemaEdit spec/v1/rule-cascade.schema.json, then make sync. Every runtime reads its own copy; the Java validator fails at load if the schema starts to use a keyword it does not implement
Fix a bug in one runtimeAdd the failing case to conformance/ first. If the reference is wrong too, fix it and run make sync
Change the engine protocolSpecification section 13, conformance/protocol.json, engine.py, then the engine of every runtime

Adding a runtime in a new language

Start at the evaluator level (specification section 11). It needs a JSON parser, decimal arithmetic with 34 significant digits and a regular-expression engine, and no YAML and no JSON Schema.

  1. Expressions (section 4): literals, var, the 45 operators, function calls, custom operators, and the scanner for portable patterns (4.4). conformance/expressions.json has 499 cases.

  2. Bundles and evaluation (sections 7 and 8): read a bundle, select rules, run the phases, produce findings, effects and commands. Check the shape of a request before evaluating.

  3. The engine protocol (section 13): a program that reads one JSON request per line and writes one response per line, with version, load, manifest, evaluate and expression. Answer compile with UNSUPPORTED and report the level evaluator. Register the three conformance operators of conformance/README.md behind an option.

  4. Certify it with the driver, and fix what it reports:

    PYTHONPATH=packages/python/src python tools/rulecheck.py conformance \
      --engine "<your command> engine --conformance-operators"

    The driver skips the compiler-level cases for an engine that does not report the level compiler. An evaluator runs 1683 cases. Four of them are compile cases in conformance/protocol.json that the driver does not skip yet; an engine that answers compile with UNSUPPORTED fails exactly those four until it reaches the compiler level.

  5. Make it part of the repository: a package under packages/<language> with a README, a test that runs the suite in process, a Makefile target, a line in make engines, a CI job on the three operating systems, and an entry in docs/languages.md.

The compiler level comes second: schema validation, inheritance, the load-time checks, the checksum and the manifests (sections 5 to 7), certified by the load-error cases and by producing exactly the published bundles. A language that only needs to enforce rules never needs it: bundles are compiled in CI by any existing compiler.

A language that needs neither a library nor a port uses the rule-cascade command or the WebAssembly module; see examples/engine-clients.

Generated files

Do not edit these by hand. python tools/lint_specs.py and python tools/rulecheck.py sync --check fail when one is stale, and .gitattributes marks them as generated so that diffs collapse them. Review their diffs all the same.

FileGenerated fromBy
packages/python/src/rule_cascade/rule-cascade.schema.json, packages/typescript/src/rule-cascade.schema.json, packages/java/src/main/resources/.../rule-cascade.schema.json, packages/go/rule-cascade.schema.jsonspec/v1/rule-cascade.schema.jsonrulecheck sync
conformance/fixtures/*.jsonThe YAML files in examples/contracts, examples/catalog and conformance/sourcesrulecheck sync
conformance/bundles/*.bundle.jsonThe fixtures listed under documents in conformance/rulesets.json, compiled by the referencerulecheck sync
expect and bundles in conformance/rulesets.jsonThe same. documents and schemaDocuments are maintained by handrulecheck sync
conformance/evaluations.jsonThe seeded request generators in tools/rulecheck.py and the reference's answersrulecheck sync
examples/frontend-react/src/manifest.client.jsonexamples/contracts/payments-transfer.ruleset.yamlrulecheck sync
examples/derived/*.ruleset.yamlThe OpenAPI descriptions of the examplesrulecheck derive; the commands are in docs/openapi.md, and a unit test checks they are current
tools/tests/fixtures/jsonlogic_expected.jsonWhat json-logic-js 2.0.5 returns for the converter's test casesThe steps at the top of tools/tests/fixtures/jsonlogic_expected.mjs

make sync is python tools/rulecheck.py sync. Build output (dist/, target/) is not committed.

Pull requests

  • Branch: <type>/<short-description>, for example feat/date-operators.
  • Commits: Conventional Commits. The types in use are feat, fix, refactor, chore, ci and docs. The scope is the area: spec, conformance, python, typescript, java, go, server, tools, examples, docs. Mark a breaking change with !, as in feat(spec)!: ....
  • A change that spans runtimes is one pull request with one commit per area: the specification, reference and conformance cases first, then each runtime.
  • One logical change per pull request. CI must be green.
  • Anything a user can observe goes in CHANGELOG.md, in the section of the version in progress.
  • Significant design choices get an ADR in docs/adr/.
  • Use the checklist in .github/pull_request_template.md.

Names for rulesets, rules, codes, files and packages are in docs/naming-conventions.md. How to write rulesets is in docs/authoring-guidelines.md.

Compatibility

AreaPromise
SpecificationMinor versions only add. Existing documents stay valid and keep their meaning
OperatorsThe meaning of an existing operator never changes; new ones arrive in a new profile
Bundle formatruleCascadeBundle is versioned like the specification: a runtime rejects a major version it does not implement
Load error codes, protocol error codes and finding fieldsStable identifiers; never renamed within a major version
Runtime APIsSemantic Versioning per package

While the specification is a draft (1.0.0-alpha.N), these promises describe the intent for 1.0; alpha versions may break, and the changelog says where.

Rendered from CONTRIBUTING.md in the repository. Edit it there.

On this page