RFC-029: Test Fixtures

ProposedNot implemented
Depends on

Deze RFC in de reeks

Vier RFC’s beschrijven lagen van dezelfde machine, van buiten naar binnen.

RFC-026, Werkvoorraad. Welke artikelen verrijkt moeten worden en in welke volgorde. Levert taken op.

RFC-027, Enrichment Quality. De rollen, de poorten en de faalvormen die bepalen of één artikel goed verrijkt is. Verbruikt taken.

RFC-028, Steps and Runtimes. Hoe één stap uit die keten draait.

RFC-029, Test Fixtures, deze RFC. De maatlat voor het geheel.

Context

Wat er nu staat

corpus/regulation/ bevat 25 YAML-bestanden: 21 regelingen die zich voordoen als Nederlands recht en 4 synthetische test_*-wetten. Alle 25 zijn schema-valide. Daarnaast staan er 5 feature-bestanden onder corpus/regulation/**/scenarios/ met samen 39 scenario’s.

De deterministische controles die vandaag in packages/pipeline/src/enrich_v2/ zijn gebouwd, geven over die 25 bestanden 435 bevindingen:

checkaantal
coverage374
binding39
enum-provenance22
schema0

Commando: cargo run --manifest-path packages/pipeline/Cargo.toml --bin law-check -- --corpus corpus/regulation $(find corpus/regulation -name '*.yaml' ! -name '.*').

De 39 binding-bevindingen zijn grotendeels één patroon: 37 keer source: {}, verspreid over 8 bestanden. Dat is een input zonder herkomst, geen regulation, geen output, geen reden.

Wat de bronpoort zegt

law-source, de bronpoort uit RFC-027, houdt het bestand tegen de officiële BWB-toestand. Over de interne Wet op de zorgtoeslag (2025-01-01.yaml, BWBR0018451, toestand 2025-01-01, 9 artikelen in de bron):

drift=7 fabricated=1 verified=2

Artikel 1a is fabricated, het staat niet in de toestand. De inhoud ervan is overigens niet verzonnen: “Onze Minister: Onze Minister van Volksgezondheid, Welzijn en Sport” is artikel 1, eerste lid, onder a. Het is dus verplaatst, en dat telt voor de reparatie.

Artikel 2 loopt na 226 tekens uiteen. De officiële tekst zegt “voor zover dat toetsingsinkomen het drempelinkomen te boven gaat” en noemt 4,289% en 13,730%, te wijzigen bij algemene maatregel van bestuur. Het interne bestand zegt 4,273% en 13,700% en noemt ministeriële regeling. De drempelclausule waarop de berekening rust ontbreekt in de tekst, terwijl het machine_readable-blok hem wel implementeert via MIN plus GREATER_THAN. Het model is dus trouwer aan de wet dan de tekst waar het onder hangt.

Artikel 5 bevat letterlijk 2. Tot en met 4. [Procedurele bepalingen over aanvraag en betaling]. In de toestand staan daar drie leden die afwijken van Awir 25, 15 en 16.

Over de Wet inkomstenbelasting 2001 (2025-01-01.yaml):

BWBR0011353 @ 2025-01-01, 498 article(s) in the source drift=4 missing=494

Nul artikelen verified. De opdracht noemt 514 artikelen; de toestand van 2025-01-01 telt er 498, en het geoogste corpus telt er 2272 op lidniveau. Welk getal klopt hangt af van de toestandsdatum en van wat je een artikel noemt. Die granulariteitsdefinitie staat niet vast.

De Algemene wet bestuursrecht in de interne set staat op valid_from: 1994-01-01. Die toestand is niet op te halen bij repository.officiele-overheidspublicaties.nl; de aanroep eindigt in een redirect-fout. Het geoogste corpus heeft als oudste Awb-toestand 2002-04-01. Een fixture op 1994-01-01 valt dus buiten het bereik van elke geautomatiseerde trouwcontrole.

Drie doelen in één map

Het diepere probleem zit in de indeling. Eén map dient nu drie doelen met drie verschillende faalbetekenissen, en daardoor is geen enkele bevinding actiegericht.

Engine-conformance. Spreekt deze engine de taal. De vier test_*-wetten horen hier, en bdd/conformance/*.feature verwijst er 34 keer naar. Een falende conformance-test betekent: de engine kan iets niet. Deze fixtures moeten per constructie deterministisch zijn en mogen nooit meebewegen met wetgeving. Ze staan vandaag in corpus/regulation/nl/wet/, waar ze worden meegevalideerd als Nederlands recht, meegeteld in de Tier A-corpusdifferentieel en gescand door script/cross-law-integriteit.py.

Wetsvalidatie. Klopt deze wet nog. Dat is bucket A, de 5 feature-bestanden. Een falend scenario betekent: de wet is veranderd of het scenario is verouderd, en een mens beslist welke van de twee. Dat oordeel is alleen mogelijk als het wetsbestand aantoonbaar de wet is. Vandaag is dat voor de zorgtoeslag aantoonbaar niet zo, dus is elke uitkomst van eligibility.feature betekenisloos als uitspraak over de zorgtoeslag. Het scenario test wel dat de engine doet wat het YAML-bestand zegt; dat is dus een engine-test.

Enricher-evaluatie. Wordt de vertaler beter. RFC-027 meet over deze 21 bestanden ongeveer 100 bevindingen op 60 gemodelleerde artikelen, met de kanttekening dat het alfa-materiaal is. Die kanttekening klopt en is meteen het probleem: een evaluatieset waarvan je moet zeggen dat hij niet representatief is, meet niets.

De 374 coverage-bevindingen illustreren het. “Artikel 17 heeft geen machine_readable, 6 leden” is een ware uitspraak over een alfa-testset en een zinloze over een fixture. Zolang dezelfde map beide is, is de teller niet te interpreteren.

Geschiktheid van het geoogste corpus

MinBZK/regelrecht-corpus bevat 3850 regelingmappen (git-tree API op main): 2770 onder wet/, 1054 onder amvb/, 25 onder beleidsregel/, 1 onder waterschaps_verordening/. Elke map bevat de volledige toestandsreeks, voor de Awb 176 toestanden vanaf 2002-04-01.

De tekst is in orde. Ik heb de artikelen 1, 2, 3 en 5 van de geoogste zorgtoeslag (toestand 2026-01-01) naast de toestand gelegd en ze komen overeen, inclusief de derogatie in 3.1 (“In afwijking van artikel 7, derde lid, van de Algemene wet inkomensafhankelijke regelingen”) en de drie AWIR-afwijkingen in 5.2 tot en met 5.4. Er zit een preamble in, en verwijzingen zijn gemarkeerd als markdown-links met een references-blok met bwb_id en artikel. Dat is materiaal waar je een fixture op kunt bouwen.

Drie dingen kloppen niet, en ze moeten opgelost zijn voordat de fixtures op dit corpus gebaseerd kunnen worden.

Eén. De geoogste bestanden nummeren op lidniveau: 1.1, 1.1.b, 2.3, 5.4. De interne bestanden nummeren op artikelniveau met alle leden in één text. De bronpoort, gebouwd om trouw te bewijzen, verwerpt een vers geoogst bestand:

=== wet_op_de_zorgtoeslag/2026-01-01.yaml (geoogst) BWBR0018451 @ 2026-01-01, 9 article(s) in the source fabricated=31 missing=5 verified=4

Harvester en bronpoort zijn het oneens over wat een artikel is. Dat is een bug in een van beide en geen ontwerpvraag over fixtures, en hij blokkeert alles wat hierna komt.

Twee. De regulatory_layer klopt niet. regeling_aanpassing_voertuigen (BWBR0025732, een ministeriële regeling) heeft regulatory_layer: WET. Van de 2770 slugs onder regulation/nl/wet/ beginnen er 715 met regeling_. RFC-003 leest dat veld voor lex-superior-prioriteit en voor de validatie dat delegation_type bij regulatory_layer past. Een delegatieketen uit het geoogste corpus is dus niet zonder meer bruikbaar.

Drie. De regeling die de open term standaardpremie invult, staat niet in het corpus. Ik heb de volledige boom van 3850 slugs doorzocht op standaardpremie en bestuursrechtelijke_premie en vind niets. De keten die de huidige fixtures modelleren is dus niet uit het corpus te reconstrueren zonder eerst te oogsten.

Schemaversies

Er zijn 17 schemaversies geregistreerd in packages/engine/src/schema.rs, van v0.2.0 tot v0.7.0, met schema/latest als symlink naar v0.7.0. De interne corpus gebruikt er drie door elkaar: 19 bestanden op v0.5.0, 1 op v0.5.3, 1 op v0.5.6, 4 met een gevouwen $schema-scalar. Het geoogste corpus is nog breder gespreid; de twee bestanden die ik ophaalde staan op v0.5.2 en v0.3.1.

Er zijn 9 conformance-manifesten (conformance/v0.5.0, v0.5.4 tot en met v0.6.0, en v0.7.0) voor 17 versies; packages/engine/tests/conformance_coverage.rs leest ze inmiddels alle via all_manifests(). De JSON-testgevallen die RFC-014 beschrijft bestaan niet.

De sprong naar v0.7.0 is niet additief. Hij voegt placement, markings en declares toe, verbreedt open_terms, en verwijdert untranslatables en enables (RFC-031). Een bestand op v0.6.0 valideert dus niet zonder meer tegen v0.7.0, en de additiviteitstest die hieronder wordt voorgesteld moet dat kunnen uitdrukken in plaats van erop te vertrouwen. Of de overige overgangen additief zijn heb ik niet gecontroleerd.

Besluit

1. Drie soorten fixtures, drie locaties, drie faalbetekenissen

De eenheid, de locatie, de eigenaar en de toelatingseis verschillen per soort. De mapstructuur brengt dat verschil aan, zodat aan het pad te zien is wat een rode uitkomst betekent.

A. Engine-conformance. Eenheid: één construct. Locatie: bdd/laws/ voor de wetsbestanden, bdd/conformance/*.feature voor de scenario’s, conformance/<schemaversie>/ voor de taalonafhankelijke JSON-gevallen uit RFC-014. Eigenaar: het engine-team, meebewegend met de engine. Falen betekent: de engine kan iets niet, of een operatie is stilletjes van betekenis veranderd.

Toelatingseis: $id begint met test_, geen bwb_id, geen url naar wetten.overheid.nl, isoleert precies één construct zodat een verwerping toewijsbaar is, en wordt aangeroepen vanuit minstens één feature of manifest. Deze bestanden worden schema-gevalideerd en verder met rust gelaten: geen bronpoort, geen coverage, geen cross-law-integriteitscheck.

B. Wetsvalidatie. Eenheid: een scenario over een vastgepinde toestand van een echte wet. Locatie: corpus/golden/nl/<laag>/<slug>/<toestandsdatum>.yaml met de scenario’s ernaast in scenarios/. Eigenaar: een genoemd mens per wet, in CODEOWNERS. Falen betekent: de wet is veranderd of het scenario is verouderd.

Toelatingseis: law-source geeft Verified voor elk artikel, law-check geeft nul schema- en nul binding-bevindingen, elke legal_basis wijst naar een artikel dat in die toestand bestaat, en naast het bestand staat een sidecar met bwb_id, toestandsdatum, een digest van de opgehaalde XML en een hash per lid (§2). Eén Drift of één Fabricated en het is geen gouden fixture. Coverage is geen toelatingseis: een gouden wet mag deels ongemodelleerd zijn, mits het ongemodelleerde deel een marking of een open_term heeft in plaats van te zijn weggelaten.

C. Enricher-evaluatie. Eenheid: een claim, niet een bestand. Locatie: eval/gold/. Eigenaar: wie de enrichment-skill wijzigt. Falen betekent: de skill is geregresseerd.

RFC-027 beschrijft het recordformaat al (id, bron, claim_type, fout, correct, reden, toets, herkomst) en de regel die het scheidt van bucket A: de runner genereert vers, het ingecheckte corpus doet niet mee. Dat is de enige constructie waarin “de vertaler wordt beter” een meetbare uitspraak is. Een claim die zich in een uitvoeringsuitkomst manifesteert hoort als Gherkin in bdd/; de vormclaims horen dat expliciet niet, want daar betekent falen iets anders.

A meet de engine tegen de taal, C meet de vertaler tegen de wet. Een bestand kan nooit beide zijn.

2. Gouden standaard uit het echte corpus, gepind op toestand

Ja, het kan op het echte corpus. Het bezwaar dat een echte wet verandert vervalt zodra je ziet wat een toestand is. regulation/nl/wet/<slug>/<datum>.yaml is de wet zoals die gold op die datum, en dat is een historisch feit dat BWB niet herschrijft. Een gouden fixture is dus een paar (bwb_id, toestandsdatum), en dat paar beweegt alleen als een mens het verplaatst.

Vastgepinde snapshots naast de toestand zijn overbodig, want de toestand is de snapshot. Er is wel een digest van de opgehaalde XML nodig in de sidecar, zodat een stille correctie bij BWB zichtbaar wordt. Daarnaast hoort er een nachtelijke run van law-source over de gouden set te draaien die meldt dat er een nieuwere toestand bestaat, zonder de build te breken.

Een digest over het hele opgehaalde document meldt een verandering zonder te zeggen waar. Een hash per lid zegt het wel. PR #727 werkt dat uit: naast de documentdigest komt er een genormaliseerde hash per lid in de sidecar, zodat een afwijking “artikel 2.75, lid 1” meldt in plaats van “de Zorgverzekeringswet”. Bij een wet van 742 lid-artikelen scheelt dat een melding tegen een zoekopdracht. De normalisatie is die van de bronpoort: witruimte binnen een alinea wordt één spatie, alineagrenzen blijven staan, en accenten, leestekens en casing blijven significant.

Twee dingen horen bij die keuze. De hash hangt aan het adres binnen het artikel (3.2.a) en niet aan een positie in de lijst. Hangt hij aan een positie, dan verschuift een ingevoegd lid elke hash erna en meldt de poort een wijziging op plekken waar niets veranderd is. En de poort loopt over de verwachte verzameling en niet over de gevonden. Een lid dat uit het bestand verdwijnt, een artikel dat leeg achterblijft en een bestand dat niet meer parseert horen alle drie een bevinding te zijn; een poort die de gevonden verzameling afloopt geeft in die drie gevallen groen.

Twee grenzen aan die aanpak, allebei gemeten. Toestanden van vóór ongeveer 2002 zijn niet ophaalbaar; de Awb op 1994-01-01 gaf een redirect-fout. En regelgeving zonder BWB-nummer (de APV Amsterdam, de afstemmingsverordening Diemen) is principieel niet te poorten. Die blijven handgeschreven en krijgen dat als status in het bestand.

De keuze is één keten plus drie losse gevallen.

WetToestandOmvangConstruct
Wet op de zorgtoeslag (BWBR0018451)2026-01-0135 lid-artikelenberekening, drempel, delegatie, cross-law, procedure
Regeling vaststelling standaardpremie (BWBR0037841)nader te bepalenkleinimplements, de invulzijde van de delegatie
Awir (BWBR0018472)2026-01-01326 lid-artikelen, art. 3/7/8 gemodelleerdpartnerbegrip, toetsingsinkomen, derogatie
Wet IB 2001 (BWBR0011353)2026-01-012272 lid-artikelen, art. 5.2 gemodelleerdrendementsgrondslag als bronwaarde
Zorgverzekeringswet (BWBR0018450)2026-01-01742 lid-artikelen, art. 1/2 gemodelleerdverzekerdenbegrip
Algemene wet bestuursrecht (BWBR0005537)2026-01-011745 lid-artikelen, art. 3:46/4:13/6:7 gemodelleerdhooks, procedures
Vreemdelingenwet 2000 (BWBR0011823)2026-01-01939 lid-artikelen, art. 69 gemodelleerdoverride, lex specialis
Burgerlijk Wetboek Boek 5 (BWBR0005288)2024-01-01390 lid-artikelen, art. 5:42 gemodelleerdopen norm ingevuld door lagere regelgeving
APV Amsterdam, afstemmingsverordening Diemenn.v.t.kleingemeentelijke invulling, handgeschreven

Dekking over soorten wetgeving

De tabel hierboven is gekozen op constructen: berekening, drempel, delegatie, cross-law, procedure, open norm. Dat is niet dezelfde as als het rechtsgebied, en beide zijn nodig.

Een testset die alleen inkomensafhankelijke regelingen dekt bewijst minder dan hij lijkt. Toeslagen rekenen met bedragen en percentages over een tijdvak; milieurecht werkt met normwaarden, drempels per inrichting, meet- en rapportageverplichtingen en een vergunningstelsel; strafrecht met kwalificaties en strafmaxima; burgerlijk recht met termijnen en rechtsvermoedens. Een vertaalinstructie die goed presteert op de ene soort kan op de andere de plank misslaan zonder dat de meetlat dat ziet, want de meetlat is op de eerste soort afgeregeld.

De gouden set moet daarom over rechtsgebieden gespreid zijn en niet alleen over constructen. De keuze hierboven dekt sociale zekerheid, belastingen, zorgverzekering, bestuursrecht, vreemdelingenrecht en burgerlijk recht. Milieurecht ontbreekt, en dat is een gat: PR #498 zette machineleesbaar milieurecht neer en is gesloten omdat de bodem eronder verschoof, maar hij laat zien om wat voor materiaal het gaat en welke constructen daar spelen. Bij de uitbreiding van de gouden set hoort minstens één milieurechtelijke regeling, en het is de moeite waard die PR er dan bij te pakken.

Dezelfde redenering geldt voor het decentrale niveau, dat in de tabel alleen via de APV en de afstemmingsverordening voorkomt, en voor Europees recht dat via een implementatiewet doorwerkt. Over soorten die niet in de set zitten doet de meting geen uitspraak.

Waarom de zorgtoeslag als kern. Het is de kleinste volledige berekening in het Nederlandse recht waarin alle zes constructen binnen negen artikelen voorkomen: een berekening met een drempel (art. 2, tweede en derde lid), een vermogenstoets met derogatie op een hogere wet (art. 3, eerste lid), twee verschillende delegatievormen (art. 2 derde lid AMvB, art. 2 zesde lid ministeriële regeling, art. 4 vaststelling standaardpremie), verwijzingen naar drie andere wetten, en een procedurele staart (art. 5). Het artikelenaantal is sinds 2006 negen gebleven; jaarlijks bewegen alleen de percentages en de grensbedragen, en dat vangt een toestandspin af. De wet heeft bovendien al een scenarios/eligibility.feature in het geoogste corpus staan.

Waarom de grote wetten er toch bij horen, ondanks omvang. Een keten die stopt bij een verzonnen bronwaarde test de keten niet. De Awir, de Wet IB 2001 en de Zorgverzekeringswet komen er als volledige toestand in en worden voor drie tot vijf artikelen gemodelleerd. Zo gaan we om met omvang: pin de wet heel, modelleer hem deels, en verantwoord het ongemodelleerde deel. De alfa-set deed het omgekeerde en hield vier artikelen over van 498, waardoor de wet als bron onbruikbaar werd en de rest van het bestand een verkeerde indruk wekt.

De Awb en de Vreemdelingenwet zitten erin omdat hooks en overrides (RFC-007) nergens anders realistisch te testen zijn. BW Boek 5 met de APV zit erin als het enige geval van gemeentelijke invulling van een rijksnorm, en tegelijk als voorbeeld van een fixture die de bronpoort maar half kan dekken: BW 5 wel, de APV niet.

3. Eén engine, meerdere schemaversies

De twee routes zijn niet gelijkwaardig. De uitvoerbaarheid geeft de doorslag.

Een bewaarde engineversie per schemaversie geeft exacte historische semantiek en maakt cross-law-uitvoering onmogelijk zodra twee wetten in één keten op verschillende schemaversies staan. Dat is vandaag al de toestand: de interne zorgtoeslag staat op v0.5.6 en verwijst naar de Awir op v0.5.0 en de Wet IB 2001 op v0.5.0. Eén uitvoering kan niet in twee engines draaien. Het is bovendien niet wat het gedeployde systeem doet: één editor, één WASM-engine, één corpus met gemengde versies.

Dus: één engine die alle uitgebrachte schemaversies leest. Dat is alleen houdbaar met een regel die vandaag impliciet is en expliciet moet worden.

Een schemaversie mag uitsluitend optionele constructen toevoegen. Een wijziging die iets verwijdert of vernauwt is een corpusmigratie, geen schemaversie.

Verhouding tot RFC-013

RFC-013 besluit het omgekeerde in vorm: de engine declareert supported-schemas, een regeling met een niet-ondersteunde versie is een harde fout, en “support all schema versions forever” staat daar als verworpen alternatief. Dat verschil is echt en het valt op te lossen, maar niet stilzwijgend.

De grond onder die verwerping is de stapel compatibiliteitscode: “after 20 schema versions, the engine is mostly compatibility code”. Additiviteit haalt die grond weg. Een versie die uitsluitend optionele constructen toevoegt vraagt geen tweede leespad, dus er is één model dat alles leest en geen enkele shim. RFC-013 verwerpt achterwaartse compatibiliteit door opstapeling, en die verwerping blijft hier staan.

Het mechanisme van RFC-013 blijft daarmee overeind, met één wijziging in wat het uitdrukt. supported-schemas blijft, de harde fout bij een onbekende versie blijft, en de lijst wordt een aaneengesloten bereik vanaf een ondergrens in plaats van een selectie. Een versie laten vallen blijft toegestaan als expliciete en geversioneerde beslissing, alleen is het voortaan eerst een corpusmigratie: de ondergrens gaat pas omhoog nadat elk document eronder omhoog is gebracht.

Vandaag verwijst de interne zorgtoeslag op v0.5.6 naar de Awir op v0.5.0. Een engine die v0.5.0 laat vallen zonder die migratie breekt de keten, en één uitvoering kan niet in twee engines draaien.

De ondergrens uit RFC-026 is een ander ding met dezelfde vorm. Die zegt dat de graafindex documenten onder schema 0.7.0 niet opneemt, omdat ze de velden missen die de graaf nodig heeft. De engine voert zo’n document nog wel uit; het staat alleen niet in de index.

Additiviteit testen

Die regel is testbaar en wordt niet getest. De concrete invulling: een test die voor elk opeenvolgend paar versies aantoont dat elk document dat valide is onder vN-1 valide blijft onder vN na uitsluitend het herschrijven van $schema. De sprong naar v0.7.0 voldoet daar niet aan (zie hierboven); de andere overgangen zijn ongecontroleerd.

De gevolgen van die route, één voor één.

Voor de testsuite. Conformance-gevallen zijn per schemaversie, zoals RFC-014 al zegt, en draaien allemaal tegen dezelfde engine. Een geval dat voor v0.5.0 is geschreven moet op de engine van vandaag nog slagen, en dat is het achterwaartse-compatibiliteitsbewijs. Concreet: conformance_coverage.rs moet over alle conformance/v*/manifest.json itereren in plaats van één include_str!, en er moet een manifest zijn per uitgebrachte versie. Het eerste is gedaan; nu zijn er negen manifesten voor zeventien versies. Een ontbrekend manifest laat de suite falen.

Voor de migratie van het corpus. Het geoogste corpus migreert niet. Gemengde versies zijn de realistische toestand van 22.000 bestanden die over een jaar zijn geoogst, en de engine leest ze allemaal. just validate blijft daar versietolerant. De gouden set migreert wel, en staat altijd op de nieuwste versie, want dat is de versie die gespecificeerd wordt. Dat maakt just validate tweeledig: tolerant over corpus/legacy en bdd/laws, strikt over corpus/golden.

Voor het uitbrengen van een nieuwe schemaversie. Vier stappen, in deze volgorde: bewijs dat de wijziging additief is; schrijf conformance/vNEW/manifest.json en de gevallen die het nieuwe construct dekken; migreer de gouden fixtures door $schema te herschrijven en opnieuw te poorten; laat het geoogste corpus staan.

4. De bestaande testset splitsen

Weggooien maakt van een ontwerpbeslissing een storing van twee weken; laten staan zoals het is houdt in stand dat een lezer er onjuiste conclusies over Nederlands recht uit trekt. We halen de set daarom uit elkaar naar wat elk bestand werkelijk is.

De vier test_*-wetten deugen als fixture maar staan op de verkeerde plaats. Ze verhuizen naar bdd/laws/. Ze zijn het enige deterministische materiaal in de repo en blijven ongewijzigd.

De vijf bucket-A-featurebestanden blijven, de wetten eronder niet. Een scenario wordt herricht op een gouden wet zodra die er is, en krijgt tot dat moment @wip (de runner slaat die al over). Dat betekent tijdelijk 39 scenario’s op inactief, en het alternatief is 39 scenario’s die groen zijn zonder betekenis.

De 21 alfa-regelingen verhuizen naar corpus/legacy/ met een README die zegt wat het is: alfa-materiaal uit de periode zonder schema en zonder skills, geen referentie, geen nieuw werk hier. Ze blijven schema-valide en blijven bruikbaar als volumemateriaal voor de loader-tests en de benchmarks.

Per afhankelijkheid, en de lijst is volledig voor zover ik hem kon vaststellen.

AfhankelijkheidWat er moet gebeuren
just validate en de pre-commit-hook validate-law-yamlPadset verbreden naar corpus/** en bdd/laws/. Tolerant over legacy, strikt over golden.
just conformance Tier A (packages/engine/tests/conformance.rs:83)Alleen het pad wijzigen. Tier A is een schema↔model-differentieel en geeft niet om wetstrouw, dus legacy is legitieme invoer. De assert checked > 0 blijft.
script/cross-law-integriteit.py (CI, fail-closed)Alleen over corpus/golden draaien. Over legacy meldt hij per constructie DANGLING en PLAIN-PARAM (37 keer source: {}), en fail-closed zou daar elke toekomstige PR blokkeren.
Loader-integratietests in src/article.rs (14 vaste paden), src/engine.rs, src/service.rs, src/resolver.rsHerschrijven tegen bdd/laws/. Ze asserteren nu op de inhoud van alfa-bestanden (vermogensgrenzen €161.329, standaardpremie == 211200, de namen van de open_terms in Participatiewet art. 8) terwijl ze de loader testen. Dit is de grootste post en tegelijk de meest lonende.
tests/trace_integration.rs met expected_zorgtoeslag_trace.txt en expected_standaardpremie_trace.txtTekstsnapshots van een trace over een alfa-wet. Tijdelijk naar een synthetische wet, daarna op de gouden zorgtoeslag. RFC-027 noemt golden-trace-snapshots; keten-checkpoints zijn er de robuustere vorm van.
tests/golden_tests.rs en tests/fixtures/*.json (54 gevallen)Corpusonafhankelijk, dus niet urgent. Wel migreren naar conformance/<versie>/: dit is feitelijk al het JSON-formaat dat RFC-014 beschrijft en het leidt nu een parallel bestaan. real_regulations.json hernoemen, de twee gevallen erin gaan over verzonnen test_wet en test_toeslag.
frontend/e2e/helpers-corpus.js en de drie specs die hem gebruikenCORPUS_ROOT naar corpus/golden/nl. De twee kopieën in frontend/e2e/fixtures/zorgtoeslag-*.yaml zijn ongesynchroniseerde forks van de alfa-wet en gaan weg.
frontend/Dockerfile regel 109-114De COPY van corpus/regulation naar het image is dode data: de ingecheckte corpus-registry.yaml heeft geen local source. Verwijderen. Hij zorgt er nu alleen voor dat elke corpuscommit het editor-image invalideert.
TUI (packages/tui/src/backend/corpus_scanner.rs)Fallback corpus/golden en daarna corpus/legacy.
Benches law_loading.rs en service_e2e.rsGeen REGULATION_PATH-override, die krijgen ze, gericht op legacy. Ze willen volume, geen trouw.
corpus/annotations/ en just validate-annotationsAnnotaties verhuizen mee met de wet die ze annoteren. CODEOWNERS bijwerken.
CI-job “Check corpus schema references” (ci.yml:161)Padset verbreden. De fallback die regel 2 en 3 afpelt bij gevouwen $schema-scalars is broos en verdient een echte YAML-parse.
Skills law-download, law-generate, law-interpretHardcoderen corpus/regulation/nl/... en noemen de alfa-zorgtoeslag als canoniek voorbeeld. Dat is het mechanisme waardoor het alfa-materiaal zich voortplant: elke nieuw gegenereerde wet wordt op een fout voorbeeld verankerd. Herrichten op de gouden zorgtoeslag. Goedkoop en met het meeste effect.

Tellers naast de poorten

Een fixture zegt of een uitkomst klopt. Voor de vraag of de enricher beter wordt is dat niet genoeg, zoals ronde 3 liet zien.

Die ronde draaide twee varianten over dezelfde bodem met één verschil. De variant zonder contextbrief kreeg twee bevindingen, de variant met brief vierentwintig. Op het oog was de eerste beter. Hij had alleen geen enkele cross-law binding gelegd waar de tweede er acht had, en een model dat niets probeert kan nergens fout in zitten. Elke poort die vraagt of iets klopt geeft terughoudendheid en leegte dezelfde score.

Daarom telt law-check sinds die ronde ook wat een vertaling ondernam: artikelen met logica tegenover artikelen met alleen een markering tegenover artikelen zonder iets, het aantal bindingen en naar hoeveel van de aangehaalde wetten, de markeringen per soort, en hoeveel outputs door een ander artikel gelezen worden.

Die laatste teller vroeg een correctie, en die legt de faalvorm bloot. In de eerste versie telde een declaratie als haar eigen lezer, en dan staat de uitkomst op 60 van 61 en zegt hij niets. Alleen consumptie van buiten het eigen artikel telt, en dan staat hij op 17 van 61. Dat verschil is de losse restrictie die de juridische review aanwees: de leeftijdstoets en de vermogenstoets worden geproduceerd en door niemand gelezen, waardoor het model toekent aan een zestienjarige en aan een miljonair terwijl elk artikel afzonderlijk klopt.

Een teller die ook een poort is, wordt een doel. Ronde 4 laat zien wat dat kost: de poort cross_law_references accepteerde een substring in de markeringen als bewijs dat een verwijzing behandeld was, en 43 van de 101 gaten in dat corpus zijn kruisverwijzingen naar een wet die al geoogst is, in de vorm van een gat opgeschreven omdat die vorm de poort passeerde. Een teller hoort daarom te tellen wat er staat en de vraag of het klopt aan een andere controle te laten (RFC-031).

Een teller is geen kwaliteitsoordeel. Meer bindingen is niet beter dan minder; het is een andere vertaling van dezelfde tekst. De vergelijking tussen twee ronden heeft beide nodig, en een ronde die alleen op bevindingen wordt afgerekend beloont de vertaling die minder aandurft.

Wat het kost en in welke volgorde

Fase 0, ongeveer een dag, geen inhoudelijke keuze. De map splitsen: corpus/regulation naar corpus/legacy, de vier test_* naar bdd/laws/, een lege corpus/golden/ aanmaken, en de ongeveer vijftien padconstanten herrichten. Semantisch verandert er niets en elke latere fase wordt daarmee een lokale wijziging. De BDD-World moet vanaf hier twee wortels preloaden.

Fase 1, blokkerend, twee tot drie dagen. Het granulariteitscontract tussen harvester en bronpoort beslechten. Of de poort leert lidnummering lezen, of de harvester levert artikelniveau met de leden erbinnen. Zonder dit kan geen enkel geoogst bestand een gouden fixture worden. In dezelfde beweging de afleiding van regulatory_layer in de harvester repareren; BWB draagt de soort in de metadata, dus dit is werk van uren, geen ontwerpvraag.

Fase 2, ongeveer een week. De eerste gouden keten: zorgtoeslag 2026-01-01, Awir, Wet IB 2001, Zorgverzekeringswet, plus de standaardpremieregeling die eerst geoogst moet worden. Elk bestand door de poort, daarna eligibility.feature erop porten. Opleverpunt: één keten waarin law-source voor elk artikel Verified geeft en just bdd groen is.

Fase 3, ongeveer een week. De overige vier ketens, één per PR: Participatiewet met Diemen, Woo, BW 5 met de APV, Awb met de Vreemdelingenwet. @wip tot af.

Fase 4, parallel, engine-team, ongeveer een week. De loader-integratietests herschrijven tegen bdd/laws/, de trace-snapshots herbaseren, tests/fixtures/*.json naar conformance/<versie>/ migreren, conformance_coverage.rs over alle manifesten laten itereren, en de compatibiliteitstest voor schemaversies schrijven, die per overgang vastlegt welke velden erbij komen en welke verdwijnen. Additiviteit is geen aanname meer sinds v0.7.0 er drie verwijdert.

Fase 5, onafhankelijk. De gouden set voor de enricher (eval/gold/) met de regressierunner uit RFC-027. Enige koppeling met het voorgaande: hij mag corpus/legacy niet lezen.

De dure post is niet het schrijven van fixtures. Het zit erin dat veertien vaste paden, twee trace-snapshots en drie Playwright-specs asserteren op de inhoud van alfa-bestanden. Dat is de rekening voor het gebruiken van één testcorpus als demodata, runtimedata en specificatie tegelijk, en die rekening komt één keer.

Wat ik niet heb kunnen controleren

  • Het corpus is niet gekloond. Alles erover komt van de GitHub-API op main (git-tree, contents, status.yaml). De branch development waar corpus-registry.yaml naar wijst, en enrich/claude en enrich/opencode, heb ik alleen steekproefsgewijs bekeken.
  • Ik heb law-source op drie interne bestanden gedraaid. De Awb-aanroep faalde op een redirect, dus daarvoor heb ik geen oordeel.
  • Van de zestien schema-overgangen heb ik er één gediffed (naar v0.7.0). Die ene blijkt inmiddels velden te verwijderen, dus over de andere zeg ik niets.
  • just bdd, just conformance en just test heb ik niet gedraaid. De uitspraken over welke test breekt bij een verplaatsing komen uit de code, niet uit observatie.
  • Of de standaardpremieregeling echt ontbreekt of onder een slug staat die ik niet heb geraden: de boom van 3850 slugs bevat geen treffer op “standaardpremie” of “bestuursrechtelijke_premie”, maar dat sluit een afwijkende naamgeving niet uit.
  • Het aantal van 22.468 bestanden uit de opdracht heb ik niet gereproduceerd; ik tel 3850 regelingmappen, elk met een toestandsreeks.

Waar ik het oneens ben met de aanname in de opdracht

“Het echte geoogste corpus is wél in orde.” Voor de tekst klopt dat en ik heb het geverifieerd. Voor de metadata niet: 715 ministeriële regelingen staan als regulatory_layer: WET onder regulation/nl/wet/, en RFC-003 leest dat veld voor prioriteit en delegatievalidatie. En het geoogste bestand faalt de eigen bronpoort van de repo met fabricated=31, omdat harvester en poort het oneens zijn over wat een artikel is. “Baseren op het echte corpus” is daarmee een substantiëlere klus dan een kopieerslag, en fase 1 hierboven bestaat alleen daarom.

“De Wet IB 2001 heeft er vier tegen 514 in de wet.” De toestand van 2025-01-01 telt 498 artikelen; het geoogste corpus telt er 2272 op lidniveau. Het getal hangt af van de toestandsdatum en van de granulariteitsdefinitie, en die definitie staat niet vast. Dat is zelf de bevinding.

“Artikel 1a bestaat niet in de wet.” Dat klopt, de poort noemt het fabricated. De inhoud is echter verplaatst: het is artikel 1, eerste lid, onder a. Dat geldt breder. De alfa-bestanden zijn overwegend verschoven en samengeperst en zelden uit het niets opgeschreven, en daarom levert herverankeren op toestanden een groot deel van het werk terug in plaats van dat het opnieuw moet.

De aanname dat de fixtures fout zijn. De meting wijst naar iets anders. Van de 435 bevindingen zijn er 374 coverage, en die zeggen “dit artikel heeft geen machine_readable”. Dat is waar over een alfa-testset en betekenisloos over een fixture. Het probleem zit erin dat één map drie doelen dient met drie faalbetekenissen, waardoor geen enkele bevinding actiegericht is. De bestanden repareren zonder de doelen te scheiden reproduceert de situatie binnen een jaar.

RegelRecht

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

Links

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

Contact

regelrecht@minbzk.nl

Part of

Bureau Architectuur
Ministry of Economic Affairs and Climate Policy