RFC-013: Execution Provenance
Status: Draft Date: 2026-04-02 Authors: Eelco Hotting
Context
The engine produces deterministic results: given the same regulation YAML, the same inputs, and the same calculation date, it always produces the same outputs. RFC-006 chose Rust precisely for this guarantee. Determinism within a single execution is necessary but not sufficient. Government agencies must be able to reproduce a specific decision months or years later, with the exact same result.
Dutch administrative law requires this:
- Awb Art. 3:46 (motivation obligation): every government decision (besluit) must rest on a sound motivation. For algorithmic decisions, the agency must be able to reconstruct how the algorithm reached its result.
- AERIUS I (ECLI:NL:RVS:2017:1259): the Raad van State ruled that automated government decisions must be transparent and auditable (inzichtelijk en controleerbaar). For rule-based systems (as opposed to ML), full logical reproducibility is the expectation.
- EU AI Act Art. 12 (mandatory August 2026): high-risk systems must automatically log events including input data and verification results. Logs must be retained for at least 6 months.
- GDPR Art. 13-15, 22: citizens have the right to "meaningful information about the logic involved" in automated decisions and the right to contest those decisions.
Reproducibility requires pinning three things: the regulation YAML, the schema version it conforms to, and the engine version that executed it. Today, the engine stamps none of these on its output. The regulation carries a $schema URL, but the engine ignores it at execution time. The engine version exists as a Cargo constant but is not included in results. There is no "decision receipt" that ties inputs, outputs, and versions together.
RFC-009 (Multi-Organisation Execution) adds another dimension: a decision may depend on values accepted from other organisations' engines, each potentially running a different engine version. Reproducing that decision requires knowing which engine this organisation ran, and which engine versions produced the accepted values from other organisations.
The schema is a specification, the engine is an implementation
The schema defines the regulation format: what fields exist, what operations are valid, what structures are allowed. The engine interprets and executes regulations that conform to the schema. Think SQL (specification) vs PostgreSQL (implementation), or JSON Schema (specification) vs any validator.
This distinction matters because third-party organisations may build their own engine implementations. A municipality (gemeente) might use a different technology stack. The schema must be precise enough that any conformant engine produces identical results for identical inputs. When two engines disagree, one has a bug. The schema is the arbiter.
Coupling schema and engine version numbers would conflate specification with implementation. A schema change (new field, new operation) is a format change. An engine change (bugfix, performance improvement) is an implementation change. They evolve on different timelines for different reasons.
Decision
Five decisions establish the reproducibility model.
1. Independent versioning with declared compatibility
The schema and engine are versioned independently. Both use semantic versioning.
Schema versions follow the existing convention: immutable directories under schema/ (e.g., schema/v0.5.0/schema.json). A published schema version is never modified. The CI protect-schema job already enforces this.
Engine versions are declared in Cargo.toml and correspond to GitHub Release tags (via release-engine.yml). The engine version is bumped for any change that could affect execution output.
Each engine release declares which schema versions it supports:
# packages/engine/Cargo.toml
[package]
name = "regelrecht-engine"
version = "0.6.0"
[package.metadata.regelrecht]
supported-schemas = ["v0.5.0", "v0.5.1", "v0.6.0"]The engine validates at startup that loaded regulations reference a supported schema version. A regulation referencing an unsupported schema version is a hard error, not a silent fallback to serde-only validation.
2. Execution Receipt
Every execution produces an Execution Receipt: an output envelope that contains everything needed to reproduce the result.
{
"provenance": {
"engine": "regelrecht",
"engine_version": "0.6.0",
"schema_version": "v0.5.1",
"regulation_id": "wet_op_de_zorgtoeslag",
"regulation_valid_from": "2025-01-01",
"regulation_hash": "sha256:a1b2c3..."
},
"engine_config": {
"connectivity": "federated",
"legal_status": "authoritative",
"untranslatable_mode": "error",
"identity": {
"name": "Dienst Toeslagen",
"organisation_id": "00000004003214345000",
"type": "INSTANCE"
}
},
"scope": {
"sources": [
{
"id": "minbzk-central",
"ref": "v2025.1",
"tree_hash": "sha256:112233..."
},
{
"id": "amsterdam",
"ref": "main@abc1234",
"tree_hash": "sha256:445566..."
}
],
"loaded_regulations": [
{
"id": "wet_op_de_zorgtoeslag",
"valid_from": "2025-01-01",
"hash": "sha256:a1b2c3..."
},
{
"id": "regeling_zorgverzekering",
"valid_from": "2025-01-01",
"hash": "sha256:b2c3d4..."
},
{
"id": "algemene_wet_inkomensafhankelijke_regelingen",
"valid_from": "2025-01-01",
"hash": "sha256:c3d4e5..."
}
],
"scopes": [
{"type": "gemeente_code", "value": "GM0363"}
]
},
"execution": {
"calculation_date": "2025-01-01",
"parameters": {
"bsn": "999993653",
"berekeningsjaar": 2025
},
"reference_date": "2025-06-15"
},
"results": {
"outputs": {
"heeft_recht_op_zorgtoeslag": true,
"hoogte_zorgtoeslag": 209692
},
"trace": { }
},
"accepted_values": [
{
"output": "toetsingsinkomen",
"value": 3200000,
"authority": "inspecteur",
"engine": "regelrecht",
"engine_version": "0.5.1",
"regulation_id": "wet_inkomstenbelasting_2001",
"regulation_hash": "sha256:d4e5f6...",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"signed": true
}
],
"timestamp": "2025-06-15T10:30:00Z"
}The receipt records:
- Which engine, version, and schema produced the result
- The engine configuration: connectivity mode (solo/federated), legal status (simulation/authoritative), untranslatable mode (RFC-012), and engine identity (RFC-009). These determine execution behavior: whether cross-org calls are made, whether outputs carry legal weight, and how untranslatable constructs are handled
- The scope: which corpus sources were loaded (by source id, Git ref, and tree hash), which regulations were loaded (by id,
valid_from, and content hash), and which jurisdiction scopes were active - The calculation date, parameters, and reference date
- Outputs and trace tree
- For cross-organisational executions (RFC-009): the provenance of each accepted value, including the other authority's engine version and regulation hash
- When the execution occurred
The scope section is what makes the receipt fully reproducible. The engine's output depends on all loaded regulations, not just the one being evaluated. IoC resolution (RFC-003) pulls in implementing regulations. Cross-law references resolve against whatever is loaded. Priority rules (RFC-010) determine which version of a regulation wins when multiple sources provide it. Two engines with different loaded regulations can produce different results for the same parameters.
The loaded_regulations array lists every regulation the engine had available during execution, each with a SHA-256 content hash. The sources array lists each corpus source with its Git ref and tree hash. Together, these allow exact reconstruction of the execution environment.
The scopes array records which jurisdiction scopes were active (per RFC-010). An engine running with scope GM0363 (Amsterdam) loads different municipal regulations than one running with GM0384 (Diemen), which changes IoC resolution for national laws that delegate to municipal ordinances.
The accepted_values section captures the cross-organisational provenance chain. When engine A accepts a value from engine B, A records B's provenance metadata. To reproduce the full execution, you need A's receipt (which tells you B's engine version and regulation hash) and B's receipt (which B stores in its own Logboek Dataverwerkingen per RFC-009).
3. Schema URL immutability
Regulation YAML files currently reference schemas via GitHub main branch URLs:
$schema: https://raw.githubusercontent.com/MinBZK/regelrecht/refs/heads/main/schema/v0.5.0/schema.jsonThis URL is mutable. A force-push or file edit on main would silently change what the URL resolves to. For a system that must reproduce decisions years later, that is unacceptable.
Schema URLs switch to Git tag references:
$schema: https://raw.githubusercontent.com/MinBZK/regelrecht/refs/tags/schema-v0.5.0/schema/v0.5.0/schema.jsonWhen a schema version is published, a Git tag schema-vX.Y.Z is created. GitHub tag protection rules prevent deletion or modification.
The engine embeds schema files at compile time (via include_str! in validate.rs) and validates by content, not by URL fetch. The URL is a pointer for humans and tooling. The content hash in the Execution Receipt is the integrity anchor.
4. Multi-organisation reproducibility
RFC-009 defines two resolution modes: Execute (compute locally) and Accept (take another organisation's authoritative determination). For reproducibility, the Accept path must capture enough provenance to reconstruct the full decision chain.
Inter-engine protocol versioning
The inter-engine protocol (RFC-009 §5) is a public API contract between organisations. It must be versioned independently from the engine. An explicit api_version field in every request and response lets organisations running different engine versions communicate:
{
"api_version": "1.0",
"law_id": "wet_op_de_zorgtoeslag",
"article": "2",
"output": "toetsingsinkomen",
...
}Within a major API version, backward compatibility is guaranteed. An engine at v0.5.1 and an engine at v0.8.0 both speak api_version: "1.0". Breaking protocol changes require a major API version bump and a migration period where engines support both versions.
Value representation on the wire is pinned: currency values are integers in cents, dates are ISO 8601 strings, booleans are JSON booleans. Internal representation of Value may differ between engine implementations.
Extended provenance in the inter-engine response
This RFC extends the RFC-009 §5 response with engine provenance:
{
"api_version": "1.0",
"value": 32000,
"type": "currency",
"authority": {
"name": "inspecteur",
"organisation_id": "00000001003214345001"
},
"provenance": {
"engine": "regelrecht",
"engine_version": "0.6.0",
"schema_version": "v0.5.1",
"law_id": "wet_inkomstenbelasting_2001",
"article": "2.18",
"regulation_hash": "sha256:d4e5f6...",
"computed_at": "2025-06-15T10:30:00Z",
"mode": "authoritative",
"has_upstream_accepts": true
},
"signature": "eyJ..."
}New fields: engine, engine_version, schema_version, regulation_hash, and has_upstream_accepts. The last signals whether this value depended on accepted values from yet another organisation, meaning the provenance chain is deeper than two hops.
Sealed receipt reproduction
When reproducing a cross-org decision, the engine uses the sealed accepted values stored in the Execution Receipt. It does not call other organisations' engines again.
Why not re-call? The other organisation may now be running a different engine version with different corpus. Re-calling would produce today's answer, not last year's. And legally, a beschikking (individual decision) stands once issued. The competent authority's determination at the time is a legal fact. Reproducing Org A's decision means re-executing A's computation with A's engine version, A's regulation, and the accepted values A received at the time.
Reproduction steps:
- Read engine A's Execution Receipt (contains A's engine version, scope with all loaded regulations and their hashes, parameters, and the accepted values with B's provenance)
- Obtain engine A's exact binary (from GitHub Release by version tag)
- Reconstruct the corpus scope: obtain all regulation YAMLs listed in
scope.loaded_regulations(verify each content hash) - Re-execute in replay mode: the engine loads the same set of regulations, uses sealed accepted values from the receipt instead of making inter-engine calls
- Verify the outputs match the original receipt
For full chain verification (optional, useful for audit or dispute resolution):
- Obtain engine B's receipt from B's Logboek Dataverwerkingen (linked by
trace_id) - Re-execute B's computation independently using B's engine version and regulation hash
- Verify the accepted value matches what B originally provided
Step 5 proves A's computation is reproducible. Steps 6-8 prove B's determination was correct. Step 5 alone satisfies the legal requirement: A is responsible for A's computation, using B's determination as a legal fact.
Version divergence between organisations
Different organisations will run different engine versions. This is expected and acceptable if:
- Each engine version passes the conformance test suite for the schema versions it supports
- The inter-engine protocol is versioned independently and backward-compatible within a major version
- Accepted values include engine provenance in their response
Two engine implementations that correctly implement the same schema version must produce identical outputs for identical inputs. When they diverge, one has a bug. A future RFC will define a language-agnostic conformance test suite that formalizes this contract, providing a shared set of input/output test cases any engine implementation can run to prove correctness per schema version.
5. Compliance requirements as machine-readable regulations
The compliance requirements in this RFC (provenance fields, schema immutability) are themselves rules about how the system must behave. In a platform that executes machine-readable law: can these compliance rules be expressed as regelrecht regulations?
The engine's operation set is designed for computation over citizen data (arithmetic, comparison, conditional logic). It cannot introspect the execution environment. A compliance regulation could define what fields a Execution Receipt must contain, but the engine cannot verify its own output structure or inspect other regulations' metadata. The compliance YAML would need external inputs (engine_version_present: true/false supplied by a test harness) to produce a verdict.
This is useful in two ways:
- The compliance requirements, expressed as a regulation, become a versioned, machine-readable document. Any engine implementation can evaluate it with a test harness that bridges output metadata back into the regulation's inputs.
- The system that executes Dutch law expresses its own compliance obligations in the same format.
Runtime self-enforcement (the engine checking its own compliance during execution) would require engine context variables ($engine.version, $regulation.hash) in the execution scope. That is a future extension.
6. Per-output provenance in receipts
The results section of the Execution Receipt includes an output_provenance map that tags each output with how it was produced:
{
"results": {
"requested_outputs": ["minister_is_bevoegd"],
"outputs": {
"minister_is_bevoegd": true,
"motivering_vereist": true,
"bezwaartermijn_weken": 4
},
"output_provenance": {
"minister_is_bevoegd": { "type": "Direct", "law_id": "vreemdelingenwet_2000", "article": "14" },
"motivering_vereist": { "type": "Reactive", "law_id": "algemene_wet_bestuursrecht", "article": "3:46", "hook_point": "preactions" },
"bezwaartermijn_weken": { "type": "Reactive", "law_id": "algemene_wet_bestuursrecht", "article": "6:7", "hook_point": "postactions" }
}
}
}This serves two purposes:
- Audit trail: which mechanism produced each output (direct execution, hook, or lex specialis override)
- Privacy-by-design filtering: the multi-output API uses provenance to decide what to include — requested outputs plus causally-entailed hook/override outputs, but never unrelated outputs from other articles
See RFC-007 for the hook provenance model.
Why
Benefits
- Execution Receipts satisfy Awb Art. 3:46 (motivation), AERIUS (auditability), and EU AI Act Art. 12 (logging). The receipt is the archivable artifact that proves how a computation was performed.
- Independent versioning lets other organisations build their own engine on their own release cadence.
- Sealed receipt reproduction makes cross-org decisions traceable and reproducible without requiring cooperation from other orgs at verification time.
- Versioning the inter-engine protocol separately from the engine lets organisations upgrade at their own pace.
- Receipts are additive — they extend
ArticleResultwithout changing execution logic. Schema enforcement and CI checks can be rolled out phase by phase. - The engine declares which schemas it supports. Adding a schema version doesn't break old regulations. Dropping support is an explicit, versioned decision.
Tradeoffs
- Every execution-affecting change must bump the engine version. This requires CI enforcement and team discipline. Without it, reproducibility is impossible.
- Including provenance, trace, and accepted values makes receipts larger than bare outputs. Trace depth is configurable; receipts can be archived and referenced by hash.
- Reproducing engine B's computation requires access to B's receipt (via Logboek Dataverwerkingen) and B's engine binary (via its release). If B does not publish releases or retain receipts, full chain reproducibility is limited to A's portion.
- The schema URL format change (
refs/heads/maintorefs/tags/schema-vX.Y.Z) requires a one-time migration of all corpus YAML files.
Alternatives Considered
Alternative 1: Coupled schema + engine versions
- Schema v0.6.0 always pairs with engine v0.6.0. The
$schemafield implicitly identifies the engine. - Rejected because it conflates specification with implementation. Prevents third-party engines from using their own version numbers. Forces engine releases for schema-only changes and vice versa.
Alternative 2: Engine backward compatibility (support all schema versions forever)
- A single engine binary can execute any historical regulation regardless of schema version.
- Rejected because it accumulates compatibility shims indefinitely. After 20 schema versions, the engine is mostly compatibility code.
Alternative 3: Content-addressed schema references (SRI hashes in YAML)
- Add
$schema_integrity: sha256-<hash>alongside the$schemaURL. - Deferred. Adds complexity to every YAML file and every tool that reads them. Immutable Git tags + compile-time embedded schemas + content hashes in Execution Receipts provide equivalent integrity without burdening the regulation format.
Implementation Notes
Phase 1: Execution Receipt and engine version stamping
- Add
engine_version(fromCARGO_PKG_VERSION) andschema_version(from the regulation's$schemafield) toArticleResult - Add
regulation_hash(SHA-256 of YAML content) toArticleResult - Extend the inter-engine protocol response (RFC-009 §5) with
engine_version,schema_version,regulation_hash,has_upstream_accepts, andapi_version - Define the
ExecutionReceiptstruct as the top-level output envelope - Add replay mode: engine can re-execute from sealed accepted values in a receipt
Phase 2: Schema version enforcement
- Add
supported-schemasmetadata toCargo.toml - Validate at regulation load time that
$schemareferences a supported version - Error on unknown schema versions in
validate.rs(fix the current silent fallback) - Migrate corpus
$schemaURLs fromrefs/heads/maintorefs/tags/schema-vX.Y.Z - Create
schema-vX.Y.ZGit tags for all published schema versions - Update
schema/latestsymlink (currently stale: points to v0.5.0, should be v0.5.1)
Phase 3: CI enforcement
- CI check: engine source changes require version bump (compare
Cargo.tomlversion between PR and main) - CI check: every
schema/vX.Y.Z/directory is registered invalidate.rs - CI check: every
$schemaURL in corpus YAML references an existing schema version - CI check:
schema/latestsymlink points to the highest semver directory - Add
rust-toolchain.tomlwith pinned stable version for reproducible builds - Add
--lockedto all CI cargo commands (currently only release builds use it)
Affected components
| File | Change |
|---|---|
packages/engine/src/engine.rs | ArticleResult gains provenance fields |
packages/engine/src/service.rs | LawExecutionService populates provenance on execution |
packages/engine/src/article.rs | from_yaml_str computes and stores content hash |
packages/engine/src/lib.rs | Re-export ExecutionReceipt type |
packages/engine/src/bin/validate.rs | Error on unknown $schema, verify all schemas registered |
packages/engine/Cargo.toml | supported-schemas metadata, version bump |
packages/shared/src/provenance.rs | New: ExecutionReceipt, ExecutionReceiptProvenance types |
corpus/regulation/**/*.yaml | $schema URL migration to tag-based refs |
schema/latest | Fix symlink: v0.5.0 to v0.5.1 |
.github/workflows/ci.yml | Version bump check, schema registration check, --locked |
rust-toolchain.toml | New: pinned Rust toolchain version |
References
- RFC-003: Inversion of Control — cross-law
open_termsandimplements - RFC-006: Language Choice — deterministic execution as core requirement
- RFC-009: Multi-Organisation Execution — Execute/Accept model, inter-engine protocol, trace model
- RFC-010: Federated Corpus — corpus distribution, temporal consistency via Git refs
- RFC-012: Untranslatables — handling constructs beyond engine expressiveness
- AERIUS I (ECLI:NL:RVS:2017:1259) — transparency and auditability of automated government decisions
- AERIUS II (ECLI:NL:RVS:2018:2454) — reinforced AERIUS I requirements
- SyRI ruling (ECLI:NL:RBDHA:2020:865) — transparency requirements for government algorithms
- EU AI Act Art. 12 — record-keeping for high-risk AI systems
- W3C PROV Data Model — provenance standard (Execution Receipt draws on Entity/Activity/Agent model)
- Logboek Dataverwerkingen — Dutch standard for per-organisation data processing logs
- MDTO — Dutch government metadata standard for durable information access
- Glossary of Dutch Legal Terms