Onze website gebruikt cookies om de site gebruiksvriendelijker te maken.

Rules as Code: lessen uit een experiment

Posted on 30/01/2026 by Joachim Ganseman

Cet article est aussi disponible en français.

In een vorig artikel fileerden we Rules as Code, een aanpak die erop gericht is om de kloof tussen regelgeving en software te verkleinen. We illustreerden daarbij dat er heel wat praktische obstakels te overkomen zijn, niettegenstaande het lovenswaardige doel. De uniforme encodering van regels met hun geschiedenis, verwevenheden en afhankelijkheden is een uitdaging die een aanzienlijke investering van mensen en middelen kan vergen. Permanent actief beheer is daarenboven nodig om elke wijziging aan de regels op te vangen. Zelfs op kleine schaal is een nauwe samenwerking tussen juristen en ontwikkelaars onontbeerlijk, want regelmatig zullen gemotiveerde beslissingen genomen moeten worden over interpretatie. Omdat industriestandaarden nog ontbreken en best practices nog volop in ontwikkeling zijn, riskeer je als early adopter de zogenaamde pioneer tax te moeten betalen. De complexe lasagna van overheidsbevoegdheden maakt een eventuele toepassing in België niet eenvoudiger.

Mede onder impuls van het uitgebreide rapport van de OESO uit 2020, hebben enkele overheden toch al volop ingezet op het uitwerken van, soms vrij grootschalige, proof-of-concepts. Er bestaan vandaag dan ook enkele frameworks die relatief matuur zijn. Ongetwijfeld is Frankrijk het voortrekkersland; het initiatief dat we hieronder zullen toelichten komt van Franse bodem. Ook in Nederland beweegt er wel wat: de Nederlandse fiscus gebruikt al enige tijd haar eigen domeintaal RegelSpraak die zij met de rule engine ALEF interpreteert en verwerkt, echter lijkt de daarover gepubliceerde broncode vooralsnog meer op methodologie dan op applicaties te focussen.

OpenFisca

OpenFisca is ontstaan in 2011 als open-source microsimulatie-motor om belasting- en uitkeringsregels (“tax & benefit system”) om te zetten naar uitvoerbare code. De effecten van die regelgeving, en eventuele wijzigingen, kunnen dan gesimuleerd worden voor zowel individuele cases als hele populaties. Websites met OpenFisca in de achtergrond zijn onder andere LexImpact (simulatie van wijzigingen in socio-fiscale wetgeving), Mes droits sociaux (simulatie van sociale rechten), en 1jeune1solution (allerhande steunmaatregelen). Buitenlandse voorbeelden zijn BenefitMe (Nieuw-Zeeland), Les meves ajudes (Barcelona), of PolicyEngine (UK/USA) – deze laatsten wel met grondige aanpassingen aan de engine.

Om ons eigen belasting- en/of sociale-zekerheidsstelsel te modelleren, moeten we een fork maken van het generieke OpenFisca country-template. Verschillende andere landen hebben er tenminste al mee geëxperimenteerd, zo vinden we in de lijst van repositories o.a. Senegal, Paraguay en Tunesië. Regionale of lokale wetgeving kan middels plugin-extensies toegevoegd worden aan een nationaal systeem, zoals deze voor Parijs. Eens de repository geïnitialiseerd, kunnen we beginnen werken aan wat misschien ooit openfisca-belgium kan worden. De modellering in OpenFisca gebeurt door het schrijven van Python-klassen en -methodes, die de entiteiten, variabelen en berekeningsformules uit de regelgeving vertegenwoordigen.

Helaas houdt het gemakkelijke deel daar ongeveer op. De country-template repository is minimalistisch en hoewel er wel documentatie, met een kleine tutorial, beschikbaar is om een eigen versie uit te bouwen, focust deze vooral op de eerste stappen. Richtlijnen over hoe we onze eigen fork best zouden structureren zodra het aantal variabelen en parameters groeit, ontbreken grotendeels. De repository van moederproject openfisca-france kan weliswaar als voorbeeld dienen, maar is dan weer erg groot, en het waarom van hun structurele of architecturale keuzes is er niet echt uit af te leiden.

Ook het aspect van een GUI of webinterface blijft onderbelicht. Nochtans is de interface van bijvoorbeeld de LexImpact simulator van de Franse inkomstenbelasting, net een sterk punt. Als leidraad voor bouwen van een webinterface verwijst men naar tutorials en slides van een workshop, waar men de eerste stappen toont in Svelte, React en VueJS. Het is echter een extra barrière voor adoptie, dat een GUI of webapp nog from scratch zelf te bouwen is bovenop een eigen OpenFisca-instantie. Het bouwen van een GUI is immers tijdrovend. Het zou nuttig zijn om OpenFisca-GUI-libraries te hebben met herbruikbare componenten voor de belangrijkste web frameworks, zodat een OpenFisca server misschien met een generieke default webinterface gebundeld kan worden. Een Drupal-plugin lijkt momenteel het enige project dat enigszins in die richting gaat.

AI to the rescue?

Gezien OpenFisca, Svelte, React en Vue allen nieuw zijn voor de auteur, en AI-tooling belooft om developers sneller te laten onboarden, grijpen we de kans om de AI-powered IDE Cursor tegelijk uit te testen. Deze kloon van Visual Studio Code is verrijkt met de mogelijkheid tot het aanroepen van (in ons geval public-cloud-gebaseerde) LLMs. Daarbij kunnen selecties uit bestanden in het project worden gemarkeerd als context bij de vraag. Cursor kan suggesties geven voor toevoegingen of wijzigingen aan bestanden die, eens goedgekeurd, direct geïntegreerd kunnen worden in de codebase.

Interageren met AI-modellen houdt privacy-risico’s in. Dit experiment vooral mogelijk omdat we werken met open-source code, gepubliceerde regelgeving, en de eveneens openbare documentatie daarvan, wat niet gevoelig is. Maar gezien alles wat zich in de IDE bevindt naar het taalmodel gestuurd kan worden, moeten we er nog steeds op letten dat we geen bestanden openen in de IDE die credentials, API keys of persoonlijke informatie bevatten. Dat blijft de verantwoordelijkheid van de individuele developer. Sowieso is het goede praktijk om voorbereid te zijn op het roteren van API keys of credentials, want in het heetst van een debugging-strijd is oversharing met een LLM snel gebeurd.

Tot slot moeten we vermelden dat dit experiment nog werd uitgevoerd met Cursor versies 1.6 en 1.7 in september-oktober 2025, met OpenAI’s GPT-4.5 en later GPT-5.0 als achterliggend taalmodel, gebruikt met een eigen API key (niet via Cursor). Latere versies hebben heel wat nieuwere features (waaronder meer agentic workflows) en het zou kunnen dat de ervaring vandaag (januari 2026) al heel anders zou zijn. De belangrijkste lessen blijven echter algemeen gelden voor alle AI-powered development, of dat nu via IDE, command line of beide gebeurt (vb. Anthropic Claude Code).

Als eerste stap voegen we de nodige documentatie toe aan ons project. Als case nemen we de Wet op de Maatschappelijke Integratie van 26 mei 2002. Samen met alle andere relevante wetten, koninklijke besluiten en omzendbrieven is die overzichtelijk geïnventariseerd op de website van de POD Maatschappelijke Integratie. Om de tekst gemakkelijk doorzoekbaar en interpreteerbaar te maken voor een LLM in een IDE, slaan we hem op als plat tekstbestand zonder opmaak, en dat voegen we toe aan een nieuw mapje voor relevant bronmateriaal in de source tree van het project. Of dat optimaal is, daar hebben we het raden naar, maar we moeten ergens beginnen.

Entiteiten

Entiteiten in OpenFisca drukken uit voor wie we de berekening maken. Dat kunnen individuen, gezinnen of andere groeperingen van mensen zijn (bedrijven, organisaties, …). Het zijn de basisbouwstenen waarvoor we later variabelen zullen kunnen specifiëren die samen een “situatie” vormen waarvoor we een berekening zullen kunnen doen. Person en Household zijn al aanwezig in de code. Een logische vraag is dus of we, op basis van de gegeven wettekst, andere entiteiten kunnen definiëren die nuttig zouden zijn.

Na het stellen van de vraag aan GPT-5 in Cursor, met de wettekst geselecteerd als context, wordt voorgesteld de volgende entiteiten toe te voegen:

  • Eligible Person for Societal Integration
  • Living Wage Recipient
  • Employment Project Participant

De voorgestelde aanpassingen aan de code zijn syntactisch correct. Geen van deze 3 zijn echter nuttig of noodzakelijk: het gaat in alledrie de gevallen om varianten van Person. De eigenschappen die maken dat ze bijvoorbeeld een leefloon zouden ontvangen, zijn veeleer variabelen toegevoegd aan de reeds bestaande Person entiteit. De waarde van die variabelen hangt bovendien af van andere variabelen die eveneens aan datzelfde individu gebonden zijn, zoals een inkomen uit werk of een handicapstatus. Entiteiten, die vooral dienen voor op zichzelf staande concepten, zijn hiervoor niet de juiste keuze.

Daarnaast lijkt GPT-5 het concept van een “rol” binnen een OpenFisca groepsentiteit verkeerd te hebben opgevat. Hij probeert “Eligible Person for Societal Integration” te construeren met verschillende “rollen” als onderdelen: “Belgian National”, “EU Citizen”, “Foreigner”, “Stateless”, “Refugee”… Dit ongetwijfeld omdat deze mogelijkheden verschijnen in Art.3, 3° lid, van de wet. In OpenFisca is een groepsentiteit echter samengesteld uit Personen die elk een rol krijgen. Een Household bevat zo Adult en Child rollen. Het is vrij nonsensicaal dat een EligiblePerson meerdere Foreigners zou kunnen bevatten. Nationaliteit of herkomst, of andere voorwaarden die gesteld worden in deze wet, zijn ook hier variabelen die gebonden zijn aan de persoon, geen entiteit op zich.

Op een ander moment werd nog een aparte entiteit gecreëerd voor het OCMW. Hoewel het logisch lijkt om de OCMWs te modelleren en als een entiteit te beschouwen – ze worden immers vermeld in de wet – is het dat hier (nog) niet. Er zijn immers geen verschillende types OCMWs met verschillende eigenschappen of rollen, waarvoor we telkens andere berekeningen moeten maken. In de context van deze wet, waarbij het de burger is voor wie we het recht op maatschappelijke steun berekenen, is het OCMW vooral een constant, invariant gegeven. In OpenFisca kunnen we dat dus vooralsnog overslaan. (Een entiteitstype “instituut” is ook niet voorzien.)

We merken hier dus dat Cursor niet “nee” kan antwoorden op de vraag of er nuttige andere entiteiten kunnen toegevoegd worden. Het kan de denkrichting achter die vraag niet bekritiseren of corrigeren uit eigen beweging. Doorheen het hele experiment bleken Cursor en GPT-5 ook een neiging te vertonen tot onnodige complexiteit. Dit is voor developers die met onbekende code of frameworks werken een groot risico: indien men te snel te ver meegaat met zulke suggesties, dreigt men later de pedalen te verliezen en achteraf erg moeilijke correcties te moeten aanbrengen aan de fundamenten van het project. Eens een verkeerde route is ingeslagen, blijkt het ook moeilijk om op de stappen terug te keren en deze weer te doen vergeten. Zeker als men ze eerst onwetend heeft toegelaten, komen ze terecht in de context en wordt er in vervolgvragen op verdergebouwd. Deze sluipende “context rot” is ondertussen een bekend probleem en een belangrijke oorzaak van tijdverlies met AI-enabled coding.

Variabelen

De kern van het model zit in de variabelen die de rechten en voorwaarden uit de wet voorstellen. Artikel 2 van de wet somt de verschillende vormen van maatschappelijke integratie op waarop iemand recht kan hebben (o.a. tewerkstelling, leefloon, geïndividualiseerd project). Artikel 3 bevat de voorwaarden waaraan een persoon moet voldoen om van dat recht gebruik te maken. We hebben deze bepalingen stap voor stap in code omgezet.

Recht op maatschappelijke integratie betekent in de praktijk dat een OCMW een persoon moet ondersteunen via (1) een job of opleiding, (2) een leefloon, of (3) een geïndividualiseerd project voor maatschappelijke integratie. Dit kan vertaald worden naar drie boolean variabelen op de Persoon-entiteit, bijvoorbeeld employment_right, living_wage_right en individualized_project_right. Cursor geeft hier een goede code-suggestie, en voorziet een eenvoudige placeholder-formule: zolang iemand “in aanmerking komt voor integratie” (een andere variabele) zou het recht gelden. We bekomen een definitie van employment_right als volgt:

class employment_right(Variable):
  value_type = bool
  entity = Person
  definition_period = MONTH
  def formula(person, period, parameters):
    return person("eligible_for_integration", period)

De invulling van deze placeholder-formule komt aan bod in het daaropvolgende Artikel 3. Die modelleert de volgende voorwaarden om in aanmerking te komen:

  • Verblijf in België (volgens de regels nader te bepalen bij KB).
  • Leeftijd: De persoon is meerderjarig (18+), of als minderjarige gelijkgesteld aan een meerderjarige volgens de uitzonderingen in deze wet.
  • Nationaliteit of verblijfsstatuut: De persoon is Belg, EU-burger (na 3 maanden verblijf), ingeschreven vreemdeling, staatloze, vluchteling of subsidiair beschermde.
  • Onvoldoende bestaansmiddelen
  • Werkbereidheid (tenzij onmogelijk om gezondheidsredenen of billijkheidsredenen).
  • Rechten uit andere stelsels uitgeput

Al deze voorwaarden komen samen in één centrale boolean variabele societal_integration_right. Die variabele geeft aan of iemand, gegeven zijn persoonlijke situatie, recht kan hebben op maatschappelijke integratie. In feite is dit de vertaalslag van “voldoet de persoon aan alle voorwaarden van art.3?”. De formule combineert alle subvoorwaarden:

class societal_integration_right(Variable):
  value_type = bool
  entity = Person
  definition_period = MONTH
  label = "Right to societal integration"
  def formula(person, period, parameters):
    residency = person("residency_status", period)
    is_major = person("is_major", period)
    nationality = person("nationality_status", period) in ["belgian", "eu_citizen", "registered_foreigner", "stateless", "refugee"]
    insufficient_income = not person("has_sufficient_income", period)
    willing_to_work = person("willing_to_work", period)
    claiming_benefits = person("claiming_benefits", period)
    return (residency and is_major and nationality and insufficient_income and willing_to_work and claiming_benefits)

Let hier vooral op enkele vreemde lacunes in de suggestie van Cursor. Zo is de naam van de variabele societal_integration_right niet gelijk aan de eerder gedefinieerde placeholder eligible_for_integration, hoewel dat wel de bedoeling is. Daarnaast wordt in de nationaliteitsvoorwaarde de mogelijkheid van subsidiair beschermden simpelweg vergeten! Tot slot is de zesde voorwaarde, dat men eerst zijn rechten laat gelden op eventuele sociale uitkeringen, wel erg rudimentair benoemd als claiming_benefits – een variabelenaam die niet echt dekt wat bedoeld wordt.

We kunnen deze suggestie dus wel aanvaarden, maar we zijn al direct verplicht om 3 correcties door te voeren. De niet-overeenkomst van de variabelenaam kunnen we daarbij nog gemakkelijk detecteren omdat de tests niet zullen werken als er nog ongedeclareerde variabelen in de code zitten. Een mankerend element in de formule, zoals een vergeten voorwaarde, is echter veel gemakkelijker over het hoofd gezien, en leidt wanneer dat ongedetecteerd blijft gegarandeerd tot fouten in de uitvoering. Hier merken we dus echt wel de noodzaak om terug te koppelen naar de wettekst om te verifiëren dat de gegenereerde code wel degelijk overeenkomt met wat de wettekst zegt. Deze terugkoppeling moet aandachtig genoeg gebeuren om ook ongelukkige benamingen of subtiele misinterpretaties van te tekst te kunnen identificeren.

Eventuele correcties kunnen daarnaast ook best zo snel mogelijk gebeuren. Als foutieve code in de editor aanwezig blijft, gaat ze immers deel uitmaken van de context die het AI-model gebruikt en dient ze zelf als fundament voor daaropvolgende suggesties. Dit kan leiden tot een situatie waarbij men suggesties blijft ontvangen waarin steeds dezelfde fouten terugkomen, die men dus ook telkens weer moet corrigeren, wat niet bevorderlijk is voor de productiviteit.

De variabelen gebruikt in de formula() methode van societal_integration_right hierboven, moeten uiteraard op hun beurt ook gedefinieerd worden: voor elk van deze variabelen moeten we een klasse schrijven. Dit kan aanleiding geven tot complexe kettingen van afhankelijkheden. Zo zou is_major een eenvoudige booleaanse inputvariabele kunnen zijn, maar we kunnen dat ook berekenen op basis van de datum van vandaag en weer een nieuwe variabele birthdate. De berekening van de formule van de variabelen kan daarnaast ook gebruik maken van de parameters van een wet – zo is de meerderjarigheid in België pas vanaf 18 jaar sinds 1 mei 1990. Dat zou ons dan weer bij het Burgerlijk Wetboek brengen, en haar geschiedenis – om het beknopt te houden gaan we daar nu niet verder op in.

Laatste opmerking: het model zoals hier gebouwd is uiteraard een vereenvoudigde weerspiegeling. Merk wel op dat we zelfs dan, slechts 3 artikels ver in een wet, al snel 10 Python-klassen hebben gedefinieerd hebben, met potentieel voor meer als we echt in de diepte zouden willen gaan. Cursor en GPT-5 schrijven daarbij relatief verbose code, met vele hulpvariabelen en -methodes, die soms echt wel eenvoudiger kan. Sommige details uit de wet, zoals de 3-maanden wachttijd voor EU-burgers, of de uitzonderingen die bestaan voor bepaalde categorieën van minderjarigen (Art. 7), zouden in een volwaardig model nog heel wat extra variabelen of condities vergen.

AI en code: enkele valkuilen

Wat betreft best practices voor de inzet van AI-hulp bij zulke projecten, identificeren we nog enkele valkuilen, naast diegene die we tot nu toe al genoemd hebben.

Teveel documentatie toevoegen in het begin leidt snel tot “context confusion“, waarbij de suggesties of de antwoorden van de LLM gebaseerd gaan zijn op stukken informatie die (nog) niet relevant zijn. Het is beter de documentatie geleidelijk toe te voegen, in gelijke tred met de functionaliteit, in plaats van de volledige analyse en achtergrond op voorhand toe te voegen aan de IDE. In het geval van regelgeving: voeg de regels artikel per artikel toe aan de IDE, naarmate de projectontwikkeling vordert, en weersta de verleiding om de hele wettekst op voorhand als “encyclopedische referentie” te integreren in de IDE.

Context rot of context poisoning ontstaat dan weer wanneer de AI een verkeerde weg is ingeslagen,  daarop voortboomt, en uiteindelijk relevantere informatie vergeet zodat het ook moeilijker wordt om ervan te herstellen. “Context quarantining“, het opdelen van het probleem in kleinere deelproblemen elk met hun eigen context, is daarvoor een logische remedie. Dit is ook de weg die de meeste “deep research” of “multi-agentic” systemen inslaan. In een IDE zou dat impliceren dat een AI-systeem de codebase en de documentatie vanaf een zekere grootte zou moeten segmenteren. Hoe dat technisch uitgewerkt kan worden achter de schermen lijkt een uitdaging van formaat, en verschillende IDEs zullen daar in de nabije toekomst waarschijnlijk hun eigen approach voor ontwikkelen.

Een andere frustratie was dat de AI soms code of bestanden verkeerd plaatste of aannam dat bepaalde dingen bestonden. Zo refereerden gegenereerde formules naar variabelen die nog helemaal niet gedefinieerd waren. Dit zorgt bij het testen natuurlijk voor foutmeldingen. We moesten de AI dan bijsturen of zelf extra variabelen invoegen om die referenties af te dekken. Ook kleine zaken, zoals de formattering van documentatie of het wel/niet aanmaken van noodzakelijk imports, vergden soms manuele correctie. Dit soort inconsistenties tonen aan dat je AI-suggesties niet blindelings kunt vertrouwen. Een developer moet voortdurend valideren of de code die gegenereerd wordt strookt met de bedoeling, en zo niet, onmiddellijk ingrijpen.

publi.codes

We willen ook nog wijzen op het bestaan van publi.codes als eventueel alternatief voor OpenFisca. Recenter en moderner, moeten de regels daar gecodeerd worden in een YAML-formaat, wat veel hanteerbaarder is dan het schrijven van subklassen in Python, en veel leesbaarder voor niet-developers. Men is in ruil daarvoor echter wel beperkt tot de bewerkingen die zijn toegelaten door de achterliggende motor. Pas vanaf de nog in ontwikkeling zijnde versie 2 komen daar mogelijkheden bij om barema’s te encoderen, of abattementen (vrijgestelde bedragen), die in België erg veelvuldig voorkomen.

De huidige versie van publi.codes is bovendien afhankelijk van het NPM ecosysteem dat tegenwoordig regelmatig geplaagd wordt door supply chain aanvallen. Publi.codes v2 zou dan weer gecompileerd worden naar OCaml, een programmeertaal die we bij Smals niet gebruiken. Gezien de kans klein is dat Smals deze programmeertaal zou willen introduceren in haar portfolio (en een ondersteunend team ervoor zou willen uitbouwen), leek het weinig nuttig om voor deze oefening ook publi.codes in de diepte te bekijken. Het valt echter wel op dat op het vlak van UI-componenten, publi.codes wel enkele libraries heeft klaarliggen.

Conclusie

Zowel OpenFisca als publi.codes zijn als systeem vooral sterk wanneer je regels kunt modelleren als expliciete, testbare berekeningen. Minder ideaal is het voor reglementering die zich bedient van discretionaire beslissingen, vrije interpretatie, uitzonderingen zonder heldere parameters, of “case management”-workflows. Het zijn primair reken- en regelsystemen, geen dossierbehandelingsplatformen. Daarmee zijn ze eventueel wel geschikt als motor voor apps die belastingen of uitkeringen op niveau van persoon/huishouden kunnen berekenen (recht op iets + bedrag), of om beleidsimpact te simuleren van eventuele wijzigingen (“wat kost deze hervorming?”, “wie wint/verliest?”). Dat kan voor beleidsmakers én burgers interessant zijn.

Toch is een OpenFisca-project niet snel even opgezet. Conceptueel is OpenFisca enigszins verwarrend voor een developer: hoewel OpenFisca gebruikmaakt van Python-klassen, dienen deze niet om objecten te modelleren, maar om entiteiten, variabelen en berekeningsregels uit de regelgeving declaratief vast te leggen. Gegeven dat er 1 klasse per variabele moet geschreven worden, en er vlotjes tientallen variabelen kunnen meespelen in een fijnmazig wetsartikel, zit men met een snel groeiende stapel code die een uitdaging is om overzichtelijk georganiseerd te krijgen. Daarnaast vergt ook de ontwikkeling van een GUI veel extra werk. Het project mist nog de nodige tooling om deze recurrente problematieken te verlichten. (Het helpt natuurlijk niet wanneer de opdrachtgevende overheid in 2020 plots de kraan dichtdraait, schijnbaar van mening dat open-source projecten per definitie zelfbedruipend kunnen zijn.)

Tot slot kunnen we nog zeggen dat dit experiment tegelijk een nuttige en leerzame reality check was over wat LLMs kunnen bijdragen, en kunnen verknoeien, aan een developer-werkomgeving. Zelf de regie stevig in handen blijven houden en werken met kleine incrementele stapjes, blijft de beste raad. De ene AI tool zal daarbij al wat minder steken laten vallen dan de andere op allerlei vlakken. Het geven van negatieve antwoorden of het detecteren van fouten in de vraagstelling blijft erg uitdagend voor LLMs en dat brengt wat risico met zich mee. AI-assistentie in IDEs evolueert echter razendsnel, en een gelijkaardig experiment zal volgend jaar ongetwijfeld anders verlopen.

Rules As Code betekent zeker niet dat we vandaag een wettekst aan een AI kunnen geven om er een programma te laten uitrollen. Wel zal er op gespecialiseerde fora de komende jaren ongetwijfeld veel aandacht gaan naar de interactie tussen wet, implementatie, en AI-tooling. Vooralsnog blijft de complexiteit van de regelgeving zelf, ook met steeds betere AI, de grootste hinderpaal voor Rules As Code projecten.

______________________

Dit is een ingezonden bijdrage van Joachim Ganseman, IT consultant bij Smals Research. Dit artikel werd geschreven in eigen naam en neemt geen standpunt in namens Smals.

 

 

Bron: Smals Research