Skip to content

Release: merge development into beta - #3204

Open
github-actions[bot] wants to merge 81 commits into
betafrom
development
Open

Release: merge development into beta#3204
github-actions[bot] wants to merge 81 commits into
betafrom
development

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

Automated PR to sync development changes to beta for beta release.

Merging this PR will trigger the beta release workflow.

Reminder: Add a major, minor, or patch label to this PR to control the version bump. Default is patch.

github-actions Bot and others added 6 commits August 31, 2026 10:24
The 2.0.6-beta.20260831102212 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.
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>
The 2.0.7 release bumped the version on main. Without this,
development stays behind main and the next development -> main 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.
….0.7

chore(release): sync main back into development
….0.6-beta.20260831102212

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.
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ bcd3452

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-31 11:40 UTC

Download the full PDF report from the workflow artifacts.

dependabot Bot added 2 commits August 31, 2026 13:55
Bumps [zod](https://github.com/colinhacks/zod) from 4.4.3 to 4.5.2.
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](colinhacks/zod@v4.4.3...v4.5.2)

---
updated-dependencies:
- dependency-name: zod
  dependency-version: 4.5.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [marked](https://github.com/markedjs/marked) from 12.0.2 to 18.0.11.
- [Release notes](https://github.com/markedjs/marked/releases)
- [Commits](markedjs/marked@v12.0.2...v18.0.11)

---
updated-dependencies:
- dependency-name: marked
  dependency-version: 18.0.11
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ 972d1ea

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-31 12:14 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ 556155b

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-31 12:46 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ 33fd1cd

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-31 13:15 UTC

Download the full PDF report from the workflow artifacts.

#3220)

The override read:

  "apexcharts": "License is MIT - license-checker misreads logo URL as custom
   license"

That was true once and is not true now. apexcharts stopped being MIT at
6.0.0: 4.7.0 and 5.0.0 are MIT, 6.0.0 and 7.0.0 are SEE LICENSE IN LICENSE.
This app declares ^7.0.0 and locks 7.0.0, so the checker flagging it is
CORRECT and the override was suppressing a true finding rather than
correcting a false one -- the licence gate was reporting green on a
dependency that is not open source.

Nothing about the dependency changes here. apexcharts 7 stays. What changes
is that the file now says what is actually true:

- 6.0.0+ ship a dual-license: free under a Community License for
  organisations below USD 2M annual revenue, paid above it, and sublicensing
  under different terms is not permitted.
- Conduction confirmed 2026-08-31 that it is below that threshold and so
  qualifies under the Community License.
- The override is therefore a deliberate acceptance, dated and attributed,
  not a claim that the checker is wrong.

The note also records the redistribution question, because the Community
License binds the organisation USING the software rather than only the one
shipping it, and this app is distributed through the Nextcloud App Store. An
installing tenant above USD 2M would need its own commercial licence. That is
flagged for a decision, not decided here.

For reference: dossiq is on apexcharts 4.7.0 (MIT) with no override, and its
bump to 7.0.0 was closed today on these grounds (dossiq#1527). apexcharts 5.x
is the last MIT release if the fleet ever needs to return to one.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ fca2265

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-31 13:48 UTC

Download the full PDF report from the workflow artifacts.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ d630b0c

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-31 14:34 UTC

Download the full PDF report from the workflow artifacts.

github-actions Bot and others added 3 commits August 31, 2026 15:01
The 2.0.9 release bumped the version on main. Without this,
development stays behind main and the next development -> main 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.
…0260831154312

chore(sync): carry beta back into development
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ 0ae5e0a

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-31 16:06 UTC

Download the full PDF report from the workflow artifacts.

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>
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ 5ddb1ca

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-31 16:45 UTC

Download the full PDF report from the workflow artifacts.

…SetupHandler (#3233)

* fix(db): bind catch(Exception) to a class that exists, and drop dead SetupHandler

Two findings that psalm-baseline.xml had been carrying as accepted noise.
Neither is noise. 187 -> 184.

lib/Db/Register.php caught `Exception` with no import, inside namespace
OCA\OpenRegister\Db. PHP resolves an unqualified catch type against the
CURRENT namespace, so that block was bound to OCA\OpenRegister\Db\Exception,
which does not exist, and it could never catch anything. Verified rather
than reasoned about:

    namespace Demo\Sub;
    try { throw new \RuntimeException("boom"); }
    catch (Exception $e) { ... }   // no import
    -> NOT CAUGHT -> RuntimeException: boom

Adding `use Exception;` binds it to the global class the code plainly meant.
Checked the rest of lib/: ObjectsController and Repair/RenameDutchColumns
also catch a bare `Exception`, but both import OCP\DB\Exception, so their
catches bind to a real class and are deliberate. Psalm flagged exactly the
one file that was broken.

lib/Service/SettingsService.php declared a property, a constructor
parameter and two docblocks for SetupHandler, a class that exists nowhere
in the tree. It was written and never read, so it was dead API surface
that would fatal the moment anything tried to supply it. Removed, along
with the `setupHandler: null` argument in Application.php and the matching
positional null in the two tests that build the service positionally.

Verified with the repo own vendor/bin/psalm 5.26.1 on PHP 8.3: errors with
the baseline emptied went 187 -> 184, and the regenerated baseline is green.
PHPUnit was NOT run here: it needs the Nextcloud server bootstrap, which
only exists inside a server checkout.

* fix(tests): realign the five positional constructor calls I missed

Removing the SetupHandler parameter shifted every later POSITIONAL
argument, and I checked for that by reading the first line after each
`new SettingsService(` and concluding the file used named arguments. It
uses both. Five calls further down SettingsServiceTest.php are positional,
and CI caught it as 13 TypeErrors, all the same one:

  Argument #14 ($appName) must be of type string, MockObject_IAppContainer
  given

which is the shift saying exactly what it was.

Verified by position this time rather than by sampling: with the parameter
gone, appName is slot 14, and all seven positional call sites across the
three test files now carry a string literal there.

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ 8edc871

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-31 17:06 UTC

Download the full PDF report from the workflow artifacts.

rubenvdlinde and others added 2 commits August 31, 2026 19:07
….0.9

chore(release): sync main back into development
…etter (#3236)

Two more entries the Psalm baseline was carrying. Both are real. 184 -> 180.

PollLinkService had a fallback that could never run:

    $pollId = (int)$this->db->lastInsertId("oc_polls_polls_id_seq");
    if ($pollId === 0) {
        // Fallback for drivers without sequence support.
        $pollId = (int)$this->db->lastInsertId();
    }

OCP\IDBConnection::lastInsertId(string $table): int takes a REQUIRED
argument, so the no-arg call raises ArgumentCountError on exactly the
drivers the fallback was written for. The surrounding catch (Throwable)
turned that into "Failed to create poll", which blames the insert rather
than the id retrieval. A zero now falls through to the explicit throw that
was already there and says what actually happened.

Checked the other four lastInsertId call sites. MagicMapper and
DedupCollidedSchemasCommand pass a table. DbalObjectSourceProvider calls it
with no argument, but on a DBAL Connection rather than IDBConnection, where
the parameter is optional. Psalm flagged only the one that was wrong.

CaseToken declared getToken TWICE:

    @method string|null getToken()
    @method void getToken(?string $token)   <- copy-paste of the setter
    @method void setToken(?string $token)

Psalm binds to the second, which is why two correct $row->getToken() calls
read as "Too few arguments". Removed; setToken on the next line was already
right. Swept lib/Db for the same slip: this was the only one of them.

Verified with the repo own vendor/bin/psalm 5.26.1 on PHP 8.3: all four
TooFewArguments are gone, errors with the baseline emptied went 184 -> 180,
regenerated baseline is green. PHPUnit was NOT run here, it needs the
Nextcloud server bootstrap that only exists inside a server checkout.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ 6dac523

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-31 17:13 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ c54b65a

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-31 17:28 UTC

Download the full PDF report from the workflow artifacts.

rubenvdlinde and others added 2 commits September 2, 2026 14:11
…ts (#3337)

This block asserted 302/303 with Location containing '#/flow-tasks/<uuid>'.
Both halves are gone: #3315 rewrote TaskController::open() to return a
TemplateResponse serving the SPA shell directly, and #3270 had already moved
the router off hash routing so that fragment addressed nothing.

It survived both changes because the block SKIPS whenever the assignee has no
VTODO-capable calendar, and CI's admin has none — 'No VTODO-supporting
calendar found for user admin' shows up in the server logs. So it has been
asserting a contract that stopped existing, without ever running to say so.

Now asserts what open() actually does: 200, and the shell rather than an API
payload or an error page. Filed as #3313 on the premise that the redirect
pointed at a route that did not exist; the route (/flow-tasks/:uuid,
flow-task-detail in src/main.js) exists now and the redirect is gone, so
nothing is left to decide.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
resolveMergeTarget() and findByUuidFollowingMerge() were built and then
called by nothing, so a merge was recorded and never followed. A user
whose active organisation had been merged away kept resolving to the row
that was merged: it still loads, its queries still return rows, and every
one of them runs under a tenant boundary that no longer applies.

Both entries into fetchActiveOrganisationFromDatabase() walk it now. The
stored active UUID is the obvious one. The auto-pick is the one that
matters more: a user's membership list can still name a merged-away
organisation, so without the walk there the first login after a merge
writes the dead UUID straight back into config and re-creates the defect.

The walk is guarded on isMerged(), because the flag lives on the row that
was merged away and almost no row carries it, so the common case costs no
query at all. Membership is then checked against the SURVIVOR, which
fails closed: a user the merge did not carry over falls through to their
own organisations rather than being handed a tenant on the strength of a
membership that ended. An unresolvable survivor keeps the row already in
hand and logs, because a read path is the wrong place to throw over a
data defect.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ bf93ec5

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-09-02 12:24 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ 1c6e55f

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-09-02 12:41 UTC

Download the full PDF report from the workflow artifacts.

…outcomes (#3338)

* feat(tasks): task expiry enforcement and configurable timeout/reject outcomes

Harvested from integriq's HITL semantics (fleet audit, wave 2): a task can
now declare onTimeout and onReject in the reserved outcome vocabulary
(skip|error|dead_letter). The timer sweep gains a third bounded range scan
that closes non-terminal tasks past their enforcing deadline through the
existing timer-outcome path (no second scheduler); a non-enforcing expiry
timer falls back to the subject task's declared onTimeout; a rejecting
completion of a task declaring onReject dead_letter records the dead-letter
outcome through the same mapping. Both behaviours are serialized and carried
by the user-task and portal-task nodes next to expiresAt.

* test(migration): cover the declared-behaviour migration

The coverage guard correctly flagged the one untested addition: the
migration adding on_timeout/on_reject and the expiry-scan index. Three
cases: fresh table gets both columns and the index, a re-run changes
nothing and returns null, an absent table is left alone.

* test(mapper): cover the sweep's task-timeout scan

lib/Migration is excluded from clover, so the previous commit recovered
nothing; the actually-uncovered addition was TaskMapper::findDueTimeouts().
The new case pins the scan's predicates (openness, declared on_timeout,
deadline order) and the floored batch limit.

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ a3e3321

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-09-02 13:02 UTC

Download the full PDF report from the workflow artifacts.

… inside (#3343)

its own card.

⚠️ THE CAUSE 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 on dossiq, while genuinely hovered: the tile carried a 2px
rgb(0,103,158) border and its own shadow, inset 9px from the wrapper's top
and 15px from its left. A non-card-fit tile sits at 1px, so its border lands
on the wrapper's own edge and reads as one card. That inset is the whole
difference, which is why it looked app-specific and was not.

nextcloud-vue#932 moves the affordance to the wrapper. No layout change:
nothing moves, one card outlines. Verified in the browser with a real pointer
hover:

  before   inner 2px rgb(0,103,158) + shadow   wrapper grey, no shadow
  after    inner transparent, no shadow        wrapper rgb(0,103,158) + shadow

Every app with clickable stat, gauge or delta tiles had this. This bump is
one of the fleet sweep that clears it.

Dependency change only: package.json and the nextcloud-vue entry in
package-lock.json.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ bda37e0

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-09-02 19:24 UTC

Download the full PDF report from the workflow artifacts.

Conduction Release Bot and others added 5 commits September 3, 2026 07:07
TWO holes in one line, and the second is the one the line was written to
prevent.

`FederationController::createObject()` is `#[PublicPage]`, `#[NoCSRFRequired]`,
and calls `saveObject()` with `_rbac: false` and `_multitenancy: false`, so
nothing downstream refuses what it lets through. It stripped `shareToken` and
`_route`, then pinned the organisation with:

    $data['@self'] = (($data['@self'] ?? []) + ['organisation' => ...]);

PHP's `+` keeps the LEFT operand's keys. So for a caller holding a writable
share token:

  1. `@self.id` SURVIVED. `saveObject()` resolves its target from the payload —
     `extractUuidAndNormalizeObject()` reads `@self.id` first, then `id` — and
     the write is PUT-semantic, so omitted fields are NULLED. A share that
     grants the right to ADD objects also granted the right to overwrite every
     existing object in the shared register/schema.

  2. `@self.organisation` SURVIVED TOO, which defeats the pin. The comment on
     that very line reads "a federated writer can never plant an object into
     another organisation". It could.

The second was found by the first test written for the first: a pin has to
overwrite, or it is only a default. Assigned rather than merged now.

`DsarCaseController::create()` gets the same treatment. It is guarded by
`requireAuthenticated()` rather than an admin setting, and stripped only
`_route`, so a create could address an existing DSAR case.

All three new tests fail against the original code and pass against the fix.
Full suite: 19037 tests, phpcs clean.

NOT CLAIMED: end-to-end exploitability. Both paths need a valid `read-write`
share token, and this was not driven against a live federated pair.
phpmd's ElseExpression rule, on the identity strip added in the previous
commit. The unset is unconditional now and the conditional only re-adds a
non-empty @self, which reads better anyway.

Scoped phpmd on the file: clean. The full-tree run is deliberately left to CI —
it OOM-killed this host once already.
createObject() was the one changed method without one. Tagged to the same
federation-scope-enforcement spec its three sibling methods in this controller
already point at.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t-address-an-object

A federated create may not address an existing object, and the organisation pin did not hold
A per-object quality score is an assessment OF the object's data, not a
fact about the thing the object describes. It had nowhere to live except
the object body, which forced every schema wanting a score to declare
`qualityScore` and `qualityStatus` as ordinary properties. Three things
followed, and all three are wrong:

- The properties appear on every form the schema drives. A case handler
  filing a case in dossiq was shown a "Quality score" number field to
  fill in, for a value the platform overwrites on save.
- Removing the declaration silently DELETES the values, because the
  store strips what the schema does not declare. So the mistake could
  not be undone without data loss.
- Two schemas scoring the same way had to agree on property names by
  convention, with nothing to enforce it.

`_quality` is now a metadata column of the same kind as `_validation`
and `_retention`, surfaced in the `@self` envelope as `quality` with
`score`, `status` and `scoredAt`.

The change is additive on purpose. The listener still writes the body
properties, but ONLY where the schema actually declares them: dropping
that outright would freeze the stored value of every schema that has
one, and a score that silently stops updating reads exactly like a score
that is simply good. A schema that has migrated declares neither, so
nothing is written to its objects and the body stays clean.

The migration sweeps every per-schema object table, adding the column
where it is missing and skipping it where present, so re-running is a
no-op. It identifies an object table by the `_uuid` column rather than
by the name prefix alone; verified against a live database, where every
`oc_openregister_table_%` carries `_uuid` and `_validation` and none yet
carries `_quality`.

Verified: 1886 unit tests in tests/Unit/Db and tests/Unit/Listener pass
(1881 before, plus the 5 added here), with the same 3 pre-existing
vendor warnings and 5 skips as the pristine tree. PHPCS, PHPStan clean
on every changed file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ a88e809

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-09-03 05:59 UTC

Download the full PDF report from the workflow artifacts.

feat(quality): the quality assessment is @self metadata, not object data
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ 3c7a322

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-09-03 07:10 UTC

Download the full PDF report from the workflow artifacts.

rubenvdlinde and others added 2 commits September 3, 2026 09:40
* feat(flow): a user task arms its own deadline

FlowTimerService::arm() had no production caller anywhere in OpenRegister.
Every reference to it lived in its own unit test, and it was never resolved
from the container by anything that runs. The escalation ladders, the working
calendar and the SLA calculator were all built, all tested and all
unreachable: a flow author could describe a deadline and nothing would ever
measure it.

UserTaskNode now arms a timer when it creates its task, bound to
subjectType 'task' and the task's uuid. That is the same pair
FlowTimerSubjectTerminalListener already cancels on, so completing or
cancelling the task disarms its deadline instead of leaving a timer firing
against a subject that is finished.

Three properties worth stating, because they are what makes this safe to add
to an engine every app shares:

- A node with no `sla` arms nothing and behaves exactly as before. All 36
  pre-existing tests pass untouched.
- An SLA that cannot be armed fails the node rather than logging and
  continuing. A task carrying a declared deadline that nothing is measuring
  is worse than a task with no deadline, because the flow reports a term it
  is not keeping; for a wettelijk deadline that is a legal defect, not a
  logging concern. No existing flow can reach this branch.
- Only keys the author actually set are forwarded, so arm()'s own defaults
  for purpose, legalEffect and onExpiry still apply instead of being
  overridden with nulls.

Verified: full suite 19045 tests green; negative control (remove the arm
call) fails testADeclaredSlaArmsATimerBoundToTheTask; no circular DI, since
FlowTimerService -> TaskService never reaches a flow node or the registry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(flow): cover the titleless-node branch the ratchet caught

The merge-base coverage guard failed the branch by 0.13%: 33 added
statements with exactly one never executed. It was `$title = null`, the
branch that turns an empty title into null before arming.

Nothing reached it because every test in the class builds its config through
a helper that always supplies a title, so the branch was dead on the coverage
report and nothing would have noticed if it stopped working.

It is a real case, not filler. `title` is optional on a user task, and the
timer renders it in the escalation notice: '' produces a blank subject line
where null lets the timer describe its subject instead. The new test passes
whitespace rather than omitting the key, because a title of spaces is still
no title and trim() is what makes the two the same.

Negative control: removing the branch so an empty title reaches the timer as
'' fails testATitlelessNodeArmsItsTimerWithNoTitle. 40 tests, 155 assertions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
#3351)

PHPUnit reported these three as risky for performing no assertions. The
reason is the same in all three, and it is worse than a missing assertion:
each put its only assertion inside a `catch` block that did not run.

    try {
        $this->service->extractObject(1, false);
    } catch (\Throwable $e) {
        $this->assertNotSame('Object already processed and up-to-date', $e->getMessage());
    }

extractObject() returns void and throws nothing on the path being tested, so
the catch never fired. And the string being compared is a LOG message that
never reaches an exception, so even when the catch DID run the assertion was
vacuous. Both tests claim to prove that extraction was not skipped, and both
pass most reliably when it is skipped.

Each now asserts on the only observable there is: the log. A helper captures
every message the service emits, and the tests assert the up-to-date line was
NOT logged, plus a control that '[ObjectHandler] Extracting text from object'
WAS. Without that control the first assertion passes just as happily when
extraction never began for an unrelated reason.

testExtractObjectWithNullUpdatedTimestamp also needed a real fixture. It set
getLatestUpdatedTimestamp() to null, which short-circuits isSourceUpToDate()
before the `?? time()` fallback is ever consulted, so it would have passed
with any fallback at all, including one that made the object look
permanently up-to-date and skipped extraction forever. With a stored chunk at
100 the test now distinguishes `?? time()` from `?? 0`.

testExtractObjectEntityExtractionFailureLogsError now tests what its name
says. It never reached the entity handler before; the mocked mappers can
drive the whole extraction, so the entity pass is made to throw and the test
asserts the error is logged AND that the call returns. Chunks are already
persisted by then, and a failing entity pass must not throw them away.

Negative controls, each applied to lib/ and reverted:
- `?? time()` -> `?? 0`: test 1 fails.
- entity catch rethrows: test 3 errors.
- force bypass removed: test 2 fails, but only when BOTH guards go. The
  bypass is implemented twice (extractObject and isSourceUpToDate) and either
  alone suffices, so neither site is individually covered. Recorded in a
  comment rather than silently left for the next reader.

No production code changed. The two `Undefined array key` warnings the
rewritten tests exposed come from a bare mock answering [] where the one real
implementation of processSourceChunks() always fills all three keys, so the
mock was made to keep the contract rather than the service made defensive
about a fake.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ 77aae7f

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-09-03 08:01 UTC

Download the full PDF report from the workflow artifacts.

rubenvdlinde and others added 2 commits September 3, 2026 10:16
…3354)

The suite reported 10 PHP warnings and 23 deprecations. Two of the warnings
were not hygiene: they were production bugs, and both had been written into
the tests as known behaviour.

ORGANISATION::SETACTIVE(FALSE) NEVER DEACTIVATED ANYTHING

It called parent::setActive(active: $value) with a NAMED argument. Entity has
no such method — it is reached through __call(array $args), where a named
argument lands under its name, so Entity::setter() read an undefined $args[0]
and the value never arrived. OrganisationController has an endpoint whose
whole job is deactivating an organisation, and it could not work.

OrganisationTest asserted assertTrue() on a value that should have been false
and called it "the current actual behavior".

ENDPOINTLOG::SETRESPONSE() STORED NULL ON EVERY CALL LOG

Same root cause, same shape. setStatusCode and setStatusMessage were
positional and worked; the response payload was dropped on every endpoint
call ever logged.

EndpointServiceTest said "setResponse uses named arg in source code (known
issue), so response may be null. We verify it was attempted" — and then
verified nothing.

Both are fixed and both now assert the correct behaviour. Reintroducing
either bug reddens its test; that was checked, not assumed.

I swept for the rest of the class rather than stopping at two: 94 Entity
subclasses, 686 magic setter names, 256 named-argument call sites in lib/.
Every other match is legitimate — $objectService->setRegister() is a real
service method, ->setObject(type:, id:) is OCP's INotification, and the last
two candidates were the regex matching self:: in a class constant. The class
is bounded at these two.

THE REST

- SettingsController: `while ($row !== false)` never terminates if fetch()
  answers null, which it can. Two other loops in the same file already used
  is_array(); this one did not.
- BsnFormat: str_pad() was handed a null, deprecated in 8.1 and a TypeError
  in 9. Cast once up front; an array/object is now refused explicitly, which
  is the contract str_pad() used to enforce further down by accident.
- SaveObject: a schema with a null slug cannot be found BY slug, and feeding
  the null to strtolower()/strcasecmp() is deprecated. Skipped instead.
- Four test files: mocks that answered [] or null where the single real
  implementation always fills the contract (rechainAll, processSourceChunks,
  handlePreValidationCascading). The mocks were made to keep the contract
  rather than the services made defensive about a fake. RechainAuditTrail
  also gained the tombstone-count assertion its message never had.
- Dynamic properties declared; ReflectionProperty::setValue() given its
  explicit null object argument at 14 sites.

Suite: 19046 tests. Warnings 10 -> 0, deprecations 23 -> 2, risky 3 -> 0
(the risky three landed separately in #3351). The last two deprecations are
ZipArchive::open() on an empty fixture file.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…them (#3340)

* feat(organisations): adopt a leaf app's organisations without losing them

Several apps grew their own organisation schema before OR's Organisation
carried the fields they needed. The slug is global per organisation, so
those copies collide: SchemaMapper::find() matches LOWER(slug) across
every app and hands back whichever row it reaches first. Task 1 added the
columns, which made reuse possible and moved nobody's rows.

This moves them, under two rules that were learned rather than chosen.

The uuid is the idempotency key and is preserved. dossiq's
migrate-partners arrived at the same rule and wrote down why: a leaf row
is free to carry no slug at all, and two rows sharing a name are routine,
so a name-derived key skips the second as already migrated and silently
merges two distinct legal entities.

Where the same legal entity already exists under a different uuid the
rows are not collapsed into one. The adopted row is created and pointed
at the existing one through mergedInto, so both uuids keep resolving and
the merge is a fact recorded on a row rather than data thrown away.
Matching runs on OIN, then RSIN, then KVK, normalised for punctuation
because the same OIN is typed with and without spaces and dots, and never
on a name. The lowest id is canonical so a repeated run picks the same
survivor, and a candidate that was itself merged away loses to a live
one.

Properties Organisation has no column for are named before the write.
OpenRegister discards an undeclared property and answers 200 with the
object, so an adoption that loses fields is otherwise indistinguishable
from one that did not.

Dry-run by default. Verified on the dev instance end to end: adoption,
a second run adopting nothing, a merge matched across punctuation, and
the negative control where clearing the shared OIN stops the merge being
reported.

* docs(organisations): record what the leaf-side measurement found

The plan for task 5.2 was to map stackiq's fields onto Organisation where
they fit and rehome the rest. Reading the entity rather than assuming,
none of the three candidates fit.

contacts is linked Nextcloud Contacts app data, not a list of contact
people. children is not stored at all: the setter says it is loaded on
demand for API responses, so a written value is dropped and recomputed.
type has a closed vocabulary and a collaboration SUBtype is a different
axis from it.

So all nine stackiq properties belong on a stackiq-owned schema and none
on Organisation, which is a different change from the one that was
planned. Also records the site counts that decide the sequencing: 22 for
opencatalogi, most of them stored references that keep resolving, against
235 for stackiq, which treats the slug as a first-class object type.

* test(organisations): drive the adopt command end to end

The coverage guard was right again: the pure rules were pinned and the
command that runs them was not, so execute(), adoptRow(),
reportUndeclared() and existingOrganisations() shipped uncovered.

Ten cases through CommandTester, and the ones that matter are the
refusals. Without --register the command says so rather than reading
whatever it can find. The default is a dry run, because the alternative
default is a command that writes to every organisation on the instance
the first time somebody types its name to see what it does. A row with no
uuid is skipped rather than adopted, since without an idempotency key it
would be duplicated on every run.

One case is worth its own line: two rows in the SAME run sharing a legal
identifier now merge, because the candidate set grows as rows are
adopted. That path had no coverage and is exactly where a by-value
parameter would have silently done nothing.

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ 5c69909

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-09-03 08:21 UTC

Download the full PDF report from the workflow artifacts.

… no object (#3346)

Apps grew their own document object to hold a publication window over an
attached file. Measuring opencatalogi's, every other property it carries
already has a home: filename and mime type are the file, description and
labels are the OR-side metadata row, the owning publication is the folder
the file lives in, and the file's text is already extracted into
openregister_chunks and searchable.

The window was the only real gap. publishFile() is a boolean, so an
attachment could not be depublished on a date independently of the record
it belongs to, which is what a WOO bijlage needs, and that alone forced a
whole object type into existence.

The second reason is stronger. ContentSearchHandler already resolves a
file chunk to its owning object through findOwningObjectUuid(), so a
keyword hit inside a file attached to a publication resolves straight to
the publication. The schema widening in opencatalogi #1391 exists only
because the attachment is a separate object outside the catalog's schema
scope. Files on publications would have made that class of bug impossible.

The window is a rule rather than a comparison because each null in it
means something different: no publication date is never published and
must not fall back to the creation time, a future one is not yet, and no
depublication date is no end date rather than an end date in the past.
Start inclusive, end exclusive, so a zero-length window publishes nothing.

A depublication date is written onto the public share's expiration, which
Nextcloud already honours. An OR-side flag alone would leave a public URL
that still serves the file, and a URL that still works is not a
depublication.

Two repairs on the way. formatFile() reported the creation time under
'published', so every file that had ever existed looked published and
'not published' was unrepresentable; the creation time is kept under
'created', where it is true. And FileMapper declared a @phpstan-type
alias named File describing a filecache ROW, which shadowed the entity of
the same name in every docblock in that file: a method annotated
@return File read as an array shape. Renaming it to FilecacheRow and
naming the entity in the generic removed 12 phpstan baseline entries.

Verified on the dev instance: the migration applies and is a no-op on
re-run, and all four window states behave correctly round-tripped through
the database, not only in memory.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ 8a6ebd2

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

Quality workflow — 2026-09-03 08:25 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ 2dab142

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-09-03 08:43 UTC

Download the full PDF report from the workflow artifacts.

rubenvdlinde and others added 2 commits September 3, 2026 12:27
…forever (#3358)

A UserTask's completion signal was REFUSED on the acceptance rig (the
assignee group did not exist at signal time), and the suspended run's
30-minute heartbeat then fired, re-suspended, and never advanced:
resume_at rolled 08:07 -> 08:37 -> ... while the task sat `completed` and
the group had long been created. The heartbeat exists precisely to recover
a missed wake; it recovered nothing.

The node was never the problem. UserTaskNode has always re-read its task on
re-entry and applied the outcome when terminal. What wedged is upstream:
persistResult() dropped EVERY parked node's resume slot whenever a pass
ended anything but `suspended`. FlowResumeState::storableWhen(suspended:)
read NOT-SUSPENDED as "nothing left to continue from", which conflates it
with TERMINAL -- and a pass legitimately ends `queued` while a node parked
in an EARLIER pass still waits: the in-request advance of a sibling branch
finalises `queued` whenever other enabled work remains, and a refused claim
does the same.

The parked node then lost the uuid of the task it was waiting on. Its next
wake found an empty slot and -- correctly, by its own idempotency guard --
created a NEW task. From that moment the ORIGINAL task's completion could
never address the node's slot, its signal was refused against the new
slot's recorded assignee (the refusal observed on the rig), every heartbeat
re-read the new open task and re-suspended, and a duplicate task sat in
somebody's inbox.

Slots now survive every pass end the run can still advance from, and drop
only on a terminal one. A recovered delivery is recorded on the task's
audit as `heartbeat-recovered`, attributed to the task's completedBy, so
the trail no longer ends at the refusal.

The symmetric cases need no new mechanism, and the change says so. A task
completed while the run was not yet suspended (the race) loses its signal
-- signal() refuses a non-suspended run -- and the node then parks on a
non-null heartbeat, so the next wake re-reads it; the race costs at most
one heartbeat period. A task concluded by TaskSequenceService reaches its
terminal state on the same row the re-read observes, so it is covered with
no sequence-specific handling. No second delivery mechanism is added: the
wake, the re-read and the outcome application are the paths that already
existed.

Proven red before the fix: FlowHeartbeatRecoveryTest drives the real
engine, dispatcher, node, stream walk, claims and commit path over
in-memory mappers, so the pass-to-pass persistence that loses the slot is
exercised exactly as the worker exercises it.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…chor (#3360)

The mapper could answer "what happened to THIS object" three ways and "has this
approval ever run" not at all. Every existing finder constrains
anchor_object_uuid, so an aggregate over a template had no anchor to pass.

buildiq is the caller that needed it. Its automation dry-run panel used to read
the newest ApprovalStep on a chain; when #3302 retired that surface there was no
replacement for the aggregate, so the panel degraded to reporting nothing at all
(buildiq#651). This restores it.

One row off the existing template_id index, ordered like its siblings and
limited in the QUERY rather than trimmed in PHP.

Tested with a negative control, not just a green run: adding an anchor predicate
and dropping the limit makes the new test fail with "Failed asserting that an
array does not contain 'anchor_object_uuid'". A finder that quietly constrained
the anchor would answer a different question than the caller asked, which is the
one thing this method must not do.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ 8a3d83e

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

Quality workflow — 2026-09-03 10:41 UTC

Download the full PDF report from the workflow artifacts.

…3362)

The changed-files coverage ratchet on #3358 was right, and it was pointing
at something real rather than at a percentage. Of the 32 statements that
change added, 30 were the body of
`FlowTaskBridge::recordHeartbeatRecovery()` -- and every test that exercised
the recovery MOCKED FlowTaskBridge, because in those tests the nodes are the
unit. So the method that writes the recovery's audit trail had no execution
coverage at all: the audit entry, its attribution, and the catch that makes
it best-effort were asserted nowhere.

That matters more than the percentage does. The guarded signal seam records
a refusal; this entry is the other half of that trail, and a silent
regression in it would make a recovered answer read as one that vanished.

Three tests through the REAL bridge, in the suite that already builds one:

- the entry is recorded as `heartbeat-recovered`, attributed to the task's
  completedBy, with a reason naming the run whose signal never arrived;
- an audit write that THROWS is swallowed, because the recovery is the node
  applying the outcome and letting the failure out would abort the very walk
  that was un-wedging the run;
- an ending nobody answered (terminated, expired -- `completedBy` is null on
  exactly those) records no actor rather than a guessed one.

And one test for the symmetric case the change documented but left unpinned:
a completion that RACED the suspension. `signal()` refuses a run that is not
suspended, so that wake is lost with nothing to retry it; the test asserts
the refusal, asserts the run parks on a non-null heartbeat, and asserts the
next wake recovers it. That is the whole basis for deciding the race needs
no new mechanism, and it is now falsifiable.

Every one of the four was checked by mutation -- breaking the action name,
the attribution, the catch, or the recovery call itself turns each red.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/openregister @ 9ef7e08

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-09-03 11:07 UTC

Download the full PDF report from the workflow artifacts.

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.

2 participants