Release: merge development into beta - #18
Open
github-actions[bot] wants to merge 1278 commits into
Open
Conversation
This was referenced Apr 18, 2026
…the CSP for the location widget
Flag case, task, bezwaar, voorstel and beroep searchable in the procest register and claim deep links for them, so the fleet-wide OpenRegister ObjectsProvider surfaces them in the Nextcloud search bar. Version bumps gate the register re-import. Consistency asserted by a vitest suite. Change: case-search-via-or-unified-search (archived 2026-07-12)
…OR search leaf' (#192) from wip/case-search-via-or-unified-search into development
Adds an Export actions menu to the Cases page (actionsComponent slot) that downloads the filtered case list through OpenRegister's export endpoint - no procest-side serialization (ADR-022). URL building lives in a unit-tested helper; 191/191 vitest green, manifest schema valid. Change: case-list-export-via-or-export-leaf (archived 2026-07-12)
…e OpenRegister export endpoint' (#193) from wip/case-list-export-via-or-export-leaf into development
…face Six OpenAPI 3.0.3 documents (zaken/documenten/catalogi/besluiten/ autorisaties/notificaties) describing every routed /api/zgw endpoint, served via a public allow-listed ZgwOpenApiController (GET /api/zgw/ openapi index + per-API yaml). A route<->spec conformance PHPUnit test fails on drift in either direction. Full unit suite green (1100 tests). Change: zgw-openapi-publication (archived 2026-07-12)
…ith route-conformance gate' (#194) from wip/zgw-openapi-publication into development
Adds BulkStatusTransitionService (preview + execute, 100-id cap, partial failure isolation) behind the transition engine's single write path, new statusTransition#bulkPreview/bulkExecute routes, column-scoped selection on the workflow board with a bulk-actions bar, and BulkTransitionDialog (NcDialog) with per-case ready/blocked preview and result reporting. Also fixes a pre-existing webpack error (dialogs v6 FilePicker imports node path; resolve.fallback path:false). PHPUnit 1120 green, vitest 209 green, build compiles. Change: case-bulk-status-transition (archived 2026-07-12)
…s (preview + per-case guards)' (#195) from wip/case-bulk-status-transition into development
Implements GET /api/ai/audit with OR-backed entry listing (caseId/type filters, paging), a new AiAuditExportController for RBAC-gated CSV/JSON export (auditors/secretariaat/beheerders/admin), and manifest pages AiOversight/AiOversightDetail for inspecting AI suggestions and human decisions. All 6 AI operations already recorded audit entries at suggestion time (verified). Fixes PHPCS/PHPMD issues in new code. PHPUnit 1141/0 fail, vitest 209/0 fail, manifest valid. Change: ai-oversight-log (archived 2026-07-12)
…ht UI for EU AI Act Article 14' (#196) from wip/ai-oversight-log into development
…g every run Fixes recurring log noise from procest's OpenRegister repair/seed steps: - register.d/25-brp-kvk.json: 'bsn' is not a registered OpenRegister string format — replace with a pattern-validated string (ADR-011 unaffected, no procest-side validator was ever added). - verwerkingsactiviteiten.json: shorten bewaartermijn strings that exceeded the OpenRegister varchar(64) column, which made every insert/update of the AVG verwerkingsregister catalogue fail with a DB truncation error. - ObjectService::findAll() takes a single $config array; the previous named-argument calls (register:/schema:/limit:) in both LinkInFlight*DecisionsRepair steps threw "Unknown named parameter" on every run. Routed through the existing SearchesObjects:: searchObjectsAsArrays() bridge instead. - KccWerkplekSeedDataService::seedRows() called saveObject() with positional args ($register, $schema, $row) against a signature whose first parameter is the object payload — fixed to named arguments. Also removed a dead method_exists($objectService, 'findObjects') guard that always returned false (that method never existed on ObjectService), which silently skipped the idempotency check on every boot. - Repair steps and boot-time seed services run without a Nextcloud user session, so anonymous callers are fail-closed by OpenRegister RBAC (#1955) on creates/updates and on reads without an explicit public grant. Added SearchesObjects::runAsSystemIfAvailable() (wraps ObjectService::runAsSystem() with a method_exists guard for back-compat) and used it in every affected repair/seed class so their own OpenRegister reads/writes — and, where scoped safely, the nested WorkflowDefinitionService calls they make — run as a trusted system principal instead of "Anonymous". This also fixes VTH workflow templates never actually seeding (caseType lookups were silently RBAC-denied, always reporting "not found"). - SeedVthWorkflowTemplates: downgrade the now-rare "caseType not found" case from warning to debug/info — it only fires on a genuine missing precondition (base-register-seed-data not yet run), not on every boot. BesluitvormingTemplateService and KccWerkplekSeedDataService are only ever invoked from boot-time repair steps (verified: no controller/live-request callers), so wrapping their full write path in runAsSystem() does not widen RBAC for any authenticated request.
… and RBAC-denying every run' (#197) from fix/log-noise-schemas into development
…uarded delete
Adds the top functional-admin wish from the zaaksysteem-family trackers
(open-zaak#693 copy, open-zaak#517 draft delete):
- POST /api/case-definitions/{id}/copy — deep-copies a case type: new id,
title prefixed 'Copy of', forced back to draft, publication fields
cleared, workflow-version pin and sibling case-type links dropped, and
every owned sub-object (statusType, resultType, roleType,
propertyDefinition, documentType, decisionType) copied and re-parented.
404 when the source does not resolve.
- DELETE /api/case-definitions/{id} — deletes draft case types only;
409 for published ones (unpublish first), 404 when missing.
- New CaseTypeCopyService follows the TemplateLibraryService pattern
(SettingsService-resolved register/schema + ObjectService single write
path, ADR-022).
- UI: Duplicate action on the case-type list rows and the detail header;
both navigate to the new draft. List delete now goes through the
guarded endpoint and surfaces the 409 as 'unpublish first'.
- Tests: CaseTypeCopyServiceTest (5) + CaseDefinitionControllerTest (5);
suite 1141 -> 1151 green.
- Fixes pre-existing BrpKvkRegisterSetsTest failure: restored format:bsn
on the BRP burgerservicenummer property (ADR-011).
- l10n: adds 16 missing en/nl key pairs flagged by check-l10n (2 new from
this change, 14 pre-existing gaps).
openspec change archived to openspec/changes/archive/2026-07-13-zaaktype-copy.
…ft + draft-only guarded delete' (#198) from wip/zaaktype-copy into development
…tor workload Adds WorkQueueService: a deterministic urgency scoring function (deadline tiers via business-day math incl. termijn extensions/pauses, priority, case age) with GET /api/work-queue (personal scored queue) and GET /api/work-queue/workload (coordinator-only per-handler open-case counts, gated the same way SubstitutionController gates coordinator actions). My Work gets an Urgency/Newest sort toggle and per-card urgency chips fed by the new endpoint, plus a workload summary bar for coordinators — without reintroducing the client-side "werkvoorraad" board that was previously retired (see openspec/specs/my-work/spec.md). Also fixes a pre-existing bug in BrpKvkRegisterSetsTest: the assertion expected a 'bsn' OpenRegister string format that the shipped fragment never registers (it intentionally pattern-validates instead, per the field's own description).
…ad-intelligent-queue
…rocest/abstraction/wave-2
…08 features mapped across the abstraction stack' (#199) from wip/market-feature-workup-doc into development
…ant SaaS + ActionRegistry)
OpenRegister's ObjectService::findAll() takes a single $config array:
findAll(array $config=[], bool $_rbac=true, bool $_multitenancy=true)
The named-argument form findAll(register:, schema:, limit:, offset:, filters:)
no longer exists. Every such call threw "Unknown named parameter $register"
at runtime. All 13 sites sit inside try/catch blocks that swallow Throwable
and degrade to an empty list, so the code paths were 100% dead but silent —
the app looked healthy while the feature never ran once.
ResetMonthlyQuotasJob is confirmed failing in the live Nextcloud log
("Procest: ResetMonthlyQuotasJob fetch failed" / "Unknown named parameter
$register"): the monthly quota reset has never reset a single quota.
Per OR's prepareFindAllConfig(), register/schema are read from INSIDE
$config['filters']; limit/offset are top-level config keys. Both keys are in
MagicSearchHandler::getReservedParams(), so they set the register/schema
context and are excluded from object-field WHERE building (no 1=0 leak).
This matches the array form already used by procest's own
Stuf\StufRegisterAccess::findAll().
Fixed (all previously dead):
- BackgroundJob/ResetMonthlyQuotasJob monthly quota reset
- Service/TenantSaasService listActive(), slugExists()
- Service/TenantOnboardingService getProgress(), markStepComplete(), countSchemaRows()
- Service/TenantAuthenticationService mandate lookup, role resolution
- Service/TenantQuotaService getQuota()
- Service/TenantConfigurationService getConfig()
- Service/TenantBillingService fetchEventsForMonth()
- Service/Actions/ActionRegistry findAction(), fetchAll()
Deliberately NOT touched (valid signatures, verified against HEAD):
- $this->findAll(...) in StufRegisterAccess / AdvisoryBodyService (local wrappers)
- $this->register->findAll(...) in StufController (procest's StufRegisterAccess)
- ConsumerMapper::findAll(filters:) and OrganisationMapper::findAll(limit:)
(different OR classes whose named params genuinely exist)
Also fixes 3 pre-existing phpcs errors in lib/Repair/ that left cs:check red
on development (via the project's own phpcbf).
phpcs: 0 errors. php -l: clean. phpunit: 1151 tests / 3847 assertions,
identical to the origin/development baseline (10 pre-existing errors are
missing ZipArchive/IRootFolder in the bare test container, not regressions).
…lls — live quota-reset job silently broken' (#200) from fix/objectservice-findall-signature into development
The beschikking sign leg only had SigningAdapterInterface + a deterministic
MockSigningAdapter stub — no real implementation existed anywhere. Add
LibresignSigningAdapter implementing that same contract, backed by
LibreSign (LibreCode)'s local OCS API via a thin LibresignApiClient
(IClientService), with signer identity resolved from the mandaat-authorised
actor, LibreSign status mapped onto pending/signed/declined via
LibresignStatusMapper, and signed files persisted through the existing
ZgwDocumentService storage path.
SigningAdapterInterface now resolves to LibresignSigningAdapter only when
IAppManager::isEnabledForUser('libresign') is true; otherwise it falls back
unchanged to the pre-existing MockSigningAdapter with a logged, translated
admin hint (also surfaced via SettingsController::index()). procest never
hard-depends on LibreSign.
See openspec/changes/archive/2026-07-13-libresign-besluit-signing/design.md
for the LibreSign API binding (routes/payloads are a documented assumption —
no LibreSign checkout was available to verify against) and the fallback
semantics.
…igital-signing adapter' (#201) from wip/libresign-besluit-signing into development
WOODecisionService could assemble a formal WOO besluit but had no bridge to actually publish it — every decision dead-ended internally despite the woo-case-type spec's "Publication to reading room" requirement being marked done. Procest doesn't own publication (ADR-Leaf-First: OpenCatalogi does), so this adds WooPublicationService + OpenCatalogiApiClient, binding to OpenRegister's confirmed Objects API the way OpenCatalogi's own frontend and backend already create/update/withdraw publications (OpenCatalogi's own PublicationsController is read-only). Redacted-only disclosure is enforced in one place (niet_openbaar always excluded, deels_openbaar only via a finalized redaction reference, never the original), mapped to the real 17-category DIWOO vocabulary (infocat014, Woo-verzoeken en -besluiten). Feature-gated on opencatalogi being installed; no hard dependency. New publish/withdraw endpoints on WOOAssessmentController and a minimal publish action + status on the existing WOO assessment view.
…logi' (#202) from wip/woo-publication-via-opencatalogi into development
…all (#780) * fix(pwa): the service worker swallowed every PDOK-via-openconnector call `E2E Tests (Playwright)` was red on `development` with three `page.evaluate: TypeError: Failed to fetch` failures in `spec-coverage/pdok-via-openconnector.spec.ts`, thrown from inside Nextcloud core's patched `window.fetch` (`core-main.js:1:80554` is the `await t(e,n)` in that wrapper — checked against stable32's shipped `dist/core-main.js`). It is not a test problem and not a Playwright problem. Measured on a CI runner with NO `page.route()` registered at all, the same fetch still throws; and with the page loaded OUTSIDE the service worker's scope, it returns 404 and Playwright's route handler fires normally. TWO PRODUCT DEFECTS, both in the mobiel-inspectie-offline PWA layer: 1. `public/service-worker.js` decided "is this a map tile?" with `/(brtachtergrondkaart|wmts|pdok|service\.pdok\.nl)/i.test(url.host + url.pathname)` — a substring test that also runs over the SAME-ORIGIN path. Since migrate-pdok-to-openconnector this app's own address lookups live at `/apps/openconnector/api/pdok/{suggest,lookup,free, reverse}`, so every one of them matched on the literal `pdok` and was answered cache-first out of the map-tile cache. The worker is registered at app-root scope, so this hit every procest page. Replaced with an exact third-party host allow-list: a request back to this Nextcloud is never a tile. 2. A Service Worker inherits the CSP of its OWN script response. `DashboardController::serviceWorker()` sent Nextcloud's default `default-src 'none'` with no `connect-src`, under which EVERY `fetch()` the worker makes is blocked. Measured on Nextcloud 32: `fetch(request)`, `fetch(request.url)` and `fetch(url, {mode:'same-origin'})` all threw inside the worker. Both strategies (`cacheFirst` / `networkFirst`) therefore always fell through to `Response.error()` — the worker could only ever break a request, never serve one, and the offline sync cache could never be populated in the first place. Now sends `connect-src 'self' https://service.pdok.nl`. Together these made `src/services/pdokService.js` REJECT rather than degrade: `handleNetworkError()` rethrows on an error with no HTTP status, so the address field broke outright instead of surfacing the 503/404 warning `openspec/specs/pdok-consumer/spec.md` requires. Why it was CI-only: the worker's scope is `generateUrl('/apps/procest/')`, which keeps the `/index.php` prefix unless `front_controller_active` is set. On CI (`php -S`) the spec's `/index.php/apps/procest/dashboard` is INSIDE that scope and the worker controls the document; on the docker images every developer uses, the same URL is OUTSIDE it and the worker is invisible. The specs now navigate into the worker's own scope and wait for `navigator.serviceWorker.controller`, so the coin flip is gone. Test hardening in the same files (each mutation-verified): - assert the ITEM, not the container. `expect(Array.isArray(result))` was true for the empty array an unintercepted server answer produces, and `expect(status).toBe(404)` passed on Nextcloud's own 404 — openconnector is genuinely absent on CI. Both fulfilled responses now carry a marker header that a real server answer cannot produce, and the payload assertion names `Lauriergracht 116`. - `addressesRegisterAvailable()` returned `status() !== 404`, so 401 from an unauthenticated context read as "the register is installed" and the test went on to seed against it. Now only a 2xx counts. - that same test resolved `process.env.NEXTCLOUD_URL || 'http://localhost:8080'` — the SHARED dev container — so it seeded and deleted OpenRegister objects in somebody else's environment whenever the suite was pointed elsewhere. Now uses the single `BASE_URL` resolver. New `spec-coverage/service-worker-scope.spec.ts` guards defect 2 directly. A third test that would have guarded defect 1 from the response alone was written, found unable to fail under the planted defect, and left out rather than kept as a check that always passes. Refs #719 * test(pwa): unit-cover the service-worker script CSP The coverage ratchet caught the 4 new statements in DashboardController::serviceWorker() as untested — correctly: the CSP on that response is the difference between a Service Worker that can fetch and one whose every request is blocked, and nothing else in the suite looks at it. Proven able to fail: with the CSP block removed, testServiceWorkerScriptGrantsConnectSrc reports "Failed asserting that 'default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'' contains \"connect-src\"."
…→ 0 (#781) * fix(a11y): associate every form control with its label (gate-40, 157 findings) Gate-40 (form-label-association) reported 157 unlabelled form controls at full scope — the single largest item in the fleet. It was also the one carrying the loudest false-positive warning: the same gate ran ~40% FP on pipelinq and ~13% on openregister, so the instruction was to sample before actioning. MEASURED FALSE-POSITIVE RATE ON PROCEST: 0% (0 of a random 20). A 20-finding random sample (seed 40) was verified one by one against the real source markup. Every one was a genuine missing association, in three shapes: 14/20 a visible <label> with NO `for=`, not wrapping the control: <div class="form-group"> <label>{{ t('procest', 'Valid from') }}</label> <input type="date" :value="form.validFrom"> </div> Visually labelled, programmatically anonymous. This is exactly what WCAG 1.3.1 (Info and Relationships) is about, and it is the gate's own name: form-label-ASSOCIATION. 5/20 no label at all — only a `:placeholder`, which is not an accessible name (it disappears on input; WCAG 3.3.2). 1/20 a <span class="property-label">, which is not a label element. So the fleet-wide FP warning does not transfer: procest's markup uses one consistent hand-rolled `.form-group > label + control` idiom, and that idiom is wrong 157 times rather than being 157 separate mistakes. THE FIX ------- Per the checker's own accepted forms (read from check_form_labels.py, not guessed): * 89 controls: `<label for="x">` + matching `id="x"` on the control. This is real HTML association, and it is identical for Nc components — `<label for="x">` + `<NcTextField id="x">` works because the `id` prop lands on the rendered <input> on both the nc-vue 8 and 9 lines. `input-id` is NOT a prop on NcTextField/NcInputField and was used nowhere. * the remainder: `aria-label`, used only where no visible label exists and adding one would change the design (controls inside table cells, compact filter toolbars, rows repeated by v-for). Never on a control that already has a visible <label> — that would override the visible text and break WCAG 2.5.3 Label in Name. * `label`-prop rendering was deliberately NOT used where a sibling <label> already shows the same string, because it would render the text twice. Inside a v-for the binding is dynamic (`:for`/`:id` built from the loop key or `aria-label` derived from loop data), so no duplicate DOM id is emitted. Every static id occurs exactly twice across src/ — once on the label, once on the control. BOTH DIRECTIONS PROVEN ---------------------- For each of the four batches, one added `for=` was removed again (leaving the `id` in place) and the checker re-run: it reported that exact control every time, then went clean when the attribute was restored. So the gate can still fail, and it is the for/id PAIR that closes it — a bare `id` does not, which also rules out "the checker went blind" as the reason for the green. gate-40 before: 157 gate-40 after: 0 No gate was weakened: no baseline, no waiver, no threshold moved, and the checker is the unmodified one CI runs (the local run reproduced CI's 157 exactly before any edit). NO REGRESSION IN THE SIBLING GATES ---------------------------------- The other markup checkers over the same 232 files are still 0: gate-12 (nc-select-labels), gate-37 (aria-hidden-focusable), gate-41 (button-name), gate-31/32 (markup-a11y), gate-44 (autocomplete), gate-43 (link-text). This matters because the two obvious cheap closures for gate-40 — `aria-hidden` on a focusable input, or an `aria-label` that contradicts visible text — are precisely what gate-37 and WCAG 2.5.3 exist to catch. npm run build OK vitest 330 passed (32 files) eslint 0 errors on all 42 files * fix(l10n): translate the 9 aria-label strings the gate-40 fix introduced `test:l10n` went red on the previous commit: 10 keys were used in source but absent from l10n/en.json. Every one of them is an `aria-label` added to name a control that had no visible label at all — so unlike a `for`/`id` pair, these DO introduce user-facing text and therefore need translating. Two of the ten were the same concept spelled two ways, coined independently in two files: "Condition description {n}" src/dialogs/ConsultationResponseForm.vue "Description condition {n}" src/views/public/ExternalConsultationResponsePage.vue Both name the description input of the n-th condition in a v-for. Unified on "Condition description {n}" (the natural English word order, and the spelling that already matched the sibling placeholder "Condition description"), which also drops the count to 9 rather than carrying a near-duplicate into the translation memory forever. en.json written with the repo's own extractor (`check-l10n.js --write`, key === English source). nl.json translated by hand rather than left to fall back to the raw English key — a Dutch-locale screen-reader user is exactly the person these labels exist for, so an untranslated aria-label would defeat the fix it belongs to: Escalation offset Escalatietermijn Extracted value for {field} Geëxtraheerde waarde voor {field} Motivation for {doc} Motivering voor {doc} Search complaints Klachten zoeken Select all fields Alle velden selecteren Select document {title} Document {title} selecteren Select field {field} Veld {field} selecteren Select redaction candidate {category} Lakkandidaat {category} selecteren Condition description {n} Omschrijving voorwaarde {n} node tests/l10n/check-l10n.js OK — every used translation key is present in l10n/en.json OK — en.json and nl.json key sets match (no missing Dutch translations) gate-40 is still 0; no new key came from a `for`/`id` fix, only from the aria-label minority. * refactor(dialogs): give every dialog its own home under src/dialogs/ (gate-13, 13 → 0) Gate-13 (modal-isolation, ADR-004) went red on the PR the moment the gate-40 commit landed — not because that commit introduced a modal, but because gate-13 is DIFF-SCOPED: touching a file that already contained an inline <NcModal>/<NcDialog> puts it in scope. Seven of the forty-two files the a11y pass touched carried that inherited debt. This clears it at FULL scope (13), not just the seven, so the next branch to touch any of them does not inherit the same surprise. WHAT THESE ACTUALLY WERE ------------------------ Eleven of the thirteen were already self-contained dialog components whose ROOT element is the <NcDialog> — CaseTransferDialog, VoorstelCreateDialog, ParafeerActieDialog, AiClassifyDialog, AiExtractDialog, BerichtenboxComposeDialog, AdviesAanvraagDialog, CreateShareDialog, CreateFederatedShareDialog, FederatedActivityPanel, BesluitRegistration. They were simply in the wrong directory. Those are `git mv`d into src/dialogs/ with NO markup change at all, which is what ADR-004 actually asks for. The remaining two were genuine inline dialogs inside a component with real content of its own, and were extracted: src/views/settings/StufAuditLog.vue -> src/dialogs/StufEnvelopeDialog.vue (prop `row`, emits `close`) src/views/settings/components/OrganisatieRolManager.vue -> src/dialogs/DeleteRolDialog.vue (props `role`, `blockedReason`; emits `close`, `confirm`) Helpers and scoped CSS moved with the markup that uses them; everything the parent still renders stayed. Visible strings, class names, prop names and emitted-event names are byte-identical to the originals. REFERENCES ---------- Every importer was found by grep, not guessed: CaseSharingTab (4), BerichtenboxTab, AdviesPanel, VoorstellenPanel, VoorstelDetail (2). Inside the moved files, 9 depth-sensitive imports were rewritten `../../../x` -> `../x`, and 3 sibling imports repointed back into views/ (AiConfidenceBadge ×2, DelegateSelectorField). `src/registry.js` and `src/customComponents.js` import only the PARENTS, never these thirteen — checked explicitly rather than assumed, because those two files are the ones that resolve components by path. No dynamic import() referenced them. The only data-testid among the thirteen (`stuf-audit-log-table`) sits on the TABLE, not the dialog, and stayed in StufAuditLog.vue; no e2e spec references any of these components. AiClassifyDialog and AiExtractDialog have zero importers anywhere — pre-existing orphans. Moved anyway, so the gate is clean at full scope rather than clean- except-for-two. BOTH DIRECTIONS --------------- A bare `<NcDialog />` planted into src/views/voorstellen/VoorstelDetail.vue: src/views/voorstellen/VoorstelDetail.vue: inline NcModal/NcDialog Removed it -> checker silent again, and `git diff` on that file shows only the two import-path lines. So the gate still detects the defect and did not go blind because files moved out from under it. VERIFIED -------- gate-13 full scope 13 -> 0 gate-40 full scope still 0 (not regressed by the moves) npm run build 0 errors npx vitest run 330 passed / 32 files (unchanged) npm run check:manifest Ajv validation PASS (0 errors), 58 pages npm run check:vue3-compile 231 components, clean 231, failed 0 npm run lint 0 errors No test was deleted, skipped or waived; no baseline, no threshold moved. * style(dialogs): blank line before rule in the two extracted dialogs (stylelint) `Vue Quality (stylelint)` went red on the dialog-extraction commit: src/dialogs/BerichtenboxComposeDialog.vue 161:1 rule-empty-line-before src/dialogs/StufEnvelopeDialog.vue 90:1 rule-empty-line-before src/dialogs/StufEnvelopeDialog.vue 100:1 rule-empty-line-before src/dialogs/StufEnvelopeDialog.vue 104:1 rule-empty-line-before Pure formatting — the CSS rules moved with the markup they style and landed without the blank line the config wants. No selector, declaration or value changed. `npm run stylelint` now exits 0.⚠️ WORTH KNOWING, AND NOT FIXED HERE: the `stylelint` npm script is stylelint src/**/*.vue src/**/*.scss src/**/*.css unquoted, so the SHELL expands the globs before stylelint sees them, and bash without `globstar` expands `src/**/*.vue` as `src/*/*.vue` — ONE directory level. The check has therefore never looked below `src/<dir>/`. Running the same stylelint with the globs QUOTED (so stylelint does its own recursive matching) reports 203 problems in the deeper tree that CI has never seen: npx stylelint "src/**/*.vue" "src/**/*.scss" "src/**/*.css" -> 203 errors None of those 203 are introduced by this branch — they are pre-existing debt in `src/views/**/**`, invisible because of how the script is written. The two files fixed above are only visible to CI because `src/dialogs/` happens to sit exactly one level down. Quoting the globs would surface all 203 at once, so that belongs in its own change rather than being smuggled into a dialog refactor. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…ix the 203 violations it exposed (#783) The stylelint scripts passed their globs UNQUOTED, so bash expanded them before stylelint ever saw them. Bash without `globstar` treats `**` as a single `*`, so `src/**/*.vue` expanded to `src/*/*.vue` — exactly one directory level. Measured on this tree: 48 files matched, out of 233 stylable files under src/. The npm stylelint job has therefore never looked deeper than one level and has always been green. Quoting the globs hands them to stylelint, which does its own recursive globbing. - before (quoted): 233 files linted, 203 violations (202 rule-empty-line-before + 1 no-duplicate-selectors) - after: 233 files linted, 0 violations, exit 0 The 202 rule-empty-line-before were fixed with stylelint's own --fix. The no-duplicate-selectors in CaseCard.vue was merged by hand: two .case-card__header blocks with no overlapping declarations, merged into one, keeping padding-left.
… 0 (#782) Ten distinct `@spec` targets did not resolve. All ten name a task or requirement that was never written, not one that moved. The bulk is one repeated cause. Seven DSO test files cite a `T11`–`T17` task block in `openspec/changes/dso-omgevingsloket/tasks.md`. That file numbers its implementation tasks `T01`–`T08` and its verifications `V01`–`V10`; there is no `T11` and there never was. The block looks like an author's private numbering for "the test for task N", offset by ten. Each tag is repointed at the task that actually created the code under test: DsoCaseServiceTest T11 → T02 (DsoCaseService) BeschikkingGenerationServiceTest T12 → T03 (beschikking service) SamenwerkverzoekServiceTest T13 → T03 (samenwerk service) DsoDeadlineJobTest T14 → T05 (DsoDeadlineJob) VergunningStatusChangedEventTest T15 → T04 (typed event) VergunningaanvraagCreatedListenerTest T16 → T04 (listener) DsoControllerTest T17 → T06 (DsoController) The remaining three: `#mentions` (3 files) → `#REQ-W2L-003`, the requirement in ncvue-w2-leaves-adoption's spec that actually covers note `@mention` notifications. `mentions` was a shorthand for a heading that does not carry it. `#P-6.2` → `#P-5.1`. migrate-role-routing-to-or-rbac's tasks.md names `tests/Unit/Service/StatusTransitionGroupAuthTest.php` explicitly, under P-5.1. P-6 has only a P-6.1, already cited by the other test. `#task-1-2` → `#task-1-1`. woo-llm-anonymisation section 1 has a single item, 1.1 `AiService::detectDeterministicPiiSpans()` — which is exactly what AiServicePiiDetectionTest covers. No spec text was added or renumbered to make a tag resolve: every new target is a line that already existed. Verified with the gate's own resolver — 36 findings before, 0 after — and the gate proven still able to fail by planting `#T14` on a throwaway test file, which it reported.
`EmailTemplateService::seedDefaultTemplates()` has been fully implemented
since case-email-integration T04 and no caller has ever reached it. It
seeds the three Dutch default templates for a caseType and is idempotent
by name, so a second call creates nothing.
This is the orphaned-write-capability shape gate-57 exists to catch: a
built capability with no way in. The fix is to wire it, not to delete it.
Adds `EmailTemplateController::seedDefaults()` and routes it at
`POST /api/casetypes/{caseTypeId}/email-templates/seed-defaults`.
The auth posture mirrors `createTemplate()` on purpose. This endpoint is a
loop over exactly that call and creates nothing a user could not create
one template at a time, so a stricter guard here would be inconsistent
rather than safer — and gate-9 (semantic-auth) checks that the attribute
matches what the body actually does.
Verified:
- gate-57 17 → 16, measured full-scope with the gate package CI runs
(ConductionNL/.github @ b8c7ead).
- No new gate: route-auth, no-admin-idor, semantic-auth, spec-coverage
and route-reachability all still pass.
- gate-25 (contract-coverage) does NOT grow: 126 with the new test,
127 without it. The endpoint ships with real coverage rather than
becoming invisible debt — proven by removing the test file and
watching the count rise, then restoring it.
- The auth gate is proven able to fail: replacing the session check
with `if (false)` turns testAnonymousCallerIsRejected red
("Failed asserting that 400 is identical to 401"), restored after.
- phpcs unchanged (721 findings on this branch and on development —
identical, all pre-existing), phpstan [OK] no errors, psalm reports
nothing on the changed files, PHPUnit 1746 passed / 5 skipped.
MandaatEscalatieService::createEscalatie and Subsidie\TussenrapportageService::createExpected were both fully implemented and reachable from nowhere. Their controllers already injected the services and exposed only the resolve half of each lifecycle: an escalation could be approved or rejected but never opened, and an interim report could be assessed but never scheduled. Wired, not deleted — both carry a @SPEC annotation pointing at a requirement that is still canonical. - MandaatMatrixController::escalateCreate, POST /api/mandate/escalations Auth posture mirrors escalateReject (@NoAdminRequired + ensureAuthenticated -> 403). initiatorId is derived from the session, never from the body, matching probe()'s identity stance. - SubsidieController::createTussenrapportage, POST /api/subsidies/uitvoeringen/{uitvoeringId}/tussenrapportages Auth posture mirrors createBeschikking (@NoAdminRequired + requireUser -> 401, 201 on success). uitvoeringId added to the bodyParams() routing-parameter strip list. 9 new PHPUnit tests cover both endpoints, including the auth gate and the "identity comes from the session" invariant. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…struct (#790) procest's phpcs run carries exactly one ERROR — every other finding is a warning. `Iv3TaakveldController::__construct()` calls `parent::__construct($appName, $request)` positionally, which the house `CustomSniffs.Functions.NamedParameters.RequireNamedParameters` sniff rejects. Every other controller in the app already passes these named. phpcs: 1 error / 501 warnings → 0 errors / 501 warnings, across 219 files. No warning was touched, suppressed or baselined — this fixes the error and nothing else. The sniff is proven live rather than vacuously green: reverting the call to its positional form takes the count straight back to 1 error, and restoring it returns to 0. phpstan [OK] no errors. PHPUnit 1746 passed, 5 skipped.
) `DsoIntakeService` carried two implementations of DSO intake. The live webhook path (`processAanvraag`, POST /api/vth/dso/intake) did the extract → build → save → save-properties inline, while an extracted `map()` + `createCase()` pair sat next to it with zero callers — an abandoned "make intake testable" refactor. `createCase()` was not merely unreachable, it had never run. It called OpenRegister's ObjectService positionally: saveObject($register, $caseSchema, $caseData) against the real signature saveObject(array|ObjectEntity $object, ?array $extend = [], $register = null, $schema = null, ...) so the schema slug landed in `?array $extend` and the register slug in `array|ObjectEntity $object` — a TypeError on the first call. The same bug was in its case-property loop. This wires the orphan instead of deleting it: - `processAanvraag()` now delegates to `map()` + `createCase()`, so there is ONE implementation of the intake write rather than two. - `createCase()` passes register/schema by NAME, fixing the latent TypeError, and reuses the existing `storeCaseProperties()` helper (which already had the argument order right) for the property rows. - `createCase()` adopts the live path's log line, so the message the webhook has always emitted is preserved. Characterization tests were written FIRST, against the pre-refactor code, and pin the observable behaviour of the live path: the exact saveObject call sequence including which parameter each named argument binds to, the case-property rows written vs skipped, and the byte shape of the JSON the Omgevingsloket receives back. They passed before the refactor and pass after it, unedited. The ObjectService test double declares OpenRegister's real parameter order, so the argument-order assertions are not vacuous: reverting to the positional call makes 12 of the 15 tests fail with "saveObject(): Argument #1 ($object) must be of type array, string given". Verification (hydra-gates @ ConductionNL/.github b8c7ead, 66 gates): - gate-57 orphaned-write-capability: 16 → 15; the only removed finding is DsoIntakeService::createCase. - gate-25 contract-coverage: 126 → 126, findings log byte-identical. - Failing-gate ID set identical before and after — no new gate fails. - composer phpstan: no errors. - composer phpcs: 1 error + 501 warnings in 219 files, identical to origin/development; the changed file carries the same 2 warnings. - vendor/bin/phpunit: 1761 tests green (1746 baseline + 15 new), 5 skipped. Behaviour note: on a MALFORMED payload with OpenRegister unavailable, the mapping now runs before the config guards, so such a request surfaces the mapping error rather than "OpenRegister is not available". Both are a 500 from the webhook and neither message is part of the response contract.
`NcModal`'s `name` prop already renders `<h2 class="modal-header__name">`
and wires it as the dialog's accessible name. `InzageExportModal` and
`InitiatorPickerModal` rendered a SECOND `<h2>` in the body carrying the
identical string, so a screen-reader user is told the dialog's name and
then immediately hears the same heading again. Six of the eight modals
that set `name` never did this; these two were the outliers.
Found by root-causing an e2e failure, not by reading the components. The
AVG spec's
getByRole('heading', { name: 'Data subject access export' })
resolved to TWO elements and Playwright failed it under strict mode:
strict mode violation: ... resolved to 2 elements:
1) <h2 class="modal-header__name">Data subject access export</h2>
2) <h2 data-v-3de9aa6e="">Data subject access export</h2>
The assertion text ("toBeVisible failed") looked like a missing heading.
It was the opposite: two of them.
This is also why the failure looked intermittent rather than constant. The
two headings mount at slightly different moments — the header with the
dialog, the second with the body slot — so an assertion resolving early
sees one and passes, and one resolving late sees two and throws. The same
commit could therefore go green on one PR and red on another, which is
exactly what happened tonight.
Adds tests/vitest/modalHeadingUniqueness.spec.js, which asserts the
invariant across EVERY modal rather than just these two, so a newly added
one is guarded the day it lands instead of whenever someone writes an e2e
test for it. It carries an explicit non-empty-scope assertion so it cannot
pass by finding no files.
Proven both ways: restoring the duplicate `<h2>` in InzageExportModal turns
the guard red ("1 failed | 11 passed"); removing it again returns 12/12.
Full vitest suite 342 passed. `npm run lint` and `npm run stylelint` both
exit 0.
…7 17 → 15 (#788) Two built-but-unreachable write capabilities in lib/Service/Bezwaar/ get a real way in. No controller anywhere injected anything from that namespace — its only two entry points were listeners. - recordAttendance(): new BezwaarHearingController, routed at POST /api/bezwaar/hearings/{sessionId}/attendance. The hearingSession schema has no manifest page, and attendance is append-only with a one-hour grace window plus an awb-art-7:7 audit entry on late corrections, so a generic edit form could not express it. Auth: @NoAdminRequired + explicit 401, mirroring AdvisoryBodyController. - recordCouncilDeviation(): wired on the event path in DecisionConcludedListener, where decidesk's conclusion makes the besluit op bezwaar final and the bezwaarDecision record already in hand carries advisoryOpinion / followsAdvice / deviationRationale. Exposing it as an endpoint would let any authenticated user forge an Archiefwet audit entry. gate-61 does not apply: the listener is registered on decidesk's DecisionConcludedEvent, not on an OpenRegister object event. BeroepService::recordJudgment() is deliberately left alone — see the PR body for the evidence on both sides.
… page (#784) (#792) * fix(admin): stop ChecklistsTab rendering one row per byte of an error page (#784) The procest admin settings page rendered a 44 MB DOM on which Playwright's role queries never terminated, so a red E2E carried no information about the change under it. The cause is not "fourteen sections mount at once" — measured per-section, ONE section held 630,388 of 632,482 elements (99.7 %); the other thirteen are 8-167 elements each. ChecklistsTab fetched `/apps/procest/api/objects/inspectionChecklist`. procest does not serve that route: its auto-exposed `/api/objects/<register>/<schema>` endpoints were deleted (appinfo/routes.php, "only engine routes remain"). Nextcloud answers an unmatched app URL with its OWN HTML page under HTTP 200, so axios never threw and `response.data` was a 45,031-character string. `response.data?.results || response.data || []` admits that string — a string has no `.results`, and a non-empty string is truthy — and `v-for` over a string iterates one item per CHARACTER. One row and two NcButtons per byte. Two changes, both needed: * Address `/apps/procest/api/vth/checklists`, the route InspectionChecklistController already serves for exactly these four verbs. Every one of them carries #[AuthorizedAdminSetting], which is the correct posture for an admin-settings control, and it routes through InspectionChecklistService, which owns checklist versioning. (OpenRegister's generic object route also resolves and was this fix's first attempt — it is the wrong choice: it bypasses procest's admin authorization and adds a second write path for a resource that has one.) * Never assign an unvalidated body to a `v-for` source. The same string-admitting idiom is hardened in the two other places it appears (adviceApi, VthDashboard). Measured on one clean rig, changing only this source and rebuilding: before after .checklists-tab__item rows 40,057 0 DOM elements 562,729 2,284 <button> 80,187 98 outerHTML bytes 44,434,205 3,370,949 getByRole('button').count() 7,012 ms 147 ms the failing assertion never returned (>60 s) 87 ms, count=1 body.ariaSnapshot() timed out (30 s) 342 ms, 17,686 chars The before column was taken with ZERO seeded data: 40,057 rows is the character length of the Nextcloud error page. This was never a data-volume problem, which is why it reproduces in CI. `ariaSnapshot()` timing out is why #784's error-context.md had no page-snapshot section and the failure read `Received: undefined`. case-types-tabs.spec.ts + spec-coverage/admin-settings.spec.ts: 6 passed. The test that used to burn its full 300 s cap now runs in 7.3 s. The new vitest spec is a true-positive control, run rather than asserted: with the component stashed back, 4 of its 6 tests fail, reporting 4104 rows for a 4104-character body. Control for the unrelated red: spec-coverage/settings-pages.spec.ts fails 10 on this rig BOTH with and without this change, so those failures are pre-existing and not caused here. None of the three files touched is on those pages' code path — ChecklistsTab is imported only by AdminRoot. No timeout was raised, no assertion weakened, no test skipped, no exclusion added. Refs #784 * fix(spec): annotate the four methods this PR changed, and repoint a dangling @SPEC gate-16 spec-coverage failed on PR #792 with 3 changed methods missing @SPEC (`ChecklistsTab::loadChecklists`, `::saveChecklist`, `VthDashboard::loadCases`); adding those docblocks shifted the diff and surfaced a fourth, `::closeEditor`. Reproduced locally against origin/development and now `# count=0`. The three checklist methods anchor at `openspec/specs/inspection-checklists/spec.md#inspection-checklist-admin-ui`, which is the canonical requirement for exactly this surface ("The system SHALL provide an admin interface for creating and managing inspection checklists"). `VthDashboard::loadCases` anchors at the canonical DSO spec FILE with no fragment, deliberately. No single requirement covers that dashboard's combined status/procedureType/gemeenteCode/activiteitgroep filter set; REQ-DSO-010 covers the location filter only, and citing it would have overstated what the spec says about the rest. The reason is recorded in the docblock so the next reader does not have to re-derive it. Also repoints that file's file-level anchor. It read `@spec openspec/changes/dso-omgevingsloket/tasks.md#T07`, and that change directory does not exist at all — the whole path was dangling, not just the fragment. A dangling @SPEC anchor is accepted silently, so it read as traceability while pointing at nothing. No spec text was written or amended for this: every anchor names a requirement that already existed. Refs #784
…ionDispatchJob (#797) * refactor(procest): TermijnNotificationDispatchJob -> DeadlineNotificationDispatchJob A code-only slice of openspec/changes/english-vocabulary, and the first procest slice — chosen because it is one of the very few procest identifiers that is neither ZGW protocol vocabulary nor wired by string. 'termijn' here is a STATUTORY DEADLINE being monitored, not a contract term, so it becomes Deadline. That reading is app-specific on purpose: hrmq uses 'termijn' for notice periods (aanzegtermijn) and decidesk for terms of office (TermijnRegeling). Three apps, three meanings, one Dutch word — it must never become a fleet word. Referenced only via ::class from TermijnNotificationService, so a missed reference is a compile error rather than a job that silently never dispatches. No route entry, no register-fragment wiring, no baseline suppression keyed to the old path — all four checked, not assumed. TermijnNotificationService itself keeps its name in this commit; it belongs with the termijnbewaking slice, where the Termijn* schemas (termijnDefinitie, termijnInstance, termijnGebeurtenis) move together with their register fragments. Archived changes under openspec/changes/archive/ keep the old name — they are a record of what was built at the time. php -l clean; class name matches file name; use statement still ordered. * fix(procest): add the @SPEC anchor gate-16 requires on the renamed job Hydra gate-16 (spec-coverage) failed this branch with "1 changed method(s) missing @SPEC". The method is DeadlineNotificationDispatchJob::run(), which the rename pulled into the diff and which had no @SPEC of its own — the class docblock carries one, but gate-16 scopes per CHANGED METHOD, not per file. Anchored to openspec/specs/burger-notifications/spec.md, the canonical spec. Deliberately NOT the change directory the class docblock still points at: @SPEC targets openspec/specs/, never openspec/changes/, because a change dir is archived once the change lands and the anchor then dangles. CONFIRMED MINE, NOT PRE-EXISTING. procest's Code Quality workflow is green on development's most recent run, and run() is only in gate-16's scope because this branch touched its file. Fixed rather than merged past.
…812) composer audit fails on development and on every open PR: Package: phpcsstandards/phpcsutils CVE: CVE-2026-65954 — Arbitrary code execution Affected: >=1.0.0-alpha1,<1.2.3 Reported: 2026-07-27 The lock pinned 1.2.2. It is a transitive dev dependency, pulled in by phpcsstandards/phpcsextra (^1.2.0), so nothing in composer.json needed to change — only the lock. WHY IT SUDDENLY APPEARS. development's last green Code Quality run predates the advisory reaching the audit database, so the gate did not fail then and does now. Nothing regressed; the check got new information. This is why it started appearing on unrelated rename PRs whose diffs contain no dependency manifest at all. BLAST RADIUS OF THE FIX: exactly one package version line in composer.lock. Verified by counting the changed "version" keys in the diff. THE BUMP IS EXERCISED, NOT JUST INSTALLED. phpcsutils is the library phpcs itself runs on, so `composer audit` reporting clean is not the only evidence: phpcs was executed against a real source file after the bump and ran to completion. A dependency bump that no build exercises is not a verified bump. VERIFIED - composer audit: "No security vulnerability advisories found." - phpcsutils 1.2.2 -> 1.2.3 in composer.lock; one package changed. - vendor/bin/phpcs runs clean against lib/Service/TermijnService.php.
* refactor(procest): TermijnNotificationDispatchJob -> DeadlineNotificationDispatchJob A code-only slice of openspec/changes/english-vocabulary, and the first procest slice — chosen because it is one of the very few procest identifiers that is neither ZGW protocol vocabulary nor wired by string. 'termijn' here is a STATUTORY DEADLINE being monitored, not a contract term, so it becomes Deadline. That reading is app-specific on purpose: hrmq uses 'termijn' for notice periods (aanzegtermijn) and decidesk for terms of office (TermijnRegeling). Three apps, three meanings, one Dutch word — it must never become a fleet word. Referenced only via ::class from TermijnNotificationService, so a missed reference is a compile error rather than a job that silently never dispatches. No route entry, no register-fragment wiring, no baseline suppression keyed to the old path — all four checked, not assumed. TermijnNotificationService itself keeps its name in this commit; it belongs with the termijnbewaking slice, where the Termijn* schemas (termijnDefinitie, termijnInstance, termijnGebeurtenis) move together with their register fragments. Archived changes under openspec/changes/archive/ keep the old name — they are a record of what was built at the time. php -l clean; class name matches file name; use statement still ordered. * refactor(procest): TermijnReporting* -> DeadlineReporting* TermijnReportingController -> DeadlineReportingController and TermijnReportingService -> DeadlineReportingService, with their two Dutch controller methods (kwartaalrapport -> quarterlyReport, jaarrekening -> annualStatement), every Dutch parameter and local ($periode -> $period, $afdeling -> $department, $jaar -> $year, $jaarPrefix -> $yearPrefix), the two named-argument call sites, the three route names, the test, and the Postman collection's description. Stacked on feat/english-vocabulary-deadline-job (#797) because that branch already renames TermijnNotificationDispatchJob; branching from development would have conflicted. FOUR DUTCH CONTRACTS ARE DELIBERATELY LEFT ALONE, and each is annotated in place so the next reader does not "finish the job" and break it: 1. $row['afdeling'] is a SCHEMA PROPERTY. OpenRegister materialises it as a real column, so it moves with a data migration, not with a rename. 2. 'periode', 'afdeling' and 'jaar' as RESPONSE KEYS are the published shape of the reports. src/views/dashboard/TermijnDashboard.vue reads annual.jaar directly, so they move only with a coordinated frontend change. 3. getParam('periode') and getParam('jaar') are QUERY-STRING parameter names — the request contract. 4. The /api/termijn/* URLs. Route NAMES follow the renamed class and methods; URLs do not. Moving them is a breaking change for every consumer and is a separate decision. NAMED ARGUMENTS. resolveQuarter(periode:) and aggregateByType(afdeling:) are called with named arguments, which PHP resolves at call time — a parameter rename that misses one is a runtime fatal that `php -l` reports as clean. Both moved with their declarations. VERIFIED - php -l clean on all four PHP files; the Postman collection still parses. - phpunit: 5 tests, 14 assertions green. - Route resolution: each 'deadlineReporting#<action>' route name was checked against the controller's actual public methods — all three resolve. Positive control: the same check against the OLD action names (kwartaalrapport, jaarrekening) reports MISSING METHOD, so it can distinguish a renamed route from a dangling one. - Residual grep over lib/, tests/, appinfo/ and src/: no live reference to either old class name or the old route prefix. PHPCS SCOPE, MEASURED RATHER THAN ASSUMED. phpcs.xml declares <file>lib</file> and nothing else, so tests/ and appinfo/ are outside the standard entirely — appinfo/routes.php carries 373 violations on development and every sibling test file is equally "dirty". Both files this change touches inside lib/ are CLEAN, including two pre-existing missing-@SPEC errors fixed here (each now anchors to openspec/specs/termijn-reporting/spec.md, the canonical spec, not the change directory the file headers point at). An earlier pass ran phpcbf over the test file and produced a 94-line diff for what is a 6-line rename. That was churn against a gate that never reads the file, so it was reverted; the test diff is now the rename alone.
…ner -> Deadline* (#804) * refactor(procest): TermijnNotificationDispatchJob -> DeadlineNotificationDispatchJob A code-only slice of openspec/changes/english-vocabulary, and the first procest slice — chosen because it is one of the very few procest identifiers that is neither ZGW protocol vocabulary nor wired by string. 'termijn' here is a STATUTORY DEADLINE being monitored, not a contract term, so it becomes Deadline. That reading is app-specific on purpose: hrmq uses 'termijn' for notice periods (aanzegtermijn) and decidesk for terms of office (TermijnRegeling). Three apps, three meanings, one Dutch word — it must never become a fleet word. Referenced only via ::class from TermijnNotificationService, so a missed reference is a compile error rather than a job that silently never dispatches. No route entry, no register-fragment wiring, no baseline suppression keyed to the old path — all four checked, not assumed. TermijnNotificationService itself keeps its name in this commit; it belongs with the termijnbewaking slice, where the Termijn* schemas (termijnDefinitie, termijnInstance, termijnGebeurtenis) move together with their register fragments. Archived changes under openspec/changes/archive/ keep the old name — they are a record of what was built at the time. php -l clean; class name matches file name; use statement still ordered. * refactor(procest): TermijnReporting* -> DeadlineReporting* TermijnReportingController -> DeadlineReportingController and TermijnReportingService -> DeadlineReportingService, with their two Dutch controller methods (kwartaalrapport -> quarterlyReport, jaarrekening -> annualStatement), every Dutch parameter and local ($periode -> $period, $afdeling -> $department, $jaar -> $year, $jaarPrefix -> $yearPrefix), the two named-argument call sites, the three route names, the test, and the Postman collection's description. Stacked on feat/english-vocabulary-deadline-job (#797) because that branch already renames TermijnNotificationDispatchJob; branching from development would have conflicted. FOUR DUTCH CONTRACTS ARE DELIBERATELY LEFT ALONE, and each is annotated in place so the next reader does not "finish the job" and break it: 1. $row['afdeling'] is a SCHEMA PROPERTY. OpenRegister materialises it as a real column, so it moves with a data migration, not with a rename. 2. 'periode', 'afdeling' and 'jaar' as RESPONSE KEYS are the published shape of the reports. src/views/dashboard/TermijnDashboard.vue reads annual.jaar directly, so they move only with a coordinated frontend change. 3. getParam('periode') and getParam('jaar') are QUERY-STRING parameter names — the request contract. 4. The /api/termijn/* URLs. Route NAMES follow the renamed class and methods; URLs do not. Moving them is a breaking change for every consumer and is a separate decision. NAMED ARGUMENTS. resolveQuarter(periode:) and aggregateByType(afdeling:) are called with named arguments, which PHP resolves at call time — a parameter rename that misses one is a runtime fatal that `php -l` reports as clean. Both moved with their declarations. VERIFIED - php -l clean on all four PHP files; the Postman collection still parses. - phpunit: 5 tests, 14 assertions green. - Route resolution: each 'deadlineReporting#<action>' route name was checked against the controller's actual public methods — all three resolve. Positive control: the same check against the OLD action names (kwartaalrapport, jaarrekening) reports MISSING METHOD, so it can distinguish a renamed route from a dangling one. - Residual grep over lib/, tests/, appinfo/ and src/: no live reference to either old class name or the old route prefix. PHPCS SCOPE, MEASURED RATHER THAN ASSUMED. phpcs.xml declares <file>lib</file> and nothing else, so tests/ and appinfo/ are outside the standard entirely — appinfo/routes.php carries 373 violations on development and every sibling test file is equally "dirty". Both files this change touches inside lib/ are CLEAN, including two pre-existing missing-@SPEC errors fixed here (each now anchors to openspec/specs/termijn-reporting/spec.md, the canonical spec, not the change directory the file headers point at). An earlier pass ran phpcbf over the test file and produced a 94-line diff for what is a 6-line rename. That was churn against a gate that never reads the file, so it was reverted; the test diff is now the rename alone. * refactor(procest): Termijn{Escalation,Pause,Extension}Service + listener -> Deadline* Four more classes in the deadline cluster: TermijnEscalationService -> DeadlineEscalationService TermijnPauseService -> DeadlinePauseService TermijnExtensionService -> DeadlineExtensionService TermijnCaseCreatedListener -> DeadlineCaseCreatedListener plus the test file, across 11 files: the DI listener registration, the daily scan job and service, TermijnController, and three test suites. Stacked on feat/english-vocabulary-deadline-reporting (#803), which is itself stacked on #797. Merge order: 797 -> 803 -> this. THE ONE DEFECT THIS SLICE PRODUCED, AND WHAT CAUGHT IT. Renaming the test FILE TermijnPauseExtensionServiceTest.php left the class inside it still called TermijnPauseExtensionServiceTest — neither of the two service-name replacements matched that token, because it is a third name. PHPUnit refused to load it ("Class DeadlinePauseExtensionServiceTest cannot be found"). The basename == class invariant I ran before committing covered only the four lib/ files and would have missed it; it now covers the tests too. Same failure mode as hrmq's provider discovery, reached from the opposite direction. LISTENER WIRING IS SAFE BY CONSTRUCTION. WorkflowListenerRegistrar registers the listener as TermijnCaseCreatedListener::class, not as a string literal, so the rename is resolved at compile time rather than silently failing to match at runtime. Checked before renaming rather than after. VERIFIED - php -l clean on every changed file. - phpunit, whole unit suite: 1777 tests, 6160 assertions, 5 skipped — green. - Every lib/ file this change touches is phpcs CLEAN. Three pre-existing issues were fixed along the way: docblock parameter-type alignment in TermijnController and TermijnDailyScanService (shifted by the longer type names), and two missing class-level @SPEC tags. Those now anchor to canonical specs — termijn-binding + termijn-pause-extension for the controller, which spans both, and termijn-escalation for the daily scan — rather than to the change directories the file headers still point at. - Residual grep over lib/, tests/, appinfo/ and src/: no live reference to any of the four old names. phpcs.xml scopes to <file>lib</file> only, so tests/ and appinfo/ are outside the standard; no formatting was applied to them.
…es (#806) * refactor(procest): TermijnNotificationDispatchJob -> DeadlineNotificationDispatchJob A code-only slice of openspec/changes/english-vocabulary, and the first procest slice — chosen because it is one of the very few procest identifiers that is neither ZGW protocol vocabulary nor wired by string. 'termijn' here is a STATUTORY DEADLINE being monitored, not a contract term, so it becomes Deadline. That reading is app-specific on purpose: hrmq uses 'termijn' for notice periods (aanzegtermijn) and decidesk for terms of office (TermijnRegeling). Three apps, three meanings, one Dutch word — it must never become a fleet word. Referenced only via ::class from TermijnNotificationService, so a missed reference is a compile error rather than a job that silently never dispatches. No route entry, no register-fragment wiring, no baseline suppression keyed to the old path — all four checked, not assumed. TermijnNotificationService itself keeps its name in this commit; it belongs with the termijnbewaking slice, where the Termijn* schemas (termijnDefinitie, termijnInstance, termijnGebeurtenis) move together with their register fragments. Archived changes under openspec/changes/archive/ keep the old name — they are a record of what was built at the time. php -l clean; class name matches file name; use statement still ordered. * refactor(procest): TermijnReporting* -> DeadlineReporting* TermijnReportingController -> DeadlineReportingController and TermijnReportingService -> DeadlineReportingService, with their two Dutch controller methods (kwartaalrapport -> quarterlyReport, jaarrekening -> annualStatement), every Dutch parameter and local ($periode -> $period, $afdeling -> $department, $jaar -> $year, $jaarPrefix -> $yearPrefix), the two named-argument call sites, the three route names, the test, and the Postman collection's description. Stacked on feat/english-vocabulary-deadline-job (#797) because that branch already renames TermijnNotificationDispatchJob; branching from development would have conflicted. FOUR DUTCH CONTRACTS ARE DELIBERATELY LEFT ALONE, and each is annotated in place so the next reader does not "finish the job" and break it: 1. $row['afdeling'] is a SCHEMA PROPERTY. OpenRegister materialises it as a real column, so it moves with a data migration, not with a rename. 2. 'periode', 'afdeling' and 'jaar' as RESPONSE KEYS are the published shape of the reports. src/views/dashboard/TermijnDashboard.vue reads annual.jaar directly, so they move only with a coordinated frontend change. 3. getParam('periode') and getParam('jaar') are QUERY-STRING parameter names — the request contract. 4. The /api/termijn/* URLs. Route NAMES follow the renamed class and methods; URLs do not. Moving them is a breaking change for every consumer and is a separate decision. NAMED ARGUMENTS. resolveQuarter(periode:) and aggregateByType(afdeling:) are called with named arguments, which PHP resolves at call time — a parameter rename that misses one is a runtime fatal that `php -l` reports as clean. Both moved with their declarations. VERIFIED - php -l clean on all four PHP files; the Postman collection still parses. - phpunit: 5 tests, 14 assertions green. - Route resolution: each 'deadlineReporting#<action>' route name was checked against the controller's actual public methods — all three resolve. Positive control: the same check against the OLD action names (kwartaalrapport, jaarrekening) reports MISSING METHOD, so it can distinguish a renamed route from a dangling one. - Residual grep over lib/, tests/, appinfo/ and src/: no live reference to either old class name or the old route prefix. PHPCS SCOPE, MEASURED RATHER THAN ASSUMED. phpcs.xml declares <file>lib</file> and nothing else, so tests/ and appinfo/ are outside the standard entirely — appinfo/routes.php carries 373 violations on development and every sibling test file is equally "dirty". Both files this change touches inside lib/ are CLEAN, including two pre-existing missing-@SPEC errors fixed here (each now anchors to openspec/specs/termijn-reporting/spec.md, the canonical spec, not the change directory the file headers point at). An earlier pass ran phpcbf over the test file and produced a 94-line diff for what is a 6-line rename. That was churn against a gate that never reads the file, so it was reverted; the test diff is now the rename alone. * refactor(procest): Termijn{Escalation,Pause,Extension}Service + listener -> Deadline* Four more classes in the deadline cluster: TermijnEscalationService -> DeadlineEscalationService TermijnPauseService -> DeadlinePauseService TermijnExtensionService -> DeadlineExtensionService TermijnCaseCreatedListener -> DeadlineCaseCreatedListener plus the test file, across 11 files: the DI listener registration, the daily scan job and service, TermijnController, and three test suites. Stacked on feat/english-vocabulary-deadline-reporting (#803), which is itself stacked on #797. Merge order: 797 -> 803 -> this. THE ONE DEFECT THIS SLICE PRODUCED, AND WHAT CAUGHT IT. Renaming the test FILE TermijnPauseExtensionServiceTest.php left the class inside it still called TermijnPauseExtensionServiceTest — neither of the two service-name replacements matched that token, because it is a third name. PHPUnit refused to load it ("Class DeadlinePauseExtensionServiceTest cannot be found"). The basename == class invariant I ran before committing covered only the four lib/ files and would have missed it; it now covers the tests too. Same failure mode as hrmq's provider discovery, reached from the opposite direction. LISTENER WIRING IS SAFE BY CONSTRUCTION. WorkflowListenerRegistrar registers the listener as TermijnCaseCreatedListener::class, not as a string literal, so the rename is resolved at compile time rather than silently failing to match at runtime. Checked before renaming rather than after. VERIFIED - php -l clean on every changed file. - phpunit, whole unit suite: 1777 tests, 6160 assertions, 5 skipped — green. - Every lib/ file this change touches is phpcs CLEAN. Three pre-existing issues were fixed along the way: docblock parameter-type alignment in TermijnController and TermijnDailyScanService (shifted by the longer type names), and two missing class-level @SPEC tags. Those now anchor to canonical specs — termijn-binding + termijn-pause-extension for the controller, which spans both, and termijn-escalation for the daily scan — rather than to the change directories the file headers still point at. - Residual grep over lib/, tests/, appinfo/ and src/: no live reference to any of the four old names. phpcs.xml scopes to <file>lib</file> only, so tests/ and appinfo/ are outside the standard; no formatting was applied to them. * refactor(procest): rename the daily-scan and seed-data deadline classes TermijnDailyScanService -> DeadlineDailyScanService TermijnbewakingSeedDataService -> DeadlineMonitoringSeedDataService DailyTermijnScanJob -> DailyDeadlineScanJob SeedTermijnbewakingData -> SeedDeadlineMonitoringData TermijnbewakingEndToEndTest -> DeadlineMonitoringEndToEndTest Stacked on feat/english-vocabulary-deadline-services (#804). Merge order: 797 -> 803 -> 804 -> this. TWO OF THESE ARE WIRED IN appinfo/info.xml, NOT BY IMPORT. The job is declared as <job>OCA\Procest\BackgroundJob\DailyTermijnScanJob</job> and the seeder as <step>OCA\Procest\Repair\SeedTermijnbewakingData</step>. These fail differently from each other, which is why both were checked rather than assumed: - an unresolvable JOB is silently reaped. Nextcloud's JobList::buildJob() logs "failed to create instance of background job" and calls removeById(), so the daily termijn sweep would simply stop running with no error surface. - an unresolvable REPAIR STEP throws during upgrade. Both entries move here. Verified by walking info.xml and resolving every class it names to a file on disk: all 13 registered jobs plus the repair step resolve. Positive control: moving DailyDeadlineScanJob.php away makes that same walk report MISSING, so the check can fail. TEST CLASS NAMES. Three of the four test classes are prefixed by the service name they cover, so the service replacement carried them; only TermijnbewakingEndToEndTest was an independent token and was renamed explicitly. The basename == class invariant was run over lib/ AND tests/ this time — the previous slice (#804) ran it over lib/ only and missed exactly this class of defect. VERIFIED - php -l clean on every changed file; info.xml parses. - phpunit, whole unit suite: 1777 tests, 6160 assertions, 5 skipped — green. - Every lib/ file this change touches is phpcs CLEAN. Pre-existing issues fixed along the way: docblock parameter-type alignment in the job and the repair step (shifted by the longer injected type names), and three missing @SPEC tags now anchored to canonical specs (termijn-escalation for the scan job, termijnbewaking-schemas for the seeder). - Residual grep over lib/, tests/, appinfo/ and src/: no live reference to any of the five old names. This caught two prose references in unrelated files (VthSeedDataRepairStep and KccWerkplekSeedDataService) that name the seeder in comments; both updated so the comments stay true.
…nts (#805) Closes the citizen-data exposure found by the gate-7 fleet re-audit (#801). Live-reproduced with two accounts before and after, status codes printed: GET /api/kcc/voorblad returned a citizen's contact history — caller phone number and free-text call summaries — at HTTP 200 to any authenticated account. 403 after; a KCC-role holder still gets 200. gate-7 reported 0 for this app throughout, and still does: .github#365 means it accepts a 401 authentication check as an authorisation guard. The evidence here is the live probe, not the gate cell. Guards at 33 call sites via the app's own CaseAccessGuard, plus a new CitizenLookupGuard for the citizen-lookup endpoints, which have no per-object owner to check against. DEPLOYMENT: CitizenLookupGuard denies until a 'kcc' group exists. 'beheerders' and 'admin' are attested group names in this app; 'kcc' and 'klantcontact' are assumptions, marked as such on the constant. Create the group or edit the constant. Also fixes a fatal in TenantMiddleware (IRequest::setParameter does not exist) that made every endpoint a 500 for any user with a tenant. Retractions: bezwaar/hearings attendance and dossier/export were never IDORs — they are dead endpoints (see #811, 24 sites testing is_array() on a ?ObjectEntity). Repairing those creates IDORs unless the guard lands first.
* refactor(procest): TermijnNotificationDispatchJob -> DeadlineNotificationDispatchJob A code-only slice of openspec/changes/english-vocabulary, and the first procest slice — chosen because it is one of the very few procest identifiers that is neither ZGW protocol vocabulary nor wired by string. 'termijn' here is a STATUTORY DEADLINE being monitored, not a contract term, so it becomes Deadline. That reading is app-specific on purpose: hrmq uses 'termijn' for notice periods (aanzegtermijn) and decidesk for terms of office (TermijnRegeling). Three apps, three meanings, one Dutch word — it must never become a fleet word. Referenced only via ::class from TermijnNotificationService, so a missed reference is a compile error rather than a job that silently never dispatches. No route entry, no register-fragment wiring, no baseline suppression keyed to the old path — all four checked, not assumed. TermijnNotificationService itself keeps its name in this commit; it belongs with the termijnbewaking slice, where the Termijn* schemas (termijnDefinitie, termijnInstance, termijnGebeurtenis) move together with their register fragments. Archived changes under openspec/changes/archive/ keep the old name — they are a record of what was built at the time. php -l clean; class name matches file name; use statement still ordered. * refactor(procest): TermijnReporting* -> DeadlineReporting* TermijnReportingController -> DeadlineReportingController and TermijnReportingService -> DeadlineReportingService, with their two Dutch controller methods (kwartaalrapport -> quarterlyReport, jaarrekening -> annualStatement), every Dutch parameter and local ($periode -> $period, $afdeling -> $department, $jaar -> $year, $jaarPrefix -> $yearPrefix), the two named-argument call sites, the three route names, the test, and the Postman collection's description. Stacked on feat/english-vocabulary-deadline-job (#797) because that branch already renames TermijnNotificationDispatchJob; branching from development would have conflicted. FOUR DUTCH CONTRACTS ARE DELIBERATELY LEFT ALONE, and each is annotated in place so the next reader does not "finish the job" and break it: 1. $row['afdeling'] is a SCHEMA PROPERTY. OpenRegister materialises it as a real column, so it moves with a data migration, not with a rename. 2. 'periode', 'afdeling' and 'jaar' as RESPONSE KEYS are the published shape of the reports. src/views/dashboard/TermijnDashboard.vue reads annual.jaar directly, so they move only with a coordinated frontend change. 3. getParam('periode') and getParam('jaar') are QUERY-STRING parameter names — the request contract. 4. The /api/termijn/* URLs. Route NAMES follow the renamed class and methods; URLs do not. Moving them is a breaking change for every consumer and is a separate decision. NAMED ARGUMENTS. resolveQuarter(periode:) and aggregateByType(afdeling:) are called with named arguments, which PHP resolves at call time — a parameter rename that misses one is a runtime fatal that `php -l` reports as clean. Both moved with their declarations. VERIFIED - php -l clean on all four PHP files; the Postman collection still parses. - phpunit: 5 tests, 14 assertions green. - Route resolution: each 'deadlineReporting#<action>' route name was checked against the controller's actual public methods — all three resolve. Positive control: the same check against the OLD action names (kwartaalrapport, jaarrekening) reports MISSING METHOD, so it can distinguish a renamed route from a dangling one. - Residual grep over lib/, tests/, appinfo/ and src/: no live reference to either old class name or the old route prefix. PHPCS SCOPE, MEASURED RATHER THAN ASSUMED. phpcs.xml declares <file>lib</file> and nothing else, so tests/ and appinfo/ are outside the standard entirely — appinfo/routes.php carries 373 violations on development and every sibling test file is equally "dirty". Both files this change touches inside lib/ are CLEAN, including two pre-existing missing-@SPEC errors fixed here (each now anchors to openspec/specs/termijn-reporting/spec.md, the canonical spec, not the change directory the file headers point at). An earlier pass ran phpcbf over the test file and produced a 94-line diff for what is a 6-line rename. That was churn against a gate that never reads the file, so it was reverted; the test diff is now the rename alone. * refactor(procest): Termijn{Escalation,Pause,Extension}Service + listener -> Deadline* Four more classes in the deadline cluster: TermijnEscalationService -> DeadlineEscalationService TermijnPauseService -> DeadlinePauseService TermijnExtensionService -> DeadlineExtensionService TermijnCaseCreatedListener -> DeadlineCaseCreatedListener plus the test file, across 11 files: the DI listener registration, the daily scan job and service, TermijnController, and three test suites. Stacked on feat/english-vocabulary-deadline-reporting (#803), which is itself stacked on #797. Merge order: 797 -> 803 -> this. THE ONE DEFECT THIS SLICE PRODUCED, AND WHAT CAUGHT IT. Renaming the test FILE TermijnPauseExtensionServiceTest.php left the class inside it still called TermijnPauseExtensionServiceTest — neither of the two service-name replacements matched that token, because it is a third name. PHPUnit refused to load it ("Class DeadlinePauseExtensionServiceTest cannot be found"). The basename == class invariant I ran before committing covered only the four lib/ files and would have missed it; it now covers the tests too. Same failure mode as hrmq's provider discovery, reached from the opposite direction. LISTENER WIRING IS SAFE BY CONSTRUCTION. WorkflowListenerRegistrar registers the listener as TermijnCaseCreatedListener::class, not as a string literal, so the rename is resolved at compile time rather than silently failing to match at runtime. Checked before renaming rather than after. VERIFIED - php -l clean on every changed file. - phpunit, whole unit suite: 1777 tests, 6160 assertions, 5 skipped — green. - Every lib/ file this change touches is phpcs CLEAN. Three pre-existing issues were fixed along the way: docblock parameter-type alignment in TermijnController and TermijnDailyScanService (shifted by the longer type names), and two missing class-level @SPEC tags. Those now anchor to canonical specs — termijn-binding + termijn-pause-extension for the controller, which spans both, and termijn-escalation for the daily scan — rather than to the change directories the file headers still point at. - Residual grep over lib/, tests/, appinfo/ and src/: no live reference to any of the four old names. phpcs.xml scopes to <file>lib</file> only, so tests/ and appinfo/ are outside the standard; no formatting was applied to them. * refactor(procest): rename the daily-scan and seed-data deadline classes TermijnDailyScanService -> DeadlineDailyScanService TermijnbewakingSeedDataService -> DeadlineMonitoringSeedDataService DailyTermijnScanJob -> DailyDeadlineScanJob SeedTermijnbewakingData -> SeedDeadlineMonitoringData TermijnbewakingEndToEndTest -> DeadlineMonitoringEndToEndTest Stacked on feat/english-vocabulary-deadline-services (#804). Merge order: 797 -> 803 -> 804 -> this. TWO OF THESE ARE WIRED IN appinfo/info.xml, NOT BY IMPORT. The job is declared as <job>OCA\Procest\BackgroundJob\DailyTermijnScanJob</job> and the seeder as <step>OCA\Procest\Repair\SeedTermijnbewakingData</step>. These fail differently from each other, which is why both were checked rather than assumed: - an unresolvable JOB is silently reaped. Nextcloud's JobList::buildJob() logs "failed to create instance of background job" and calls removeById(), so the daily termijn sweep would simply stop running with no error surface. - an unresolvable REPAIR STEP throws during upgrade. Both entries move here. Verified by walking info.xml and resolving every class it names to a file on disk: all 13 registered jobs plus the repair step resolve. Positive control: moving DailyDeadlineScanJob.php away makes that same walk report MISSING, so the check can fail. TEST CLASS NAMES. Three of the four test classes are prefixed by the service name they cover, so the service replacement carried them; only TermijnbewakingEndToEndTest was an independent token and was renamed explicitly. The basename == class invariant was run over lib/ AND tests/ this time — the previous slice (#804) ran it over lib/ only and missed exactly this class of defect. VERIFIED - php -l clean on every changed file; info.xml parses. - phpunit, whole unit suite: 1777 tests, 6160 assertions, 5 skipped — green. - Every lib/ file this change touches is phpcs CLEAN. Pre-existing issues fixed along the way: docblock parameter-type alignment in the job and the repair step (shifted by the longer injected type names), and three missing @SPEC tags now anchored to canonical specs (termijn-escalation for the scan job, termijnbewaking-schemas for the seeder). - Residual grep over lib/, tests/, appinfo/ and src/: no live reference to any of the five old names. This caught two prose references in unrelated files (VthSeedDataRepairStep and KccWerkplekSeedDataService) that name the seeder in comments; both updated so the comments stay true. * feat(repair): migrate procest's Dutch deadline columns to English Adds RenameDutchDeadlineColumns, the data-migration half of procest's English vocabulary slice. No property is renamed in this commit — the migration lands first, because TermijnService and TermijnController cannot be renamed until the stored data can follow them. Stacked on feat/english-vocabulary-deadline-scan (#806). Merge order: 797 -> 803 -> 804 -> 806 -> this. WHY A MIGRATION IS NEEDED AT ALL. OpenRegister does not store an object as a JSON blob keyed by property name. Each schema property is a real, snake_cased COLUMN in oc_openregister_table_{register}_{schema}. MagicMapper ADDS a column on sync and never renames. A register-only rename therefore leaves the data in the Dutch column while every read looks at the English one and finds null. THIS APP HAS TWO REGISTERS, AND THE OBVIOUS SCOPING MISSES ONE. Measured on the reference install: `procest` holds 1051 rows and `procest-default` holds 107. The sibling step in decidesk resolves a single exact slug. Applying that shape here migrates 40 columns and silently leaves 29 behind — measured both ways: exact slug = 'procest' -> 40 columns slug LIKE 'procest%' -> 69 columns so the register set is resolved by PREFIX and every match is migrated. The failure that was avoided is the worst kind available here: a step that reports success having covered only part of the data. zaaktype IS EXEMPT AND DELIBERATELY ABSENT FROM THE MAP. It is the field name in the statutory ZGW wire format this app both consumes and emits, so it is exempt under the fleet rule — even though at 14 shard tables it is the second most widespread Dutch column here, which is exactly why leaving it out needed saying rather than looking like an oversight. COLLISIONS ARE REFUSED, NOT MERGED. `omschrijving` and `beschrijving` both mean `description`. Measured: they do not co-occur in any shard table on this install, so the guard is a safety net rather than an active path. A later fragment could introduce a pair, and a silent merge would destroy one of two values, so the step migrates neither and logs. SOFT-DELETED ROWS ARE MIGRATED TOO. The back-fill does not filter on `_deleted` — a restored row must not come back with a null subject. 36 non-null values exist across the mapped columns, 35 of them live. VERIFIED - php -l clean; info.xml parses; phpcs CLEAN under the app's standard, including its @SPEC anchor requirement. - Dry run of the step's exact resolution: 40 renames under `procest`, 29 under `procest-default`, 0 back-fills, 0 ambiguous. zaaktype untouched on all 14 tables carrying it. NOT VERIFIED. The step has not been executed against a database — only its resolution and its statement set were computed. It runs on the next upgrade; its idempotence and its back-fill branch are reasoned from the code and the sibling steps, not observed. Say so rather than let "migration verified" stand in for it. * fix(repair): use information_schema, not IDBConnection introspection phpstan failed this branch with three errors, all in the new repair step: Call to an undefined method OCP\IDBConnection::getPrefix(). Call to an undefined method OCP\IDBConnection::getSchema(). (x2) They are real. Read from the running server's own lib/public/IDBConnection.php, the interface exposes getQueryBuilder, getTypedQueryBuilder, getError, getDatabasePlatform, getDatabaseProvider, getShardDefinition and getCrossShardMoveHelper — and nothing else beginning with "get". Both methods I called exist on the concrete OC\DB\Connection, not on the OCP interface this step is typed against. WHY EVERY OTHER CHECK PASSED. `php -l` parses a call to a method that does not exist, and phpcs is a style tool. A nonexistent method on an injected interface is invisible to both. Only phpstan sees it, and I did not run phpstan locally before opening the PR — I described the step as verified on the strength of lint + phpcs + a SQL dry run, none of which touch the PHP API surface. The dry run in particular measured what the STATEMENTS would do, which is exactly why it looked convincing while the code that would issue them could not run at all. THE FIX follows openregister's own RegisterService::magicTableNames(), which solves the same problem: query information_schema and anchor the match on the `openregister_table_` MARKER rather than a computed prefix. That file documents why the obvious alternative fails — getQueryBuilder()->getTableName('') returns the literal `*PREFIX*` placeholder, resolved only when a query executes through the NC DB layer, which a raw information_schema string never is; a LIKE built from it matches zero tables and silently reports every register empty. Column introspection moves to information_schema.columns for the same reason. Matching is tightened while here: the suffix after `openregister_table_<id>_` must be all digits, so register 17 cannot match register 170's shard tables. VERIFIED - phpstan: [OK] No errors, whole-project run, same command as CI. - php -l clean; no `db->getSchema()` or `db->getPrefix()` call remains. THE SAME DEFECT IS IN FOUR SIBLING PRs, all authored today and all failing phpstan for this identical reason: openbuild#176, opencatalogi#850, decidesk#467 and softwarecatalog#488. Each needs this same change. None can merge while red, so nothing shipped — but each of their PR bodies claims verification that never covered the API surface. * style(repair): split shardTables() and drop the short variable phpmd flagged two things the information_schema rewrite introduced: CyclomaticComplexity shardTables() is at 10; the threshold is 10. ShortVariable $at is below the 3-character minimum. The marker-matching loop moves into isShardOf(), which fixes both: the extraction takes shardTables() back under the threshold, and $at becomes $offset. Behaviour is unchanged — same marker anchoring, same digits-only suffix check that keeps register 17 from matching register 170's tables. Also capitalises one inline comment, which phpcs requires. ALL THREE GATES RUN LOCALLY THIS TIME, which is the point. The original defect in this file shipped because I ran php -l and phpcs and called it verified; phpstan was the only gate that could see a call to a nonexistent interface method, and I had not run it. This round: phpcs CLEAN phpstan [OK] No errors phpmd 0 findings on this file * test(repair): cover the migration's scoping decisions The PHPUnit job was failing on the COVERAGE RATCHET, not on a test: Coverage current: 30.52% (14092/46169 statements) Coverage merge base: 30.55% (14000/45824 statements) FAIL: coverage dropped by 0.03% against the merge base. This change adds 345 statements. Adding code without tests drops coverage. Both suite legs reported 1803 and 1777 tests with zero failures, so "PHPUnit FAILED" here meant "you added untested code" — which was true. The migration shipped with no test at all. These ten tests pin the two decisions that determine what the step touches — which shard tables are in scope, and when a rename is refused — plus two invariants that are load-bearing but were only asserted in prose: that `zaaktype` stays out of the map because it is the statutory ZGW wire name, and that every destination is snake_case (MagicMapper DROPS a camelCase column whose snake_case twin exists, so a camelCase destination would be deleted). The DDL/DML paths are deliberately not unit-tested: they need a live database. A POSITIVE CONTROL FOUND A FALSE CLAIM IN MY OWN COMMENT. The first version of the shard-matching test asserted that register 17 must not match register 170's tables, quoting the comment above the guard. Deleting the ctype_digit guard and re-running left that test GREEN — it could not fail, because the marker already ends in '_', so `openregister_table_17_` is not a substring of `openregister_table_170_85`. The trailing underscore handles 17-vs-170; the digits check guards something else entirely: derived tables like `…_17_85_backup` and non-shards like `…_17_audit`, both of which contain the marker and would otherwise receive an ALTER TABLE. The test now asserts that case and goes RED when the guard is removed. The code comment, which stated the wrong reason, is corrected in the same commit. VERIFIED - phpunit: 10 tests, 24 assertions green; whole unit suite 1787/6184. - Positive control: guard removed -> 1 failure; restored -> 10 pass. - phpcs clean, phpstan [OK] No errors.
…dy called (#794) (#795) * fix(admin): implement the seven admin routes four settings tabs already called (#794) Four admin settings tabs were entirely non-functional: Mandate Matrix Administration, Mandate Matrix System Settings, AWB Term Definitions and Consultation Management. They called procest API URLs that appinfo/routes.php never declared. Nextcloud answers an unmatched app URL with its own HTML page under HTTP 200, so nothing threw and nothing logged. Live-probed before the fix, printing the content-type rather than the status — all seven returned 200 text/html at 40,061 bytes, byte-identical to a nonsense control URL, while /api/settings and /api/vth/checklists returned application/json. Only ChecklistsTab (#784) was loud, because it used the string-admitting `x?.results || x || []`. The other seven use the correct guard, `Array.isArray(x) ? x : (x?.results || [])`, which properly discards the HTML string and thereby turned the identical defect into a silent empty tab. The guard is right and stays; a working guard masked a broken caller. What this adds - MandaatRegistryController — GET besluiten, POST/PATCH mandaten. - OrganisatieRolController — full CRUD for rollen (with the delete guard) and toewijzingen. Split from the above on the domain boundary; one class holding all four registries exceeded the public-method budget. - TermijnDefinitieController — GET/POST/PATCH definities (REQ-TERM-ADMIN-001). - ConfiguredRegistryService — generic list/save/delete over a config-key-named schema, with no authorization of its own, so every caller must carry a guard. - MandaatRegistryService — the referential-integrity guard the spec requires: a role held by a Mandaat or an active assignment refuses deletion with 409. - The two settings tabs now POST the app's own guarded /api/settings, their keys are registered in CONFIG_KEYS, and AdminSettings provides the initial state both tabs were already calling loadState() for but nothing supplied. Authorization posture, checked deliberately Every new method carries #[AuthorizedAdminSetting]. Repointing the frontend at OpenRegister's generic object route also resolves and also removes the symptom — that was the retracted first fix for #784, and it bypasses procest's admin authorization. Verified against a live instance: non-admin gets 403 on all seven new routes, while the @NoAdminRequired control still returns 200, so the probe distinguishes rather than uniformly failing. Pre-existing bugs found and fixed while verifying - InspectionChecklistService::deleteChecklist passed `id:` to ObjectService::deleteObject, whose parameter is `$uuid`. Every call raised "Unknown named parameter $id", was swallowed by the catch, and returned false — the admin Delete button 500'd on every checklist. Confirmed live before and after. - MandaatToewijzingenTable sent `{ totEnMet }` to end an assignment. The schema field is `validUntil`, and saveObject is PUT-semantic, so that payload would have nulled userId, rolId, toewijzingType and validFrom off the record. - An earlier draft of the delete guard checked organisatieRol/rol/rolId only. The Mandaat schema names the reference `gemandateerdeRol`, so the guard would have failed OPEN for every mandate. Read from the shipped schemas, not guessed. Verified on an isolated seeded rig (:8091): create/list/update/delete round trips for all four registries; the delete guard refuses a referenced role (409, naming what blocks it) and permits an unreferenced one (200); settings persist across a container restart; and an unregistered key is dropped by the CONFIG_KEYS allowlist, which is what makes registering the new keys load-bearing. composer check:strict: ALL CHECKS PASSED (1791 tests). eslint 0 errors. USE_LOCAL_LIB=false npm run build: exit 0. Refs #784, #786, #792 * test(admin): cover the new registry surfaces; add the two missing l10n keys Two CI failures on this PR, both real and both mine. l10n coverage / test:l10n — the two new error strings ('Saving failed', 'Saving failed ({status})') were not in l10n/en.json. Extracted with check-l10n.js --write and translated on the nl.json side, so no Dutch-locale string falls back to the raw English key. Coverage ratchet — 1777 tests passed and the PHPUnit cell still failed, which is the ratchet, not a broken test: the change added 261 statements and coverage fell 0.1% against the merge base. Fixed by covering the new code rather than by touching the threshold. - ConfiguredRegistryServiceTest — list/save/delete, both the numeric and the slug resolution paths. Two are regression guards worth keeping: delete must pass the identifier as `uuid` (passing `id:` is what made InspectionChecklistService::deleteChecklist 500 on every call), and on the numeric path register/schema must be NESTED under `@self` (a top-level filter silently returns an empty set, which reads as 'there is no data' rather than as a malformed query). - MandateRegistryControllersTest — the three controllers: status codes, schema routing, 409-with-a-reason on a referenced role, 422 vs 500, and that a client-supplied `id` in the body cannot override the routed one. Discovered while writing these: a live instance stores NUMERIC register/schema ids (register 14, organisatie_rol_schema 153), so `searchObjects()` — not the slug bridge — is the production path. The first draft of the test stubbed only the slug bridge and returned an empty set, which is exactly the failure this code has to survive.⚠️ The ratchet could not be reproduced locally: no code-coverage driver is installed here ('No code coverage driver available'), so CI is the authority on whether this clears it.
…ec (#786) (#798) * feat(inspection): build ChecklistService, REQ-003 of the canonical spec (#786) REQ-003 of `openspec/specs/inspection-checklists/spec.md` was never built. This adds it: `OCA\Procest\Service\ChecklistService`, with the four methods the requirement names — `completeItem()`, `getProgress()`, `validateCompletion()` and `getConformitySummary()` — all pure with respect to the checklist payload, so the same calculation runs server-side, in a dry-run preview, and in a unit test.⚠️ I did NOT write the requirement. REQ-001, REQ-002 and REQ-003 already exist verbatim in the canonical spec under `## Execution Surface (retrofit)`, and the spec's frontmatter lists them under `retrofit_extensions`. The implementation follows that text rather than the other way round.⚠️ This is deliberately NOT a rename of `lib/Service/Inspection/ChecklistService.php`. The spec's own Notes block keeps the two apart on purpose — "the top-level service handles per-run progress, the namespaced one handles templates. Consolidation deferred to a future refactor change." Merging them would contradict the spec, not tidy it. Consistency with the existing implementation Answer semantics are taken from `Inspection\ChecklistService` so the two can never disagree about which answer is a failure: `ja_nee_nvt` → `nvt` is not-applicable and `nee` is non-conforming; `fotoRequired` is the tri-state `nooit`/`bij_nee`/`altijd`, not a boolean; an item's id is `id`, else `order`, else its position. Those SHAPE rules moved into `ChecklistPayloadReader` so they are stated once rather than restated per consumer — and so `ChecklistService` stays under the class-complexity budget. Two decisions worth reviewing rather than assuming - A response row that exists but carries no usable value is NOT an answer. Counting it would let a run of blank required items report itself complete. - `completeItem()` enforces the photo gate at answer time, not only at submission. REQ-001's scenario requires the item to REMAIN INCOMPLETE when a mandatory photo is missing, which validating only at submission would not do. Tests: 21 cases, including negative controls that would pass against a wrong implementation — an optional item may be left blank (a validator flagging every unanswered item would also satisfy the "reports both required items" test), an orphan response does not inflate progress, and an empty checklist is 100% rather than a division by zero. Still outstanding on #786, deliberately not attempted here: REQ-001 (`InspectionController`) and REQ-002 (`InspectionService`). REQ-001 delegates to this service, so it is now unblocked. composer lint/phpcs/phpmd/psalm/phpstan: clean. PHPUnit: 1793 tests, 6192 assertions, 0 failures. Refs #786 * test(inspection): declare @uses on the payload reader and give it its own suite The PHPUnit cells failed with 1793 tests passing and 8 RISKY — not the coverage ratchet and not a broken test. phpunit.xml sets `beStrictAboutCoverageMetadata="true"`, and ChecklistServiceTest declared only `@covers \OCA\Procest\Service\ChecklistService` while every case also executes ChecklistPayloadReader, which the service composes directly (REQ-003 requires it to be constructible with no dependencies, so the reader cannot be injected and mocked away). Fixed the way this repo already handles it — DeelzaakServiceTest carries the same `@uses` for CaseObjectReader for the same reason — plus a real `@covers` suite for the reader rather than leaving it covered only by association.⚠️ I did not catch this locally because every local run used `--no-coverage`: the risky-test check only fires when coverage is collected, and there is no coverage driver installed in this environment. A green `--no-coverage` run is not evidence about the cells that failed. ChecklistPayloadReaderTest also pins one thing worth keeping: PHP coerces a numeric-string array key to an int, so an item keyed by `order: 4` lands under int 4. Lookups coerce the same way so behaviour is correct, but an assertSame on array_keys() does not see what you wrote — the first draft of that test asserted the wrong thing and failed. phpcs 0 errors. 30 tests, 61 assertions pass. * fix(test): stop PHPUnit parsing my explanatory prose as a coverage annotation The @uses fix worked — 8 RISKY became 0 — and introduced 16 PHPUnit warnings, which fail the cell just as hard, with all 1802 tests passing: "@Covers suite" is invalid The docblock prose explaining the fix contained a literal coverage-annotation token followed by the word 'suite'. PHPUnit scans the whole docblock and matches those tokens MID-LINE, not just at the start of one, so it dutifully registered a coverage target literally named 'suite'. Rewritten to describe the annotations without naming them.⚠️ The first attempt at THIS fix reintroduced the bug, because the warning I wrote about the token contained the token. 🔑 Same family as the fleet's gate-19/gate-26 prose-parsing defect (.github#358): an explanatory comment is not inert, and documenting a parser-driven defect can re-trigger the defect you are documenting.
…d is_array() on show() (#811) (#814) * fix(security): per-object authorisation for the four dwangsom endpoints (#811) All four DwangsomController verbs are @NoAdminRequired and carried no per-object check. The private method they called was named ensureAuthenticated() and documented as a "per-object authorization guard"; it only established that somebody was logged in. Measured on a live rig, owner and attacker arms, before this change: the attacker column was byte-identical to the owner column on all four endpoints, so nothing was refusing anybody on the basis of the object. A DwangsomBerekening has no owner field. It reaches a case through termijnInstance -> zaak, so OwningCaseResolver gains a two-hop resolveVia() and the decision is delegated to CaseAccessGuard, read access for the GET and mutation access for the three writes. Every unresolvable link denies. This lands the guard BEFORE the is_array() predicate is repaired. Those endpoints are dead today (see the follow-up commit), so repairing them first would have converted four dead endpoints into live unguarded ones, two of which mutate penalty amounts. * fix(dwangsom): repair the is_array() predicate on show(), now that it is guarded (#811) ObjectService::find() is declared `: ?ObjectEntity` and never returns an array, so `is_array($row) === false` was true for every existing berekening and GET /api/termijn/dwangsom/{id} answered 404 to everyone — its own case assignee included. Measured on a live rig against an object created and read back through the OpenRegister API. Normalised through jsonSerialize() instead, using is_callable() rather than method_exists(): ObjectEntity reaches accessors through Entity::__call(), for which method_exists() is false. Deliberately scoped to the read verb. beschikking(), bezwaar() and bezwaarHeroverweging() are dead for the same reason but their post-repair behaviour changes penalty amounts, and that is not something to settle from here. They now carry the guard, so repairing them later cannot ship an IDOR. The tests use a double that returns an ENTITY rather than an array — an array-returning double would pass against the broken predicate too. Negative control: restoring the old predicate fails exactly one test, testAuthorisedCallerGetsTheBerekeningNormalisedFromAnEntity, with 'Failed asserting that 404 is identical to 200'.
…nlink (#793) (#796) * fix(deelzaak): stop unlinkSubCases reporting success over a partial unlink (#793) `unlinkSubCases()` is a synchronous fan-out of `saveObject()` on the request path — the openregister#2420 shape, invisible to gate-61 because that gate inspects post-event listeners and this is a plain service seam.⚠️ The remedy the issue proposes first — defer the whole thing to a background job — cannot be applied as-is, and that is the main finding here. `DeelzaakDeleteWarningModal::confirmDelete()` awaits this call and then deletes the parent. Enqueue-and-return would delete the parent while the children still point at it, so the "orphans survive" guarantee (REQ-DZS-006-B) would be broken by the very change meant to protect it. Deferral needs the parent delete moved into the same job; that is left to #793 and deliberately NOT done here. What IS fixed, because it is a correctness bug rather than a performance one: the implementation took `listSubCases()`'s `_limit => 200` page and reported plain success. A parent with more than 200 sub-cases had the remainder silently left linked, and the caller then deleted the parent — orphaning them under a dead reference while the API answered `200 OK` with a count that under-reported. Per-record failures were swallowed the same way. Changes - `unlinkSubCases()` pages to exhaustion and returns `{unlinked, failed, total, complete}`. A `$seen` set makes each sub-case count exactly once: a record whose save fails stays in the filter's result set, so without it the loop both spins forever and double-counts. Extracted the per-page work into `unlinkPage()` to keep the complexity within budget. - `DeelzaakController::unlink()` answers `207 Multi-Status` when the unlink is incomplete, so a partial result is distinguishable by status code alone. - `deelzaakApi.unlinkSubCases()` returns the whole result. `complete` defaults to false when the field is absent, so an older server that still answers with a bare count blocks the delete rather than silently permitting the failure mode this change closes. - `DeelzaakDeleteWarningModal` deletes the parent ONLY when `complete` is true, and otherwise tells the user how many sub-cases could not be detached. Tests `DeelzaakUnlinkSubCasesTest` — 6 cases over a stub that pages the way OpenRegister does. Includes the negative control (no sub-cases → complete zero, without which an always-incomplete implementation would pass the failure tests), the 450-records-over-a-200-page regression, and the all-failures case that hangs the suite if the no-progress guard is removed. That last test caught a real defect in my first implementation: failed records were re-queried on the next page and double-counted, reporting `failed: 2` for one failing record. composer check:strict: ALL CHECKS PASSED (1791 tests). vitest 13 passed. eslint 0 errors. USE_LOCAL_LIB=false npm run build: exit 0. Refs openregister#2420 * test(deelzaak): cover unlink() status reporting; fix gate-16 and l10n Three CI failures on this PR, all real and all mine. Each reproduced locally before fixing, and re-measured after. gate-16 spec-coverage — `deelzaakApi.js::unlinkSubCases` had a JSDoc but no `@spec` anchor. Reproduced against the canonical gate package (.github @81c8c97) with an explicit base: `HYDRA_GATE_BASE_REF=origin/development` → `count=1` naming that method, `count=0` after. Anchored at the existing 'Sub-case deletion protection' requirement, which already says the system MUST clear parentCase on ALL child cases — I did not write a requirement to justify the change; the requirement is what the change implements. l10n coverage / test:l10n — the new partial-unlink error string was missing from en.json and nl.json. Extracted and translated; the check now reports both key sets matching. Coverage ratchet — the PHPUnit cells failed with no failing test, which is the ratchet, not a broken suite. Covered the new controller code rather than touching the threshold: DeelzaakUnlinkControllerTest asserts 200 on a complete unlink, 207 on a partial one, 401 for an anonymous caller, and a clean 200 when there are no sub-cases. The 200 case is the negative control — without it, a controller that always answered 207 would satisfy the 207 test on its own.⚠️ The ratchet could not be reproduced locally: no code-coverage driver is installed here, so CI is the authority on whether this clears it. phpcs 0 errors, phpmd clean, PHPUnit 10 new tests pass, vitest 13 pass. * test(deelzaak): declare @uses so the unlink paging test is not reported risky phpunit.xml sets beStrictAboutCoverageMetadata="true". DeelzaakUnlinkSubCasesTest declared only @Covers ::unlinkSubCases, but the paging loop calls listSubCases() and the service is built with a REAL CaseObjectReader (as DeelzaakServiceTest does, for the same reason) — so both execute without being the subject and every case would be reported RISKY, failing the PHPUnit cell with no failing test. Caught by the identical failure on the sibling REQ-003 branch rather than by waiting for this PR's cells to come back red.⚠️ Not reproducible locally: the risky-test check only fires when coverage is collected, and there is no coverage driver in this environment, so every local run (--no-coverage) is silent about it. * fix(test): stop PHPUnit parsing my explanatory prose as a coverage annotation Same defect the sibling REQ-003 branch hit, fixed here before this PR's cells reported it: the docblock prose explaining the @uses declarations contained a literal coverage-annotation token followed by the word 'so'. PHPUnit matches those tokens MID-LINE anywhere in a docblock, so it registered a coverage target literally named 'so' and failed the cell with an 'is invalid' warning while every test passed. Rewritten to describe the annotations without naming them. 🔑 Same family as the fleet's gate-19/gate-26 prose-parsing defect (.github#358): an explanatory comment is not inert. Checked the rest of tests/ for the shape — the three pre-existing occurrences are backtick-wrapped and demonstrably do not warn (CI's 16 warnings were all from the one file). * fix(test): declare coverage at CLASS scope so the cells stop reporting risky Third and final shape of this one. After the prose fix the cell still failed: `Tests: 1787 … Risky: 5`, naming DeelzaakController and DeelzaakService as 'executed code that is not listed as code to be covered or used'. Cause: I declared coverage METHOD-scoped (::unlink, ::unlinkSubCases). Under beStrictAboutCoverageMetadata, constructing the subject runs `__construct` — and for the service also the SearchesObjects trait — none of which IS the named method, so they count as undeclared even though the collaborators were declared. Fixed by matching the convention already used throughout tests/Unit: DeelzaakServiceTest and InspectionChecklistControllerTest both declare at class scope. This also explains why the sibling #794 branch never hit it — its tests were class-scoped from the start, and its ConfiguredRegistryService trait usage was attributed to the class rather than reported. 🔑 Method-scoped coverage metadata is strictly harder to satisfy than class-scoped, and the difference only shows up when coverage is collected — which this environment cannot do.
…cision (#815) Applies the ratified besluit = decision equivalence to procest's mandate schema: the `mandateringsBesluit` schema (key, slug, title, description) becomes `mandateDecision`, with five of its properties and the cross-schema reference that names it. besluitNummer -> decisionNumber besluitNaam -> decisionName inWerkingtreding -> effectiveFrom vervalDatum -> expiryDate vorigBesluit -> previousDecision mandaat.mandateringsBesluit -> mandaat.mandateDecision THERE WAS NEVER A SCHEMA MERGE TO DO. This slice was previously described as blocked because procest "already declares a decision schema, so renaming Besluit onto it is a merge". Measured: procest declares exactly one `decision` schema, already fully English with twelve English properties, and there is no bare `Besluit` schema anywhere to collide with it. What carries the word is two differently-named schemas and five properties. The premise was wrong. WHAT IS DELIBERATELY NOT RENAMED `wettelijkeGrondslag` stays. It is owned by FIVE other procest schemas (dwangsomUitbetaling, mandaat, subsidieBeschikking, termijnDefinitie, terugvordering) that still declare the Dutch spelling, and the column migration is REGISTER-scoped: renaming it here would migrate their data out from under their own declarations. A shared property half-renamed is worse than one not renamed at all. It moves fleet-wide, with them, in one slice. `legalBasis` already exists on another procest schema, which is a second reason not to introduce a partial second source for it. The `status` enum VALUES (concept / vastgesteld / vervallen) stay. Enum values are stored data, not identifiers — rewriting them rewrites every row. Same call as openconnector#1213. The SchemaSlugMap VALUE stays `mandaterings_besluit_schema`. Only the key is the slug; the value is the app-config key under which this schema's numeric id is already stored on every existing install. Renaming it would orphan that id and the schema would silently resolve to nothing. The import endpoint accepts BOTH spellings. Its request-body keys are a published contract; the English names are preferred and the Dutch ones still read, so a client that has not been updated does not break. MIGRATION. Six columns added to the existing register-scoped repair step. Each was verified to be owned by exactly one schema before being added — that check is what kept wettelijke_grondslag out, and it is the same hazard decidesk's step avoids by scoping to a register, only here it is within one app. THREE PRE-EXISTING UI BUGS FOUND, DELIBERATELY NOT "FIXED" BY RENAMING - MandaatEditor.vue binds form.inWerkingtreding onto a MANDAAT, which declares validFrom/validUntil and has no such property. - MandaatMatrixTable.vue reads b.vervaldatum — lowercase d — matching no schema property at all. - BevoegdhedenPanel.vue reads both off API rows whose shape is not verifiable without running it. Renaming these would have cemented bindings that already point at nothing. Only MandaatMatrixWidget.vue, which reads the renamed reference property, was updated. The three need their own investigation. VERIFIED - phpunit: 1867 tests, 6346 assertions green (5 skipped, same as baseline). - php -l clean; the fragment parses as JSON. - Named-argument parity: findPriorBesluit -> findPriorDecision, parameter besluitNummer -> decisionNumber, both call sites moved and diffed against the declaration. - phpcs: 0 errors on every file this change touches except MandaatImportServiceTest, which carries 46 — identical count on the unmodified file, measured by stashing.
…l six owners (#816) * refactor(mandaat): rename the mandateringsBesluit schema to mandateDecision Applies the ratified besluit = decision equivalence to procest's mandate schema: the `mandateringsBesluit` schema (key, slug, title, description) becomes `mandateDecision`, with five of its properties and the cross-schema reference that names it. besluitNummer -> decisionNumber besluitNaam -> decisionName inWerkingtreding -> effectiveFrom vervalDatum -> expiryDate vorigBesluit -> previousDecision mandaat.mandateringsBesluit -> mandaat.mandateDecision THERE WAS NEVER A SCHEMA MERGE TO DO. This slice was previously described as blocked because procest "already declares a decision schema, so renaming Besluit onto it is a merge". Measured: procest declares exactly one `decision` schema, already fully English with twelve English properties, and there is no bare `Besluit` schema anywhere to collide with it. What carries the word is two differently-named schemas and five properties. The premise was wrong. WHAT IS DELIBERATELY NOT RENAMED `wettelijkeGrondslag` stays. It is owned by FIVE other procest schemas (dwangsomUitbetaling, mandaat, subsidieBeschikking, termijnDefinitie, terugvordering) that still declare the Dutch spelling, and the column migration is REGISTER-scoped: renaming it here would migrate their data out from under their own declarations. A shared property half-renamed is worse than one not renamed at all. It moves fleet-wide, with them, in one slice. `legalBasis` already exists on another procest schema, which is a second reason not to introduce a partial second source for it. The `status` enum VALUES (concept / vastgesteld / vervallen) stay. Enum values are stored data, not identifiers — rewriting them rewrites every row. Same call as openconnector#1213. The SchemaSlugMap VALUE stays `mandaterings_besluit_schema`. Only the key is the slug; the value is the app-config key under which this schema's numeric id is already stored on every existing install. Renaming it would orphan that id and the schema would silently resolve to nothing. The import endpoint accepts BOTH spellings. Its request-body keys are a published contract; the English names are preferred and the Dutch ones still read, so a client that has not been updated does not break. MIGRATION. Six columns added to the existing register-scoped repair step. Each was verified to be owned by exactly one schema before being added — that check is what kept wettelijke_grondslag out, and it is the same hazard decidesk's step avoids by scoping to a register, only here it is within one app. THREE PRE-EXISTING UI BUGS FOUND, DELIBERATELY NOT "FIXED" BY RENAMING - MandaatEditor.vue binds form.inWerkingtreding onto a MANDAAT, which declares validFrom/validUntil and has no such property. - MandaatMatrixTable.vue reads b.vervaldatum — lowercase d — matching no schema property at all. - BevoegdhedenPanel.vue reads both off API rows whose shape is not verifiable without running it. Renaming these would have cemented bindings that already point at nothing. Only MandaatMatrixWidget.vue, which reads the renamed reference property, was updated. The three need their own investigation. VERIFIED - phpunit: 1867 tests, 6346 assertions green (5 skipped, same as baseline). - php -l clean; the fragment parses as JSON. - Named-argument parity: findPriorBesluit -> findPriorDecision, parameter besluitNummer -> decisionNumber, both call sites moved and diffed against the declaration. - phpcs: 0 errors on every file this change touches except MandaatImportServiceTest, which carries 46 — identical count on the unmodified file, measured by stashing. * refactor(procest): rename wettelijkeGrondslag to legalBasis, all six owners Renames the property across EVERY schema that declares it, in one change: dwangsomUitbetaling, mandaat, mandateDecision, subsidieBeschikking, termijnDefinitie and terugvordering — plus the seed data, six services, three Vue files and the affected test fixtures. WHY ALL SIX AT ONCE. This was deliberately held out of the mandate-decision slice. The column migration is REGISTER-scoped: it walks every shard table in the procest registers and renames any column whose name is in its map. Renaming `wettelijkeGrondslag` for one schema would therefore have moved the data of the other five out from under declarations that still used the Dutch name — a silent null-read on each. A shared property is renamed for all its owners or for none. With all six moved, `wettelijke_grondslag => legal_basis` is safe to add to the map, and it is added here. MY FIRST MEASUREMENT OF THE FOOTPRINT WAS SHORT. It ran against the shared checkout, where TermijnReportingService.php had not yet been renamed to DeadlineReportingService.php by #806, so that consumer did not appear. Three Vue files were missed the same way. The residual grep after editing is what surfaced them; the count went from 27 references to 40. NOT EVERY VUE BINDING WAS REAL. The sibling slice found MandaatEditor binding `inWerkingtreding` onto a mandaat that has no such property. This one is different and was checked rather than assumed: `mandaat` genuinely declares wettelijkeGrondslag, so its editor field, its validator and the matrix widget's legalLink() all bind something that exists, and all three move. THE CSV HEADER STAYS READABLE. MandaatImportService reads the legal basis from an operator-supplied CSV column. That header is an external input format, so the new spelling is preferred and the old one still accepted — an existing import file does not break. VERIFIED - phpunit: 1867 tests, 6347 assertions green (5 skipped, same as baseline). - Two tests failed on the first run and were fixed: an assertion in DwangsomUitbetalingServiceTest reading the old key, and four fixtures in TermijnServiceTest. Both now pass. - php -l clean on every changed file; all four JSON files parse. - phpcs: TermijnServiceTest ends at 47 errors against a 74-error baseline — my edit first pushed it to 96 through misaligned array arrows, and phpcbf both corrected that and cleared pre-existing debt. Every other changed file is at or below its baseline. * style(mandaat): realign the import payload array after the legalBasis rename phpcs failed #816 with 7 'Array double arrow not aligned correctly' errors, all in the payload built by buildMandaatPayload(). The cause is mine: 'legalBasis' is shorter than 'wettelijkeGrondslag', so every arrow in that array needed re-aligning and I did not re-run phpcs on this file after the edit. phpcbf-applied; the file is back to 0 errors. No behaviour change — whitespace only, confirmed by the diff being 7 insertions and 7 deletions on the same lines, and by MandaatImportServiceTest still passing. * docs(mandaat): add @SPEC anchors to the two methods this PR changed gate-16 (spec-coverage) failed #816 with '2 changed method(s) missing @SPEC'. The gate is diff-scoped and it is right: buildMandaatPayload() and collectChangedFields() were both edited by the legalBasis rename and neither carried an anchor. gate-16 PASSES on development, so this is caused by this branch, not inherited. Both now point at the change that introduced them. NOT FIXED HERE: gate-53 (effective-manifest-crossref, 8 failures). Measured against development's own latest Code Quality run — it reports the IDENTICAL 8 cross-reference failures there, so it is pre-existing and untouched by this branch. src/manifest.json contains no reference to any property this PR renamed (grep for wettelijkeGrondslag returns 0).
… to mandate (#817) * WIP DO NOT MERGE — mandaat -> mandate, schema + migration half only INCOMPLETE. The four schemas, their eight properties, the slug map and the column migration are done and verified. The 716 code references across 56 files are NOT. Merging this state gives every one of them a silent null read: the schema declares `mandate`, the code still asks for `mandaat`, and OpenRegister returns null rather than erroring. Committed so the ownership analysis and the migration boundary are not lost. DONE — all four owners, one register scope mandaat -> mandate (61-mandaat-matrix) mandaatGebruik -> mandateUsage (61-mandaat-matrix) mandaatEscalatie -> mandateEscalation (61-mandaat-matrix) mandaatRegeling -> mandateArrangement (30-beschikking) mandaatNummer -> mandateNumber mandaatId -> mandateId mandaatVersieId -> mandateVersionId targetMandaatId -> targetMandateId mandaatGroepen -> mandateGroups ondermandaatToegestaan -> subMandateAllowed mandaatNiveau -> mandateLevel mandaatGegeven -> mandateGranted Verified after: 0 procest-scoped schemas or top-level properties still carry `mandaat`. Ownership was checked per property BEFORE editing — each is declared by exactly one schema, and all four schemas sit in the `procest` register, so the register-scoped migration covers every owner. That is the condition the wettelijkeGrondslag slice established and it is satisfied here. SLUG MAP: keys follow the new slugs, VALUES deliberately unchanged. Each value is the appconfig key holding that schema's numeric id on existing installs; renaming it orphans the id and the schema silently resolves to nothing. DELIBERATELY EXCLUDED - `akkoord-mandaat` is a lifecycle ENUM VALUE, i.e. stored data. Renaming it rewrites every row — a separate, riskier migration, same call as openconnector#1213 and procest#815. - `mandaatregelingId` and the `mandaatNiveau` inside `mandateGranted` are NESTED under an object property, so they live in that column's JSON rather than as columns. A column rename cannot reach them; they need a JSON-rewrite migration. Documented in the map. TO FINISH: 716 references in lib/ and src/ across 56 files — the four Mandaat* PHP service/controller classes, MandaatRegistryService, MandaatEscalatieService, MandaatCheckService, MandaatImportService, and the Vue files (MandaatMatrixTab, MandaatEditor, MandaatImportPanel). Then this is mergeable. Note MandaatCsvParser::REQUIRED_COLUMNS and the CSV headers are an external input contract — alias both spellings rather than breaking operator files. * WIP: data-contract half — property reads in 3 services Continues f8a55fa. Renames the schema PROPERTY READS that break when the schema half lands: mandaatGroepen -> mandateGroups (MandaatVerifier, 3 sites) and mandaatGegeven -> mandateGranted (BeschikkingService, 2 sites). MandaatCsvParser::REQUIRED_COLUMNS deliberately NOT renamed and now documented in place. Those are CSV HEADERS — an external input contract operators' files already use. I renamed it, then reverted: it would reject every import file in the field. MandaatImportService maps headers onto the renamed properties and accepts either spelling, which is the pattern used for wettelijkeGrondslag and omschrijving. STILL WIP DO NOT MERGE — the remaining data-contract sites (MandaatCheckService, MandaatGebruikService, MandaatEscalatieService, MandaatGuard, and 6 Vue files) are not done. * refactor(mandaat): complete the data-contract half of mandaat -> mandate Every code site that READS or WRITES a renamed schema property now agrees with the schema. Combined with f8a55fa and 4c5a187, this branch is coherent: schema, migration and data contract all move together. WHAT THIS IS NOT. The Mandaat* class names, file names and local variables are still Dutch — roughly 650 references. They are cosmetic: nothing resolves through them at runtime, so they can follow in their own change without risk. The 66 references that actually BREAK are the ones handled here, and isolating that subset is what made the slice finishable. DATA SITES MOVED MandaatVerifier mandaatGroepen -> mandateGroups (3) BeschikkingService mandaatGegeven -> mandateGranted (2) MandaatGebruikService mandaatId -> mandateId, mandaatVersieId -> mandateVersionId, and the findAll filter MandaatEscalatieService targetMandaatId -> targetMandateId MandaatImportService the payload key, and two reads of stored objects 6 Vue files mandaatNummer -> mandateNumber, mandaatNiveau -> mandateLevel, mandaatGegeven -> mandateGranted DELIBERATELY NOT MOVED, each for a measured reason - MandaatCsvParser::REQUIRED_COLUMNS and every $row['...'] read: CSV HEADERS, an external input contract operators' files already use. I renamed REQUIRED_COLUMNS, then reverted — it would reject every import file in the field. Now documented in place. - `['mandaatId' => ...]` in MandaatEscalatieService: an INTERNAL return-array key from resolveEscalationPath(), not a schema property. - `mandaatNiveau` INSIDE mandateGranted, and `mandaatregelingId`: nested under an object property, so JSON within a column rather than columns. A column rename cannot reach them; they need a JSON-rewrite migration. The outer key moves, the inner does not — asserted that way in BeschikkingServiceTest. - `akkoord-mandaat`: a lifecycle ENUM VALUE, i.e. stored data. VERIFIED - phpunit: 1882 tests, 6393 assertions green (5 skipped, same as baseline). Four tests failed first and were fixed: three BeschikkingServiceTest cases on the mandateGranted/mandateGroups fixtures, one MandaatEscalatieServiceTest on targetMandateId. - php -l clean on every changed file; all JSON parses. - phpcs: 58 errors across the changed files against a 106-error baseline on the two test files — phpcbf cleared pre-existing debt while fixing my alignment. Both lib/ services are at 0 errors. - 0 procest-scoped schemas or top-level properties still carry `mandaat`.
…ticeOfDefault* (#818) Replaces #810, which was branched from a stale stack: its diff carried a dozen files that #803/#804/#806/#809/#816/#817 have since landed, so rebasing it meant resolving 8 conflicts across controllers, services, tests and a Postman collection. Branched fresh from development this does the same work as a 40-reference change across 10 files with zero conflicts. IngebrekestellingService -> NoticeOfDefaultService IngebrekestellingController -> NoticeOfDefaultController registerIngebrekestelling() -> registerNoticeOfDefault() ROUTE NAMES MOVE, URLs DO NOT. A route name resolves to its controller class, so `ingebrekestelling#register` had to become `noticeOfDefault#register` or the router 500s on that endpoint. The URLs stay `/api/termijn/ingebrekestellingen`: that is the published contract and moving it is a breaking change for every consumer — a separate decision from this rename. Both route names were verified to resolve to NoticeOfDefaultController.php after the change. NOT IN THIS CHANGE: the `ingebrekestelling` SCHEMA in 60-termijnbewaking.json, its reference property, and the `ingebrekestelling-ontvangen` lifecycle enum value. The schema is a data contract — renaming it needs an ownership check and a column migration, and the enum value is stored data. Class names carry no data and move independently, which is why this is a clean slice. VERIFIED - phpunit: 1882 tests, 6393 assertions green (5 skipped, same as baseline). - php -l clean on every changed file; the Postman collection still parses. - Route names resolve: both noticeOfDefault#* map to a file that exists. - Residual grep finds no IngebrekestellingService / IngebrekestellingController / registerIngebrekestelling anywhere in lib/, tests/ or appinfo/. - phpcs: 6 errors across the four changed files against a 386-error baseline. My edit added 5; phpcbf fixed those and cleared pre-existing debt with them.
) Renames the property across all five procest-scoped owning schemas and every code site that reads or writes it. complaint.omschrijving -> description bewijsstuk.beschrijving -> description mandate.omschrijving -> description informatieobject.beschrijving -> description informatieobjecttype.omschrijving -> description ALL OWNERS IN ONE MIGRATION SCOPE. Ownership was checked before editing: these five are the only procest-scoped declarations, they collapse onto one English name without colliding (no schema declares both spellings, and none already declares `description`), and all five live in the `procest` register — so the register-scoped repair step covers every owner. Verified after: 0 procest-scoped schemas still declare either spelling. NO MIGRATION ENTRY NEEDED. `omschrijving => description` and `beschrijving => description` are ALREADY in RenameDutchDeadlineColumns' COLUMN_MAP, together with the guard that refuses to migrate when two sources target one destination in a single table. That guard cannot fire here: measured, no schema carries both. `agendapunt.omschrijving` IS DELIBERATELY EXCLUDED. It lives in lib/Settings/ori_register.json, which declares register slug `ori`, NOT `procest`. The repair step matches REGISTER_SLUG_PREFIX = 'procest', so ORI's shard tables are outside its scope entirely — renaming it would ship a rename with NO migration behind it. ORI is also Open Raadsinformatie, a Dutch municipal open-data standard, so it is plausibly wire-exempt. It needs its own `ori`-scoped migration and an exemption decision; neither belongs here. THE BREAKING SUBSET WAS ISOLATED FIRST. `omschrijving`/`beschrijving` appear 233 times, but only 42 of those BREAK when the schema moves — property reads, filter keys, request params. The rest are Dutch prose, comments, wire-adapter field names (StUF/ZGW/ZTC) and ORI consumers. Renaming those would be wrong or pointless. Sizing the breaking subset first is what made this finishable; the same split turned mandaat (716 refs, 66 breaking) into procest#817. PUBLISHED CONTRACTS KEEP BOTH SPELLINGS - ZaakdossierController reads an HTTP request param: `description` preferred, `beschrijving` still accepted, so an un-updated client does not break. - ZaakdossierService does the same for its metadata array. - MandaatImportService maps a CSV column header the same way. VERIFIED - phpunit: 1882 tests, 6393 assertions green (5 skipped, same as baseline). One test failed first — MandaatImportServiceTest asserting the diff field name — and was fixed. - php -l clean on every changed file; all four register JSONs parse. - phpcs: 46 errors across the changed files, IDENTICAL to the pre-change baseline measured by stashing. - Residual grep: no omschrijving/beschrijving anywhere in src/. A NOTE ON THE TEST RUN. The first attempt reported a phantom failure because the disk hit 100% and truncated the copied vendor/ mid-copy — vendor/bin/phpunit did not exist. Freed 5.5G of my own stale worktrees, re-copied vendor, and re-ran. The numbers above are from the intact run.
…821) * i18n(procest): make BesluitPublicatiePanel's source strings English First slice of the Dutch-UI-source-string work. Four strings in one component move from Dutch source text to English, with the Dutch preserved as the TRANSLATION rather than deleted: 'Bekijk publicatie in DROP/LVBB' -> 'View publication in DROP/LVBB' 'De publicatie kon niet worden...' -> 'The publication could not be sent.' 'Er is geen DROP/LVBB-endpoint...' -> 'No DROP/LVBB endpoint has been configured.' 'Er is nog geen besluit vastge...' -> 'No decision has been recorded to publish yet.' BOTH l10n FILES MOVE. `l10n/nl.js` is what Nextcloud loads at runtime; nl.json is what the tooling reads. Changing only the JSON would leave Dutch users seeing the new ENGLISH source, because the translation would stop resolving. TWO MISTAKES MADE AND FIXED, both worth recording: 1. I first regenerated both files from parsed JSON. That re-sorted and re-indented everything: a 2915-insertion diff for a four-string change, unreviewable and impossible to audit for unintended edits. Reverted and rewrote the four keys IN PLACE instead — the diff is now 14 lines. 2. The in-place rewrite silently DROPPED an entry: 3126 keys before, 3125 after. Cause: 'The publication could not be sent.' already existed as an English key, so renaming the Dutch identity entry onto it produced a duplicate that JSON parsing collapsed. Both copies carried the identical Dutch value, so no meaning was lost, and the redundant line is removed explicitly. The count legitimately drops by one because two entries merged into one. STATUTORY NAMES ARE EXEMPT — AND THEY ARE MOST OF WHAT THE DETECTOR FINDS. DocumentAssessmentPanel.vue scored highest in the app (7 hits) and needed NO change: every hit was a Woo exception ground ('5.1.1 Eenheid van de Kroon', '5.2.4 Vertrouwelijkheid beraadslaging'), numbered categories defined verbatim in the Wet open overheid. Translating them would misstate the legal ground a publication is withheld under. They stay, like wire field names and enum values. That is why this work is not mechanical: the detector cannot tell UI prose from statutory text, so every string needs reading. VERIFIED - Key-level diff of nl.json against HEAD: exactly 4 Dutch keys removed, 3 English keys added, and NO surviving key changed value. - nl.js EXECUTED, not just parsed: registers 2436 entries (2437 before, the same merge-of-one), and the new English key resolves to the Dutch text. - No Dutch source string remains in the component. * i18n(procest): retarget the renamed keys in all 13 other locales The l10n gates caught what the first commit missed: renaming a SOURCE string orphans its key in EVERY language, not just Dutch. Measured after the first commit: 14 locale files still keyed on the old Dutch source text — be, bs, ca, en, is, lb, mk, nb, rm, ru, sr, tr, uk. Each of those translations had silently stopped resolving, because the component now emits the English source and no locale but nl carried it. Each file's KEY is rewritten to the English source; the VALUE — that locale's own translation — is untouched. So a Belarusian or Ukrainian user keeps exactly the string they had. `en.json` is among them, which is why "l10n coverage (en.json)" failed: the gate requires every source string to be present there. NO .js FILES NEEDED CHANGING beyond nl.js, which the first commit already did. Checked rather than assumed: be.js, ru.js and en.js carry zero occurrences of the old key — those locales hold it in .json only. THE COST OF THIS WORK IS HIGHER THAN THE STRING COUNT SUGGESTS. Four source strings touched 15 files across 14 languages. Any estimate of the remaining 600-900 Dutch UI strings has to multiply by the locale fan-out, and the l10n gates are what makes the omission visible — they should be run per slice, not at the end. VERIFIED - All 38 locale JSON files parse. - No orphaned Dutch source KEY remains anywhere in l10n/ (the two hits in nl.json / nl.js are the Dutch as a VALUE, which is the intended end state). - 14 locale files now key on the English source.
Second slice, using the method the first one established. Twelve Dutch source
strings across five components move to English, with each locale keeping its own
translation.
'Akkoord door' -> 'Approved by'
'Beschikbaar voor agendering' -> 'Available for scheduling'
'De actie kon niet worden uitgevoerd.' -> 'The action could not be performed.'
'De beschikking is samengesteld...' -> 'The decision has been composed as a draft.'
'De beschikking kon niet worden...' -> 'The decision could not be drafted.'
'De geadresseerde ontbreekt nog...' -> 'The addressee is still missing and is required.'
'De motivering ontbreekt nog...' -> 'The rationale is still missing and is required.'
'Deze stap is verplicht en kan niet...' -> 'This step is mandatory and cannot be skipped.'
'Er zijn geen besluiten gereed...' -> 'No decisions are ready for scheduling for this body.'
'Geef een reden waarom deze stap...' -> 'Give a reason why this step is being skipped...'
'Geen beschikbare items' -> 'No available items'
'Geen beschikking gevonden' -> 'No decision found'
KEPT, and this is the judgement the detector cannot make:
- 'College van B&W' is the statutory name of a municipal executive body, not
UI prose. Same footing as the Woo exception grounds left in
DocumentAssessmentPanel and as wire field names.
- 'Grounds for Objection (Gronden van Bezwaar)' is already English-first with
the Dutch legal term in parentheses, which is the right shape.
LOCALE FAN-OUT. 12 strings retargeted 387 keys across 38 locale files. The first
slice missed this and the l10n gates caught it: a Dutch SOURCE string is the key
every other language hangs off, so renaming it orphans be/bs/ca/en/is/lb/mk/nb/
rm/ru/sr/tr/uk too. Each file's key moves; its own translation value does not.
VERIFIED
- Source diff is 12 lines and every one is a t('procest', ...) literal —
0 non-t() lines changed.
- All 38 locale JSON files parse.
- Entry counts UNCHANGED in every locale file. The first slice silently lost
one to a duplicate-key collapse when an English key already existed; this
slice was checked for that explicitly and there are no duplicates.
- 0 orphaned Dutch source keys remain anywhere in l10n/.
- en.json carries all 12, which is what the l10n coverage gate requires.
- nl.js EXECUTED: 2436 entries, sample key resolves to its Dutch text.
Third and final slice for procest's src/. Twelve Dutch source strings across
eight components move to English.
KEPT — the judgement calls
'College van B&W' statutory body name
'Grounds for Objection (Gronden van Bezwaar)' already English-first
'Inadmissible (niet-ontvankelijk)' already English-first
A COLLISION THIS SLICE EXPOSED, AND THE RULE IT GIVES
'Verplicht' translates to 'Required' — which ALREADY EXISTS as a separate source
string in every locale. Renaming onto it produced a duplicate key in all 37
files, silently collapsing two entries per file with no way to tell which value
survived. The integrity check caught it before commit.
Correct handling for a rename whose TARGET ALREADY EXISTS is DELETE, not rename:
the component now emits t('procest', 'Required'), which already has a
translation in every locale, so the old 'Verplicht' entry is simply removed. Its
translation is not lost — the surviving 'Required' entry already carried it.
That generalises: before retargeting a key, check whether the English target is
already a source string. If it is, the entry is deleted and the existing
translation takes over.
VERIFIED
- Source diff is 13 lines, every one a t('procest', ...) literal — 0 non-t()
lines changed.
- All 38 locale JSON files parse; 349 keys renamed, 38 colliding entries
deleted.
- Entry-count delta is -1 or 0 in every file — exactly the removed 'Verplicht'
entry, and 0 where the locale never had it. No unexplained loss.
- 0 duplicate keys, 0 orphaned Dutch source keys anywhere in l10n/.
- en_US.json flagged as lacking 'Required'; checked, and it never had either
key (675-entry partial locale). Nothing removed from it.
- nl.js EXECUTED: 2436 entries, sample key resolves to its Dutch text.
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.
Automated PR to sync development changes to beta for beta release.
Merging this PR will trigger the beta release workflow.