Onder de motorkap
De specificatie schrijven, afdwingen en uitbreiden
Wat uchar oplevert, is een geschreven specificatie. Deze pagina beschrijft hoe die specificatie is opgebouwd, hoe scripts haar mechanisch afdwingen en hoe je eraan werkt. Waarom uchar bestaat en de licentie staan op het overzicht. Twee woorden komen overal terug: een gate is een controlescript over de hele repository (§ 5), en een landing is het samenvoegen van afgerond werk in main.
Under the hood
Writing, enforcing and extending the specification
What uchar delivers is a written specification. This page describes how that specification is structured, how scripts enforce it mechanically and how you work on it. Why uchar exists and its license are on the overview. Two words recur throughout: a gate is a check script over the whole repository (§ 5), and a landing is the merge of finished work into main.
De specificatie
De specificatie bestaat uit zes Markdown-documenten in spec/, naast de JSON-schema's en de verifier:
| spec/ | inhoud |
|---|---|
IR-SPEC.md | de opbouw van een IR-document: modellaag, regellaag, expressies en condities, stratificatie en defaults, met de WF-regels waaraan een document moet voldoen |
RUNTIME-SEMANTICS.md | wat een IR-document bij uitvoering betekent: leegte, rekenkunde en afronding, tijdlijnen, verdeling, het evaluatiemodel met de foutsoorten en de lezing van een voorwaarde, stratificatie, defaults, tijdselectie, sum types met de afwezigheidsmarkers, en lijst- en recordwaarden, met uitgewerkte voorbeelden |
CONSTRUCTS.md | het constructenregister: per construct de herkomst, het verdict en de afwijkingen van v0 |
ASP-TRANSLATION.md | hoe de Clingo-backend elk construct naar ASP vertaalt |
PLAN.md | het programma: visie, ontwerpprincipes, fasen, de werkwijze met agent-sessies en de open besluiten |
IR-REVIEW.md | de eerste review van de IR van regelspraak-java, waaruit uchar voortkwam |
The specification
The specification consists of six Markdown documents in spec/, alongside the JSON schemas and the verifier:
| spec/ | contents |
|---|---|
IR-SPEC.md | the structure of an IR document: model layer, rule layer, expressions and conditions, stratification and defaults, with the WF rules a document must satisfy |
RUNTIME-SEMANTICS.md | what an IR document means when it runs: emptiness, arithmetic and rounding, timelines, allocation, the evaluation model with the fault kinds and the reading of a condition, stratification, defaults, time selection, sum types with the absence markers, and list and record values, with worked examples |
CONSTRUCTS.md | the construct register: for each construct its origin, its verdict and its deviations from v0 |
ASP-TRANSLATION.md | how the Clingo backend translates each construct into ASP |
PLAN.md | the programme: vision, design principles, phases, the way of working with agent sessions and the open decisions |
IR-REVIEW.md | the first review of regelspraak-java's IR, from which uchar grew (in Dutch) |
IR-SPEC.md behandelt na methode en conventies de modellaag (hoofdstuk 2), de regellaag (3), de expressie- en conditielaag (4, verreweg het langste), stratificatie (5) en defaults (6), gevolgd door de WF-index; hoofdstukken 1, 7, 10 en 11 zijn nog skelet.
Mirror, then diverge
v0 is de getrouwe JSON-dump van de IR van regelspraak-java, op een vastgepinde commit en met al zijn eigenaardigheden. v1 wijkt bewust af waar de eerste review van die IR (spec/IR-REVIEW.md) aannames van Drools aanwees die in de IR terecht waren gekomen; zo wordt salience, de vuurprioriteit van een Drools-regel, gedeclareerde stratificatie. Elke afwijking is een geregistreerde delta in spec/CONSTRUCTS.md, "never a silent rewrite". De brekende afwijkingen zitten samen in het formaat uchar-ir-v1; een construct dat erbij komt, is een optionele uitbreiding (§ 2).
Spec-discipline
Elke voorschrijvende zin, normatief in de zin van standaarden, dus een MUST, vraagt een controleerbaar artefact:
After the method and the conventions, IR-SPEC.md covers the model layer (chapter 2), the rule layer (3), the expression and condition layer (4, by far the longest), stratification (5) and defaults (6), followed by the WF index; chapters 1, 7, 10 and 11 are still skeletons.
Mirror, then diverge
v0 is the faithful JSON dump of regelspraak-java's IR, at a pinned commit, warts included. v1 deviates deliberately where the first review of that IR (spec/IR-REVIEW.md) found Drools assumptions that had ended up in the IR; salience, the firing priority of a Drools rule, for example, becomes declared stratification. Every deviation is a tracked delta in spec/CONSTRUCTS.md, "never a silent rewrite". The breaking deviations are together in the uchar-ir-v1 format; a construct added to it is an optional extension (§ 2).
Spec discipline
Every normative sentence requires a checkable artifact:
- Every MUST carries an ID `WF-###` and comes with: a check in
`spec/verifier/`, at least one passing case in `corpus/golden/`, and one
deliberately failing case in `corpus/negative/`. Where those artifacts do
not yet exist, the WF index marks them `OPEN` — the debt is visible,
never silent.
- Every semantics clause carries a worked example with concrete expected
output; those examples are the reference interpreter's test suite
(`ref/`), verbatim.
…
- **A spec sentence without a machine-checkable artifact is not done.**
De WF-index nummert de regels doorlopend, op 7 november 2026 van WF-001 tot en met WF-129. Een vervallen regel houdt zijn nummer en zijn rij in de index; zes regels zijn vervallen. Op vijf na heeft elke geldende regel een map in negative/: WF-008, WF-010 en WF-019 staan gemotiveerd OPEN, het rode corpus test WF-044 (§ 7), en WF-106 kan geen check hebben, omdat het formaat per objecttype maar één supertype toelaat.
The WF index numbers its rules consecutively, on 7 November 2026 from WF-001 through WF-129. A withdrawn rule keeps its number and its row in the index; six rules are withdrawn. All but five of the rules in force have a directory in negative/: WF-008, WF-010 and WF-019 are OPEN with a stated reason, the red corpus tests WF-044 (§ 7), and WF-106 cannot have a check, because the format allows only one supertype per object type.
Het constructenregister
spec/CONSTRUCTS.md is de scope-matrix van de IR, met één rij per construct. Het verdict is core, dialect, sugar, later(…), non-goal of een samengestelde vorm en legt het oorspronkelijke voorstel vast; wat geland is, staat in de delta-cel. "Kernel or sugar?" is de GHC-Core-vraag: heeft een construct een eigen kernelknoop nodig? Twee rijen, ingekort:
The construct register
spec/CONSTRUCTS.md is the scope matrix of the IR, with one row per construct. The verdict is core, dialect, sugar, later(…), non-goal or a composite form, and records the original proposal; what has landed is stated in the delta cell. "Kernel or sugar?" is the GHC Core question: does a construct need its own kernel node? Two rows, abridged:
spec/CONSTRUCTS.md · # 8
- Construct
- Exception trees with priorities + conflict detection
- Origin
- Catala default calculus
- Verdict (v1)
later(Phase 4)- Kernel or sugar?
kernel- v0→v1 delta
- new: adopted default model; conflicts → the §9.5 inconsistency channel. Design decided (2026-08-16): a first-class defaults group per target with Catala's fall-through — the highest default that decides wins … Landed (2026-08-16/17): the defaults group rides the wire in both target kinds (
prioritizedDefaults, … - Notes
- the hard dependency for Catala interop. Catala distinguishes under-determination (EmptyError) from over-determination (ConflictError) — v1 must keep those outcomes distinct. …
spec/CONSTRUCTS.md · # 10
- Construct
- General folds / function values
- Origin
- Catala
- Verdict (v1)
later(Phase 5)- Kernel or sugar?
- kernel (design at Phase 5)
- v0→v1 delta
- new kernel (extension per the IS Method, never emitter-produced): the
ListTypedatatype (IR-SPEC ch. 2.4, WF-065), the optional top-levelfunctionDefs(ch. 2.11, WF-067 — DMN's Business Knowledge Model, closed bodies, acyclic call graph), and the eight ch. 4.8 nodesListLiteral/Lambda/IfThenElse/Invoke/Fold/MapList/FilterList/ListLength… - Notes
- full-Catala expressibility is the Phase-5 goal (decided 2026-08-16; PLAN §6 Phase 5) — no longer a subset stance; DMN's Business Knowledge Model + invocation (named reusable logic, bind-by-name) lands under the same plan
Phase 4 en Phase 5 in deze rijen zijn fasen van het plan: het defaultmodel in de IR, en Catala als brontaal (spec/PLAN.md §6). Of een construct uitvoerbaar is, is in v1 een feit op IR-niveau: het heeft gespecificeerde semantiek, of het staat in het document als Unresolved, met zijn bron.
Phase 4 and Phase 5 in these rows are phases of the plan: the default model in the IR, and Catala as a source language (spec/PLAN.md §6). Whether a construct can be executed is, in v1, a fact at IR level: it has specified semantics, or it stands in the document as Unresolved, with its source.
Het wire-formaat
Een IR-document is JSON volgens spec/schema/uchar-ir-v1.schema.json, een casusbestand volgt uchar-case-v0.schema.json; beide zijn JSON Schema draft 2020-12, met een $id onder https://uchar.dev/schema/. Dat $id is een identificatie en resolveert (nog) niet. Een IR-document heeft vier verplichte topvelden, format, model (de declaraties), rules (een lijst per regelvorm) en diagnostics, en optioneel vocabularies (hoofdstuk 2.12). Elk object zet additionalProperties: false, elke knoop heeft een discriminator "node", en de namen van knopen, velden en enumeratiewaarden zijn Engels; gedeclareerde namen houden de taal waarin ze geschreven zijn:
The wire format
An IR document is JSON following spec/schema/uchar-ir-v1.schema.json, a case file follows uchar-case-v0.schema.json; both are JSON Schema draft 2020-12, with an $id under https://uchar.dev/schema/. That $id is an identifier and does not resolve (yet). An IR document has four required top-level fields, format, model (the declarations), rules (one list per rule form) and diagnostics, plus the optional vocabularies (chapter 2.12). Every object sets additionalProperties: false, every node has a "node" discriminator, and the names of nodes, fields and enumeration values are English; declared names keep the language they were written in:
"condition": {
"node": "CharacteristicCheck",
"characteristic": "volwassen",
» "negated": true,
"host": "Persoon",
…
"node": "RuleGroup",
"name": "groei",
» "recursive": true,
"members": [
"maak kind"
Canonieke vorm
De dump is deterministisch: declaratievolgorde, getallen zonder exponent, \uXXXX voor niet-ASCII-tekens. Gate 2 leest elk ir.json in golden en red in met behoud van de getallexemen, schrijft het canoniek terug en eist byte-identiteit; zo verschijnt elk verschil tussen schema en emitter als een byteverschil. Daarnaast eist gate 2 dat elk object in golden, red en negative in de canonieke sleutelvolgorde van het schema staat.
Uitbreidingen, en velden zonder semantiek
Een uitbreiding van het formaat is een optioneel veld of een knoop, nooit verplicht en nooit null: ze staat er helemaal of niet. Een deel van die velden draagt geen semantiek, en geen uitvoerder mag er een uitkomst uit afleiden. Dat zijn de benamingen per vocabulaire (hoofdstuk 2.12), origin (hoofdstuk 2.13, WF-121), dat bij een element dat de frontend zelf maakte onder een naam die geen bron declareert, de vorm en de declaratie noemt waaruit het voortkomt, en op een Default de velden name, de naam die de bron aan het blok geeft, en statuteAnnotations, de // Bron:-regels boven het blok, één string per regel (hoofdstuk 6.1). Gate 13 haalt deze velden weg, of verwisselt en verschuift de benamingen, en eist dat ref, Clingo en beide takken van de Drools-backend dezelfde uitkomst geven. In het voorbeeld hieronder maakte de frontend een defaultsgroep voor de eigen uitzonderingen van het subtype Strandjutter op de geërfde regel meldtermijn:
Canonical form
The dump is deterministic: declaration order, numbers without an exponent, \uXXXX for non-ASCII characters. Gate 2 parses every golden and red ir.json while preserving the number lexemes, re-emits it canonically and requires byte identity; any difference between schema and emitter thus shows up as a byte difference. Gate 2 also requires every object in golden, red and negative to follow the schema's canonical key order.
Extensions, and fields without semantics
An extension of the format is an optional field or a node, never required and never null: it is written whole or not at all. Some of those fields carry no semantics, and no executor may derive an outcome from them. These are the designations per vocabulary (chapter 2.12), origin (chapter 2.13, WF-121), which, on an element the frontend made itself under a name no source declares, names the form and the declaration it comes from, and on a Default the fields name, the name the source gives the block, and statuteAnnotations, the // Bron: lines above the block, one string per line (chapter 6.1). Gate 13 removes these fields, or exchanges and rotates the designations, and requires ref, Clingo and both branches of the Drools backend to give the same outcome. In the example below the frontend made a defaults group for the subtype Strandjutter's own exceptions to the inherited rule meldtermijn:
»// Bron: art. 3h lid 2
»Afwijking van meldtermijn: meldtermijn tot het vergunningseinde
geldig altijd
De meldtermijn van een Strandjutter moet gesteld worden op zijn vergunningseinde
indien zijn vergunningseinde gevuld is.
{
"node": "PrioritizedDefaults",
"name": "meldtermijn voor Strandjutter",
"subject": "Strandjutter",
"attribute": "meldtermijn",
…
{
"node": "Default",
» "name": "meldtermijn tot het vergunningseinde",
"justification": {
…
» "statuteAnnotations": [
» "art. 3h lid 2"
]
}
…
» "origin": {
» "form": "exceptionForSubtype",
» "from": {
» "kind": "namedForm",
» "name": "meldtermijn"
}
}
Het vormanker heeft wel semantiek. Een knoop met anchor: "form" in het filter van een fold of in de toets van een kwantor leest het onderwerp van de vorm in plaats van het element, en de knoop IdentityCheck toetst of het element het onderwerp zelf is (hoofdstuk 4.1a, WF-122; in RegelSpraak "de X zelf"). Het vormanker is een voorlopig besluit, dat de maintainer kan terugdraaien.
Eenheden, toekenningen en afwezigheidsmarkers
De eenheid van een getal is een lijst factoren: elke factor is een Unit-knoop, met een gehele exponent waar die niet 1 is (hoofdstuk 2.5, WF-124). Hieronder is €/dg^2 de euro (EUR) maal de dag (dg) tot de macht −2. Een objectcreatie geeft het gemaakte object zijn waarden in assignments, een lijst MemberAssignment-knopen (hoofdstuk 3.3). Een attribuut met afwezigheidsmarkers heeft een eigen sum type met de constructors waarde, onbekend en niet van toepassing. PayloadAccess leest de waarde, en WF-127 eist dat die knoop onder een toets van zijn constructor staat (hoofdstuk 4.7).
The form anchor does carry semantics. A node with anchor: "form" in the filter of a fold or in the test of a quantifier reads the subject of the form instead of the element, and the node IdentityCheck tests whether the element is the subject itself (chapter 4.1a, WF-122; in RegelSpraak "de X zelf"). The form anchor is a provisional decision, which the maintainer can reverse.
Units, assignments and absence markers
The unit of a number is a list of factors: each factor is a Unit node, with a whole exponent where that is not 1 (chapter 2.5, WF-124). Below, €/dg^2 is the euro (EUR) times the day (dg) to the power −2. An object creation gives the created object its values in assignments, a list of MemberAssignment nodes (chapter 3.3). An attribute with absence markers has a sum type of its own with the constructors waarde, onbekend and niet van toepassing. PayloadAccess reads the value, and WF-127 requires that node to stand under a test of its constructor (chapter 4.7).
"node": "Attribute",
"name": "daggeldstijging",
"datatype": {
"node": "Number",
"whole": true,
"decimals": 0,
"sign": "unspecified",
"unit": [
{
"node": "Unit",
"abbreviation": "EUR",
"system": "Munt",
"canonical": "EUR",
"factorToCanonical": {
"num": 1,
"den": 1
}
},
{
"node": "Unit",
"abbreviation": "dg",
"system": "Sleeptijd",
"canonical": "dg",
"factorToCanonical": {
"num": 1,
"den": 1
},
» "exponent": -2
}
]
Casusbestanden
Een case*.json ligt naast een ir.json en bevat format, asOfDate, input en precies één van expected en expectedFault. Getallen zijn exacte lexemen in strings, nooit JSON-floats; wat in de invoer ontbreekt is leeg, en kenmerken beginnen onwaar. Een tijdlijn is een lijst segmenten met from (inclusief) en to (exclusief); een segment zonder from of zonder to heeft een open grens. Een verwachte tijdlijn wordt vergeleken in de canonieke vorm van RUNTIME-SEMANTICS hoofdstuk 3.1. Een afwezigheidsmarker staat er als {"constructor": "onbekend"}, een waarde van een attribuut met afwezigheidsmarkers als {"constructor": "waarde", "payload": …}. Een verwachting van de soort inconsistent vraagt een feit op het foutkanaal en mag de code van de foutsoort noemen (code, zie § 4).
Case files
A case*.json sits next to an ir.json and contains format, asOfDate, input and exactly one of expected and expectedFault. Numbers are exact lexemes in strings, never JSON floats; whatever the input omits is empty, and characteristics start false. A timeline is a list of segments with from (inclusive) and to (exclusive); a segment without from or without to has an open bound. An expected timeline is compared in the canonical form of RUNTIME-SEMANTICS chapter 3.1. An absence marker is written as {"constructor": "onbekend"}, a value of an attribute with absence markers as {"constructor": "waarde", "payload": …}. An expectation of kind inconsistent asks for a fact on the error channel and may name the code of the fault kind (code, see § 4).
{
"format": "uchar-case-v0",
"asOfDate": "2026-03-15",
"input": {
"instances": [
{
"label": "J1",
"objectType": "Jutter",
"attributes": { "aantal aangemelde vondsten": { "number": "14" } },
"roles": {},
"characteristics": []
},
…
"parameters": { "vondstdrempel": { "number": "12" } }
},
"expected": [
{ "kind": "attributeValue", "label": "J1", "attribute": "totaal aangemelde vondsten", "value": { "number": "14" } },
{ "kind": "characteristicPresent", "label": "J1", "characteristic": "penninggerechtigd" },
…
{ "kind": "consistent" }
Een rode case legt de foutsoort vast met haar code uit hoofdstuk 5.7b van RUNTIME-SEMANTICS en de parameters van die fout. In dit voorbeeld noemen de delen van een datumconstructie geen kalenderdag, want 31 april bestaat niet (EF-032):
A red case records the fault kind by its code from chapter 5.7b of RUNTIME-SEMANTICS and the parameters of that fault. In this example the parts of a date construction name no calendar day, since 31 April does not exist (EF-032):
"expectedFault": {
"code": "EF-032",
"params": {
"year": "2026",
"month": "4",
"day": "31"
}
}
De verifier en het negatieve corpus
De verifier in spec/verifier/ is geschreven in OCaml: een getypeerde AST, een strikte loader waarin onbekende of ontbrekende velden laadfouten zijn, en de WF-checks in checks.ml (meer dan 10.000 regels), uitputtend per knoopsoort, zodat een formaatuitbreiding zonder checks niet compileert. ref gebruikt dezelfde AST en loader. Uitvoer: verifier: PASS (exitcode 0) of per overtreding een regel WF-###.<slug> FAIL <pointer>: <message> (exitcode 1); een laadfout geeft exitcode 2. WF-### is de regel, de slug noemt de tak van die regel die afging, met één slug per berichtsjabloon, en samen vormen ze de sleutel. De pointer is een JSON-pointer in het document. verifier --arms geeft hetzelfde rapport voor een programma: per overtreding de sleutel, de pointer en de benoemde parameters die het bericht noemt, als JSON. verifier --dependency-edges drukt de afhankelijkheidsgraaf van hoofdstuk 5 af, die gate 15 naast de regelgraaf van de Drools-backend legt.
Negatieve documenten
Een map in corpus/negative/ heet wf###-slug; gate 3 leidt de regel af uit de mapnaam en eist dat de verifier het document om die regel afwijst. Een expect.txt in de map legt de tak vast: de eerste regel is een sleutel WF-###.<slug> van die regel, elke volgende regel een paar naam=waarde, en de verifier moet die sleutel melden met elk paar onder de parameters van één overtreding. De pin noemt een sleutel en parameters en geen berichttekst, dus een herformulering van een bericht breekt geen pin. Elke JSON-pointer die het rapport citeert, moet in het document bestaan. Het document moet laadbaar zijn, niet schema-geldig: de fout is semantisch. Een voorbeeld:
The verifier and the negative corpus
The verifier in spec/verifier/ is written in OCaml: a typed AST, a strict loader in which unknown or missing fields are load errors, and the WF checks in checks.ml (more than 10,000 lines), exhaustive per node kind, so a format extension without checks does not compile. ref uses the same AST and loader. Output: verifier: PASS (exit code 0) or, per violation, a line WF-###.<slug> FAIL <pointer>: <message> (exit code 1); a load error exits with code 2. WF-### is the rule, the slug names the arm of that rule that fired, one slug per message template, and together they form the key. The pointer is a JSON pointer into the document. verifier --arms gives the same report for a program: per violation the key, the pointer and the named parameters the message quotes, as JSON. verifier --dependency-edges prints the dependency graph of chapter 5, which gate 15 compares with the rule graph of the Drools backend.
Negative documents
A directory in corpus/negative/ is named wf###-slug; gate 3 derives the rule from the directory name and requires the verifier to reject the document for that rule. An expect.txt in the directory pins the arm: its first line is a key WF-###.<slug> of that rule, each further line a pair name=value, and the verifier must report that key with every pair among the parameters of one violation. The pin names a key and parameters, not message text, so rewording a message breaks no pin. Every JSON pointer the report cites must resolve in the document. The document must load but need not be schema-valid: the defect is semantic. An example:
- **WF-046 (stratified negation).** No dependency cycle — including a
WF-045-admissible one — may carry a negative edge. Equivalently, **a
stratum assignment exists**: give every form the smallest stratum such
that along every positive edge the consumer's stratum is ≥ the producer's,
and along every stratum-strict or negative edge strictly greater
(strictness on an admissible creation cycle discharged by the generation
progression, not a boundary). "No negative edge on a cycle" is exactly
the stratifiability condition; the verifier rejects a program for which no
assignment exists. Evaluation then proceeds by strata
(`RUNTIME-SEMANTICS.md` ch. 6.3): a negated read sees a settled producer,
a channel test a complete channel. v0's single agenda boundary is the
two-stratum special case. …
…
*v0 mirror note:* v0 warns on a circular reference at build time and bounds
execution with a fire cap; WF-045/046 make the refusal structural.
Het golden-document jutterswet-penningjaar leest een kenmerk genegeerd, maar niet op een cyclus:
The golden document jutterswet-penningjaar reads a characteristic negated, but not on a cycle:
Regel Kenmerktoekenning aanmeldplichtig
geldig altijd
» Een Jutter is aanmeldplichtig
» indien hij niet penninggerechtigd is.
De negatieve case wf046-negated-read-in-recursive-cycle legt de negatie wel op een cyclus: een recursieve objectcreatie schrijft leeftijd, een kenmerkregel leidt daaruit volwassen af, en de creatie leest volwassen genegeerd. Dan bestaan er geen strata:
The negative case wf046-negated-read-in-recursive-cycle puts the negation on a cycle: a recursive object creation writes leeftijd, a characteristic rule derives volwassen from it, and the creation reads volwassen negated. Then no strata exist:
$ spec/verifier/_build/default/bin/verifier.exe corpus/negative/wf046-negated-read-in-recursive-cycle/ir.json
»WF-046.negative-edge-cycle FAIL /rules/objectCreations/0/versions/0: negative dependency edge on a cycle: maak kind reads what persoon volwassen produces while persoon volwassen is reachable from it (no strata exist)
exit=1
$ spec/verifier/_build/default/bin/verifier.exe --arms corpus/negative/wf046-negated-read-in-recursive-cycle/ir.json
WF-046.negative-edge-cycle /rules/objectCreations/0/versions/0 {"reader":"maak kind","producer":"persoon volwassen"}
exit=0
$ spec/verifier/_build/default/bin/verifier.exe corpus/golden/jutterswet-penningjaar/ir.json
verifier: PASS
exit=0
Dit document heeft geen expect.txt; een ander document voor dezelfde regel wel:
This document has no expect.txt; another document for the same rule has one:
WF-046.negative-edge-cycle
reader=uitgesloten tenzij penninggerechtigd
producer=penninggerechtigd tenzij uitgesloten
Runtime-semantiek en de referentie-interpreter
spec/RUNTIME-SEMANTICS.md legt in elf hoofdstukken vast wat een IR-document betekent, met vergelijkingen, tabellen en uitgewerkte voorbeelden die letterlijk tests van ref/ worden. De kernbeslissingen:
- leegte
- Leeg is "the distinguished absence of a value, belonging to no datatype", dus geen nul en geen onwaar; FEEL-achtige null-propagatie is een non-goal. Een schrijver die leeg oplevert, beslist niets. De afwezigheidsmarkers
onbekendenniet van toepassingzijn gevulde waarden van een sum type (hoofdstuk 9.5). - rekenkunde
- Exact en rationeel: een getal is een breuk t/n in laagste termen, dus 1/3 × 3 = 1.
- tijdlijnen
- "a function from days to values that is constant on each of finitely many periods", over alle tijd: een dag zonder segment is leeg. Een operator of voorwaarde zonder eigen tijdsemantiek leest een tijdlijn per dag; voor leegte gelden een period-coalescing en een contributor-skipping rule.
- verdeling
- "recurrence equations over recipient groups": één gesloten definitie voor alle drie de methoden.
- fixpoint
- "A rule set does not run; it holds": de betekenis is het kleinste fixpoint van de vergelijkingen. Ook geschreven waarden, fouten en deponeringen op het foutkanaal tellen zoals ze aan het fixpoint staan (hoofdstuk 5.7).
- negatie
- Een genegeerde lezing van een afgeleid kenmerk ziet het fixpoint van het positieve deel; geketende negatie krijgt een stratum per schakel.
- effective type
- Een type-annotatie op de wire is een afleiding uit de operanden; een verwijzing heeft er geen, omdat de declaratie de enige bron is. De spec zegt over kopie en afleiding: "Neither is a source"; gate 13 dwingt dat af.
Een uitgewerkt voorbeeld
Sectie 4.6 geeft een verdeling met absolute waarden:
Runtime semantics and the reference interpreter
spec/RUNTIME-SEMANTICS.md fixes in eleven chapters what an IR document means, with equations, tables and worked examples that become tests of ref/ verbatim. The core decisions:
- emptiness
- Empty is "the distinguished absence of a value, belonging to no datatype", so neither zero nor false; FEEL-style null propagation is a non-goal. A writer that yields empty decides nothing. The absence markers
onbekendandniet van toepassingare filled values of a sum type (chapter 9.5). - arithmetic
- Exact and rational: a number is a fraction t/n in lowest terms, so 1/3 × 3 = 1.
- timelines
- "a function from days to values that is constant on each of finitely many periods", over all time: a day without a segment reads empty. An operator or condition without a time semantics of its own reads a timeline per day; for emptiness a period-coalescing and a contributor-skipping rule hold.
- allocation
- "recurrence equations over recipient groups": one closed definition for all three methods.
- fixpoint
- "A rule set does not run; it holds": the meaning is the least fixpoint of the equations. Written values, faults and deposits on the error channel, too, count as they stand at the fixpoint (chapter 5.7).
- negation
- A negated reading of a derived characteristic sees the fixpoint of the positive part; chained negation gets one stratum per link.
- effective type
- A type annotation on the wire is a derivation over the operands; a reference has none, because the declaration is the one source. The spec says of copy and derivation: "Neither is a source"; gate 13 enforces this.
A worked example
Section 4.6 gives an allocation with absolute values:
### 4.6 Worked examples (absolute; `ref/` tests verbatim)
…
- Proportional + floor + rest: 1001 over 3:2, whole euros → raw 600,6 /
400,4 → **600, 400**, rest **1**.
In ref/test/test_ref.ml staat hetzelfde voorbeeld:
The same example in ref/test/test_ref.ml:
(* 4.6-c proportional + floor + rest: 1001 over 3:2, whole euros *)
let s, r =
alloc Proportional ~decimals:0 "1001" [ rcp ~weight:(n "3") "a"; rcp ~weight:(n "2") "b" ]
in
» assigned "WV-4.6-c" s [ ("a", Some "600"); ("b", Some "400") ];
» check "WV-4.6-c-rest" (rest_is r "1");
De suite (dune build @ref/runtest) meldt test_ref: all N worked examples pass, met N boven de duizend; gate 5 bewaakt een ondergrens voor dat getal.
De referentie-interpreter
ref/ is een "small, slow, readable tree-walker" naar het model van WebAssembly: exacte breuken, en buiten de geïmplementeerde deelverzameling altijd een fout met positie. --noop slaat de evaluatie over, zodat gate 7 kan eisen dat golden cases dan falen; --trace schrijft voor elke vorm met een voorwaarde de lezing van die voorwaarde in het fixpunt (RUNTIME-SEMANTICS hoofdstuk 5.12), die gate 17 naast de lezingen van de Drools-backend legt. Bij een rode case meldt de runner elke waargenomen foutsoort als CODE-regel en de vastgelegde als CODE-PINNED-regel; gate 8 leest die (zie § 7).
The suite (dune build @ref/runtest) reports test_ref: all N worked examples pass, with N above a thousand; gate 5 enforces a lower bound on that number.
The reference interpreter
ref/ is a "small, slow, readable tree-walker" on the WebAssembly model: exact fractions, and outside the implemented subset always a fault with its position. --noop skips evaluation, so that gate 7 can require golden cases to fail; --trace writes, for every form with a condition, the reading of that condition at the fixpoint (RUNTIME-SEMANTICS chapter 5.12), which gate 17 compares with the readings of the Drools backend. For a red case the runner reports every observed fault kind as a CODE line and the pinned one as a CODE-PINNED line; gate 8 reads those (see § 7).
usage: runner [--noop | --trace] <ir.json> <case.json>
$ runner corpus/golden/jutterswet-penningjaar/ir.json …/case.json
oracle: PASS (10 expectations)
exit=0
$ runner --noop corpus/golden/jutterswet-penningjaar/ir.json …/case.json
MISMATCH: J1.totaal aangemelde vondsten = leeg, expected 14
MISMATCH: J1 lacks characteristic "penninggerechtigd"
…
exit=1
$ runner corpus/red/date-with-no-calendar-day/ir.json …/case1-31-april.json
CODE EF-032 {"year":"2026","month":"4","day":"31"}
CODE-PINNED EF-032 {"year":"2026","month":"4","day":"31"}
oracle: expected fault observed (EF-032)
exit=0
ref/ en spec/verifier/ delen geen code met de productieruntime van de Drools-backend (backends/drools, pakket dev.uchar.regelspraak.runtime); gate 1 zoekt in beide naar vijf tokens:
ref/ and spec/verifier/ share no code with the production runtime of the Drools backend (backends/drools, package dev.uchar.regelspraak.runtime); gate 1 searches both for five tokens:
git grep -nIE 'RegelSpraakRuntime|org\.drools|rsPlus|rsVerdeelOpVolgorde|Breuk' -- "$zone"
De Drools-backend
De Drools-backend leest het IR van de wire, zoals ref en Clingo. Eén analyse (TmsTranspiler) maakt per regel een plan, en twee weergaven bouwen daaruit een regelbasis: het executable model van Drools (PatternDslRendering), de tak die gate 12 met ref en Clingo vergelijkt en waarmee de distributie bouwt, en DRL-tekst (DrlRendering), waarin elke RegelSpraak-toets en -berekening een Java-functie is met de RegelSpraak-zin erboven. Beide takken leiden af met truth maintenance: een afgeleid feit wordt logisch ingevoegd en rust op de waarden die de regel las, en Drools trekt het in zodra een van die waarden verandert. Tijdens het vuren schrijft de backend dus geen objecten, en hij heeft geen no-loop en geen salience nodig. Gate 12 eist dat de DRL-tekst per golden dezelfde statusregel geeft als het executable model, en gates 13 en 17 draaien beide takken. De routes van een regel naar Drools staan in § 6.
The Drools backend
The Drools backend reads the IR off the wire, as ref and Clingo do. One analysis (TmsTranspiler) makes a plan per rule, and two renderings build a rule base from it: Drools' executable model (PatternDslRendering), the branch that gate 12 compares with ref and Clingo and that the distribution builds with, and DRL text (DrlRendering), in which every RegelSpraak test and calculation is a Java function with the RegelSpraak sentence above it. Both branches derive with truth maintenance: a derived fact is inserted logically and rests on the values the rule read, and Drools withdraws it as soon as one of those values changes. While it fires, the backend therefore writes no objects, and it needs no no-loop and no salience. Gate 12 requires the DRL text to give the same status line per golden as the executable model, and gates 13 and 17 run both branches. The routes from a rule to Drools are in § 6.
De lezing van een voorwaarde
De lezing van de voorwaarde van een vorm op een object is de evaluatie van die voorwaarde op het fixpoint, de eindtoestand van de sessie, vastgelegd per knoop (RUNTIME-SEMANTICS hoofdstuk 5.12). Een lezing schrijft niets en verandert geen uitkomst. Een uitleg die zegt waarom een kenmerk geldt of waarom een waarde leeg bleef, leest de lezing, in plaats van de voorwaarde opnieuw uit te rekenen met een eigen semantiek. Elk paar van vorm en object heeft één status: HOLDS, FAILS, FAULTS, DISPLACED (onder meer een default die niet aan de beurt was, omdat een hogere default van zijn keten besliste) of OUTSIDE_VALIDITY. Elke knoop heeft een van vier uitkomsten: niet bereikt, een oordeel, een waarde met haar eenheid, of een fout met de EF-code van haar soort. Een knoop die de evaluatie als tijdlijn leest, heeft die tijdlijn als waarde. De knopen van een voorwaarde die per periode gelezen wordt, hebben het deel van de tijd waarin ze gelden.
runner --trace drukt de lezingen van een golden case af in de canonieke tekst van dat hoofdstuk, elke regel na reading: per lezing <vorm> <object> <status> en per knoop <vorm> <object> <nummer> <soort> <gevonden>. De vorm is de JSON-pointer van de regelversie, het object het label uit de case, en de knopen zijn genummerd in preorder, de voorwaarde eerst. Voor de regel "Een Jutter is penninggerechtigd indien zijn totaal aangemelde vondsten groter of gelijk is aan de vondstdrempel" van jutterswet-penningjaar:
The reading of a condition
The reading of a form's condition on an object is the evaluation of that condition at the fixpoint, the final state of the session, recorded node by node (RUNTIME-SEMANTICS chapter 5.12). A reading writes nothing and changes no outcome. An explanation that says why a characteristic holds or why a value stayed empty reads the reading, instead of evaluating the condition again in a semantics of its own. Every pair of form and object has one status: HOLDS, FAILS, FAULTS, DISPLACED (among others a default that was not at its turn, because a higher default of its chain decided) or OUTSIDE_VALIDITY. Every node has one of four outcomes: not reached, a verdict, a value with its unit, or a fault with the EF code of its kind. A node that the evaluation reads as a timeline has that timeline as its value. The nodes of a condition read per period have the part of all time in which they hold.
runner --trace prints the readings of a golden case in the canonical text of that chapter, each line after reading: per reading <form> <object> <status> and per node <form> <object> <number> <kind> <found>. The form is the JSON pointer of the rule version, the object the label from the case, and the nodes are numbered in pre-order, the condition first. For the rule "Een Jutter is penninggerechtigd indien zijn totaal aangemelde vondsten groter of gelijk is aan de vondstdrempel" (a Jutter is entitled to the penning if his total of reported finds is at least the find threshold) of jutterswet-penningjaar:
$ runner --trace corpus/golden/jutterswet-penningjaar/ir.json …/case.json
…
reading /rules/rules/1/versions/0 "J1" 0 Comparison true
reading /rules/rules/1/versions/0 "J1" 1 AttributeReference 14
reading /rules/rules/1/versions/0 "J1" 2 ParameterReference 12
»reading /rules/rules/1/versions/0 "J1" HOLDS
reading /rules/rules/1/versions/0 "J2" 0 Comparison false
reading /rules/rules/1/versions/0 "J2" 1 AttributeReference 3
reading /rules/rules/1/versions/0 "J2" 2 ParameterReference 12
»reading /rules/rules/1/versions/0 "J2" FAILS
…
De Drools-backend maakt op verzoek een build met lezingen: naast de regel van elke vorm met een voorwaarde staat dan een leesregel, en DroolsBackendMain --readings drukt de lezingen af in dezelfde tekst. Gate 17 legt per casusbestand van elke golden de trace van ref naast de lezingen van beide Drools-takken en eist dat ze regel voor regel gelijk zijn; alleen mag de backend bij een lid van een kwantorfold unordered schrijven waar hij geen volgorde bijhoudt. De twee uitvoerders delen geen code (gate 1). Clingo levert geen lezingen.
Foutsoorten en meldingscodes
Hoofdstuk 5.7b van RUNTIME-SEMANTICS geeft elke foutsoort een code EF-###, met haar kanaal, de parameters die het voorval aanwijzen, de clausule die haar definieert en een getuige in het corpus. Er zijn vier kanalen: de case wordt bij het inlezen geweigerd (case input), de sessie breekt af (fault), elke uitvoerder weigert met naam een vorm waaraan de specificatie geen betekenis geeft (declined), of de sessie zet een feit op het foutkanaal en loopt door (§9.5). Een code wordt nooit hergebruikt of hernummerd. Een uitvoerder meldt de code naast de tekst van zijn melding, niet erin; de tekst verschilt per uitvoerder, de code niet. Daarom legt een rode case een code met parameters vast (§ 2), en vergelijkt gate 8 codes en geen tekst.
Elk onderdeel heeft daarnaast een codeschema voor zijn eigen meldingen: WF-###.<slug> voor de verifier (§ 3), RGS-### voor de RegelSpraak-frontend, DROOLS-### voor de vormen die de Drools-backend weigert, en REF-### en CLINGO-### voor de eigen weigeringen en grenzen van ref en Clingo. De laatste vier hebben elk één register: diagnostic-codes.tsv, DroolsCode.java, ref/codes.tsv en backends/clingo/codes.tsv. Gates 8, 10 en 12 houden elk register in beide richtingen gelijk aan de broncode: elke code die de broncode gebruikt, staat in het register, en elke code in het register komt in de broncode voor, op vervallen codes na. De frontend en de Drools-backend halen de tekst van een melding uit een catalogus per taal, Nederlands en Engels; de frontend schrijft standaard Nederlands, de Drools-backend Engels. Ref, verifier en Clingo schrijven alleen Engels.
On request the Drools backend makes a build with readings: beside the rule of every form with a condition there is then a reading rule, and DroolsBackendMain --readings prints the readings in the same text. Gate 17 compares, per case file of every golden, the trace of ref with the readings of both Drools branches and requires them to be equal line by line; the one allowance is that the backend may write unordered for a member of a quantifier fold where it keeps no order. The two executors share no code (gate 1). Clingo gives no readings.
Fault kinds and message codes
Chapter 5.7b of RUNTIME-SEMANTICS gives every fault kind a code EF-###, with its channel, the parameters that identify the occurrence, the clause that defines it and a witness in the corpus. There are four channels: the case is refused while it is read (case input), the session aborts (fault), every executor refuses by name a form to which the specification gives no meaning (declined), or the session puts a fact on the error channel and continues (§9.5). A code is never reused or renumbered. An executor reports the code beside the text of its message, not inside it; the text differs between executors, the code does not. That is why a red case pins a code with parameters (§ 2), and why gate 8 compares codes and not text.
Each component also has a code scheme for its own messages: WF-###.<slug> for the verifier (§ 3), RGS-### for the RegelSpraak frontend, DROOLS-### for the forms the Drools backend refuses, and REF-### and CLINGO-### for the own refusals and limits of ref and Clingo. The last four each have one register: diagnostic-codes.tsv, DroolsCode.java, ref/codes.tsv and backends/clingo/codes.tsv. Gates 8, 10 and 12 hold each register to the source code in both directions: every code the source uses is in the register, and every code in the register occurs in the source, retired codes excepted. The frontend and the Drools backend take the text of a message from a catalogue per language, Dutch and English; the frontend writes Dutch by default, the Drools backend English. Ref, the verifier and Clingo write English only.
De zeventien gates
Een gate is een script in scripts/ dat een eigenschap van de hele repository controleert. De hele set draait lokaal met make gates vóór elke landing (§ 8), en een rode gate houdt de landing tegen. .github/workflows/gates.yml legt de volgorde en de omgeving per gate vast, maar draait alleen als iemand hem met de hand start.
| # | Script | Wat het bewijst |
|---|---|---|
| 1 | check-self-contained.sh | artefactzones zonder lokale paden of verwijzingen naar private repositories; corpusdata zonder TOKA-materiaal buiten de zone toka/; ref en verifier zonder namen uit de productieruntime van Drools |
| 2 | check-ir-schema.sh | elke ir.json in golden en red valideert tegen het schema en komt na inlezen en terugschrijven byte-identiek terug; golden, red en negative staan in de canonieke sleutelvolgorde van het schema |
| 3 | check-verifier.sh | de verifier bouwt, ook los van de rest van de werkruimte; golden en red zijn schoon; elk negatief document wordt afgewezen om de regel uit zijn mapnaam, en met een expect.txt met de vastgelegde sleutel en parameters; elke geciteerde JSON-pointer bestaat; elke sleutel heeft één berichtsjabloon |
| 4 | check-case-schema.sh | elk casusbestand valideert tegen het casusschema, ligt naast een ir.json en staat in de juiste zone: golden met expected, red met expectedFault |
| 5 | check-oracle.sh | de suite van ref slaagt, en ref speelt elke golden case exact na |
| 6 | check-clingo.sh | de Clingo-backend vertaalt de golden cases naar ASP, en het stabiele model geeft dezelfde absolute verwachtingen als ref (ref ≡ Clingo); een golden waarvan de ASP-codering de vorm niet draagt, staat met naam, code CLINGO-### en reden op een lijst en weigert op elke case met die code; de eigen suite van de backend slaagt |
| 7 | check-noop.sh | elke golden case faalt onder runner --noop, op de overlevers na die met reden in het script staan |
| 8 | check-red.sh | elke rode case faalt bij het laden of in de evaluatie als de foutsoort die expectedFault.code noemt, met de vastgelegde parameters: in ref, in Clingo en in Drools, op de cases na die per uitvoerder met reden en met de code van een eigen weigering of grens op een lijst staan; de registers van REF-### en CLINGO-### kloppen met de broncode |
| 9 | check-catala.sh | de Catala-frontend bouwt en zijn suite slaagt; elke golden met een source.catala_en regenereert byte-identiek |
| 10 | check-regelspraak.sh | de RegelSpraak-frontend bouwt en zijn JUnit-suite slaagt; elke golden met een source.rgs her-emitteert byte-identiek; elke melding heeft een code RGS-### uit het register en een tekst in de Nederlandse en de Engelse catalogus; een Nederlandse tekst die gelijk is aan de Engelse of een Engels functiewoord bevat, staat met reden op een lijst |
| 11 | check-toka-parity.sh | gate over de TOKA-casus: de TOKA-tests slagen tegen de frontend, de frontend schrijft de vastgelegde IR byte-identiek opnieuw, elke replay-slice is verifier-schoon, en ref, Clingo en Drools spelen de casusgevallen na |
| 12 | check-drools.sh | de Drools-backend laadt elke golden uit zijn ir.json en speelt de cases op beide takken: een gespeelde golden geeft exact de verwachtingen van gates 5 en 6 (ref ≡ Clingo ≡ Drools), een vorm die de backend niet draagt, weigert hij met een code DROOLS-### en een reden, en de DRL-tekst geeft per golden dezelfde statusregel als het executable model; de JUnit-suite van de backend slaagt; register en catalogi van DROOLS-### kloppen met de broncode |
| 13 | check-reftype-derivation.sh | een opzettelijk gewijzigde eenheid of aantal decimalen in een type-annotatie op een afgeleide positie verandert geen uitkomst in ref, Clingo en beide Drools-takken; evenmin een weggelaten, verwisselde of rondgeschoven tabel met benamingen, een weggelaten origin, of weggelaten namen en bronaantekeningen van defaults |
| 14 | check-flogic.sh | de F-logic-frontend bouwt en zijn suite slaagt; elke golden met een source.flogic her-emitteert byte-identiek |
| 15 | check-graph.sh | de regelgraaf van de Drools-backend is gelijk aan de afhankelijkheden die de verifier volgens IR-SPEC hoofdstuk 5 afleidt; bij een golden met source.rgs is de graaf uit de RegelSpraak-bron gelijk aan die uit ir.json |
| 16 | check-scenario.sh | elke golden met een scenario.rgs regenereert zijn vastgepinde casebestanden en ir.json byte-identiek |
| 17 | check-readings.sh | de trace van ref (runner --trace) en de lezingen van beide Drools-takken zijn per casusbestand van elke golden gelijk, in de canonieke tekst van RUNTIME-SEMANTICS hoofdstuk 5.12 (§ 4) |
Elke gate waarvan een telling ongemerkt kan krimpen, draagt in zijn script een ondergrens EXPECTED_MIN_<naam>, met reden en meetdatum, en faalt met het gemeten getal en de ondergrens; een gate zonder ondergrens zegt in zijn script waarom. De waarden staan in de scripts en in de gatelijst van CLAUDE.md. Gate 9 vraagt een lokale catala-installatie; zonder catala faalt het script, dus het draait op een machine die catala heeft.
Regels voor de gate-set
The seventeen gates
A gate is a script in scripts/ that checks a property of the whole repository. The full set runs locally with make gates before every landing (§ 8), and a red gate blocks the landing. .github/workflows/gates.yml records the order and the environment per gate, but runs only when someone starts it by hand.
| # | Script | What it proves |
|---|---|---|
| 1 | check-self-contained.sh | artifact zones free of local paths and references to private repositories; corpus data free of TOKA material outside the toka/ zone; ref and verifier free of names from the Drools production runtime |
| 2 | check-ir-schema.sh | every golden and red ir.json validates against the schema and round-trips byte-identically; golden, red and negative follow the schema's canonical key order |
| 3 | check-verifier.sh | the verifier builds, also apart from the rest of the workspace; golden and red are clean; every negative document is rejected for the rule in its directory name, and with an expect.txt with the pinned key and parameters; every cited JSON pointer resolves; every key has one message template |
| 4 | check-case-schema.sh | every case file validates against the case schema, sits next to an ir.json and is in the right zone: golden with expected, red with expectedFault |
| 5 | check-oracle.sh | ref's suite passes, and ref replays every golden case exactly |
| 6 | check-clingo.sh | the Clingo backend translates the golden cases into ASP, and the stable model gives the same absolute expectations as ref (ref ≡ Clingo); a golden whose form the ASP encoding does not carry is listed by name, with a code CLINGO-### and a reason, and refuses on every case with that code; the backend's own suite passes |
| 7 | check-noop.sh | every golden case fails under runner --noop, except the survivors listed with a reason in the script |
| 8 | check-red.sh | every red case fails at load or in evaluation as the fault kind expectedFault.code names, with the pinned parameters: in ref, in Clingo and in Drools, except the cases listed per executor with a reason and the code of a refusal or limit of its own; the registers of REF-### and CLINGO-### match the source code |
| 9 | check-catala.sh | the Catala frontend builds and its suite passes; every golden with a source.catala_en regenerates byte-identically |
| 10 | check-regelspraak.sh | the RegelSpraak frontend builds and its JUnit suite passes; every golden with a source.rgs re-emits byte-identically; every message has a code RGS-### from the register and a text in the Dutch and the English catalogue; a Dutch text that equals the English one or contains an English function word is listed with a reason |
| 11 | check-toka-parity.sh | gate over the TOKA casus: the TOKA tests pass against the frontend, the frontend re-emits the recorded IR byte-identically, every replay slice is verifier-clean, and ref, Clingo and Drools replay the cases |
| 12 | check-drools.sh | the Drools backend loads every golden from its ir.json and plays the cases on both branches: a played golden gives exactly the expectations of gates 5 and 6 (ref ≡ Clingo ≡ Drools), a form the backend does not carry is refused with a code DROOLS-### and a reason, and the DRL text gives the same status line per golden as the executable model; the backend's JUnit suite passes; the register and catalogues of DROOLS-### match the source code |
| 13 | check-reftype-derivation.sh | a deliberately altered unit or number of decimals in a type annotation on a derived position changes no outcome in ref, Clingo and both Drools branches; nor does a removed, exchanged or rotated table of designations, a removed origin, or removed names and statute annotations of defaults |
| 14 | check-flogic.sh | the F-logic frontend builds and its suite passes; every golden with a source.flogic re-emits byte-identically |
| 15 | check-graph.sh | the Drools backend's rule graph equals the dependency edges the verifier derives per IR-SPEC chapter 5; for a golden with a source.rgs, the graph built from the RegelSpraak source equals the one built from ir.json |
| 16 | check-scenario.sh | every golden with a scenario.rgs regenerates its pinned case files and ir.json byte-identically |
| 17 | check-readings.sh | the trace of ref (runner --trace) and the readings of both Drools branches are equal per case file of every golden, in the canonical text of RUNTIME-SEMANTICS chapter 5.12 (§ 4) |
Every gate whose count could shrink unseen carries a floor EXPECTED_MIN_<name> in its script, with its reason and measurement date, and fails naming the measured number and the floor; a gate without a floor says in its script why. The values are in the scripts and in the gate list of CLAUDE.md. Gate 9 needs a local catala installation; without catala the script fails, so it runs on a machine that has catala.
Rules for the gate set
The gate set grows monotonically; a green subset is not green, an empty input set is a failure, and counts carry explicit floors so shrinkage cannot pass silently.README.md
… every gate whose count could shrink unseen carries an `EXPECTED_MIN_<name>` constant in its own script, with its reason and its measurement date, and fails naming both the measured number and the floor. They are lower bounds, not equalities: they catch shrink — a suite that quietly runs half, a corpus zone that quietly halves — while growth needs no bump, so an ordinary corpus card touches no protected script. Raising a floor is a deliberate act, done at an occasion; a deliberate removal lowers it in the same commit. The accepted weakness, recorded rather than hidden: a count that stays equal while its content shifted passes, and the name-carrying lists are what cover that direction. Gate 12's `EXPECTED_PLAYED` stays an equality … A gate that gets no floor carries one sentence in its script saying why.
- Een lege zone laat een gate falen; bij gate 2 doet zijn ondergrens dat, bij gate 4 doen de ondergrenzen van gates 5 en 8 het.
- Lijsten op naam, met reden, in gates 6, 7, 8, 10, 11, 12, 13, 15 en 17 falen zodra een vermelding verouderd is of naar een verdwenen case wijst.
Een ondergrens in een script, met haar meetdatum. De zone bevat meer documenten; de ondergrens vangt krimp, geen groei:
- An empty zone makes a gate fail; for gate 2 its floor does this, for gate 4 the floors of gates 5 and 8.
- Lists by name, with reasons, in gates 6, 7, 8, 10, 11, 12, 13, 15 and 17 fail as soon as an entry is out of date or points to a case that is gone.
A floor in a script, with its measurement date. The zone holds more documents; the floor catches shrinkage, not growth:
EXPECTED_MIN_NEGATIVE=568 # 2026-11-02: measured 568 (corpus/negative/**/ir.json), card dimensions-executed, the fifteen witnesses of WF-040, WF-079 and WF-125 on cells; 350 on 2026-09-10
De routes van regels naar Drools
uchar vertaalt elke regel één keer en geeft die analyse aan Drools in twee vormen: als DRL-tekst, die Drools zelf parseert, en rechtstreeks als executable model via de PatternDSL-API, zonder gegenereerde Java-bron. Een frontend die LLVM-IR maakt, heeft dezelfde keuze: de IR als tekst schrijven (.ll) of haar met IRBuilder in het geheugen opbouwen. De backend gebruikt Drools 8.44.0.Final, de versie die backends/drools/pom.xml vastlegt.
The routes from rules to Drools
uchar translates every rule once and hands that analysis to Drools in two forms: as DRL text, which Drools parses itself, and directly as an executable model through the PatternDSL API, without generated Java source. A frontend that produces LLVM IR has the same choice: write the IR as text (.ll) or build it in memory with IRBuilder. The backend uses Drools 8.44.0.Final, the version backends/drools/pom.xml pins.
DRL_TEXT door de DRL-pijplijn en EXEC_MODEL rechtstreeks naar het executable model. Grijs gestippeld: Drools' eigen codegen, die uchar niet gebruikt. Bij een pijl staat de klasse die de stap zet; de onderste regel noemt wie welke tak gebruikt.DRL_TEXT through the DRL pipeline and EXEC_MODEL directly to the executable model. Grey and dotted: Drools' own codegen, which uchar does not use. Beside an arrow is the class that takes the step; the bottom line names who uses which branch.- analyse
IrLoaderlaadtir.jsonin eenRegelSpraakModel.TmsTranspilermaakt daaruit per regel een plan (RulePlan): de linkerkant in volgorde (patronen, negaties, accumulates en toetsen) en de consequentie. Een afgeleid kenmerk of een afgeleide waarde voegt de consequentie logisch in, en alles wat de afleiding leest, staat als patroon in de linkerkant, zodat Drools haar intrekt als dat verandert. Beide weergaven nemen de plannen (samen hetModelplan) over zonder zelf iets te beslissen; wat de analyse weigert, weigeren beide takken met dezelfde zin.- feitklassen
FactModelGeneratorschrijft per objecttype een Java-klasse (dev.uchar.regelspraak.facts), enBeanCompilercompileert die in het geheugen metjavax.tools, één keer per inhoud per JVM. De analyse laadt de beanklassen uit deClassLoaderdie dat oplevert, en de DRL-build krijgt hem metKieHelper.setClassLoader; beide takken werken dus met dezelfde klassen. Een relatie met twee meervoudige rollen heeft geen veld op een bean en staat, zoals een relatie die een vorm maakt, alsRelationLink-feiten in de sessie.- DRL_TEXT
DrlRenderingschrijft hetModelplanals DRL-tekst (TmsTranspiler.drl()): regels in het dialect MVEL, elke toets en berekening als Java-functionmet de RegelSpraak-zin als commentaar erboven.KieHelpergeeft de tekst aan de DRL-pijplijn van Drools:DrlParsermaakt er eenPackageDescrvan, enKnowledgeBuilderImplbouwt daaruitRuleImpl-objecten met MVEL-voorwaarden (MVELConstraint) en compileert de functies. Een tekst die Drools weigert, is een fout van de weergave (IllegalStateException), geen weigering van een vorm.- EXEC_MODEL
PatternDslRenderingroept per plan de PatternDSL aan (PatternDSL.rule(…).build(…),pattern(…).expr(…).bind(…)) en verzamelt de regels in eenModelImpl, eenorg.drools.model.Model; voorwaarden en berekeningen zijn lambda's dieClosureRenderinguit het plan samenstelt.KieBaseBuilder.createKieBaseFromModelgeeft hetModelaanKiePackagesBuilder, die erRuleImpl-objecten met lambda-voorwaarden (LambdaConstraint) van maakt. De build schrijft voor de regels geen tekst en geen Java-bron.- RuleImpl, KieBase
- Beide takken eindigen in
RuleImpl-objecten in eenInternalKnowledgePackage(drools-base). DeKieBasezet elkeRuleImplmetReteooBuilder.addRuleom in knopen van het netwerk (org.drools.core.reteoo), en eenKieSessionevalueert dat netwerk met de klassen inorg.drools.core.phreak(RuleNetworkEvaluator,PhreakJoinNode, …). - codegen van Drools
- Drools kan ook zelf van DRL een executable model maken:
ModelGeneratorschrijft uit eenPackageDescrJava-bron met PatternDSL-aanroepen, die bij de Maven-build van het project wordt gecompileerd, of tijdens het draaien metKieBuilder.buildAll(ExecutableModelProject.class). uchar gebruikt die codegen niet. - afnemers
- Na de build behandelt
DroolsBackendbeide takken gelijk: dezelfde regels onder dezelfde namen, dezelfde feiten en dezelfde driver (insert,fire,fixpointOf,setDerivedValues). De beslisservice van regelspraak-toolchain (imagedecision-service) bouwt opEXEC_MODEL;bin/regelspraak drlen de playground van de dienst tonen de tekst vanDRL_TEXT. Gates 12, 13 en 17 spelen het corpus af op beide takken, en gate 12 eist per golden dezelfde statusregel (gates).
- analysis
IrLoaderloadsir.jsoninto aRegelSpraakModel.TmsTranspilermakes a plan of it per rule (RulePlan): the left-hand side in order (patterns, negations, accumulates and tests) and the consequence. The consequence inserts a derived characteristic or a derived value logically, and everything the derivation reads is a pattern in the left-hand side, so Drools retracts the derivation when that changes. Both renderings take over the plans (together theModelplan) without deciding anything themselves; what the analysis refuses, both branches refuse in the same sentence.- fact classes
FactModelGeneratorwrites a Java class per object type (dev.uchar.regelspraak.facts), andBeanCompilercompiles them in memory withjavax.tools, once per content per JVM. The analysis loads the bean classes from the resultingClassLoader, and the DRL build gets it throughKieHelper.setClassLoader; both branches therefore work with the same classes. A relation with two to-many roles has no field on a bean and stands, like a relation a form creates, in the session asRelationLinkfacts.- DRL_TEXT
DrlRenderingwrites theModelplanas DRL text (TmsTranspiler.drl()): rules in the MVEL dialect, every test and computation as a Javafunctionwith the RegelSpraak sentence as a comment above it.KieHelperhands the text to Drools' DRL pipeline:DrlParserturns it into aPackageDescr, andKnowledgeBuilderImplbuildsRuleImplobjects with MVEL constraints (MVELConstraint) from it and compiles the functions. A text that Drools rejects is a fault of the rendering (IllegalStateException), not a refusal of a form.- EXEC_MODEL
PatternDslRenderingcalls the PatternDSL per plan (PatternDSL.rule(…).build(…),pattern(…).expr(…).bind(…)) and collects the rules in aModelImpl, anorg.drools.model.Model; constraints and computations are lambdas thatClosureRenderingassembles from the plan.KieBaseBuilder.createKieBaseFromModelhands theModeltoKiePackagesBuilder, which makesRuleImplobjects with lambda constraints (LambdaConstraint) of it. The build writes no text and no Java source for the rules.- RuleImpl, KieBase
- Both branches end in
RuleImplobjects in anInternalKnowledgePackage(drools-base). TheKieBaseturns everyRuleImplinto nodes of the network withReteooBuilder.addRule(org.drools.core.reteoo), and aKieSessionevaluates that network with the classes inorg.drools.core.phreak(RuleNetworkEvaluator,PhreakJoinNode, …). - Drools' codegen
- Drools can also make an executable model from DRL itself:
ModelGeneratorwrites Java source with PatternDSL calls from aPackageDescr, compiled in the project's Maven build or at run time withKieBuilder.buildAll(ExecutableModelProject.class). uchar does not use that codegen. - consumers
- After the build,
DroolsBackendtreats both branches alike: the same rules under the same names, the same facts and the same driver (insert,fire,fixpointOf,setDerivedValues). The decision service of regelspraak-toolchain (imagedecision-service) builds onEXEC_MODEL;bin/regelspraak drland the service's playground show the text ofDRL_TEXT. Gates 12, 13 and 17 replay the corpus on both branches, and gate 12 demands the same status line per golden (gates).
Vragen over de routes
Het executable model wordt in Drools toch als Java-code gegenereerd? Waarom is er van de exec-model-tak geen tekst te lezen?
Dat doet Drools' eigen codegen. De exec-model-tak gebruikt die niet: PatternDslRendering roept de PatternDSL aan terwijl de backend het document bouwt, en het Model bestaat alleen in het geheugen. Voor de regels wordt geen DRL en geen Java-bron geschreven. Te lezen is de DRL-tekst van dezelfde analyse: build.execModelTranspiler().drl() geeft haar op een build van elke tak, en bin/regelspraak drl <bron.rgs> van de distributie drukt haar af. Per regel geeft drlPieces() het stuk tekst met zijn functies, en originOf(regel) de herkomst: de vorm, de soort, de rol en bij een default de JSON-pointer in het IR-document.
Wat is de PatternDSL?
De Java-API van Drools waarmee een programma een regel zonder tekst opbouwt: de klassen PatternDSL en DSL in het pakket org.drools.model (drools-canonical-model). Een regel bestaat uit variabelen (declarationOf), patronen met voorwaarden en bindingen (pattern(…).expr(…).bind(…)), losse toetsen (expr) en een consequentie (on(…).execute(…)); voorwaarden en consequentie zijn lambda's. PatternDSL.rule(pakket, naam).build(…) geeft een Rule, en een ModelImpl verzamelt regels en globals tot een Model.
Hieronder één regel uit het corpus in drie vormen: de RegelSpraak-bron, de DRL-tekst die de backend ervan maakt, en dezelfde regel met de PatternDSL. De derde is zelf opgesteld. PatternDslRendering bouwt dezelfde onderdelen uit het plan, met eigen namen voor de voorwaarden, toetsen over een rij waarden en een consequentie over een willekeurig aantal variabelen, en schrijft er geen tekst van.
Questions about the routes
Isn't Drools' executable model generated as Java code? Why is there no text to read of the exec-model branch?
That is what Drools' own codegen does. The exec-model branch does not use it: PatternDslRendering calls the PatternDSL while the backend builds the document, and the Model exists only in memory. No DRL and no Java source is written for the rules. What can be read is the DRL text of the same analysis: build.execModelTranspiler().drl() gives it on a build of either branch, and the distribution's bin/regelspraak drl <source.rgs> prints it. Per rule, drlPieces() gives the piece of text with its functions, and originOf(rule) the origin: the form, the kind, the role and, for a default, the JSON pointer into the IR document.
What is the PatternDSL?
The Java API of Drools with which a program builds a rule without text: the classes PatternDSL and DSL in the package org.drools.model (drools-canonical-model). A rule consists of variables (declarationOf), patterns with constraints and bindings (pattern(…).expr(…).bind(…)), standalone tests (expr) and a consequence (on(…).execute(…)); constraints and consequence are lambdas. PatternDSL.rule(package, name).build(…) gives a Rule, and a ModelImpl collects rules and globals into a Model.
Below is one rule from the corpus in three forms: the RegelSpraak source, the DRL text the backend makes of it, and the same rule written with the PatternDSL. The third is hand-written. PatternDslRendering builds the same parts from the plan, with its own names for the constraints, tests over an array of values and a consequence over any number of variables, and writes no text of it.
Regel penninggerechtigd tenzij uitgesloten
geldig altijd
Een Jutter is penninggerechtigd
» indien hij niet uitgesloten is.
»// hij niet uitgesloten is
function boolean toets_penninggerechtigdTenzijUitgesloten(Object $uitgesloten) {
return ComputeHelpers.characteristic($uitgesloten, Boolean.FALSE);
}
rule "penninggerechtigd tenzij uitgesloten"
dialect "mvel"
when
$onderwerp : dev.uchar.regelspraak.facts.Jutter()
$stand_0 : CharacteristicState( characteristic == ElementKey.of("jutter", "uitgesloten", true), subject == $onderwerp, $uitgesloten : holds )
eval( toets_penninggerechtigdTenzijUitgesloten($uitgesloten) )
then
» insertLogical(new HasCharacteristic($onderwerp, ElementKey.of("jutter", "penninggerechtigd", true)));
end
import static org.drools.model.DSL.*;
import static org.drools.model.PatternDSL.*;
Class<Object> jutter = (Class<Object>) build.beanLoader().loadClass("dev.uchar.regelspraak.facts.Jutter");
Variable<Object> onderwerp = declarationOf(jutter, "$onderwerp");
Variable<CharacteristicState> stand = declarationOf(CharacteristicState.class, "$stand_0");
Variable<Boolean> uitgesloten = declarationOf(Boolean.class, "$uitgesloten");
Rule r = rule("dev.uchar.regelspraak.tms", "penninggerechtigd tenzij uitgesloten").build(
pattern(onderwerp),
pattern(stand)
.expr("kenmerk", s -> s.characteristic().equals(ElementKey.of("jutter", "uitgesloten", true)))
.expr("onderwerp", onderwerp, (s, o) -> s.subject() == o,
betaIndexedBy(Object.class, Index.ConstraintType.EQUAL, 0, CharacteristicState::subject, o -> o))
.bind(uitgesloten, CharacteristicState::holds),
expr("toets", uitgesloten, u -> ComputeHelpers.characteristic(u, false)),
» on(onderwerp).execute((drools, o) ->
» drools.logicalInsert(new HasCharacteristic(o, ElementKey.of("jutter", "penninggerechtigd", true)))));
De regel leest uitgesloten, een kenmerk dat andere regels afleiden. Hij leest daarom niet het veld van de bean, maar het feit CharacteristicState, dat er per object en kenmerk precies één keer is en de afleiding draagt, of anders de invoer. Wat hij afleidt, voegt hij logisch in (insertLogical in DRL, logicalInsert in de PatternDSL); Drools trekt het in als de stand van uitgesloten verandert.
Hoe kunnen regels draaien zonder dat er per regelset Java wordt gecompileerd?
In de exec-model-tak zijn de lambda's gecompileerd met de backend zelf. Een lambda is een object: gecompileerde code plus de waarden die het bij het aanmaken meekrijgt. Per regelset maakt ClosureRendering eigen exemplaren, met de constanten en de posities uit het plan. Twee dingen worden wel per regelset gecompileerd: de feitklassen, die BeanCompiler in het geheugen compileert, één keer per inhoud per JVM; en in de DRL-teksttak de Java-functies van de tekst, die de DRL-build van Drools compileert.
Waarom bouwt de backend het model rechtstreeks, en genereert hij geen DRL of Java?
Het rechtstreeks gebouwde Model verschilt hierin van de DRL-pijplijn en van Drools' eigen codegen:
- Er is geen tekststap. Voorwaarden zijn gecompileerde Java-lambda's (
LambdaConstraint), geen MVEL-expressies die Drools uit tekst opbouwt. - De lambda's roepen de rekenhulpen in
RegelSpraakRuntimeenComputeHelpersrechtstreeks aan. De DRL-tekst roept dezelfde methoden aan via hun klasse, zodat beide takken met dezelfde code rekenen. - Een veld van een bean leest de exec-model-tak via een getypeerde getter (
Accessors, eenMethodHandle), zonder reflectie. - De beslisservice krijgt de regels bij het starten, als bronnen en descriptor, en niet bij de Maven-build. Drools' eigen codegen zou ze dan bij het starten in Java-bron omzetten en die compileren (
ExecutableModelProject);PatternDslRenderingbouwt hetModelzonder die stap.
De DRL-teksttak is de tweede weergave van dezelfde analyse: gate 12 eist per golden dezelfde statusregel als op de exec-model-tak, en de tekst is wat mensen lezen. De vuurtijd van die tak is op het schaalmodel (ScaleTrial) 1,8 tot 2,3 keer die van het executable model.
Waarom Drools?
uchar gaat uit van een werkend paar: RegelSpraak en een compiler van RegelSpraak naar Drools, de toolchain regelspraak-java (README.md van uchar, "Why these languages and engines"). Drools is een productieregel-engine op de JVM. De referentie-interpreter en Clingo delen geen code met de productieruntime en toetsen dat de IR geen aannames van Drools overneemt.
Waarom gebruikt de Drools-backend geen NO_LOOP en geen salience, en wel een vuurplafond?
Beide takken leiden af met truth maintenance. Een afgeleid kenmerk of een afgeleide waarde is een feit dat een regel logisch invoegt; de invoer blijft op de beans, en tijdens het vuren schrijft geen regel een bean en roept geen regel update aan. Elke waarde waarop een afleiding rust, is een patroon in de linkerkant van de regel; verandert ze, dan trekt Drools in wat erop rustte. Een vertaling die velden van beans overschrijft, heeft maatregelen nodig tegen de reactiviteit van de engine zelf: NO_LOOP op een regel die zijn eigen onderwerp schrijft, salience uit de stratificatie, update alleen bij een gewijzigde waarde. Deze vertaling heeft die niet nodig; de DRL-tekst bevat geen no-loop, salience, modify of update.
Agendagroepen gebruikt de backend wel, voor de creaties. Een object- of feitcreatie vuurt alleen aan een fixpunt van de regellaag. Haar regels staan in de groepen uchar-feitcreatie en uchar-objectcreatie, en DroolsBackend.fire vuurt per stratum in generaties: eerst de regellaag tot rust, dan per generatie een fase waarin het die groepen de focus geeft.
Het vuurplafond is max(10 000, 4 × R × N0), met R het aantal regels van de KieBase en N0 het aantal feiten vlak voor het vuren. fire berekent het opnieuw aan het begin van elke creatiefase en verlaagt het nooit. Het plafond begrenst een fout in de vertaling en is geen norm: de build weigert een kring in de afhankelijkheidsgraaf (Cycles), en een keten van creaties eindigt bij het creatiebudget, waar een creatie op diepte 101 een fout is, zoals in ref en Clingo. Een run die het plafond haalt, heeft geen fixpunt; fixpointOf en setDerivedValues weigeren zo'n sessie.
Hoe weet je dat de exec-model-tak klopt als je haar regels niet als tekst kunt lezen?
Gate 12 speelt elke golden af op beide takken, tegen dezelfde vastgelegde uitkomsten die gate 5 (ref) en gate 6 (Clingo) controleren. De goldens in corpus/golden/anchor zijn met de hand berekend, dus overeenstemming tussen uitvoerders geldt niet op zichzelf als juist. De DRL-teksttak moet per golden dezelfde statusregel geven als de exec-model-tak, en BranchDumpsCorpusWitnessTest in de suite van de backend vergelijkt per case van beide takken de invoer, wat fixpointOf geeft, de feiten aan het fixpunt en de uitkomst van het vuren. Gate 13 verdraait type-annotaties en laat velden zonder semantiek weg, en eist op beide takken dezelfde uitkomst. Gate 17 houdt de lezingen van beide takken gelijk aan de trace van ref (lezingen).
Gebruikt de backend truth maintenance?
Ja, op beide takken. Een afgeleid kenmerk is een HasCharacteristic, een afgeleide waarde een Value met haar schrijver, beide logisch ingevoegd; een evaluatiefout is ook een logisch feit (Fault). De KieBase draait in EqualityBehaviorOption.EQUALITY: een gesteld feit neemt een gelijk logisch feit over, en gelijke logische feiten zijn één feit met een rechtvaardiging per regel. Eén soort vorm voegt gesteld in en niet logisch: de object- en feitcreatie. Ze vuurt alleen aan een fixpunt van de regellaag, en wat ze maakt, met de toekenningen en de koppelingen, blijft staan, zoals in de referentie-interpreter (RUNTIME-SEMANTICS.md, hoofdstuk 5.5). Daarom vuurt fire een sessie van een model met een creatie één keer; voor andere invoer opent een afnemer een verse sessie.
Waarom drie uitvoerders?
Dat staat op het overzicht, onder Drie uitvoerders, één antwoord. De referentie-interpreter is de uitvoerbare definitie van RUNTIME-SEMANTICS.md en deelt geen code met de productieruntime van Drools; Clingo rekent met stable models, en een aanname van Drools in de IR geeft een fout in de vertaling naar ASP; Drools is de productie-engine waar uchar van uitgaat. Zijn een backend en ref het oneens, dan beslist de specificatie en niet een meerderheid van uitvoerders (backends/README.md).
The rule reads uitgesloten, a characteristic that other rules derive. It therefore reads not the bean's field but the fact CharacteristicState, of which there is exactly one per object and characteristic, carrying the derivation or else the input. What it derives it inserts logically (insertLogical in DRL, logicalInsert in the PatternDSL); Drools retracts it when the standing of uitgesloten changes.
How can rules run without compiling Java per rule set?
In the exec-model branch the lambdas are compiled with the backend itself. A lambda is an object: compiled code plus the values it is given when it is created. Per rule set, ClosureRendering creates instances of its own, with the constants and the positions from the plan. Two things are compiled per rule set: the fact classes, which BeanCompiler compiles in memory, once per content per JVM; and in the DRL-text branch the Java functions of the text, which Drools' DRL build compiles.
Why does the backend build the model directly instead of generating DRL or Java?
The directly built Model differs from the DRL pipeline and from Drools' own codegen in these points:
- There is no text step. Constraints are compiled Java lambdas (
LambdaConstraint), not MVEL expressions that Drools builds from text. - The lambdas call the computation helpers in
RegelSpraakRuntimeandComputeHelpersdirectly. The DRL text calls the same methods through their class, so both branches compute with the same code. - The exec-model branch reads a field of a bean through a typed getter (
Accessors, aMethodHandle), without reflection. - The decision service gets the rules at start-up, as sources and a descriptor, not at the Maven build. Drools' own codegen would then turn them into Java source at start-up and compile it (
ExecutableModelProject);PatternDslRenderingbuilds theModelwithout that step.
The DRL-text branch is the second rendering of the same analysis: gate 12 demands the same status line per golden as on the exec-model branch, and the text is what people read. The fire time of that branch is 1.8 to 2.3 times that of the executable model on the scale model (ScaleTrial).
Why Drools?
uchar starts from a working pair: RegelSpraak and a compiler from RegelSpraak to Drools, the toolchain regelspraak-java (uchar's README.md, "Why these languages and engines"). Drools is a production rule engine on the JVM. The reference interpreter and Clingo share no code with the production runtime and test that the IR takes over no assumptions of Drools.
Why does the Drools backend use no NO_LOOP and no salience, but a fire cap?
Both branches derive with truth maintenance. A derived characteristic or a derived value is a fact that a rule inserts logically; the input stays on the beans, and while the session fires no rule writes a bean or calls update. Every value a derivation rests on is a pattern in the rule's left-hand side; when it changes, Drools retracts what rested on it. A lowering that overwrites fields of beans needs measures against the engine's own reactivity: NO_LOOP on a rule that writes its own subject, salience from the stratification, update only on a changed value. This lowering needs none of them; the DRL text contains no no-loop, salience, modify or update.
The backend does use agenda groups, for the creations. An object or fact creation fires only at a fixpoint of the rule layer. Its rules sit in the groups uchar-feitcreatie and uchar-objectcreatie, and DroolsBackend.fire fires per stratum in generations: first the rule layer to rest, then per generation a phase in which it gives those groups the focus.
The fire cap is max(10 000, 4 × R × N0), with R the number of rules of the KieBase and N0 the number of facts just before the fire. fire computes it again at the start of every creation phase and never lowers it. The cap bounds a fault of the lowering and is not a norm: the build refuses a cycle in the dependency graph (Cycles), and a chain of creations ends at the creation budget, where a creation at depth 101 is a fault, as in ref and Clingo. A run that reaches the cap has no fixpoint; fixpointOf and setDerivedValues refuse such a session.
How do you know the exec-model branch is right when its rules cannot be read as text?
Gate 12 replays every golden on both branches, against the same pinned outcomes that gate 5 (ref) and gate 6 (Clingo) check. The goldens in corpus/golden/anchor are computed by hand, so agreement between executors does not by itself count as correct. The DRL-text branch must give the same status line per golden as the exec-model branch, and BranchDumpsCorpusWitnessTest in the backend's suite compares per case of both branches the input, what fixpointOf gives, the facts at the fixpoint and the outcome of the fire. Gate 13 twists type annotations and leaves out fields without semantics, and demands the same outcome on both branches. Gate 17 holds the readings of both branches equal to ref's trace (readings).
Does the backend use truth maintenance?
Yes, on both branches. A derived characteristic is a HasCharacteristic, a derived value a Value with its writer, both inserted logically; an evaluation fault is a logical fact too (Fault). The KieBase runs in EqualityBehaviorOption.EQUALITY: a stated fact takes over an equal logical fact, and equal logical facts are one fact with a justification per rule. One kind of form inserts stated facts, not logical ones: the object and fact creation. It fires only at a fixpoint of the rule layer, and what it makes, with its member assignments and links, stays, as in the reference interpreter (RUNTIME-SEMANTICS.md, chapter 5.5). That is why fire fires a session of a model with a creation once; for other input a consumer opens a fresh session.
Why three executors?
That is on the overview page, under Three executors, one answer. The reference interpreter is the executable definition of RUNTIME-SEMANTICS.md and shares no code with Drools' production runtime; Clingo computes with stable models, and a Drools assumption in the IR gives an error in the translation to ASP; Drools is the production engine uchar starts from. When a backend and ref disagree, the specification decides, not a majority of executors (backends/README.md).
De corpuszones
Het corpus is het uitvoerbare testmateriaal van de specificatie (herkomst: zie het overzicht). Stand op 7 november 2026:
| Zone | Inhoud | Omvang |
|---|---|---|
golden/ | ir.json, cases met verwachte waarden, soms een bronbestand dat de IR byte-identiek her-emitteert | 365 mappen |
golden/anchor/ | handgeschreven cases met absolute verwachte waarden; beschermd pad | 42 mappen |
negative/ | per map één ir.json dat de regel uit de mapnaam schendt | 639 mappen, 118 WF-regels |
red/ | verifier-schone documenten met expectedFault | 65 mappen met 87 cases; bij 50 mappen toetst gate 8 ook de foutcode van Drools, de andere 15 staan met reden op een lijst |
Golden en anker samen: 407 ir.json en 1135 casusbestanden.
Anker, rood en toelating
The corpus zones
The corpus is the specification's executable test material (provenance: see the overview). Figures as of 7 November 2026:
| Zone | Contents | Size |
|---|---|---|
golden/ | ir.json, cases with expected values, sometimes a source file that re-emits the IR byte-identically | 365 directories |
golden/anchor/ | hand-written cases with absolute expected values; protected path | 42 directories |
negative/ | per directory one ir.json that violates the rule in the directory name | 639 directories, 118 WF rules |
red/ | verifier-clean documents with expectedFault | 65 directories with 87 cases; for 50 directories gate 8 also checks the fault code of Drools, the other 15 are listed with a reason |
Golden and anchor together: 407 ir.json files and 1135 case files.
Anchor, red cases and admission
Hand-written golden cases with **absolute expected values**, permanently pinned. This subset is the parity-independent ground truth of the differential square (`spec/PLAN.md` §8): backend agreement is never accepted as truth by itself — every expectation here is computed by hand from the normative semantics (`spec/RUNTIME-SEMANTICS.md`), and no emitter, interpreter, or solver output is ever pasted back in.
Een rode case moet waarneembaar falen, bij het laden of in de evaluatie, met de vastgelegde code en parameters: in ref, in Clingo en in Drools. Dat geldt ook voor WF-044 (Unresolved, foutsoort EF-019). Uitzonderingen staan met reden op de lijsten van gate 8: een case waarvan Drools de vorm niet draagt, met de code van zijn weigering, en een case waarop ref of Clingo om een eigen reden faalt, met die code. Een fout die alleen de vangnetcode van een uitvoerder draagt, de code van de laatste tak van een dispatch, telt als een andere code dan de vastgelegde. Golden bevat alleen expected, red alleen expectedFault.
Wie een case met de hand schrijft, rekent de verwachte waarden eerst zelf uit; ref bevestigt ze daarna. Gesynthetiseerde cases (golden/synth*, 33 mappen) komen alleen binnen via scripts/admit.sh, volgens CLAUDE.md "only when the full differential square agrees", dus alleen als ref, Clingo en Drools op dezelfde uitkomst uitkomen; elk verschil gaat naar adversariële triage, "never a case to admit".
A red case must fail observably, at load or in evaluation, with the pinned code and parameters: in ref, in Clingo and in Drools. This holds for WF-044 too (Unresolved, fault kind EF-019). Exceptions are listed with a reason on the lists of gate 8: a case whose form Drools does not carry, with the code of its refusal, and a case on which ref or Clingo fails for a reason of its own, with that code. A fault that carries only an executor's fallback code, the code of the last arm of a dispatch, counts as a code other than the pinned one. Golden contains only expected, red only expectedFault.
Whoever writes a case by hand computes its expected values first; ref then confirms them. Synthesized cases (golden/synth*, 33 directories) enter only through scripts/admit.sh, in the words of CLAUDE.md "only when the full differential square agrees", that is, only when ref, Clingo and Drools reach the same outcome; any disagreement goes to adversarial triage, "never a case to admit".
Lokaal draaien
De scripts vragen een git-checkout; de overige eisen hangen per gate af (versies alleen waar vastgelegd):
- OCaml
- OCaml 5 met opam en dune:
opam install dune yojson zarith - Python 3
- voor een groot deel van de gates; gates 2 en 4 vragen daarbij
jsonschemaofuv - clingo
- op Ubuntu uit het pakket
gringo - catala
- alleen voor gate 9
- JDK
- 21 of hoger, voor gates 10 tot en met 13 en 15 tot en met 17, en voor het Drools-been van gate 8, dat zonder Java met een melding wordt overgeslagen
- ANTLR, JUnit
antlr-4.13.2-complete.jarenjunit-platform-console-standalone-1.10.2.jar, die de Makefile van de frontend infrontends/regelspraak/lib/zet- Maven
- voor de Drools-backend (Drools 8.44.0.Final). Gates 8, 11, 12, 13, 15 en 17 nemen de jar zoals hij is wanneer hij nieuwer is dan elke bron, en bouwen hem anders opnieuw, offline (
mvn -o); de eerste keer is daarom één onlinemvn -B -f backends/drools/pom.xml packagenodig - make
- GNU make 3.81 of nieuwer, voor
make gates
De hele set draait met één commando in de root van de repository:
Running it locally
The scripts need a git checkout; the other requirements vary per gate (versions only where pinned):
- OCaml
- OCaml 5 with opam and dune:
opam install dune yojson zarith - Python 3
- for many of the gates; gates 2 and 4 also need
jsonschemaoruv - clingo
- on Ubuntu from the
gringopackage - catala
- only for gate 9
- JDK
- 21 or later, for gates 10 to 13 and 15 to 17, and for the Drools leg of gate 8, which is skipped with a message when Java is missing
- ANTLR, JUnit
antlr-4.13.2-complete.jarandjunit-platform-console-standalone-1.10.2.jar, which the frontend's Makefile places infrontends/regelspraak/lib/- Maven
- for the Drools backend (Drools 8.44.0.Final). Gates 8, 11, 12, 13, 15 and 17 take the jar as it is when it is newer than every source, and rebuild it otherwise, offline (
mvn -o); the first time therefore needs one onlinemvn -B -f backends/drools/pom.xml package - make
- GNU make 3.81 or later, for
make gates
The full set runs with one command in the root of the repository:
make gates
make gates controleert eerst of de gate-scripts van het Makefile dezelfde zijn als die van .github/workflows/gates.yml, in dezelfde volgorde, en als die van de gatelijst in CLAUDE.md; wijken ze af, dan stopt het voordat er iets gebouwd is. Daarna bouwt het naast elkaar de drie bomen die de gates lezen: de OCaml-werkruimte, de RegelSpraak-frontend en de jar van de Drools-backend, elk alleen als een bron nieuwer is dan het product. Dan draait het de zeventien gates parallel in dezelfde checkout, zoveel tegelijk als de machine cores heeft, met een log per gate onder out/gates/<run>/ en een summary.txt met een regel per gate, in de volgorde van gates.yml. De stap after-run draait de drie bouwstappen daarna opnieuw; elk moet vers zijn, anders veranderde er tijdens de run een bron of bouwde een gate zelf. Een rode bouwstap, een rode gate, een rode after-run of een ontbrekende regel geeft een exitcode ongelijk aan nul. Op 25 oktober 2026 duurde de hele set 283 seconden, op een machine met 16 cores; gate 12 duurt het langst, omdat het de JUnit-suite van de Drools-backend draait (Result van de kaart parallel-gate-replays).
Een enkele gate draait met zijn eigen script, in de omgeving die het Makefile hem geeft:
make gates first checks that the gate scripts of the Makefile are those of .github/workflows/gates.yml, in the same order, and those of the gate list in CLAUDE.md; if they differ, it stops before anything is built. Then it builds, side by side, the three trees the gates read: the OCaml workspace, the RegelSpraak frontend and the Drools backend jar, each only when a source is newer than the product. Then it runs the seventeen gates in parallel in the same checkout, as many at once as the machine has cores, with one log per gate under out/gates/<run>/ and a summary.txt with one line per gate, in the order of gates.yml. The step after-run then runs the three builds again; each must be fresh, otherwise a source changed or a gate built during the run. A red build, a red gate, a red after-run or a missing line gives a non-zero exit code. On 25 October 2026 the full set took 283 seconds on a machine with 16 cores; gate 12 takes longest, because it runs the JUnit suite of the Drools backend (Result of the card parallel-gate-replays).
A single gate runs with its own script, in the environment the Makefile gives it:
sh scripts/check-self-contained.sh # gate 1
sh scripts/check-ir-schema.sh # gate 2
opam exec -- sh scripts/check-verifier.sh # gate 3
sh scripts/check-case-schema.sh # gate 4
opam exec -- sh scripts/check-oracle.sh # gate 5
opam exec -- sh scripts/check-clingo.sh # gate 6
opam exec -- sh scripts/check-noop.sh # gate 7
opam exec -- sh scripts/check-red.sh # gate 8
opam exec -- bash scripts/check-catala.sh # gate 9
bash scripts/check-regelspraak.sh # gate 10
opam exec -- bash scripts/check-toka-parity.sh # gate 11
bash scripts/check-drools.sh # gate 12
opam exec -- bash scripts/check-reftype-derivation.sh # gate 13
opam exec -- bash scripts/check-flogic.sh # gate 14
opam exec -- bash scripts/check-graph.sh # gate 15
bash scripts/check-scenario.sh # gate 16
opam exec -- bash scripts/check-readings.sh # gate 17
Verifier en ref afzonderlijk:
The verifier and ref on their own:
dune build --root spec/verifier
spec/verifier/_build/default/bin/verifier.exe corpus/golden/<case>/ir.json
dune build @ref/runtest
Gates 2, 3 en 5 nemen ook losse bestanden; gates 6 en 12 nemen geen argumenten. WAY-OF-WORKING.md schrijft gate 1 voor vóór elke commit en make gates vóór elke landing.
Gates 2, 3 and 5 also take individual files; gates 6 and 12 take no arguments. WAY-OF-WORKING.md prescribes gate 1 before every commit and make gates before every landing.
Bijdragen
Stateloze agent-sessies bouwen uchar (zie herkomst). Een sessie onthoudt niets; daarom is de repository de toestand. Het plan zegt waarom de controles mechanisch zijn:
Contributing
Stateless agent sessions build uchar (see provenance). A session remembers nothing; that is why the repository is the state. The plan explains why the checks are mechanical:
Reward hacking requires no gradients: a coding agent told to "make the tests pass" will, some fraction of the time, special-case the corpus, weaken an assertion, or edit the verifier. The defenses are therefore policy, mechanized, not optimism.spec/PLAN.md §8
Kaarten
Werk loopt via kaarten, werkbeschrijvingen met vooraf vastgelegde acceptatiecriteria, in backlog/new/, doing/ en done/. De maintainer plant en wijst elke sessie haar werk toe. Het formaat:
Cards
Work runs through cards, work descriptions with acceptance criteria stated up front, in backlog/new/, doing/ and done/. The maintainer schedules and assigns each session its work. The format:
Card format (`backlog/new/YYYYMMDD-<slug>.md`):
# <title>
Goal: <one paragraph — the artifact this card produces>
Acceptance: <runnable commands + expected outcome; the gate(s) that must be green>
May touch: <explicit files/dirs — the disjointness contract>
Out of scope: <explicit>
Een kaart gaat in de eerste commit naar doing/ en bij de merge naar done/, met een Result:-noot: wat landde, wat bewust bleef liggen, en voorgestelde tekst voor PLAN.md of CONSTRUCTS.md, die alleen de hoofdsessie verwerkt, de eigen sessie van de maintainer op main.
Eén sessie, één kaart, één branch
Elke kaart krijgt een branch card/<slug> in een eigen worktree en gaat fast-forward-only naar main. Parallel werk mag alleen bij onderzoek, review of kaarten waarvan de lijsten met te wijzigen bestanden ("May touch") elkaar niet overlappen; een mergeconflict betekent dat de kaarten verkeerd zijn afgebakend. Bij rode gates aan de start stopt de sessie.
A card moves to doing/ in the first commit and to done/ on merge, with a Result: note: what landed, what was deliberately left, and proposed text for PLAN.md or CONSTRUCTS.md, which only the main session, the maintainer's own session on main, applies.
One session, one card, one branch
Every card gets a branch card/<slug> in its own worktree and goes to main fast-forward only. Parallel work is allowed only for research, review or cards whose lists of files they may change ("May touch") do not overlap; a merge conflict means the cards were scoped wrongly. If the gates are red at the start, the session stops.
git mv backlog/new/X.md backlog/doing/X.md
git worktree add ../wt/<slug> -b card/<slug>
git merge --ff-only card/<slug>
Beschermde paden
Beschermde paden mag een sessie alleen voorstellen; de maintainer merget. CLAUDE.md beschermt binnen spec/ de twee bindende documenten, schema/ en verifier/; CODEOWNERS claimt met opzet heel spec/. GitHub dwingt die claim alleen af als een regel op main review door code owners eist. Een ruleset op main blokkeert force-pushes en het verwijderen van de branch, maar eist geen pull request en dus geen review. De review verloopt volgens het mandaat- en reviewprotocol van WAY-OF-WORKING.md.
Protected paths
A session may only propose changes to protected paths; the maintainer merges. Within spec/, CLAUDE.md protects the two normative documents, schema/ and verifier/; CODEOWNERS deliberately claims all of spec/. GitHub enforces that claim only when a rule on main requires review from code owners. A ruleset on main blocks force pushes and deletion of the branch, but it requires no pull request and therefore no review. Review follows the mandate-and-review protocol of WAY-OF-WORKING.md.
# Protected paths (see CLAUDE.md and spec/PLAN.md §8): maintainer review required.
# GitHub enforces this only with branch protection that requires review from
# code owners. The repository is public since 2026-10-12, so the free org plan
# allows that; it is not enabled (2026-10-13). Without it, the
# mandate-and-review protocol of WAY-OF-WORKING.md carries the review.
/spec/ @yhavinga
/corpus/golden/anchor/ @yhavinga
/.github/ @yhavinga
/scripts/ @yhavinga
/WAY-OF-WORKING.md @yhavinga
/CLAUDE.md @yhavinga
/Makefile @yhavinga
Namen
Elke identifier is Engels: klassen, methoden, velden, OCaml-modules en -constructors, ASP-predicaten, shellvariabelen, CLI-vlaggen en testnamen, en ook elke bestands- en mapnaam. Waar de wire een naam voor een begrip heeft, gebruikt de code die naam, anders de term uit de Engelse spec. Nederlands blijft wat als Nederlands geschreven is: domeininhoud (de Jutterswet, gedeclareerde namen, RegelSpraak-bronnen, caselabels en de golden-namen jutterswet-*, synth* en ww-*), de sleutelwoorden van RegelSpraak, de regels van de ANTLR-grammatica, enumeratiewaarden in v0-spelling die op de wire of in een melding komen, de namen die de productieruntime bij naam leest, de variabelen in de DRL-tekst die de Drools-backend maakt, en de 47 TOKA-fragmenten. Kaarten, Result:-noten en commitberichten mogen Nederlands of Engels zijn; de identifiers die ze noemen, zijn Engels. De volledige afspraak staat onder "Conventions" in CLAUDE.md.
Spec-first
De distributie regelspraak-toolchain, opvolger van regelspraak-java, neemt uchar op als git-submodule en bouwt daaruit de RegelSpraak-frontend en de Drools-backend; de commit van de submodule is de pin. Een delta landt eerst in uchar (spec, corpus, schema); daarna verzet de distributie haar pin. Een pincontrole daar faalt als de submodule niet geïnitialiseerd is, niet op de vastgelegde commit staat of eigen wijzigingen heeft, als die commit niet op main van uchar staat, of als ANTLR, Drools, Jackson of JUnit een andere versie heeft dan in uchar.
Names
Every identifier is English: classes, methods, fields, OCaml modules and constructors, ASP predicates, shell variables, CLI flags and test names, and every file and directory name too. Where the wire has a name for a concept, the code uses that name, otherwise the term of the English spec. What was written in Dutch stays Dutch: domain content (the Jutterswet, declared names, RegelSpraak sources, case labels and the golden names jutterswet-*, synth* and ww-*), the keywords of RegelSpraak, the rules of the ANTLR grammar, enumeration values in v0 spelling that reach the wire or a message, the names the production runtime reads by name, the variables in the DRL text the Drools backend generates, and the 47 TOKA fragments. Cards, Result: notes and commit messages may be Dutch or English; the identifiers they name are English. The full convention is under "Conventions" in CLAUDE.md.
Spec first
The distribution regelspraak-toolchain, successor of regelspraak-java, includes uchar as a git submodule and builds its RegelSpraak frontend and Drools backend from it; the submodule's commit is the pin. A delta lands in uchar first (spec, corpus, schema); the distribution then moves its pin. A pin check there fails when the submodule is not initialised, not on the recorded commit or has changes of its own, when that commit is not on uchar's main, or when ANTLR, Drools, Jackson or JUnit differs in version from uchar.
Wat nog komt
Van het plan staat alleen fase 6 nog open: "Catala as a backend: opportunistic, possibly never". De specificatie markeert wat nog open is ter plekke met OPEN, en voor tijd in de tabel van RUNTIME-SEMANTICS hoofdstuk 3.12; de besluiten over het project als geheel houdt spec/PLAN.md bij in zijn § 9.
Een deel van de besluiten in die paragraaf is voorlopig: een sessie nam het tijdens het werk aan een kaart, en de maintainer kan het terugdraaien. PLAN § 9 noemt bij de meeste besluiten wat open blijft. Een greep, op 7 november 2026:
What comes next
Of the plan, only phase 6 is still open: "Catala as a backend: opportunistic, possibly never". The specification marks what is still open with OPEN where it occurs, and for time in the table of RUNTIME-SEMANTICS chapter 3.12; the decisions about the project as a whole are kept in § 9 of spec/PLAN.md.
Some of the decisions in that section are provisional: a session took them while working on a card, and the maintainer can reverse them. PLAN § 9 states for most decisions what remains open. A selection, on 7 November 2026:
- instantiegeordende kring
- Een waarde van een object mag dezelfde waarde van zijn voorganger in een keten lezen (IR-SPEC hoofdstuk 5.3). Een lezing over een relatie waarvan de rol aan de kant van het type van de kring meervoudig is, wordt geweigerd.
- vormanker
- Een knoop met het anker heeft in hoofdstuk 5.12 geen eigen lezing, en de RegelSpraak-frontend schrijft geen geankerde kwantorfold, hoewel het IR die vorm heeft.
- kenmerk van een subtype
- Een kenmerk dat op een subtype is gedeclareerd, mag op een object van het supertype getoetst worden; een attribuut van een subtype en een kenmerk dat twee zustertypen declareren nog niet.
Van de ontwerpvoorstellen en audits in spec/proposals/ noemt elk document in zijn statusregel wat het is: een voorstel, een aangenomen ontwerp of een audit. Eén ervan beschrijft werk dat buiten v1 valt:
- instance-ordered cycle
- A value of an object may read the same value of its predecessor in a chain (IR-SPEC chapter 5.3). A read over a relation whose role on the side of the cycle's type is to-many is refused.
- form anchor
- A node with the anchor has no reading of its own in chapter 5.12, and the RegelSpraak frontend writes no anchored quantifier fold, although the IR has that form.
- characteristic of a subtype
- A characteristic declared on a subtype may be tested on an object of the supertype; an attribute of a subtype and a characteristic that two sibling types declare may not yet.
Of the design proposals and audits in spec/proposals/, each document states in its status line what it is: a proposal, an adopted design or an audit. One of them describes work that lies outside v1:
- DEONTIEK-EISENLIJST
- eisen voor een normatieve laag (plichten, bevoegdheden, handelingen, overtredingen), met herkomst · staande lijst; de normatieve laag zelf valt buiten v1requirements for a normative layer (duties, powers, acts, violations), with provenance · standing list; the normative layer itself lies outside v1
Het lopende werk staat als kaarten in backlog/ (§ 9). Als optie vastgelegd is een verificatiebackend op SMT (smt-backend), die vragen over alle mogelijke casussen tegelijk beantwoordt. De schema's uchar-ir-v1.schema.json en uchar-case-v0.schema.json hebben elk een $id onder uchar.dev, en dat is een identificatie die (nog) niet resolveert.
Ongoing work is kept as cards in backlog/ (§ 9). On record as an option is a verification backend on SMT (smt-backend), which answers questions about all possible cases at once. The schemas uchar-ir-v1.schema.json and uchar-case-v0.schema.json each have an $id under uchar.dev, which is an identifier that does not resolve (yet).
Verkend, niet gepland: RegelRecht en ALEF
Een verkenning van 14 oktober 2026 ging na of twee projecten van de Nederlandse overheid op de IR aan te sluiten zijn: RegelRecht van het ministerie van Binnenlandse Zaken en Koninkrijksrelaties als brontaal, en ALEF van de Belastingdienst als uitvoerder. Beide staan buiten het plan; de verkenning las en telde, en bouwde niets.
RegelRecht legt een wet vast als YAML, één bestand per wetsversie (licentie EUPL-1.2), en voert die uit met een eigen engine in Rust. Een frontend voor zelfstandige berekeningen is haalbaar, mits hij elke vorm die hij niet vertaalt met een reden weigert, zoals de frontends voor Catala en F-logic doen. Rekenkunde, vergelijkingen, afronding en datumdelen hebben een directe tegenhanger in de IR. FOREACH vertaalt naar de knopen voor lijsten, filters en folds in de IR, die nog niet in alle drie de uitvoerders draaien. Meer dan frontendwerk vragen drie onderdelen: de aanroepen tussen wetten en de delegatie aan lagere regelingen, de externe gegevensbronnen, en het onderscheid tussen afwezig en onbekend dat RegelRecht in zijn RFC-036 maakt. Daar betekent null dat het register zegt dat er niets is, en Unknown dat het gegeven niet is aangeleverd; een onbekende waarde draagt de namen van de ontbrekende gegevens. De IR kent één leeg, en een invoer die in een casus ontbreekt, is leeg. Daarnaast kent de IR de afwezigheidsmarker onbekend, een gevulde waarde: het attribuut heeft een waarde die niet vast te stellen is. Een regel kan de marker schrijven, en hij draagt geen namen van gegevens (RUNTIME-SEMANTICS hoofdstuk 9.5). Bij de verkenning stond uitvoerbare logica in enkele tientallen wetsversies in de RegelRecht-repository; het veel grotere geoogste corpus bevatte wettekst zonder uitvoerbare logica.
ALEF is de omgeving waarin de Belastingdienst RegelSpraak schrijft, gebouwd op JetBrains MPS (licentie EUPL-1.2). De editor is projectioneel: RegelSpraak bestaat er alleen als MPS-model. Een backend zou daarom de IR en de casussen via de API van MPS omzetten naar ALEF-modellen voor gegevens, regels en testsets, en ALEF zijn eigen generator en tests laten draaien. Voor wat beide ondersteunen, levert dat een vierde uitvoerder op, van de eigenaar van de taal. Voor de rest wordt het een vergelijking van dialecten, onder meer in de tijdsafhankelijke evaluatie, de behandeling van leeg en de keuze van parametersets. Waar de RegelSpraak-specificatie over tijd zwijgt, volgt de runtime-semantiek de lezing van ALEF; de gronden per lezing staan in spec/proposals/IMPLICIT-TIME.md. Geprioriteerde defaults en subtypen hebben in ALEF geen tegenhanger.
Omdat elke brontaal en elke uitvoerder één vertaling kent, maken zulke aansluitingen ook combinaties mogelijk: RegelSpraak-tekst uitgevoerd door ALEF, en een RegelRecht-wet uitgevoerd door ALEF, Drools, Clingo of de referentie-interpreter. Een combinatie dekt alleen wat beide kanten ondersteunen.
Explored, not planned: RegelRecht and ALEF
An exploration on 14 October 2026 examined two projects of the Dutch government as possible connections to the IR: RegelRecht, of the Ministry of the Interior and Kingdom Relations, as a source language, and ALEF, of the Dutch Tax Administration, as an executor. Both are outside the plan; the exploration read and counted, and built nothing.
RegelRecht records a law as YAML, one file per version of the law (license EUPL-1.2), and runs it with its own engine in Rust. A frontend for self-contained calculations is feasible, provided it refuses every form it does not translate, with a reason, as the Catala and F-logic frontends do. Arithmetic, comparisons, rounding and date parts have a direct counterpart in the IR. FOREACH translates to the IR's nodes for lists, filters and folds, which do not yet run in all three executors. Three parts need more than frontend work: calls between laws and delegation to lower regulations, external data sources, and the distinction between absent and unknown that RegelRecht makes in its RFC-036. There, null means the register says there is none, and Unknown means the fact was not supplied; an unknown value carries the names of the missing facts. The IR has a single leeg (empty), and an input missing from a case is leeg. Besides, the IR has the absence marker onbekend (unknown), a filled value: the attribute has a value that cannot be established. A rule can write the marker, and it carries no names of facts (RUNTIME-SEMANTICS chapter 9.5). At the time of the exploration, executable logic existed in a few dozen versions of laws in the RegelRecht repository; the much larger harvested corpus held statute text without executable logic.
ALEF is the environment in which the Dutch Tax Administration writes RegelSpraak, built on JetBrains MPS (license EUPL-1.2). Its editor is projectional: RegelSpraak exists there only as an MPS model. A backend would therefore convert the IR and the cases, through the MPS API, into ALEF models for data, rules and test sets, and let ALEF run its own generator and tests. For what both support, that yields a fourth executor, from the owner of the language. For the rest it becomes a comparison of dialects, among other things in time-dependent evaluation, the treatment of leeg and the choice of parameter sets. Where the RegelSpraak specification is silent about time, the runtime semantics follows ALEF's reading; the grounds of each reading are in spec/proposals/IMPLICIT-TIME.md. Prioritized defaults and subtypes have no counterpart in ALEF.
Because each source language and each executor needs only one translation, such connections also make combinations possible: RegelSpraak text executed by ALEF, and a RegelRecht law executed by ALEF, Drools, Clingo or the reference interpreter. A combination covers only what both sides support.