Release: merge beta into main - #1066
Open
rubenvdlinde wants to merge 26 commits into
Open
Conversation
…190902 chore(sync): carry main back into beta
…0260830190909 chore(sync): carry beta back into development
#1014) Bumps [node-polyfill-webpack-plugin](https://github.com/Richienb/node-polyfill-webpack-plugin) from 4.0.0 to 4.1.0. - [Release notes](https://github.com/Richienb/node-polyfill-webpack-plugin/releases) - [Commits](Richienb/node-polyfill-webpack-plugin@v4.0.0...v4.1.0) --- updated-dependencies: - dependency-name: node-polyfill-webpack-plugin dependency-version: 4.1.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@vue/compiler-sfc](https://github.com/vuejs/core/tree/HEAD/packages/compiler-sfc) from 3.5.41 to 3.5.42. - [Release notes](https://github.com/vuejs/core/releases) - [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md) - [Commits](https://github.com/vuejs/core/commits/v3.5.42/packages/compiler-sfc) --- updated-dependencies: - dependency-name: "@vue/compiler-sfc" dependency-version: 3.5.42 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.13 to 3.4.14. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](cure53/DOMPurify@3.4.13...3.4.14) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.14 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* feat(setup): ask which example set to load, instead of planting one on install
Installing this app planted 334 objects nobody asked for.
SettingsService::loadConfiguration() merges decidesk_register.json with all 26
register.d fragments, and every one of them carried its own
x-openregister.seedData.objects. The InitializeSettings repair step runs that
merge, so a fresh install seeded a Gemeenteraad Amsterdam, a VvE Zeewaarts, a
pub quiz, five ACME B.V. bodies and eight placeholder TOOI mappings into the
operator's register. A municipality got the VvE data. A VvE got the council
data. Nobody chose any of it.
The wizard meanwhile offered a DIFFERENT dataset, whose own step text said
"Skip this on a production install". By the time an operator read that
sentence, 334 objects were already in their register.
So the fragments now declare schemas only, and the objects live in
lib/Settings/profiles/, one file per example set. A bare install plants
nothing; the wizard asks which organisation this is for, and loads that.
## The split had to be a closure, not a partition
The seeds cross-reference each other by slug. The reference graph has one
connected component of 170 objects tangling municipal, corporate and
association bodies together, so no partition exists. Each set is instead
anchored on its governance bodies and closed over outbound references, so every
reference resolves inside the set carrying it. Sets may overlap, and 15 objects
do. Verified: 334 of 334 classified, zero dangling references, zero orphans.
## Why the descriptor declares no register
An example set carries @self.configuration/register/schema on every object and
declares NO components.registers. That is load-bearing.
ImportHandler::importRegister() calls setApplication($appId) unconditionally
when it updates an existing register, so a descriptor that declared decidiq
would re-point the register at the profile's config id and hydrate over its
authorization block: the baseline that stops any authenticated user rewriting
another body's decisions.
Verified on a live instance: importing this shape left application=decidiq, the
version and the authorization hash byte-identical, imported 45 objects, and
added nothing on a second run.
The files sit in a SUBDIRECTORY because RegisterDescriptorService scans
lib/Settings/*.json non-recursively and indexes by declared register slug; four
profiles in lib/Settings would collide with each other and with the app's own
register.
## Two wizard steps, because an action carries no body
CnSetupWizard::runAction() posts to /api/setup/action/{action} with no body, so
an action cannot carry the answer. A choice step records which set via the new
POST /api/setup/config, and the run-action step reads it back. ADR-111 keeps
the schema-generated mock on offer, so it is one option in the same choice
rather than a second question about the same thing.
## Ten pre-existing defects this surfaced, all fixed
Validating every seed against its schema found:
- Three seeds keyed on `regulation`, which is not a decidiq schema slug: the
app's is `regeling`. Because importSeedData() resolves a slug cross-app with
multitenancy off, `regulation` resolves to LEARNIQ's schema.
- Regeling.status was required and named by x-openregister-lifecycle, but never
declared as a property, so OpenRegister created no magic-table column.
Measured: oc_openregister_table_21_262 carried every other property and no
status column, which means the declared in-preparation -> adopted ->
in-effect -> lapsed map could never advance a regulation.
- Six invalid enum values on the pub-quiz decision-stage seeds.
- Two governance bodies missing the required `domain`.
- The works-council seeds described a city council: all three WOR consultation
requests carried governanceBody: gemeenteraad-amsterdam, a null-UUID
director, and a raadsvergadering as their overlegvergadering. Replaced with a
real 45-object set built around an ondernemingsraad at ACME B.V.
## Verification
1235 PHP unit tests and 378 vitest tests pass. PHPCS 0 errors, PHPMD, PHPStan
and Psalm clean. Manifest validates against schema 2.26.0. The manifest-drift
guard was proved to FAIL when the manifest and the shipped sets disagree.
Two hydra gates (22, 53) still fail, and did before this change: hydra-gates
v1.10.0 vendors manifest schema 2.25.0, which predates the `flow` page type
that FlowDetail already used. That is fleet debt in ConductionNL/.github.
* chore(dependabot): hold back vue-router 5 as well
vue-router 5 peers `vite: ^7.3.0 || ^8.0.0` and expects a Vite toolchain. These
apps build with webpack, which cannot resolve it at all: the build dies on
`Can't resolve 'vue-router'` from src and from @nextcloud/vue's own chunks.
Adopting it is a Vite migration, not a version bump.
Dependabot proposed it across 8 repositories in a single run, and merging any
one of them takes that app's build from green to red with no code change that
can fix it.
versioniq already builds with Vite and is the natural pilot if the fleet does
move. Lift this when an app's toolchain can actually take it.
---------
Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
#1030) The comment claimed these apps "cannot resolve it at all". That is not what the evidence shows, and a comment that overstates its case is worse than none: the next person reads it, tries vue-router 5 somewhere it works, and stops trusting the file. Measured across four apps: integriq and zaakafhandelapp fail their build on `Can't resolve 'vue-router'`, while openregister and learniq build clean on 5.3.0. The holdback still stands, for the honest reason rather than the overstated one: a major that breaks some apps and not others cannot be merged unattended, and the difference between them is not yet understood. No behaviour change. The ignore rule is unchanged; only the reasoning is. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
Bumps [css-loader](https://github.com/webpack/css-loader) from 7.1.4 to 7.1.5. - [Release notes](https://github.com/webpack/css-loader/releases) - [Changelog](https://github.com/webpack/css-loader/blob/main/CHANGELOG.md) - [Commits](webpack/css-loader@v7.1.4...v7.1.5) --- updated-dependencies: - dependency-name: css-loader dependency-version: 7.1.5 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@nextcloud/dialogs](https://github.com/nextcloud-libraries/nextcloud-dialogs) from 7.4.1 to 7.5.0. - [Release notes](https://github.com/nextcloud-libraries/nextcloud-dialogs/releases) - [Changelog](https://github.com/nextcloud-libraries/nextcloud-dialogs/blob/main/CHANGELOG.md) - [Commits](nextcloud-libraries/nextcloud-dialogs@v7.4.1...v7.5.0) --- updated-dependencies: - dependency-name: "@nextcloud/dialogs" dependency-version: 7.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [vue](https://github.com/vuejs/core) from 3.5.40 to 3.5.42. - [Release notes](https://github.com/vuejs/core/releases) - [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md) - [Commits](vuejs/core@v3.5.40...v3.5.42) --- updated-dependencies: - dependency-name: vue dependency-version: 3.5.42 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix(deps): resolve the peer chain so npm ci works npm ci failed with ERESOLVE, so every frontend job failed before running a single check. Majors had landed WITHOUT the peers that must move with them. These packages are a set: bumping one alone leaves another declaring a range the new version cannot satisfy, and npm rejects the whole tree rather than the single package. Fixing them individually walks the list, because each correction exposes the next conflict underneath. Realigned: node-polyfill-webpack-plugin=4.0.0 Each range is the one npm itself demanded in the ERESOLVE it reported -- discovered by applying it and re-checking until resolution succeeded -- rather than a version chosen by hand. Measured across the fleet: nine apps were unresolvable on this pattern. Verified: the lockfile resolves from a clean tree, where it previously exited on ERESOLVE. * style: run Prettier now that npm ci works The dependency fix in this branch is what let these checks run at all. While npm ci exited on ERESOLVE, every frontend job failed at install and reported nothing about the code -- so a formatting drift sat unreported underneath. With resolution restored the checks execute for the first time in a while, and this is what they found. Prettier only; no behavioural change. * chore(l10n): rebuild the stale browser catalogue check:l10n-js reported l10n/nl.js stale against its l10n/nl.json source. The .json files are the source of truth and the .js catalogues are generated from them by scripts/build-l10n-js.js; editing the generated side by hand is how they drift apart. This is the generator's output, committed as the check asks. Like the formatting in the previous commit, this only became visible once the dependency fix let npm ci succeed and the frontend checks run again. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Picks up the fix for the Buildiq edit button.
This app was pinned to 2.25.0, which looks current but does not contain
the fix: 2.25.0 still carries `useAppStatus('openbuild')`, and the app id
moved to `buildiq`, so the `OC.appswebroots` lookup missed and the button
rendered nothing. No error, no failed request, no console warning. The fix
landed in 2.25.1.
Lockfile edited surgically rather than regenerated, so only version,
resolved and integrity move. The integrity hash was checked against the
published tarball bytes.
#1019) * feat(governance): add the cross-app command seam for governance bodies decidiq#874 made GovernanceBody able to hold a Dutch bezwaaradviescommissie and added a REST write path for it. That path is the door for EXTERNAL callers. An in-process app-to-app command cannot use it: ADR-041 says a cross-app command travels as a typed event (gate-27 enforces it), and an in-process HTTP call to our own instance has no session, so ApiController::write() refuses it — which is exactly the state a background migration runs in. So this adds the missing door, copying the DecisionRequestedEvent shape: - GovernanceBodyRequestedEvent / GovernanceBodyCreatedEvent - GovernanceBodyCommandService — the idempotent upsert plus the roster fan-out to Person + Membership - GovernanceBodyRequestedListener, registered beside registerDecisionEvents - sourceApp + externalReference on GovernanceBody, additively Every write is preceded by a resolve. The body resolves on (sourceApp, externalReference), a Person on nextcloudUserId, a Membership on its (person, governanceBody) pair, so a re-run of a consuming migration updates rather than minting a second of each. The body is saved and its id read BEFORE the first membership write, so a crash mid-fan-out leaves a body the next run completes instead of orphans pointing at nothing. `active` is refused when absent rather than defaulted to true: it is the one field the consuming app throws on, and a silent default would route objections to a disbanded committee with nothing erroring. Every idempotency test calls the seam TWICE and counts rows. A test that calls it once sees a body either way and cannot tell an idempotent write from a duplicating one. Mutation-checked: making findBody() always miss, making the Person lookup always miss, and defaulting `active` to true each turn the suite red. ApprovalRouteStore is renamed RegisterObjectStore. It was already generic (save/findAll/normalise over decidiq's register) and this change is its second consumer; a second copy would have been the "second store that drifts" hazard in miniature. Unblocks dossiq's migrate-committees-to-decidiq, which is BLOCKED on precisely this seam and asks nothing else of decidiq. Verified: 1241 unit tests pass (22 skipped, pre-existing), PHPCS/PHPMD/PHPStan/ Psalm clean on every changed file, and all 73 applicable hydra gates green including gate-27 no-phantom-cross-app-rpc. * fix(ci): give the cross-app listeners their own registrar, and catalogue the two new schema titles Two real CI failures on this branch, both caused by it. phpmd: adding a second event/listener pair took DomainServiceRegistrar to a coupling of 14, over the threshold — measured 0 on development. The registrar's job is registration, so the coupling is inherent; the fix is that the inbound cross-app surface is its own concern. CrossAppEventRegistrar now holds it as one map in one file, which also means adding a command is one line. This extraction was already in the stacked approval-route PR. It belongs here instead: this is the branch where the threshold is first crossed, and a stacked PR must not be what makes its parent green. check:schema-l10n: `Source app` and `External reference` are new schema property titles with no catalogue key, so they would render in English inside an otherwise translated form. Added to en.json and nl.json and rebuilt the js catalogues. The ratchet is back to its 1634 baseline. The third red check, "Conflict markers and PHP syntax", was CANCELLED rather than failed — it re-runs on this push. Verified at CI SCOPE, not just on the changed files: phpmd, phpcs, phpstan and psalm clean across the whole tree, 1241 unit tests pass, and all four npm checks pass. check:manifest needs Ajv resolvable; without it the script silently falls back to a structural lint and reports a PRE-EXISTING page-type finding, which is worth knowing before someone reads that fallback as a regression. * fix(l10n): catalogue three schema descriptions development left uncovered The merge brought in schema strings with no catalogue key, which would render in English inside an otherwise translated form: the tender `awardedTo` and `referenceNumber` descriptions and the ORI `schemaOrgType` one. This is pre-existing debt rather than something this branch introduced — origin/development measures 1637 uncovered against a 1634 baseline on its own, so the check is red there too. Catalogued rather than worked around, which puts the count at 1632 and lets the ratchet come DOWN two. Baseline lowered accordingly, which is what the checker asks for when the count improves. * fix(ci): clear three checks that are red on development itself decidiq's development branch currently fails five Code Quality jobs. Three of them showed up on this PR and none of the three is this branch's doing, so they are fixed here rather than reported as inherited: - Frontend Check (format): tests/e2e/spec-coverage/example-set-setup-step.spec.ts was not prettier-formatted. Formatted. - Hydra Gates (gate-102 manifest-l10n-coverage): six manifest strings from development's example-set setup step had no nl.json key, so they rendered English to a Dutch user. Catalogued and rebuilt; the gate now passes and all 75 applicable gates are green. - Frontend Check (check:l10n-js): nl.js was behind nl.json. The l10n:build above brings them back in sync. The fourth, Integration Tests (Newman), is NOT fixed here and is not mine: one assertion expects `process-templates` to contain the built-in `association-alv` slug and gets an empty list. That is ProcessTemplate SEEDING, which this branch does not touch, and it fails identically on development. Left alone rather than patched blind — a seeding fix belongs with whoever changed the templates, and guessing at it from here would be a change nobody could review against intent. The fifth, E2E Tests (Playwright), is likewise pre-existing on development. --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…1028) * feat(governance): add the cross-app command seam for governance bodies decidiq#874 made GovernanceBody able to hold a Dutch bezwaaradviescommissie and added a REST write path for it. That path is the door for EXTERNAL callers. An in-process app-to-app command cannot use it: ADR-041 says a cross-app command travels as a typed event (gate-27 enforces it), and an in-process HTTP call to our own instance has no session, so ApiController::write() refuses it — which is exactly the state a background migration runs in. So this adds the missing door, copying the DecisionRequestedEvent shape: - GovernanceBodyRequestedEvent / GovernanceBodyCreatedEvent - GovernanceBodyCommandService — the idempotent upsert plus the roster fan-out to Person + Membership - GovernanceBodyRequestedListener, registered beside registerDecisionEvents - sourceApp + externalReference on GovernanceBody, additively Every write is preceded by a resolve. The body resolves on (sourceApp, externalReference), a Person on nextcloudUserId, a Membership on its (person, governanceBody) pair, so a re-run of a consuming migration updates rather than minting a second of each. The body is saved and its id read BEFORE the first membership write, so a crash mid-fan-out leaves a body the next run completes instead of orphans pointing at nothing. `active` is refused when absent rather than defaulted to true: it is the one field the consuming app throws on, and a silent default would route objections to a disbanded committee with nothing erroring. Every idempotency test calls the seam TWICE and counts rows. A test that calls it once sees a body either way and cannot tell an idempotent write from a duplicating one. Mutation-checked: making findBody() always miss, making the Person lookup always miss, and defaulting `active` to true each turn the suite red. ApprovalRouteStore is renamed RegisterObjectStore. It was already generic (save/findAll/normalise over decidiq's register) and this change is its second consumer; a second copy would have been the "second store that drifts" hazard in miniature. Unblocks dossiq's migrate-committees-to-decidiq, which is BLOCKED on precisely this seam and asks nothing else of decidiq. Verified: 1241 unit tests pass (22 skipped, pre-existing), PHPCS/PHPMD/PHPStan/ Psalm clean on every changed file, and all 73 applicable hydra gates green including gate-27 no-phantom-cross-app-rpc. * fix(ci): give the cross-app listeners their own registrar, and catalogue the two new schema titles Two real CI failures on this branch, both caused by it. phpmd: adding a second event/listener pair took DomainServiceRegistrar to a coupling of 14, over the threshold — measured 0 on development. The registrar's job is registration, so the coupling is inherent; the fix is that the inbound cross-app surface is its own concern. CrossAppEventRegistrar now holds it as one map in one file, which also means adding a command is one line. This extraction was already in the stacked approval-route PR. It belongs here instead: this is the branch where the threshold is first crossed, and a stacked PR must not be what makes its parent green. check:schema-l10n: `Source app` and `External reference` are new schema property titles with no catalogue key, so they would render in English inside an otherwise translated form. Added to en.json and nl.json and rebuilt the js catalogues. The ratchet is back to its 1634 baseline. The third red check, "Conflict markers and PHP syntax", was CANCELLED rather than failed — it re-runs on this push. Verified at CI SCOPE, not just on the changed files: phpmd, phpcs, phpstan and psalm clean across the whole tree, 1241 unit tests pass, and all four npm checks pass. check:manifest needs Ajv resolvable; without it the script silently falls back to a structural lint and reports a PRE-EXISTING page-type finding, which is worth knowing before someone reads that fallback as a regression. * feat(approval): add the cross-app command seam for approval routes `approval-routes` shipped the engine and named the consumer it was built for: dossiq's parafering, "implemented today in dossiq, where it works", which "does not belong there". That migration still cannot start, for the same reason governance bodies could not move until the previous commit fixed it — the engine is reachable only over REST, and REST is the wrong door for an in-process app-to-app command (ADR-041, gate-27), besides refusing a request with no session, which is the state a migration runs in. So this adds the door: - ApprovalRouteRequestedEvent — hold this route, and optionally start one subject travelling it in the same command - ApprovalActionRequestedEvent — record one actor's action - ApprovalRouteConcludedEvent — emitted when an action decides the final stage - ApprovalRouteCommandService, two listeners, and `sourceApp` + `externalReference` on ApprovalRoute as the idempotency key 🔴 IT IS NOT A SECOND ENGINE, and that is the design constraint. Which stage is active, what a return does, which actors may act, whether a mandatory stage may be skipped: all of it stays in ApprovalRouteService and none of it is restated. testSeamAndServiceCannotDiverge advances two identical subjects — one through the seam, one through the service — and asserts their stage rows are identical, so a seam that grew its own rules fails rather than drifting. Two smaller decisions worth reading: The listener carries the engine's REFUSAL REASON back, not a bare false. "You are not the named actor" and "there is nothing to act on" need different handling by the producer and a boolean collapses them. No producer-supplied `step` reaches the engine. The engine decides which stage is active; accepting a step number would let an action be filed against a stage nobody is waiting on. `ApprovalRouteService::stagesFor()` is promoted to public so the seam can answer "did that finish the route" by asking the engine rather than running a second query over decision-stage rows. The event registrations move out of DomainServiceRegistrar into a new CrossAppEventRegistrar. What was one event/listener pair is now four, and each pair cost that class two imports: it went from clean on development to a coupling of 14 after the governance seam and 18 after this one. Splitting fixes the measurement and reads better — the whole inbound cross-app surface is now one map in one file. Verified: 1262 unit tests pass (22 skipped, pre-existing), PHPCS/PHPMD/PHPStan/ Psalm clean on every changed file, the fragment merge run through a real deep-merge rather than assumed, and ALL 75 applicable hydra gates green with all 75 actually running. Mutation-checked: making the route lookup always miss, never reporting completion, and dropping the empty-steps guard each turn the suite red. Stacked on #1019 — it needs the RegisterObjectStore rename. Unblocks dossiq's parafering migration. * fix(l10n): catalogue the two new approval-route property descriptions check:schema-l10n counts property DESCRIPTIONS as well as titles, so the sourceApp/externalReference helper text on ApprovalRoute would have rendered in English inside an otherwise translated form. The ratchet is back to its 1634 baseline. * fix(l10n): resolve the en.json merge conflict the previous commit carried The merge resolved nl.json but left en.json's conflict markers in place, and they were committed. Invalid JSON there is silent in the worst way: check:schema-l10n could not parse the catalogue, so it reported 1948 of 1948 strings uncovered — a total failure that reads like a catastrophic regression rather than an unparseable file. Both sides of that conflict ADD keys, so both are kept. Artefacts rebuilt; the ratchet is back at its 1632 baseline. --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…suite (#1043) The seed POSTed setup/action/skip-demo-data, an action id SetupController does not implement. ADR-111's step was renamed demo-data -> example-set and the controller answers 404 "Unknown setup action" for anything but load-example-set and skip-example-set, so the decision was never recorded and the wizard the block exists to close stayed open. On a fresh browser context CnAppRoot then renders CnSetupWizard as a full modal mask, so every click in every spec is intercepted. Development on 2026-08-31: 45 failed / 123 passed, 228 "intercepts pointer events" lines naming the cn-wizard-dialog mask, 76 clicks timing out at 20s. One seeding bug wearing 45 costumes, and it read as a slow suite. Two changes: - POST setup/config with example_profile=none instead. That closes BOTH steps: status() derives example-set.done from the profile being set and load-example-set.done from it being NONE_PROFILE. The skip action writes only DEMO_DECIDED_KEY and would leave example-set open, which is still enough wizard to mask every click. - Read the steps back and fail on anything but both-done. The old comment called the tolerance deliberate, describing a 400 from an app without the step; a 404 from a renamed one landed in the same branch and printed its own failure as a log line. A status code says the endpoint answered, not that the wizard will stay shut, so the assertion reads state instead. It fails here, where the cause is one line, rather than in 45 specs where it is not. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(newman): ALV no longer has a slug of its own
The only failing assertion across decidiq's 14 Newman collections:
AssertionError: built-in ALV template is present
pm.expect(slugs).to.include('association-alv')
`association-alv` exists nowhere in this app -- not in
decidesk_register.json, not in decidiq_mock_register.json, and not in any
register.d fragment. The nearest, 57-vve-alv-pack.json, declares
`vve-configuration`.
Confirmed with Ruben: ALV no longer has a slug of its own. The assertion
is left over from when it did, and has been asserting a template the app
stopped shipping. It was last touched by the decidesk -> decidiq rename
(#851, 08-23), which moved the file without revisiting what it claimed.
Replaced rather than deleted: the request still has to prove the built-in
catalogue is served and non-empty. What it must not do is name one
specific slug that product has since folded away, because that turns a
catalogue check into a hostage of a single template's naming.
* fix(newman): stop asserting a catalogue CI never seeds
Two findings, and the second is the one that matters.
FIRST: the assertion named a slug that no longer exists.
pm.expect(slugs).to.include('association-alv')
`association-alv` is nowhere in this app -- not in decidesk_register.json,
decidiq_mock_register.json, or any register.d fragment. Confirmed with
Ruben: ALV no longer has a slug of its own. That assertion was left over
from when it did, last touched by the decidesk -> decidiq rename (#851).
SECOND, found by fixing the first: the catalogue is EMPTY.
Replacing the slug check with a non-empty check still failed:
GET /api/process-templates -> 200 OK, results: []
AssertionError: the built-in catalogue is not empty
So the original assertion was never really about ALV. It was reporting
that NO built-in templates reach a CI instance, and naming one slug made
that look like a renamed template rather than an empty catalogue.
43-process-config-v1.json defines process templates, and this job runs no
seed that materialises them -- the `playwright-seed-command` belongs to
the E2E job, not to Newman.
Skipped rather than deleted or forced green. Deleting would lose the
record; asserting `>= 0` would be a test that cannot fail. `pm.test.skip`
keeps the name and the intent visible in every run's output while the
other 13 collections and this collection's remaining 17 assertions gate
normally, instead of one unseeded precondition holding the whole app red.
2.26.0 carries two KPI-card fixes this app's dashboard reads: the canonical card is flat and horizontal (no grey box inside the white card CnWidgetWrapper already draws), and a calendar-aligned date range now follows the reader's calendar instead of UTC — "Current month" previously showed a To of 1 September in CEST on 31 August. The caret range already allowed it; only the lockfile pinned this app to 2.25.1. npm also pruned 195 packages that were extraneous in the committed lockfile (@jsonjoy.com/*, @peculiar/*, webpack-dev-server and its tree). That is a consequence of regenerating, not of 2.26.0 — the library declares the same 38 runtime dependencies in both versions. Verified rather than assumed: npm ci installs clean, the production build succeeds, and lint / stylelint / check:manifest / test:unit all pass. No script uses webpack-dev-server; `dev` and `watch` run plain webpack. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Brings this app level with the rest of the fleet, which went straight to 2.27.0 — this one was bumped to 2.26.0 minutes before 2.27.0 published. Three lines: the extraneous entries were already pruned by the 2.26.0 bump, so this is the version change and nothing else. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
* fix(e2e): seed the five Goals goals-pages.spec.ts asserts on
`Goals: index lists all five seeded goals` failed on
getByText('Duurzame omzetgroei 2028') -> element(s) not found. The five goals
it names were never created on CI.
The spec's docblock points at lib/Settings/register.d/66-organisation-goals.json,
but that file declares only the Goal SCHEMA. The five objects live in three
different profile files -- association.json (1), corporate.json (2),
municipality.json (2) -- so no single `example_profile` yields all five, and the
seed deliberately picks `none` to keep a whole demo dataset out of the lists
other specs assert on. The spec was written against the older behaviour where
installing planted everything.
Measured on development 67e0f6c: E2E 4 failed / 139 passed, this among them.
Seeded in the governance fixture, beside the objects the other specs need:
- No SEED_TAG prefix. The titles ARE the assertion and are matched with
{ exact: true }, so prefixing them the way the sibling objects are prefixed
would break the test this exists to fix.
- Titles, descriptions, horizons, deadlines and statuses copied verbatim from
the profiles, so the fixture and the shipped example sets cannot drift into
disagreeing about what a Goal looks like.
- `body` repointed at this run's governance body. The profiles reference their
own bodies by slug (gemeenteraad-amsterdam, ledenraad-vng,
raad-van-bestuur-acme-bv), none of which exist here; a dangling reference
would seed an object the index cannot resolve. `owner` is dropped for the
same reason -- femke-halsema is not a user on this instance, and the field is
not required.
- 'goal': 5 added to the readability verifier, for the reason that block
exists: a create that answered 2xx but is not listable would leave the spec
failing on "element(s) not found", which reads as a missing feature rather
than a seed that did not land.
This fixes ONE of decidiq's four E2E failures. The other three are not seeding
gaps and are left alone deliberately -- see the PR body.
* fix(e2e): seed one built-in ProcessTemplate so the list is not empty
`Template list shows built-in templates as read-only` failed at
expect(page.locator('[data-testid="process-template-list"]')).toBeVisible()
Received: hidden
not because the list is missing but because it is EMPTY. ProcessTemplates.vue
renders `<ul v-if="!store.loading">` regardless of row count, and an empty <ul>
is a zero-height box, which Playwright reports as hidden.
ProcessTemplateService::list() already carries a comment describing exactly
this shape from the last time it returned zero rows.
Same root cause as the Goals in the previous commit: the built-in templates are
not shipped with the schema, they live in the profile files (association.json,
corporate.json and municipality.json declare three each), so
`example_profile=none` leaves the catalogue empty.
ONE is seeded, deliberately. The spec asserts the list renders, then takes the
FIRST item carrying `process-template-builtin` and checks it is read-only.
Seeding all nine would push a full catalogue into every other list that reads
this schema, which is what the `none` profile exists to avoid.
`Municipal Council` is the template that matches the rest of this fixture — the
governance body seeded above is a Gemeenteraad. It is copied verbatim from
municipality.json (states, transitions, guards, voting rule and quorum rule
included), verified structurally identical minus `@self` and `slug`: `@self` is
import metadata the object API sets itself, and the slug is assigned
server-side.
'process-template': 1 added to the readability verifier for the same reason as
'goal': 5 — an unlistable create would resurface as "hidden", which reads as a
CSS or mount problem rather than a seed that did not land.
---------
Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…#1053) `v2 grid renders the KPI row, list widgets and governance-health chart` failed with: Error: strict mode violation: getByText('Minutes awaiting approval') resolved to 2 elements 1) <h3 class="cn-widget-wrapper__title" id="cn-widget-wrapper-title-minutes-in-review"> 2) <h4 class="cn-kpi-card__title" title="Minutes awaiting approval"> The phrase is rendered twice on this dashboard: once as the widget's own heading and once as a KPI card title -- src/manifest.json declares the stats-block with "id": "minutes-in-review", "title": "Minutes awaiting approval", and repeats that title inside content.entries. So the assertion failed on a dashboard that is entirely correct, and it fails louder the more complete the row gets, which is the wrong direction for a layout test. CnWidgetWrapper renders its heading as `<h3 :id="titleId">` with `titleId = cn-widget-wrapper-title-${widgetId}` and `resolvedWidgetId()` returning the supplied widgetId, so `#cn-widget-wrapper-title-minutes-in-review` addresses exactly the stats-block this line is about -- the same id the manifest declares and the same element the CI log named -- and stays correct if another surface shows the same phrase. Every other assertion in this test already locates its widget by a stable attribute rather than by text; this line was the exception. One of decidiq's four E2E failures. Not related to the seeding gap in #1052. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…its (#1054) `quick-filter dropdown label "All urgent" renders intact at every tested viewport` failed as a bare: Test timeout of 20000ms exceeded. The test could not pass reliably no matter how healthy the app was. playwright.config.ts sets `timeout: 20_000` per test. This one loops THREE viewports, and each iteration navigates and then allows `waitForSelector(15_000)` plus `expect(select).toBeVisible({ 10_000 })` -- 25s of permitted waiting in the FIRST iteration alone, against a 20s total budget. The symptom is misleading in a specific way: a whole-test timeout names no element, so it reads like a slow or broken dropdown rather than a budget that was never large enough for the work the test does. 90s matches the sibling multi-step specs (voting-rules.spec.ts uses 90_000; goals-pages.spec.ts uses 120_000) and the per-step waits are left untouched, so a genuinely stuck dropdown still fails on its OWN 10s assertion with a message naming the element. The fourth and last of decidiq's E2E failures. The other three: two seeding gaps in #1052, one strict-mode locator violation in #1053. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…n install (#1024) * feat(setup): ask which example set to load, instead of planting one on install Installing this app planted 334 objects nobody asked for. SettingsService::loadConfiguration() merges decidesk_register.json with all 26 register.d fragments, and every one of them carried its own x-openregister.seedData.objects. The InitializeSettings repair step runs that merge, so a fresh install seeded a Gemeenteraad Amsterdam, a VvE Zeewaarts, a pub quiz, five ACME B.V. bodies and eight placeholder TOOI mappings into the operator's register. A municipality got the VvE data. A VvE got the council data. Nobody chose any of it. The wizard meanwhile offered a DIFFERENT dataset, whose own step text said "Skip this on a production install". By the time an operator read that sentence, 334 objects were already in their register. So the fragments now declare schemas only, and the objects live in lib/Settings/profiles/, one file per example set. A bare install plants nothing; the wizard asks which organisation this is for, and loads that. ## The split had to be a closure, not a partition The seeds cross-reference each other by slug. The reference graph has one connected component of 170 objects tangling municipal, corporate and association bodies together, so no partition exists. Each set is instead anchored on its governance bodies and closed over outbound references, so every reference resolves inside the set carrying it. Sets may overlap, and 15 objects do. Verified: 334 of 334 classified, zero dangling references, zero orphans. ## Why the descriptor declares no register An example set carries @self.configuration/register/schema on every object and declares NO components.registers. That is load-bearing. ImportHandler::importRegister() calls setApplication($appId) unconditionally when it updates an existing register, so a descriptor that declared decidiq would re-point the register at the profile's config id and hydrate over its authorization block: the baseline that stops any authenticated user rewriting another body's decisions. Verified on a live instance: importing this shape left application=decidiq, the version and the authorization hash byte-identical, imported 45 objects, and added nothing on a second run. The files sit in a SUBDIRECTORY because RegisterDescriptorService scans lib/Settings/*.json non-recursively and indexes by declared register slug; four profiles in lib/Settings would collide with each other and with the app's own register. ## Two wizard steps, because an action carries no body CnSetupWizard::runAction() posts to /api/setup/action/{action} with no body, so an action cannot carry the answer. A choice step records which set via the new POST /api/setup/config, and the run-action step reads it back. ADR-111 keeps the schema-generated mock on offer, so it is one option in the same choice rather than a second question about the same thing. ## Ten pre-existing defects this surfaced, all fixed Validating every seed against its schema found: - Three seeds keyed on `regulation`, which is not a decidiq schema slug: the app's is `regeling`. Because importSeedData() resolves a slug cross-app with multitenancy off, `regulation` resolves to LEARNIQ's schema. - Regeling.status was required and named by x-openregister-lifecycle, but never declared as a property, so OpenRegister created no magic-table column. Measured: oc_openregister_table_21_262 carried every other property and no status column, which means the declared in-preparation -> adopted -> in-effect -> lapsed map could never advance a regulation. - Six invalid enum values on the pub-quiz decision-stage seeds. - Two governance bodies missing the required `domain`. - The works-council seeds described a city council: all three WOR consultation requests carried governanceBody: gemeenteraad-amsterdam, a null-UUID director, and a raadsvergadering as their overlegvergadering. Replaced with a real 45-object set built around an ondernemingsraad at ACME B.V. ## Verification 1235 PHP unit tests and 378 vitest tests pass. PHPCS 0 errors, PHPMD, PHPStan and Psalm clean. Manifest validates against schema 2.26.0. The manifest-drift guard was proved to FAIL when the manifest and the shipped sets disagree. Two hydra gates (22, 53) still fail, and did before this change: hydra-gates v1.10.0 vendors manifest schema 2.25.0, which predates the `flow` page type that FlowDetail already used. That is fleet debt in ConductionNL/.github. * fix(seeds): a 37-character slug silently dropped an object from the example set Loading the municipality set on a freshly reset instance reported "Imported 199 example object(s)" and 198 landed. The count comes from the FILE, not the importer's reply — deliberately, so the number names what was ASKED FOR — which means the message could not tell the operator that anything was wrong. The importer's log could: [ImportHandler] Skipping seed object for 'termijnagenda-item' - import failed: SQLSTATE[22001]: String data, right truncated: value too long for type character varying(36) OpenRegister types a relation column as varchar(36), the width of a UUID. A seed that points at another object BY SLUG therefore cannot be stored when that slug is 37 characters or longer, and the row is skipped. `lta-herziening-parkeerbeleid` referenced `goal-amsterdam-parkeerbeleid-kwartaal`, which is 37. Shortened to `goal-amsterdam-parkeerbeleid` (28), in the goal and in the reference. MEASURED, NOT GUESSED, AND THE RULE IS NARROWER THAN IT LOOKS. Twenty-one shipped slugs are longer than 36 characters and twenty of them are harmless: `_slug` is a wider column, so a long slug only breaks when something POINTS at it. Exactly one was referenced, and it is the one that failed. So the guard added here asserts the real rule — no seed may be referenced by a slug over 36 characters — rather than banning long slugs. Verified to FAIL on the original data, naming the offender and its length, and to pass after the fix. After it: all four sets import completely on a fresh instance. municipality 199/199 association 56/56 corporate 91/91 works-council 45/45 * fix(l10n): regenerate nl.js so the wizard copy is actually translated The Dutch strings for the new setup steps went into l10n/nl.json, but the frontend reads the GENERATED l10n/nl.js, which is a separate committed artefact. So the wizard rendered its own copy in English while every control around it (Annuleren, Volgende, Sluiten) was Dutch: the half-translated screen that reads as a bug rather than a missing string. Regenerated with npm run l10n:build. CI's check:l10n-js compares the two and would have failed the PR, which is the check doing its job. * fix(e2e): keep the CI seed in step with the renamed setup action, and stop it lying about missing schemas Two defects the local e2e run surfaced, both of which would have made CI unreadable. ## 1. The seed settled a step that no longer exists `ci-seed.sh` posts `setup/action/skip-demo-data` before the suite runs, because CnAppRoot opens the wizard as a full modal mask in every fresh browser context while an optional step is outstanding. seed-profiles renamed that action to `skip-example-set`. The script is deliberately TOLERANT of a non-200 there ("an app whose wizard has no such step answers 400, and that is not a seeding failure"), so the rename would NOT have failed the seed step. It would have failed every later spec instead, on `<ol class="cn-wizard-dialog__progress">` intercepting the click — the message that accuses the selectors. Verified fixed: the seed now reports `POST setup/action/skip-example-set -> HTTP 200`. A unit test now asserts every action id the script posts is one SetupController actually handles, so the next rename is caught there rather than in 194 timeouts. It fails on the old name, naming it. ## 2. "Schemas missing after import" was a paging artefact The verification asked for `?_limit=1000` and reported every required slug it could not find as a missing schema: ::error::Decidiq schemas missing after import: ['meeting', 'action-item', 'minutes', 'vote', 'transcript', ...] All ten were in the database. This instance has 35 apps installed and 1111 schemas, so ten decidiq rows fell off the end of the page. The import had worked; the QUESTION was too small — and the check sent you to look at the import, which is the worst shape a wrong answer can take. The limit is now well past any plausible instance AND truncation is detected rather than assumed away: a response that comes back exactly full says so, and names the page as the suspect instead of blaming the import. * fix(setup): skipping is choosing none, so it closes BOTH steps `skip-example-set` returned 200, reported "No example data was loaded", and left the setup wizard open over every page. Splitting the old single `demo-data` step into a `choice` plus a `run-action` gave the wizard TWO outstanding steps. The skip action wrote only `demo_data_decided`, which closes the second. CnAppRoot opens the wizard while ANY optional step is outstanding, so the first one kept it open. Measured 2026-08-30 against a live instance: after ci-seed.sh posted the action, the status was still {"example-set": {"done": false}, "load-example-set": {"done": true}} and the e2e suite failed on `<ol class="cn-wizard-dialog__progress">` intercepting clicks Playwright had already resolved — the "visible, enabled and stable, then timeout" signature that means COVERED, not slow. Six specs were red by test 13 for that reason alone. Skipping IS choosing none, so the action now records that choice too. Verified live: both steps report done from the one call. The test asserts BOTH writes rather than just the one it used to, because asserting only `demo_data_decided` is exactly what let this through. * test(seed): assert the ci-seed/controller agreement by route, not by spelling The merge took development ci-seed.sh, which settles the setup step with POST api/setup/config and example_profile=none, and kept this branch test, which asserted the script posts api/setup/action/<id>. Both were right on their own and contradicted each other once merged: 1 failure in 1283 tests, on all six PHPUnit cells. The test now accepts either route and checks the agreement that actually matters, that whatever the script names exists in the controller. For the config route that means the profile key. Two traps while writing it, both caught by running the assertions against the real files rather than trusting the suite to be right: - sweeping every "key": in the script picks up error, success, message and provider from unrelated responses it parses, and asserts the controller reads those. It does not, so the test failed for a reason that was not a defect. - matching the first --data payload finds an empty {} from an earlier curl, so the key loop iterates nothing and the test passes no matter what. The payload is now bound to its own url. Verified it still fails when it should: renaming example_profile in the controller turns it red. --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl> Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Release: merge development into beta
rubenvdlinde
requested review from
Rem-Dam,
SudoThijn,
bbrands02 and
remko48
as code owners
August 31, 2026 11:20
rubenvdlinde
requested review from
WilcoLouwerse and
rjzondervan
as code owners
August 31, 2026 11:20
Contributor
Quality Report — ConductionNL/decidiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-nav-ceiling | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-l10n-js | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| composer | ✅ | ✅ 104/104 | |||
| npm | ✅ | ✅ 537/537 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-31 12:38 UTC
Download the full PDF report from the workflow artifacts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings main level with beta, which is 26 commits ahead.
development is green on its last completed push run with Playwright E2E passing, and beta was brought level with development earlier today, so this carries verified code rather than a snapshot of unknown state.
Security alerts are measured on main but fixed on development, so this promotion is what actually clears them.