Rule Cascade
ReferenceDecision records (ADRs)

ADR 0005: Compile once into a bundle; evaluators and compilers are separate conformance levels

Status: accepted

Status: accepted

Context

In 1.0.0-alpha.1 every runtime loaded source documents: JSON Schema validation, inheritance, override policies, the static checks and the checksum, before it could evaluate anything. A new language had to port all of that, and every service that enforced rules carried a schema validator and, in practice, a YAML parser, although YAML parsers do not agree on what a ruleset says.

Decision

Loading and evaluating are split. A compiler turns a ruleset and its parents into a bundle: one plain JSON document with the bundle format version (ruleCascadeBundle), the ruleset id, version and checksum, and the server and the client manifest. An evaluator reads a bundle and evaluates. It checks the format of the bundle (BUNDLE_UNSUPPORTED, BUNDLE_INVALID) and otherwise trusts it.

The specification defines two conformance levels to match. The compiled fixtures are published in conformance/bundles, so an evaluator is certified without a compiler, and every compiler must produce exactly those bundles.

Consequences

  • A runtime for a new language is an evaluator: expressions, selection, findings. That is the cheap half, and it is all an application needs at run time.
  • One compilation is evaluated everywhere. Two services cannot disagree because one of them read the YAML differently or skipped a check.
  • A bundle is trusted input. It is a build artefact: built in CI, stored immutably, verified by the deployment. An evaluator cannot recompute the checksum, because the bundle does not contain the resolved source.
  • A bundle contains the server manifest, so it never goes to a browser. Browsers get the client manifest.
  • A bundle carries no title, scope or status, and a source ruleset cannot extend a parent that is present only as a bundle. Tools that need those work from the sources.
  • Compiled files in the repository can go stale. rulecheck sync regenerates them and tools/lint_specs.py fails when they differ from their sources.

Alternatives considered

  • Every runtime loads sources, as before. Each language pays for the loader, and each service for its dependencies and its start-up checks.
  • Ship the two manifests as separate files. Two artefacts that must stay in step; a bundle keeps them under one name and one checksum.
  • A binary or bytecode format. Smaller, but unreadable in review and in an incident, and it needs a reader written for every language. JSON needs no new tooling anywhere.
  • Evaluators repeat the load checks. That removes the benefit. The trust boundary is the artefact store, as it is for the application's own code.

On this page