Release: merge development into beta - #1573
Open
github-actions[bot] wants to merge 69 commits into
Open
Conversation
The 0.3.9-beta.20260831102631 release bumped the version on beta. Without this, development stays behind beta and the next development -> beta promotion conflicts on the version file. Version files resolve to development's side, which is the higher line, so this never moves a version backwards.
…1572) Per-widget `valueColor` is applied as an INLINE style on the number, so it beats the canonical card accent, and a hex literal opts the card out of NL Design System theming — which kpi-card.css forbids outright, because the nldesign app re-themes by overriding the Nextcloud tokens. Two outcomes, no third: * a colour that merely restated the default accent is deleted — `#0082c9` IS `--color-primary-element` in the default theme, so the tile looks identical and now re-themes correctly; * a colour that carried meaning becomes `variant`, which is themed and drives the icon tint and the number together so the two cannot disagree. `variant` on a stat/delta resolves through VARIANT_COLORS, which was moved onto the `-text` tokens first (nextcloud-vue#888) — the plain fill tokens failed WCAG AA at 1.08:1 as a foreground colour, and these conversions would have been the first tiles to hit that path. Edited as text rather than re-serialised, so the manifest keeps its hand-laid-out formatting; every `valueColor` in this file was confirmed to sit on a KPI widget first. Manifest schema validation passes.
Dependabot cannot propose composer updates for this app. Its updater fails
with:
Your requirements could not be resolved to an installable set of packages.
- edgedesign/phpqa[v1.27.0, ..., v1.27.2] require ext-xsl * ->
it is missing from your system.
config.platform pins php 8.3 so composer resolves against a known PHP version,
but says nothing about extensions. edgedesign/phpqa requires ext-xsl and the
resolving environment does not have it, so the resolve fails before any bump
can be computed.
CI is unaffected, which is why this went unnoticed: composer install replays
the committed lock and never re-resolves, so the pipeline stays green while
dependabot -- which does re-resolve -- fails every time. The shared quality.yml
installs no xsl extension anywhere and never invokes phpqa.
Declaring ext-xsl beside the php pin makes resolution assume exactly what the
committed lockfile already assumes. Verified in a clean composer:2 container
without --ignore-platform-reqs: the same file fails without this line and
resolves (103 installs, lock written) with it.
Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde
requested review from
WilcoLouwerse,
bbrands02 and
rjzondervan
as code owners
August 31, 2026 10:42
….3.9-beta.20260831102631 chore(release): sync beta back into development
Levels this app with the fleet. 2.27.2 adds two fixes the earlier 2.27.0 pin does
not carry:
* headerless is no longer chromeless — a flat KPI card in a borderless wrapper
had no card, border or background at all;
* a stat `variant` paints from the `-text` tokens rather than the fill tokens,
which failed WCAG AA at 1.08:1 as a foreground colour. The KPI colour
cleanup converted hardcoded values to `variant`, so this is what makes those
conversions contrast-safe.
Lockfile only, and npm pruned nothing.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Dependabot cannot update these packages. Its npm updater fails with: npm error code EOVERRIDE npm error Override for <pkg> conflicts with direct dependency The package is listed BOTH as a direct dependency and as a literal overrides entry pinned to the same range. Dependabot bumps the direct dependency and does not move the override, npm rejects the mismatched pair, and the update is abandoned -- so these packages can never be updated, security advisories included. npm's self-reference is the idiom for exactly this: $<name> tells the override to follow whatever the direct dependency resolves to, instead of a range that has to be kept in lockstep by hand. This repo already uses it (see the vue entry under @vue/test-utils). Transitive consumers still get pinned to one version -- the direct dependency's -- which is what the override was there to do. What changes is that the pin tracks the dependency instead of duplicating it. Verified in a clean container: bumping the direct dependency with the literal override reproduces the EOVERRIDE above; with the self-reference the same bump resolves. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* feat(parafering): an approval route is a flow, not a schema An approval route is a sequence of manual sign-offs that must be taken before a decision may be attached to a case. That is a flow with a person at each step. It was modelled instead as a schema, twice: dossiq's `parafeerroute` and decidiq's `ApprovalRoute`. The second was mine, added last week in decidiq#1028, and it was the wrong shape. This supersedes it. Each step becomes a `dossiq.askPerson` node rather than a bare `openregister.awaitSignal`. The difference is the one an approver notices: askPerson raises a real dossiq TASK against them and waits for the answer in the same node, so the step lands in the work queue they already read. A raw await signal waits for an answer nobody was asked for. The chain ends at `dossiq.requestDecision`, because that is what an approval route is FOR. The case schema already carries `decisions` as a relation to decidiq Decision objects, so the destination was already there. A route whose step names no actor is refused whole rather than projected partly: askPerson rejects an empty assignee, and a flow that quietly omits a sign-off somebody expects is worse than no flow. Verified against the live instance: 7 real routes, 2 to 6 steps each, all projected; a re-run updates 7 and creates 0. "Collegeadvies - Omgevingsvergunning" came out as juridische dienst, teamleider VTH, afdelingshoofd VTH, portefeuillehouder, then the decision. The projections arrive DISABLED. The routes still drive parafering, and two live copies would ask every approver twice. Retiring the route, the schema and the settings page is the next change, once these have been checked against real work. The visible symptom this exists to fix is a settings menu with too many entries, and this change removes none of them; it makes removing one possible. Also: WorkflowTemplateFlowMigrator and this one shared 133 lines of identical machinery, now the ProjectsOntoFlows trait. Extracting it broke the working migrator — I dropped the `\Flow\` segment from the FlowService id, and every unit test stayed green because the container is a mock that answers to any id. The live dry-run caught it, reporting "OpenRegister exposes no FlowService" and exiting 0. The id is now a named constant with a test that asserts the string. * fix(quality): count outcomes apart from rows, and test the occ command Two CI findings on the previous commit, both real. psalm: "Cannot add an array to a non-array" on `$summary[$row['outcome']] + 1`. The summary held counts and the row LIST in one array, so incrementing by key reads as "add 1 to whatever is at that key", and nothing stopped the key being `rows`. An outcome named `rows` would have appended to the row list instead of counting. Counts and rows are now separate structures, merged only in the return. Coverage ratchet: the change added 216 statements, 157 of them an occ command with no test. This repo had no command tests at all, so there was nothing to mirror; there is now a CommandTester suite covering what the command actually owns, which the migrator's own tests do not reach: it refuses without --user because a flow's owner is permanent, it refuses an unknown uid rather than guessing, it exits non-zero on a partial run, and `--dry-run` reaches the migrator AS a dry run. That last one earned its place — I pinned `$dryRun = false` and it went red. A command that accepted the flag and passed false would write while reporting it had not.
…ation (#1584) The dossiq `tenant` schema has been DEPRECATED for a while, and says so itself: "Tenant identity now lives on OpenRegister's Organisation entity; this schema is retained only so pre-migration rows remain readable until sunset." `occ dossiq:migrate-tenants` moves the rows. OpenRegister carries the Organisation model, and openregister#3247 is giving it the chain-partner fields dossiq's partner schema still holds. What never happened is the last step: the Organisations settings entry stayed in the menu, pointing at an index over the deprecated schema. On this instance that index lists zero rows, because the data has already moved. An admin opening it learns nothing and edits nothing. The page stays routable for deep links and e2e specs; only the entry goes, declared in menu-layout.json under `removals` per ADR-044 Decision 2. gate-53 warns, correctly, that a cross-app waiver is a REDUCED GUARANTEE: it checks only that `openregister` is a known fleet app id, and cannot check that page `organisation` exists there, is reachable, or carries this functionality — nor can the waiver ever rot. So the judgement is written into removalsCoverageNote to be re-checkable, the way BesluitvormingAgenda's is. Measured against the running instance: openregister declares page id `organisation` (route /organisation) with a live `Organisations` menu entry, and ships OrganisationsIndex.vue, OrganisationDetails.vue and JoinOrganisation.vue. Unlike the decidesk case, dossiq DOES declare a manifest dependency on openregister, so the target is guaranteed present.
…ROCEEDING (#1585) gate-60 reported 18 Tier B warnings. Sixteen adopted, two refused. 18 -> 2. Adopted, across dossiq_register, dossiq_mock_register and 62-handler-vervanging: Case BriefcaseOutline -> FolderAccountOutline Case Type BriefcaseVariantOutline -> FolderCogOutline Consultation MessageAlertOutline -> CommentQuestionOutline Map Layer MapOutline -> LayersOutline Objection FileDocumentAlertOutline -> FileAlertOutline Role AccountHardHat -> AccountKeyOutline Task CheckboxMarkedOutline -> CheckboxMarkedCircleOutline Substitution AccountSwitch -> AccountSwitchOutline Map Layer and Role were the clear wins. A map LAYER drawn as a map, and a role assignment drawn as a construction worker in a hard hat, were both just wrong. REFUSED: objectionProceeding, which keeps Gavel. TWO schemas carry the title "Objection", and the gate can only see the title, so it asks both to become FileAlertOutline. They are different things: objection Bezwaarschrift, the objection LETTER -> FileAlertOutline, adopted objectionProceeding the AWB objection CASE: statutory deadlines, hearing waiver, dwangsom accrual -> Gavel, kept A gavel is right for a legal proceeding and a document icon is not. The two sharing a title is the underlying oddity; renaming one is a UI-visible change and belongs in its own PR.⚠️ AccountKeyOutline had to be REGISTERED in src/icons.js in the same commit. It was not there, and an icon a manifest names but the registry does not hold renders as NOTHING. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
) * chore(register): retire catalog, rename location to case-location Two slug collisions. `catalog` collided with opencatalogi, which owns the word: 15 properties against this register's 5, and zero property overlap. Dossiq does not publish, so it has no business owning a publication catalog. Nothing in src/ or lib/ read it; the only references were three `catalog` properties on caseType, documentType and decisionType, which go with it. `location` collided with shillinq. This register already TITLED its own "Case Location"; the slug simply never followed. Renaming it to `case-location` says what it is and clears the collision, and one manifest binding follows. occ openregister:schemas:prune-retired --app dossiq --slug catalog --apply * fix(register): move the schema KEY with the slug, this register keys by slug Measured on the descriptor: key equals slug for every other schema here. Renaming only the slug made this one the single exception, and a reference that names a schema by bare string then has no way to stay right. The key is anchored to the schema-entry indent, so a nested property with the same name cannot be caught by it: dossiq has four `location` PROPERTIES, and an unanchored rename reshaped all four. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
) The dashboard shipped two Refresh controls side by side. CnActionsMenu renders one as the first item of the page-level Actions overflow menu, and the manifest declared another as a header button next to that menu. They did the same thing. A "type": "refresh" headerAction is dispatched as emit(PAGE_REFRESH_CHANNEL, {}), which is the exact signal CnActionsMenu broadcasts on refresh-channel="cn:page:refresh". Same channel, same subscribers. Removing the header action takes away the duplicate and nothing else. An app that wants to keep a prominent Refresh button can stand the menu item down with showRefresh: false instead. openregister's dashboard does that. Shipping both is what this removes.
…ssiq (#1594) dossiq#1531 made dossiq SEND its objection advisory committees to decidiq as GovernanceBody objects, so the decision app holds them alongside the meetings and decisions they exist for. It left dossiq's own settings page in place, which meant the fleet had two places to edit a committee and only one of them was authoritative. This is the other half of that decision. The menu entry goes; decidiq's Organisation surface is where a committee is designed and maintained, with the members, evaluations, efficiency and template tabs dossiq's index never had. What dossiq keeps is the seeding and the delegation. The `bezwaaradviescommissie` schema stays because the seed still writes it. What goes is the pretence that dossiq is where you edit one. The pages stay routable for deep links and e2e specs; only the entry is retired, under `removals` per ADR-044 Decision 2. gate-53 warns that a cross-app waiver is a reduced guarantee it cannot verify and cannot rot, so the judgement is written into removalsCoverageNote to be re-checkable. Measured on the running instance: decidiq declares `GovernanceBodies` (route /governance-bodies) and `GovernanceBodyDetail` behind a live menu entry labelled Organisation.⚠️ dossiq declares NO manifest dependency on decidiq, deliberately — decidiq is an optional peer and CommitteeDelegationService degrades when it is absent — so nothing in this repo corroborates the claim and gate-53 says so. Adding a dependency to quieten that warning would change runtime semantics to flatter a gate.
…#1596) The shipped Landelijke Handhavingsstrategie matrix declares its actor-type axis as burger / bedrijf / overheid / recidivist, and labels all twelve of its government cells `actorType: "government"`. LhsRecommendationService indexes cells by "severity:behaviour:actorType" and THROWS when the triple misses: throw new RuntimeException('Geen LHS-cel gevonden voor combinatie ' . $key) So it does not degrade. A quarter of the matrix — every combination involving a government actor, twelve of forty-eight — was unreachable, while the axis went on offering `overheid` as a choice. An inspector picking it got an exception instead of an enforcement recommendation. Measured against the running instance before the fix: burger, bedrijf and recidivist resolve; overheid resolves to nothing. Found while sizing the move of these matrices onto OpenRegister's decision tables. Nothing in the app compared the cells against the axes they belong to, so three tests now do: every cell value exists on its axis, every axis value has at least one cell, and the grid is complete with no duplicate triple. The first two are the same defect seen from opposite ends, and either alone can be satisfied by half a fix — which is why both are asserted.
* feat(lhs): an enforcement matrix is a decision table The Landelijke Handhavingsstrategie matrix is a three-axis lookup — severity by behaviour by actor type, yielding an intervention. That is a decision table, and openregister#3186 gave the fleet one evaluator for those, whose own suite proves this exact shape evaluates. dossiq instead indexes the cells by hand into a "severity:behaviour:actorType" dictionary and throws when the triple misses. That is not merely duplicate work: it is what let the shipped matrix label all twelve government cells `government` while the axis said `overheid`, leaving a quarter of the strategy unreachable with nothing to notice (dossiq#1596). Each cell becomes one rule keyed by its own triple. The table declares UNIQUE, because a grid has exactly one cell per triple — and UNIQUE turns an overlapping pair into a refusal rather than something silently resolved by declaration order, which is what the hand-indexed dictionary quietly gave up. 🔴 A matrix whose cell names a value absent from its own axis is SKIPPED, not projected. Projecting it would carry the defect across while looking like a migration that worked: the rule would be as unreachable in the table as the cell is in the matrix. Verified against the running instance — the shipped matrix is refused today, twelve cells off-axis, and becomes projectable once dossiq#1596 lands. The projection arrives disabled, like its two siblings. The matrix still drives recommendations, and a table that also answered would be a second source of truth for an enforcement decision. Retiring the bespoke lookup and the settings page is the next change. 2628 tests green; phpcs, phpmd and phpstan clean. * style(lhs): pull the empty-summary shape out of the two guards phpcs: one line at 164 characters. The two early returns each spelled the whole summary inline, so the shape was duplicated as well as long. One helper now, which also gave the runAs guard somewhere to say WHY it refuses: a written table inherits its owner and organisation from whoever wrote it, permanently.
…stranded every run (#1597) * feat(flow): finish case-flow-human-steps: document step config, decision resume tests, task waiting-case section The shipped besluit-document node carried config keys its node class refuses (template/outputName where DossiqMergeTemplateNode requires templateSlug and targetField), so every run stranded at the document step. Fix the declaration, declare case.besluitDocument so the store keeps the rendered text, and pin both in CaseFlowDeclarationTest. Add the missing 4.2 verification to DecisionConcludedListenerTest (a concluded decision resumes the run naming its ref; an unrelated one leaves it suspended), the task half of 6.1 (TaskWaitingCaseSection on the TaskDetail page, rendering nothing for a task without a flowRun), the single-copy assertion for 2.2 in the e2e, Dutch keys for the new strings, and tick tasks.md by content. * style(flow): prettier formatting for the waiting-case section and its spec * fix(flow): satisfy the hydra gates on the waiting-case section gate-16: tag mounted() with its spec. gate-55/60: the manifest icon TimerSandComplete was outside both icon registries and would have rendered nothing; use CheckboxMarkedCircleOutline, the ADR-077 canonical task icon, which src/icons.js already registers, in the manifest and the component. gate-52: record the documented ratchet exception with its reason (a conditional cross-object link no built-in widget expresses). gate-105, pre-existing: drop the Dashboard's second Refresh, which CnActionsMenu already renders.
* feat(parafering): a flow can ask for a paraaf, not just a task Projecting an approval route onto a flow of dossiq.askPerson nodes made the route runnable, but not yet a replacement. askPerson raises a generic task, and parafering reads a parafeeractie. The two carry different fields, and the two that matter are onBehalfOf and mandate: who signed on whose behalf, under which mandate. That is administrative-law record. Switching the projection on as it stood would have put generic tasks in approvers' queues, left the parafering screens empty, and stopped recording the mandate chain: a loss of record dressed as an engine change. So dossiq.askParaaf raises a parafeeractie and waits for it. The parafeeractie gains flowRun and flowNode, because resuming has to name the node, not just the run: a run holds one awaiting slot per node and cannot say which of them a paraaf answers. askPerson records both on its task for the same reason. The projected node also carries the route step's own order rather than its position in the chain. The parafering screens read step, so it has to mean what the route meant, and the two differ as soon as a route numbers its steps from anything but one. Nothing is enabled or retired here. The route still drives parafering. What remains is a dual-path service so a voorstel already carrying a routeSnapshot finishes the way it started; a hard cutover would strand whatever is mid-parafering. * test(parafering): cover the node's refusals, and stop testing a return shape production never sends The coverage ratchet failed the PR, and it was right to: the node added 96 statements and the tests reached 89.45% of the changed files against a 93.62% base. Sixteen statements had no test. They were not padding. Every refusal path was among them: no voorstel to attach a paraaf to, OpenRegister absent, the parafeeractie schema unconfigured, a stored paraaf that came back without an id. Each is a case where raising or suspending anyway would leave a run waiting on a question nobody was asked, so each is now pinned. One of them was worth more than its line count. ObjectService::saveObject() returns an ObjectEntity; the fake returned a plain array, so was the branch production ALWAYS takes and the suite NEVER did. There is now a fake that returns an entity, and it asserts the id is read from INSIDE it: reading the entity itself would store something no resume can match. Completing the FlowNodeResumeState stub came out of the same look. The real class carries set(), isResuming() and clear(); the stub carried none of them. Nothing in lib/ calls them today, so this was latent rather than live, but a stub missing a method the real class has is exactly what lets a future call site pass here and fatal against OpenRegister.
… screen (#1590) * feat(reassignment): reassign from the cases page, not from a settings screen Reassignment is an act on CASES, so it belongs on the cases somebody is looking at. It lived behind a settings page that asked first which handler to empty, which is the wrong question when what you actually have is a handful of cases in front of you. The Cases index now carries a `reassign` bulk action: tick the rows, pick who takes them. WHY A NEW SERVICE RATHER THAN A SECOND METHOD. The existing operation answers "move everything open that belongs to handler A to handler B". This answers "these rows, to this person", and the rows are hand-picked so their assignees may all differ. Putting both on one class took its complexity from under the threshold to 62 against a limit of 50, and phpmd was right to say so: they are two operations that happen to share a write. The write is now the WritesReassignments trait, so the audit entry cannot drift between them. 🔴 The audit records each case's OWN previous assignee, not one batch value. A batch-level `from` is truthful only when every row came from the same handler, which is exactly what a hand-picked selection does not guarantee — it would name the wrong person on most rows. The rows still share a batchId, so the selection stays recoverable as one act. There is a test, and it goes red if the per-case read is replaced by a constant. A case already assigned to the receiver is counted as done and NOT rewritten: rewriting it would append an audit entry saying it moved from somebody to themselves. WHY THE DIALOG IS MOUNTED AND NOT DECLARED. The manifest supports `handler: "open-modal"` with a `target`, which reads like the right answer. It is not, yet: CnIndexPage emits an `open-modal` event and nothing in the library listens for it, so declaring it would ship a bulk action that does nothing when clicked. This uses the function-handler path that dossiq already uses for the Voorstellen row action, and the comment says why so the next person does not "fix" it back. The Substitutions & reassignment admin entry is retired; self-service substitution already lives in personal settings, where it belongs. The page stays routable. 2628 tests green; phpcs, phpmd, psalm and manifest validation all clean. * fix(quality): move the new tests to their own file, format, and translate Three CI findings on the previous commit. PHPUnit reported three of my tests RISKY, not failed: "executed code that is not listed as code to be covered or used: SelectionReassignmentService". I had put tests for a NEW class into the OLD class's test file, whose @Covers restricts what it may touch. They now live in SelectionReassignmentServiceTest with @Covers on the class they actually test, which is where a reader would look for them anyway. Prettier: the dialog was unformatted. Ran the repo's own formatter. l10n: seven strings were used and not declared. Extracted into en.json, and the Dutch side written rather than left as English — dossiq is a Dutch-facing app, and "Reassign" already had "Opnieuw toewijzen", so the new strings follow it. One of the seven, "Where the automation lives", was pre-existing debt the extractor surfaced; it is translated too rather than left half-done. en.json and nl.json key sets now match with no missing Dutch translations. * test(reassignment): cover the endpoint, and rebuild the browser catalogues Two CI findings. The coverage ratchet: the change adds 124 statements and the new controller action had no test, so coverage of the code this change touches fell 8.36%. Four tests now cover what the action owns rather than what the service already proves: it forwards the ticked ids and the session actor, it answers 400 when `caseIds` arrives as something other than an array (it comes off the wire, so a caller can send a string), it surfaces the service's refusal as a 400, and an unexpected failure is a logged 500 — a selection that half moved must not be reported as a clean 200. check:l10n-js: the browser catalogues were stale after the seven new keys. Ran the repo's own `l10n:build`. * test(reassignment): cover the failure paths, and let the fixture stand alone The coverage ratchet was still short: 124 added statements, 1.73% down. The uncovered half was the failure paths, which is the half worth testing — four tests now cover them: a write that refuses is reported per case and does NOT abort the rest of the selection (the user ticked them all; a silent stop leaves them believing every one moved), a missing receiver is refused, an absent OpenRegister refuses rather than reporting a clean run, and a case the store returns as an ENTITY rather than an array is still read. That last one matters beyond coverage: OpenRegister's find() returns an ObjectEntity in production and an array in some paths, and a reader that handled only one shape would report every case "not found" against the other. Separately, the shared ObjectService stub interface was declared INSIDE CaseReassignmentServiceTest. Two test classes mock it now, so the second could only run when the first happened to load: `phpunit <that file>` alone died on "Class or interface does not exist" while the full suite passed. It is now its own file, and composer's autoload-dev maps OCA\Dossiq\Tests\ so PHPUnit can find it without loading a sibling test first. 2636 tests green with --fail-on-risky; phpcs and phpstan clean. * fix(quality): create the spec the tags point at, isolate the dialog, tag the trait Three gate findings, all real and all mine. gate-46 spec-anchor-existence: four @SPEC tags named openspec/changes/reassignment-is-a-bulk-action/... and that change did not exist. A reference that resolves to nothing is worse than no reference: it reads as though the rationale is written down and sends the reader looking. The change is now written, including the two things worth recording — why the selection operation is its own service, and why the audit reads each case's own previous assignee rather than one batch value. gate-13 modal-isolation: the dialog sat in src/components/. ADR-004 puts NcDialog-based components in src/dialogs/, so it moved there. gate-16 spec-coverage: the four trait methods and readCase carried no @SPEC. Tagged against the change above, which now exists to be tagged against. * style(imports): keep the import order the linter expects after the dialog move * fix(reassignment): tag the dialog's methods with the spec they implement gate-16 named three: the dialog's title, its lead line and its submit. Moving the component into src/dialogs for gate-13 made it a changed file, and a changed method without an @SPEC is what gate-16 exists to catch. The two rendered strings point at REQ-RBA-004, which is the requirement that reassignment is reachable from the cases page at all; submit points at REQ-RBA-001, the one that makes a selection reassign as a single act.
* feat(reports): one Reports page of cards, in the footer ADR-112. Reports was a menu PARENT at order 55, in the middle of the operational entries a case handler uses all day, with three report pages beneath it. Reports are not an operational surface: you go there deliberately, occasionally, and having them among the daily entries costs the daily entries the top of the list. It is now a leaf in the footer group at order 95, so it renders between Documentation (90) and Features & roadmap (100), and it points at one page carrying the three reports as cards: Processing time, Deadline monitoring, Process mining. A card carries what a menu item cannot — a description and a category — so a reader can tell the throughput reports from the process analysis without opening either. The three pages stay ROUTABLE. Deep links, e2e specs and the cards all address them by route name; only their menu entries go, retired in menu-layout.json under `removals` with `removalsReplacedBy` naming Reports, which is how every other placement decision in this app is declared (ADR-044 Decision 2). Verified: gate-104 reports 0 findings, gate-53 passes and raises nothing for the three new removals, and the assembled menu has no report entry left in the main nav. 🔴 BLOCKED ON nextcloud-vue#893. `type: "reports"` is not in the installed schema (2.27.0), so `npm run check:manifest` fails with "/pages/45/type must be equal to one of the allowed values". This cannot merge until that PR releases and dossiq takes the bump. Left as a draft rather than merged behind a green that would only mean the validator had not looked. * fix(reports): require the nc-vue that knows the page type, and stop shipping Dutch as source Three things the merge with development surfaced. The manifest declared `type: reports`, and the pinned nextcloud-vue did not know that type: its schema enum stopped at `custom`, so `check:manifest` failed on the one page this branch exists to add. The range said ^2.24.3, which a fresh install could satisfy with a version that has no reports page at all. It now says ^2.28.0, the first release carrying the type in its schema, the component, and the built bundle. The four card and page descriptions had no translation keys. The l10n parity checker reads the manifest precisely because it is data rather than source, and it caught them; the Dutch is written rather than extracted. The category names were written in Dutch and used as source strings, so an English instance read `Doorlooptijd en termijnen`. They are English now, with the Dutch in nl.json where the rest of it lives. That last one only reaches the screen once nextcloud-vue#904 lands: CnReportsPage currently renders its manifest strings untranslated, so today the keys sit unused. English source is the right value either way, and the fleet checker would have flagged the Dutch the moment it learned to read this field. * fix(reports): give the cards the icons their menu entries carried gate-60 failed on CalendarClockOutline: it is not registered in src/icons.js, and an unregistered icon renders as nothing at all rather than falling back to something. The deadline card would have shipped blank. The fix that is also the better design is to reuse what the retired menu entries used. Analytics carried ChartLine, TermijnDashboardMenu carried ClockAlertOutline and ProcessMiningDashboardMenu carried ChartSankey, all three already registered. A reader who knew those entries now finds the same glyph on the card that replaced each one. ChartSankey also settles a gate-60 Tier B warning on its own: the process-mining concept has a canonical icon under ADR-077, and SitemapOutline was not it.
…tops writing an unsigned signature (#1609) * fix(parafering): a returned voorstel goes back, and a fully-signed one is accorded Three status literals in BesluitvormingParafeerService are not values the schema allows, and OpenRegister runs hard validation by default, so two of the service's three outcomes could not be written at all. A parafeeractie's action enum is parafered, returned, advised, skipped, accorded. The service compared against 'retour', which is not in it. That branch was therefore unreachable, and an approver who sent a voorstel back fell through to the advance below: the voorstel moved FORWARD to the next approver. A rejection was read as an approval, which is the worst way for this to fail. Had it been reachable it would have written status 'retour', and the voorstel status enum has no such value either. The spec says what both should be, and has all along: action 'returned' sets status 'teruggestuurd'. VoorstelDetail.vue keys the steller's resubmit button off exactly that value, so nothing offered a way back. The completion path wrote 'gereed_voor_agendering', which is not a voorstel status. getStatusAfterAdvance() in src/utils/parafeerEngine.js has returned 'geaccordeerd' for this transition all along, so the front end and the back end disagreed about the end of the chain. The unresolvable-action default moves from 'approved' to 'parafered' for the same reason: every value the service handles should be one the schema declares. Behaviour is unchanged, since neither equals 'returned'. The existing test could not have caught any of it. It mocks saveObject with a canned return and asserts the canned value, so it passes whatever the service writes, and its own fixture used action 'approved', which is not in the enum either. The new tests record what was SAVED and assert that. The fourth reads the register JSON and checks every status literal the service writes against the enum, which is the version of this that catches the class rather than these two instances. Reverting the three literals turns three of the four red. * fix(parafering): the paraaf node waits, it does not write an unsigned signature dossiq#1602 shipped dossiq.askParaaf creating the parafeeractie up front, as a standing request. It could not have worked, and the tests could not have told us. parafeeractie declares action among its required properties and OpenRegister runs hard validation by default, so a paraaf raised without one is rejected on save. openspec/specs/parafering-actions says the same from the other side: the schema SHALL enforce voorstel, step, actor, action. The node's tests passed because their fake accepted whatever it was handed; teaching that fake the schema's required properties turned five of eighteen red at once, all of them the raise path. The schema was right. A parafeeractie is the record of a sign-off somebody gave, not a request that they give one, and the enum has no value meaning "not yet signed" because there should not be one. Writing a blank paraaf to stand for "awaiting" would put an unsigned signature into an administrative-law record. So the node records the ask in the run's own awaiting slot, which already carries the assignee that OpenRegister's resume guard consults, and creates nothing. It keeps refusing a step with no actor, a run with no voorstel and a run with no resume slot: the slot is where the assignee lives, and an awaiting step that cannot say who may answer it is one anybody can answer. The node no longer needs SettingsService or an object service at all, which is the shape of the correction: it had a dependency only because it was doing something that was not its job. A test now reads the register JSON and asserts action is required, so the reason lives next to the decision. If someone later makes it optional, that fails and asks them to reconsider the node rather than finding out in production. Remaining, unchanged: the approver's paraaf still has to carry flowRun and flowNode so the run can be resumed from it. Those fields are on the schema already; what stamps them is the next step.
`npm run lint` was `eslint src`, so two whole trees were never linted. Across
the fleet that hid roughly 3,900 errors, none of which any CI leg had shown.
Most of it was the config, not the code, and the same two defects were in
every app.
**scripts/ had no config block at all.** These are CommonJS Node CLI checkers,
and flat config defaults every `.js` to ESM with browser-ish globals, so eslint
read the CommonJS wrapper itself as undefined identifiers: `require`,
`process`, `__dirname`, `__filename`, `module`. A `scripts/**` block now
declares the environment. Declaring beats suppressing here: `no-undef` is the
rule that catches a genuinely misspelled identifier, and dozens of fake
findings would bury a real one. A second block covers `scripts/**/*.mjs`, which
is ESM and needs Node's globals without the CommonJS wrapper.
**The tests block applied a non-TypeScript-aware rule to TypeScript.** It named
`tests/**/*.ts` while setting the CORE `no-unused-vars`, which v9 deliberately
turns off for `.ts` in favour of the `@typescript-eslint` version. The core
rule reads the parameter names inside a function TYPE as bindings, so
t?: (app: string, key: string) => string
reports `app` and `key` as unused variables, and every unused `catch (e)` in a
`.ts` spec reports twice. The block is split now: `.js`/`.mjs` on the core
rule, `.ts`/`.tsx` on the TypeScript one, same patterns on both.
Also: stale `eslint-disable` comments naming plugins eslint 10 no longer
registers, which are themselves errors ("Definition for rule ... was not
found"), and a rule that must not parse shell scripts.
The genuinely real findings were the useful part: dead locals, unused imports,
dead helper functions, unused `catch` bindings, extensionless relative imports,
and a handful of `== null` comparisons spelled out so they still match null AND
undefined.
Verified per app: `npm run lint` 0 errors over src + tests + scripts,
`prettier --check` clean, and the unit suite still green.
* wip: checkpoint before rate-limit pause (2026-09-01 09:45) * test(e2e): satisfy the repo lint on the flow runs spec imports --------- Co-authored-by: Ruben van der Linde <release-bot@conduction.nl>
…ntity (#1644) * fix(flow): run every flow step's storage work as the run's acting identity MergeTemplateHandler saved its merged case bare, so under FlowRunWorker the write was refused as "User 'Anonymous' does not have permission to 'update' objects in schema 'Case'" and the seeded journey stopped at besluit-document (merge_template_failed, run f087ae22): the case never got its decision document and never closed. DQ#1625 fixed this defect for SetStatusHandler and two nodes, and this fourth instance shipped anyway — so this change fixes the class, not the instance. Every remaining flow handler and node that performs ObjectService storage on the worker path now routes it through FlowRunAsScope, delegated writes included: MergeTemplateHandler, SetFieldHandler, CreateTaskHandler, CreateSubCaseHandler, EvaluateDecisionHandler, BesluitvormingActivateHandler, BesluitvormingPublishHandler, DossiqEnsureCommitteeNode and DossiqSetVoorstelStatusNode. A structural test walks all three flow-facing directories and fails any storage-performing file that ships bare, with a closed, reason-bearing allowlist for the four files the worker never executes; a per-handler regression test pins MergeTemplateHandler's write to the run's runAs identity and the loud refusal of one that resolves to nobody. * test(flow): name what the committee node test covers and leans on
…snapshot (#1647) * fix(flow): a handler's save no longer clobbers writes made after its snapshot Every case-saving flow handler (setStatus, setField, evaluateDecision, mergeTemplate) full-saved its flow-item snapshot through the PUT-semantic saveObject(), erasing whatever other writers stored after the snapshot was taken. Measured live on the closure rig (case a53cfc92/dc16d6dd, audits 512->515 and 725->728, same second): the document step wrote besluitDocument and the status step one hop later saved the older snapshot over it. - CaseFieldWriter: handlers now apply ONLY their own fields to the STORED case, through patchObject() when the installed OpenRegister has it, else by re-reading and applying the delta to the fresh read. - ActionResult.caseChanges + DossiqFlowNodeBase: a handler's writes travel with the outgoing item, so the next step's snapshot is current. - Case schema: declare the five signal-written fields (aanvulling, voorbereiding, toetsRegisterB, tweedeToets, commissieBesluit) the store silently dropped; extend the declaration test's writer walk to handler-written fields (signalKey, setField, evaluateDecision outputs, targetField) so the next undeclared one reds before it ships. - e2e closure: assert run status 'stopped' per the engine contract (EndNode throws FlowStop; 'completed' is only a drained marking). * fix(flow): give the partial-write closures an honest void shape PHPStan flagged the arrow functions: a fn returning the result of a void method returns null while declaring mixed. Statement closures with a void return say what happens. * test(flow): list CaseFieldWriter as used code in the strict-coverage suites All six PHPUnit matrix cells failed on 4 risky tests: the handler suites carry @Covers metadata, phpunit.xml sets beStrictAboutCoverageMetadata and failOnRisky, and under CI's Xdebug coverage driver the new CaseFieldWriter counts as executed-but-unlisted code. Local runs had no coverage driver, so the risky verdict never fired; reproduced red in a php:8.3 container with pcov scoped to lib/ (pcov's auto directory picks src/ and hides lib/), and green with these @uses lines. The real openregister at development was also checked: patchObject(objectId, data, register, schema), find(id, register:, schema:) and runAs(user, operation) all match the writer's calls, and the functional suite passed in every cell.
…age (#1649) The flows index is a named-source page, so it has no register+schema and CnPageRenderer cannot infer a detail page for it: without an explicit target the row click and the row menu's Edit fall back to the source's /flows path instead of the page this manifest declares. rowRoute names the FlowDetail route, the same wiring openregister#3315 gave its own flows page — CnPageRenderer then auto-sets rowClickToView and routes row-click / edit-open through onRowOpen to FlowDetail. Pairs with the nextcloud-vue fix that collapses the flows row menu to one live Edit plus Copy and Delete; the note is updated to describe the page as it is (an index over the named source, not a custom page).
…er apps register case types (#1646) * feat(nav): gather the work surfaces under "My work" "Work queue" becomes "My work", and it now holds the four surfaces a handler actually works from: the cases assigned to them, every case, their tasks, and the workflow board. WHAT MOVED, AND WHY THE INNER PAGE WAS RENAMED TOO -------------------------------------------------- The group and one of its children were BOTH called "My work" once the group took the name. A sidebar reading "My work > My work" says nothing about what the inner entry holds, so the page — a case index scoped to `assignee = current user` — is now "Assigned to me", which is what it actually shows. "Cases" is no longer a separate top-level entry. It moves into the group as "All issues", which is the same index unscoped. That is a relocation rather than a removal, so its ROUTE is untouched: /cases still resolves for bookmarks, shared links and the specs in this suite that target it. "Tasks" gains a menu entry. The PAGE already existed and was reachable only by URL. SENTENCE CASE ------------- "All issues", not "All Issues". Voice rule 8, and every existing label in this sidebar already follows it ("My work", "Workflow board"). THE TRANSLATION KEYS ARE THE LABELS ----------------------------------- "Assigned to me" and "All issues" are new keys, so both needed en.json and nl.json and a catalogue rebuild. Nothing automated catches a miss here: the l10n check scans `t()` calls in source, and a manifest label is translated per-field by nc-vue rather than through a literal in code. A renamed label passes every gate and then shows up in English on a Dutch instance. Verified: manifest Ajv PASS (0 errors), lint 0, l10n en/nl key sets match, catalogues rebuilt and current. * feat(case-types): let other apps register the work they handle * feat(case-types): let other apps register the work they handle * fix(icons): register FormatListChecks, so the Tasks entry has one gate-60 caught this: the icon IS in the ADR-077 vocabulary but was not registered in src/icons.js, and an unregistered name renders as NO ICON AT ALL rather than falling back to something. The new Tasks menu entry would have shipped blank. Registering it is the fix rather than swapping to a glyph dossiq already has: pipelinq uses FormatListChecks for its Tasks entry, and ADR-077 exists so the same concept carries the same glyph in every app. Picking a different one here to dodge the gate would have traded a missing icon for an inconsistent one.
…hem (#1654) * refactor(decisions): dossiq stops authoring decisions; decidiq owns them The audit, the umbrella change, and the unambiguous retirements. - openspec change dossiq-decisions-to-decidiq: full A/B/C/BLOCKED audit table, decidiq capability matrix, grey areas recorded for a ruling. - Remove CaseDecisionsTab (local decision create/edit/delete, mounted on no page) and VoorstelCreateDialog (orphaned local proposal dialog). The besluitvorming leaf authors; the read-only case-decisions widget displays outcomes. - Deprecate the DMN decision-table stack in place; retirement is blocked on openregister flow-decision-tables and recorded as such. - LocalDecisionAuthoringTest: closed, reason-bearing allowlists for decision-schema writers and DecisionTableEvaluator consumers. A new local decision writer or evaluator consumer fails the suite. * refactor(decisions): dossiq stops authoring decisions; decidiq owns them The audit, the umbrella change, and the unambiguous retirements. - openspec change dossiq-decisions-to-decidiq: full A/B/C/BLOCKED audit table, decidiq capability matrix, grey areas recorded for a ruling. - Remove CaseDecisionsTab (local decision create/edit/delete, mounted on no page) and VoorstelCreateDialog (orphaned local proposal dialog). The besluitvorming leaf authors; the read-only case-decisions widget displays outcomes. - Deprecate the DMN decision-table stack in place; retirement is blocked on openregister flow-decision-tables and recorded as such. - LocalDecisionAuthoringTest: closed, reason-bearing allowlists for decision-schema writers and DecisionTableEvaluator consumers. A new local decision writer or evaluator consumer fails the suite.
…nregister's case layer) (#1657) * spec(openspec): open retire-cmmn-caseplanstate, draining the CMMN runtime onto openregister's case layer * fix(manifest): declare requiredApp on the humaniq hours tile Gate-55 (detail-page-discipline) now requires a widget reading another app's register to declare requiredApp. The case-kpis-hours tile on CaseDetail sums humaniq TimeEntry hours; without the declaration it queries an app that may not be installed and renders 0 instead of a set-up state. CnDetailWidgetHost reads the flag and shows the not-installed state without issuing the request.
…ck (#1651) * feat(case-detail): one strip for the case's collections, one data block Twelve grid cells where there were twenty-two, nine tabs where there were six. Dropped: case-process the second Data block. Its Status field is FOLDED into case-core rather than lost, carrying the editable:false override with it — without that the field becomes click-to-edit and a caseworker can set a status the workflow owns. case-documents documents case-voorstellen proposals case-assistant case assistant cmmn-case-plan adaptive case plan Folded into the tabs widget: case-decidesk-decisions Decisions (decidiq's) case-contacts Contacts case-locaties Locations Dossiq stops rendering its OWN decisions. case-decisions (an object-list over dossiq's decision schema) and case-kpis-decisions (its count) are gone; the Decisions tab is decidiq's widget. This is the surface-only first step of moving decisions to decidiq wholesale — the schemas, services and ZGW controllers are untouched here and follow in their own change. Checked after rebuilding the layout: no widget referenced but undefined, none placed twice, none both tabbed and placed, none defined but unplaced. Each dropped widget had exactly two manifest hits — its definition and its layout cell — so nothing else referenced them, and no e2e asserted them. The Proposals/Voorstellen hits in the suite are the standalone /voorstellen page, untouched here. 'Contacts' is a new l10n key (nl: Contacten); 'Connected contacts' was too long for a tab in a nine-tab strip. Inserted textually rather than by re-serialising the catalogue — round-tripping it through a dict and sorting rewrote all 3437 keys and buried the change in a 1171-line diff. check:manifest, test:l10n, prettier, eslint over src+tests+scripts, and all 19 applicable hydra gates pass. * fix(case-detail): clean up after dropping the widgets, and gate the hours tile Four things the full-repo gate run caught that the diff-scoped one did not. CI forces a full run (77 gates); scoping to the diff only opened 19, which is why this branch went out red. 1. gate-53 — dropping case-voorstellen orphaned the Voorstellen route. Its menu entry had been removed earlier on the basis that something else linked it, and that something was this widget's viewAllRoute. Voorstellen was the ONLY removal in menu-layout.json with no removalsReplacedBy entry. The menu entry is restored: proposals leave the case page as intended, and the index stays reachable. Not deleted, deliberately. Voorstellen is the parafering approval surface — VoorstelDetail is the parafeerroute multi-step approver flow — and #1632 moved parafering onto the flow engine end to end yesterday. Parafering is to be rebuilt on flows and decidiq with the current code DEPRECATED, which is a migration, not a deletion, and tearing out the UI mid-migration would take the work with it. 2. The slot map still pointed widget-case-assistant and widget-cmmn-case-plan at components whose widgets no longer exist. Removed, along with their registry entries and the three now unreachable components (CaseAssistantPanel, CmmnCasePlanPanel, CmmnPlanItemNode — they referenced only each other). 3. gate-55 — case-kpis-hours now declares requiredApp: humaniq. This is the gap left open when the tile shipped: it aggregates humaniq's register, so on an install without humaniq it 404s and renders 0, which is what a real zero renders. nextcloud-vue#926 makes the host render a set-up state instead, and .github#663 makes the gate refuse the next one. 4. e2e — the Locations spec asserts the case-location REQUEST rather than the empty state, because the two look identical. Locations is now in a LAZY tab, so the widget never mounted and never queried; the poll timed out on zero responses and read as the 404 returning. The spec opens the tab first. All 77 applicable gates green, check:manifest, test:l10n, prettier, eslint over src+tests+scripts, and all 6 specs still collect. * style(manifest): keep the 2-space indentation development uses A direct 'npx prettier --write src/manifest.json' reformatted the file to tabs. The repo's own 'npm run format' leaves this file alone, so development keeps it at two spaces — calling prettier on it directly rewrote all 9391 lines around a three-line change. Third time today a re-serialising or reformatting writer has buried a small change: the l10n catalogue, the manifest schema, and now this. The diff against the previous commit is the 5 lines it should be. * docs(menu): record why Voorstellen came back, and correct the count The removalsCoverageNote still said 15 entries and still described Voorstellen as needing no waiver 'because CaseDetail's case-voorstellen widget carries viewAllRoute'. This branch dropped that widget, so the sentence documented a route that no longer existed. Voorstellen was the only removal in that list resting on a single widget rather than on a waiver, which is exactly why a layout change could orphan it and none of the other thirteen. Restored rather than waived: naming a replacement surface would have asserted something untrue, since no other surface carries proposals. Parafering is moving onto flows and decidiq under parafering-to-decidiq and approval-routes-are-flows; whichever change retires the proposals surface for real should re-remove this entry with a waiver that is true at that point. Also restores the multi-line integrationsSection that a direct prettier call collapsed. All 77 applicable gates green. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
* spec(termijnbewaking): umbrella change, deadlines move onto engine timers * feat(termijn): phase 1, the core termijn engine moves onto engine timers * fix(termijn): quality pass, registrar split and derived accrual simplification * chore(deps): hydra-gates 1.11.0, the vendored manifest schema catches up with the page types * docs(termijn): task 1.1 reflects the reused generic resolver
* fix(e2e): follow the nav labels #1646 renamed dossiq#1646 gathered the work surfaces under a 'My work' group and renamed three entries at once: Cases became 'All issues', the My work PAGE became 'Assigned to me' once the GROUP took that name, and Voorstellen became 'Proposals'. It added work-navigation.spec.ts for the new structure but left every OTHER spec asserting the old labels. Seven specs have been red on development ever since — navigation.spec's three tests and pages.spec's Cases page — and this branch is where they get fixed rather than inherited. Worse than the seven: three call sites wrapped navTo in . Those did not go red. navTo throws by name when no label matches, the catch swallowed it, and the spec carried on against whatever the Dashboard renders — deelzaak-support, related-case-linking and cases-crud have been asserting against the wrong page rather than failing. navigation.spec now matches what the nav renders: Dashboard is the only top-level visible leaf; 'All issues' is asserted with the other collapsed-group leaves by href, because display:none removes an element from the accessibility tree and getByRole resolves to zero even under toHaveCount; the group header is 'My work'. The click test EXPANDS the group and then clicks, rather than navigating to the href. Going straight to the href would pass without the group ever opening, and opening it is precisely the interaction a reader depends on. navTo now accepts a RegExp as well as an exact string, and the call sites pass one covering both locales. A pinned exact label is a standing tripwire: this rename broke every call site naming the old string at once, and the next one will too. 217 tests still collect across 48 files; all 74 applicable gates green. * fix(e2e): follow the nav labels #1646 renamed dossiq#1646 gathered the work surfaces under a "My work" group and renamed three entries at once: Cases became "All issues", the My work PAGE became "Assigned to me" once the GROUP took that name, and Voorstellen became "Proposals". It added work-navigation.spec.ts for the new structure but left every OTHER spec asserting the old labels. Seven specs have been red on development ever since — navigation.spec's three tests and pages.spec's Cases page — and this branch fixes them rather than inheriting them. Worse than the seven: three call sites wrapped navTo in a catch that swallows everything. Those did NOT go red. navTo throws by name when no label matches, the catch discarded it, and the spec carried on against whatever the Dashboard renders — deelzaak-support, related-case-linking and cases-crud have been asserting against the wrong page rather than failing. navigation.spec now matches what the nav renders: Dashboard is the only top-level visible leaf; "All issues" is asserted with the other collapsed-group leaves by href, because display:none removes an element from the accessibility tree and getByRole resolves to zero even under toHaveCount; the group header is "My work". The click test EXPANDS the group and then clicks, rather than navigating to the href. Going straight to the href would pass without the group ever opening, and opening it is precisely the interaction a reader depends on. navTo now accepts a RegExp as well as an exact string, and the call sites pass one covering both locales. A pinned exact label is a standing tripwire: this rename broke every call site naming the old string at once, and the next one will too. 217 tests still collect across 48 files; all 74 applicable gates green. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…els via integriq's ADR-041 seam (#1664) * feat(publication): deliver besluit publications through integriq's ADR-041 seam; declare publications on the case schema * style(publication): analyzer stubs for the optional integriq contract, simpler terminal projection
… and records (#1666) * feat(parafering): the runtime moves to the decision app; dossiq raises and records dossiq delegates parafering exactly like it delegates decisions now: raise a voorstel's chain in the decision app, wait for its ApprovalRouteConcludedEvent, and keep only case-data records. The local engine retires with no facade — BesluitvormingParafeerService, ParafeerActieService, the parafeer-actie controller and routes, and the whole dossiq-local flow projection (ParaferingFlowGateway and its per-route cutover flag, the paraaf nodes, the resume listener, the projection migrator). The front-end sign-off surface goes with it; the case page now shows the recorded history read from OpenRegister. ParaferingRaiseService raises and fails closed. ParaferingConclusionService + ParaferingConcludedListener record the outcome, preserving onBehalfOf and mandate and keeping the frozen procest.parafering.* audit trail. RaiseInFlightParaferingenInDecidiq re-raises voorstellen already mid-parafering. LocalParaferingRuntimeTest pins that no local engine returns. Depends on decidiq's parafering-route-runtime, which MUST merge first. * docs(parafering): tag the parafeeractie read service with its spec * style(parafering): prettier-format the reworked surfaces and prune stale eslint suppressions The retirement deleted files whose suppression entries then pointed at nothing, and eslint refuses a suppression that no longer occurs. Pruned with eslint's own --prune-suppressions, which also dropped entries already stale on the base; the tree lints clean (warnings only) and formats clean.
…esting (#1671) Reported on dossiq: hovering a KPI tile paints a second, smaller card inside the tile. decidiq does not, which made it look app-specific. It was not.⚠️ THE DIFFERENCE IS AN INSET, NOT A COMPONENT. A card widget renders `flush` and then has its padding put BACK by the card-fit rule (`padding: 8px 14px`), so the KPI sits inside the wrapper while the WRAPPER draws the border, radius and background the user reads as "the card". The library's `--clickable:hover` rule then drew a 2px border and a drop shadow on the KPI itself, 8-14px in from the edge being hovered. Measured live, while genuinely hovered: dossiq card-fit stat widget inset 9px top / 15px left 2px rgb(0,103,158) + its own shadow decidiq stats block, no card-fit inset 1px all round border lands on the wrapper's edge decidiq was never a different component. Its tile is not card-fit, so it has no padding to be inset by. That is the whole difference. nextcloud-vue#932 moves the affordance to the wrapper: the inner border and shadow are suppressed on card-fit hosts and the wrapper takes them instead. No layout change, nothing moves, one card outlines. Verified in the browser with a real pointer hover before and after: before inner 2px rgb(0,103,158) + shadow wrapper grey, no shadow after inner transparent, no shadow wrapper rgb(0,103,158) + shadow Released as 2.31.1 and confirmed present in the published dist/nextcloud-vue.css, which is what `@conduction/nextcloud-vue/css/index.css` imports, and in this app's built bundle after the bump.⚠️ This is not a dossiq-only fix. Every app with clickable stat, gauge or delta tiles had it, pipelinq included. They pick it up on their next nextcloud-vue bump. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…pped (#1669) The consolidation rebuilt the CaseDetail layout from a list of tuples carrying only id, widgetId and the four grid coordinates. Seven cells had also carried a showTitle flag, and rebuilding silently dropped it. The result is visible on every case: the three KPI tiles, the core data block, the tabs strip and the hours tile each draw a CnWidgetWrapper header on top of content that already names itself. "Time left" renders twice, and a read-only KPI tile grows an Actions menu the Cards-vs- Widgets split says a card must not have. Restored on the six survivors as false, and on initiator as true. case-process, case-assistant and cmmn-case-plan also carried a flag and are gone, so theirs goes with them. Nothing caught this. Every gate passed, the manifest validated, and the E2E was green, because no assertion described what a KPI tile is supposed to LOOK like — only that its value renders. So this adds one: the label appears exactly once in the tile's own grid cell, and the cell carries no Actions button. It fails against the dropped-flag manifest and passes against this one. The wider lesson is the same one that rewrote 3437 l10n keys and 6612 schema lines earlier today: rebuilding a structure from a narrower in-memory shape silently discards whatever the original carried that the shape has no field for. Diff the result against the original, not against the intent. All 74 applicable gates green; 7 tests collect in the spec (was 6). Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…e copies Four changes that all reduce to the same thing: dossiq had its own answer to questions the platform already answers, and its own word for things it already had a word for. They interleave in the manifest and the l10n files, which is why they arrive together. VOCABULARY. The sidebar said "All issues" for the list of cases, the case detail's document tab said "Dossier", and the demo-data step offered "example dossiers". Three words for one thing, in an app named after the case file. The Dutch was right all along: "Alle zaken" was already the translation of "All issues", which is what a drifted source string looks like. Left alone deliberately: "Issue advice" is the verb, and "Systemic issues" in process mining names a defect, not a case. Also fixes a half-Dutch screen, where the enforcement page's title was "Handhavingsstrategie" under an English label. STORE (ADR-080). OpenRegister owns store discovery and shipped GenericStoreService; dossiq never adopted it, so an administrator who wants a case type builds it by hand. StoreController injects the engine client by composition, never extends: a cross-app base is resolved by the autoloader, and Nextcloud's router reflects every controller during route matching, so an absent OpenRegister would 500 every route in this app rather than only the store's. Install is dossiq's own and writes case CONFIGURATION only. That allowlist is a security boundary: without it, Install is a remote write primitive pointed at live case records. Its refusal tests carry a negative control. The registry token is write-only, and an empty field on save keeps the stored one, because the form cannot show it and so posts empty on every unrelated edit. With no registry the engine answers not_configured without a network call, which is the only reason this surface may carry the word Store. PARTNERS. A ketenpartner is an organisation, and OpenRegister owns Organisation. The superset claim was verified against a RUNNING instance rather than the entity class, because a property dropped by jsonSerialize() is invisible to the picker either way. The rows move on upgrade rather than waiting for an occ command, which is what makes retiring the page safe in the same change. ENFORCEMENT VOCABULARY (dossiq#1596). RenameDutchValues translated ONE member of the LHS actorType axis, overheid -> government, and could not reach the matrix cells because they live inside a JSON column. The recommendation schema then offered a value no cell could match, so a quarter of the national enforcement strategy threw "Geen LHS-cel gevonden" — which reads like bad input, not a broken axis. The tell that this was a slip: the other three axis values were never translated. A set is translated whole or not at all. Four gate and test findings on the way, all real. StoreOutline was not registered in src/icons.js, which renders NO icon rather than a fallback. The "cases list" visual baseline resolved the label 'Cases', renamed in #1646; shootByNav guards the click behind isVisible, so it had been shooting the DASHBOARD under the name cases.png ever since. work-navigation's "reachable by direct link" test used a hash deep link under createWebHistory, so it too asserted against the Dashboard. And two spec-coverage helpers wrapped the fail-fast nav helper in .catch(() => {}), which would have run those specs against whatever rendered.
…'s signalAs and native runAs scoping (#1674) * refactor(flow): adopt the engine's signalAs and native runAs scoping openregister#3332 gave the flow engine the two seams every consumer was rebuilding; this is the dossiq follow-up it named. - Delete lib/Service/FlowRunAsScope.php and its test: RegistryStepDispatcher now executes every contributed node inside ObjectService::runAs() as the run's validated acting identity, so the twelve local wraps (3 nodes, 8 handlers, 1 guidance comment) unwrap. - TaskCompletionResumeListener collapses the assignee guard + mapper resolve + bare signal() into one FlowRunSignalService::signalAs() call addressed at the task's node, obeying FlowSignalRefused per reason. - The DQ#1644 scanner inverts from 'flow files must wrap' to 'flow files must NOT wrap', and pins the wrapper file as deleted. - Test stubs for FlowRunSignalService and FlowSignalRefused match the real dev signatures; per-handler runAs tests retire with the wrapper, each suite noting the duty moved to the engine (RegistryStepDispatcherRunAsTest). * feat(kcc): route contact moments through OpenRegister's decision tables Wave 4 of the fleet's engine consolidation (engine: openregister#3329, reference adopter: humaniq#289). The KCC routing rules were dossiq's last private rule matcher; they now compile into one inline decision table (hit policy FIRST, enabled rules ascending by priority) evaluated by the shared pure DecisionTableEvaluator, constructed directly. - The KCC dialect stays app-side as derivations: the subject+summary haystack, keyword/regex predicates as boolean columns, the KvK customer-type rule, folded time-of-day windows, day of week. - Equality cells are quoted literals so a channel named '-' stays a literal; a rule the legacy engine could never match compiles to nothing. - Fails closed without the shared engine; no fallback matcher. - RoutingEngine::evaluate() stays only as the parity oracle (deprecated, no new callers): KccRoutingParityTest sweeps a 40-cell matrix over both paths. Agent ranking is untouched. Staged retirement in the change's tasks. - tests/Stubs/Service/Dmn are now verbatim copies of the engine's pure Dmn classes (openregister@2839ab901a), so the standalone suite proves the real semantics. * style(kcc): expand the day-of-week ternary the shared phpcs ruleset refuses
… retires
Phase 2 of lhs-matrix-is-a-decision-table. The LHS matrix is severity by
behaviour by actor type yielding an intervention, which is a decision table
and nothing more exotic, and OpenRegister already carries one evaluator for
that shape.
recommend() now evaluates the projected table and reads the matrix only where
no projection exists. The fallback is not hedging: projecting a table needs an
owner for the object it writes, so it is an occ command a person runs and
cannot happen unattended on upgrade, and an instance that has not run it must
still be able to enforce. The Enforcement strategy settings page and menu
entry are retired; authoring lives in the Decision Tables (DMN) admin tab.
The projection now arrives ENABLED. Shipping it disabled was right in phase 1,
when the matrix was still the lookup and a second answering table would have
been a second source of truth. It is wrong now: with the evaluator as the
lookup, a disabled table silently hands the question back to the matrix and
makes the migration a no-op that reports success. The test that asserted
`enabled === false` is INVERTED rather than deleted, because the flag still
matters, just the other way round.
Two defects in phase 1 that only phase 2 could expose:
- a re-run minted a SECOND table. saveObject() was called with no id, and
the lookup resolves by provenance marker and takes the first match, so
which duplicate answered an enforcement question was arbitrary. A re-run
now resolves the existing table and updates it.
- nothing protected an edited table. The projection is one-way and the
matrix no longer has a settings page, so an overwrite would replace an
administrator's work with a source they cannot read. A re-run whose rules
differ now refuses and says so.
That guard could NOT be a byte comparison, and the first version was.
OpenRegister drops an empty-string property on save, so a rule projected with
`annotation: ""` comes back without the key. Measured against a running
instance, all 48 rules "differed" immediately after the run that wrote them —
a guard that refuses every re-run is not cautious, it is broken, and it would
have read as the migration being permanently stuck. It compares the decision
each rule encodes now: id, inputs, outputs, keyed and sorted.
Verified live on a dedicated instance: first run created 48 rules, second
reported updated with exactly one marked table, an edited rule was refused and
survived, and a restored table updated again.
Also fixes the third declaration site of the vocabulary split, which gate-101
found after the first two were fixed by hand: a demo lhsRecommendation still
carried `government`, so a fresh instance shipped a record the lookup could
never resolve. LhsAxisVocabularyTest now asserts every site rather than the
two that happened to be noticed.
The commit before this one carried only the new LhsDecisionTableLookup file.
`git commit` without paths commits the INDEX, and the index held just the one
`git add`ed file, so the fifteen modified files it described were left in the
working tree and never reached the branch. The message was accurate about the
work; the commit was not. This is the rest of it, pushed forward rather than
amended because the branch is shared.
- recommend() evaluates the projected decision table and reads the matrix
only where no projection exists
- the projection arrives ENABLED, and its test is inverted rather than
deleted, because the flag still matters the other way round
- a re-run resolves the existing table by marker and updates it instead of
minting a second one carrying the same marker
- a re-run REFUSES a table whose rules were edited, comparing the decision
each rule encodes rather than its bytes: OpenRegister drops an
empty-string property on save, so a byte comparison called all 48 rules
edited immediately after the run that wrote them
- the Enforcement strategy page, its menu entry and its menu-layout entry
retire; authoring moves to the Decision Tables (DMN) admin tab
- the demo lhsRecommendation stops carrying `government`, the third
declaration site of the vocabulary split and the one gate-101 found after
two were fixed by hand
Resolves the l10n conflicts, which were the reason CI had not run. A CONFLICTED PR DOES NOT GET pull_request WORKFLOW RUNS. GitHub cannot compute the merge commit those workflows run against, so it dispatches nothing — and the PR still showed four green checks, from the push and dynamic events, against the forty-nine its siblings were running. Four green looked like a pass and was the absence of the test. Both sides appended new keys to the tail of en.json and nl.json, so the resolution is a UNION: taking either side alone drops the other change's strings silently, and every one of them is a string a user reads. The .js files are generated, so they are regenerated from the resolved .json rather than merged by hand. Verified afterwards: no duplicate keys on either side, en/nl key sets still match, and no conflict marker survives anywhere in the tree.
`npm run format` is a real CI leg here, listed deliberately because the shared workflow has none: without it the tree drifts back out of format between merges. These three were written by hand and never passed through it. Formatting only — eslint, vitest, the manifest validator and the l10n check all still pass unchanged.
…ine (#1678) The inventory (openspec/changes/case-status-onto-engine-lifecycle) maps every transition source and records the verdicts. What this commit takes: - delete WorkflowEngineService, a dead second entry point to the transition engine (zero production callers, only its own test) - retire the dead vergadering mini-engine (VergaderingCaseService, VergaderingDeadlineJob, tests, info.xml registration): it wrote literal status strings into case.status, a statusType-reference field, and scanned for cases that cannot exist; termijnbewaking task 2.4 is amended to record the retirement (nothing to migrate) - fix the complaint x-openregister-lifecycle in both register manifests: it was in the legacy list dialect, at the wrong nesting level, with Dutch state names against an English enum, so the declared machine was dead as written; now object-form, enum-anchored and equal edge for edge to ComplaintService::TRANSITIONS - add LocalStatusMachineryTest, the class-catching scanner: retired classes stay retired, the transition-table census is closed and reason-bearing, and the complaint declaration parity is pinned StatusTransitionService stays the single write path for the per-caseType dynamic case machine, with the line recorded in the change design.
* feat(cases): a New case dialog a case handler can actually fill The button opened a properties-and-JSON table listing all 48 of the case schema's properties, unordered, with `qualityScore`, `casePlanState` and `archiveNomination` among the first screenful. Three changes, in the three places that were actually wrong. The schema now says which of its properties a person deals with. Fourteen carry an `order`, so every surface that reads `fieldsFromSchema` (the create form, the detail data widget, generated table columns) puts case type, title and description where a handler looks for them. Twenty are marked `visible: false`: engine-written JSON, computed quality scores, flow signal payloads, denormalised projections. Nobody types those and nobody reads them raw. `extensionCount`, `handoffSource` and `workflowVersion` are deliberately NOT among them: the case detail Process widget lists them explicitly, and `visible: false` is evaluated before the include whitelist, so hiding them would have blanked three cells with nothing to show for it. The New case action then narrows to the nine fields somebody filing a case types. The detail page still edits the rest; one schema, two surfaces. `case.caseType` declares `x-openregister-extends-form`, so choosing a case type adds that type's own propertyDefinitions to the form and writes the answers as caseProperty rows once the case exists. dossiq has modelled the ZGW eigenschap pattern for a long time and never rendered it anywhere. And the properties tab, which is where those definitions are authored, was writing three fields into a void. `format` was never a propertyDefinition field; the schema has always called it `propertyType`, so every type an admin chose was dropped and read back as the default. `maxLength` and `requiredAtStatus` were not declared at all, so a 200-character cap read back as no cap. The tab now writes `propertyType` from the schema's own enum (its old `text` and `datetime` were not even members of it), the schema declares the other two, and `requiredAtStatus` stores the status reference rather than its display name, which would have broken on the first rename. A Required toggle is new: without `isRequired` nothing could ever be made mandatory. Also prunes 10 files' worth of eslint suppressions that no longer fire. * fix(l10n): the new properties tab strings, in both catalogues The type dropdown, the Required toggle and the resolved requiredAtStatus label are eight new source strings. Adds them to en.json and nl.json, rebuilds the .js mirrors, and lowers the schema-l10n baseline by the 41 it improved. * fix(cases): un-hide the eight properties a handler or a widget is meant to see Eight case properties carried `visible: false` while something in the app asked to display them, which `fieldsFromSchema` resolves in the worst order: the visibility check runs BEFORE the include whitelist, so a widget that names the property renders a blank cell and says nothing. description, startDate, endDate, plannedEndDate, result ordered by this change extensionCount, handoffSource, workflowVersion named by the Process widget The case DESCRIPTION was on no surface at all: not the create form, not the detail data widget, not the table. The case detail Process widget lists ten fields and six of them could never render. The rule applied is mechanical rather than taste: a property that carries an `order`, or that a manifest `include`/`columns` list names, must not be hidden. Twenty-nine engine-written properties stay hidden. Also drives the case-type picker from the combobox rather than the field wrapper in the e2e. Clicking `[data-cn-field]` leaves NcSelect closed, so the run failed twenty seconds later on a missing option instead of at the missed click. * build(deps): @conduction/nextcloud-vue 2.31.0, for the open-form action keys The manifest declares includeFields on the New case action, and the v2 schema is additionalProperties:false, so gate-22 and gate-53 both rejected it against 2.29.0. 2.31.0 carries manifest schema 2.28.0. The range moves too, not just the lockfile: `^2.29.0` would let a fresh resolve pick a version whose schema does not know the key, and the failure would read as a manifest defect rather than a dependency one. * docs(openspec): the change the create-form work implements The e2e tags pointed at openspec/changes/friendly-case-create-form, which did not exist — a dangling anchor that spec-anchor-existence rejects and that tells a later reader nothing. Adds the proposal, tasks and spec, and re-anchors each e2e test at the requirement it actually exercises rather than at tasks.md. Also corrects a comment in the spec that asserted the seeded case type sorts last in the picker's preloaded page. The picker does cap that page, which is why the test types; the ordering claim was never measured. * docs(openspec): drop the em-dashes, which also broke the e2e anchors The requirement headings read `REQ-FCF-001 — The New Case Dialog…`, and a heading's anchor slug renders that em-dash as a DOUBLE hyphen (`req-fcf-001--the-new-case`). The @e2e tags pointed at the single-hyphen form, so both anchors dangled: spec-anchor-existence would have rejected them and a reader clicking through would have landed nowhere. Removing the em-dash fixes the anchor and satisfies the voice rule that bans em-dashes in documentation, which is the same edit either way. Verified by slugifying every heading and checking each tag against the set. * fix(e2e): type in the case type picker only when the option is not preloaded The spec typed the run prefix unconditionally. Typing sends the term to the server and REPLACES the preloaded options with whatever comes back, so on CI it turned a list that already held the seeded case type into an empty one and the option was 'not found' twenty seconds later. Now it types only when the option is absent from the preloaded page, which is both what a handler does and what keeps the preloaded path intact. * fix(e2e): match the picker option by text, and name the answer row Two defects in this spec, both found by running it rather than reading it. vue-select splits an option label into adjacent spans, and Playwright joins those with a space when it computes an accessible name — so `E2EZAAK-…-4944 Subsidie` computed as `E2EZAAK-…-494 4 Subsidie` and an exact name match never hit while the option sat plainly on screen. The same rendering shows in every snapshot ("Omgevingsv ergunning"), which is easy to read past as cosmetic. textContent concatenates without the separator, so hasText sees the label the way a reader does. The last assertion indexed `answers[0]` and read back the enum's default 'Sport'. Both questions ARE answered and the API promises no order, so it looked like the ceiling had never been written when the feature was correct all along. It now finds the row whose propertyDefinition is the ceiling's. All four tests pass against a clean instance. * fix(l10n): the new schema strings read like helper text, and are translated A schema property's description renders as helper text under its field. I had written these two for a code reviewer — 'OpenRegister has always discarded it, because the schema did not declare it' — which is the right thing to say in a commit and the wrong thing to put under an input. Rewritten short and user-facing, then added to en.json and nl.json with the titles, so check:schema-l10n counts them as covered. The baseline moves 2814 -> 2825 for the 11 strings that arrived with the development merge; those are other schemas' additions. It stays well under development's own 2855, so the ratchet still tightens rather than slackens. * fix(e2e): keep the case type, delete only what this spec filed The afterAll deleted the case type it created. That is what a tidy fixture looks like and it is why an unrelated spec went red: anything still holding that id — a case another spec filed, the dashboard table resolving caseType for a label — then fetched an object that was gone. It surfaced as ui-pages.spec.ts failing its 'dashboard mounts without console errors' assertion on a 404, a different file asserting about a different page. The trace named it: 404 /apps/openregister/api/objects/dossiq/caseType/7bf85ae4-… Cases and their answers are still cleaned up. The case type and its property definitions stay: CI throws the instance away after the run, so a leftover type costs nothing, while a dangling reference costs a red suite pointing at the wrong file. (The other 404 in that trace is /apps/hermiq/api/chat/health — hermiq is not installed on the runner. Pre-existing, unrelated.) --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
The coverage ratchet failed with "coverage of the code this change KEEPS or
ADDS dropped by 11.79%; this change adds 324 statements to those files", and
it was right: LhsDecisionTableLookup, MigratePartnersToOrganisations and
RealignLhsActorTypeVocabulary all shipped untested.
The lookup is tested against OpenRegister's OWN evaluator, the verbatim copy
under tests/Stubs, not a hand-written fake. That mattered immediately: the
first fixture used quoted FEEL literals and failed, which sent me to check
what the migrator actually writes — bare values. Aligning the fixture to the
migrator's real output is what makes these tests evidence that the projection
EVALUATES, rather than evidence that a fake agrees with its caller. The
projection and the overwrite guard had been proven live; evaluation had not.
What each file pins is the behaviour that would be invisible if it broke:
- the lookup DECLINES rather than answers — on a disabled table, on another
matrix's table, on an unresolvable evaluator, on an unconfigured instance.
Every decline hands the question back to the matrix, so a decline it
should not have made leaves no trace in the result.
- it resolves by MARKER, so a renamed table still answers.
- the realign step re-checks each row before writing, because the filter is
sent server-side: a backend that cannot express it returns everything, and
a naive rewrite would relabel every citizen and business as a government
actor — worse than the defect being repaired.
- the partner step NEVER throws. An exception aborts the upgrade, and a
partner that has not moved is recoverable where a half-upgraded instance
is not.
2778 tests now, up from 2761. phpstan, prettier and eslint unchanged.
Say cases, adopt the store, and stop keeping private copies of what OpenRegister owns
… none Two loose ends from the label fix that shipped in #1677. THE CASES BASELINE WAS A SCREENSHOT OF THE DASHBOARD, and now there is proof rather than an inference: `cases-visual-linux.png` and `dashboard-visual-linux.png` were byte-identical, same md5. `shootByNav` resolves its label behind `if (isVisible)`, so when dossiq#1646 renamed the entry the click silently never happened and the shot was taken wherever the app had landed — the Dashboard, under the name cases.png, for as long as that rename has been in. #1677 fixed the LABEL, which made the test correct and left the baseline wrong: the next visual run would have compared the real cases page against a dashboard image and failed, looking like a regression in the page rather than a stale artefact. Regenerated, and it now differs from the dashboard, which is the assertion that was never available before. The store baseline is new. gate-26 accepted the visual TEST added in #1677, but every other visual test carries a committed baseline and this one did not, so the first person to run the visual project would have hit a missing snapshot. Both shot against a dedicated instance. The `visual` project is deliberately excluded from CI — tests/e2e/playwright.config.ts is the config the workflow resolves, precisely so a CI Linux runner never tries to byte-match a dev-container PNG — so these are for local runs and carry no CI risk.
Found reviewing my own merged code from #1677, and it is a hole in the boundary that PR was largely about. OpenRegister resolves the object it writes FROM THE PAYLOAD: `extractUuidAndNormalizeObject()` reads `$object['@self']['id'] ?? $object['id']` and treats a match as the uuid to UPDATE. So a store item whose component carried the uuid of this municipality's live case type would replace it — and `saveObject` is PUT-semantic, so keys the payload omits are nulled rather than left alone. The case type would not merely change, it would be gutted. THE SCHEMA ALLOWLIST DOES NOT COVER THIS. It governs which schema a component may write, never whether the write creates or replaces. A component naming `caseType` — entirely legitimate, exactly what the store is for — is the attack. I wrote that allowlist and called it the boundary; it was half of one. Identity is not a remote registry's to supply. `id`, `uuid` and `@self` are stripped before the write, so an installed component is always a NEW local object. If install ever needs to be idempotent it must key on something dossiq controls. The test carries a negative control: removing the strip makes it fail.
`global task list page renders with add button and empty state` asserted `No items found` outright, so it could only pass on a register with zero tasks. The shared instance carries 8 task rows, measured on 2026-09-02, so the assertion could only ever fail there and had nothing to do with the feature. The scenario it covers is `task-management/spec.md#view-the-global-task-list`, and rows satisfy "view the global task list" better than emptiness does. It now accepts either the rendered rows or the empty state, and rejects neither. The Add Task button and the no-server-error assertion are unchanged, so the spec still fails if the page does not mount. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
The cases visual baseline was a screenshot of the dashboard
…verwrite A store install creates, and can never replace a local object
…ired arg order and the id filter that matches nothing (#1686) * fix(or-contract): repair the retired saveObject arg order and dead id filters the fakes kept green Two OpenRegister contract drifts shipped as green-CI/dead-runtime defects (live-rig proof, DQ#1663): - saveObject: 29 call sites still used the retired (register, schema, object) positional order; the real signature takes the object FIRST, so every one of them threw at runtime and, among other things, no beslistermijn timer ever armed. All 29 now go through named arguments or the SearchesObjects saveObjectAsArray bridge (which gains a uuid pass-through for update sites). - searchObjects: 7 call sites filtered on a top-level 'id' key, which addresses a schema property no schema declares and silently matches zero rows (the parafering raise never found its voorstel). They now use the resolving get-by-id path (ObjectService::find via findObjectAsArray, plus a StufRegisterAccess::findById for the StUF wrapper). - The adjacent shape drift the honest fakes then exposed: find() returns an ObjectEntity, never an array, so an is_array() check after it is a dead path. Fixed at the 12 sites the reworked fakes cover; the remaining sites of that class are catalogued for a follow-up. Tests that cannot lie again: FakeTermijnStore (and the per-test fakes it exposed) now declare the REAL saveObject signature - the retired order fatals in tests the way it does live - and their searches resolve ids only through @self/find, returning zero rows for top-level id filters exactly like the live service. OrContractRegressionTest pins both contracts explicitly. Also folded in: the live-journey e2e list assertion was pagination-blind (identifier-asc, 20 per page - the created case lands on the LAST page of any lived-in rig); it now uses the ?title= deep-link filter. * chore(ci): re-fire the pull_request dispatch that never arrived for this branch * test(repair): pin the LHS realign fake to the real saveObject signature The merged repair test's fake declared saveObject without the uuid parameter, so the trait's named-argument call errored and zero writes landed.
…tity-returning ': array' methods (#1694) * fix(or-contract): finish the is_array-on-find sweep the honest fakes catalogued PR #1686 fixed 12 sites of the class 'is_array() on the return of ObjectService::find(), which returns an ObjectEntity and never an array' and catalogued the rest for a follow-up. This is that follow-up, swept repo-wide rather than trusting the catalogue: - Bezwaar/* (15 find sites): AdvisoryCommitteeService, BeroepService, BezwaarCreationHook, DecisionService, HearingService and PanelIndependenceChecker all guarded find() results with is_array(), so every 'not found' guard fired on FOUND objects (committee referral, advice-request transitions, hearing attendance/minutes, beroep filing and cascade, decision publication) — the features always threw, or the dead true-branch silently skipped the work (panel independence author resolution, hearing case resolution). - Subsidie/* (3 find sites): BeschikkingService.publish, SubsidieService.transitionAanvraag and VaststellingService.finalize answered 'niet gevonden' for records that exist. - Singles (6): NoticeOfDefaultController.show (always 404), VoorstelBesluitController's IDOR gate (every voorstel unresolvable), DossierCompiler.compile (always 'Case not found'), CaseEmailRepository.findTemplate (always null), BeroepEscalationListener (dwingend derivation never ran), and InspectionChecklistService, whose get_object_vars() on an ObjectEntity read an empty array so the photo-required guard never fired. All go through the SearchesObjects bridge (findObjectAsArray + null check), as #1686 chose. The same files carried the adjacent drift #1686 named: ': array' methods returning saveObject()'s ObjectEntity — a TypeError on every live call. 22 such sites (Bezwaar 11, Subsidie 11) now go through saveObjectAsArray with an honest local fallback. Deliberate non-sites, left alone: ZgwService/ZgwRulesBase/ ZgwZrcRulesService and the sharing/transfer services handle both shapes via an explicit jsonSerialize() fallback, and SelectionReassignmentService reads the entity via getObject(). The per-file fakes that kept this green are repinned to the real contract (entity-shaped returns, DoesNotExistException on a miss, real argument orders) in BezwaarCreationHookTest, VaststellingServiceTest, DossierCompilerTest and VoorstelBesluitControllerContractTest, matching the shared FakeTermijnStore. PHPUnit 2771 green; php -l, PHPCS, Psalm, PHPStan, PHPMD (both rulesets, per subdir, fresh pdepend cache) all clean. * test(beroep): cover the repaired find/save contract, which the coverage ratchet demanded The sweep added statements to files with zero unit coverage (the Bezwaar services), and the changed-files coverage ratchet correctly read that as 'adding code without tests': 18.64% head vs 18.84% base, scoped to the 22 changed files. BeroepService was the largest untested surface (7 repaired sites), so it gets the contract pins: register() resolves an EXISTING contested beslissing (pre-fix: always threw), a truly missing one still refuses, addFileInspectionRequest() appends to the existing beroep, and the reopen_objection cascade resolves the source bezwaar and fires the beroep-reopen transition. All four run against the shared entity-shaped FakeTermijnStore, so an is_array regression fails the way it fails live.
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.