Changelog & Roadmap¶
Changelog¶
v2026.09.8 — Every Release Ships Its Own SBOM, and the Tree Is Audited Daily (September 2026)¶
How a release reaches production: Deployment. The mechanism across repositories: Supply-Chain Pinning and ICTU Dependency Guideline. Measured suites: Testing.
Every release ships with its own SBOM, committed and uploaded. When an advisory lands against something that shipped months ago, the question is what that version contained, and only a document written at the time answers it. scripts/write-sbom.mjs (npm run sbom) writes docs/sbom/<name>-<version>.cdx.json — CycloneDX, production dependencies only, read from the lockfile without installing — and is a bump-release step, after the version bump, since the filename carries the version. sbom.yml runs on a push to main, on demand, and on pull requests that touch the tooling; a promotion to main is the release here, so it uploads the document as an artifact and asserts that the released version has one. Two copies, because artifacts expire after ninety days on a public repository while the committed copy does not. --check is strict and belongs where the release is cut; --verify-release belongs to the promotion, where a missing document fails but drift only warns, because a promotion carries every commit merged since the release.
Dependencies are audited daily, on both acc and main. Every other gate runs on a commit, so a new advisory against unchanged code was seen by nothing, and Dependabot watches acc rather than the main production deploys from. dependency-audit.yml runs at 05:17 UTC and on demand, reads each branch's lockfile with npm audit --package-lock-only, fails on a high or critical advisory in production dependencies, reports the rest, and keeps one tracking issue open, updated or closed so a scheduled failure reaches someone. scripts/audit-tree.mjs groups findings by advisory rather than by package — on 24 September this repository's 28 moderate entries were three advisories, 24 of them the same @tiptap/core. An audit that cannot run exits 2 and counts as a finding, never as a clean tree. Two fixes followed its first run: the script is copied to $RUNNER_TEMP before the branch checkouts remove it, and the job is named dependency-audit, because a second job called audit made the required audit check ambiguous.
A lockfile that disagrees with package.json fails under its own name. npm ci already refused one, but only as an EUSAGE error inside "Install dependencies for the formatter", three steps into a job about pinning. A step named "Lockfile matches package.json" now runs npm ci --dry-run --ignore-scripts before that install — --ignore-scripts because a dry run still runs the root postinstall, which failed on a clean checkout. What it cannot catch is recorded beside it: it proves consistency with the pull request's own base, not with a base that has moved since, so dependency pull requests are merged one at a time, each rebased onto the merged acc.
Renovate waits for a major's first patch, and holds ubuntu 26.04 on record. An allowedVersions rule for the npm manager excludes X.0.0, so the earliest a new major can arrive is X.0.1. Ubuntu 26.04 was assessed on 24 September 2026 and deferred by a rule carrying its reason and the condition that ends it; the other queued majors stay behind Dependency Dashboard approval, where a person sees them.
Two Semgrep findings answered in the code. The quality service's tag matcher took a string and built both of its regular expressions from it, so only the call sites kept data out of the pattern; its parameter is now a MeasuredTag union of exactly 'decision', 'uitvoeringsregel' and 'inputData', and the compiler enforces what a comment used to assert. The promotion drift guard's failure message now passes its values as arguments behind a constant format string, so a %s in a workflow or target name can no longer consume the path list. concurrently, which runs the two dev servers together, moved to v10.
v2026.09.7 — A Promotion Deploys in Order, and Every Deploy Check Gets to Run (September 2026)¶
How a release reaches production: Deployment. The Thuisbatterij processes: BPMN Modeler. Measured suites: Testing.
A promotion runs the three production deploys in order instead of racing them. Each production workflow used to start itself on a push to main with its own path filter, and the three started at once. On the v2026.09.6 promotion the ROPA site finished deploying before the backend had started building — production served new pages against the previous API for several minutes, under three green checks. promote-to-production.yml is now the only workflow a push to main starts that deploys anything. It calls the three deploys as reusable workflows: a changes job decides what is needed, the backend goes first, then the two sites in parallel. A failed backend stops both sites; a skipped one does not, so each site checks the backend's result against a positive list rather than just "not failed". The path rules moved out of the triggers into scripts/promotion-targets.mjs, with a test the changes job runs before using the script; four of its checks guard against the site workflows' pull_request paths drifting from those rules. It fails safe towards deploying everything — a promotion whose commit range cannot be read is not a promotion that changed nothing.
The production-site preview on a promotion pull request is kept, on purpose. The two production site workflows carry a pull_request trigger on main, so opening a promotion pull request deploys a preview of the production site built from acc. That was an open question; it is now a recorded decision — look at the real thing before promoting, rather than infer it from an acceptance build. The costs are written down with it: a public URL on a production resource serving unreleased code, an environment slot, and a teardown that depends on a close job GitHub will not run while the pull request has a merge conflict. The backend is excluded deliberately: a preview site is a page to look at, a preview backend on production would be a second live API against production data.
Every deploy check runs, instead of the first failure hiding the rest. "Verify v1 endpoints" ran its assertions as early exits, so the first to fail stopped the rest. On the v2026.09.6 promotion the OpenAPI/health version comparison failed inside the deploy's stale window, and the native-binding assertion below it never ran. Each check now records its failure and the step fails at the end, so the log shows all of them. Collecting rather than reordering, deliberately: reordering only changes which check gets to mask the others.
build-info.json is read at start-up, closing a false pass in the deploy gate. It used to be read on first use, on the assumption that the file cannot change without a restart. It can: a zip deploy overwrites it while the previous process is still serving, and the restart comes afterwards — so the old process read the new file the first time anything asked, and the first thing to ask was the deploy's own build.sha check. Seen on the v2026.09.6 promotion: two seconds apart, one process reported /v1/openapi.json at 2026.09.6 and /v1/health at 2026.09.5. The read now happens at module load, so version and build are bound together and a stale process reports itself as stale. See Backend.
The two production site deploy jobs can be told apart. Both were named "Build and Deploy Job", and required checks match by job name, so neither could be required, referenced or distinguished — the v2026.09.6 promotion pull request showed two identical rows. They are now "Build and Deploy Production Frontend" and "Build and Deploy Production ROPA Site".
Thuisbatterij processes in swimlanes, with Dutch names and a missing-information form. The subsidy application is drawn in a pool with Aanvrager, Behandelaar and Systeem lanes, and its decision subprocess in one with Behandelaar and Systeem; element ids are unchanged and names are Dutch. "Aanvullende gegevens opvragen" pointed at an embedded HTML form that never existed, so nothing could set supplementReceived; it now uses a deployable form-js form, thuisbatterij-aanvullende-gegevens. The example versions are bumped so the Modeler replaces cached copies.
Housekeeping. Constants, a type and a helper shared by the DSO Explorer moved from shared.tsx into tokens.ts, so Fast Refresh can hot-swap shared.tsx again. lint-staged moved to v17, verified by running the pre-commit hook end to end on a deliberately mis-formatted file. zizmor-action moved to v0.6.4 with its register row in SECURITY-PIPELINE.md updated on the same branch, and the validator's Node to 24.21.0.
v2026.09.6 — An Activity's Whole Chain in One Call, and a Quality Profile That Shows Its Working (September 2026)¶
The integration in prose: DSO Integration. The published contract: API Specification. What a release is checked against: Post-deployment verification. Measured suites: Testing.
A sixth DSO API, and an activity's whole chain in one call. The chain an activity hangs from — its legal source, the annotations on that source, and both its rule sets — spans an API this backend did not proxy: Omgevingsdocumenten Presenteren (Ozon) v8.5.2, at omgevingsdocumenten/api/presenteren/v8, which the existing DSO keys already authenticate against. ozon.service.ts is the client, with the quirks the plan front-loaded rather than discovered: the full OGC Content-Crs value, the slash-to-underscore identificatie transform, and environment-specific toepasbare-regel ids. Three passthrough routes carry it — POST /v1/dso/regelingen/zoek, GET /v1/dso/regelingen/{id}/annotaties and GET /v1/dso/regelingen/{id}/documentstructuur/{wId} — and a fourth, composite route joins all three legs: GET /v1/dso/activiteiten/{urn}/dossier returns one Dossier with its QualityProfile. The /v1/dso surface goes from twelve routes to sixteen, and the OpenAPI document from 63 paths to 68; a test asserts each Ozon route conforms to the operation that describes it, so the document and the implementation cannot drift apart silently. The join itself lives in exactly one service, dossier.service.ts, by design: a fan-out across three upstream APIs should have one place that knows how the pieces fit together.
An activity is scored on two axes, and never reduced to one number. quality.service.ts scores the assembled chain on legibility — how readable the rules are as they stand — and recoverability — how far a reader can recover the reasoning from the published material. The two are kept apart deliberately: blending them into a single grade hides which of the two is weak, and the profile exists to compare activities and municipalities, which is exactly what a grade flattens. Every name is classified semantic, opaque-resolvable or opaque-dangling, and identity resolvability is derived from what the lookup actually returned rather than assumed by construction, so an identity that cannot be resolved scores as such instead of being credited. Conclusie and Indieningsvereisten are always reported separately and never blended, and a rule set that is absent says so rather than reporting zeros — zeros would read as measured-and-empty. The evidence travels with the counts: per-item naming classes, each input's own vraagTekst resolved through its uitvoeringsregelRef, and the legal-source articles, so a figure such as "3/7 semantic, 4 opaque" can be audited by the municipality whose data it describes.
The Quality Profile tab. A fourth tab beside Concepts, Works and Activities. It renders the profile of the activity selected in Activities, with Scorecard as the default layout and a matrix for Compare that keeps the two authorities in separate columns. The selection now lives in DsoExplorer rather than in ActiviteitenTab — selectedUrn, the active validity date, the authority OIN and the level all survive a tab switch; changing Level or Authority, clicking Load and closing the detail panel still clear it. The activity detail panel gains a compact quality-profile teaser, which is the one place the two rule sets are summed into a single decision-naming and input-naming row — a pointer at the tab, not a score. It renders from cache only: the dossier call fans out across three upstream APIs, so selecting an activity must never trigger it, and a test asserts exactly that, because the failure mode is a detail panel that feels broken. A Dossier .md download turns a dossier into a circulable document via scripts/dso-dossier.mjs; renderDossier moved into scripts/dossier-render.mjs so the CLI and the browser share it, with a test asserting the two references are the same function object rather than merely producing equal output.
A legal source resolves per bestuurslaag, and national activities are no longer rejected. The dossier refused any mnre URN without an authority parameter. That guard was LDE's own, not a DSO requirement, and it rejected activities that needed nothing: RijksmonArchMonument carries its own Conclusie and Indieningsvereisten and scores 23/23 semantic, yet answered 400 — advising the caller to supply a municipality, which no national activity has. The lookup also hardcoded regelingtype_003, the gemeente instrument, so for any provincie, waterschap or rijk activity it searched for a document type that authority never publishes. Each bestuurslaag has its own core instrument — Omgevingsplan, Omgevingsverordening, Waterschapsverordening, AMvB — and the level now comes from bestuursorgaan.bestuurslaag, falling back to the authority code prefix, with an explicit authority still overriding both. Where several regelingen of the right type exist (the Rijk publishes two AMvBs) they are tried in order, capped at three, because each annotation graph can be megabytes. A taxonomy node's correctly-empty dossier now points at its children: the Dossier carries childActivityUrns taken from the RTR response step 1 already fetches, so it costs no extra upstream call, and the tab lists them as links that load the child's own profile — Rijksmonumentenactiviteit now points at RijksmonArchMonument and RijkmonMonument, which carry a Conclusie and Indieningsvereisten each.
A partial dossier says which half is missing. When a candidate regeling could not be fetched, the summary still said that none of them annotate the activity — asserting a negative about a document nobody had read. Checked-and-not-matching is now reported separately from could-not-be-fetched, and each failure names its subject: documentComponent carries the wId, dmn the rule identifier, toepasbareRegels the functioneleStructuurRef. A reader can tell "we looked and it is not there" from "we could not look", which is what makes a partial dossier honest rather than broken. The regelingen search also pages now — mnre1034 returns a full page of 100, so an authority's instrument could fall off page one and be reported as "no regeling of type …", a wrong answer rather than an error — and a cap-truncated search is recorded distinctly from a genuine miss. getToepasbareRegels threads the validity date through in the same dd-MM-yyyy the other RTR-side calls use and picks by most recent begindatum rather than by position; it had been pairing a historical legal source with current executable rules and taking toepasbareRegels[0] with no ordering guarantee.
Four fixes that each made the profile lie. The datum parameter was forwarded unchanged to two upstreams using different date formats — the RTR expects dd-MM-yyyy, Ozon expects YYYY-MM-dd — so no value satisfied both, and the failure was silent: the annotations leg was caught into provenance.failures while legalSource.available stayed true, rendering a real regeling title above "0 juridische regels". The route keeps dd-MM-yyyy and converts before calling Ozon. GUID detection required separators, but IMOW URN local names are 32 contiguous hex characters, so GUID-named activities scored as semantic — the exact case the quality profile exists to surface. The tag scanner hardcoded the dmn: prefix and so reported an un-prefixed DMN as having nothing opaque. And cross-layer consistency compared two layers where the design defines three.
The child-activity fan-out is capped, and stops writing after teardown. Opening an activity fired one request per child simultaneously — 24 at once for a 23-child parent (#196). Five workers now pull from a shared cursor, so the same work arrives in waves, and Promise.allSettled semantics are kept: a failing child never blocks the rest, and one that resolves without an omschrijving still falls back to its URN. Names appear as each child resolves rather than when the slowest does. Separately, the component had no cancellation guard at all — not a flag, not an AbortController — so an in-flight fan-out could write names for an activity the user had already left. That was latent while everything raced to finish at once; a pool drains over time, which would have made it real. The repeat cost is gone too: activity detail, the hottest DSO read, is now TTL-cached for five minutes in a shared utility registered by name, so the cache routes report and clear across every cache rather than a hardcoded list. Staleness there is accepted deliberately — DELETE /v1/cache/clear is the escape hatch.
The Thuisbatterij bundle reaches the Modeler, and tenanted processes find their DMNs. The Flevoland Thuisbatterij files sat in public/examples/flevoland/ and stopped there: fetchable by URL, invisible in the app, with no entry in the version registry, no seeding block, no form definitions and no document template — the only bundle in public/examples with no way into the UI. It is now seeded as example_thuisbatterij_aanvraag (shell) and example_thuisbatterij_decision (subprocess), with its three forms and its document template. Two defects surfaced with it. Operaton resolves a business rule task's decisionRef inside the process instance's own tenant, so a process deployed under tenant-id flevoland cannot see a DMN deployed without one, and the engine refuses to instantiate it at all — a 500 from process start and an unexplained "De aanvraag kon niet worden ingediend" on the ACC citizen dashboard. decisionRefTenantId="${null}" points them back at the shared untenanted DMNs, and EXAMPLE_VERSIONS was bumped for the four affected processes so existing users re-seed. Thuisbatterij was also the last bundle on camunda:formRefBinding="latest" for all three forms; with latest, Operaton resolves a form by key across deployments, and on ACC that key sits in 53 deployments, so the start form 500s and the citizen dashboard shows "Formulier kon niet worden geladen" with no field ever rendering. And the e2e-fixtures manifest described bpmn, forms, documents and subProcesses while shipping no .dmn at all, although every process in the bundle calls decisions — so a stack rebuilt from the documented bundle deployed cleanly, served its start forms cleanly, and then failed at the first business rule task with "no decision definition deployed with key 'AwbCompletenessCheck'", one key at a time, four rounds of the same discovery.
The deploy target comes from the backend. The deploy modal and the exported README now ask the backend which Operaton it deploys to, through GET /v1/dmns/process/deploy-target, instead of the frontend's build-time VITE_OPERATON_BASE_URL — which is removed from all three .env files, because a build-time copy can drift from the backend's configured OPERATON_BASE_URL. A successful answer is cached for the session; a failure is not, so the modal names the target as soon as the backend is reachable again. The outbound-guard follow-ups from v2026.09.5 are finished: the client pins the http adapter and HTTP/1 on every request, a refused redirect surfaces as EOUTBOUNDREFUSED, the unreachable ::/128 and ::1/128 entries are removed, test-connection gets its own request schema with apiToken optional, and the credential-leak test searches the whole call rather than part of it.
One Node version, read from one file — and a probe that checks it on the host. .nvmrc now carries 24.21.0, and all four deploy workflows read it through node-version-file, replacing three drifting literals (20.20.2 frontend, 22.23.2 backend). Both frontend workflows build build:acc / build:prod on the runner, from the tree npm ci installed, and upload packages/frontend/dist with skip_app_build: true — until now Oryx built the shipped bundle inside a floating container with npm install, on a Node it chose itself, so the code that passed the tests and the code that shipped were different builds. Both backend workflows install from the lockfile (npm ci --omit=dev --workspace=@linked-data-explorer/backend in a staging copy, 349 packages at the root lockfile's versions) instead of re-resolving every caret range at deploy time. All twelve jobs moved to ubuntu-24.04, the ACC deploy workflows filter in a changes job so their build checks can be required on acc, a change to .nvmrc now builds and deploys rather than shipping a new Node untested, and a root .npmrc sets min-release-age=14 for the transitive tree Renovate's own cooldown cannot reach.
The one check that looked like it covered the native binding did not. The Prepare deployment package step already asserted that require('libxmljs2') loads, but it runs on the runner — so it proves the binary matches the Node that built it, and nothing proved it loads on the host. Those are the same question only while the two agree, and on 23 September they stopped: the Node 24 move left a Node-22 binary answering on a Node-24 host, and DMN validation returned "NODE_MODULE_VERSION 127. This version of Node.js requires 137" for every user while health, build.sha, all three shape layers and /v1/dmns reported fine. libxmljs2 builds against NAN rather than N-API, so its binary is bound to NODE_MODULE_VERSION; build.sha cannot catch this and is not at fault, because it answers which commit's JavaScript is running and that answer was correct. Both backend workflows now POST a minimal DMN to /v1/dmns/validate on the deployed app. It is read-only — that endpoint validates content and deploys nothing — and it fails only on a message naming NODE_MODULE_VERSION or "was compiled against", never on a DMN that is merely invalid, so it cannot start failing because the XSD tightens or the fixture drifts.
Tests and tooling. About 350 lines of new test cover the branches the four new modules had left untested — the dossier service, the Ozon client, the quality service and the TTL cache — and the dossier join's fixture now carries nearly 600 lines of real decoy annotations from the production graph, because the old fixture was clean enough that a wrong join would still have passed. Six existing assertions that named a behaviour they could not fail on were made to bite: the TTL boundary at exactly ttlMs is now asserted live, clear() with no key asserts the cache is empty rather than that one key is gone, the cache-hit test no longer compares an object with itself, and the wId assertion checks the whole value rather than a prefix. Every test built bestuursorgaan without a bestuurslaag field, so all of them exercised the code-prefix fallback while production takes the other branch — a case where the two disagree now pins which wins, and bestuurslaag is validated against the known levels instead of being trusted through a bare type assertion. npm run dso:dossier renders a dossier from the command line, and a path-handling fix makes it run on Windows.
v2026.09.5 — The API Describes Itself, and Stops Fetching Whatever It Is Told (September 2026)¶
The published contract: API Specification. What a release is checked against: Post-deployment verification. Measured suites: Testing.
The API publishes its own contract. /v1/openapi.json now serves an OpenAPI 3.1 description, built from packages/backend/openapi/openapi.yaml and written in five phases (#133–#137) that closed #129. It describes 63 paths. Before this release, /v1/health and the root page both advertised "documentation": "/v1/openapi.json", and the path answered 404 — API-51 of the Dutch Government API Design Rules only appeared to be met. Two tests keep the document honest: every route response is validated against it (npm run test:contract), and a route-coverage test fails when a /v1 route is neither documented nor listed as pending. The pending list has since been closed, so every /v1 route is documented. Spectral lints the document against the NL API Design Rules 2.2.1 in both backend deploy workflows, and where the API departs from a rule the exception is recorded in the document rather than silenced in the linter.
Errors are RFC 9457 problem details. Every error the API produces itself is now application/problem+json with status, title, detail and instance, replacing five different envelopes: { success, error: { code, message } } on most routes, a bare string on /dso and /vendors, TriplyDB's own shape, a message-only 503 on the TriplyDB connection test, and a chain failure nested under data.error. code survives as an extension member, because it is what existing callers branch on. type is about:blank — there is no documentation page per kind of problem, and a URL that does not resolve would be worse than none. A failed chain execution keeps its partial result under data, beside the problem, so the steps that ran are not lost. Success responses are unchanged: they still carry { success: true, data }.
Two classes of client mistake stopped being reported as server failures. A body that does not parse now answers 400 MALFORMED_BODY, and one over the 10 MB limit 413 PAYLOAD_TOO_LARGE, whose detail names the limit (#143); both used to be 500s. And the asset and ROPA routes validate their input before it reaches Postgres (#150), answering 400 INVALID_INPUT with a detail naming every field that failed, where a missing field or malformed id used to come back as a 500. The checks follow the database's own constraints. A blank or whitespace-only title, id or name is refused on the fields that identify a record (#156).
Nothing leaves for a host a caller named without being checked (#142). Before this release the production backend would request any host a caller named. Every caller-supplied SPARQL endpoint — GET /norms, the five DMN reads, chain execution, both vendor reads, merged SHACL validation and POST /triplydb/query — is now checked at the route and refused with 400 INVALID_INPUT when it is not https:, carries credentials, or points to an internal address. Internal addresses are recognised in every textual IPv4 and IPv6 form, including IPv6 forms that embed an IPv4 address. A second check sits in the HTTP client itself: its agents refuse a name that resolves to an internal address, redirects are re-checked, and it pins proxy: false, because axios otherwise reads HTTP(S)_PROXY and swaps in a tunnelling agent that skips the lookup. TriplyDB calls that forward the caller's token must go to https: on a host in TRIPLYDB_ALLOWED_HOSTS, and GET /triplydb/assets now takes that token in an Authorization: Bearer header rather than a query parameter.
Processes deploy only to the configured Operaton. POST /dmns/process/deploy no longer builds an Operaton client from the request; it always deploys through the shared client, which carries OPERATON_API_KEY, so Operaton credentials stay on the backend. An operatonUrl equal to the configured one is still accepted from older frontends; any other answers 400, and operatonUsername and operatonPassword are ignored. All three fields are deprecated. The deploy modal no longer offers them and names the Operaton it deploys to instead. The Local Jena endpoint preset appears only in development builds, the only place the backend admits local endpoints — ALLOW_LOCAL_ENDPOINTS enables that for local development and is off everywhere else.
A report-only Content-Security-Policy, and the SPARQL editor goes through the backend (#161). A Vite plugin writes staticwebapp.config.json into the build output, so Azure Static Web Apps applies it: a Content-Security-Policy-Report-Only header naming the build's own backend in connect-src, together with Reporting-Endpoints, X-Content-Type-Options: nosniff and Referrer-Policy: strict-origin-when-cross-origin. A missing or invalid API URL fails the build rather than producing a policy that points nowhere. POST /v1/csp-reports accepts both report formats and logs one line per violation, storing nothing. The policy could only mean something once the SPARQL editor stopped fetching arbitrary endpoints from the browser: queries now go through POST /v1/triplydb/query, so the #142 checks apply to every one of them and the api.allorigins.win fallback proxy is gone — queries no longer pass through a third party. The connection badge now always reads Proxied via Backend. On the way, the Organizations view was found never to have worked: it passed its query and endpoint arguments in reverse, so the browser fetched the query text as a URL.
Tailwind is built, not fetched. index.html loaded the Tailwind Play CDN on every page load, in acceptance and production — third-party JavaScript executing in the application's origin, on an unversioned URL that cannot be pinned with an integrity hash. Tailwind 3.4 now builds with the application through PostCSS; staying on v3 keeps every class meaning exactly what the CDN served. An inert import map naming six packages on esm.sh with floating ranges was removed with it. That closes the last open Semgrep Code finding, recorded under v2026.09.4 (#96).
A deploy is recorded by the backend, not hoped for by the browser. A deploy could report success while nothing reached Postgres: the bundle was recorded by a second, unawaited request the browser made afterwards, whose failure was swallowed, and which was skipped altogether when the browser had no matching identifier in local storage. The deploy route now records the bundle itself, finding the stored process by the id in the BPMN or creating a minimal row. Recording cannot fail a deploy that Operaton has already accepted, so the response says whether it was recorded and why not, and the modal shows that as a warning rather than a tick. Saving a process also takes bpmnProcessId from the saved XML, so renaming the process id no longer creates a second row on the next deploy (#156).
Any DSO authority, chosen by level. The Activities tab offered four hardcoded authorities — Lelystad, Flevoland, Ede and Gelderland. Two dropdowns replace them, Level and Authority, generated from the government organisations register by npm run authorities:generate: 342 municipalities, 12 provinces, 21 water boards and 12 ministries, each with its OIN. Every page of an authority's activities is now fetched and combined (#158); measured against DSO production for Zuid-Holland on 19 September, 516 of 516.
The SHACL validator no longer passes what it did not check. Production reported every file Valid · All checks passed with all three shape layers Not loaded, while the same file against acceptance was invalid with 25 errors. Two faults combined: the production workflow never copied packages/backend/shapes into the deploy package, and the service computed valid from errors alone — a layer that did not load reports none. Both are fixed. The post-deploy gate that checks the shapes loaded now allows about three minutes for the new build to start, because the previous build keeps answering the health check in the meantime.
The backend reports which build is running. /v1/health gains a build block — sha, run and a label — from a deploy/build-info.json both backend workflows write into the artifact, and the post-deploy verification now waits until build.sha equals the commit being deployed, so a deploy that leaves the previous artifact serving fails instead of passing. version and the API-Version header are unchanged.
Smaller fixes. A disallowed CORS origin gets an ordinary response without an Access-Control-Allow-Origin header instead of a 500 (#145). DMN XML export fetches /v1/dmns/{identifier}/xml with the identifier URL-encoded; a duplicate legacy handler mounted ahead of the router had been answering every /api/dmns/:x/xml request, so that alias never sent its deprecation headers — it now does (#132). form_schemas.status and document_templates.status are NOT NULL, with any NULL backfilled on start (#151). The error banner and Settings panel animations, which named a Tailwind plugin that was never installed, are real now, and Settings closes on the full-width views it would have covered (#160, #76).
Tooling. The installed dependencies are checked against the lockfile before npm run dev starts and as the first step of the pre-push hook, so a clone that was never reinstalled after a lockfile change stops with npm ci named instead of failing lint on the wrong tool versions. bump-release checks the GitLab mirror at each release and prints the push command without running it. Renovate exempts lock-file maintenance from its pull-request limits, holds minor updates of pre-1.0 packages for approval like majors, and no longer raises engines floors. The supply-chain register records zizmor-action v0.6.3.
v2026.09.4 — The Lockfile Gets a Slot, and a Build (September 2026)¶
The mechanism behind this release, across repositories: Supply-Chain Pinning — the npm tree. Measured suites: Testing.
Lockfile-only changes are built, tested and deployed. The root package-lock.json and package.json now trigger the backend and frontend workflows in acceptance and production. Before, a change to the lockfile alone — Renovate's lock-file maintenance above all — was built, tested and deployed by nothing: every deploy workflow was path-filtered to its own package, and the one file all three workspaces share appeared in none of those filters. The 338-package refresh in v2026.09.3 reached acceptance only because an unrelated change happened to follow it. The first two pull requests after the change showed it working in both directions: a frontend-only dependency bump ran the backend's tests, and a backend-only bump ran the frontend build — each testing the application its shared lockfile could move.
Major updates wait for approval, so the lockfile refresh gets a slot. Lock-file maintenance is eligible only inside its Monday schedule, while ordinary updates are eligible any day, so they filled every free slot in Renovate's concurrency limit of five first. Giving lock-file maintenance prPriority: 10 was not enough on its own: priority orders branches eligible in the same run, and never keeps a slot free for a later one. What did was putting major updates behind Dependency Dashboard approval — the queue competing with the refresh was almost entirely majors (npm 12 and two workspace major groups), which are never merged on autopilot anyway. Behind approval they wait as checkboxes and hold no slot. Security fixes are exempt: vulnerabilityAlerts sets dependencyDashboardApproval: false explicitly, so one that happens to be a major version never waits on a click.
One refresh now arrives as one pull request. The per-workspace group rules had caught lock-file maintenance too and split each refresh into three identical ones; they now list every update type except lockFileMaintenance.
The last Semgrep false positive is suppressed in the code, not the dashboard. An Object.assign in test-case storage was flagged as a prototype-pollution risk; its only caller passes a fixed timestamp field, and the data is the user's own. The nosemgrep now sits on the line with its reason, and says when it would stop being true — if updateTestCase ever receives imported or URL-supplied data. Every Code suppression in the repository now lives in source. One open Code finding remains, and it is a true positive: the Tailwind Play CDN running from a third-party origin in the production frontend (#96).
Dependency updates, each rebased onto the refreshed tree before merging and verified locally with lint, the full suite and the build: backend axios 1.20.0 and @types/n3 1.26.2; frontend bpmn-js 18.26.0, @bpmn-io/properties-panel 3.53.0, camunda-bpmn-moddle 7.0.2 and @testing-library/react 16.3.3.
v2026.09.3 — Semgrep Gates Acceptance and Production (September 2026)¶
Semgrep Code and Supply Chain scan every pull request, and gate both acc and main. One scan job covers all three workspaces through the single root lockfile, so there is no per-workspace fan-out to keep in step. Supply Chain covers what nothing else here did: check-supply-chain verifies GitHub Actions pins and says nothing about package-lock.json. Introduced as a reporting check, it became a required check in both rulesets the same day, once the first baseline had been triaged from 86 findings to 5, none blocking. The application now reaches acceptance and production only through a scan of the exact commit being deployed.
Four details worth keeping, the last of them found here:
--no-suppress-errors. By defaultsemgrep cireports errors during analysis and still exits 0 — a scanner that cannot run would read as a clean scan.- Only pull-request runs are cancelled when superseded. A push run on
accormainwrites the Semgrep Cloud baseline, and cancelling one leaves the dashboard describing a scan that never finished. /examples/is ignored with a leading slash. A rootexamples/holds reference material, whilepackages/frontend/public/examples/is served — an unanchored rule matches both.- A
.semgrepignorereplaces Semgrep's default ignore list; it does not extend it. The first version silently brought 16 test files back into scope. The finding count came out the same either way, because none of them happened to trip a rule — only diffing the scanned file sets showed it.
The transitive dependency tree refreshed for the first time since January. Renovate maintains the dependencies a manifest names; the tree underneath moves only through lock-file maintenance, which the recommended preset leaves off. Here it had been enabled on 29 August and then rate-limited behind open pull requests ever since, so it had never run — rollup still sat at 4.55.1 from January although 4.59.0 was out in February. Forced by hand, one refresh moved 338 packages, 82 of them runtime dependencies, every one inside a range the manifests already declared and none published within the 14-day cooldown. It closed 63 of 66 Semgrep Supply Chain findings, including all five classed as reachable, all HIGH. The three left are bound by a tilde range in express and by a major version of @tiptap/core, and no refresh can close them.
Hardening from the first triage — three latent defects fixed where they live rather than where they are currently called from:
- Wildcard CORS matched on a bare path prefix. Any future sibling route whose name began with
public—/v1/ropa/publications, say — would have inherited wildcard CORS instead of the credentialed allowlist.isPublicPathnow matches a public mount or a path below it, never a sibling. See RoPA Records — the public routes. - BPMN process metadata was written into the XML unescaped, through
String.replace, where$1,$&and$'are expanded: a value ofA$'Bwrote the rest of the document into the attribute. Values are now escaped on write and decoded on read inronlAttributes.ts, whose attribute names are a closed TypeScript union. - A DMN export failure logged the DMN id inside the console format string, where a stray
%sswallowed the very error the line exists to report. The id is now logged as data.
Semgrep's ten application Code findings went to two; every remaining false positive carries a scoped nosemgrep naming the single rule, on the single line, with its reason.
v2026.09.2 — Twelve of Twelve, and Every Gate Now Blocks (September 2026)¶
The ladder in full: RIP Phase Ladder. Measured suites: Testing. Cross-repository posture: Coverage Floor.
R5.3 closes the Flevoland ladder. (vervroegde) Ingebruikname / Oplevering is modelled as RipR53Process — 15 nodes, 14 flows, 3 lanes, 7 forms and 6 documents — and it was the one phase left unmodelled for want of a design, so R5.2 and R5.4 were built to step over it. The sheet lands exactly where those two said it would, which is the useful confirmation: the ladder's shape was inferred correctly from its neighbours' entry and exit criteria before the middle existed.
The phase is a single early choice — oplevering or (vervroegde) ingebruikname — opening two near-mirror paths that never rejoin, differing only in what they are called, which sjabloon they use, and where they leave to. There is no loop anywhere in the phase: a rejected schouw sends its restpunten back to R5.2 rather than round again here.
check-supply-chain is now a blocking check. continue-on-error: true is removed from the audit job's pin-truth step, so a register that no longer describes the workflows fails the gate. What it was waiting on is resolved rather than abandoned: Renovate rewrites workflow pins and their version comments together and never touches SECURITY-PIPELINE.md, so every action bump would fail the register half — and blocking on that would fail a required check on routine dependency updates, which is how gates get resented and then bypassed. The answer was to update the register on the bump's own branch before merging, so the check is green on the pull request rather than only on acc afterwards. That habit was exercised twice — on the checkout v7 and setup-node v7 bumps — before the promotion.
Both exercises are worth recording because the check behaved exactly as designed: pin truth passed and only the register was stale. Renovate's digest and its rewritten comment agreed with each other and with GitHub; what had drifted was the document claiming to describe them. That is the drift this check exists to catch, and it caught it on the branch rather than after the merge.
The backend runs its tests on a pull request. azure-backend-acc.yml triggered on push alone, so 1140 backend tests ran only after a merge and a backend pull request reached acc with audit as its only check. The pull_request trigger arrives with six deploy-side steps gated on the event, arranged as per-step conditions rather than a job split so the check name stays stable and no ruleset entry changes. Closes #46.
The per-file 80% branch floor is enforced natively, in jest.config.js and vite.config.ts. Against a package average the threshold is inert — backend sits at 92.22% and frontend at 90.59%, and one file falling to 40% barely moves either.
And then it was given room to breathe. The floor landed measured-clean but with no slack: ChainBuilder/TestCasePanel.tsx sat at exactly 80.00% and thirteen more files between 80 and 85, so the first uncovered branch added to any of them would have turned CI red on an unrelated change. Twelve files raised, package branches 90.59% → 92.88%, files under 85% from fourteen to one, tests 1042 → 1073. No production code changed — test files only.
Every new test was mutation-checked, and five were asserting nothing
A test written against code that already exists passes on its first run, which proves nothing about whether it can fail. Each new test had the branch it targets deliberately broken and had to fail before being kept.
Four of the five share a shape worth knowing on any React codebase: an
assertion that "nothing happened" stays true when the handler throws
partway through, because React reports a throw inside a click handler on
window's error event rather than rejecting the click. See
Raising coverage without writing hollow tests.
Every pull request is audited, not only those targeting acc or main. The audit triggered on pull_request filtered to those branches, so a stacked pull request based on a feature branch matched no trigger and accumulated no audit at all — while still reporting mergeStateStatus=CLEAN with zero checks, which reads as ready and is not. The moment its parent merged and GitHub retargeted it, the required audit was missing and the pull request blocked permanently, because a retarget emits no pull_request event. Closing and reopening was the only way out, and it was needed four times in one session on 2 September.
The changelog identifies the build, not just the release — see Build Provenance. The commit SHA says what was built; the run number distinguishes two builds of identical code, which is what makes the pair a build id rather than a code id. With nothing injected it reads local build: never blank, and never resembling a deployed artifact when it is not one.
Getting Started is removed. The Tutorial view, its tests and the 596 lines of tutorial.json behind it are deleted, along with ViewMode.TUTORIAL and the four view-mode guards that kept other panels hidden while it was showing. It is kept in one commit with the icon-rail fix deliberately, because the second is what makes the first safe to reason about: the rail was already losing icons off the bottom, so removing one only moved the threshold rather than fixing it.
Dependency updates were verified on a scratch branch off acc rather than trusting the pull request's own green tick, which had run against a tree two merges stale. npm ci, lint, the full suite, build, typecheck and check-format all clean. The backend workflow ran no tests on a pull request at that point, so a green tick there said nothing about whether a dependency broke anything — the gap this same release closed.
Prettier is kept off Semgrep Guardian's scratch files. Guardian writes a .semgrep/ directory into the working tree, one per directory it scans, and check-format failed on the guardian.yml inside them. .gitignore already excluded them so they never reach a commit; .prettierignore did not, so a clean tree still reported two style violations in files nobody authored.
v2026.09.1 — Nine Phases, and the Typechecker That Was Never Running (September 2026)¶
The ladder as a whole: RIP Phase Ladder. Measured suites: Testing.
Nine RIP phases were modelled in one release, taking the ladder from two to eleven. R2.3, R2.4, R3.1, R3.2 and R4.1 cover estimation, design and procurement; R5.1, R5.2 and R5.4 cover preparation, supervision and delivery; R6.1 closes the project. Only R5.3 remained unmodelled at this release. Each is authored here and deployed through the BPMN Modeler — the RONL Business API adopts them into its phase catalogue separately.
R5.2 is the densest phase in the ladder, and the only one whose subject is a period rather than a deliverable. 56 nodes and 67 flows over 36 forms and 11 documents, modelled as a parallel split into the weekly cycle, invoicing and the delivery request, joined before R5.3. The flat alternative was rejected for a concrete reason: one loop containing every stream would force an invoice and a delivery request through every week, and deadlock a real instance on the parallel join. Three rework loops remain — weekstaat rejected, AWR overview rejected, and work not finished.
Where the source sheets and BPMN semantics disagree, the disagreement is recorded rather than smoothed over. R6.1's sheet draws two Einde proces markers, one per closing action; they are modelled as a single end event behind a parallel join, because they mean the same completion and one exit keeps the phase end detectable the way every other phase's is. R5.4's sheet draws an Aannemer band carrying no activity — the contractor is written about but never acts in that phase — and an empty lane would deploy as a lane no task can land in, so it is omitted and the reason left in a text annotation. R5.1's BO13.1 branch is a round trip rather than an exit: on ja the installation part is handed to Beheer en Onderhoud, which communicates the handover back, so the leg rejoins the document check and the phase keeps its single R5.2 exit.
R2.3 is the first phase generated from the reusable BPMN toolchain rather than laid out by hand, and it surfaced a start-form trap: the start form was moved into the first task, because on a programmatic start the gating variable would be unset, leaving the instance with no selectable outgoing flow.
Fifteen type errors had accumulated invisibly, because nothing in the repository ran tsc. build is vite build, which strips types through esbuild without checking them; lint is ESLint; test is Vitest. None of the three typechecks. A typecheck script now exists at the root and in both workspaces, and a Typecheck step runs in all four Azure deployment workflows, so the gap cannot reopen. Four of the errors were in the ChainComposer test fixtures and each was fixed by correcting the fixture rather than loosening an assertion — the instructive one being a missingInputs array of strings where RequiredInput was declared, which passed because only its length reaches the DOM. Exactly what an absent typechecker hides.
Node versions are pinned to exact patches in the deployment workflows — 20.20.2 for the frontend, 22.23.2 for the backend, 24.19.0 for the supply-chain audit — with the engines floors raised to match. This closes, for this repository, the floating-runtime gap that Supply-Chain Pinning records as an open exception elsewhere.
Per-file branch coverage clears 80% across both packages. Frontend branches rise from 65.67% to 90.62% and backend from 91.49% to 92.22%, with every file above the bar — see Testing for the measured figures. Two jsdom limitations had to be worked around for the d3 drag and zoom paths: jsdom rejects view in the MouseEvent constructor, and SVGSVGElement carries no width/height baseVal for the default extent d3-zoom computes.
The deploy modal keeps its actions reachable. With bundles now reaching 48 resources it had grown past the viewport, pushing Deploy and Close off-screen. It is restructured into a pinned header, a scrolling body and a pinned footer — the body needing min-h-0 to shrink below its content, since a flex child defaults to min-height: auto and will not.
Frontend dependencies updated: bpmn-js 18.24, form-js 1.25, bpmn-js-properties-panel 5.64, React 19.2.8, TypeScript 5.9.3, Prettier 3.9.6 and the surrounding ESLint and testing-library packages. Verified before merge rather than after: check-format passes untouched under Prettier 3.9.6, and TypeScript 5.9 produces exactly the same fifteen pre-existing type errors as 5.8 — no new ones — so the tsc gate landing in the same release covers precisely the intended set.
v2026.09.0 — A Deploy That Refuses an Incomplete Bundle (September 2026)¶
A bundle could deploy with none of its documents and still show a green panel. The deploy modal filtered referenced forms and document templates against the browser's own storage and dropped every miss silently. RipR21Process deployed with its BPMN and twelve forms but none of its three documents.
The failure surfaced a long way from its cause, which is why it is worth recording in full. At runtime the RONL Business API read ronl:signatureRef from the engine, looked for rip-pdp.document in the deployment, threw SIGNATURE_TEMPLATE_NOT_FOUND and returned 500 from the task-spec endpoint. ProjectDetail fetches that spec inside a Promise.allSettled and degrades a failure to "no signature required" — so the phase-exit approval task rendered an ordinary form instead of the signing panel, and the end-to-end journey failed on a missing panel. Three components away from the modal that dropped the resource.
Missing resources are now listed by name and the deploy button is disabled while any remain, matching the existing board-owner and organization guards. unmatchedForms was already computed but rendered nowhere, and is surfaced alongside. extractDocumentRefs now also follows ronl:signatureRef: a task binding a template through that attribute alone never contributed it, and rip-pdp survived only because it carries both.
Forms bind to their own deployment instead of the latest version. Tenanting a process definition made the process unambiguous and its forms ambiguous: camunda:formRefBinding="latest" resolves a form key across the whole repository rather than within the bundle, so deploying AwbShellProcess under tenant flevoland produced ENGINE-03109 — the key kapvergunning-start existed for multiple tenants. "deployment" resolves the form from the process definition's own deployment, unique by construction, so tenanted and untenanted copies coexist and no deployment history has to be destroyed.
91 of 109 references were converted, across the three trees that hold BPMN: packages/frontend/public/examples/ (seed and deploy source for the Awb, Zorgtoeslag, DvTP and HR bundles), examples/organizations/ (authoring source for RIP and HR-capacity), and e2e-fixtures/ (what the RONL Business API's E2E suite deploys). thuisbatterij and ind stay on "latest" because their form keys resolve nowhere in the repository.
Already-deployed definitions do not heal
public/examples is seeded into localStorage and re-fetched only when its version rises, so the seven seeded BPMN entries in EXAMPLE_VERSIONS were bumped — without that, the edit would be invisible to every existing user. ACC users get the fix only after a frontend deploy and a page load that re-seeds, and the e2e-fixtures bundles must be re-imported through the BPMN Modeler before an E2E run exercises them.
v2026.08.9 — Changelog gains ci and repo scopes, and stops crashing on an unknown one (August 2026)¶
Three entries in the previous release changed no deployable's code at all, and the scope field offered only frontend, backend and both — so none of the three was true for them. Labelling any of it both would have told a reader that a release touching only .github/ had changed application code. ci now covers pipeline and supply-chain work, mirroring the tag ronl-business-api already uses so both repositories label the same kind of work the same way; repo covers everything else that ships no application code — documentation, and the Operaton deploy bundles under examples/ and e2e-fixtures/. Those bundles are worth distinguishing from a frontend change, because the frontend serves its own copies from packages/frontend/public/examples/, while the root directories are read only by the backend's tests and deployed to Operaton separately.
An unrecognised scope would have taken the whole panel down. The commit-type union had no ci, but unknown types fall back to other, so a ci entry rendered as a generic document icon — filing an entire supply-chain effort under "other" rather than crashing. ScopeBadge had no such fallback: changelog.json is imported as untyped JSON and cast at the module boundary, so TypeScript never checks the scope strings the data actually carries. An unrecognised one reached SCOPE_BADGE[scope], yielded undefined, and would fail on config.cls. The type system cannot help here by construction, which is what makes the guard — not the types — the thing that made the new scope tags safe to introduce.
The release command lands through a pull request. Two steps of /bump-release were not merely out of date but impossible: it fast-forwarded acc locally and asked separately about pushing, and both halves are now rejected, because acc requires a pull request and a passing audit. It pushes the branch and opens a pull request instead — merging is the push. The merge method is called out explicitly, because both alternatives silently break the changelog entry that names every commit by SHA: squash collapses them into one new commit, and rebase replays them as new commits, preserving the count while replacing every hash. A new first step reconciles open Renovate pull requests, which rewrite package-lock.json — the same file a version bump edits.
v2026.08.8 — Four advisories closed, and a formatter that reformatted five untouched files (August 2026)¶
Twenty-three backend packages were brought up to date, of which exactly one carried an advisory: express 4.18.2 → 4.22.2. The rest — helmet, pg, winston, dotenv, cors, sparql-http-client and the TypeScript, ESLint and Jest toolchain — are routine currency rather than security work, and are recorded as such rather than presented as advisory fixes.
Three further advisories closed through the security fast-lane, which clears the 14-day cooldown for known advisories — the one class of update that must not wait. axios ^1.6.5 → ^1.18.0 (lockfile 1.20.0), spanning fourteen minor releases and the widest jump in this release; fast-xml-parser ^5.3.5 → ^5.7.0 (lockfile 5.11.1); and vite ^6.2.0 → ^6.4.3. The split between a raised floor and a newer resolved version is rangeStrategy: bump working as intended: raise the range to the safe minimum, let the lockfile pin what is current.
Only one of the three ran a full check. The frontend workflow triggers on pull requests, so the vite update ran lint, tests and a preview deploy; the backend workflow triggers on push only, so the axios and fast-xml-parser updates had no pull-request check that said anything about whether a dependency had broken something. Both were verified against the full backend suite locally before merging. The same asymmetry meant concurrently's branch was never rebased — it did not conflict — so the merge was trialled locally first and the lockfile checked with npm ci, because a clean textual merge of two lockfile diffs can still produce a file npm refuses.
Prettier 3.9 reformatted five files nobody had touched. It formats short union types on a single line where 3.7 produced the leading-pipe multiline style, so those five began failing prettier --check the moment the upgrade landed. No workflow runs check-format — the deploy workflows run lint and tests only — so nothing in CI reported it. The gate that catches this is the pre-push hook, which means the symptom would otherwise have been the next person's push failing on files they had never opened.
v2026.08.7 — Supply-chain pinning enforced, and a silently inert Renovate (August 2026)¶
Cross-repository detail: Supply-chain gate.
Nothing this pipeline downloads or executes may float. All twenty action references across the six deployment workflows are now commit digests with their version in a trailing comment, and a zizmor gate refuses any pull request that reintroduces a floating tag. Findings go from 40 to 0. Each workflow also gains the hardening zizmor was reporting: persist-credentials: false on checkout, explicit workflow and job permissions, and a concurrency group keyed on the pull-request number rather than the ref — because github.ref alone puts the pull_request(closed) teardown and the push deploy that a merge fires into one group, where they cancel each other at random.
renovate.json supplies the other half of the policy: a 14-day cooldown with internalChecksFilter: strict, cleared by vulnerabilityAlerts for known advisories, and dependencies grouped per workspace. SECURITY-PIPELINE.md records what is pinned and, more importantly, what is not — the static-web-apps-deploy Docker image behind four workflows, and the backend deploy step's lockfile-less npm install.
Renovate had been opening nothing at all. It validates strictly and rejects unknown options, so the five "//"-prefixed keys used as JSON comments were read as five invalid settings rather than ignored, and Renovate stopped raising pull requests as a precaution. That is correct behaviour from it — but it meant the half of the supply-chain policy that keeps pins current was inert from the moment it landed. Pins without updates decay into an unpatched tree, so a silently inert Renovate is precisely the failure the audit exists to prevent. Every comment moved to a description field, valid at the top level and inside any nested object; no policy changed, only the annotation style.
The gate now validates the configuration too. renovate-config-validator runs as a second step in the same audit job, so it is covered by the existing required status check and needs no ruleset change. It runs under if: always(), so a zizmor failure cannot hide a broken configuration behind it, and with no filename argument — passing one switches the validator into global-config mode, which applies different rules than the repository config this file actually is. --strict earned its place immediately: it fails on configuration Renovate would silently auto-migrate, which surfaced baseBranches, renamed upstream to baseBranchPatterns and therefore invisible on every previous run.
The validator runs on the Node version Renovate requires. renovate@44.50.3 declares engines.node ^24.11.0 while the runner defaults to Node 22, and npm accepts that mismatch with an EBADENGINE warning rather than refusing — so the validator had been running unsupported and still reporting green. setup-node is placed before the zizmor step rather than beside the validator it serves: a step following a failed one is skipped, so putting it after would leave the validator's if: always() running on whatever Node the runner defaulted to, precisely when zizmor had already failed and the logs were being read.
v2026.08.6 — A ValidSign signature on the R2.1 phase exit (August 2026)¶
One attribute is the switch for the whole signing feature. ronl:signatureRef="rip-pdp" is added to Task_AccorderenProjectplan4 — the "Accorderen Projectplan 4. Uitgangspunten VO-fase" task that closes R2.1. The RONL Business API resolves ronl:signatureRef on a user task and, when present, replaces that task's plain approval form with a ValidSign signing ceremony: the phase document is rendered from its deployed template, a signature package is created, and the Operaton task completes only once the signature lands.
The parity test had been failing since that commit. RipR21Process.bpmn exists in two places the test locks together byte for byte — examples/organizations/flevoland/rip-phase-21/ is the authored source, e2e-fixtures/flevoland/ the mirror — and the attribute had been added to the mirror only. Because the backend deploy workflow triggers on push and not on pull requests, no pull request runs these tests: the failure would have surfaced on acc after merge, where npm test gates the deploy step, leaving a red acceptance branch and no deployment. The xmlns:ronl namespace was already declared in both files and the byte delta was exactly the length of the attribute, so the repair adds one attribute rather than reflowing the document.
v2026.08.5 — The R2.2 VO bundle, and a parity test that locks each bundle's two copies together (August 2026)¶
Bundle contents and deployment: RIP R2.2 VO Bundle.
RipR22Process picks up where R2.1's "Fase 1 voltooid → R2.2" end event left off — four lanes and nine user tasks, from R2_2 - VO.pdf (rev. 21-11-2024). All five branches of the opening parallel split rejoin the join gateway. The source PDF does not draw it that way: it shows Inventariseren kabels en leidingen and Aanvragen raamvergunning leaving the pool into CO1 and JU3.5 and never returning, which as control flow deadlocks at the join. Those hand-offs are textAnnotations instead, because CO1 and JU3.5 do not exist as fixtures and a callActivity would dangle at deploy time and fail the manifest's calledElement test. They are referenced by several phases and belong in a shared bundle of their own.
Nine forms and five document templates complete the bundle. One form per user task, bound by camunda:formRef, with field types kept inside the eight the R2.1 bundle already uses, since nothing else is exercised against this Camunda 7.21 stack. One template per green "Format …" box in the specification — KES, Ontwerptoelichting, Objectenboom, Bevindingenformulier and Hoeveelheidsbepaling — because a Format in the diagram and a .document here are the same thing: a template with bindings. The blue outputs beside them in the spec are instances of these templates rather than artifacts of their own. Their zone keys are signOff and contactInformation from the start, unlike R2.1's templates, which shipped with signoff and contactInfo — keys DocumentZones never declares, leaving their signature blocks unrendered until v2026.08.4 repaired them.
The templates would have imported and attached to nothing. R2.1 wires each document template to the task that produces it with ronl:documentRef — the attribute DocumentTemplateSelector writes and BpmnCanvas reads to render the document badge on a task. The R2.2 specification never mentioned it, so the process shipped without it. The attribute is single-valued, so a task carries at most one template, and four of the five bind. rip-objectenboom stays unattached deliberately: Task_OpstellenConceptVO produces both the Ontwerptoelichting and the Objectenboom, and its one slot went to the Ontwerptoelichting. The Objectenboom still ships and imports normally — it simply carries no task badge, and its reference is maintained in Relatics instead.
Each bundle now exists twice on disk, and a test keeps the copies identical. Authored under examples/, imported and deployed from e2e-fixtures/ — with nothing stopping the two drifting, and they had: v2026.08.4 repaired the document zone keys in the e2e-fixtures copies only, and the examples copies kept the dead keys until they were re-pasted by hand. A new test asserts every file in a mirrored bundle is byte-identical to its twin; new bundles opt in by adding an entry to MIRRORED_BUNDLES.
rip-phase1-swimlanes is renamed to rip-phase-21. The -swimlanes suffix distinguished the bundle from a competing rip-phase1/ draft that has since been deleted, so it distinguished nothing, and the directory name no longer matched the process it holds. rip-phase-21/ holds RipR21Process and rip-phase-22/ holds RipR22Process, which makes the two obvious siblings. Contents are untouched, and exactly one reference to the old path existed — the source field of the RipR21Process entry in e2e-fixtures/manifest.json. Nothing resolves this directory at runtime: the application serves examples from packages/frontend/public/examples/, which has never held the RIP bundles.
v2026.08.4 — Signature blocks that had never rendered, and the DSO API surface documented (August 2026)¶
Two defects had shipped with every deployment of the three RIP document templates. The templates used signoff and contactInfo where DocumentZones declares signOff and contactInformation; DocumentCanvas iterates ZONE_ORDER and calls getZoneBlocks('signOff'), so the lowercase key meant the Signatures block was dropped silently — the three signature lines in these templates had never rendered at all. They also declared processKey: "RipPhase1Process" while the BPMN they deploy with declares RipR21Process, which is also the key the fixture manifest lists them under. The authored examples/ copies were brought back in line with the e2e-fixtures/ mirror ahead of the parity test that would later enforce it mechanically.
The DSO Viewer's full API surface was written down, mapping each viewer feature to the upstream API behind it — Stelselcatalogus v3 for concepts, RTR Gegevens v2 for activities, Zoekinterface v2 for werkzaamheden search, Opvragen Werkzaamheden v1 for werkzaamheid detail, and Toepasbare Regels Uitvoeren Gegevens v1 for rule metadata — together with the call path per feature, a complete endpoint map, pre- and production base URLs, and the transport conventions. That material is already reflected on DSO integration and API reference.
The Activity Detail panel's child fan-out was recorded as the viewer's heaviest interaction. The RTR returns onderliggendeActiviteiten as bare HAL hrefs with no omschrijving, so the panel fires one extra activity-detail request per child, in parallel, purely to resolve names: opening a single activity costs 1 + N upstream calls, and 24 for an activity with 23 children. There is no cache and no concurrency cap, which makes it the first candidate for memoisation.
v2026.08.3 — A test gate, and two defects it did not catch (August 2026)¶
Full inventory, commands and coverage: Testing.
Every deploy now runs the suites, and a failure blocks the deploy. None of the six Azure workflows previously ran a test step. That was a deliberate P7 decision taken when backend coverage was first measured at 13.82% statements — gating on a number that low would have been theatre — and it was recorded as conditional on backend breadth improving. v2026.08.2 took the backend from 16.79% to 98.06%, meeting the condition. The backend workflows already installed and linted, so a Jest step slots in beside the existing lint; the frontend workflows had no npm steps at all, because the Static Web Apps action builds inside its own container and runs none of this repository's scripts, and they gain an explicit install, lint and test sequence ahead of the deploy action. The two ropa-site workflows are deliberately untouched — that package is a static index.html with no build and no tests.
E2E fixture BPMNs were undeployable, and nothing caught it. BPMN 2.0's tProcess is an ordered sequence — laneSet*, flowElement*, artifact*, … — so once an artifact appears, no further flow element may follow. The commit that added the on-canvas "E2E FIXTURE" warning inserted its textAnnotation and association directly after the first flow element, leaving four of the five fixtures rejected by Operaton's XSD validation on deploy. TreeFellingPermitSubProcessE2E was the one file with the banner correctly at the end, and the only one that deployed. Each banner moved to just before </bpmn:process>; all five now validate against bpmn-moddle's BPMN20.xsd. The manifest integrity test checked file existence, process ids and calledElement references but never whether the BPMN would deploy — it now asserts the ordering rule directly.
The BPMN deploy dialog sent the wrong process key for every model. doc.querySelector('process') is a CSS type selector, which matches only the null namespace, so it never found the <bpmn:process> element that real bpmn-js output always emits. All four call sites fell through to their own fallbacks: deployments posted the literal string "process" instead of the model's id, and sub-process lookups by calledElement never matched. A findProcessElement helper now matches on local name across namespaces. The test covering this had asserted the fallback as correct, blaming a jsdom quirk — half right, since its fixture declared none of the prefixes it used, so DOMParser rejected the document outright and returned a <parsererror> in which nothing was findable, masking the real defect underneath.
The error overlay is reachable from the view that raises it. It lived inside App's right panel, which is hidden whenever viewMode is Orchestration — the only view from which Refresh Cache can be triggered. A failed cache clear set the error state correctly and had nowhere to render, staying silent until the user happened to navigate elsewhere, where a stale error then appeared out of context. The overlay is now a direct child of the workspace container and renders in every view.
v2026.08.2 — DMN deploy/evaluate proxies & ten dead validation rules (August 2026)¶
v2026.08.2 — Feature & Patch (August 18, 2026)
POST /v1/dmns/deploydeploys raw DMN XML ad hoc, without requiring a pre-registered LDE norm identifier — built for the CPSV Editor's DMN tab, which holds an uploaded or generated file with no registry entry of its own. A thin wrapper around the provenoperatonService.deployDrd().POST /v1/dmns/evaluate/:decisionKeyproxies an evaluate call server-side. Both routes exist for the same reason: the CPSV Editor called Operaton directly from the browser, which CORS blocks for a local dev origin. The evaluate proxy forwards Operaton's response byte-for-byte and status-for-status — the raw success array or exception object, not the usual{success, data, error}envelope — because the DMN tab reads Operaton's own JSON. A newevaluateRaw()passes variables straight through, skipping the type inferenceevaluateDecision()does for its different caller contract, which would otherwise double-wrap an already Operaton-shaped body. See API Reference and the CPSV Editor's DMN Implementation.- Ten CPRMV validation rules were dead code and had been since the file was written.
cprmvAttr()'s primary lookup calledel.attr({ name, ns }), which in libxmljs2 0.37 is the setter overload, not a namespaced getter: it sets attributes literally namednameandnson the element and returns a value with no.value(), so the call threw. The function's owntry/catchswallowed the throw and returnednull, so the workingattrs()fallback beneath it was never reached.EXEC-002–EXEC-010andCON-001–CON-003therefore never fired for any DMN, while the validator reported a clean result — and every inspected element was mutated with two junk attributes. Dropping the object-form fast path is the whole fix. See DMN Validation Reference. CPRMV_NSwas additionally hardcoded to the legacycprmv.open-regels.nl/0.3.0/namespace, so a DMN using the currentstandaarden.open-regels.nl/standards/cprmv/0.4.1#namespace short-circuited on anEXEC-001"CPRMV not declared" before reaching any check.CPRMV_NAMESPACESnow accepts both.- New
EXEC-011/EXEC-012validate cell-leveldct:source/cprmv:isBasedOnformat on grounded DMN cells — the cell-level grounding the CPSV Editor now emits — andEXEC-013closes a pre-existing gap wherecprmv:extends' format was never checked at all. INT-007false-positive fix:FEEL_RESERVEDcarried the singularyear/month/day/… for the single-word built-ins but not the pluralyears/months, which are the leading words of theyears and months duration(...)built-in's own name. Those words aren't preceded by a dot, so only the reserved-word check catches them. Reproduced live againstHvA_full_dmn_export-patched.dmn: 12 warnings across 6 decisions, all false positives from this one construct.- BPMN shell/subprocess matching now keys on
shellId, notbpmnProcessIdalone. Once a shell is deliberately duplicated — an e2e-fixtures copy keeps the samebpmn:processid, since that is the real production Operaton key — the catalog rendered every subprocess under every shell sharing that id.shellId(the parent shell's own local record id) is now stored alongsidecalledElement, with a fallback to the old match for records saved earlier. - That fix did not survive a remount until the data actually reached the database:
hydrateFromServer()treats the Postgres list as authoritative and replaces local state on everyBpmnModelermount.process_definitions.statushadCHECK (status IN ('example','wip'))with no'e2e', so saving an e2e process violated the constraint and failed silently (fetch()does not reject on non-2xx), andshell_idwas not in the schema, upsert or list query at all. Addsshell_id(additive, idempotent) and widens thestatusCHECK. - New E2E status badge for imported e2e-fixtures forms and documents, detected at import from a self-describing marker in each fixture. A shared
StatusBadgecomponent replaces three near-identical inline badge blocks acrossProcessList,FormListandDocumentList. The badge is orange — indigo read too close to EXAMPLE's blue. - Seed versions bumped for
example_tree_felling,example_zorgtoeslag_provisionalandexample_zorgtoeslag_finalso existing users' copies actually receiveshellId; without theEXAMPLE_VERSIONSbump,seed()skipped re-saving them and both stores kept theshellId-less copy. - Backend statement coverage raised from 16.79% to 98.06% (branches 18.10% → 89.18%) by adding 31 test files and extending 3, with no production code changed. This is the campaign that found the
cprmvAttr()defect above — documented first as a testing-scope decision, fixed two commits later. Line endings normalised to LF via.gitattributes, closing a loop where a Windows checkout produced CRLF files thatformat:checkrejected and Git kept renormalising back. See Testing.
Files: packages/backend/src/routes (/v1/dmns), packages/backend/src/services (operaton.service.ts, dmn-validation.service.ts), packages/backend/src/db, packages/frontend/src/components (BpmnModeler, StatusBadge)
v2026.08.1 — Mandatory deploy organization & the e2e-fixtures bundle (August 2026)¶
v2026.08.1 — Feature (August 14, 2026)
- Organization is now mandatory when deploying a BPMN process. The Deploy action will not submit without one, and sends it to Operaton as its native tenant-id (
POST /deployment/create'stenant-idfield) — closing the gap where a process could deploy with no tenant-id at all, invisible to any tenant-scoped lookup an application later makes against it. - Shared DMN decisions resolve as untenanted from tenant-scoped business-rule-tasks. Confirmed empirically against a live Operaton instance: a business-rule-task's
camunda:decisionRefresolves against a decision definition under the exact same tenant-id as the calling process instance, with no fallback to a shared untenanted decision even when one exists.camunda:decisionRefTenantIdcan override this, but only as an EL expression evaluating to null (${null}) — a literal empty string is silently ignored. Applied to all 7 business-rule-tasks across the 4 fixture BPMNs that reference genuinely tenant-agnostic regulatory logic. - A new
e2e-fixtures/<tenant>/directory with amanifest.jsonand an integrity test becomes the single source of truth for the RONL Business API's E2E suite, replacing two pre-existing and already-diverged "examples" locations. - The e2e sub-processes were renamed (
TreeFellingPermitSubProcessE2E,ZorgtoeslagProvisionalSubProcessE2E) so LDE's own catalog stops conflating the fixture copy with the seeded-example copy of the same sub-process — previously both shared abpmn:processid, so importing the fixture could silently reuse the example's stale content. - Fixed a fixture that Operaton's BPMN parser rejected (
ENGINE-09005): atextAnnotation/associationpair immediately preceding the file's firstbusinessRuleTask. Every other fixture has ascriptTaskin that position, which validates fine, so this adjacency had never been parsed for real. Moving the annotation to the end of the process body is also the more conventional placement. - Fixed a copy-pasted
processKeyon the Zorgtoeslag provisional document template, and nested each sub-process fixture under its shell'ssubProcessesarray rather than listing it as a flat sibling.
Files: packages/frontend/src/components/BpmnModeler, e2e-fixtures/
v2026.08.0 — Ede and Gelderland location presets (August 2026)¶
v2026.08.0 — Feature (August 6, 2026)
- The DSO Viewer's Activities tab gains the municipality of Ede and the province of Gelderland alongside the existing Lelystad and Flevoland presets. One array drives both the location filter buttons and the authority-name lookup used when importing forms, so no other change was needed.
- OINs were sourced from overheid.nl's Identificatiecodes records and cross-checked structurally — each OIN embeds the organisation's own RSIN, and both new values follow the same
00000001<RSIN>000pattern as the existing entries.
Files: packages/frontend/src/components/DsoExplorer
v2026.07.1 — DMN validator: missing-id deploy failures and FEEL false positives (July 2026)¶
v2026.07.1 — Patch (July 23, 2026)
- Adds
BIZ-010throughBIZ-014, flagging<input>,<output>,<rule>,<inputEntry>and<outputEntry>elements missing theidattribute as errors. Operaton's DMN transformer requiresidon these decision-table clause elements even though the DMN 1.3 XSD marks it optional, and rejects such files withDMN-02011at deploy time — a class of failure this validator previously reported as fully valid. - Fixes two
INT-007false-positive sources. FEEL names may legally contain spaces, and a bare<inputExpression>that is itself one multi-word declared name was being shredded word-by-word by the identifier tokenizer, with every word flagged as an unresolved variable; a whole-string match against declared and produced names is now tried first, falling back to tokenization. Separately,FEEL_RESERVEDwas missing the single-word FEEL date/time component functions (year,month,day,hour,minute,second), so ayear(...)call flaggedyearitself as unresolved. - Found while debugging an Amsterdam DMN's Operaton deploy failure in the CPSV Editor repository; together the fixes cut that file's Interaction Rules warnings from 83 to 12. The residual 12 — multi-word names embedded inside compound expressions, plus one likely real DRD-wiring gap in the source — are documented as open issues rather than fixed, since closing them properly needs a materially larger longest-match tokenizer change.
Files: packages/backend/src/services/dmn-validation.service.ts
v2026.07.0 — Test suite (P0–P6.8), CalVer, and a coverage baseline (July 2026)¶
v2026.07.0 — Infrastructure (July 22, 2026)
- A phased test suite lands across both packages, taking the repository from zero test files —
npm testexited 1 with "No tests found" — to full-stack coverage. Backend P0–P3 covered utilities, errors and middleware, the ropa/vendor/assets services, and the five smallest routes with supertest. Frontend P4–P6.8 bootstrapped Vitest and covered pure-logic utils, the 11-module service layer withmsw, then every component directory in coupling-severity order: no third-party coupling first, then@dnd-kit, then one embedded editor library, then two, with the top-level integration components last. See Testing. - P7 measured real coverage and deliberately deferred CI wiring. At the time: 109 backend tests at 13.82% statements, against 557 frontend tests at 74.03%. No CI test step was added to the Azure workflows, blocking or non-blocking, because the backend gap was breadth — whole route and service files never touched — rather than depth. The condition for revisiting it is recorded: once backend breadth improves, or the team accepts breadth alone as a gate. (Backend breadth was subsequently closed in v2026.08.2; the CI step remains deferred.)
- Two real pre-existing tooling gaps surfaced and were fixed, both latent only because the repository had no test files:
tsconfig.eslint.jsonextendedtsconfig.jsonwithout overriding itsexcludeof**/*.test.ts, so ESLint's type-aware parser could not see any test file; and.gitignore's blanket*.jsrule, meant for compiled output, silently blockedjest.config.jsfrom ever being tracked. - Release versions switch to CalVer (
YYYY.MM.patch), matching the CPSV Editor. The sequence is product-wide rather than per-scope, so a backend-only and a frontend-only release still share the next number. Historical SemVer entries are left as they are. - Local development now points at the
ronl-operatoncontainer from the RONL Business API stack (localhost:8081) instead of the remote instance, anddocker:checkverifies it alongsideronl-postgres.
Files: packages/backend (Jest), packages/frontend (Vitest, RTL, msw), root package.json
v1.9.13 — Per-commit changelog format (July 2026)¶
v1.9.13 — Enhancement (July 22, 2026)
- The in-app Changelog renders
"format": "commits"entries — an icon-and-colour header per commit, an sha/author trailer, a scope badge and an Upcoming/Released status badge — alongside the existing"sections"shape, adopting the same convention as the RONL Business API and the CPSV Editor. Legacy entries render exactly as before. - A repo-local release command is now tracked with the repository, tailored to this real npm-workspaces monorepo: frontend/backend scope, no endpoint-map reconciliation step (the route registry is already a self-maintaining single source of truth), and a note that
packages/ropa-sitehas nopackage.jsonand is never version-bumped even though it deploys via its own path filter.
Files: packages/frontend/src/components/Changelog.tsx
v1.9.12 — Query library resolves CPRMV 0.4.1 datasets (July 2026)¶
v1.9.12 — Patch (July 9, 2026)
- Rules with Their Services, Count Rules per Service and Services with All Their Rules (Detailed) joined
?rule cpsv:implements ?servicedirectly. Since the CPSV-AP RuleShape change, acpsv:Rule'scpsv:implementspoints at aneli:LegalResource— the resource the service declares viacv:hasLegalResource— so datasets published in the newer shape returned no rules at all. The queries now UNION over both link paths. - NL-SBB Concepts and Services broke at the variable-to-DMN hop: a concept's
dct:subjectpoints at a bare DMN variable URI (<dmnUri>/input/N) that newer exports emit without acpsv:isRequiredBy/cpsv:producesedge, so the concept could not reach its DMN or service. The query keeps the explicit edge for older data and, when absent, derives the DMN URI from the variable URI. - Auto-generated DMN decision rules (placeholder "Decision rule <id>" titles) are filtered out of rule listings, and
SELECT DISTINCTde-duplicates. For the Flevoland Thuisbatterij dataset this surfaces its 3 business rules — previously hidden entirely — and 21 concepts, while dropping roughly 60 placeholder rows across the catalogue.
Files: packages/frontend/src/utils (sample query library)
v1.9.11 — Board-owner deploy fix & Operaton error surfacing (July 2026)¶
v1.9.11 — Patch (July 2, 2026)
- Fixed
boardOwnerinjection breaking BPMN deploys whose<bpmn:process>carries a<bpmn:documentation>child.injectBoardOwnernow skips past any leading<documentation>element(s) before inserting or locatingextensionElements, preserving valid BPMN schema order (documentation must precede extensionElements). - Deploy failures now surface Operaton's real error: the deployment service captures and logs the response body (
operatonResponse/operatonStatus) instead of only the generic Axios message, andgetErrorDetails()now checksisAxiosErrorbefore the genericErrorbranch — previously dead code, sinceAxiosError extends Erroralways matched the generic branch first and silently discardedresponse.data.
Files: packages/backend/src/services (deployment / BPMN board-owner injection)
v1.9.10 — /v1/norms CPRMV version selector (June 2026)¶
v1.9.10 — Feature (June 30, 2026)
- A new
?cprmv_version=query parameter on/v1/normsselects which CPRMV vocabulary version to query and emit — one of0.3.0,0.3.2, or0.4.1(else400 INVALID_PARAM), defaulting to0.3.0. All three carry flatcprmv:Ruleresources with identical predicates, so the rules query is one shape with the namespace swapped;0.3.0/0.3.2bindcprmv:to thecprmv.open-regelsversioned-path IRI,0.4.1to thestandaarden.open-regels0.4.1#IRI. - Per-ruleset metadata (
dataset_versions) differs by version:0.3.xreadscprmv:Dataset(dct:issued+dcat:version);0.4.1has nocprmv:Datasetand readscprmv:RuleSet(cprmv:validFrom, which doubles as the ETag /Last-Modifiedfreshness signal since0.4.1has nodct:issued). - This is the LDE consumer side of the CPSV editor's CPRMV version selector. Full detail: Backend —
/v1/normsand the API Stability Contract (0.3.2/0.4.1are experimental,0.3.0is the stable default).
Files: packages/backend/src/services/sparql.service.ts, packages/backend/src/routes (/v1/norms)
v1.9.9 — Deploy-time board ownership & RIP leadRole (June 2026)¶
v1.9.9 — Feature (June 22, 2026)
- Process board ownership. The Deploy modal now requires a board owner: a new "Board ownership" section auto-detects the board from the process's candidate groups (infra/rip → Infra-board, caseworker/hr → Caseworker) and lets you override it. Deployed BPMN is stamped with a process-level
camunda:property boardOwner(explicit choice or auto-derived).boardOwneris persisted on theprocess_definitionsrecord (newboard_ownercolumn) and exposed via/bundles/public, so downstream consumers (ronl-business-api Procesbibliotheek and archive split) can read it. - RIP Phase 1 leadRole. The Map-role outputs script now sets a
leadRoleprocess variable, derived from the intakeprojectType(contractbeheer →manager-pb, otherwiseprojectleider). Distinct from the taskcandidateGroups:leadRolenames who owns the project in the portfolio, not who can claim its tasks.
Files: packages/frontend/src/components (Deploy modal), packages/backend/src/services (deployment, process_definitions, /bundles/public)
v1.9.8 — CPRMV SHACL: ParameterWaarde & TemporalRule shapes (June 2026)¶
v1.9.8 — Feature (June 17, 2026)
- Added
cprmv:ParameterWaardeShapetargetingcprmv:ParameterWaarde:skos:notation[1,1]xsd:stringandskos:prefLabel[1,n]rdf:langStringare mandatory;schema:value[0,1]xsd:decimal,schema:unitCode[0,1]xsd:string,dct:description[0,1]rdf:langString, andcprmv:validFrom/validUntil[0,1]xsd:dateare optional. - Added
cprmv:TemporalRuleShapetargetingcprmv:TemporalRule:cprmv:validFrom[0,1]xsd:date,cprmv:validUntil[0,1]xsd:date,cprmv:confidenceLevel[0,1]xsd:string, andcprmv:isBasedOn[0,n]sh:class cpsv:Rule— all optional. - Also adds the
skos:,schema:, anddct:@prefixdeclarations the new shapes require; no changes to existing shapes. (The CPSV editor enforces theParameterWaardeShapeclient-side as of its v1.10.4.)
Files: CPRMV SHACL shapes (cprmv custom layer)
v1.9.7 — SHACL display fixes (June 2026)¶
v1.9.7 — Patch (June 15, 2026)
- SHACL Validator: long issue messages and focus-node locations were truncated with a single-line CSS ellipsis and could not be read. They now wrap in full (
break-words/break-all) and expose the complete text on hover (title tooltip). - SHACL backend: removed the 60-character cap on the offending values reported for cardinality (
maxCount/uniqueLang) violations, so the full value appears in the message.
Files: packages/frontend/src/components/ShaclValidator.tsx, packages/backend/src/services/shacl-validation.service.ts
v1.9.6 — CPRMV 0.4.1 DMN discovery + chain fixes (June 2026)¶
v1.9.6 — Patch (June 13, 2026)
- DMNs published under the new CPRMV 0.4.1 namespace (e.g.
vast_bedrag_op_vestiging) were missing from/v1/dmnsand the ChainBuilder DMN picker —getAllDmnsand the chain-link queries only matchedcprmv:DecisionModelunder the old 0.3.0 namespace. Both namespaces are now matched side by side until existing 0.3.0 data is migrated. - Fixed "Fill with test data" in the ChainBuilder input form not visibly filling Integer/Double fields when the RDF-sourced test value is
0(the input rendered empty because0 || '' === '').
Files: packages/backend/src/services/sparql.service.ts, packages/frontend/src/components (ChainBuilder)
v1.9.5 — DSO deploy-ready DMN + SHACL CPRMV layer (June 2026)¶
v1.9.5 — Minor (June 11, 2026)
- DSO Integration: extracted DSO DMNs now carry
camunda:historyTimeToLive, so they deploy to Operaton exactly as handed off — the consumer no longer has to patch the DMN first. LDE now produces a fully deploy-ready and evaluatable DMN (DMN 1.3, input ids, FEEL-safe variable names, outputtypeRefs, history TTL). Forms imported from DSO show a green DSO badge in the Form Editor list instead of the generic yellow WIP badge. - SHACL Validator: a third CPRMV 0.4.1 shape layer now validates uploaded Turtle alongside CPSV-AP 3.2.0 and RONL Custom; the results panel renders it automatically. Added valid/invalid test fixtures for every layer (CPRMV, CPSV-AP, RONL) plus a malformed-Turtle case.
Files: packages/backend/src/services/shacl-validation.service.ts, packages/backend/shapes/cprmv/**, packages/backend/src/services/dso.service.ts, packages/frontend/src/components/DsoExplorer.tsx, packages/frontend/src/components/FormEditor.tsx
v1.9.4 — DSO Phase 2d + DMN publish handoff (June 2026)¶
v1.9.4 — Minor (June 10, 2026)
- Activities tab name search: fixing a location (Lelystad / Flevoland) loads that authority's full activity set in one call and reveals a search box that live-filters by name.
- ↓ Import into LDE (Indieningsvereisten) saves the generated form-js scaffold straight into the Form Editor as a draft, named after the activity and tagged with the readable authority name (falling back to the RTR code).
- Publish via CPSV Editor (Conclusie) opens the CPSV Editor with a deep-link to publish the extracted DMN to TriplyDB, where the LDE DMN picker can consume it — no local DMN store needed.
- Extracted DMNs are normalized to deploy and evaluate on Operaton (DMN 1.2 → 1.3, missing input ids added, FEEL-safe variable names, explicit output
typeRef). Verified end-to-end: the normalizedHoutopstandVellendecision deploys (all 7 decisions) and the root decision evaluates without the previous FEEL error.
Files: packages/backend/src/routes/dso.routes.ts, packages/backend/src/services/dso.service.ts, packages/frontend/src/components/DsoExplorer.tsx
v1.9.3 — DSO Phase 2a + 4 (June 2026)¶
v1.9.3 — Minor (June 9, 2026)
- Activity Detail panel now shows an Applicable Rules section listing toepasbare regels fetched live from the DSO Uitvoeren Gegevens API, grouped by rule type (Conclusie / Indieningsvereisten) with validity date and STTR version.
- ↓ STTR downloads the raw STTR XML for any rule type; ↓ Extract DMN (Conclusie) extracts the embedded DMN decision table as a standalone
.dmn; ↓ Form scaffold (Indieningsvereisten) generates a form-js JSON scaffold from the STTR questionnaire (boolean → checkbox, list → select, number → number field, attachment → labelled textfield). - Added
ronl:dsoActiviteitUrnonTreeFellingPermitSubProcesslinking it tonl.imow-gm0995.activiteit.HoutopstandVellen(Gemeente Lelystad).
Files: packages/backend/src/routes/dso.routes.ts (/toepasbare-regels, /toepasbare-regels/:id/sttr, /toepasbare-regels/:id/dmn, /toepasbare-regels/:id/form-scaffold), packages/backend/src/services/dso.service.ts, packages/frontend/src/components/DsoExplorer.tsx
v1.9.2 — BPMN shell/subprocess auto-linking (June 2026)¶
v1.9.2 — Patch (June 9, 2026)
- Uploaded BPMN processes that form a shell/subprocess pair are now automatically linked: a process with call-activity elements is classified as a shell, and any process whose BPMN process ID is targeted by a shell's call-activity becomes its subprocess. The relationship is detected both on fresh imports and retroactively on startup, so previously uploaded standalone processes are reclassified without a re-upload.
- Removed two unused TypeScript imports (
DsoWerkzaamheid,zoekActiviteiten) inDsoExplorer.
Files: packages/frontend/src/components/BpmnModeler (process classification), packages/frontend/src/components/DsoExplorer.tsx
v1.9.0–v1.9.1 — SHACL Validator (June 2026)¶
v1.9.0 — Minor (June 4, 2026) · v1.9.1 — Patch (June 5, 2026)
A new SHACL Validator view validates CPSV-AP Turtle against the canonical CPSV-AP 3.2.0 shapes and RONL-authored shapes before publishing to TriplyDB.
- New backend endpoints
POST /v1/shacl/validate(file-local) andPOST /v1/shacl/validate-merged(unions the file with the already-published graph via a read-only SPARQLCONSTRUCTbefore validating). - Two result layers: CPSV-AP 3.2.0 (the SEMIC shapes vendored verbatim — 32 shapes) and RONL Custom (at most one
foaf:homepage/dct:identifier/cv:spatialper organisation; onedct:title/dct:descriptionper language on a rule). - v1.9.1 vendored the canonical CPSV-AP file and collapsed the earlier Core/Vocabularies split into a single CPSV-AP layer, added the Not loaded vs OK distinction, capped offending values at 60 characters, and added a conformant example plus deterministic merge-simulated test coverage.
Files: packages/backend/src/services/shacl-validation.service.ts, packages/backend/src/routes/shacl.routes.ts, packages/backend/src/types/shacl-rdf.d.ts, packages/backend/shapes/**, packages/frontend/src/components/ShaclValidator.tsx
v1.8.2 — DMN XML download (May 2026)¶
v1.8.2 — Patch (May 20, 2026)
- New endpoint
GET /v1/dmns/:identifier/xmlstreams the deployed DMN XML from Operaton as<identifier>.dmnwith the correctContent-TypeandContent-Dispositionheaders. - DMN list and detail responses now include an
xmlUrlfield pointing to the download endpoint, making it self-discoverable. - Backward compatible: the legacy
GET /api/dmns/:definitionKey/xmlroute remains available.
Files: packages/backend/src/routes/dmn.routes.ts, packages/frontend/src/types (DmnModel)
v1.8.1 — DMN validator: INT-007 false positives eliminated (May 2026)¶
v1.8.1 — Patch (May 19, 2026)
The Interaction Rules layer no longer flags valid intra-DRD references, and now parses FEEL expressions instead of matching the whole <inputExpression> text.
requiredDecision targets resolved¶
An <inputExpression> may legitimately reference a value produced by another decision wired in via <informationRequirement><requiredDecision> — that name is the producing decision's <variable name> or <decisionTable> <output name>, never an <inputData>. INT-007 now resolves requiredDecision targets and treats their output variables as satisfied, mirroring the requiredInput → inputData resolution INT-001 already performs.
FEEL expressions parsed, not whole-text matched¶
Previously the entire <inputExpression><text> was treated as one variable name, so date and time(aanvraagDatum) demanded an <inputData name="date and time(aanvraagDatum)"> and any operator expression false-fired. A new shared extractFeelIdentifiers() helper strips string literals, unwraps built-in calls (date(...), date and time(...), number(...), string(...), not(...), …), drops FEEL keywords/operators and qualified-name segments after a dot, and checks each referenced identifier individually.
Decision outputs excluded from the input-contract check¶
Output variable names (decision <variable> and decision-table <output name>) are, by construction, never external inputs and are no longer subject to the must-have-matching-inputData requirement. Genuine gaps still raise INT-007 — now naming the specific identifier rather than the raw expression string.
No API change¶
POST /v1/dmns/validate is unchanged; only the interaction-layer issue set for affected files differs (fewer false-positive warnings). Verified against real DMNs that deploy and evaluate on Operaton: RONL_Heusden_Heusdenpas.dmn, RONL_SVB_Leeftijden.dmn, EmployeeRoleAssignment.dmn, tree-felling-decision.dmn, and replacement-tree-decision.dmn all validate clean; backend tsc --noEmit passes.
Files: packages/backend/src/services/dmn-validation.service.ts
v1.8.0 — Concurrent applicable periods per ruleset (May 2026)¶
v1.8.0 — Minor (May 15, 2026)
dataset_versions becomes a list per rulesetid¶
A single BWB ruleset can have multiple cprmv:Dataset records — different applicable periods of the same law (e.g. the 2025-01-01 and 2026-01-01 editions of the Participatiewet) are concurrent and equally authoritative, not competing versions. The v1.7.0 design treated them as competing and used FILTER NOT EXISTS to surface only the latest, hiding any earlier applicable period. v1.8.0 surfaces them all.
dataset_versions[<rulesetid>]is now a list of{ version, published_at, title }records, not a single record. Replaces the v1.7.x object shape.- List is pre-sorted:
versiondescending with nulls at the end, ties broken bypublished_atdescending. Element[0]is the most-recent applicable version of that ruleset. - SPARQL query simplified: dropped the
FILTER NOT EXISTSsubpattern and now fetches allcprmv:Datasetrecords. Grouping and sort live in the service layer.
"dataset_versions": {
"BWBR0015703": [
{
"version": "2026-01-01",
"published_at": "2026-05-15T06:57:21Z",
"title": "Participatiewet"
},
{
"version": "2025-01-01",
"published_at": "2026-05-15T07:45:36Z",
"title": "Participatiewet"
}
]
}
Cache headers preserved across the shape change¶
ETagnow hashes every(version, published_at)pair indataset_versions— a new applicable period being added to any ruleset changes the ETagLast-Modifiedismax(published_at)across all records in the response (not just the first per ruleset)Cache-Controlsemantics unchanged: full headers when every rulesetid has metadata;no-cacheotherwise
Breaking change¶
Replaces v1.7.1 (which was never used by external G2G consumers — the API stability contract hadn't been promised yet at that release). The shape change from object to list is detected at integration time, not silent runtime breakage. Anyone consuming v1.7.0/v1.7.1 needs to wrap dataset_versions[<id>] access in array indexing or iteration.
Documentation¶
- API stability contract updated with the list-shape, the multi-applicable-period rationale, and how consumers can match a rule's
applicable_dateto a specific Dataset record - Quick-reference table gains two new rows (multi-record explanation and rule→Dataset lookup recipe)
Files: packages/backend/src/utils/etag.ts, packages/backend/src/services/norms.service.ts, packages/backend/src/routes/norms.routes.ts, docs/iou-architectuur/linked-data-explorer/architecture/backend.md, docs/iou-architectuur/linked-data-explorer/reference/api-stability.md
v1.7.0 — Per-rulesetid dataset versioning & HTTP cache headers (May 2026)¶
v1.7.0 — Minor (May 14, 2026)
Per-rulesetid dataset versioning on /v1/norms¶
Each BWB ruleset (BWBR0002471, BWBR0004044, …) is now published as a distinct cprmv:Dataset resource in TriplyDB, each on its own publication cadence. The response envelope carries a dataset_versions map keyed by cprmv:rulesetId so G2G consumers can see exactly which version of each ruleset they're reading.
"dataset_versions": {
"BWBR0002471": { "version": "2025.1.0", "published_at": "2025-01-15T00:00:00Z" },
"BWBR0015703": { "version": "2026.1.0", "published_at": "2026-01-15T00:00:00Z" }
}
- New envelope field
dataset_versions— only contains entries for rulesetids that have acprmv:Datasetrecord; rulesetids without one are silently absent (transitional state during rollout) - New envelope field
cprmv_version— backend constant extracted from the CPRMV namespace URI, describes which vocabulary the backend speaks independently of which data is published - Backend picks the latest
cprmv:Datasetper rulesetid via aFILTER NOT EXISTSSPARQL subpattern — historical versions remain queryable in TriplyDB but consumers see only the latest - Versions follow CalVer per ruleset:
<year>.<cycle>.<patch>(e.g.2026.1.0for the first publication of 2026,2026.1.1for a correction) - Internal 60-second cache on the dataset metadata SPARQL query keeps the metadata lookup off the hot path
HTTP cache headers for G2G consumers¶
When every rulesetid in the response has dataset metadata, the response carries strong cache headers:
ETagis an opaque 8-hex hash over the sorteddataset_versionsmap plus all filter parameters that affect the response shapeLast-Modifiedismax(published_at)across the response's datasets in RFC 7231 format —If-Modified-Sincereturns304only when nothing in the consumer's query has been republished- For single-rulesetid queries (
?rulesetid=<id>), the304 Not Modifiedshort-circuit happens before the expensive rules SPARQL query — only the cheap cached metadata lookup runs - Safe-by-default partial-coverage policy: if any rulesetid in the response lacks dataset metadata, all three cache headers degrade to
Cache-Control: no-cacheso consumers can't be misled into serving stale data for an unversioned ruleset
API stability contract published¶
New IOU documentation page at /linked-data-explorer/reference/api-stability — the binding contract for G2G consumers covering:
- The four versioning layers (API contract, dataset versions, CPRMV vocabulary, backend service)
- The immutable primary-key promise:
(rulesetid, applicable_date, rulesetid_index)is the eternal PK; consumers can cache permanently and never invalidate - The
rule_id_path_keyfield as the logical identifier for querying "the current value of this rule" across its lifetime - Per-rulesetid publication detection mechanics and partial-coverage behaviour
- Breaking-change criteria warranting
/v2/normsand the 24-month deprecation policy
Files: packages/backend/src/utils/etag.ts (new), packages/backend/src/services/norms.service.ts, packages/backend/src/routes/norms.routes.ts, docs/iou-architectuur/linked-data-explorer/architecture/backend.md, docs/iou-architectuur/linked-data-explorer/reference/api-stability.md (new)
v1.6.3 — Stable keys and per-ruleset aggregation (May 2026)¶
v1.6.3 — Patch (May 14, 2026)
Stable keys and version indices on /v1/norms¶
- New
rule_id_path_keyfield:rule_id_pathwith the date and index segments removed, e.g."BWBR0002471_2025-01-01_0, Artikel 2, lid 6"→"BWBR0002471, Artikel 2, lid 6"; stable across versions of the same ruleset, suitable as a deduplication key when aggregating norms acrossapplicable_datevalues - New
rulesetid_indexfield: the integer index segment after the date inrule_id_path(e.g. the_0inBWBR..._2025-01-01_0); distinguishes multiple versions published on the same date - Both new fields, together with
applicable_date, are derived from a single regex pass overrule_id_path; all three emit JSONnullwhen the path does not match the canonical<rulesetid>_<YYYY-MM-DD>_<index>[, <rest>]shape - Key insertion order extended:
rulesetid,applicable_date,rulesetid_index,rule_id_path,rule_id_path_key— identifier metadata grouped first, then the path and its derived stable key together
Per-rulesetid aggregation¶
- Response envelope now carries an
aggregations.norms_per_rulesetidmap alongsiderules, listing the count of top-level rules percprmv:rulesetIdin the filtered result set - Counts are keyed by the authoritative
cprmv:rulesetIdvalue (not parsed from the path), so non-conformingrule_id_pathvalues still aggregate correctly - Sum of values equals
data.total, letting clients render ruleset-level summaries without a second pass overrules - Additive change — existing readers of
data.rulesanddata.totalsee no breaking change
Files: packages/backend/src/services/norms.service.ts, packages/backend/src/routes/norms.routes.ts, docs/iou-architectuur/linked-data-explorer/architecture/backend.md
v1.6.2 — Shared route registry & content-negotiated root page (May 2026)¶
v1.6.2 — Patch (May 13, 2026)
Single source of truth for v1 routes¶
- New backend module
packages/backend/src/routes/registry.ts: every v1 route's mount path, router, summary, and category lives in one array. Adding a route is a one-line entry that both registration and the root page pick up automatically. - Refactored
packages/backend/src/routes/index.tsto iterate the registry for v1 mounting; legacy/api/*deprecation aliases stay hand-mounted (deprecated routes are intentionally excluded from the registry to steer consumers towards/v1/*) - Mount-order semantics preserved: more specific paths still precede their parents (
/v1/chains/templatesbefore/v1/chains) so Express route precedence behaves exactly as before
Content-negotiated root page¶
GET /now serves HTML whenAcceptincludestext/html(browsers) and JSON otherwise (curl, fetch with defaultAccept, programmatic pollers); both views are derived from the shared route registry so they cannot drift- HTML view groups endpoints by category (Health & monitoring, Discovery, Execution, Assets, Integrations) and badges public-CORS endpoints; styling is inline with no external dependencies
- JSON payload preserves backwards compatibility: same top-level keys as before (
name,version,environment,status,documentation,health,endpoints,legacy); existing programmatic clients see no breaking change - Closes the drift gap that was missing
/v1/dso,/v1/norms,/v1/assets/*,/v1/cache,/v1/edocs,/v1/ropa,/v1/processand/v1/chains/templatesfrom the previous hand-coded listing
Deployment tier display label¶
- New
DEPLOYMENT_ENVenvironment variable distinguishes ACC from PROD whenNODE_ENVis'production'for both; falls back toNODE_ENVwhen unset so local development needs no change - Added
config.displayEnv(string) andconfig.deploymentEnv(raw value) topackages/backend/src/utils/config.tswith mappings:prod/production→PROD,acc/acceptance/staging→ACC,dev/development/local→development,test→test, unknown values pass through - Used consistently by the HTML root page and the
/v1/healthresponse so the displayed environment stays in sync across surfaces - Configuration is per Azure App Service:
az webapp config appsettings set ... --settings DEPLOYMENT_ENV=acc(orprod)
Files: packages/backend/src/routes/registry.ts (new), packages/backend/src/utils/rootView.ts (new), packages/backend/src/routes/index.ts, packages/backend/src/index.ts, packages/backend/src/utils/config.ts, packages/backend/.env.example, packages/backend/src/routes/health.routes.ts
v1.6.1 — Norms publish endpoint (May 2026)¶
v1.6.1 — Patch (May 12, 2026)
/v1/norms — new¶
New backend route GET /v1/norms exposing all cprmv:Rule paths and norms from TriplyDB in the publish format consumed by the SPARQL editor's norm publisher. The response mirrors the cprmv-example.json shape exactly: fully-qualified RDF/CPRMV keys for type, id, definition, and contains; short keys for situatie, norm, per, rulesetid, applicable_date, and rule_id_path.
- Parent rules and their
cprmv:containschildren are aggregated into a single nested object per parent; key insertion order is preserved across runs (matching the example file) - Optional
?endpoint=query parameter overrides the default TriplyDB endpoint, matching the pattern already used by/v1/dmns - Response wrapped in the standard
ApiResponseenvelope withdata.rules(array) anddata.total(filtered count)
Filtering by ruleset identifier and applicable date¶
- New
applicable_dateattribute derived from the_YYYY-MM-DD_segment embedded inrule_id_path(e.g."BWBR0015703_2026-01-01_0, Artikel 20, ..."yields"2026-01-01");nullwhen the path carries no parseable date - Optional
?rulesetid=filter (exact-match oncprmv:rulesetId, e.g.BWBR0015703); validated against/^[A-Za-z0-9_-]+$/ - Optional
?applicable_date=filter (matches paths containing_<date>_); validated against/^\d{4}-\d{2}-\d{2}$/ - Filters can be combined; invalid values return
400 INVALID_PARAMbefore any SPARQL fires - Validated filter values are applied as SPARQL
FILTERclauses server-side (exact-match on?rulesetId,CONTAINSon?ruleIdPath); regex validation is the injection-prevention contract
Maintenance¶
tsconfig.jsoncleanup: removed"ignoreDeprecations": "6.0"which only became valid in TypeScript 6.0 and broke CI on TypeScript 5.x. Editor-side deprecation warnings are addressed via local.vscode/settings.jsonpointing at the workspace TypeScript instead. Full migration tomoduleResolution: "nodenext"deferred until ESM-on-Node maturity warrants the per-file.jsextension changes.
Files: packages/backend/src/services/norms.service.ts (new), packages/backend/src/routes/norms.routes.ts (new), packages/backend/src/routes/index.ts, packages/backend/tsconfig.json
v1.6.0 — Multilingualism & pending-until-Save editing (April 2026)¶
v1.6.0 — New Feature (April 27, 2026)
Multilingualism — language and organization metadata¶
BPMN processes, Camunda forms, and document templates now carry an optional ISO 639-1 language code (en, nl, de) and an open-ended organization key. The model is sibling-artefact i18n: each artefact exists once per language, with the LDE deploy modal warning on mixed-language bundles. DMNs stay language-agnostic — variable keys remain stable English so a single DMN serves both English and Dutch sibling BPMNs.
- Database —
language VARCHAR(2)andorganization VARCHAR(100)columns added toprocess_definitions,form_schemas, anddocument_templateswith partial indexes; nullable, existing rows coexist asNULL - BPMN moddle descriptor —
LanguageMixinandOrganizationMixinextendingbpmn:ProcessinronlModdleDescriptor.json; survivesaveXMLround-trip via the existingronlnamespace registration - List panels — new
ArtefactListToolbar(search + language filter + match counter) shared by Process, Form, and Document lists; collapsible organization groups; subprocesses follow their shell's organization regardless of their own tag - Editor footer panel — uniform pattern across all three editors:
LanguageSelectorandOrganizationSelector(with autocomplete from existing organization keys); BPMN footer additionally retains RoPA and DSO selectors - Filename-based language inference on import —
.bpmn,.form,.documentfiles with a<id>.<lang>.<ext>suffix are auto-tagged on import; precedence: in-file value → filename → untagged - Form export —
Export .formnow wraps the form-js schema with top-levellanguageandorganizationkeys and uses a language-suffixed filename; round-trip integrity for all three artefact types - Deploy-time language consistency check — amber warning surfaces inline in the deploy modal when a bundle mixes languages, listing the offending codes; mirrors the existing RoPA-missing warning UX
Files: packages/backend/src/db/migrate.ts, packages/backend/src/db/types.ts, packages/backend/src/db/mappers.ts, packages/backend/src/domain/types.ts, packages/backend/src/services/assets.service.ts, packages/frontend/src/types/index.ts, packages/frontend/src/types/document.types.ts, packages/frontend/src/components/BpmnModeler/ronlModdleDescriptor.json, packages/frontend/src/components/common/LanguageSelector.tsx, packages/frontend/src/components/common/OrganizationSelector.tsx, packages/frontend/src/components/common/ArtefactListToolbar.tsx, packages/frontend/src/components/BpmnModeler/BpmnModeler.tsx, packages/frontend/src/components/BpmnModeler/BpmnCanvas.tsx, packages/frontend/src/components/BpmnModeler/ProcessList.tsx, packages/frontend/src/components/FormEditor/FormEditor.tsx, packages/frontend/src/components/FormEditor/FormCanvas.tsx, packages/frontend/src/components/FormEditor/FormList.tsx, packages/frontend/src/components/DocumentComposer/DocumentComposer.tsx, packages/frontend/src/components/DocumentComposer/DocumentList.tsx, packages/frontend/src/services/bpmnService.ts, packages/frontend/src/services/formService.ts
Pending-until-Save editing model¶
Footer edits across BPMN, Form, and Document editors no longer persist immediately. They accumulate in a draft state on the editor parent and flush atomically when Save is clicked. Navigation guards confirm before discarding unsaved changes.
- Editor architecture — footer state lifted to the editor parent (
BpmnModeler,FormEditor,DocumentComposer); children receive effective values via props and report changes via callbacks; Save is the single point of persistence - Shell → subprocess atomic save — saving a BPMN shell propagates
languageandorganizationto all linked subprocesses in one write; idempotent (skips subprocesses already aligned); shell wins unconditionally; example subprocesses (readonly: true) are skipped - Save button dirty tracking fixed — Form Save button was always-enabled (regression); now starts disabled, enables on first edit, disables after Save
- Document load window —
DocumentComposerno longer flipshasChangesspuriously on document load; TipTap's mount-timeonUpdateevents are suppressed for the macrotask following load viaisLoadingRef
HR-capacity Dutch reference bundle¶
The first multi-language reference bundle: 1 BPMN, 8 forms, 2 documents under examples/organizations/flevoland/HR-capacity/nl/, all tagged language=nl, organization=flevoland. Same CapacityClaimRouting DMN serves both the English and Dutch siblings.
Bug fixes¶
- BPMN persistence:
languageandorganizationnow included in theBpmnService.saveProcessPOST payload (previously dropped silently, causing values to vanish on hydration) OrganizationSelectoris now controlled (value/onChange) rather than uncontrolled (defaultValue/onBlur); switching artefacts refreshes the input correctlyBpmnCanvasno longer resets on parent re-renders —handleElementSelectstabilised via a ref, modeler-init effect deps narrowed toxml
Known limitation¶
The form-js properties panel loses input focus when typing pauses (Field label, Description, Key). Upstream form-js issue #86, marked wontfix by bpmn-io. Not LDE-caused, not fixable from React without forking form-js. Workaround: edit .form JSON in a code editor and re-import — filename-based language inference handles the language tag automatically.
v1.5.3 — DSO Works tab & OIN preset fix (April 2026)¶
v1.5.3 — Patch (April 2026)
Works tab — new¶
New Works tab in the DSO Explorer (between Concepts and Activities) backed by the Zoekinterface API.
- Search werkzaamheden by user intent (e.g. "boom kappen") with autocomplete suggestions after 2 characters; selecting a suggestion immediately fires the search
- Each result shows the human-readable
omschrijving, thefunctioneleStructuurRef(full concept URI — the Phase 4 pivot to STTR files), and the short werkzaamheid URN - Selecting a result opens a detail panel showing the current version's
omschrijving, validity period, and full version history with start/end dates and a "current" badge - Autocomplete uses
POST /werkzaamheden/_suggereerwith 300ms debounce - Reloads on DSO environment switch with the same clean-slate behaviour as the other tabs
Activities tab — OIN preset fix¶
The Lelystad and Flevoland presets now use POST /activiteiten/_zoek with bestuursorgaan.oin filter instead of _wijzigingen — returning activities valid on a given date rather than a delta sync of changed activities.
- Activities now appear in the list with their full
omschrijvingvisible immediately, without requiring parallel detail fetches - Date field defaults to today for OIN presets (yesterday offset removed — no longer needed with
_zoek) - Pagination works correctly in OIN mode — Load button reloads the authority list for the selected date
- Empty state distinguishes between no activities found in general vs no activities found for the selected authority on the selected date
Bug fixes¶
- Activity Detail panel always using pre-production DSO when opened from the Activities list —
envwas missing from thegetActiviteitDetailcall, causing 404 for production-only activities - Activity Detail panel not re-fetching when DSO environment is switched while a URN is already selected —
envadded to theuseEffectdependency array - Activity Detail panel showing stale date context in OIN preset mode —
activeDatumnow set correctly byloadByOinalongside the result
Backend — DSO service¶
zoekinterfaceBaseUrlandopvragenWerkzaamhedenBaseUrladded to bothdsoanddsoProdconfig blocks — defaults baked in, no new env vars requiredPOST /v1/dso/werkzaamheden/zoek— proxies Zoekinterface/werkzaamheden/_zoekPOST /v1/dso/werkzaamheden/suggereer— proxies Zoekinterface/werkzaamheden/_suggereerGET /v1/dso/werkzaamheden/:urn— proxies Opvragen Werkzaamheden/werkzaamheden/{urn}(version history, without expand —_expandScopeenum value not yet resolved)POST /v1/dso/activiteiten/oinnow uses/activiteiten/_zoekwithbestuursorgaan.oinbody field anddatuminstead of_wijzigingenwithdatumVanaf
v1.5.2 — DSO Explorer enhancements (April 2026)¶
v1.5.2 — Patch (April 2026)
- DSO environment selector added to the Settings panel (gear icon): switch between pre-production and production DSO independently of the LDE environment, persisted across sessions in localStorage
- Environment badge in the DSO Explorer header updates to reflect the active DSO environment (amber for pre-production, green for production)
- Both DSO environments use separate API keys configured via
DSO_API_KEYandDSO_API_KEY_PRODenvironment variables - Location presets (Lelystad, Flevoland) initially filter by authority OIN — replaced with
_zoekin v1.5.3 - Child activities in the detail panel now show human-readable names fetched in parallel after the parent detail loads
- Graceful 404 handling in the detail panel: activities not available in the active DSO environment show a clear message instead of a raw error
v1.5.1 — Dev infrastructure (April 2026)¶
v1.5.1 — Infrastructure (April 2026)
Docker readiness check¶
New packages/backend/scripts/check-docker.sh verifies the ronl-postgres container is up and healthy before nodemon starts the backend.
- Coloured terminal output: green for ready, yellow for unhealthy, red for missing or stopped
- Suggests the exact
docker startordocker runcommand if the container is missing - Backend
devscript now runs the check first;dev:fullanddev:backendscripts added to rootpackage.jsonfor one-command monorepo startup
Files: packages/backend/scripts/check-docker.sh, packages/backend/package.json, package.json
v1.5.0 — DSO Integration Phase 1 (March 2026)¶
v1.5.0 — New Feature (March 2026)
DSO Explorer¶
New top-level view for browsing the Digitaal Stelsel Omgevingswet from inside LDE.
- Concepts tab — full-text search across the Stelselcatalogus
- Activities tab — RTR
activiteitenlist with date filtering and a detail panel showingbestuursorgaan, validity, parent and child activities, and which rule types (Conclusie,Indieningsvereisten,Maatregelen) are present
BPMN — DSO activiteit linkage¶
- New
ronl:dsoActiviteitUrnmixin onbpmn:ProcessinronlModdleDescriptor.json - New
DsoActiviteitSelectorcomponent pinned to the BPMN Modeler footer (sibling ofRopaSelector) - Live URN verification against DSO RTR; on success shows the omschrijving, authority block, and a direct link to the public RTR viewer
- URN survives
saveXMLround-trips and shows up in process exports
Backend — DSO service¶
- New
src/services/dso.service.tscovering Stelselcatalogus and RTR endpoints - New
src/routes/dso.routes.tsat/v1/dso/... - Pre-production and production base URLs configurable via env; API keys per environment
- Path-aware DSO environment selection via
X-Dso-Envheader
Files: packages/frontend/src/components/BpmnModeler/DsoActiviteitSelector.tsx, packages/frontend/src/services/dsoService.ts, packages/backend/src/services/dso.service.ts, packages/backend/src/routes/dso.routes.ts
v1.4.0 — RoPA Records & GDPR Article 30 Compliance (March 2026)¶
v1.4.0 — New Feature (March 28, 2026)
RoPA Records — PostgreSQL¶
Two new tables appended to the migrate.ts DDL block. Migrations run automatically at backend startup — no manual schema step required.
ropa_records— one row per process (shell or subprocess); keyed onbpmn_process_idwith a unique index so re-running the seed updates rows in place rather than inserting duplicates;statuscolumn (draft/active/archived) controls public visibilityropa_personal_data_fields— one row per personal data field collected by the linked forms;ON DELETE CASCADEfromropa_records;special_categoryboolean flags Art. 9/10 GDPR fields- New
src/types/ropa.types.ts—RopaRecord,RopaPersonalDataField,PublicRopaRecord - New
src/services/ropa.service.ts—listRopa,getRopaById,getRopaByBpmnProcessId,upsertRopa(transactional: record header + field rows in oneBEGIN/COMMIT),deleteRopa,listPublicRopa - New
src/db/seed-ropa.ts— idempotent seed for four active records coveringAwbShellProcess,TreeFellingPermitSubProcess,AwbZorgtoeslagProcess, andZorgtoeslagProvisionalSubProcess
Files: src/db/migrate.ts, src/types/ropa.types.ts, src/services/ropa.service.ts, src/db/seed-ropa.ts
RoPA Records — API routes¶
- New
src/routes/ropa.routes.ts— authenticated asset routes at/v1/assets/ropa:GET(list),POST(upsert, returns{ id }),DELETE /:id,GET /by-bpmn-id/:bpmnProcessId - New
src/routes/ropa.public.routes.ts— CORS-open public route at/v1/ropa/public;?organisation=query parameter filters bycontroller_name ILIKE '%…%'; stripscontrollerContact,dpoContact, andschemaVersionbefore returning; onlystatus = 'active'records returned - Global CORS middleware in
src/index.tspatched with path-aware logic:/v1/ropa/publicbypasses the origin whitelist entirely (origin: '*'); all other routes remain subject toCORS_ORIGINenv var
Files: src/routes/ropa.routes.ts, src/routes/ropa.public.routes.ts, src/routes/index.ts, src/index.ts
RoPA Editor — LDE UI¶
New RoPA Records view in the LDE sidebar (ScrollText icon). ViewMode.ROPA added to the enum in types/index.ts.
RopaEditor.tsx— root orchestrator; list state, load/save/delete; passesrecord={null}for new records,key={activeId}onRopaRecordEditorfor clean remount on selection changeRopaList.tsx— left panel; shell records first, subprocess records second; DRAFT / ACTIVE / ARCHIVED badges; delete control per recordRopaRecordEditor.tsx— four-tab editor:- Record — all GDPR Art. 30 mandatory fields; Lookup from knowledge graph button fires a SPARQL query against the TriplyDB RONL endpoint via
POST /v1/triplydb/queryand returns a pick-list ofeli:LegalResourceentries - Personal Data Fields — Hydrate from forms reads
camunda:formRefvalues from the process XML, loads matching form schemas fromFormService, and appends one row per component with akeyproperty; each row is classified with a data category and Art. 9/10 flag - BPMN Link — reads
ronl:ropaReffrom the matching process XML viaBpmnService; Write ronl:ropaRef to BPMN injects the attribute and persists viaBpmnService.saveProcess; status indicator: not linked / linked (green) / points to different record (amber) - Status — three lifecycle buttons with confirmation dialog before activation
- New
src/services/ropaService.ts— fetch-based API client for all five backend operations - New
src/types/ropa.types.ts(frontend mirror)
Files: src/components/RopaEditor/RopaEditor.tsx, RopaList.tsx, RopaRecordEditor.tsx, src/services/ropaService.ts, src/types/ropa.types.ts, src/types/index.ts, src/App.tsx
RoPA Selector — BPMN Modeler¶
- New
src/components/BpmnModeler/RopaSelector.tsx— renders as a fixed footer panel pinned below the scrollable process list inProcessList.tsx; only shown whenactiveProcessis non-null; reads the currentronl:ropaReffrom the process XML by regex; writes viahandleRopaRefChangeinBpmnModeler.tsx ProcessList.tsx— two new props:activeProcess: BpmnProcess | nullandonRopaRefChange: (ropaRef: string | undefined) => void;RopaSelectorrendered outside theoverflow-y-autoscroll container so it stays pinned regardless of list lengthBpmnModeler.tsx— newhandleRopaRefChange: ensuresxmlns:ronldeclaration on<definitions>, then sets, updates, or removesronl:ropaRefon the<bpmn:process>opening tag; persists viaBpmnService.saveProcessronlModdleDescriptor.json— second type entry added:RopaRefMixinextendsbpmn:ProcesswithropaRefas anisAttr: trueString property; without this registration the attribute is silently dropped by bpmn-js onsaveXML()- Deploy modal —
ropaRefMissingflag set whenronl:ropaRefis absent from the process XML; amber non-blocking warning rendered between the resource list and the resource count line
Files: src/components/BpmnModeler/RopaSelector.tsx, ProcessList.tsx, BpmnModeler.tsx, BpmnCanvas.tsx, ronlModdleDescriptor.json
RoPA Public Site — MVP¶
New package packages/ropa-site/ — a zero-dependency static HTML/CSS/JS site with no build step.
index.html— fetchesGET /v1/ropa/public, renders collapsible cards per record with full GDPR Art. 30 field display, personal data fields table with colour-coded data categories and Art. 9/10 red badges, Provincie Flevoland dark green house style; shells rendered before subprocessesstaticwebapp.config.json— Azure Static Web Apps navigation fallback and no-cache headers- Deployed as a separate Azure Static Web Apps resource (
ropa-flevoland-acc) independent of the LDE frontend; GitHub Actions workflow scoped topackages/ropa-site/**path filter
Files: packages/ropa-site/index.html, packages/ropa-site/staticwebapp.config.json, packages/ropa-site/README.md, .github/workflows/azure-static-web-apps-ropa-flevoland-acc.yml
v1.3.0 — PostgreSQL Asset Storage & AWB Process Hierarchy (March 2026)¶
v1.3.0 — Infrastructure (March 25, 2026)
PostgreSQL asset storage¶
BPMN processes, form schemas, and document templates now persist to PostgreSQL via the LDE backend rather than living exclusively in browser localStorage.
- New
src/db/pool.ts—pg.Poolinitialised fromDATABASE_URL; null-guarded so the backend starts without a database configured - New
src/db/migrate.ts— idempotent DDL (CREATE TABLE IF NOT EXISTS) forprocess_definitions,form_schemas, anddocument_templates; called fromstartServer()beforeapp.listen() - New
src/services/assets.service.ts—listBpmn,upsertBpmn,deleteBpmn,getBpmnByBpmnProcessId,listForms,upsertForm,deleteForm,listDocuments,upsertDocument,deleteDocument - New
src/routes/assets.routes.ts—GET/POST/DELETEfor each asset type;GET /v1/assets/bpmn/by-bpmn-id/:bpmnProcessIdfor subprocess bundle resolution; all routes return503 DB_NOT_CONFIGUREDwhen pool is null - Registered at
/v1/assetsinsrc/routes/index.ts packages/backend/package.json—pg: ^8added todependencies,@types/pg: ^8todevDependencies
Write-through cache strategy: saves update localStorage immediately and fire a background POST to the backend. Reads remain synchronous from localStorage — zero-latency UI at all times.
Hydration on mount: each editor runs hydrateFromServer() on mount — GET /v1/assets/{type} merges server records with local read-only examples and updates the localStorage cache. Falls back to local cache silently if the backend is unreachable.
Files: src/db/pool.ts, src/db/migrate.ts, src/services/assets.service.ts, src/routes/assets.routes.ts, src/routes/index.ts, src/index.ts, packages/backend/package.json, src/services/bpmnService.ts, src/services/formService.ts, src/services/documentService.ts
AWB shell / subprocess hierarchy¶
The BpmnProcess type and process library now model the two-layer AWB shell pattern explicitly.
BpmnProcessinterface extended withbpmnProcessId?: string,processRole?: 'shell' | 'subprocess' | 'standalone', andcalledElement?: stringbpmnProcessIdis extracted from<process id="...">in the XML automatically on save and import viaextractBpmnProcessId()- All five seeded example processes carry explicit role and relationship metadata
ProcessList.tsxrenders a hierarchical grouped view: shell entries are top-level, their subprocesses are indented with a tree connector; standalone and unclassified processes appear as top-level entriesSHELL(violet) andSUB(teal) role badges added to process cardsprocess_definitionstable storesprocess_role,called_element, andbpmn_process_idas indexed columns
Files: src/types/index.ts, src/components/BpmnModeler/BpmnModeler.tsx, src/components/BpmnModeler/ProcessList.tsx
Schema versioning¶
schema_version INTEGER NOT NULL DEFAULT 1 column added to all three PostgreSQL tables, enabling server-side re-seeding of example assets across all users and devices when source files change.
Zorgtoeslag example processes¶
Three new versioned example processes added: AwbZorgtoeslagProcess (shell), ZorgtoeslagProvisionalSubProcess, and ZorgtoeslagFinalSubProcess (both subprocesses). Four new example forms: zorgtoeslag-provisional-start, zorgtoeslag-provisional-review, zorgtoeslag-final-review, zorgtoeslag-notify-applicant.
Files: src/utils/exampleVersions.ts, public/examples/toeslagen/
Azure deployment — troubleshooting notes¶
During ACC deployment the following issues were encountered and resolved:
permission denied for schema public— Azure PostgreSQL Flexible Server (PostgreSQL 15+) revokesCREATEon the public schema from non-superusers by default. Fixed by runningGRANT ALL ON SCHEMA public TO lde_userplusALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO lde_useras admin.- CI/CD health check failing (
503) — caused by the above permission error crashingmigrate()beforeapp.listen()was reached. The App Service showed "Application Error" page with no log output viaaz webapp log tail; logs were only retrievable viaaz webapp log download.
See PostgreSQL Deployment for the full provisioning guide including these fixes.
v1.2.0 — RIP Phase 1 Bundle & eDOCS Integration (March 2026)¶
v1.2.0 — New Feature (March 14, 2026)
RIP Phase 1 Bundle¶
New deployment bundle for the Regular Infrastructure Projects (RIP) Phase 1 workflow — Provincie Flevoland.
- 20-step BPMN process (
RipPhase1Process.bpmn) covering project definition and preliminary design preparation: intake form, intake meeting, intake report, PSU organisation, PSU execution, PSU report, risk file preparation, preliminary design principles, and two approval gateways with rejection loops RipProjectTypeAssignment.dmn— assignscandidateGroupsandassignedRolesfromprojectTypeanddepartment; all rules resolve toinfra-projectteam/infra-medewerker; designed for granular RBAC extension without BPMN changes- 7 forms:
rip-intake,rip-intake-meeting,rip-intake-report,rip-psu-organize,rip-psu-execution,rip-risk-file,rip-approval(reusable at both approval gateways) - 3 document templates:
rip-intake-report.document(column 2),rip-psu-report.document(column 3),rip-pdp.document(column 4) - Bundle deployed to
examples/organizations/flevoland/rip-phase1/
Files: examples/organizations/flevoland/rip-phase1/
eDOCS Integration¶
New backend service and external task worker for OpenText eDOCS document management.
EdocsServicewraps the eDOCS REST API:connect(session token caching with auto re-authentication on 401/403),ensureWorkspace,uploadDocument,getWorkspaceDocuments,healthCheckExternalTaskWorkerpolls Operaton viafetchAndLock(long-polling, 20s timeout) for two topics:rip-edocs-workspace(create/retrieve project workspace, writeedocsWorkspaceId) andrip-edocs-document(render and upload document, write named output variable)- Stub mode (
EDOCS_STUB_MODE=true, default) — all methods return realistic fake responses; full process runs end-to-end without a live eDOCS server; no code changes needed when switching to live - Worker started in
app.listen()callback; stopped cleanly onSIGTERM/SIGINT - 4 new REST endpoints:
GET /v1/edocs/status,POST /v1/edocs/workspaces/ensure,POST /v1/edocs/documents,GET /v1/edocs/workspaces/:id/documents - New environment variables:
EDOCS_BASE_URL,EDOCS_LIBRARY,EDOCS_USER_ID,EDOCS_PASSWORD,EDOCS_STUB_MODE
Files: packages/backend/src/services/edocs.service.ts, packages/backend/src/services/externalTaskWorker.service.ts, packages/backend/src/routes/edocs.routes.ts
DMN Validator — Interaction Rules¶
- INT-005 scoped to DRDs only — no longer fires on standalone single-decision DMNs;
<inputData>elements on standalone models serve as input contract declarations for CPSV publishing and do not require<informationRequirement>wiring - INT-007 (new) — warns when an
<inputExpression>references a variable name with no matching top-level<inputData>declaration; without this, the CPSV Editor generates an empty request body on deploy
File: packages/backend/src/services/dmn-validation.service.ts
v1.1.2 — Bug Fix (March 11, 2026)
Form Editor¶
- Save now correctly persists the current schema.
saveSchema()in form-js 1.20.x returns the schema object directly — not wrapped in{ schema }. Destructuring assumption causedundefinedto be written tolocalStorage, making the active form disappear on the next render. - Export .form fixed for the same reason.
- Vite
dedupeconfig added forpreact/preact/hooks/preact/compatto prevent duplicate Preact instances after npm version round-trips involving@bpmn-io/*packages; fixesTypeError: Cannot read properties of undefined (reading 'context')on the form canvas.
Known issue: Typing in a properties panel field (label, key, etc.) loses focus after the first character. Upstream form-js 1.20.x issue — Preact re-renders the properties panel internally on every change event. Will be resolved when an upstream fix is available.
File: packages/frontend/vite.config.ts, packages/frontend/src/components/FormEditor/FormCanvas.tsx
v1.1.1 — Enhancement (March 10, 2026)
Import from file¶
- BPMN Modeler — import
.bpmnfiles via the Upload button in the process list header; process name derived from thenameattribute on the<process>element, falling back to filename - Form Editor — import
.formfiles; name derived from the schemaidfield, falling back to filename - Document Composer — import
.documentfiles; receives a freshidand timestamps on import to avoid collisions with existing templates - All imported items open immediately in their respective editor and are persisted to
localStorage
Files: BpmnModeler/BpmnModeler.tsx, FormEditor/FormEditor.tsx, DocumentComposer/DocumentComposer.tsx
v1.1.0 — Document Composer (March 2026)¶
v1.1.0 — New Feature (March 8, 2026)
Document Composer¶
New Document Composer view for authoring formal government decision document templates (beschikkingen).
- Three-panel layout matching BPMN Modeler and Chain Builder conventions: document list (left), zone canvas (centre), Bindings panel (right)
- Fixed-zone document structure: Letterhead, Contact Information, Reference, Body, Closing, Sign-off, and optional Annex
- Five draggable block types: rich text (TipTap with bold, italic, headings, lists), variable placeholder, image (from TriplyDB), separator, horizontal rule, and spacer
- Blocks dragged from the Content library onto zones; reordering within and across zones by drag
- Image library tab fetches assets from the active TriplyDB dataset
- Documents stored in
localStorageunderlinkedDataExplorer_documentTemplates; create, rename, delete, and Save as… actions - Export document template as a
.documentJSON file - Read-only example document pre-loaded: Kapvergunning Beschikking (linked to
AwbShellProcess)
Files: DocumentComposer.tsx, DocumentCanvas.tsx, DocumentList.tsx, ZonePanel.tsx, TextBlockEditor.tsx, ImageBlock.tsx, VariableBlock.tsx, BindingPanel.tsx, document.types.ts, documentService.ts
Variable Bindings¶
- Bindings panel maps
{{placeholder}}tokens in rich-text blocks to Operaton process variable keys - Discover Variables button queries
GET /v1/process/:key/variable-hintsfor all variables used by completed instances of a given process definition key - Discovered variables shown as clickable chips labelled with type (
String,Boolean,Double, etc.) - Each binding records placeholder, variable key, source (
processordmn_output), and optional label
File: BindingPanel.tsx
BPMN Modeler integration¶
- Link decision template dropdown injected into the bpmn-js properties panel for
UserTaskelements (notStartEvent) - Selecting a template writes
camunda:documentRefto the BPMN XML - Purple badge (📄) rendered on the canvas below the element, below the existing green form badge
- Badge positioned at
bottom: -36(vs.bottom: -22for the form badge) so both badges are visible simultaneously DocumentTemplateSelector.tsxfollows the identical injection pattern asFormTemplateSelector.tsx
Files: BpmnModeler/DocumentTemplateSelector.tsx, BpmnCanvas.tsx
v1.0.1 — Bug Fix & Internal (March 2026)¶
v1.0.1 — Bug Fix (March 7, 2026)
Bug fix¶
Fixed Task_Phase6_Notify and Task_RequestMissingInfo appearing pre-claimed in the caseworker dashboard. camunda:assignee="demo" removed; camunda:candidateGroups="caseworker" added to both tasks so they are correctly visible in the task queue.
Internal — example file migration and version registry¶
- Example
.bpmnand.formfiles moved topublic/examples/flevoland/as the single source of truth. Inline schemas removed frombpmnTemplates.tsandFormEditor.tsx. - Added
exampleVersions.tswithEXAMPLE_VERSIONSrecord (keyed by example name, value is an integer version). The app compares stored versions inlocalStoragekeylinkedDataExplorer_exampleVersionsagainstEXAMPLE_VERSIONSand re-fetches any example whose version has been incremented. - Developer workflow: edit the file in
public/examples/, mirror the change toexamples/organizations/, increment the version inexampleVersions.ts, commit. Existing users receive the updated example without clearinglocalStorage.
Files: exampleVersions.ts, bpmnTemplates.ts, FormEditor.tsx, public/examples/flevoland/
v1.0.0 — Form Editor & One-Click Deploy (March 2026)¶
v1.0.0 — Major Release
Form Editor¶
New Form Editor view powered by @bpmn-io/form-js (schemaVersion 16, MIT licensed). Forms are authored as JSON schema, stored in localStorage, and available immediately to the BPMN Modeler.
- Two-panel layout: form list (left) and
@bpmn-io/form-jseditor canvas (right) - Create, rename, and delete WIP forms; three seed EXAMPLE forms are read-only
- Three built-in examples:
kapvergunning-start(citizen-facing),tree-felling-review(caseworker review),awb-notify-applicant(caseworker notification) - Export individual forms as
.formJSON files compatible with Camunda Modeler and Operaton FormServicelocalStorage CRUD shared with the BPMN Modeler — no sync step required
Files: FormEditor.tsx, FormCanvas.tsx, FormList.tsx, formService.ts
BPMN Modeler — Form integration¶
- Link to Form dropdown in the properties panel for
UserTaskandStartEventelements - Writes
camunda:formRefandcamunda:formRefBinding="latest"to the BPMN XML camunda:formRefBinding="latest"means Operaton always resolves the most recent deployment of that form ID — no version pinning needed- Green badge overlay on
UserTaskandStartEventelements when a form is linked DmnTemplateSelectorpre-selection bug fixed — dropdown now correctly reflects an existingcamunda:decisionRefwhen opening properties for an already-linked element
Files: BpmnCanvas.tsx, FormTemplateSelector.tsx
BPMN Modeler — One-click deploy¶
- Deploy button opens a modal listing all resources to be bundled: main BPMN, subprocess BPMNs (resolved via
calledElementattributes), and all.formfiles referenced bycamunda:formRef - All resources deployed in a single multipart
POST /api/dmns/process/deployto Operaton —camunda:formRefresolves at runtime because BPMN and forms share the same deployment ID - Configurable Operaton endpoint field pre-filled from
VITE_OPERATON_BASE_URL - Optional HTTP Basic Auth credentials per deployment
- Unmatched form references (in BPMN but not in localStorage) shown in modal before deploying
- Deploy button disabled after a successful deployment to prevent accidental re-deploy
Files: BpmnCanvas.tsx (frontend), dmn.routes.ts + operaton.service.ts (backend)
v0.9.x — DMN Syntactic Validation (February 2026)¶
v0.9.1 — Date Input Validation Fix
Fixed a false "Missing 1 required input(s)" error in the Chain Composer when a DMN contains an optional Date input whose test value is intentionally null (e.g. overlijdensdatum in zorgtoeslag_resultaat).
The root cause was a two-part gap between how RDF stores test data and how the validator tracks input state. In TriplyDB, a null value cannot be represented as a schema:value triple, so optional date variables have no testValue property at all on the DmnVariable object returned by the backend. The Fill with test data button in InputForm.tsx only wrote a key into the inputs state object when testValue was defined — silently skipping null-default dates. The validator in ChainBuilder.tsx then checked input.identifier in inputs, found the key absent, and pushed the variable into missingInputs.
Two fixes were applied:
InputForm.tsx— the Fill button now explicitly setsDateinputs tonullwhentestValueisundefined, ensuring the key is always registered in state after filling.ChainBuilder.tsx— the validator now exemptsDateinputs from the missing-input check when no value is present, consistent with the existing exemption forBooleaninputs (which default tofalsewithout user action). An unset date is a valid input state, not an authoring error.
v0.9.0 — DMN Validator
Added DMN Validator feature. The DMN Validator lets you validate one or more DMN files against the RONL DMN+ syntactic layers. It is accessible from the shield icon (🛡) in the sidebar. You can drop any number of .dmn or .xml files onto the validator at once, or add files incrementally — the drop zone remains visible at the top of the panel whenever files are loaded. Files are validated independently and displayed side-by-side for easy comparison.
The validator runs on the shared backend at POST /v1/dmns/validate and is used both by this Linked Data Explorer's standalone DMN Validator view and by the CPSV Editor's inline validation in the DMN tab.
v0.8.x — Governance & Vendor Integration (February 2026)¶
v0.8.4 — Vendor Services
Added vendor service discovery: ronl:VendorService resources are queried alongside DMN metadata, surfaced as blue count badges on DMN cards, and displayed in a detail modal with full provider information.
v0.8.3 — DMN Governance Badges
Three-state validation badge system using RONL Ontology v1.0 properties (ronl:validationStatus, ronl:validatedBy, ronl:validatedAt). Badges visible in both the DMN list and the Chain Composer. Organisation names resolved via skos:prefLabel.
v0.8.1 — BPMN DRD/DMN Selector
DmnTemplateSelector now loads both locally-saved DRD templates and regular DMNs from the backend, displayed in grouped options. Purple info card for DRDs shows chain composition. Auto-populates camunda:decisionRef with prefixed DRD entry-point identifier.
v0.7.x — BPMN Modeler & DRD Templates (February 2026)¶
v0.7.3 — DRD Template Linking (partial)
DMN template dropdown in BPMN properties panel implemented. Exact identifier auto-population working; variable compatibility validation planned for a future release.
v0.7.2 — DRD Template System
Users can save DRD-compatible chains as named templates stored in localStorage. Templates are endpoint-scoped. DRD templates load via the new "My Templates" panel.
v0.7.1 — Semantic Variable Matching Fix
Fixed findEnhancedChainLinks SPARQL query to correctly detect both exact and semantic matches. Heusdenpas chain now shows all 10 variable relationships across 3 DMNs.
v0.7.0 — BPMN Modeler Foundation
Full BPMN 2.0 editor using bpmn-js v18.12.0 with official Camunda/Operaton properties panel (bpmn-js-properties-panel). Three-panel layout: process list, canvas, properties. Tree Felling Permit example auto-loaded on first visit. localStorage persistence and .bpmn export.
v0.6.x — DRD Generation & Enhanced Validation (February 2026)¶
v0.6.2 — Semantic Analysis Tab
Semantic Analysis tab added to Chain Builder. Displays cross-agency variable equivalences and chain suggestions. Backend endpoints: /api/dmns/semantic-equivalences, /api/dmns/enhanced-chain-links, /api/dmns/cycles.
v0.6.1 — DRD Generation
Save DRD-compatible chains as single executable DRD files deployed to Operaton. Automatic <informationRequirement> wiring, entry-point detection, and deployment ID tracking.
v0.6.0 — Enhanced Validation
Validation engine distinguishes DRD-compatible chains (all exact matches) from sequential chains (semantic matches present). Clear UI states: green (DRD), amber (sequential), red (invalid). Separate save paths.
v0.5.x — Multi-Endpoint & Test Data (January 2026)¶
v0.5.5 — SPARQL & Export Improvements
Added "Service Rules Metadata" query (cprmv:Rule → eli:LegalResource → cpsv:PublicService). CSV export with timestamped filenames and proper escaping. RDF URI collision fix for cprmv:Rule instances.
v0.5.4 — Multi-Endpoint Chain Execution
Chain execution now correctly uses the selected endpoint throughout the full execution flow. Automatic test data population from schema:value in TriplyDB TTL files. Fallback to testData.json for legacy DMNs.
v0.5.3 — Dynamic Endpoint Selection
Switch between TriplyDB datasets in real time without page reload. Backend caches DMN metadata per endpoint (5-minute TTL). Connection indicator shows direct vs proxied connection status.
v0.4.x — API Versioning & Export (January 2026)¶
v0.4.0 — Backend API v1
Migrated all endpoints to /v1/* following Dutch Government API Design Rules. Legacy /api/* endpoints retained with Deprecation headers. API-Version header in all responses. Chain export as JSON or BPMN 2.0 diagram.
v0.3.x — Chain Builder UI (January 2026)¶
v0.3.1 — Bug Fixes
Enhanced error messages for Operaton failures. Synchronized test data between preset and manual chain. Fixed execution progress visibility on first run.
v0.3.0 — Chain Builder UI
Visual drag-and-drop chain builder. Real-time validation with input requirements. Dynamic form generation for DMN inputs. Chain execution with step-by-step progress tracking. In-app tutorial (accessible via ? icon). Deployment metadata display.
v0.2.0 — DMN Discovery & Orchestration View (January 2026)¶
SPARQL-based DMN discovery using CPRMV vocabulary. Three-panel orchestration view: DMN list, chain composer placeholder, details panel. Real-time search and filter. Input/output variable inspection. Automatic chain detection by variable matching. SPARQL result parsing for multiple query response formats.
v0.1.0 — Initial Release (January 2026)¶
React-based SPARQL visualisation and query tool. Interactive D3.js force-directed graph. Multiple endpoint support. Query editor with sample library and CORS proxy fallback. SELECT query results table. TypeScript interfaces and Vite build tooling.
Notable backend bug fixes¶
These fixes are documented here because they involve non-obvious root causes that are likely to recur.
TriplyDB health check returning HTTP 400¶
Root cause: The health check was calling axios.get(triplydbEndpoint) without a query parameter. SPARQL endpoints reject bare GET requests — they require either a POST with a query body or a GET with a ?query= parameter.
Fix: Updated health.routes.ts to call sparqlService.healthCheck(), which executes a minimal SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 1 query.
File: packages/backend/src/routes/health.routes.ts
/v1/* endpoints returning 404 after Azure deployment¶
Root cause: The GitHub Actions deployment step used cp -r dist/* deploy/, which flattened the compiled output. The package.json start script references dist/index.js, and health.routes.js inside dist/routes/ uses require('../../package.json') — both paths broke when the dist/ folder was removed.
Fix: Changed the deployment step to cp -r dist deploy/, preserving the directory structure.
Files: .github/workflows/azure-backend-acc.yml, .github/workflows/azure-backend-production.yml
Root endpoint referencing deprecated /api/* paths¶
Root cause: The GET / root response had not been updated when API versioning was introduced, so it still advertised /api/health and /api/ as the documentation and health URLs.
Fix: Updated index.ts to reference /v1/* endpoints in the root response and added a legacy block explicitly marking the old paths as deprecated.
File: packages/backend/src/index.ts
Roadmap¶
Frontend — Phase 2¶
The following items are planned but not yet scheduled. Phase 1 features (v0.1–v0.8) are complete.
Database migration
Move chain templates and BPMN processes from localStorage to a server-side database. Enables user authentication and ownership, process versioning with history, and public sharing with access control. PostgreSQL is the planned backend, consistent with the RONL Business API stack.
Collaborative editing
Multiple users editing shared process definitions. Real-time or optimistic-update model TBD.
Advanced BPMN properties panel
Full editing of all BPMN element properties: form fields, execution listeners, input/output mappings, conditional expressions, timers. Currently only name and DMN reference are editable.
DRD export and versioning
Export generated DRD XML for versioning and sharing. Track DRD version history alongside template evolution.
Certification registry
A queryable view of all ronl:VendorService resources with ronl:certificationStatus "certified", enabling cross-service comparison of certified vendor implementations.
Multi-hop semantic chains
The current semantic validation checks only adjacent DMN pairs. Phase 2 will extend this to multi-hop: DMN1 → DMN2 → DMN3 where the connection between DMN1 and DMN3 is bridged semantically through DMN2.
Semantic concept browser
UI to explore the skos:exactMatch network: graph view of all concepts and their relationships, filterable by DMN or variable type, with search by concept URI.
Backend API — versioning roadmap¶
Shipped in v2026.09.5
An OpenAPI 3.1 description served at /v1/openapi.json, with every route response validated against it in the test suite and the document linted against the NL API Design Rules 2.2.1 in CI. Errors as RFC 9457 problem details.
Shipped in v2026.09.6
A shared TTL cache utility registered by name, so GET /v1/cache/stats and DELETE /v1/cache/clear report and clear across every cache rather than a hardcoded list. Its first members are the DSO activity-detail read (dso-activiteit, five minutes) and the DMN caches. This is a service-level cache, keyed on the upstream call each service makes — not the response layer below.
Still planned
Rate limiting. A per-endpoint response caching layer — the named registry above caches what services fetch, not what routes return, so an unchanged /v1 response is still recomputed and re-serialised on every request.
v1.0.0 (planned)
Full Dutch Government API Design Rules compliance (API-02, API-10) — API-16 and API-51 are met as of v2026.09.5, with the remaining departures recorded as exceptions in the OpenAPI document. Production-grade monitoring and alerting. Performance target: <800ms for any chain execution.
v2.0.0 (future)
Remove all legacy /api/* endpoints. Evaluate Dutch naming for business resources (/v2/besluitmodellen etc.) per API-04. Enhanced orchestration: parallel chain execution where dependency graph allows. Batch execution support for multiple input sets.