§

Voor de DevOps-engineer

De beslisservice bouwen, uitrollen en draaiend houden

Je beheert een beslisservice, een HTTP-dienst gebouwd uit RegelSpraak-bronnen (.rgs) en een JSON-descriptor. De dienst staat in regelspraak-toolchain, de eerste afnemer van uchar en de opvolger van regelspraak-java: een distributie zonder eigen semantiek, die uchar als git-submodule meedraagt en daaruit de RegelSpraak-frontend en de Drools-backend van uchar bouwt. De commit van de submodule is de pin, en die moet op de main van uchar staan. De dienst vuurt via backends/drools van uchar, dezelfde Drools-backend die gate 12 met de referentie-interpreter en Clingo in het differentiaalvierkant houdt (§ 10).

For the DevOps engineer

Building, deploying and running the decision service

You operate an HTTP decision service built from RegelSpraak sources (.rgs) and a JSON descriptor. The service lives in regelspraak-toolchain, uchar's first consumer and the successor of regelspraak-java: a distribution without semantics of its own, which carries uchar as a git submodule and builds uchar's RegelSpraak frontend and Drools backend from it. The submodule's commit is the pin, and it must be a commit on uchar's main. The service fires through uchar's own backends/drools, the same Drools backend that gate 12 holds to the reference interpreter and Clingo in the differential square (§ 10).

§ 1 wat what

Van bronnen en descriptor naar een HTTP-dienst

De descriptor (<naam>.service.json) staat naast de .rgs-bronnen en legt vast welke regels onder welk pad draaien, zonder eigen RegelSpraak-betekenis. HTTP loopt via com.sun.net.httpserver uit de JDK. Vóór de poort opengaat, gebeurt dit:

  1. de bronnen (of één uchar-ir-v1-document), de descriptor en de parameterset-sidecar lezen;
  2. de samengevoegde bron resolven;
  3. de descriptor valideren, met alle schendingen tegelijk, en de standaarden in parameters toetsen aan hun declaratie;
  4. per entrypoint de vormen nemen die de scope noemt en die als uchar-ir-v1-document door de Drools-backend van uchar laten bouwen, op de exec-model-tak: fact-beans in het geheugen gecompileerd, dan een KieBase; wat uchar weigert, maakt het entrypoint tot marker. Voor een entrypoint dat uitlegt (het descriptorveld explanation, standaard aan) volgt een tweede build van hetzelfde document, met lezingen; daarop vuurt een verzoek met ?explanation=true (§ 4);
  5. voor een entrypoint dat uchar draagt, de scope op volledigheid controleren;
  6. parametersets laden.

De schendingen van stap 4 en 5 verzamelt de dienst over alle entrypoints. Na het laatste entrypoint stopt hij met exit 1 bij een onopgeloste naam of een onvolledige scope, en anders met exit 3 bij een marker, tenzij markers zijn toegestaan (§ 3). Na stap 6 zijn de KieBases onveranderlijk; elk verzoek krijgt een eigen sessie.

De root-pom.xml is een Maven-reactor; alle modules staan op 0-SNAPSHOT. Hij bouwt uit de submodule uchar/ de Drools-backend en de RegelSpraak-frontend van uchar, en daarnaast de eigen modules toolchain (CLI, engine-facade, scenariorunner), service en service-quarkus. De module service bundelt alles in één jar, die bin/regelspraak draait. Een checkout heeft vóór elke build de submodule nodig (git submodule update --init --recursive). De eerste build op een machine haalt eenmaal de Maven-afhankelijkheden op (mvn -B -DskipTests package); daarna bouwt make verify offline: eerst make check-pin, dan mvn -B -o verify -Dmaven.test.skip.exec=true. Die eigenschap slaat de testsuite van de Drools-backend van uchar over; de suites van de eigen modules draaien wel. uchar draait zijn suites voordat een commit op zijn main landt, en de pin is altijd zo'n commit.

Buiten Docker start je de dienst met bin/regelspraak serve <descriptor.service.json> <source.rgs…> [--allow-markers] [--detail] [--host H] [--port N] of met make serve FILE="…" PORT=8080. In plaats van .rgs-bronnen mag er ook één uchar-ir-v1-document staan, bijvoorbeeld uit een andere frontend; dat wordt in zijn geheel geserveerd: stap 4 bouwt het één keer, en de scope van elk entrypoint moet al zijn vormen dekken. Paden zijn relatief aan regelspraak-toolchain, in § 10 aan uchar.

From sources and a descriptor to an HTTP service

The descriptor (<name>.service.json) sits next to the .rgs sources and states which rules run under which path, without adding RegelSpraak meaning of its own. HTTP runs on the JDK's com.sun.net.httpserver. Before the port opens, the service:

  1. reads the sources (or one uchar-ir-v1 document), the descriptor and the parameter-set sidecar;
  2. resolves the merged source;
  3. validates the descriptor, collecting all violations at once, and checks the defaults in parameters against their declarations;
  4. for each entrypoint, takes the forms its scope names and has uchar's Drools backend build them as a uchar-ir-v1 document, on the exec-model branch: fact beans compiled in memory, then a KieBase; whatever uchar refuses makes the entrypoint a marker. An entrypoint that explains (the descriptor field explanation, on by default) gets a second build of the same document, with readings; a request with ?explanation=true fires on that one (§ 4);
  5. for an entrypoint uchar carries, checks the scope for completeness;
  6. loads the parameter sets.

The service collects the violations of steps 4 and 5 over all entrypoints. After the last entrypoint it stops with exit 1 on an unresolved name or an incomplete scope, and otherwise with exit 3 on a marker, unless markers are allowed (§ 3). After step 6 the KieBases are immutable; every request gets its own session.

The root pom.xml is a Maven reactor; all modules are at 0-SNAPSHOT. It builds uchar's Drools backend and RegelSpraak frontend from the submodule uchar/, plus the modules toolchain (CLI, engine facade, scenario runner), service and service-quarkus. The service module bundles everything into one jar, which bin/regelspraak runs. A checkout needs the submodule before any build (git submodule update --init --recursive). The first build on a machine fetches the Maven dependencies once (mvn -B -DskipTests package); after that make verify builds offline: first make check-pin, then mvn -B -o verify -Dmaven.test.skip.exec=true. That property skips the test suite of uchar's Drools backend; the suites of the toolchain's own modules do run. uchar runs its suites before a commit lands on its main, and the pin is always such a commit.

Outside Docker you start the service with bin/regelspraak serve <descriptor.service.json> <source.rgs…> [--allow-markers] [--detail] [--host H] [--port N] or with make serve FILE="…" PORT=8080. Instead of .rgs sources it also takes one uchar-ir-v1 document, for example from another frontend; that document is served whole: step 4 builds it once, and every entrypoint's scope must cover all of its forms. Paths are relative to regelspraak-toolchain, and in § 10 to uchar.

1 bronnen en descriptor lezen read sources and descriptor volgorde uit SOURCES_MANIFEST; sidecar naast de descriptor order from SOURCES_MANIFEST; sidecar next to the descriptor 2 samengevoegde bron resolven resolve the merged source resolverfout → MALFORMED, exit 1 resolver error → MALFORMED, exit 1 3 descriptor valideren validate the descriptor alle schendingen in één ServiceError all violations in one ServiceError 4 per entrypoint: scope → DroolsBackend van uchar per entrypoint: scope → uchar's DroolsBackend beans en KieBase in het geheugen: een JDK in het image beans and KieBase in memory, hence a JDK in the image 5 entrypoint dat uchar draagt: scope volledig? entrypoint uchar carries: scope complete? scope onvolledig → INCOMPLETE_SCOPE, exit 1 scope incomplete → INCOMPLETE_SCOPE, exit 1 6 parametersets laden load parameter sets overlap is een startfout; een periodegat een waarschuwing overlap is a start failure; a period gap a warning poort 8080 gaat open: GET /info, POST /<pad> port 8080 opens: GET /info, POST /<pad> KieBases onveranderlijk · sessie per verzoek KieBases immutable · one session per request exit 2 exit 2 foute argumenten, vóór stap 1 wrong arguments, before step 1 exit 1 exit 1 elke andere startfout; stderr any other start failure; stderr exit 3 exit 3 alleen markers in de scope markers only in the scope
De startvolgorde: alles gebeurt vóór de poort bindt, zodat een antwoord op /info een geslaagde start betekent en de probes daarop kunnen wachten. Een fout stopt de container met de exitcode rechts. Met ALLOW_MARKERS=true start de dienst ondanks markers, en krijgt elk verzoek aan zo'n entrypoint 503 met het MARKER-antwoord.
The start-up order: everything happens before the port binds, so an answer on /info means a successful start and the probes can wait for it. A failure stops the container with the exit code on the right. With ALLOW_MARKERS=true the service starts despite markers, and every request to such an entrypoint gets 503 with the MARKER answer.
§ 2 descriptor descriptor

De descriptor en zijn velden

Een JSON-Schema is er niet; de velden volgen uit de code. Het hoogste niveau kent service (name verplicht), entrypoints, parameters (defaults, per verzoek te overschrijven) en parameterSets (bestandsnaam van een sidecar, § 6). De .rgs-bronnen staan er niet in; in de container komen ze via SOURCES_MANIFEST (§ 3).

Entrypoint-veldVerplichtBetekenis
namejanaam van het entrypoint
pathjawordt POST /<pad>; kebab-case, zonder slash vooraan
scope.ruleNames, scope.ruleGroupsminstens éénde vormen die het entrypoint vuurt: regels, rijen van beslistabellen, defaults-groepen en de andere vormen van de genoemde regels en regelgroepen; een genoemde regel zonder vorm in het model van uchar is een marker
inputneeobjecttypen waarvan elk verzoek minstens één instantie levert
outputneeObjecttype.attribuut of Objecttype:kenmerk voor het antwoord
inputElementsneedoor de aanroeper geleverd; de scope-controle slaat ze over
scopeCheckOptOutneetrue zet de scope-controle uit
vocabularyneehet vocabulaire waarin het entrypoint antwoordt als het verzoek er geen kiest (§ 5); een onbekend vocabulaire laat de dienst niet starten
explanationneeafwezig of true: bij de start ook een build met lezingen voor de uitleg (§ 4); false: alleen de build die antwoordt, en een verzoek om uitleg krijgt 400 UNSUPPORTED

Een onbekende naam in scope, input of output is een startfout, net als een tijdsafhankelijk kenmerk in output en een standaard in parameters met meer decimalen dan de parameter declareert. De scope-volledigheidscontrole weigert de start (INCOMPLETE_SCOPE) als een vorm in de scope, van welke soort ook, een element leest dat alleen buiten de scope ontstaat.

Onbekende velden worden genegeerd

Elk descriptor-record negeert onbekende velden. Een verkeerd gespeld veld geeft geen foutmelding en heeft geen effect; controleer na de start op /info of entrypoints en parameters kloppen.

De Jutterswet-fixture uit de servicetests, met de bron ingekort:

The descriptor and its fields

There is no JSON Schema; the fields follow from the code. The top level has service (name required), entrypoints, parameters (defaults that a request can override) and parameterSets (file name of a sidecar, § 6). The .rgs sources are not in it; in the container they come through SOURCES_MANIFEST (§ 3).

Entrypoint fieldRequiredMeaning
nameyesname of the entrypoint
pathyesbecomes POST /<pad>; kebab-case, no leading slash
scope.ruleNames, scope.ruleGroupsat least onethe forms the entrypoint fires: rules, decision-table rows, defaults groups and the other forms of the named rules and rule groups; a named rule without a form in uchar's model is a marker
inputnoobject types of which every request supplies at least one instance
outputnoObjecttype.attribuut or Objecttype:kenmerk for the response
inputElementsnosupplied by the caller; the scope check skips them
scopeCheckOptOutnotrue switches the scope check off
vocabularynothe vocabulary the entrypoint answers in when the request chooses none (§ 5); an unknown vocabulary stops the service from starting
explanationnoabsent or true: at startup also a build with readings for the explanation (§ 4); false: only the build that answers, and a request for an explanation gets 400 UNSUPPORTED

An unknown name in scope, input or output is a startup error, and so are a time-dependent characteristic in output and a default in parameters with more decimals than the parameter declares. The scope completeness check refuses to start (INCOMPLETE_SCOPE) when a form in scope, of whatever kind, reads an element that is only produced outside the scope.

Unknown fields are ignored

Every descriptor record ignores unknown fields. A misspelled field produces no error and has no effect; after startup, check on /info that the entrypoints and parameters are right.

The Jutterswet fixture from the service tests, with its source abridged:

JSONservice/src/test/resources/chain/jutterswet.service.json
{
  "service": { "name": "Jutterswet strandvondsten", "version": "1.0.0" },
  "entrypoints": [
    {
      "name": "bergloon van een strandvondst",
»      "path": "bergloon",
      "scope": { "ruleNames": ["bergloon van een strandvondst"] },
      "input": ["Strandvondst"],
      "output": ["Strandvondst.bergloon"]
    },
    {
      "name": "meldplicht van een wrakstuk",
      "path": "meldplicht",
      "scope": { "ruleNames": ["meldplicht van een wrakstuk"] },
      "input": ["Wrakstuk"],
      "output": ["Wrakstuk:meldplichtig", "Wrakstuk:aangemeld"]
    },
    {
      "name": "bergingsplicht van een strandvondst",
      "path": "bergingsplicht",
      "scope": { "ruleNames": ["bergingsplicht van een strandvondst"] },
      "input": ["Strandvondst"],
      "output": ["Strandvondst:bergingsplichtig"]
    }
  ]
}
RegelSpraakservice/src/test/resources/chain/jutterswet-strandvondsten.rgs
Objecttype de Strandvondst (mv: Strandvondsten)
	de vondstwaarde Numeriek (niet-negatief geheel getal) met eenheid eur;
	het bergloon Numeriek (niet-negatief getal met 2 decimalen) met eenheid eur;
…
Regel bergloon van een strandvondst
	geldig altijd
»		Het bergloon van een Strandvondst moet berekend worden als zijn vondstwaarde maal 0,25
		indien de Strandvondst aangemeld is.
§ 3 opzet container

De opzet van de container

make image bouwt met docker/Dockerfile het image decision-service:dev (IMAGE_REPO, TAG). Dat vraagt Docker, netwerktoegang (Maven haalt in de build zijn plugins en afhankelijkheden op) en een checkout met de uchar-submodule; de build draait één Maven-reactorbuild van de module service.

De runtime-laag is eclipse-temurin:21-jdk-jammy, een JDK: de dienst compileert fact-beans in het geheugen en stopt op een JRE met geen JDK-compiler beschikbaar. Het image luistert op poort 8080 en heeft een HEALTHCHECK op /info. Het entrypoint-script start de dienst zo:

The container contract

make image builds the image decision-service:dev (IMAGE_REPO, TAG) from docker/Dockerfile. This needs Docker, network access (Maven fetches its plugins and dependencies inside the build) and a checkout with the uchar submodule initialised; the build runs one Maven reactor build of the service module.

The runtime layer is eclipse-temurin:21-jdk-jammy, a JDK: the service compiles fact beans in memory, and on a JRE it stops with geen JDK-compiler beschikbaar (no JDK compiler available). The image listens on port 8080 and has a HEALTHCHECK on /info. The entrypoint script starts the service like this:

shdocker/entrypoint.sh
exec /app/bin/regelspraak serve "$DESCRIPTOR" $SOURCES --port "$PORT" $FLAGS
VariabeleDefaultEffect
PORT8080HTTP-poort
DESCRIPTORvoorbeeld in /app/rules/pad naar de .service.json
SOURCES_MANIFESTvoorbeeld in /app/rules/één .rgs-pad per regel, in volgorde, of één uchar-ir-v1-document; paden zonder spaties
ALLOW_MARKERSfalsetrue start met entrypoints die uchar niet draagt; elk verzoek daaraan krijgt 503
PLAYGROUNDniet gezettrue opent /playground; niet voor productie
DETAILniet gezettrue beantwoordt de vlaggen trace, coverage en fixpoint (§ 4); niet voor productie, want die velden tonen elke tussenwaarde
JAVA_OPTSniet gezetJVM-opties, bijvoorbeeld -Xmx1g
REGELSPRAAK_JAR/app/regelspraak.jardoor het image gezet; niet wijzigen

PLAYGROUND, DETAIL en JAVA_OPTS horen niet bij het containercontract: entrypoint.sh en de chart geven ze niet door, maar de dienst en de JVM lezen ze zelf uit de omgeving, dus docker run -e DETAIL=true werkt. Andere variabelen leest de container niet, en voor een variabele die niet gezet is, geldt de standaardwaarde uit de tabel. Een logniveau is er niet; zonder logframework gaat alles naar stdout en stderr.

De regelset is ingebakken; DESCRIPTOR_SRC vervangt bij de build de descriptor. Een volume op /app/rules met eigen DESCRIPTOR en SOURCES_MANIFEST serveert een andere regelset, maar de chart kent geen volumes (§ 7). Het standaardimage bakt de TOKA-casus in, een fictieve wet uit de ALEF-cursus van de Belastingdienst; de Belastingdienst liet op 13 oktober 2026 weten dat die casus vrij te geven bedoeld is, een schriftelijke bevestiging ontbreekt nog. DESCRIPTOR_SRC vervangt alleen de descriptor; de ingebakken regels blijven. Een eigen regelset mount je op /app/rules met eigen DESCRIPTOR en SOURCES_MANIFEST, of je bouwt er een eigen image voor.

Een marker is een entrypoint dat uchar niet draagt: de emitter, de loader of de Drools-backend van uchar weigert het, bijvoorbeeld omdat de exec-model-tak een construct niet lowert, of een regelnaam in de scope levert geen vorm op. Een marker stopt de start met exit 3; met ALLOW_MARKERS=true start de dienst wel en antwoordt dat entrypoint met 503. Een onopgeloste naam, die de resolver aan geen declaratie bond, is geen marker maar een fout in de bron: exit 1, ook met ALLOW_MARKERS=true. Startfouten stoppen de container vóór de poort opengaat:

SituatieExitUitvoer
descriptor- of manifestbestand ontbreekt1descriptor niet gevonden: … of bronnen-manifest niet gevonden: …
foute argumenten voor ServiceMain2melding en gebruiksregel
entrypoints die uchar niet draagt, en geen andere startfout3per marker MARKER: … met de zin van uchar
elke andere startfout: I/O, resolve, descriptor, onopgeloste naam, scope, parametersets, poort binden1de schendingen van de eerste stap die faalt (§ 1); een mislukte compilatie van de fact-beans geeft ook exit 1, met een stacktrace
VariableDefaultEffect
PORT8080HTTP port
DESCRIPTORexample in /app/rules/path to the .service.json
SOURCES_MANIFESTexample in /app/rules/one .rgs path per line, in order, or one uchar-ir-v1 document; no spaces in paths
ALLOW_MARKERSfalsetrue starts with entrypoints uchar does not carry; every request to one gets 503
PLAYGROUNDnot settrue opens /playground; not for production
DETAILnot settrue answers the flags trace, coverage and fixpoint (§ 4); not for production, since those fields show every intermediate value
JAVA_OPTSnot setJVM options, for example -Xmx1g
REGELSPRAAK_JAR/app/regelspraak.jarset by the image; do not change

PLAYGROUND, DETAIL and JAVA_OPTS are not part of the container contract: entrypoint.sh and the chart do not pass them on, but the service and the JVM read them from the environment themselves, so docker run -e DETAIL=true works. The container reads no other variables, and a variable that is not set takes the default from the table. There is no log level; without a logging framework, everything goes to stdout and stderr.

The rule set is baked in; DESCRIPTOR_SRC replaces the descriptor at build time. A volume on /app/rules with your own DESCRIPTOR and SOURCES_MANIFEST serves a different rule set, but the chart has no volumes (§ 7). The default image bakes in the TOKA casus, a fictitious act from the Belastingdienst's ALEF course; on 13 October 2026 the Belastingdienst indicated that the casus is meant to be releasable, and written confirmation has not yet been received. DESCRIPTOR_SRC replaces only the descriptor; the baked rules stay. For your own rule set, mount it on /app/rules with your own DESCRIPTOR and SOURCES_MANIFEST, or build an image of your own.

A marker is an entrypoint that uchar does not carry: uchar's emitter, loader or Drools backend refuses it, for example because the exec-model branch does not lower a construct, or a name in the scope yields no form. A marker stops the start with exit 3; with ALLOW_MARKERS=true the service starts and that entrypoint answers 503. An unresolved name, which the resolver bound to no declaration, is not a marker but a fault in the source: exit 1, also with ALLOW_MARKERS=true. Startup errors stop the container before the port opens:

SituationExitOutput
descriptor or manifest file missing1descriptor niet gevonden: … or bronnen-manifest niet gevonden: …
invalid arguments to ServiceMain2message and usage line
entrypoints uchar does not carry, and no other startup error3MARKER: … per marker, with uchar's sentence
any other startup error: I/O, resolve, descriptor, unresolved name, scope, parameter sets, port binding1the violations of the first step that fails (§ 1); a failed fact-bean compilation also exits 1, with a stack trace
§ 4 endpoints endpoints

Endpoints en statuscodes

Endpoints and status codes

MethodeMethod PadPath VoorwaardeCondition SuccesSuccess FoutenErrors
POST/<pad>per entrypoint200400, 404, 405, 422, 500, 503
GET/info—200405, 500
GET/playgroundPLAYGROUND=true200405, 500
POST/playground/analysePLAYGROUND=true200400, 405, 500, 503 (15 s)
*elk ander padany other path——404 {"error":"onbekend pad: '<pad>'"}

Op POST /<pad> geeft een lege of ongeldige body of een verzoek dat niet valideert 400, een andere methode 405, een fout in de regels of in de waarden van deze invoer, een grens van de Drools-backend van uchar of een sessie zonder fixpunt 422 (§ 5), een onverwachte exceptie 500 en een entrypoint dat uchar niet draagt 503 (alleen na een start met ALLOW_MARKERS=true). Antwoorden zijn application/json; het Content-Type van het verzoek wordt niet gecontroleerd.

?explanation=true voegt een array explanation toe met een Nederlandse zin per toegekend en niet-toegekend kenmerk, per defaults-groep en per waarde die een gewone regel berekent, met de waarden die de regel las en, waar de regels er een noemen, de grondslag. Een zin noemt ook de periode of cel; een lege waarde heet er leeg, een afwezigheidsmarker staat er bij naam. Consistenties en creaties krijgen geen zin. Het verzoek vuurt dan één keer, op de build met lezingen van het entrypoint, en de zinnen komen uit de lezingen van die sessie. ?explanationParts=true voegt explanationParts toe: per zin een boom van stukken met hun soort, waarvan de bladeren samen de zin vormen, voor een afnemer die de uitleg zelf opmaakt. Zonder deze vlaggen is het antwoord byte-identiek.

Een dienst die met DETAIL=true of --detail gestart is, beantwoordt drie vlaggen die de sessie tonen: ?trace=true geeft per vorm die vuurde de ids die zijn regels bonden en hoe vaak ze vuurden, ?coverage=true per vorm van het entrypoint FIRED of NOT_FIRED, en ?fixpoint=true het fixpunt: elke waarde met haar herkomst (input, derived of creation), de gemaakte objecten, de koppelingen, de fouten en de inconsistenties. Zonder DETAIL krijgt elk van die vlaggen 400 UNSUPPORTED met de vlag als field.

/info beschrijft de draaiende dienst:

On POST /<pad>, an empty or malformed body or a request that fails validation returns 400, any other method 405, a fault in the rules or in the values of this input, a limit of uchar's Drools backend or a session without a fixpoint 422 (§ 5), an unexpected exception 500 and an entrypoint uchar does not carry 503 (only after a start with ALLOW_MARKERS=true). Responses are application/json; the request's Content-Type is not checked.

?explanation=true adds an array explanation with one Dutch sentence per assigned and per withheld characteristic, per defaults group and per value an ordinary rule computes, with the values the rule read and, where the rules name one, the legal ground. A sentence also names the period or cell; an empty value is called leeg (empty) there, and an absence marker appears by name. Consistencies and creations get no sentence. The request then fires once, on the entrypoint's build with readings, and the sentences come from the readings of that session. ?explanationParts=true adds explanationParts: per sentence a tree of parts with their kind, whose leaves together form the sentence, for a consumer that lays out the explanation itself. Without these flags the response is byte-identical.

A service started with DETAIL=true or --detail answers three flags that show the session: ?trace=true gives, per form that fired, the ids its rules bound and how often they fired, ?coverage=true gives FIRED or NOT_FIRED per form of the entrypoint, and ?fixpoint=true gives the fixpoint: every value with its origin (input, derived or creation), the created objects, the links, the faults and the inconsistencies. Without DETAIL each of those flags gets 400 UNSUPPORTED with the flag as its field.

/info describes the running service:

GET /infoschematischschematic
{ "service":        { "name", "version" },
  "entrypoints":    [ { "name", "path", "vocabulary"?, "executable", "refusal"?,
                        "coverage": [ { "kind", "name", "verdict", "reason"? } ],
                        "markers": [ … ] } ],
  "skipped":        [ … ],
  "parameters":     { … },
  "parameterSets":  [ { "name", "values": { "<param>": [ { "from", "to", "value" } ] } } ],
  "periodWarnings": [ … ],
  "vocabularies"?:  [ { "name", "default", "language"?, "complete",
                        "withoutDesignation": [ { "kind", "owner"?, "name" } ] } ] }

coverage is het oordeel van de exec-model-tak van uchar per vorm; de soorten in kind zijn de sleutels van uchar (regel, defaults, …). Omdat uchar geen entrypoint bouwt met een vorm die het niet lowert, toont een uitvoerbaar entrypoint alleen LOWERED. Een entrypoint dat uchar weigert, heeft "executable": false, de zin van uchar in refusal en het construct in markers; skipped verzamelt die van de hele dienst en is dus alleen gevuld na een start met ALLOW_MARKERS=true.

Er is geen aparte health-, readiness- of OpenAPI-route. Probes gaan naar /info: de poort gaat pas open na de compilatie, dus een antwoord betekent een geslaagde start. De OpenAPI-specificatie ontstaat alleen tijdens mvn test en zit niet in het image, want de image-build slaat tests over. Heeft het model vocabulaires, dan schrijft OpenApiEmitter één document per vocabulaire, in de namen van dat vocabulaire, met de andere spellingen die de dienst ook accepteert in x-spellings en x-valueSpellings. Elke operatie documenteert de vlaggen van uitleg en detail, en het 422-antwoord als EvaluationFault, NoFixpoint of Limit.

coverage is the verdict of uchar's exec-model branch per form; the kinds in kind are uchar's keys (regel, defaults, …). Since uchar builds no entrypoint with a form it does not lower, an executable entrypoint shows only LOWERED. An entrypoint uchar refuses has "executable": false, uchar's sentence in refusal and the construct in markers; skipped collects those of the whole service and is therefore only filled after a start with ALLOW_MARKERS=true.

There is no separate health, readiness or OpenAPI route. Probes go to /info: the port only opens after compilation, so any response means startup succeeded. The OpenAPI specification is generated only during mvn test and is not in the image, because the image build skips tests. If the model declares vocabularies, OpenApiEmitter writes one document per vocabulary, in that vocabulary's names, with the other spellings the service also accepts in x-spellings and x-valueSpellings. Every operation documents the explanation and detail flags, and the 422 response as EvaluationFault, NoFixpoint or Limit.

§ 5 aanvraag request

Een aanvraag en het antwoord

A request and its response

verzoekrequestschematischschematic
{ "asOfDate"?:   "yyyy-MM-dd",
  "parameters"?: { "<parameternaam>": <waarde>, … },
  "instances": [
    { "type": "<objecttype>", "id": "<label>",
      "attributes"?:      { "<attribuut>": <waarde>, … },
      "characteristics"?: { "<kenmerk>": true|false | { "periods": [ … ] }, … },
      "roles"?:           { "<rol>": "<id>" | ["<id>", …], … } } ],
  "relations"?: [ { "factType": "<naam>", "roles": { "<rol>": "<id>", … } } ] }
antwoordresponseschematischschematic
{ "instances": [
    { "id": "<label>", "type": "<objecttype>",
      "attributes"?:      { "<attribuut>": <waarde|null>, … },
      "characteristics"?: { "<kenmerk>": true|false, … } }, … ],
  "inconsistencies": [ "<regelnaam>", … ],
  "inconsistencyDetails": [ { "rule": "<regelnaam>", "subject": "<id>"|null }, … ],
  "explanation"?: [ … ], "explanationParts"?: [ … ],
  "trace"?: [ … ], "coverage"?: [ … ], "fixpoint"?: { … } }

asOfDate heeft rekendatum als alias; verschillen ze, dan volgt 400. Datums zijn ISO, elk id is uniek, en een getal met eenheid mag kaal (in de gedeclareerde eenheid) of als {"value": 80, "unit": "eur"}. Een tijdsafhankelijke parameter of een tijdsafhankelijk attribuut krijgt een tijdlijn (timeline), een tijdsafhankelijk kenmerk zijn perioden (periods); het voorbeeld hieronder toont beide. Een gedimensioneerd attribuut krijgt zijn cellen, {"cells": [{"labels", "value"}]}. Een attribuut met afwezigheidsmarkers neemt ook {"marker": "onbekend"} of {"marker": "niet van toepassing"}; een marker die het attribuut niet declareert, is 400 UNKNOWN_MARKER. Een scalaire waarde op een tijdsafhankelijk attribuut en relaties die niet binair of wederkerig zijn, weigert de dienst als UNSUPPORTED; een rol naar één object die een tweede, ander doel krijgt, is 400 WRONG_TYPE. Het antwoord geeft tijdlijnen, cellen en afwezigheidsmarkers in dezelfde vorm, met getallen zonder eenheid: exact waar de waarde eindigt, anders met 16 significante cijfers (100/3 in Numeriek (getal) wordt 33.33333333333333). Een instantie noemt alleen de attributes en characteristics die de output van het entrypoint selecteert, en inconsistencyDetails noemt per mislukte controle de regel en het id van de instantie waarover ze gaat. Foutbodies:

asOfDate has rekendatum as an alias; if they differ, the result is 400. Dates are ISO, every id is unique, and a number with a unit may be bare (in the declared unit) or written as {"value": 80, "unit": "eur"}. A time-dependent parameter or attribute takes a timeline, a time-dependent characteristic its periods; the example below shows both. A dimensioned attribute takes its cells, {"cells": [{"labels", "value"}]}. An attribute with absence markers also takes {"marker": "onbekend"} or {"marker": "niet van toepassing"}; a marker the attribute does not declare is a 400 UNKNOWN_MARKER. A scalar on a time-dependent attribute and relations that are neither binary nor reciprocal are refused as UNSUPPORTED; a role to one object that gets a second, different target is a 400 WRONG_TYPE. The response gives timelines, cells and absence markers in the same form, with numbers without a unit: exact where the value terminates, otherwise with 16 significant digits (100/3 in Numeriek (getal) becomes 33.33333333333333). An instance names only the attributes and characteristics the entrypoint's output selects, and inconsistencyDetails names, for each failed check, the rule and the id of the instance it is about. Error bodies:

JSON
{"error": "verzoek valideert niet", "violations": [{"field": …, "kind": …, "error": …}]}
{"error": …, "field": "body"}
{"error": …}
{"status": "EVALUATION_FAULT", "error": …, "faults": [{"kind": …, "error": …}]}
{"status": "NO_FIXPOINT", "error": …}
{"status": "LIMIT", "error": …, "code": …, "reason": …}
{"status": "MARKER", "error": …, "reason": …, "markers": [ … ]}

De eerste vorm hoort bij een verzoek dat niet valideert: elke schending noemt haar field, haar kind (bijvoorbeeld WRONG_TYPE, UNKNOWN_FIELD, UNKNOWN_MARKER, UNSUPPORTED of MISSING_INSTANCE) en een melding. De tweede hoort bij een onbruikbare body (beide 400), de derde bij 404, 405 en 500. De vierde tot en met de zesde zijn 422. EVALUATION_FAULT geeft per fout haar kind en de zin van uchar, die een object van het verzoek bij zijn id noemt. Een fout in het fixpunt heeft de soort DIVISION_BY_ZERO, COMPUTE_FAULT, WRITERS_CONFLICT, INPUT_FORM_FAULT, WRITE_BOUNDARY_FAULT, OPEN_SEGMENT_FAULT, CHARACTERISTIC_PERIODS_FAULT of SECOND_FILLING_FAULT; een waarde die uchar vóór elke sessie weigert, krijgt INPUT_PRECISION_FAULT (te veel decimalen) of CELL_FAULT (een cel die niet bestaat of twee keer staat). NO_FIXPOINT betekent dat de sessie geen fixpunt bereikte (§ 8). LIMIT betekent dat de Drools-backend van uchar een waarde niet kan houden, met de grens in code (bijvoorbeeld DROOLS-312); dat is geen fout van de regels of van de invoer. De zevende hoort bij 503 voor een entrypoint dat uchar niet draagt. Een deling door een numerieke nul in de regels is geen fout: het attribuut wordt null en de regel staat onder inconsistencies. De Quarkus-variant geeft dezelfde bodies.

Namen en vocabulaires

Heeft het model vocabulaires (zie De talen), dan mag een verzoek elke naam canoniek schrijven of als benaming in een van de vocabulaires: objecttypen, attributen, kenmerken, feittypen, rollen, parameters en enumeratiewaarden, ook gemengd. De dienst vergelijkt namen zoals de caselezer van uchar: een reeks spaties en tabs is één spatie, en alleen A–Z is hoofdletterongevoelig. Een naam wordt gelezen binnen zijn bereik: de leden van een objecttype met de geërfde, de rollen van een feittype, de waarden van een domein, de rest modelbreed. Leest een naam als twee verschillende declaraties, dan volgt een 400 met soort AMBIGUOUS_NAME die beide lezingen noemt; ?inputVocabulary=<naam> (of canonical) legt vast in welk vocabulaire de namen gelezen worden.

?vocabulary=<naam> kiest de namen van het antwoord, van inconsistencies en van explanation. Zonder die parameter geldt het vocabulary van het entrypoint in de descriptor, en anders de canonieke namen; een onbekend vocabulaire geeft een 400 met soort UNKNOWN_VOCABULARY. Een declaratie zonder benaming in het gekozen vocabulaire houdt haar canonieke naam. Alleen het standaardvocabulaire is gegarandeerd volledig; /info meldt per vocabulaire welke declaraties geen benaming hebben. De regeltekst wordt nooit hernoemd: de uitleg citeert haar zoals ze geschreven is.

Een benaming hoort bij het contract van de dienst: wie een benaming wijzigt, wijzigt de namen die een afnemer van dat vocabulaire stuurt en terugkrijgt. Het woord canonical is gereserveerd; een model met een vocabulaire van die naam start niet.

Getest voorbeeld: bergloon

De body komt uit een geteste HTTP-round-trip (ChainServiceTest); de curl-vorm is zelf opgesteld. Wrakstuk is in de fixture een subtype van Strandvondst.

The first form belongs to a request that fails validation: each violation names its field, its kind (for example WRONG_TYPE, UNKNOWN_FIELD, UNKNOWN_MARKER, UNSUPPORTED or MISSING_INSTANCE) and a message. The second belongs to an unusable body (both 400), the third to 404, 405 and 500. The fourth to sixth are 422. EVALUATION_FAULT gives per fault its kind and uchar's sentence, which names an object of the request by its id. A fault at the fixpoint has the kind DIVISION_BY_ZERO, COMPUTE_FAULT, WRITERS_CONFLICT, INPUT_FORM_FAULT, WRITE_BOUNDARY_FAULT, OPEN_SEGMENT_FAULT, CHARACTERISTIC_PERIODS_FAULT or SECOND_FILLING_FAULT; a value that uchar refuses before any session gets INPUT_PRECISION_FAULT (too many decimals) or CELL_FAULT (a cell that does not exist or is given twice). NO_FIXPOINT means the session reached no fixpoint (§ 8). LIMIT means that uchar's Drools backend cannot hold a value, with the limit in code (for example DROOLS-312); that is no fault of the rules or of the input. The seventh belongs to a 503 for an entrypoint uchar does not carry. A division by a numeric zero in the rules is not a fault: the attribute becomes null and the rule is listed under inconsistencies. The Quarkus variant returns the same bodies.

Names and vocabularies

If the model declares vocabularies (see The languages), a request may write every name canonically or as its designation in any of the vocabularies: object types, attributes, characteristics, fact types, roles, parameters and enumeration values, also mixed. The service compares names as uchar's case reader does: a run of spaces and tabs is one space, and only A–Z is case-insensitive. A name is read within its scope: the members of an object type with the inherited ones, the roles of a fact type, the values of a domain, the rest model-wide. If a name reads as two different declarations, the result is a 400 with kind AMBIGUOUS_NAME that names both readings; ?inputVocabulary=<name> (or canonical) fixes the vocabulary the names are read in.

?vocabulary=<name> chooses the names of the response, of inconsistencies and of explanation. Without that parameter the entrypoint's vocabulary from the descriptor applies, otherwise the canonical names; an unknown vocabulary gives a 400 with kind UNKNOWN_VOCABULARY. A declaration without a designation in the chosen vocabulary keeps its canonical name. Only the default vocabulary is guaranteed to be complete; /info lists per vocabulary which declarations have no designation. The rule text is never renamed: the explanation quotes it as written.

A designation is part of the service's contract: changing a designation changes the names a consumer of that vocabulary sends and receives. The word canonical is reserved; a model with a vocabulary of that name does not start.

Tested example: bergloon

The body comes from a tested HTTP round trip (ChainServiceTest); the curl form was written for this page. In the fixture, Wrakstuk is a subtype of Strandvondst.

curlcurl zelf opgesteldcurl composed for this page
curl -sS -X POST 'http://127.0.0.1:8080/bergloon' \
  -H 'Content-Type: application/json' \
  -d '{ "instances": [
        { "type": "Wrakstuk", "id": "W1",
          "attributes": { "vondstwaarde": 240, "bergingsdiepte": 12 },
          "characteristics": { "aangemeld": true } } ] }'

De test verwacht 200 en voor W1 een bergloon van 60.00 (240 × 0,25); met "aangemeld": false wordt dat null.

Getest voorbeeld: tijdlijn en perioden

De body komt uit TimelineHttpTest, met het golden jutterswet-opgave-per-periode van uchar als bron. In het antwoord mist een segment met een open grens zijn from of to.

The test expects 200 and a bergloon of 60.00 for W1 (240 × 0.25); with "aangemeld": false it becomes null.

Tested example: timeline and periods

The body comes from TimelineHttpTest, with uchar's golden jutterswet-opgave-per-periode as its source. In the response, a segment with an open bound lacks its from or to.

JSONservice/src/test/java/dev/uchar/regelspraak/toolchain/service/http/TimelineHttpTest.java
{"instances":[{"type":"Jutter","id":"J5",
  "attributes":{
»    "stormtoelage":{"timeline":[{"from":"2025-01-01","to":"2025-03-01","value":7}]},
    "opgegeven stormtoelage":{"timeline":[{"from":"2025-11-01","to":"2025-12-01","value":12}]}},
»  "characteristics":{"stormwacht":{"periods":[{"from":"2025-10-01","to":"2026-01-01"}]}}}]}

Voorbeeld met parameterset ongetest

Zelf opgesteld, niet door een test gedekt. Naar het uchar-corpusgeval jutterswet-penningdrempel. vondstdrempel heeft twee intervallen, dus asOfDate is verplicht. Op 7 november 2026 tegen de dienst gedraaid: op 2025-06-01 geldt drempel 12, J1 wordt penninggerechtigd, J2 niet.

Example with a parameter set untested

Composed for this page, not covered by a test. Modeled on the uchar corpus case jutterswet-penningdrempel. vondstdrempel has two intervals, so asOfDate is required. Run against the service on 7 November 2026: on 2025-06-01 the threshold is 12, J1 becomes penninggerechtigd, J2 does not.

JSONzelf opgesteldcomposed for this page
{
  "service": { "name": "Jutterswet penningdrempel", "version": "0.1.0" },
  "entrypoints": [
    {
      "name": "penninggerechtigdheid van een jutter",
      "path": "penninggerechtigd",
      "scope": { "ruleNames": ["Kenmerktoekenning jutter penninggerechtigd"] },
      "input": ["Jutter"],
      "output": ["Jutter:penninggerechtigd"]
    }
  ],
»  "parameterSets": "penningdrempel.parameters.json"
}
§ 6 peildatum reference date

Peildatum en parametersets

Parameters waarvan de waarde van de peildatum afhangt, staan in een sidecar naast de descriptor (§ 5), met per waarde een optioneel interval from–to, grenzen inclusief. Per parameter telt de vereniging van de intervallen over alle sets; overlap is verboden, aansluiten mag. Een tijdsafhankelijke parameter (voor elke maand) krijgt zijn tijdlijn in het verzoek of de descriptor, niet in de sidecar.

Een parameter met meer dan één interval, of met één begrensd interval, heeft meerdere vintages. De peildatum, asOfDate in het verzoek, kiest de vintage en is ook de rekendatum; parameters in het verzoek gaan voor.

SituatieMomentGevolg
sidecar onleesbaar of ongeldig (ook bij overlap)startexit 1
parameter ook als default in parametersstartexit 1 (ambigue autoriteit)
parameterset noemt een tijdsafhankelijke parameterstartexit 1
meerdere vintages, geen asOfDateverzoek400 op asOfDate
asOfDate in een gat tussen intervallenverzoek400 op asOfDate

/info toont de geladen sets en de periodWarnings, gaten tussen de geldigheid van regels en van parameters; bij de start staan die ook als WAARSCHUWING: op stderr.

scripts/as-of-date-vintage-check.sh (aanroep in § 9, stap 7) is adviserend: het waarschuwt bij meer dan één vintage als het tweede argument false is en draait nergens automatisch. Het faalt alleen (exit 1) op een sidecar zonder lijst onder parameterSets. De Helm-value parameterSets.multiVintage voegt alleen een NOTES-tekst toe.

Bekende afwijking

Het script telt alleen intervallen: één begrensd interval geeft geen waarschuwing, terwijl de dienst dan wel een asOfDate eist.

Reference date and parameter sets

Parameters whose value depends on the reference date live in a sidecar file next to the descriptor (§ 5), with an optional interval from–to per value, bounds inclusive. For each parameter, the union of its intervals across all sets counts; overlap is forbidden, adjacent intervals are allowed. A time-dependent parameter (voor elke maand) gets its timeline in the request or the descriptor, not in the sidecar.

A parameter with more than one interval, or with one bounded interval, has multiple vintages. The reference date, asOfDate in the request, selects the vintage and is also the calculation date (the RegelSpraak global rekendatum); parameters in the request take precedence.

SituationWhenResult
sidecar unreadable or invalid (overlap included)startupexit 1
parameter also a default in parametersstartupexit 1 (ambigue autoriteit)
parameter set names a time-dependent parameterstartupexit 1
multiple vintages, no asOfDaterequest400 on asOfDate
asOfDate in a gap between intervalsrequest400 on asOfDate

/info shows the loaded sets and the periodWarnings, gaps between the validity of rules and of parameters; at startup these also appear on stderr as WAARSCHUWING:.

scripts/as-of-date-vintage-check.sh (invocation in § 9, step 7) is advisory: it warns about more than one vintage when the second argument is false and is not run automatically anywhere. It fails (exit 1) only on a sidecar without a list under parameterSets. The Helm value parameterSets.multiVintage only adds text to the NOTES.

Known deviation

The script only counts intervals: one bounded interval produces no warning, while the service does require a asOfDate in that case.

§ 7 kubernetes kubernetes

Kubernetes en de Helm-chart

De chart helm/decision-service/ rendert één Deployment en één ClusterIP-Service. values.yaml zet image.pullPolicy: IfNotPresent (gekozen voor kind) en laat descriptor en sourcesManifest leeg, zodat de defaults van het image gelden. Overlays geef je met -f mee:

Kubernetes and the Helm chart

The chart helm/decision-service/ renders one Deployment and one ClusterIP Service. values.yaml sets image.pullPolicy: IfNotPresent (chosen for kind) and leaves descriptor and sourcesManifest empty, so the image defaults apply. Pass the overlays explicitly with -f:

bronnen sources .rgs-bestanden · service.json .rgs files · service.json parametersets (sidecar) parameter sets (sidecar) make image make image Dockerfile: Maven-build, Dockerfile: Maven build, JDK 21-runtime, regels ingebakken JDK 21 runtime, rules baked in decision-service:<tag> decision-service:<tag> naar de registry pushed to the registry helm upgrade --install helm upgrade --install values-dev / values-prod values-dev / values-prod kubernetes-cluster kubernetes cluster Deployment (replicas) Deployment (replicas) pod: entrypoint.sh → bin/regelspraak serve pod: entrypoint.sh → bin/regelspraak serve env: PORT, DESCRIPTOR, env: PORT, DESCRIPTOR, SOURCES_MANIFEST, ALLOW_MARKERS SOURCES_MANIFEST, ALLOW_MARKERS compileert alles vóór de poort bindt compiles everything before the port binds probes probes GET /info GET /info startup 5 s × 120 startup 5 s × 120 readiness 5 s readiness 5 s liveness 10 s liveness 10 s Service Service ClusterIP :8080 ClusterIP :8080 afnemer consumer POST /<pad> POST /<pad> ?explanation=true ?explanation=true JSON in en uit JSON in and out geen ingress en geen volumes in de chart; een andere regelset is een nieuw image no ingress and no volumes in the chart; a different rule set is a new image
Van bron naar draaiende pod. De regels zitten in het image; de chart rendert één Deployment en één Service, en alle drie de probes vragen /info.
From source to running pod. The rules live in the image; the chart renders one Deployment and one Service, and all three probes ask /info.
SleutelKeyvalues.yamlvalues-dev.yamlvalues-prod.yaml
replicas1—2
resources.requests.memory—512Mi1Gi
resources.requests.cpu—250m500m
resources.limits.memory—2Gi2Gi
resources.limits.cpu—12

Alle probes doen httpGet op /info; de startupProbe geeft de compilatie ongeveer tien minuten:

All probes do an httpGet on /info; the startupProbe gives compilation about ten minutes:

ProbepathperiodSecondsfailureThreshold
startupProbe/info5120
readinessProbe/info5—
livenessProbe/info10—

Uitrollen, samengesteld uit values.yaml en de README:

Deploying, composed from values.yaml and the README:

sh
helm upgrade --install <release> helm/decision-service \
  -f helm/decision-service/values-prod.yaml \
  --set image.repository=<repo> \
  --set image.tag=<tag>

make kind-smoke

De laatste stap van make gates, de controle vóór elke landing, op de host: image bouwen, in het kind-cluster regelspraak laden (aangemaakt als het ontbreekt; CLUSTER= noemt een ander), de chart met values-dev.yaml installeren en dan controleren:

H2
de golden-scenarioset van de in-process-test, nu over HTTP tegen de dienst in het cluster; een scenario dat de scenarioresolver van uchar weigert, wordt als die weigering getoetst;
H3
een markerregel in de scope moet exit 3 en CrashLoopBackOff geven; met allowMarkers=true moet de pod starten, moet /info de marker noemen en moet een verzoek aan dat entrypoint 503 krijgen;
H4
twee images die in één parameterwaarde verschillen, na elkaar uitgerold, laten bij hetzelfde verzoek één kenmerk omslaan.

Nodig: kind, kubectl, helm, docker, mvn met gevulde ~/.m2, python3 en curl, en een checkout met de uchar-submodule. make gates-no-docker draait de stappen van make gates die geen Docker nodig hebben.

Beperkingen van de chart

  • geen volumes of volumeMounts, terwijl de documentatie een volume op /app/rules noemt
  • geen Ingress
  • geen securityContext
  • geen timeoutSeconds op de probes
  • Service-poort vast op 8080

make kind-smoke

The last step of make gates, the check before every landing, on the host: build the image, load it into the kind cluster regelspraak (created when absent; CLUSTER= names another), install the chart with values-dev.yaml, then check:

H2
the golden scenario set of the in-process test, now over HTTP against the service in the cluster; a scenario that uchar's scenario resolver refuses is checked as that refusal;
H3
a marker rule in scope must cause exit 3 and CrashLoopBackOff; with allowMarkers=true the pod must start, /info must name the marker, and a request to that entrypoint must get 503;
H4
two images that differ in one parameter value, deployed one after the other, flip one characteristic for the same request.

Requires kind, kubectl, helm, docker, mvn with a populated ~/.m2, python3 and curl, plus a checkout with the uchar submodule. make gates-no-docker runs the steps of make gates that need no Docker.

Limitations of the chart

  • no volumes or volumeMounts, although the documentation mentions a volume on /app/rules
  • no Ingress
  • no securityContext
  • no timeoutSeconds on the probes
  • Service port fixed at 8080
§ 8 gedrag behavior

Operationeel gedrag

Staat
Geen: elk verzoek krijgt een nieuwe sessie op onveranderlijke KieBases. Replica's delen niets en compileren elk zelf.
Threads
Eén virtuele thread per verzoek, zonder bovengrens.
Opstartduur
Niet gemeten. Budgetten: 180 s in Docker, ongeveer 10 min in de startupProbe, 600 s in kind-smoke.
Vuurplafond
Geen timeout per verzoek. Het vuren stopt na max(10 000, 4 × R × N) vuringen, met R het aantal regels van de KieBase en N het aantal feiten vlak vóór het vuren; het antwoord is dan 422 NO_FIXPOINT met de zin van uchar in error (geen fixpunt: het vuurplafond van … vuringen is bereikt). Het creatiebudget van uchar geeft hetzelfde antwoord. De dienst logt er niets bij.
Uitleg
Een entrypoint dat uitlegt, houdt twee KieBases van hetzelfde document in het geheugen; geheugen en opstartduur groeien daarmee (niet gemeten). Met "explanation": false in de descriptor bouwt het er één.
Playground
Na 15 s volgt 503.
Body
Geen groottelimiet.
Afsluiten
Geen graceful shutdown: lopende verzoeken worden bij SIGTERM niet expliciet afgewikkeld.
Logs
Alleen stdout en stderr; geen log per verzoek, geen metrics.
Heap
Standaard geen -Xmx; de JVM-default binnen de containerlimiet geldt (niet gemeten). JAVA_OPTS kan er een zetten, maar de chart heeft daar geen waarde voor.

Een 400 noemt de fout van de afnemer per veld (violations[].field en kind; UNSUPPORTED is een vorm die de dienst niet draagt), een 422 een fout in de regels of in de waarden van deze invoer, een grens van de Drools-backend van uchar of een sessie zonder fixpunt, een 503 op POST /<pad> een entrypoint dat uchar niet draagt, een 500 een onverwachte exceptie in de dienst, met de stacktrace in de podlog.

De Quarkus-variant

service-quarkus is een optionele adapter op Quarkus 3.15.5 over dezelfde kern, met SmallRye Health (/q/health/live, /q/health/ready) en config-injectie, maar zonder de exitcodes 1, 2 en 3. Ze geeft dezelfde antwoorden en foutbodies, ook de 422- en 500-antwoorden, en leest dezelfde queryparameters als de plain dienst, maar heeft geen playground. De omgevingsvariabelen heten er PORT, DESCRIPTOR, SOURCES_MANIFEST, ALLOW_MARKERS en DETAIL. Dockerfile, chart en kind-smoke bestaan alleen voor de plain dienst, de default. Kies Quarkus voor aparte liveness- en readiness-probes, met een eigen image en een aangepaste chart. Niet vastgesteld is of Quarkus de kern bij het booten bouwt of pas bij de eerste injectie.

Operational behavior

State
None: every request gets a new session on immutable KieBases. Replicas share nothing and each compiles on its own.
Threads
One virtual thread per request, with no upper bound.
Startup time
Not measured. Budgets: 180 s in Docker, about 10 min in the startupProbe, 600 s in kind-smoke.
Firing cap
No timeout per request. Firing stops after max(10,000, 4 × R × N) firings, with R the number of rules of the KieBase and N the number of facts just before firing; the response is then 422 NO_FIXPOINT with uchar's sentence in error (geen fixpunt: het vuurplafond van … vuringen is bereikt: no fixpoint, the firing cap was reached). uchar's creation budget gives the same response. The service logs nothing for it.
Explanation
An entrypoint that explains keeps two KieBases of the same document in memory; memory use and startup time grow accordingly (not measured). With "explanation": false in the descriptor it builds one.
Playground
503 after 15 s.
Body
No size limit.
Shutdown
No graceful shutdown: in-flight requests are not explicitly drained on SIGTERM.
Logs
stdout and stderr only; no per-request log, no metrics.
Heap
No -Xmx by default; the JVM default within the container limit applies (not measured). JAVA_OPTS can set one, but the chart has no value for it.

A 400 names the client's error per field (violations[].field and kind; UNSUPPORTED is a form the service does not carry), a 422 a fault in the rules or in the values of this input, a limit of uchar's Drools backend or a session without a fixpoint, a 503 on POST /<pad> an entrypoint uchar does not carry, a 500 an unexpected exception in the service, with the stack trace in the pod log.

The Quarkus variant

service-quarkus is an optional adapter on Quarkus 3.15.5 over the same core, with SmallRye Health (/q/health/live, /q/health/ready) and config injection, but without the exit codes 1, 2 and 3. It returns the same responses and error bodies, the 422 and 500 responses included, and reads the same query parameters as the plain service, but has no playground. Its environment variables are PORT, DESCRIPTOR, SOURCES_MANIFEST, ALLOW_MARKERS and DETAIL. The Dockerfile, chart and kind-smoke exist only for the plain service, which is the default. Choose Quarkus for separate liveness and readiness probes, with your own image and an adapted chart. It has not been established whether Quarkus builds the core at boot or only on first injection.

§ 9 runbook runbook

Runbook in tien stappen

Vul <…> zelf in; curl verwacht de dienst op lokale poort 8080.

A runbook in ten steps

Fill in <…> yourself; curl expects the service on local port 8080.

  1. BouwenBuild

    Op een host met Docker en netwerk; zet het image daarna in de registry.

    On a host with Docker and network access; then push the image to the registry.

    sh
    make image IMAGE_REPO=<repo> TAG=<tag>
  2. Lokaal proberenTry it locally

    Met -e PLAYGROUND=true opent ook /playground; niet voor productie.

    With -e PLAYGROUND=true, /playground opens as well; not for production.

    sh
    docker run --rm -p 8080:8080 <repo>:<tag>
    curl -sS http://127.0.0.1:8080/info
  3. UitrollenDeploy

    pullPolicy: IfNotPresent is voor kind gekozen; gebruik per uitrol een nieuwe tag.

    pullPolicy: IfNotPresent was chosen for kind; use a new tag for every rollout.

    shsamengesteld uit values.yaml en READMEcomposed from values.yaml and README
    helm upgrade --install <release> helm/decision-service \
      -f helm/decision-service/values-prod.yaml \
      --set image.repository=<repo> \
      --set image.tag=<tag>
  4. De rollout volgenFollow the rollout

    Ready betekent: alles gecompileerd. Port-forward volgens de NOTES van de chart.

    Ready means everything has compiled. Port-forward as described in the chart's NOTES.

    sh
    kubectl rollout status deploy/<release>-decision-service --timeout=600s
  5. Controleren na de uitrolCheck after the rollout

    Bekijk in /info coverage, markers, skipped en periodWarnings.

    Inspect coverage, markers, skipped and periodWarnings in /info.

    sh
    curl -sS http://127.0.0.1:8080/info | jq '.skipped, .periodWarnings'
  6. Een .rgs-wijziging uitrollenRoll out an .rgs change

    De regels zitten in het image: herhaal stap 1 en 3 met een nieuwe tag.

    The rules are in the image: repeat steps 1 and 3 with a new tag.

  7. Een vintage toevoegenAdd a vintage

    Interval zonder overlap aan de sidecar toevoegen, controle draaien, bij meerdere vintages parameterSets.multiVintage op true zetten, bouwen, uitrollen, /info nakijken. Afnemers sturen dan een asOfDate mee.

    Add the interval to the sidecar without overlap, run the check, set parameterSets.multiVintage to true if there are multiple vintages, build, deploy, inspect /info. Clients must then send a asOfDate.

    sh
    scripts/as-of-date-vintage-check.sh <parameter-sets.json> <true|false>
  8. Pod in CrashLoopBackOffPod in CrashLoopBackOff

    Exit 3: uchar draagt een entrypoint niet; pas de scope of de regels aan. allowMarkers=true start de andere entrypoints, maar dat ene antwoordt met 503. Exit 1: de log noemt de schendingen van de eerste stap die faalt (§ 1).

    Exit 3: uchar does not carry an entrypoint; adjust the scope or the rules. allowMarkers=true starts the other entrypoints, but that one answers 503. Exit 1: the log lists the violations of the first step that fails (§ 1).

    sh
    kubectl logs --previous <pod>
    kubectl get pod <pod> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}'
  9. Bij 422 en 5xxOn 422 and 5xx

    Een 422 met "status":"NO_FIXPOINT" betekent dat het vuren het vuurplafond of het creatiebudget bereikte (§ 8); de zin van uchar staat in error van het antwoord, niet in de log. Een 422 met "status":"EVALUATION_FAULT" is een fout in de regels of in de waarden van deze invoer, met per fout de soort en de zin van uchar in faults. Een 422 met "status":"LIMIT" is een grens van de Drools-backend van uchar (§ 5). Een 500 is een onverwachte exceptie in de dienst; de stacktrace staat in de podlog. Een 503 met "status":"MARKER" is een entrypoint dat uchar niet draagt, een 503 op /playground/analyse de tijdslimiet van de playground; een 400 komt van de afnemer.

    A 422 with "status":"NO_FIXPOINT" means that firing reached the firing cap or the creation budget (§ 8); uchar's sentence is in the response's error, not in the log. A 422 with "status":"EVALUATION_FAULT" is a fault in the rules or in the values of this input, with each fault's kind and uchar's sentence in faults. A 422 with "status":"LIMIT" is a limit of uchar's Drools backend (§ 5). A 500 is an unexpected exception in the service; the stack trace is in the pod log. A 503 with "status":"MARKER" is an entrypoint uchar does not carry, a 503 on /playground/analyse the playground's time limit; a 400 comes from the client.

    sh
    kubectl logs <pod> | grep -A 20 'Exception'
  10. LogsLogs

    Alleen stdout en stderr; geen logniveau.

    stdout and stderr only; no log level.

    sh
    kubectl logs <pod> | grep -E 'WAARSCHUWING:|MARKER:|niet uitvoerbaar:'
§ 10 uchar uchar

Hoe de Drools-backend van uchar hierin past

De dienst heeft geen eigen transpiler. Hij bouwt elk entrypoint met de Drools-backend van uchar, DroolsBackend.build(document, EXEC_MODEL), over het uchar-ir-v1-document van de vormen van dat entrypoint, en vult, vuurt en leest elke sessie via de publieke ingang van die backend (insert, fire, fixpointOf, setDerivedValues). Beans maakt de dienst niet zelf: hij schrijft het verzoek als de invoer van een case, en de backend leest die zoals gate 12 elke case leest. De backend analyseert elke regel één keer (TmsTranspiler) en geeft die analyse in twee weergaven: als executable model met Java-lambda's (PatternDslRendering), dat de dienst vuurt en waaruit /info zijn coverage haalt, en als DRL-tekst (DrlRendering), waarin toetsen en berekeningen Java-functies zijn met de RegelSpraak-zin erboven, die bin/regelspraak drl en de playground tonen. Beide weergaven leiden af met truth maintenance: een regel zet een afgeleid feit logisch in het werkgeheugen, en Drools haalt het weer weg zodra de voorwaarde niet meer geldt.

De backends/drools van uchar leest de ir.json van een golden, nooit de RegelSpraak-bron. Gate 12 (scripts/check-drools.sh) speelt elke golden af en eist dezelfde uitkomst als de referentie-interpreter en Clingo: het differentiaalvierkant ref ≡ clingo ≡ Drools. Niet-ondersteunde vormen worden per vorm met reden geweigerd. Gate 12 speelt elke golden op beide weergaven, en de DRL-tekst moet per golden dezelfde uitkomst geven als het executable model. Het aantal spelers ligt vast als gelijkheid, en elke golden die niet speelt, staat bij naam en met reden vastgepind. In de toolchain speelt make check-service-replay de cases van de goldens die gate 12 speelt, opnieuw af via de dienst, over HTTP; wat de dienst niet aanneemt, staat bij naam en met reden uitgesloten. De gates staan op Onder de motorkap, de IR op De talen.

How uchar's Drools backend fits in

The service has no transpiler of its own. It builds each entrypoint with uchar's Drools backend, DroolsBackend.build(document, EXEC_MODEL), over the uchar-ir-v1 document of that entrypoint's forms, and fills, fires and reads every session through that backend's public entry (insert, fire, fixpointOf, setDerivedValues). The service makes no beans itself: it writes the request as the input of a case, and the backend reads that input as gate 12 reads every case. The backend analyses each rule once (TmsTranspiler) and renders that analysis in two forms: as an executable model with Java lambdas (PatternDslRendering), which the service fires and from which /info takes its coverage, and as DRL text (DrlRendering), in which tests and computations are Java functions with the RegelSpraak sentence above them, which bin/regelspraak drl and the playground show. Both renderings derive with truth maintenance: a rule inserts a derived fact logically into working memory, and Drools retracts it as soon as the condition no longer holds.

uchar's backends/drools reads a golden's ir.json, never the RegelSpraak source. Gate 12 (scripts/check-drools.sh) replays every golden and requires the same result as the reference interpreter and Clingo: the differential square ref ≡ clingo ≡ Drools. Forms the backend does not carry are refused per form, with a reason. Gate 12 plays every golden on both renderings, and the DRL text must give the same outcome per golden as the executable model. The number of players is pinned as an equality, and every golden that does not play is pinned by name, with its reason. In the toolchain, make check-service-replay replays the cases of the goldens that gate 12 plays through the service, over HTTP; what the service does not accept is excluded by name, with its reason. For the gates, see Under the hood; for the IR, see The languages.

OnderdeelComponentVersieVersion
RegelSpraakv2.3.0
Java21
Drools8.44.0.Final (backend van uchar; de toolchain pint dezelfdeuchar's backend; the toolchain pins the same)
Quarkus3.15.5
Jackson2.21.7
ANTLR4.13.2
Helm: decision-service0.1.0 (appVersion 0-SNAPSHOT)