Skip to content

feat(flow): automatic actions become OpenRegister flows, and the dead surface goes - #1343

Merged
rubenvdlinde merged 11 commits into
developmentfrom
chore/retire-dead-action-system
Aug 24, 2026
Merged

feat(flow): automatic actions become OpenRegister flows, and the dead surface goes#1343
rubenvdlinde merged 11 commits into
developmentfrom
chore/retire-dead-action-system

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Closes the C2 block of page-topology-cleanup — observation 8 ("/settings/automatic-actions belongs in openregister as new flow functionality").

What this does

Node ids procest.*dossiq.* (the app-id rename missed this directory; the nodes landed after it)
Migration occ dossiq:actions:migrate-to-flows --user=<uid> [--dry-run]
Retired both /settings/automatic-actions pages · ActionRegistry + ActionHandlerLocator (556 lines, zero consumers)
Kept the six handler classes — the flow nodes are built on them

The finding this rests on

automaticAction objects have never executed. SideEffectDispatcher runs the separate Service\Transitions vocabulary keyed on an inline type; nothing reads these objects at run time. The page administered a capability with no runtime behind it — the same shape as the AVG page C1 retired.

Two schema descriptions asserted the opposite ("resolved at dispatch time by ActionRegistry", "only published actions are dispatched by SideEffectDispatcher"). Both are corrected in place rather than deleted: a reader who trusted them would conclude the objects worked.

Why a command and not a repair step

FlowService refuses to create a flow without a signed-in owner and an active organisation — it throws rather than store an orphan that can never be seen, run or edited again. An upgrade runs as nobody, and runAsSystem() elevates RBAC while leaving both null. An upgrade-time migration could only ever have thrown.

--user is required with no default: the created flows inherit that identity permanently.

⚠️ The flows land ENABLED

These are configured emails, webhooks and document generation that have never fired. Enabling them means they start. That was a deliberate call (the stored config said what it wanted and was never honoured) — --dry-run exists for it and the command description says so up front.

An action type no node implements is skipped, never wrapped in a flow. Writing one would rebuild the spawnCase defect fixed earlier in this programme: stored config naming a handler nothing answers to, reporting success and doing nothing.

Also fixed

The Configuration menu link still pointed at /settings/admin/procest after the rename, so it 404'd.

Verification

  • suite 2412 green · vitest 343 green
  • phpcs 0 errors · phpmd 0 · phpstan [OK] · webpack builds
  • node catalogue verified against the live instance (GET /api/flow/node-catalog → 45 nodes, 15 of them this app's, both id spaces intact)
  • 6 new tests cover create / skip / update / unidentifiable / dry-run / one-fails-rest

🤖 Generated with Claude Code

The app-id rename (#1329) rewrote 1765 files and did not reach this directory,
because these fifteen nodes landed after it. So an OpenRegister node catalogue
served `procest.action.sendEmail` under an app named Dossiq — the flow-node id
is one more channel the rename inventory has to cover, alongside the ones
info.xml already documents.

BREAKING, deliberately and with the tradeoff named. A node id is a runtime
lookup: an OR flow referencing `procest.sendEmail` now resolves to nothing, and
a missing node is silent — no error, the step simply has no node. Registering
both ids with the old one deprecated was the alternative and was declined: these
nodes are days old, field usage is effectively nil, and a permanent alias buys
compatibility nobody needs at the price of a catalogue that shows every action
twice forever.

The class names move with the ids (ProcestActionNode -> DossiqActionNode, and so
on for all nineteen files) — a class called Procest* inside namespace OCA\Dossiq
is the same half-finished rename read from the other side.

BOTH ID SPACES STAY DISTINCT. `dossiq.*` is the live transition vocabulary the
dispatcher fires; `dossiq.action.*` is the configured-action catalogue. Both
still ship a sendEmail, so SideEffectDispatcherTest::testItResolvesTheLiveIdSpace
keeps proving the dispatcher resolves the live space and not the catalogue —
crossing them would run a different class with different config keys.

Also renamed the personal-settings mount id (`procest-personal-settings` ->
`dossiq-`): template and mount call are one pair, no stored data, nothing else
reads it.

LEFT ALONE, on purpose: `procest.tenant.*` / `procest.parafering.*` audit action
keys and the `Open Registers/Procest` register folder are values already written
into stored rows and on disk — info.xml calls them frozen and they are. Likewise
the historical issue refs, archived @SPEC paths, MigrateUserPreferences' OLD_APP_ID,
and `procestermijn`, which is a Dutch word and not the app.

LIVE-VERIFIED: GET /api/flow/node-catalog returns 45 nodes, 15 dossiq.*, zero
procest.*. Suite 2406 green, phpcs 0 errors, phpmd 0, phpstan [OK].
… surface goes

`automaticAction` objects have never executed. `SideEffectDispatcher` runs the
SEPARATE `Service\Transitions` vocabulary keyed on an inline `type`; nothing has
ever read these objects at run time. So `/settings/automatic-actions` was an
admin surface over a capability with no runtime behind it — the same shape as
the AVG page C1 retired.

MIGRATION (`occ dossiq:actions:migrate-to-flows --user=<uid> [--dry-run]`).
Each action becomes a flow whose single action node is the `dossiq.action.*`
node this app already contributes, wrapped in a manual trigger and an end so
OpenRegister will actually run it. That is what makes the configuration
executable for the first time.

IT IS A COMMAND, NOT A REPAIR STEP, AND THAT WAS FORCED. `FlowService` refuses
to create a flow without a signed-in owner AND an active organisation — it
throws rather than store an orphan that could never be seen, run or edited
again. An upgrade runs as nobody, and `runAsSystem()` elevates RBAC while
leaving both null, so an upgrade-time migration could only ever have thrown.
`--user` is required and has no default: the flows inherit that identity
permanently, so a guessed owner would be permanent too.

THE FLOWS LAND ENABLED, which the user chose with the consequence stated: these
are configured emails, webhooks and document generation that have never fired,
and enabling them means they start. `--dry-run` exists for exactly that reason
and the description says so.

An action type NO node implements is SKIPPED, not wrapped. Writing a flow around
an unresolvable node would rebuild the defect the previous commit just fixed in
the VTH catalog — stored config naming a handler nothing answers to, reporting
success and doing nothing. Idempotency comes from a `dossiq:automaticAction:
<tenantId>:<slug>` marker stored on the flow itself, so a re-run rediscovers the
link from the flow rather than from a ledger that can be lost; the marker is
built from two schema-REQUIRED fields read WITHOUT defaults, because defaulting
either would collapse several actions onto one marker and each migration would
overwrite the last one's flow.

RETIRED: both `/settings/automatic-actions` pages, and `ActionRegistry` +
`ActionHandlerLocator` (556 lines, zero consumers — grepped across lib, src,
tests and routes). The menu entry becomes a deeplink to
`/apps/openregister/#/flows`, hash-routed, which is the trap C1 hit. The six
handler classes STAY: the flow nodes are built on them, so they are now live
code for the first time rather than dead code.

TWO SCHEMA DESCRIPTIONS WERE LYING and are corrected in place rather than
deleted, because the lie is the useful part of the history: `automaticAction`
claimed its slug was "resolved at dispatch time by ActionRegistry", and
`isPublished` claimed "only published actions are dispatched by
SideEffectDispatcher". Neither was ever true. A reader who trusted them would
conclude the objects worked.

Also fixed: the Configuration menu link still pointed at `/settings/admin/procest`
after the rename, so it 404'd.

Tests (6): the create path asserts trigger+action+end and the decoded config;
the skip path proves an unimplemented type writes nothing; the update path
proves a re-run touches the existing flow instead of making a second; an
unidentifiable action fails rather than sharing a marker; a dry run writes
nothing; and one throwing action does not abort the rest.

Suite 2412 green, vitest 343 green, phpcs 0 errors, phpmd 0, phpstan [OK],
webpack builds.
…meetings (D1)

decidiq owns agenda-building and meetings, and already surfaces them on a dossiq
case through the `decidesk-decisions` integration leaf. The two pages here were
the second implementation of the same thing.

THE BLOCKER WAS REAL AND IS NOW GONE, VERIFIED AT RUNTIME. This retirement was
held back because dossiq's leaf showed a "Besluitvorming unavailable" fallback
until decidiq shipped its init-script wiring. That wiring is on decidiq
`development` and deployed. Checked in the browser on a dossiq page rather than
by reading decidiq's source: `OCA.OpenRegister.integrations.list()` returns 28
leaves including `decidesk-decisions` (label "Besluitvorming") carrying tab,
widget, widgetCompact, widgetExpanded and widgetEntity surfaces.

⚠️ THE PR NUMBER IN THE BLOCKING TASK WAS WRONG. It named "decidesk PR #100",
which in both decidesk and decidiq is "chore(spec): merge
p2-minutes-and-decisions-other-t2", merged 2026-04-19 — an unrelated change. A
reader checking that reference would have concluded the blocker still stood, or
that it had cleared four months ago; neither is what happened. Verifying by
CONTENT is the only check that catches a citation like that.

REMOVED: `/besluitvorming/agenda` + `/besluitvorming/vergaderingen/:id` and
their manifest fragment · AgendaCompilerView, VergaderingDetailView, AgendaItem ·
their registry entries · AgendaController + AgendaService + its test · the two
`agenda#` routes · three API-client functions.

The chain is followed all the way down deliberately. Leaving AgendaController
behind would leave routes nothing can reach, which is the dead-endpoint shape
this programme already retired once on the AVG page; leaving AgendaService would
leave a service with no caller and a test that keeps it green.

`generateAgenda` was worse than unused: it POSTed to `/agenda/generate`, and no
route for that has ever been declared. Every call it made 404'd.

Also removed: the `BesluitvormingGroup` menu entry, which had no route, no href
and — since nothing in the menu declares a parent — no children either. The
renderer drops it silently, so it was invisible rather than broken; it is still
config asserting a group that does not exist.

KEPT: BesluitPublicatiePanel and `publishBesluit` (DROP/LVBB publication is a
real capability with a live route), and `mandaat#mandaatCheck`, whose route
serves the server-side guard even though the client function had no caller.
Noted, not fixed here: BesluitPublicatiePanel is registered but referenced by no
manifest page, and `besluitvorming#activateTemplate` has no frontend caller.

Suite 2406 green, vitest 343 green, phpstan [OK], webpack builds.
@rubenvdlinde
rubenvdlinde requested a review from Rem-Dam as a code owner August 24, 2026 07:53
Nextcloud's Dispatcher binds a controller argument BY NAME —
`$this->request->getParam($param, $default)` — never by position. Nine routes
declared a placeholder whose name no parameter answered to, so the argument
resolved to null, the non-nullable string typehint threw a TypeError, and the
request came back 400 with an empty body. For any input. On every call.

MEASURED AGAINST THE RUNNING INSTANCE, before and after:

  parafeerRoute#start              400 (empty)  ->  500 {"error":"Starten van parafering mislukt"}
  parafeerRoute#completeStep       400 (empty)  ->  500 {"error":"Stap kon niet worden voltooid"}
  parafeerRoute#skipStep           400 (empty)  ->  400 {"error":"Geldig stapnummer is vereist"}
  parafeerRoute#addStep            400 (empty)  ->  500 {"error":"Stap toevoegen mislukt"}
  voorstelBesluit#registerBesluit  400 (empty)  ->  404 {"error":"Voorstel niet toegankelijk"}
  subsidie#finalizeVaststelling    400 (empty)  ->  400 {"error":"Kon vaststelling niet vaststellen"}
  subsidie#signBeschikking         400 (empty)  ->  400 {"error":"Kon beschikking niet ondertekenen"}
  subsidie#publishBeschikking      400 (empty)  ->  400 {"error":"Beschikking niet gevonden"}
  caseRelation#destroy             400 (empty)  ->  403 {"ok":false,"reason":"access_denied"}

Every "after" is a DOMAIN answer about the deliberately bogus id I sent. That is
the proof: the method body now runs. The before was the framework refusing to
call it at all.

Cause: the Dutch->English vocabulary sweep renamed the method parameters
(`$voorstelId`->`$proposalId`, `$beschikkingId`->`$decisionId`,
`$aardRelatie`->`$natureRelationship`) and left the URLs behind. The placeholder
name is not decoration; it is the binding key.

NOTHING CAUGHT THIS, AND THAT IS THE INTERESTING PART. The route exists. The
controller exists. The method exists. gate-6 (route-reachability) verifies
exactly those three facts and passes. Only the NAMES disagreed, and no check
compared them. A caller saw a bare 400 and would reasonably read it as "my
request was malformed".

THE GUARD (tests/Unit/AppInfo/RoutePlaceholderBindingTest.php) reflects over
every declared route and asserts that each required, non-nullable parameter of
its target method is named by a placeholder in the URL. Written for the five I
had found — it immediately found FOUR MORE I did not know about, all confirmed
dead against the live instance before I touched them. That is the whole argument
for writing the check rather than fixing the instances.

ALSO FIXED, and only visible once the route worked:
`BeschikkingService::publish()` called `find()` OUTSIDE its try block.
OpenRegister's find() throws on a missing object rather than returning a
non-array, so the throw escaped as an HTML 500 — and the `is_array()` guard
below it could never fire, making 'Beschikking niet gevonden' a message no
caller could ever receive. `sign()`, ten lines down, has always wrapped its call.
Now they match, and that message is reachable.

E2E (5 new tests, all passing against a live instance): a retirement is the one
change nothing else in the suite can catch — every other spec asserts something
renders, so deleting a page leaves them green by not running.
tests/e2e/spec-coverage/retired-surfaces.spec.ts asserts BOTH halves: the old
view is gone AND its replacement is reachable. Asserting only the first would
pass just as happily on a build where the capability vanished.

The first version of that spec failed for a reason that had nothing to do with
what it tested — a retired route falls through to the app root, so its
console-error assertion was grading the DASHBOARD's network traffic, which
legitimately 404s against an instance whose register is not seeded.

DOCS: docs/user-guide/admin/02-automatic-actions.md described a rule engine with
triggers, conditions and a "Last run" column. The schema has no trigger field and
no condition field, and nothing ever executed one. It documented a feature that
did not exist. Rewritten around the migration command and the flow editor, with
the correction stated rather than quietly dropped, and its five stale screenshots
removed along with the capture block that produced them.

Suite 2407 green, phpcs 0, phpmd 0, phpstan [OK].
…ma, not the property

`bacAdviceRequest` declares `bezwaar` — a REQUIRED property — whose $ref points
at the `objectionProceeding` schema. AdvisoryCommitteeService wrote the objection
id under `objectionProceeding`: the name of the SCHEMA, not of the property. So
every advice request ever created omits a required property and carries an
undeclared one instead.

WHAT A USER SAW. Nothing. BezwaarDetail's advice-request stats-blocks and its
"Adviesaanvragen" object-list filter on `bezwaar: @objectId`
(src/manifest.json:1852, 1871, 1906), so a bezwaar rendered NO advice requests
however many it had. An empty list is also exactly what a bezwaar with no advice
requests looks like, so there was never anything to notice. No error, no warning,
no failing test — the schema, the manifest and the service each held a
self-consistent view, and only the service's disagreed.

FIXED IN THREE PLACES, because a rename that only moves the write leaves the
reads pointing at nothing:
  - the write now sets `bezwaar` (AdvisoryCommitteeService:178)
  - both reads go through `objectionIdOf()`, which prefers `bezwaar` and falls
    back to the legacy key — AND LOGS A WARNING when it has to. A silent
    fallback would make the repair step look unnecessary: reads would keep
    working, the manifest filters would keep showing nothing, and the only
    symptom would stay invisible. Which is how this lasted.
  - `BackfillAdviceRequestObjection` copies the legacy key onto the declared one
    for rows already in the field.

The backfill reads the legacy key WITHOUT a default. A row carrying neither key
is skipped, not written with `bezwaar => ''` — that would satisfy the required
property while pointing at nothing, producing a row that looks repaired and is
still invisible to the filter that needed it.

NOT changed: the `objectionProceeding` key inside the audit-trail payload
(:199). That is a free-form label describing the referenced schema, which is
accurate, and rewriting it would change the meaning of entries already written.

Tests (5): the copy, a correct row left alone, an unrepairable row skipped rather
than blanked, idempotency across a re-run, and a no-op when OpenRegister is
absent — an upgrade must not fail because a projection could not complete.

Found while scoping D2 (bezwaaradviescommissie -> decidiq governance-body); it is
independent of that migration and worth fixing on its own.

Suite 2412 green, phpcs 0, phpmd 0, phpstan [OK].
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/dossiq @ 5c6cdc0

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
check-schema-l10n
composer ✅ 106/106
npm ✅ 552/552
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-24 08:27 UTC

Download the full PDF report from the workflow artifacts.

…ctie path is the live one

dossiq had TWO parafering APIs. `/api/parafeer-actie` is the one the UI calls and
the one that works: it records the action, advances the proposal, applies
accordering effects and emits the audit event. `/api/parafeer-route/...` was a
parallel implementation of the same flow that has never served a single request —
its placeholder bound no argument, so every call answered 400 (fixed one commit
ago, which is what made the choice visible rather than theoretical).

REMOVED (2,627 lines): the four routes, ParafeerRouteController,
ParafeerRouteService (663 lines), ParaferingStepActivator, VoorstelRouteMapper,
ParaferingApprovalBridge (331 lines), parafeerRouteApi.js, AddStepDialog.vue and
two test files that kept the dead code green.

EVERY CAPABILITY IS ACCOUNTED FOR, not assumed:
  start          -> the `besluitvormingActivate` transition action, which runs
                    BesluitvormingParafeerService::activate(). Already live.
  complete-step  -> parafeeractie `parafered` / `accorded`. Already live.
  skip-step      -> parafeeractie `skipped`, which is in that endpoint's action
                    enum. SkipStepDialog is RE-POINTED at it rather than deleted,
                    so the "Stap overslaan" button works for the first time.
  add-step       -> no live equivalent. Adding a step mid-route is not in the
                    parafeeractie vocabulary, so the button goes with the API.
                    Stated rather than quietly dropped: this is the one thing
                    that stops being offered — and it never worked either.

ADR-022's OpenRegister ApprovalChain delegation goes too, and its own code shows
why it had to. `delegateToApprovalWorkflow()` returned early unless
`proposal.approvalChainUuid` was set; that property is not declared on the
`proposal` schema and was written in exactly one place — inside
`ParafeerRouteService::startParafering()`, which was itself unreachable. So the
bridge short-circuited on every call, the "legacy in-array path" its comments
called a migration window was the only path there has ever been, and no approval
event dossiq raised has ever reached OpenRegister. Removing it changes no
behaviour; it removes a claim.

Verified against the running instance, not just the suite: the retired routes now
answer 405 (gone, rather than 400 = bound wrong), and a `skipped` action posted to
/api/parafeer-actie reaches the service and comes back with a domain answer about
the bogus id I sent it.

Suite 2402 green, vitest 343 green, phpcs 0, phpmd 0, phpstan [OK], webpack builds.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/dossiq @ 5201731

Check PHP Vue Security License Tests
lint ⏭️
phpcs ⏭️
phpmd ⏭️
psalm ⏭️
phpstan ⏭️
phpmetrics ⏭️
eslint ⏭️
stylelint ⏭️
build ⏭️
composer ⏭️ ⏭️
npm ⏭️ ⏭️
app:check-code ⏭️
info.xml ⏭️
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-24 08:59 UTC

Download the full PDF report from the workflow artifacts.

…ed formatting

CI caught what my local run did not, in the one way that matters: it runs phpmd
TWICE, and only the second pass uses
`vendor/conduction/hydra-gates/quality-config/phpmd-unusedparams.xml`. I ran the
first config alone and read its silence as clean.

`ParafeerActieService::propagateDecision()` took `$proposal` solely to hand it to
`delegateToApprovalWorkflow()`, which the previous commit removed along with the
ApprovalChain bridge. With that gone the parameter had no reader — so the call
site was still passing an array nobody looked at. Removed from the signature, the
docblock and the call.

Also: `prettier --check` covers `{js,ts,vue,css,scss}` and two of my files were
unformatted. The three `src/manifest.d/*.json` warnings prettier also reports are
pre-existing AND out of the check's scope — JSON is not in that glob — so they
are neither caused nor fixed here.

Suite 2402 green, both phpmd configs clean, phpstan [OK], `npm run format` clean.
…ved and nothing said so

D4 asked whether dossiq's consumption of decidiq is render-and-read only. It is:
the leaf tab issues no write verb, dossiq writes nothing into decidiq's register,
and every cross-app command travels as a typed event. The audit passed — and
then found that the events themselves had stopped resolving.

decidiq renamed its namespace from OCA\Decidesk to OCA\Decidiq WITH NO ALIAS.
dossiq named only the old spelling, in two places, with two different failure
modes:

  LISTENER (WorkflowListenerRegistrar) — guarded by class_exists, so it went
  false and simply stopped registering. Every concluded decision quietly stopped
  materialising a ZGW Besluit here. Nothing errored, and nothing could: a
  class_exists guard that goes false looks EXACTLY like the optional app not
  being installed, which is the case it was written for.

  DISPATCH (ContractDecisionDelegationService) — fails closed, so it threw
  "Decision service unavailable: decidesk is not installed" on an instance where
  it was installed. Every contract decision blocked, behind a message pointing at
  the wrong problem entirely.

PROVEN LIVE before and after, not inferred:

  before: OCA\Decidesk\Event\DecisionConcludedEvent   MISSING
          OCA\Decidesk\Event\DecisionRequestedEvent   MISSING
          OCA\Decidiq\Event\DecisionConcludedEvent    EXISTS
          OCA\Decidiq\Event\DecisionRequestedEvent    EXISTS

  after:  OCA\Decidiq\Event\DecisionConcludedEvent    1 listener (was 0)

Both constants now LIST the spellings, newest first, and resolve to the first
that exists. That is not politeness toward the old name — it is the only thing
that keeps the integration working through an upgrade where the two apps move at
different times, which is exactly the window this broke in.

An app cannot move another app's class name; it can only follow it. So
CrossAppEventNamesTest asserts the property worth holding: each constant is a
LIST, names the current namespace, still names the old one, and prefers the
current one. Pinning a single spelling fails the test now instead of silently
disabling a feature later.

NOT changed, deliberately: `sourceApp => 'procest'` and
DecisionConcludedListener::SOURCE_APP. That value is this app's id AS THE OTHER
APP KNOWS IT — decidiq echoes back what dossiq sends — so it moves only when both
sides move together. Its own comment already says so.

Suite 2406 green, phpcs 0, both phpmd configs 0, phpstan [OK], format clean.

The phpstan ignore is scoped to the one resolver method: neither namespace is on
this app's analysis path (that is the point of resolving them as strings), so
class_exists can never be proven true and the method reads as returning only
null. Same family as the ApprovalStepNotificationListener entries above it.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/dossiq @ 05e871b

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
check-schema-l10n
composer ✅ 106/106
npm ✅ 552/552
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-24 09:42 UTC

Download the full PDF report from the workflow artifacts.

…lyses as real

CI's phpstan rejected the previous commit with an error my container never
produced: `class_exists() with '\OCA\Decidesk\Event…'|'\OCA\Decidiq\Event…' will
always evaluate to false` (function.impossibleType). Neither namespace was on
the analysis path, so the analyser proved the lookup dead — reporting a fix for
a real outage as dead code.

The root cause is the same either way: only the OLD namespace was ever stubbed.
This adds `tests/Stubs/Decidiq/Event/*` alongside the existing Decidesk pair and
scans both in phpstan.neon, so both spellings resolve and the resolution reads
as what it is. That is better than an ignore, which would have silenced the
symptom while leaving the analyser blind to the contract.

The earlier `resolveRequestEventClass() never returns string` ignore is REMOVED,
not kept alongside: with both classes resolvable, class_exists can return true
and the method demonstrably returns a string. Two errors, one cause.

The bootstrap now loops the two namespaces × two events rather than repeating a
four-line block, and stubs each only when the real class is absent — unchanged
behaviour, one place to add the next spelling.

THREE TESTS PINNED THE OLD LITERAL and started failing the moment both stubs
loaded: they imported `OCA\Decidesk\Event\*` and asserted the dispatched object
was an instance of it, while the resolver now prefers the current spelling. That
is the same defect as the production code had, one layer down — a test that
names a class the code no longer builds. Pointed at the current namespace, with
a comment saying why and pointing at CrossAppEventNamesTest, which guards the
ordering they depend on.

⚠️ Local phpstan passed on the previous commit and CI did not. The two disagree
about which error fires, so `[OK] No errors` in the container is not proof the
gate passes. The fix addresses the cause both report.

Suite 2406 green, phpcs 0, both phpmd configs 0, phpstan [OK], format clean.
…old the wrong story

Two CI failures, both mine.

GATE-16 (spec-coverage): `BackfillAdviceRequestObjection::run()` and
`::getName()` are new PUBLIC methods and carried no `@spec`. The class had one;
the gate is per-method. Added.

E2E: `retired-surfaces.spec.ts` asserted the decision app registers its
`decidesk-decisions` leaf, and returned `null` for TWO different things — the
OpenRegister integration registry being absent, and the leaf being missing from
a registry that is present. Every null was then reported as "OpenRegister
integration registry not present".

So the message was wrong in both directions. dossiq's CI does not install the
decision app, so the registry is legitimately absent — an environment fact this
PR cannot affect — and it was reported as a failure. And had the leaf actually
gone missing with the registry present, that REAL finding would have been
described as an environment problem and dismissed.

A lookup failure must not wear the same words as a judgement. The probe now
returns three distinguishable outcomes:

  registry absent  -> test.skip() naming the reason. Not a defect here.
  registry present, leaf missing -> a real failure, and the message now LISTS
                                    what the registry actually holds instead of
                                    guessing.
  leaf present     -> asserted as before.

Suite 2406 green, phpcs 0, phpstan [OK], format clean.
… hunk

Fifteen files conflicted, all in the Procest -> Dossiq Flow rename. They
collide on every hunk because the two sides also differ in indentation, which
makes the diff look far larger than the actual disagreement.

Resolved toward this branch, but checked rather than assumed. For each of the
24 hunks I compared the two sides with indentation and blank lines
normalised:

  - nine Tx*Node files: this branch is a strict SUPERSET. It already carries
    development's getDisplayName()/getDescription() additions and adds the
    renamed nodeId on top.
  - six files where development had lines this branch lacks: every one of
    those lines is an old `Procest*` class name or a `procest.action.*` id —
    precisely what this branch renames.

The case worth naming is SideEffectDispatcherTest. Development adds a test
asserting the dispatcher resolves the LIVE id space rather than the
catalogue's, because both action systems ship a sendEmail. That test is not
lost: this branch has the same test with `dossiq.action.sendEmail`.

Checked afterwards that nothing renamed too far. The surviving `procest`
strings are all correct:

  - `procestermijn` / `procesType` in the ZGW validators are Dutch archiving
    terms, not the app name.
  - `procest.tenant.*` and `procest.parafering.*` in TenantAuditTrailService
    are marked FROZEN PREFIX in the source and are already written into stored
    audit rows, so they must not move with the app id.

php -l passes on all 22 touched files, and the listener catalogue resolves to
the renamed classes.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/dossiq @ 9bc60d7

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
check-schema-l10n
composer ✅ 106/106
npm ✅ 552/552
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-24 19:13 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit 1799478 into development Aug 24, 2026
50 of 80 checks passed
@rubenvdlinde
rubenvdlinde deleted the chore/retire-dead-action-system branch August 24, 2026 20:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant