Rule Cascade
ReferenceDecision records (ADRs)

ADR 0010: A ruleset that needs a newer runtime fails at load, not on each request

Status: accepted

Status: accepted

Context

New operators arrive in a new minor version of the specification (section 10). A runtime used to check only the major version of a document and the format version of a bundle, and an evaluator trusts a bundle without running the load checks again. So a 1.0 runtime that received a bundle using a 1.1 operator loaded it, and then failed closed with RULE-EVALUATION-ERROR on every request that reached the rule. The first sign of the wrong deployment order was blocked users.

The first operators that needed a new version were isDate and isDateTime. Without them a rule cannot tell a date that exists (2024-02-29) from one that does not (2023-02-29), so a well-formed but impossible date reached daysBetween or yearsBetween and failed closed instead of showing the user a message.

Decision

  • The version a ruleset needs travels with it. A document that uses an operator added in 1.1 declares ruleCascade: 1.1.0; a ruleset whose resolved version is 1.0 and that uses one does not load (OPERATOR_NOT_IN_VERSION). The resolved ruleset takes its parent's ruleCascade when the parent's minor version is later, so a 1.0 child of a 1.1 parent is a 1.1 ruleset. Patch versions never change the resolved value, so the checksums of existing chains stay the same. Every manifest carries the resolved value.
  • A runtime refuses what it does not implement. A document of a later minor version, and a bundle's manifest or a single manifest whose ruleCascade has another major version, a later minor version, or is not a version at all, are refused with VERSION_UNSUPPORTED. A document of another major version, or whose ruleCascade is not a version, already fails the schema (SCHEMA_INVALID). An absent ruleCascade in a manifest is 1.0.0, so existing bundles keep loading.
  • The bundle format does not change. ruleCascadeBundle stays 1.0.0: the version of the rules and the version of the container are different questions.
  • isDate and isDateTime are built on each runtime's existing date parser: they are true exactly when the date operators would accept the value, and never an evaluation error.

Consequences

  • Deploying a bundle to a runtime that is too old fails when the bundle is loaded, at start-up or on a refresh, where it can be caught and rolled back.
  • Raising ruleCascade is a major change of a ruleset (authoring guidelines 1.5): consumers on an older runtime refuse it. Its checksum changes even when the rules do not.
  • Runtimes released before this decision check only the major version, so they still accept a 1.1 bundle and fail per request. The protection starts with the first runtimes that implement 1.1. Upgrade every runtime that evaluates a ruleset before the ruleset uses a 1.1 operator.
  • Each new operator now also needs a row in each runtime's table of operators and the version that added them.

Alternatives considered

  • Bump the bundle format to 2.0.0. Old runtimes would refuse new bundles today, but every bundle, 1.0 rules included, would need a new runtime, and the format did not change.
  • Let the compiler infer the version from the operators used. No author action, but the version a document conforms to would be invisible in the source, and a reviewer could not see a ruleset stop working on older runtimes in the diff.
  • Check only at evaluation time. That is what happened before: the failure reaches users.

On this page