RFC-039: Addressable Execution Traces

AcceptedPartially implemented
Depends on

Implementation note. The format is published at schema/trace/v1/trace-schema.json and the engine emits it: the envelope, node_id, anchor, legal_basis, source on a data-source resolution, and type_spec where the law declares one. A trace of the zorgtoeslag validates against the schema. yaml_path and uri are part of the format and carried by PathNode, but nothing populates them yet, so they are absent from every trace an engine produces today. Consumers were converted with the rest of the work: the regular expression over message is gone from the demo’s lineage view and the graph stepper reads anchor.law_id.

Context

An engine records how it reached a result, as a tree of steps. A step cannot say which article it came from.

The outermost step of a zorgtoeslag evaluation is called:

wet_op_de_zorgtoeslag (2025-01-01 {bsn: 999993653} hoogte_zorgtoeslag)

A law id, a date, the parameters, an output name. No article, and none anywhere below it either. A step that reads a fact out of a register comes out like this:

{ "node_type": "resolve", "name": "polis_status", "resolve_type": "DATA_SOURCE", "result": "ACTIEF", "message": "Resolving from SOURCE insurance: ACTIEF" }

Everything a reader needs beyond the bare value is in that English sentence. So consumers parse it. To learn which organization supplied a fact about a citizen, you match ^Resolving from SOURCE ([^:]+):, and a citizen-facing “Gebruikte gegevens” view is built that way today. To learn which law a step is in, you look for a # in its name. To learn that "result": 209692 is an amount in cents rather than euros or months, there is nothing to read at all.

name is not an address either. One zorgtoeslag trace contains Resolving from PARAMETERS: $BSN eight times over: eight steps, one name, different laws. A consumer that wants to point at one of them has nothing to point with, so a second renderer of the same tree ended up numbering steps by their position, inventing an address the format does not offer.

The material is in scope at evaluation time and never joined. An article carries its number, its verbatim text and a link to wetten.overheid.nl. The law schema allows a legal_basis reference on a field, an action and every operation. An addressing scheme exists in regelrecht://{law_id}/{output}#{field}.

No RFC owns any of this. RFC-013 defines the Execution Receipt and leaves the trace as "trace": { } in its envelope example. Four roadmap work packages already treat the trace as a legal document: “Rechtsbescherming en de execution trace”, “Informatiepositie burgers/ondernemers”, “Lakmoesproef besluiten met execution trace en burgerdossier”, and “Werkwijze rechterlijke macht I”. None is answerable while a step cannot name its provision.

Decision

The trace becomes a published document format, schema/trace/v1/trace-schema.json. How an engine fills it is its own business; the format is what a second engine has to produce, so that a municipality running its own engine can produce a trace a court recognizes.

The same register step under this format:

{ "node_type": "resolve", "name": "polis_status", "node_id": "n1.0", "result": "ACTIEF", "anchor": { "law_id": "zorgverzekeringswet", "article": "2", "paragraph": "1" }, "yaml_path": "articles.2.machine_readable.execution.input.polis_status", "source": { "kind": "DATA_SOURCE", "provider": "insurance" } }

The document is {"trace_version": 1, "root": …}, and a step carries:

FieldWhat it denotes
node_idAddress of the step within this trace: the index chain from the root, n, n0, n0.2.
anchorThe innermost article the step was evaluated within, whatever the document cites.
legal_basisThe citation the document declares for the element at yaml_path, as written.
yaml_pathAddress of the element within the law document, as written.
uriConvenience address of the value, as a regelrecht:// URI.
sourceWhere a resolved value came from: kind, provider, and the law a scoped source was registered for.
type_specDeclared unit and precision of the result, so 209692 can be shown as an amount in euros.

Its own version axis. schema/trace/v<trace_version>/, not a law-schema version directory. A trace shape and a law shape change for different reasons, and the note sidecar of RFC-005 went under schema/v0.5.2/ and v0.5.3/ and then froze there while the law schema moved on to v0.5.9. A published trace version is immutable on the same footing as a published law schema.

yaml_path uses the path language the corpus already speaks. Dotted, and for a list item keyed on its output, name or number rather than its index. The corpus addresses parts of a law this way in the configuration that decides, per law, which lines open by default, so a step’s address is already an expanded_paths entry. It also reads: voldoet_aan_voorwaarden.value.conditions.0 says something, /execution/actions/0/value/conditions/0 does not. And it addresses one branch of a nested operation, which has no name for a URI to use. The path names the element as written, so an action carrying its operation inline is actions.X.values.1 where one nesting it under value is actions.X.value.values.1. A path that does not select the element it names is worse than no path, so an engine that normalizes those two forms internally must keep the distinction or omit the field.

uri is a convenience. anchor.law_id and yaml_path are the address; the URI is nicer to read where it can be written, and often it cannot. The scheme puts the law id before a /, so a law whose id is alcoholwet/vergunning/rotterdam has no expression in it at all, and 31 of the 79 law ids in corpus/demo contain a slash; it also has nothing to put in the output segment for a step naming an input or an open term. It is then absent, and absence does not mean the step denotes no value.

anchor and legal_basis stay apart. One is where the engine was, the other is what the document claims. Collapsing them would hide a citation hung on the wrong provision. anchor is reported for every step inside an article, including a bare arithmetic operation; legal_basis is absent when that element cites nothing, which is why explanation occurs on a legal_basis and never on an anchor.

message is presentational. It exists to be read by a person, its wording is not a contract, it may be in any language, and parsing it is a defect. Every fact it states is in a field. Three consumers parse it today, and each is a consequence of there being nothing else to read.

Trace depth stays all or nothing. The claim in RFC-013 that it is configurable is withdrawn. Folding a subtree is a presentation decision, and whatever is doing the presenting knows how much room it has and who is reading.

Out of scope, deliberately: exposing the receipt to a browser, an HTTP execution endpoint, correlating traces across organizations (the trace_id of RFC-009), and signing a trace. Staged execution under RFC-008 returns outputs and pending inputs with no trace at all, so a decision that unfolds over several stages cannot yet be explained as a whole. That gap deserves an issue of its own: a staged beschikking is where a citizen most needs an explanation.

Why

Benefits

A step can name its provision, so a trace can be read beside the statute. That is what Awb article 3:46 asks of a motivation and what AERIUS I (ECLI:NL:RVS:2017:1259) means by inzichtelijk en controleerbaar, and it makes the four roadmap work packages answerable. Four consumers get shorter. The regular expression goes, anchor.law_id retires both string parses in the graph stepper, units become showable, and the terminal rendering stops being an interface. A recorded trace also becomes a fixture, so a renderer can be built and tested against one with no engine in the loop.

Tradeoffs

The envelope is a breaking change, at the boundary an engine offers a browser as much as on disk. A caller is handed the root step directly today, and everything reading it that way will read root out of a document instead, Traceability and Engine in these docs included. That is deliberate, because a trace that travels needs its version inside it, and the same work converts those consumers anyway. Version 1 is the first version, not a claim about what came before.

The document grows, by an amount nothing has measured yet: an anchor carrying a wetten.overheid.nl link and a Juriconnect reference is around 200 bytes, a step can hold two, and a step’s current JSON is on the order of 150. A densely cited law therefore costs several times its present size and a law citing nothing costs almost nothing. Traces are produced on request and archived by hash, so the cost lands at archiving rather than in the hot path.

Anchors will be sparse. Under corpus/regulation/ the element-level reference object occurs twice, both on the zorgtoeslag article this work annotated to have something to read; the other nine legal_basis occurrences are four law-level authority lists and five plain strings on open terms. That form is used 344 times under corpus/demo/, and carries an explanation every time. A step whose element cites nothing reports nothing rather than borrowing the enclosing article’s citation, which leaves the gap visible.

node_id addresses a position, not an identity. A memoized cross-law result is one childless step where a cache miss is a whole subtree, so asking for one output and asking for two can number the same evaluation differently, and adding a step to the instrumentation renumbers everything below it. Stability holds within a trace document, which is what a fixture pins and a URL fragment names.

A trace reads back, but not byte for byte. A decimal is written through a float, so a whole one goes out as 100.0, reads back as an integer, and goes out again as 100. That bounds what a fixture can assert: a golden trace has to be compared against a freshly serialized trace, in one direction, rather than against a re-serialization of its own parse. Fixing the underlying serialization would change a shape every consumer already reads and belongs to its own RFC.

Alternatives Considered

Keep reading the message. Rejected: three consumers already do it, one is broken on units, and the string they parse exists to be read by a person, so every rephrasing is a silent break in a citizen-facing view.

A separate explanation document. Rejected: two accounts of one evaluation have to be kept in agreement, and the second is derived from the same missing information as the first. The trace has the right shape and is missing its anchors.

An event stream instead of a tree. Rejected: every consumer wants the tree, so this moves the same work into four places to save it in none.

Anchors only in the receipt. Rejected: the receipt does not reach the browser, which is where a citizen reads an explanation, and per-output provenance is the wrong granularity anyway. It names the article that produced an output, not the provision behind each step.

Derive a unit for an intermediate result. Deferred. An engine can already infer one for its own type checking, but from a single article’s symbols, so a cross-law operand falls outside it, and it has no notion of precision. Version 1 reports a declared type_spec only.

References

  • schema/trace/v1/trace-schema.json, the format this RFC decides
  • Traceability and Engine, which describe the current shape and change with this
  • Awb article 3:46, motiveringsplicht; AERIUS I, ECLI:NL:RVS:2017:1259
  • W3C PROV-DM, provenance vocabulary

RegelRecht

An exploration by Bureau Architectuur of the Dutch Ministry of Economic Affairs and Climate Policy into the possibilities of transparent, executable legislation.

Links

GitHub repository
How it works
Stay informed
Roadmap (Dutch)
Documentation
Research

Contact

regelrecht@minbzk.nl

Part of

Bureau Architectuur
Ministry of Economic Affairs and Climate Policy