RFC-048: A Cell Answers From Its Chronicle Through a Reduction

ProposedPartially implemented
Depends onRequired by

Decision

A reduction (RFC-022 §4.1) is rulework of its own kind: regulatory_layer: REDUCTIE. It lives with the cell. It reads the cell’s decisions and returns one answer, which fills an open term of the article that asks it (implements). The cell answers that term, the engine records who answered, and the cell explains its own answer on request. This holds wherever the article runs, also when another article calls it anew.

The example is “slechts eenmaal … per schooljaar” (Leerplichtwet 1969 art. 13a lid 2).

PartWhat it does
Art. 13aDeclares the open term verlof_al_verleend_in_schooljaar, which the head decides per case (decided_per_case_by), and refuses leave when it is true.
School policySays when the school year of a date begins and ends. It reads no decisions.
ReductionFills that open term (implements). It reads the decisions under art. 13a, asks the policy for the school year, and says whether the child already got leave in it.
CellAnswers the engine when an article reads the term: it runs the reduction on its own chronicle and returns the value with its provenance.

The decision records the answer in fields. It is not among the decision’s findings: the reduction filled the term, not the head.

1. Reading decisions: source: {decisions}

source: {decisions: leerplichtwet_1969#13a} means “the decisions taken under this article”.

  • Each row is one decision, with its fields and outputs under their own names.
  • A data source supplies the rows, or a parameter of the same name in a test. Without either, execution fails. It never returns an empty list.
  • The key stands alone in its source.
  • Only a REDUCTIE may use it. A REDUCTIE may not have hooks, overrides, execution.produces or open_terms. Since schema v0.8.5 it may have implements. The loader refuses implements on a REDUCTIE whose $schema names an older version, as the schema of that version does. The loader accepts implements on a version without $schema.

A reduction may call other rulework as usual. The school’s reduction gets the school year from the policy through source.regulation.

2. What counts as granted

A decision counts when all of these hold:

  • verlof_verleend is true;
  • $besluit.bsn_jongere equals the parameter $bsn_jongere (rows are read through $besluit, RFC-049);
  • its first leave day falls in the school year of the leave asked.

A school year runs from 1 August to 31 July. The Wet op het primair onderwijs (art. 1) uses that period, but it is not in the corpus, so the policy has no legal_basis. Leave that spans two school years counts for the first. That is our own choice.

3. The open term and the cell

What happens inside a cell is the cell’s responsibility. The position paper puts it as “Reductie vindt altijd plaats ín de cel waar de betreffende chronolexogrammen zijn vastgelegd, op verzoek van een businessproces”, and the cell returns the lexostatus to the process that asked. The project set the same line from the law’s side on 9 October 2026. The law defines the interface of a cell, and the cell decides how it reduces. A reduction in the law format is one way to do that.

So the engine does not run the reduction. A caller may give an execution an answerer (OpenTermAnswerer, passed in a Call next to the findings). When an article reads an open term, at any depth, the engine asks the answerer first. The cell is that answerer during a decision. It answers only a term that one of its reductions in force on the day names in implements. The engine knows nothing of cells or chronicles.

Art. 13a leaves the term to the head (decided_per_case_by: het hoofd (artikel 13a, tweede lid)), without a default. The answer comes before the order of precedence of RFC-047: it wins over an implementation and over a finding of the head. Without any of them the execution fails. A missing answer therefore never grants a second leave.

The cell passes the reduction the parameters it declares, by name, from the article that asks the term (bsn_jongere, eerste_verlofdag). The engine hands those over in its question.

The engine records the answer as one step. ArticleResult.open_terms reports it as filled_by: {answer: ...}, with the cell (answerer), the reduction (law_id, valid_from of the version) and how far it read the chronicle (records_read, last_record). The trace has one node for the term with the same provenance, and no FOREACH. The engine also never runs a reduction as an implementation by itself: a REDUCTIE stays out of the implements index, so its implements is the cell’s declaration and nothing more.

A cell is opened with its directory: Cell::open and Cell::in_memory take it as a required argument. They load the reductions from reducties/<id>/<valid_from>.yaml in that directory, so a cell cannot be opened without them. The loader refuses a reduction that fills no open term, since nothing would ask its answer. It also refuses a version whose $id is not its directory’s name or whose file is not named after its valid_from, and any entry under reducties/ that is not such a version. A hidden entry, such as .gitkeep, is ignored. The loader would otherwise skip that file silently, and the term would become the head’s. When the cell decides:

  • reading::load loads every version of the reductions into the service, so the cell can run them there, and unloads them after the decision; it refuses a law already loaded under a reduction’s id;
  • reading::refuse_undeclared_terms refuses a reduction in force on the day that fills a term its article does not declare that day, since the engine would never ask it;
  • the Reader answers the engine; for each input with source.decisions it passes the cell’s own decisions as rows, under the input’s name, so no data source is involved.

The cell directory decides which terms are the cell’s. A term that a reduction in reducties/ implements, in any version, is the cell’s to fill. Without its directory the cell could not tell such a term from a finding of the head, which is why opening a cell requires it.

The cell refuses a decision or a replay on a day on which no reduction in force fills such a term. This holds from the first valid_from of a version that implements the term. Before that day the term was the head’s, so a decision taken then still replays. The error names the term and the reducties directory. The cell checks this condition with reading::unfilled for every decision, whether that decision reaches the term or not.

A cell without reductions is opened with a directory that has no reducties, or an empty one. In that cell a term is the head’s finding. A path that is not a directory is an error, and so is a reducties that is not a directory.

A lexostatus is the answer a cell gives from its own facts (RFC-022 §4.1). Cell::answers lists the lexostatussen the cell can answer: per version of each reduction in its directory, the open term it fills (law, article, open_term), the reduction’s $id and the version’s valid_from. This list is a RegelRecht addition. In the Chronolexografie position paper a cell reduces its chronicle on request into a lexostatus. The paper does not have a cell declare which lexostatussen it can answer; that idea comes from RFC-022 §4.1.

The answerer serves the whole decision, so the engine reaches the cell at any depth. When Awb 7:11 calls art. 13a anew (RFC-050), the engine asks the cell in that call. The cell reads its chronicle as it stood before the decision and leaves the contested decision out of the rows.

Outside a cell nothing implements the term. A scenario then gives the head’s finding under the id of the term, at the top or under findings of a decision value.

Cell::explain takes a decision again with tracing, on its own day, with the cell answering from the chronicle as it stood before that decision. Its trace shows the answer as one node. It fails when the outcome differs from the one recorded. A different lexostatus fails the replay only where it changes that outcome.

Cell::explain_answer(service, decision, term) is the cell’s own account. It replays the decision and runs the reduction for that term with tracing, with the parameters and the chronicle the decision gave it. That trace shows the reduction’s FOREACH: which decisions it read and which the filter skipped. It is separate from the decision’s trace, and it is not stored.

Why

  • Law and policy contain nothing about the chronicle. A jurist reads both without knowing what a cell is.
  • The school-year rule is written once, in policy.
  • Anyone can supply the answer. A BDD scenario passes the head’s finding.
  • The cell answers; the engine records who answered. The reasoning is one call away (Cell::explain_answer), also for a nested call.
  • A reduction uses the same rule language as an article, so it is type-checked and traced in the same way.

Tradeoffs

  • The head can still give a finding on the term, but wherever the cell answers it is set aside; the trace says so.
  • The trace of a decision no longer shows why the cell answered as it did. A reader who wants that asks the cell, and the cell replays its reduction to say it.
  • The directory is the only record of which terms are the cell’s. A cell loaded from the wrong directory, one without the reduction, replays the answer the reduction gave as the head’s finding, with the same outcome. Only provenance stored with the decision would catch that, and the cell stores none.
  • An article cannot yet be called per FOREACH element. The reduction asks the policy once, for the leave asked, and compares each decision with those dates.
  • Only the directory keeps a reduction out of the corpus. Nothing yet refuses a REDUCTIE under corpus/regulation.

Alternatives Considered

  • The reduction returns a list, and policy computes from it. The policy would then hold logic that exists only because of the chronicle.
  • A binding or a reduction language in cell configuration (PR #1689, the sketch in RFC-022 §4.1). Cell configuration is a last resort, and a second language would have no type checker or trace.

Implementation

  • Schema v0.8.2: regulatory_layer: REDUCTIE and source.decisions. Schema v0.8.5: implements on a REDUCTIE; load_check::reject_misplaced_reduction refuses it under an older $schema.
  • Engine: Source::decisions, resolved through a data source or a parameter of the same name; load_check::reject_misplaced_reduction. The answerer (answer.rs: OpenTermAnswerer, Question, Answer, AnswerProvenance, Call) is asked first in resolve_open_term; FilledBy::Answer and PathNode.answer record it. The resolver leaves a REDUCTIE out of its implements index.
  • Corpus: art. 13a with the open term, the school-year policy, and the reduction under corpus/cells/obs_de_klimop/.
  • packages/cel: Reduction, reading::Reader, reading::load, reading::unfilled, Cell::open, Cell::in_memory, Cell::answers, Cell::explain, Cell::explain_answer.
  • Not built: calling an article per FOREACH element, and a reduction across cells.

Note: RFC-051 adds source.executograms for the executograms of a stream of the cell, and has the cell answer inside its own reductions. The school’s reduction reductie_verlof_valt_buiten_schoolvakanties (RFC-051) fills verlof_valt_buiten_schoolvakanties of art. 13a.

Note: RFC-050 first had the cell add bestreden_besluit_herroepen to each row: whether a decision on an objection revoked the decision. Awb 7:11 now holds the procedure in rulework. A row therefore carries the decision’s id instead, and nothing the cell adds. The reduction reads the decisions under Awb 7:11 as a second decisions input and leaves out a grant that one of them revoked.

Changed on 2026-10-16: the first version had art. 13a ask the answer as a parameter, and the cell ran the reduction and passed the answer by name (Bound::answer). In a nested call the engine could not find it, so the cell refreshed the fields of the decision value it passed (RFC-050), and the trace of art. 13a did not show why the reduction answered as it did. Now the reduction fills an open term with implements, and this RFC no longer forbids that key on a REDUCTIE.

Changed on 2026-10-17: the version of 16 October had the engine run the reduction as an implementation, so the decision’s trace held the reduction’s FOREACH. The cell bound its decisions as a data source. Now the cell answers the term through an answerer the caller passes, the engine records who answered and from what, and the cell explains its answer apart (Cell::explain_answer). implements on a REDUCTIE stays, as the cell’s declaration of the term it answers.

References

  • Leerplichtwet 1969 art. 13a; Wet op het primair onderwijs art. 1
  • RFC-022 §4.1: lexostatus and reduction
  • RFC-047: the decision on the application

RegelRecht

An exploration into transparent, executable legislation, and one of the three projects in the starting selection of the Nederlandse Digitale Dienst.

Links

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

Contact

regelrecht@minbzk.nl

Part of

Nederlandse Digitale Dienst
Ministry of Economic Affairs and Climate Policy