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 syncregenerates them andtools/lint_specs.pyfails 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.