Skip to content

Release: merge development into beta - #2

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

Release: merge development into beta#2
github-actions[bot] wants to merge 1746 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

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit 5162411
Branch 2/merge
Event pull_request
Generated 2026-04-13 08:54 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24334560020

Summary

Group Result
PHP Quality FAIL
Vue Quality FAIL
Security FAIL
License FAIL
PHPUnit FAIL
Newman FAIL
Playwright FAIL

PHP Quality

Tool Result
lint FAIL
phpcs FAIL
phpmd FAIL
psalm FAIL
phpstan FAIL
phpmetrics FAIL

Vue Quality

Tool Result
eslint FAIL
stylelint FAIL

Security

Ecosystem Result
composer FAIL
npm FAIL

License Compliance

Ecosystem Result
composer FAIL
npm FAIL

PHPUnit Tests

PHP Nextcloud Result
Overall FAIL

Integration Tests (Newman)

Result
FAIL

E2E Tests (Playwright)

Result
FAIL

Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit a16aa41
Branch 2/merge
Event pull_request
Generated 2026-04-13 08:55 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24334571362

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security FAIL
License PASS
PHPUnit SKIP
Newman SKIP
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm FAIL

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (215 total)

Metric Count
Approved (allowlist) 215
Approved (override) 0
Denied 0

PHPUnit Tests

PHPUnit tests were not enabled for this run.

Integration Tests (Newman)

Newman integration tests were not enabled for this run.

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit 670cc9a
Branch 2/merge
Event pull_request
Generated 2026-04-13 09:02 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24334897802

Summary

Group Result
PHP Quality FAIL
Vue Quality FAIL
Security FAIL
License FAIL
PHPUnit FAIL
Newman FAIL
Playwright FAIL

PHP Quality

Tool Result
lint FAIL
phpcs FAIL
phpmd FAIL
psalm FAIL
phpstan FAIL
phpmetrics FAIL

Vue Quality

Tool Result
eslint FAIL
stylelint FAIL

Security

Ecosystem Result
composer FAIL
npm FAIL

License Compliance

Ecosystem Result
composer FAIL
npm FAIL

PHPUnit Tests

PHP Nextcloud Result
Overall FAIL

Integration Tests (Newman)

Result
FAIL

E2E Tests (Playwright)

Result
FAIL

Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit 2dc646a
Branch 2/merge
Event pull_request
Generated 2026-04-13 09:03 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24334903317

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security FAIL
License PASS
PHPUnit SKIP
Newman SKIP
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm FAIL

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (215 total)

Metric Count
Approved (allowlist) 215
Approved (override) 0
Denied 0

PHPUnit Tests

PHPUnit tests were not enabled for this run.

Integration Tests (Newman)

Newman integration tests were not enabled for this run.

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit 93d5eb2
Branch 2/merge
Event pull_request
Generated 2026-04-13 09:23 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24335752189

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security FAIL
License PASS
PHPUnit SKIP
Newman SKIP
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm FAIL

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (215 total)

Metric Count
Approved (allowlist) 215
Approved (override) 0
Denied 0

PHPUnit Tests

PHPUnit tests were not enabled for this run.

Integration Tests (Newman)

Newman integration tests were not enabled for this run.

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit 9d0afc3
Branch 2/merge
Event pull_request
Generated 2026-04-13 09:55 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24337124007

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security FAIL
License PASS
PHPUnit SKIP
Newman SKIP
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm FAIL

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (215 total)

Metric Count
Approved (allowlist) 215
Approved (override) 0
Denied 0

PHPUnit Tests

PHPUnit tests were not enabled for this run.

Integration Tests (Newman)

Newman integration tests were not enabled for this run.

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit 435f2e1
Branch 2/merge
Event pull_request
Generated 2026-04-13 10:15 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24337976932

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security FAIL
License PASS
PHPUnit SKIP
Newman SKIP
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm FAIL

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (215 total)

Metric Count
Approved (allowlist) 215
Approved (override) 0
Denied 0

PHPUnit Tests

PHPUnit tests were not enabled for this run.

Integration Tests (Newman)

Newman integration tests were not enabled for this run.

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit a9c695b
Branch 2/merge
Event pull_request
Generated 2026-04-13 10:20 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24338175657

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security FAIL
License PASS
PHPUnit SKIP
Newman SKIP
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm FAIL

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (215 total)

Metric Count
Approved (allowlist) 215
Approved (override) 0
Denied 0

PHPUnit Tests

PHPUnit tests were not enabled for this run.

Integration Tests (Newman)

Newman integration tests were not enabled for this run.

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit c9e3bc8
Branch 2/merge
Event pull_request
Generated 2026-04-13 12:34 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24343509348

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit PASS
Newman PASS
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (416 total)

Metric Count
Approved (allowlist) 416
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall PASS

Code coverage: 0% (0 / 3 statements)

Integration Tests (Newman)

Result
PASS

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit 7654d34
Branch 2/merge
Event pull_request
Generated 2026-04-13 14:38 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24349332420

Summary

Group Result
PHP Quality PASS
Vue Quality FAIL
Security FAIL
License FAIL
PHPUnit FAIL
Newman FAIL
Playwright FAIL

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint FAIL
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (416 total)

Metric Count
Approved (allowlist) 416
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall FAIL

Integration Tests (Newman)

Result
FAIL

E2E Tests (Playwright)

Result
FAIL

Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit 974a8e6
Branch 2/merge
Event pull_request
Generated 2026-04-13 14:41 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24349355643

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit PASS
Newman PASS
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (416 total)

Metric Count
Approved (allowlist) 416
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall PASS

Code coverage: 0% (0 / 3 statements)

Integration Tests (Newman)

Result
PASS

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit 62a4b1d
Branch 2/merge
Event pull_request
Generated 2026-04-13 14:47 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24349766345

Summary

Group Result
PHP Quality FAIL
Vue Quality FAIL
Security FAIL
License FAIL
PHPUnit FAIL
Newman FAIL
Playwright FAIL

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (416 total)

Metric Count
Approved (allowlist) 416
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall FAIL

Integration Tests (Newman)

Result
FAIL

E2E Tests (Playwright)

Result
FAIL

Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit e1e0d31
Branch 2/merge
Event pull_request
Generated 2026-04-13 14:49 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24349777561

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit PASS
Newman PASS
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (416 total)

Metric Count
Approved (allowlist) 416
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall PASS

Code coverage: 0% (0 / 3 statements)

Integration Tests (Newman)

Result
PASS

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit 89bce6c
Branch 2/merge
Event pull_request
Generated 2026-04-13 14:54 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24350135281

Summary

Group Result
PHP Quality FAIL
Vue Quality FAIL
Security FAIL
License FAIL
PHPUnit FAIL
Newman FAIL
Playwright FAIL

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (416 total)

Metric Count
Approved (allowlist) 416
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall FAIL

Integration Tests (Newman)

Result
FAIL

E2E Tests (Playwright)

Result
FAIL

Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit 025bc65
Branch 2/merge
Event pull_request
Generated 2026-04-13 14:55 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24350144732

Summary

Group Result
PHP Quality FAIL
Vue Quality FAIL
Security FAIL
License FAIL
PHPUnit FAIL
Newman FAIL
Playwright FAIL

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer FAIL
npm PASS

npm dependencies (416 total)

Metric Count
Approved (allowlist) 416
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall FAIL

Integration Tests (Newman)

Result
FAIL

E2E Tests (Playwright)

Result
FAIL

Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit 4a9fdf1
Branch 2/merge
Event pull_request
Generated 2026-04-13 14:58 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24350215258

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit PASS
Newman PASS
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (416 total)

Metric Count
Approved (allowlist) 416
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall PASS

Code coverage: 0% (0 / 3 statements)

Integration Tests (Newman)

Result
PASS

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit f49cf7f
Branch 2/merge
Event pull_request
Generated 2026-04-13 15:01 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24350447586

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit PASS
Newman PASS
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (416 total)

Metric Count
Approved (allowlist) 416
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall PASS

Code coverage: 0% (0 / 3 statements)

Integration Tests (Newman)

Result
PASS

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit 46851d6
Branch 2/merge
Event pull_request
Generated 2026-04-13 16:03 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24353432528

Summary

Group Result
PHP Quality FAIL
Vue Quality FAIL
Security FAIL
License FAIL
PHPUnit FAIL
Newman FAIL
Playwright FAIL

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint FAIL

Security

Ecosystem Result
composer PASS
npm FAIL

License Compliance

Ecosystem Result
composer PASS
npm FAIL

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall FAIL

Integration Tests (Newman)

Result
FAIL

E2E Tests (Playwright)

Result
FAIL

Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit 14de23d
Branch 2/merge
Event pull_request
Generated 2026-04-13 16:06 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24353443990

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit PASS
Newman PASS
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (416 total)

Metric Count
Approved (allowlist) 416
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall PASS

Code coverage: 0% (0 / 3 statements)

Integration Tests (Newman)

Result
PASS

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit f0757f4
Branch 2/merge
Event pull_request
Generated 2026-04-13 16:11 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24353702618

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit PASS
Newman PASS
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (416 total)

Metric Count
Approved (allowlist) 416
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall PASS

Code coverage: 0% (0 / 3 statements)

Integration Tests (Newman)

Result
PASS

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit 8055aef
Branch 2/merge
Event pull_request
Generated 2026-04-13 16:16 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24353955450

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit PASS
Newman PASS
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (416 total)

Metric Count
Approved (allowlist) 416
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall PASS

Code coverage: 0% (0 / 3 statements)

Integration Tests (Newman)

Result
PASS

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit a8263a7
Branch 2/merge
Event pull_request
Generated 2026-04-13 16:34 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24354796361

Summary

Group Result
PHP Quality FAIL
Vue Quality FAIL
Security FAIL
License FAIL
PHPUnit FAIL
Newman FAIL
Playwright FAIL

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan FAIL
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint FAIL

Security

Ecosystem Result
composer FAIL
npm PASS

License Compliance

Ecosystem Result
composer FAIL
npm PASS

npm dependencies (416 total)

Metric Count
Approved (allowlist) 416
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall FAIL

Integration Tests (Newman)

Result
FAIL

E2E Tests (Playwright)

Result
FAIL

Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit 1c00b50
Branch 2/merge
Event pull_request
Generated 2026-04-13 16:37 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24354803809

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit PASS
Newman PASS
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (416 total)

Metric Count
Approved (allowlist) 416
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall PASS

Code coverage: 0% (0 / 3 statements)

Integration Tests (Newman)

Result
PASS

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit a821cfa
Branch 2/merge
Event pull_request
Generated 2026-04-13 16:45 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24355284069

Summary

Group Result
PHP Quality FAIL
Vue Quality FAIL
Security FAIL
License FAIL
PHPUnit FAIL
Newman FAIL
Playwright FAIL

PHP Quality

Tool Result
lint PASS
phpcs FAIL
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics FAIL

Vue Quality

Tool Result
eslint FAIL
stylelint FAIL

Security

Ecosystem Result
composer FAIL
npm PASS

License Compliance

Ecosystem Result
composer FAIL
npm PASS

npm dependencies (416 total)

Metric Count
Approved (allowlist) 416
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall FAIL

Integration Tests (Newman)

Result
FAIL

E2E Tests (Playwright)

Result
FAIL

Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit 6bb58b5
Branch 2/merge
Event pull_request
Generated 2026-04-13 17:27 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24355290782

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit PASS
Newman PASS
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (416 total)

Metric Count
Approved (allowlist) 416
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall PASS

Code coverage: 0% (0 / 3 statements)

Integration Tests (Newman)

Result
PASS

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit 77ae624
Branch 2/merge
Event pull_request
Generated 2026-04-13 17:29 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24357245977

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License FAIL
PHPUnit FAIL
Newman FAIL
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (416 total)

Metric Count
Approved (allowlist) 416
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall FAIL

Integration Tests (Newman)

Result
FAIL

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit aac3d46
Branch 2/merge
Event pull_request
Generated 2026-04-13 17:32 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24357336979

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit PASS
Newman PASS
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (416 total)

Metric Count
Approved (allowlist) 416
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall PASS

Code coverage: 0% (0 / 3 statements)

Integration Tests (Newman)

Result
PASS

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/decidesk
Commit 886a6a0
Branch 2/merge
Event pull_request
Generated 2026-04-13 17:53 UTC
Workflow Run https://github.com/ConductionNL/decidesk/actions/runs/24358305298

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit PASS
Newman PASS
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (416 total)

Metric Count
Approved (allowlist) 416
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall PASS

Code coverage: 0% (0 / 3 statements)

Integration Tests (Newman)

Result
PASS

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/decidesk @ 4fd7b43

Check Result
PHP lint
PHP phpcs
PHP phpmd
PHP psalm
PHP phpstan
PHP phpmetrics
eslint
stylelint
Security (composer)
Security (npm)
License (composer) ✅ 100/100
License (npm) ✅ 416/416
PHPUnit
Newman
Playwright ⏭️

Coverage: 0% (0/3 statements)


Quality workflow — 2026-04-13 18:03 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/decidesk @ 7bb85fd

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm
PHPUnit
Newman
Playwright

Quality workflow — 2026-04-13 18:11 UTC

Download the full PDF report from the workflow artifacts.

rubenvdlinde and others added 30 commits August 7, 2026 10:15
…to the relation filter

Two regressions this branch introduced, both mechanical, neither a real
behaviour change.

PHPUnit (2 errors of 806 tests, identical on all four legs)

  Error: Call to undefined method MockObject_ObjectService::matching()
  lib/Service/ParticipationPublicationService.php:318

ParticipationPublicationService resolves TWO collaborators from the DI
container -- OpenRegister's ObjectService and, new on this branch,
decidesk's own ObjectRelationFilter. The test's container stub was
`->method('get')->willReturn($this->objectService)`: unconditional, so it
answered BOTH ids with the ObjectService mock, and the relation-filter call
landed on an object that has no such method. Dispatch on the requested id.

The real ObjectRelationFilter is used rather than a mock -- it is a
dependency-free pure filter, and a mock would assert nothing about the
disclosure boundary the scoping exists to enforce. The digest fixtures
therefore now carry the structured `relations` array ReactionIntakeService
actually writes, and a third reaction pinned to a DIFFERENT consultation
asserts it is excluded: the OpenRegister filter pins the related id but not
the related schema, and this digest is public output.

Proven to fail: making `references()` return true unconditionally fails
testReactionDigestIsPiiFree (3 != 2); restoring the blanket container stub
reproduces both original errors verbatim.

eslint / lint-check (1 error, shared by both jobs)

  StateMachineEditor.vue 15:65 Parsing error: nested-comment

The component's header comment quotes the literal `<!---->` while explaining
that Vue 3 substitutes a comment node for a crashed subtree. `<!--` inside an
HTML comment is a nested-comment parse error, so the whole SFC failed to
parse. Reworded; the explanation is unchanged.

Proven to fail: reinstating the literal restores the error at 15:65.

Verified: 806 tests, 0 errors (PHP 8.4), matching development.
fix(e2e): seed the fixture, key relation filters correctly, wire the orphan dashboard widgets
)

The 32 floor was raised on the premise that nothing tested below it. That is
false here: this repo's own CI runs stable31, and min-version is enforced at
install time, so occ app:enable refuses on 31 and the e2e seed fails with
"is not installed or enabled".

The original reason for a 32 floor no longer holds either. It came from
openregister implementing OCP\ContextChat\IContentProvider, an interface
absent before NC 32. openregister#2372 removed every eager reference to that
class, so it is only loaded inside interface_exists() guards and the header is
never read on an older server. openregister#2380 restored its own 28 floor on
that evidence.
…ects

Two tools in the same pipeline gave OPPOSITE instructions about where an
`@spec` tag should point, and following the one that runs FIRST manufactured
findings for the one that runs SECOND.

`SpecTagSniff` runs as a blocking `PHP Quality (phpcs)` job and told every
developer, in its file docblock and in its own warning text:

    @SPEC openspec/changes/{change-name}/tasks.md#task-N

A change directory is temporary by definition — completing a change moves it
to `openspec/changes/archive/<date>-<name>/`, and renaming or dropping one
removes the target outright. Every tag written to that instruction dangles
from that moment on, and gate-46 (spec-anchor-existence) reports it. The
developer who wrote the tag had followed this sniff's own advice.

Measured on portaliq: 100 unresolved gate-46 targets, and 260 of its 385 live
tags pointing into a change directory. The sniff ships identically in 20
ConductionNL repos, so grinding the tags without fixing the sniff regenerates
them at the rate changes are archived.

This changes the docblock example and BOTH warning messages to the canonical
form gate-46 and the project rule agree on:

    @SPEC openspec/specs/{capability}/spec.md#requirement-{slug}

The method-level message previously carried no guidance at all, so a developer
reading it had only the class message to copy from; it now names the same
canonical shape.

Behaviour is unchanged: severity stays WARNING (verified via phpcs — an
untagged class and public method still report 0 errors / 2 warnings, and a
tagged file still reports nothing), and an `openspec/changes/...` target is
still accepted, since this sniff only checks that a tag is PRESENT.

No `@spec` tags are repointed here — this repo's existing tags are untouched.

Refs ConductionNL/.github#228
Fleet-wide Playwright instrument sweep, ConductionNL/.github#188. Neither
change can alter a verdict; both change whether you can see why a verdict
happened.

This repo's `trace` is already `retain-on-failure`, so only the timeout half applies here.

No repo in the fleet set `globalTimeout`. The shared quality.yml Playwright
job is `timeout-minutes: 45`, and a job cancelled by that cap produces no
verdict and no artifacts: the trace upload is `if: failure()` and the report
upload is `if: always()`, and neither runs on a cancelled job, while
`gh pr checks` still renders it as "fail". Runs cancelled at ~45m16s have
been observed in this fleet. Measured overhead in that job before the
`Run Playwright tests` step starts is 2.0-2.4 min, so 38m leaves ~7 min of
margin while guaranteeing a tally and its artifacts.
…l-target

fix(phpcs): stop the SpecTagSniff instructing the pattern gate-46 rejects
decidesk was the last repo in the fleet still installing NC stable31, and it
is the leg that blocked #425.

openregister declares min-version="32" (openregister#2384, merged today), and
decidesk installs it as an `additional-apps` entry while `src/manifest.json`
names it a hard dependency. On stable31 `occ app:enable openregister` refuses
with "not compatible with this version of the server" — but the shared
workflow runs it as

    php occ app:enable "$name" || echo "::warning::Failed to enable $name, continuing..."

so the refusal is a WARNING, the run continues without its data layer, and
dies ~70s later on missing schemas. That reads like an app fault. It is not.

Order compounded it: the newman, playwright and journeydoc-capture jobs each
check the server out at `fromJSON(inputs.nextcloud-test-refs)[0]`, so stable31
sitting first put all three on the one version openregister cannot load.

Two halves, both required:
- matrix: '["stable31","stable32"]' -> '["stable32"]'. stable33 deliberately
  not added; this removes an impossible leg, it does not widen the matrix.
- info.xml: <nextcloud min-version> 28 -> 32, per the rule that an app's floor
  must be >= the maximum floor of every app it hard-depends on
  (openconnector#1172/#1173). <php min-version="8.3"/> and max-version="34"
  are unchanged.

openspec/app-config.json carries the same matrix and moves with it, so
/app-verify does not report drift.

This reverses #424, whose premise has expired: it restored the 28 floor on the
grounds that openregister had gone back to 28 in openregister#2380. #2384 moved
it back to 32 deliberately and fleet-wide, and names decidesk as one of eight
consumers that must move their matrices to stable32.
)

The canonical AppHost route table (`OCA\OpenRegister\AppHost\Routes::standard()`)
ships `['name' => 'settings#update', 'url' => '/api/settings', 'verb' => 'PUT']`,
and decidesk's own openregister-absent fallback in `appinfo/routes.php`
re-declares it verbatim (decidesk#377) — so the route is live on BOTH paths.

`AppHost\Bootstrap::aliasControllerUnlessLeafDefinesIt()` only substitutes
OpenRegister's `GenericSettingsController` when the leaf does NOT ship a class
of that name. decidesk ships `lib/Controller/SettingsController.php`, so the
alias is skipped and decidesk owes every method the canonical table routes to
`settings#`. It had index/create/load but no update().

Measured 2026-08-08 on the dev instance:

    GET  /apps/decidesk/api/settings -> 200   (positive control)
    PUT  /apps/decidesk/api/settings -> 500

    ReflectionException: Method
    OCA\Decidesk\Controller\SettingsController::update() does not exist
    at lib/private/AppFramework/Utility/ControllerMethodReflector.php:40

Moves the create() body into update() and makes create() a delegate, so the
POST alias (still used by src/store/modules/settings.js::saveSettings) keeps
byte-identical behaviour. Both carry #[AuthorizedAdminSetting] — the middleware
only evaluates attributes on the DISPATCHED method, so delegation inherits
nothing, and the write reaches instance-wide IAppConfig.

Adds two unit test files, each with a positive control so a green cannot be
produced vacuously.

Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…, not in flight (#436)

* fix(decision): require outcome + decisionDate only in terminal states, not in flight

`Decision` declared `outcome` (enum adopted|rejected) and `decisionDate` as
unconditionally `required`, so every motion/amendment/resolution remapped onto
the supertype by ADR-005 was refused at creation:

    400 "The required properties (decisionDate, outcome) are missing."

An in-flight motion has no legal outcome. That contradicts the schema's own
`lifecycle` description ("outcome ... the voting result, set when reaching
'decided'") and ADR-005's orthogonality of lifecycle and outcome.

PO decision: required only at the end, not in flight.

Terminal states, derived not guessed
------------------------------------
`decided`, `enacted`, `archived` — from the register's own `lifecycle` enum and
`x-openregister-lifecycle` map: `decided` is the first state past the vote and
the other two are reachable only through it. `withdrawn` is deliberately NOT
terminal-for-outcome: it ends the lifecycle but the decision was never decided,
so demanding an adopted/rejected result there would forbid withdrawing a draft.

OpenRegister does NOT enforce conditional `required` — measured
---------------------------------------------------------------
JSON Schema `if`/`then` is the idiomatic shape, and it is inert here.
`Db\Schema` has no if/then field, and `Schema::getSchemaObject()` rebuilds the
validated object from a fixed key list (title/description/version/type/required/
$schema/$id/properties), so the block never reaches the validator.

Live in-memory probe against the installed OpenRegister, with positive controls:

  CONTROL unconditional required missing (no title)   valid=false
  CONTROL enum violation (outcome=banana)             valid=false
  in-flight draft, no outcome                         valid=true
  TERMINAL decided, no outcome/decisionDate           valid=true   <- if/then ignored

The controls prove the probe can report false; the terminal case still passes.
So an `if`/`then` on the schema would have been decorative. It is not shipped.

Enforcement lives where the transition happens
----------------------------------------------
- `DecisionTransitionGuard::TERMINAL_OUTCOME_STATES` + `getMissingTerminalFields()`
  (pure, no DI, exhaustively unit-tested). An out-of-vocabulary `outcome` such
  as the `pending` placeholder counts as missing — recording "pending" as the
  result of a vote is the same defect as recording nothing.
- `DecisionLifecycleService::resolveStateGateRejection()` refuses entry into any
  terminal outcome state and names the missing fields.

Schema `required` is now [title, text, decisionType].

Proved both directions
----------------------
- in-flight motion (lifecycle=voting, no outcome/decisionDate) is ACCEPTED
- decision entering decided/enacted without them is REJECTED, not persisted

Can-fail proofs, both for the right reason:
- restore the old `required[]` on the fixed register -> the in-flight seed goes
  red with exactly the original message "The required properties (decisionDate,
  outcome) are missing"; with the new list it validates true
- neutralise `getMissingTerminalFields()` -> the three terminal tests plus the
  guard test go red, each printing the defective object that got written

Seeds
-----
Measured with OpenRegister's own validator, `@ref:` tokens resolved as
ImportHandler does: 8 of 19 decision seeds imported before, 19 of 19 after.
The 11 refusals had three distinct causes, only one of which was this decision:
- 8 x missing decisionDate (+ text on 6) -- the PO issue
- 1 x `motionType: "motie"`, not in the enum [motion|amendment|order|procedural]
- 2 x bare slugs in `route`/`supersedes` where `format: uuid` applies; only
  `@ref:`-prefixed strings are resolved by ImportHandler::replaceRefTokens()
Also `voteType: open|secret` -> `named|anonymous` (enum is
named|anonymous|unanimous-consent|acclamation). `motie-woonlasten-2025` keeps
NO outcome and NO decisionDate: it is the in-flight case this change unblocks,
and the register would otherwise never exercise it.

`RegisterJsonTest::testDecisionSeedsRespectTerminalCompleteness` pins both
halves so relaxing `required[]` cannot quietly become a licence to ship a
decided decision with no result.

Scope
-----
Register change, checked for blast radius: no other app declares a Decision with
the outcome/decisionDate pair. procest has a `decision` schema but with
`required: []` and neither property. Untouched.

Known gap: a raw write straight to OpenRegister's object API bypasses this gate,
exactly as it already bypasses the chair-only and quorum gates. Recorded in the
schema's `x-decidesk-terminal-completeness` note.

Quality: phpcs 0 errors (5 pre-existing warnings), phpmd 0, psalm 0, phpstan 0,
phpunit 808/808 (29 skipped).

* fix(e2e+seeds): assert the in-flight contract on the form; revert an @ref seed change that lost a seed

Two corrections to the previous commit, both driven by CI evidence rather than
by my own model of the import path.

1. The Add Decision dialog test encoded the OLD contract
--------------------------------------------------------
`decision-management.spec.ts` asserted `'Decision date *'` and `'Outcome *'`
were visible. The asterisk is CnFormDialog rendering
`field.label + (field.required ? ' *' : '')`, so it is a direct UI read of the
schema's `required[]`. Removing those two from `required[]` correctly removed
their asterisks and the test went red — the change working, not breaking.

The test now asserts the new contract in both directions: `Title *`, `Text *`
and `Decision type *` are present, `Decision date *` and `Outcome *` have
count 0, and both fields are still ON the form (optional, not removed).
`Decision date` needs `.first()` now that the asterisk no longer disambiguates
the datetime-picker's two labels.

2. Reverted the `@ref:` seed tokens — they LOST a seed
------------------------------------------------------
The previous commit rewrote three seeds' `amends`/`route`/`supersedes` bare
slugs into `@ref:<schema>:<slug>` tokens. Measured consequence in CI:

    [ImportHandler] Skipping seed object for 'decision' - import failed:
    SQLSTATE[22001]: String data, right truncated:
    value too long for type character varying(36)

`@ref:decision:motie-duurzaamheid-2025` is 41 characters and the relation
column is `varchar(36)`: the token is written BEFORE any resolution on this
path. `Amendement Cultuursubsidie verhogen` appears 4x in development's CI log
and 0x in mine, and the live decision count went 25 -> 24. Reverted; the
bare-slug form is what actually imports.

RETRACTION: the previous commit's "8 of 19 seeds imported before, 19 of 19
after" is WRONG and is withdrawn
-------------------------------------------------------------------------
That number came from an in-memory probe that ran OpenRegister's validator over
the raw seed JSON. It is not a faithful model of the seed import path. Measured
against the live CI instance on development, all ten seeds the probe called
"REFUSED" are present — including every one missing `decisionDate`, and both
bare-slug relation seeds. The seed import path does NOT enforce the schema's
`required[]`.

What the probe DID model correctly is the object-API path
(`POST /api/objects/decidesk/decision`), which does validate — that is where
`400 "The required properties (decisionDate, outcome) are missing"` comes from,
it is what #425's Newman fixtures and the e2e fixtures hit, and it is what the
form's `*` reflects. So this change unblocks API/UI creation of in-flight
decisions; it was never a seed-import fix.

The seed edits are therefore DATA-QUALITY corrections, not import unblockers:
terminal seeds gain the `decisionDate` the new rule requires, the in-flight
motion drops its out-of-enum `outcome: "pending"`, and `motionType`/`voteType`
gain in-enum values. `RegisterJsonTest::testDecisionSeedsRespectTerminalCompleteness`
pins them.

Quality: phpcs 0 errors, phpmd 0, phpunit 808/808 (29 skipped).

* test(decision): pin every transition target as terminal-or-in-flight (fail-closed)

The terminal-completeness gate keys off the transition's TARGET state, so a
transition added later whose target nobody classified would skip the gate
silently — the same fail-open shape `transitionLifecycle()` had when it chose
MOTION_TRANSITIONS for every objectType that was not literally 'amendment'.

`testEveryTransitionTargetIsClassified` walks the transition map and asserts
each target is classified exactly once (XOR) as terminal or in-flight, so
adding a state without deciding which it is fails the suite instead of quietly
disabling the check.

Can-fail proof: adding an unclassified `ratify -> ratified` transition makes it
red naming the state and both options.

phpunit 809/809 (29 skipped).

---------

Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
… become 404/400, and migrate the lifecycle vocabulary (#425)

* fix(adr-005): migrate the PHP half of the Decision-supertype fold

ADR-005 folded Motion and Amendment into Decision as decisionType values and
deleted the motion/amendment/resolution schemas from decidesk_register.json.
The frontend migrated (src/store/store.js remaps both logical types onto the
decision schema) and tests/Unit/RegisterJsonTest.php asserts the schemas are
gone. The PHP never migrated: 13 lib/ files still addressed the deleted slugs.

That is not a silent miss. ObjectService::setSchema() rethrows
DoesNotExistException, which is neither InvalidArgumentException nor
RuntimeException, so it escapes every controller catch clause and the endpoint
answers 500 where it owes 404 or 400. Measured: "unknown motion is 404 not 500"
returned 500, and the amendment-order, minutes-draft and voting-behaviour paths
all reached a dead schema.

Mapping implemented, taken from ADR-005's discriminator table and the register:
  motion     -> schema `decision`, decisionType=motion
  amendment  -> schema `decision`, decisionType=amendment
  resolution -> schema `decision`, decisionType=resolution
  Amendment.parentMotion -> Decision.amends (the register declares `amends` as
  "replaces the retired Amendment -> Motion relation")

DecisionSchema holds that mapping in one place. Reads by id re-establish the
type through the discriminator, because an id no longer proves what it points
at; writes stamp decisionType, which is `required` and defaults to
`meeting-outcome` — omitting it would silently mistype every motion.

Also closes a fail-open uncovered while migrating: transitionLifecycle() chose
MOTION_TRANSITIONS for any objectType that was not literally 'amendment', so an
unknown value got the motion table instead of a refusal. It is now a match with
a throwing default, evaluated before the register is touched.

SubmissionDeadlineListener subscribed to schemas ['motion','amendment'] — both
deleted — so the deadline gate fired on nothing at all. It now subscribes to
`decision` and narrows on the discriminator inside the handler.

* refactor(adr-005): spell the decision vocabulary inline, per the repo's own idiom

The DecisionSchema helper class introduced in the previous commit cost 29 PHPMD
findings against a baseline of ZERO: 21 StaticAccess (the project forbids static
access outright) and CouplingBetweenObjects 13 in three classes that sat at 12 —
any new class reference trips them. The class is gone; the tokens are spelled
where they are used, which is how OriController::DECISION_TYPE_MAP and the rest
of decidesk already spell schema slugs. Every site carries its ADR-005 rationale.

Two complexity findings were also mine and are fixed by construction rather than
by threshold:

- detectConflicts() reached CC 10 / NPath 264 because the discriminator check
  added a branch to a method that already carried the whole overlap comparison.
  The comparison moves to hasTextOverlap() + significantWords().
- AmendmentOrderService hit class complexity 51. The added guard collapses to one
  condition: a missing object and a decision of the wrong type are the same
  answer — this id is not an amendment — and an absent object has no
  discriminator, so `($amendment['decisionType'] ?? null) !== 'amendment'`
  covers both without the `$entity === null ||` arm.

Measured on this tree, all against origin/development's own numbers:
  phpcs   0 errors / 97 warnings / 97 files  (identical to baseline)
  phpmd   0 findings                          (identical to baseline)
  psalm   0 errors
  phpstan 0 errors
  phpunit 812 tests / 2771 assertions / 0 errors (baseline 806 / 2761 / 0)

* test(newman): seed decisions, not the schemas ADR-005 deleted

Three collections seeded their fixtures straight into
/apps/openregister/api/objects/decidesk/{motion,amendment} — schemas that no
longer exist. Every seed answered 404, so `motionId` / `amendmentA` /
`amendmentB` interpolated to the empty string and the downstream requests
measured nothing about the code under test. Measured on a live instance:
"Setup: motion created (201)" failed in decidesk-motion-amendment,
decidesk-voting-rules and decidesk alike.

Seeds and teardowns now address `decision` and carry `decisionType`, and the
amendment fixtures link through `amends` rather than the retired `parentMotion`.

board-portal is deliberately untouched: it targets routes retired by 2bb71af,
and that commit enumerates retargetings as well as deletions, so some of its 24
assertions may describe behaviour that still exists elsewhere. Deciding that
collection's fate is a separate question for a human.

* fix(voting): an unknown meetingId is a fail-closed 403, not a 500

Measured live while verifying the ADR-005 work: POST /api/voting-rounds with
"meetingId": "meeting-x" answered 500, and the trace showed it never reached the
subjectType validation it was supposed to fail on:

  MagicMapper::findInRegisterSchemaTable  "Object not found in magic table"
  -> ObjectService::find
  -> ParticipantResolver::resolveGovernanceBodyId
  -> ParticipantResolver::resolveMeetingParticipants
  -> VotingRoundGuard::hasRole / requireRoles / requireChairOrSecretary
  -> VotingController::open

Two contracts were being broken at once. resolveGovernanceBodyId() documents
"returns null when the meeting cannot be found", but OpenRegister's find() THROWS
for an unknown id rather than returning null — so its `=== null` branch was
unreachable and the documented answer was never delivered. VotingRoundGuard's
docblock promises "fail closed: any failure yields a 401/403", and instead an
auth guard emitted a 500.

Not-found is translated where it is documented, and nowhere else: the catch is
narrowed to DoesNotExistException, so every other failure still propagates.

This is NOT the ADR-005 fold — it is a second, independent cause of the same
symptom, and it is why the Newman assertion "Open: invalid subjectType rejected
(400)" reported 500 both before and after the schema migration.

The test double now models find() faithfully (it throws), which is what let this
surface at all; a double that returned null would have hidden every caller that
treats not-found as a return value.

* fix(adr-005): migrate the lifecycle VOCABULARY, not just the schema slug

#425 moved the PHP off the retired `motion`/`amendment` schemas onto
`decision`. It did not move the words. Thirteen files went on writing
`submitted | debating | adopted | rejected`, and all four are outside the
`Decision.lifecycle` enum — so every transition wrote a value OpenRegister
refuses. Measured: the identical payload with `deliberating` validates
where `debating` is rejected.

The mapping, per ADR-005:

  submitted -> proposed
  debating  -> deliberating
  voting    -> voting        (unchanged)
  adopted   -> lifecycle `decided` + outcome `adopted`
  rejected  -> lifecycle `decided` + outcome `rejected`
  withdrawn -> withdrawn     (unchanged)

`adopted`/`rejected` are values of `Decision.outcome`, an axis orthogonal
to `lifecycle` — the schema says so in as many words. They are therefore
gone from the transition tables entirely and arrive as a new `$outcome`
argument alongside `newState: 'decided'`. Keeping them as pseudo-states
would have rebuilt the conflation the fold exists to remove and made the
two-dimensional truth (decided AND rejected) inexpressible.

Two guards were broken by the stale vocabulary in OPPOSITE directions,
which is why neither announced itself:

- AmendmentOrderService::UNDECIDED_STATES could only ever match `voting`,
  so assertAmendmentsDecided() had become close to a no-op and FAILED
  OPEN — a motion could be voted while its amendments were in flight.
- DECIDED_STATES (`adopted|rejected`) could match nothing, so the
  ordering check FAILED CLOSED on every sibling.
  The two lists now partition the enum, asserted as such.

- MotionAmendmentService::hasTextOverlap() filtered on two impossible
  states, so amendment conflict detection matched nothing at all.
- RUNNING_MOTION_LIFECYCLES was a THIRD vocabulary ('under-discussion'
  appears nowhere else); the widget filtered the API on values no object
  can hold and was permanently empty.
- Three tab components each carried their own copy of a lifecycle->badge
  map in the retired vocabulary. The map now lives in one place.

Terminal completeness is enforced at the transition boundary, reusing
DecisionTransitionGuard rather than a second copy: entering
decided/enacted/archived without a valid outcome is refused, and
decisionDate is stamped once and never restamped. This does not fail
open — that shape is what #425 was repairing in the first place.

DecisionLifecycleVocabularyTest reads the enum out of the SHIPPED
register and holds the service constants against it. A hardcoded list in
a test would just be a fourteenth place for the vocabulary to drift, and
the reason the original defect was invisible is that every test used the
same retired words as the code it tested. It carries an autoloader
positive control: NC hijacks OCA\Decidesk\* to the DEPLOYED app once
base.php loads, so the test asserts the classes it reflects resolve to
this worktree.

Can-fail proof: restoring `voting => [adopted, rejected]` turns the new
test red naming 'adopted' in MOTION_TRANSITIONS on both the enum and the
outcome-axis assertions; dropping `withdrawn` from DECIDED_STATES turns
the partition assertion red. Both restored.

Measured on PHP 8.3: phpunit 828 tests / 3006 assertions / 0 failures
(was 823 with 5 errors + 5 failures against the stale fixtures); phpcs 0
errors / 97 warnings (baseline); phpstan 0.

* refactor(motion): extract the lifecycle state machine out of MotionService

Adding the ADR-005 outcome axis pushed MotionService past three PHPMD
limits that were clean on the parent commit — class complexity 57 (max
50), coupling 15 (max 13), and applyOutcome() cyclomatic complexity 10.
Measured both sides: `phpmd lib text phpmd.xml` is empty at HEAD~1 and
reported those three at HEAD, so they were new debt, not inherited.

The metric was reporting something real. A state machine, an
authorization gate (the co-signer minimum) and a persistence path had
accreted inside a class whose subject is "a motion" and which also owns
co-signatures, budget-impact notes, amendment application and
forwarding. They move to MotionLifecycleTransitioner whole, rather than
being trimmed to fit under a threshold.

MotionService::transitionLifecycle() stays as a thin delegate, so the
seam is unchanged: every caller (MotionController, VotingRoundCloser,
VotingRoundPreflight) and every existing test still addresses the state
machine through MotionService.

The two strict test containers now build the REAL transitioner rather
than a double. Both files exist to test logic that now lives inside it —
the co-signer gate, an unknown decisionType refused before the register
is touched, a decision of another type answering Not Found — so a double
would have them assert against a stand-in for their own subject. Both
containers deliberately throw on unregistered ids; that discipline is
kept and the new id is registered explicitly.

Can-fail re-proven after the move: restoring `voting => [adopted,
rejected]` in the new home turns DecisionLifecycleVocabularyTest red on
both the enum and the outcome-axis assertions, naming 'adopted' in
MOTION_TRANSITIONS. Restored.

Measured: phpmd 0 (both rulesets), phpcs 0 errors / 98 warnings, phpstan
0, psalm 0, phpunit 828 tests / 3006 assertions / 0 failures.

* fix(dashboard): the running-processes stage vocabulary existed nowhere but here

The widget filtered the API on `[submitted, under-discussion, voting]`.
That is a THIRD vocabulary — not the retired Motion states, not the
ADR-005 Decision enum. 'under-discussion' appears in no schema, no
service and no fixture; it was invented in the change that built the
widget and copied into the canonical spec. Since the array is passed
straight to `getMotions({ lifecycle: ... })`, the widget queried for
values no stored object can hold and was permanently empty.

The vitest suite could not catch it: its fixtures used the same invented
words as the code, so the two halves agreed with each other. That is not
a measurement. A stage-key assertion against the real enum is added, and
it is the only assertion in the file that could have failed.

openspec/specs/dashboard/spec.md carried the same three words in two
scenarios and is corrected — it is the canonical spec, and leaving it
would have made the fixed code contradict its own requirement.

`stages()` gains the @SPEC tag gate-16 asked for, pointing at the
canonical spec rather than the archived change that introduced the
defect.

Measured: gate-16 spec-coverage FAIL(1) -> PASS at gate package 566ac8c;
the full-repo failing-gate set is now name-for-name identical to
origin/development's (19 gates), with gate-25 down 27 -> 26.

---------

Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…nore that eats source (#437)

Six independent defects, each measured at gate package 651e5c5 with
NODE_PATH set (require.resolve('ajv') →
/home/rubenlinde/.../node_modules/ajv/dist/ajv.js; without it gates 22
and 53 fail for the wrong reason and prove nothing).

**MotionIntegrations rendered nothing** (gate-53, 1 → 0).
src/manifest.json page 15 names the component on /motions/:id/integrations
but nothing registered it, so resolution fell through to a blank page.
The component already existed; only the registration was missing. Its
three siblings were registered all along.

**Five dashboard rows were mouse-only** (gate-32, 5 → 0).
Each row navigates on @click with no role, no tabindex and no key
handler: not reachable by keyboard at all (WCAG 2.1.1). They now carry
role="button", tabindex="0" and enter/space handlers invoking the same
method, with .prevent so space activates instead of scrolling. Each also
gains a :focus-visible style — a row that can be tabbed to but not seen
is worse than one that cannot be reached (WCAG 2.4.7).

**A proxy field labelled only by placeholder** (gate-40, 1 → 0).
A placeholder is not an accessible name and vanishes on first keystroke,
so the field lost its only description exactly while being filled in
(WCAG 3.3.2, 4.1.2). Replaced with NcTextField, which carries a real
<label for> — the idiom the rest of this repo already uses — rather than
bolting aria-label onto a raw input.
gate-40's fleet-average FP rate is ~58%, so this was adjudicated on its
own evidence rather than dismissed: the finding is genuine.

**Nine unscoped <th>** (gate-43, 2 → 0). All are column headers in a
<thead><tr>, so scope="col" throughout — checked per table, not assumed.

**An indefinite pulse with no escape** (gate-45, 1 → 0).
The over-time clock pulses forever. Removing it under
prefers-reduced-motion loses no information: "over time" is also carried
by --color-error and by the visible "Over time" tag. Opacity is pinned
to 1 so an interrupted animation cannot settle at 0.4 and read as
disabled.

**.gitignore silently swallowed real source.**
`**/*Analysis*`, `**/*encoding*` and `**/update*Settings*` carried no
negations. This repo already lost this argument once — `**/*references*`
ate userPreferences.js because "Preferences" embeds "references". Four
plausible names were created and their status read back with the only
instruments that answer honestly (`git ls-files --others
--exclude-standard` + `git status --porcelain --ignored`; `git
check-ignore -q` is unusable, it exits 0 when a NEGATION matches):

  lib/Service/VotingAnalysisService.php  !! IGNORED
  src/utils/updateUserSettings.js        !! IGNORED
  lib/Service/encodingHelper.php         !! IGNORED
  src/utils/dataAnalysisChart.vue        !! IGNORED

All four are now ?? untracked-and-visible, while "lib/PR adds stuff"
stays !! ignored — the negations rescue source without un-ignoring the
scratch files the globs exist for.

Can-fail proof, one item reverted at a time, each gate re-run and
required to name that exact item:
  gate-43 → "unscoped=1/6" on MemberCsvImportDialog.vue
  gate-45 → AgendaItemTimer.vue motion-without-reduced-motion-fallback
  gate-32 → RecentDecisionsWidget.vue:40 rule=missing[role=,tabindex=]
  gate-53 → "MotionIntegrations … registered in neither"
  gate-40 → VotingRoundPanel.vue rule=input-without-label
All restored; all five back to PASS.

Measured: full-repo failing gates 19 → 14. gate-60 icon-vocabulary went
from SKIPPED(wiring) to PASS — it had never actually run here, because
vue-material-design-icons was absent; installed, it passes on its own
evidence. eslint on the touched files: 0 errors, 15 warnings (baseline
on development: 0 errors, 20). vitest 283 passed.

Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…, and two dead seams (#438)

Measured at gate package 365fa31 with NODE_PATH set (require.resolve('ajv')
→ .../node_modules/ajv/dist/ajv.js). Every run had a 0-byte .err and
verdicts were read from stdout, never the exit byte.

  gate-3  stub-scan                        1 -> 0
  gate-5  route-auth                      13 -> 0
  gate-9  semantic-auth                    1 -> 0
  gate-18 notification-dialect       WARNING -> PASS
  gate-49 controller-exception-translation 1 -> 0
  gate-57 orphaned-write-capability         2 -> 0
  full-repo failing gates                  19 -> 14

**Thirteen admin endpoints declared nothing** (gate-5). Audit-log query,
governance reports, multilingual reconciliation and regulator export all
call requireAdmin() in the body but carried no auth attribute at all.
Nextcloud treats "no attribute" as admin-required, so they worked — but
"deliberately admin-only" and "someone forgot" look identical in the
source, and AuditLogController's own class docblock had already noticed,
warning that the absence "is silently bypassed in some test setups".
They now carry #[AuthorizedAdminSetting(AdminSettings::class)], which is
this repo's existing, live idiom for an admin-only REST endpoint
(MemberImportController, SettingsController) — not a new invention. It
also lets an admin DELEGATE these to a group rather than hardcoding "is a
server admin". Body guards are kept as defence in depth.

`#[NoCSRFRequired]` would also have satisfied gate-5. It was not used:
it says nothing about admin and would have weakened CSRF protection to
quiet a checker.

**DecisionController::publish said the opposite of what it did**
(gate-9). It carried #[NoAdminRequired] — "any authenticated user" —
while the next statements refuse non-administrators and its own docblock
said "Requires Nextcloud admin role". The attribute is what a reader, an
auditor and the middleware all see first.

**getStats 500'd on an unknown participant** (gate-49). OpenRegister's
find() THROWS DoesNotExistException; it does not return null. The
`!== null` branch was unreachable for the case it was written for, and
the exception escaped as a 500 on an ordinary "no such participant"
request — the same defect class as ParticipantResolver, fixed in #425.
An absent participant now answers 403, deliberately fail-CLOSED: a 404
would let any authenticated user enumerate participant UUIDs. The
service call translates DoesNotExist to 404 and InvalidArgument to 400.
The catches are narrowed, and a third test asserts a RuntimeException
still PROPAGATES — narrowing is only a fix if the narrowing is real; a
blanket catch would turn an OpenRegister outage into a tidy 403 and hide
it from monitoring.

**Nobody was ever told a decision went public** (gate-57 + gate-18).
DecisionNotificationService::notifyOnPublish had zero callers, zero
tests and no DI registration, and `isPublished` was the one Decision
event with NO declarative rule — so the notification simply never
happened. ADR-031 keeps notifications declarative and gate-18 flagged
the class by name, so the rule lands in the register as
`decisionPublished` (mirroring its six siblings) and the dead imperative
class is removed rather than wired up.

**createPreference was a second name for one write** (gate-57). A
self-described "(alias)" delegating to updatePreference, which is itself
an upsert. No callers in lib/, src/ or tests/ — nothing wrote through
it, so nothing can be orphaned by its removal.

**A retired job that could never retire** (gate-3). OverdueActionItemsJob
was emptied to a no-op with a comment saying it was kept "so the
registered oc_jobs row reaps cleanly" — while still declared in
appinfo/info.xml, so Nextcloud re-registered it on every app update and
cron ran an empty job every 24 hours, forever. Both halves are removed.
No repair step is needed: read from the running server's own
lib/private/BackgroundJob/JobList.php::buildJob(), an unresolvable job
class is logged and then dropped via removeById() — "Remove job from
disabled app or old version of an app". Measured, not assumed; a repair
step was written first and deleted once the source said it was redundant.

Can-fail proof — one item reverted at a time, each gate naming it:
  gate-5  AuditLogController.php:126 method=verify rule=missing-auth-attribute
  gate-9  DecisionController.php:225 method=publish rule=no-admin-required-annotation-with-admin-body
  gate-57 NotificationPreferenceService.php:194 method=createPreference
  gate-49 removing the catch makes the new test error with the escaping
          DoesNotExistException — the live 500, reproduced
All restored; all back to PASS.

Measured: phpunit 826 tests / 3009 assertions / 0 failures (823 + 3 new,
minus the 5 belonging to the deleted job); phpcs 0 errors / 98 warnings;
phpstan 0; psalm 0; phpmd 0 on both rulesets.

Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
Measured at gate package 365fa31, NODE_PATH set, 0-byte .err, 0 gates
SKIPPED(wiring): gate-46 spec-anchor-existence 18 -> 4, and the last 4
live in lib/BackgroundJob/OverdueActionItemsJob.php, which #438 deletes —
so the pair takes this gate to 0.

Three distinct causes, none of them "the tag was missing":

**A trailing full stop swallowed into the path — 6 tags.** `@spec
openspec/specs/decision-management/spec.md.` resolves to a filename
ending in a dot. All three targets exist; only the sentence punctuation
was wrong. This is the cheapest possible defect and it had made three
real, current, canonical specs unreachable.

**Tags pointing INTO archived changes — 8 tags.** `@spec` targets the
canonical `openspec/specs/`, never a change directory: a change is
archived once it lands, and the tag then dangles. Each was retargeted to
the canonical spec that survived its change, verified to exist on disk:

  publish-decisions-via-opencatalogi  -> specs/public-publication
  meeting-transcription-ai-minutes    -> specs/meeting-transcription
  citizen-participation               -> specs/citizen-participation
  authorizedadminsetting-fix-fleet x2 -> specs/admin-settings
  board-meeting-resolutions           -> specs/decision-management
  retrofit-2026-05-26-preferences-api -> specs/user-settings (x2)
  pluggable-integration-registry      -> specs/nextcloud-integration

**A path that was never valid and a placeholder anchor — 4 tags.**
MeetingService carried `openspec/changes/spec/tasks.md#task-1` three
times — a directory literally named "spec" that has never existed, so
this tag could not have resolved on any commit. Retargeted to
specs/meeting-management. ActionItemController and actionItemApi.js
carried `#task-2.x`, an anchor with a literal "x" placeholder in it;
both now point at specs/action-item-board-via-deck-leaf, whose
requirements they implement.

No spec file was created to make an anchor resolve. Every target was
checked to exist before the tag was pointed at it, and where a canonical
spec genuinely did not exist the tag was moved to the one that describes
the code rather than to an invented file.

Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…am that was meant to prevent it never fired (#441)

OpenRegister's ObjectService::find() THROWS DoesNotExistException for an
unknown id — it does not return null. Three places were written against
the opposite contract, each with an `if ($entity === null) return 404;`
branch that could not run for the case it was written for:

  MinutesCorrectionController::addCorrection      -> 500 "Internal server error"
  MinutesCorrectionController::resolveCorrection  -> 500 "Internal server error"
  TranscriptionController::retentionConfig        -> 500 + stack trace (uncaught)

The first two were not even visibly broken: DoesNotExistException extends
\Exception, so the method's own `catch (Exception)` swallowed it and
returned a tidy server error. The third had no try/catch at all and the
exception escaped the controller.

**The seam that should have caught all of this had the same defect.**
TranscriptRepository is where the transcription surface converts "absent
object" into the app's own MissingObjectException — the exception every
TranscriptionController action already renders as 404. Both of its
fetchers were written as `if ($entity === null) throw …`, so the
conversion NEVER happened and every `catch (MissingObjectException) ->
404` on that surface was unreachable for the case it exists to serve.
fetchMeeting() and fetchTranscript() now share one fetchObject() that
actually converts.

Only DoesNotExistException is converted, and the null branch is kept
alongside the catch: find() is typed `?ObjectEntity`, so null stays
reachable in principle, and both answers mean the same thing to the
caller. Any other failure still propagates — turning an OpenRegister
outage into "not found" would tell the caller, and monitoring, that data
is absent when the data layer is down.

retentionConfig's read-modify-write moved into the repository. Two
reasons, the second being the one that mattered: a controller doing plain
object CRUD is what ADR-022 exists to stop, AND the controller's private
copy of the lookup is precisely where the defect lived. TranscriptionController
no longer depends on ObjectService at all (coupling 13 -> 11, back under
the PHPMD threshold it had crossed).

Can-fail proof — all three reverted at once, and each reproduced its own
live failure mode: the two Minutes tests failed "500 is not identical to
404", and the Transcription test ERRORED with the escaping exception,
which is the stack-trace 500 exactly. Restored; all four green.

The assertions name the HTTP STATUS the caller is owed, not "a
JSONResponse came back" — a container-level assertion passes just as
happily on the 500, which is how this survived. A fourth test pins that a
non-DoesNotExist failure is STILL a 500, so the narrow catch cannot
quietly widen into a catch-all.

Measured: phpunit 832 tests / 3010 assertions / 0 failures; phpcs 0
errors / 98 warnings; phpstan 0; psalm 0; phpmd 0 on both rulesets.

Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…gs menu (#439)

* fix(e2e,manifest): a wait that never settles, and a Settings > Settings menu

Measured at gate package 365fa31, NODE_PATH set, 0-byte .err, 0 gates
SKIPPED(wiring).

  gate-58 e2e-networkidle   1 -> 0
  gate-63 settings-surface  2 -> 1

**A wait that could only ever time out** (gate-58). The docs-screenshot
helper navigated and then called
`waitForLoadState('networkidle').catch(() => {})`. networkidle never
settles on Nextcloud — long-polling endpoints and the notification stream
keep a request open indefinitely (ADR-074 rule 4) — and the line's own
comment admitted it: "idle never fires on some pages". So every
navigation burned the full timeout and then swallowed the failure. It
bought a delay, not a guarantee, and the screenshot's actual
precondition — content painted — was never asserted, only waited out.

Replaced with the settle this repo already uses and trusts:
`waitUntil: 'domcontentloaded'` on the goto, then the existing
`waitForContentReady()` from tests/e2e/visual/_visual-helpers, which
asserts the header and content root are visible and polls spinners and
"Loading …" placeholders away. A positive signal about the page rather
than the absence of a signal about the network. Reused rather than
reinvented so the two capture suites cannot drift.

**The gear foldout rendered Settings > Settings** (gate-63 D4). The
`section: "settings"` entry was labelled "Settings" while the foldout
button is already called Settings (ADR-079 D4). Renamed to
"App configuration", which names the surface rather than repeating its
parent.

Can-fail proof: restoring the networkidle line turns gate-58 red naming
`tests/e2e/docs-screenshots.spec.ts:112`; restoring the label turns
gate-63 back to 2 findings, the second naming the D4 collision verbatim.
Both restored.

ADR-079 D1 is deliberately NOT closed here — see the PR body. Closing it
means migrating the register-mapping admin UI into the NC settings
framework, which is real work, not a rename.

* revert(manifest): defer the ADR-079 D4 rename until D1 can land with it

The D4 rename ("Settings" -> "App configuration" on the settings-foldout
entry) is correct and its can-fail proof held. It cannot land ALONE.

CI runs the gates DIFF-SCOPED. On development, src/manifest.json is
untouched, so gate-63 never enters scope and the job passes. This PR
touched src/manifest.json, which pulled gate-63 into scope — and with it
the ADR-079 D1 finding that this PR deliberately did NOT close. The
result was a Hydra Gates failure on a job that passes on development:
a NEW failing job name, which is exactly what the merge rule forbids,
caused by a change that is itself an improvement.

D1 is not a rename. The in-app /settings page carries instance
administration (a version widget and a substantial register-mapping
widget configuring OpenRegister schema mappings for every Decidesk object
type), and lib/Settings/AdminSettings.php does NOT render register
mapping — it provides version, publicationConfig and transcript-retention
defaults only. Deleting the in-app page today would drop that admin UI
outright, and renaming it would dodge D1 rather than satisfy it, because
register mapping is not the "operational domain data" the D1 exception
covers.

So D4 travels with D1, in a PR that ports the register-mapping surface
into the NC settings framework and verifies it in a browser. This PR
keeps the gate-58 fix, which touches no manifest and is independently
correct.

---------

Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…8 assertions) (#420)

* fix(newman): two Meeting seeds violated the shipped Meeting schema

Both collections open by seeding a Meeting through OpenRegister's object
API. Both payloads were rejected with 400, the captured id interpolated to
`undefined`, and every downstream request then missed its route and fell
through to the SPA catch-all (a GET-only route), which answers 405. That is
why 18 assertions failed on two one-line fixture bugs.

decidesk-meeting-agenda: `meetingMode: "physical"` is not a member of the
Meeting.meetingMode enum `["in-person", "digital", "hybrid"]`.

decidesk-minutes: the seed omitted `meetingMode` entirely. It is listed in
Meeting.required and, unlike Decision.decisionType (default
"meeting-outcome"), carries no default — so nothing filled it in. The
neighbouring decidesk-lifecycle seed is likewise missing a required
property and still returns 201 precisely because that property has a
default; required alone does not predict a 400.

Verified against lib/Settings/decidesk_register.json. No assertion was
relaxed, skipped or removed.

* fix(newman): retire the board-portal collection, and three seeds that pointed at deleted schemas

Four independent defects, each verified against the shipped register and
appinfo/routes.php rather than against the test's own expectations.

**board-portal drove 40 requests at routes that do not exist.** Commit
2bb71af ("C3 retire-board-portal") deleted the board-CRUD controllers
outright: `boards`, `resolutions`, `board-member` and `board-meeting`
appear ZERO times in appinfo/routes.php. Every write 404'd to an HTML
error page, `pm.response.json()` threw, and the file cascaded — 24 of the
repo's 76 Newman failures, the single largest block.

That commit enumerates DELETIONS and RETARGETINGS separately, so the
collection was adjudicated per request rather than deleted wholesale:

  32 requests exercised the DELETED parallel CRUD (board = governance-body,
     board-meeting = meeting, resolution = decision, board-member =
     Person+Membership). They assert on endpoints the app deliberately no
     longer has; the equivalent behaviour is covered against the universal
     entities by decidesk.postman_collection.json and its siblings.

   8 requests exercised RETARGETED, still-live routes — regulator export
     and multilingual reconciliation, both present in routes.php. All 8
     are PRESERVED verbatim in the new decidesk-governance-reporting
     collection: same methods, same bodies, same assertions. The only
     change is that `boardId` is now seeded from a real governance-body
     instead of a retired board. RegulatorExportController still names the
     parameter `boardId`, so the wire contract is unchanged.

Two of those preserved assertions were wrong about the app and are
corrected against the controllers: status() returns {summary, results},
not the `queue` property the old test demanded; process() returns 200 or
422 depending on whether there was anything to process, and the response
SHAPE is now asserted either way so a 422 must still be a well-formed
answer rather than an HTML page.

**Three voting-rules seeds POSTed to a schema ADR-005 deleted.** They
targeted /objects/decidesk/motion — `motion` is absent from the register
(only `decision` survives) — so all three 404'd and cascaded into 22
failures. Fixed to `decision`; the bodies already carried
`decisionType: motion` from #425.

⚠️ The reason this was not caught earlier: the `raw` URL had ALREADY been
corrected to `.../decidesk/decision` while `path[]` still said `motion`.
Newman resolves from path[], so the fix looked done in review and every
request still went to the deleted schema. A repo-wide scan now confirms
0 raw/path[] mismatches across all collections.

**Two decision seeds omitted the required `decisionType`.** Found by
validating every object seed against the shipped register rather than by
reading failures: 34 seeds checked, and these two were rejected before
any assertion could run. The decidesk seed also set outcome/decisionDate
with no lifecycle, so it now states `lifecycle: decided` explicitly —
terminal fields without a terminal state is the shape #436 and #425 spent
two PRs separating.

**Left red deliberately — 2 board-proxy seeds.** They target the deleted
`board-proxy` schema, so they 404. There is no honest replacement:
ProxyVoteService::SCHEMA is `vote`, but the `vote` schema declares NONE
of the fields the proxy row uses (meetingKoppeling, grantorKoppeling,
holderKoppeling, scope, proxyStatus, registeredAt) and REQUIRES two the
row never sets (value, castAt) — while proxyAuthorization's own
description says it is "a sibling … never a replacement". Retargeting to
either would be a guess the register contradicts, so the seeds are left
as they are and the underlying inconsistency is reported rather than
papered over.

**Left red deliberately — 7 authz assertions** (4 admin-settings,
3 process-config) expecting 403 for a non-admin and receiving 200. I
could not establish the cause and will not change an assertion over a
live authz surface on a guess. Read from the running server's own
lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php:
`AuthorizedAdminSetting` is strictly enforced (admin, else delegated
group, else NotAdminException), and an attribute-less method is refused
too — so the endpoints ARE protected and the routes/attributes are wired
correctly. A cookie-jar hypothesis (admin session outranking basic auth)
was tested directly against a live instance and DISPROVEN: bogus basic
credentials returned 401 even when replaying an admin session cookie.
Changing these to expect 200 would convert a false red into a false green
over real authorization.

---------

Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
… updates (#369)

* build(deps): bump the npm_and_yarn group across 2 directories with 16 updates

Bumps the npm_and_yarn group with 2 updates in the / directory: [@cyclonedx/cyclonedx-npm](https://github.com/CycloneDX/cyclonedx-node-npm) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).
Bumps the npm_and_yarn group with 11 updates in the /docs directory:

| Package | From | To |
| --- | --- | --- |
| [dompurify](https://github.com/cure53/DOMPurify) | `3.3.1` | `3.4.12` |
| [fast-uri](https://github.com/fastify/fast-uri) | `3.1.0` | `3.1.5` |
| [follow-redirects](https://github.com/follow-redirects/follow-redirects) | `1.15.11` | `1.16.0` |
| [postcss](https://github.com/postcss/postcss) | `8.5.6` | `8.5.25` |
| [qs](https://github.com/ljharb/qs) | `6.14.2` | `6.15.3` |
| [@babel/plugin-transform-modules-systemjs](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-modules-systemjs) | `7.29.0` | `7.29.8` |
| [mermaid](https://github.com/mermaid-js/mermaid) | `11.12.3` | `11.16.0` |
| [shell-quote](https://github.com/ljharb/shell-quote) | `1.8.3` | `1.10.0` |
| [svgo](https://github.com/svg/svgo) | `3.3.2` | `3.3.4` |
| [ws](https://github.com/websockets/ws) | `7.5.10` | `7.5.13` |
| [websocket-driver](https://github.com/faye/websocket-driver-node) | `0.7.4` | `0.7.5` |



Updates `@cyclonedx/cyclonedx-npm` from 4.2.1 to 5.0.0
- [Release notes](https://github.com/CycloneDX/cyclonedx-node-npm/releases)
- [Changelog](https://github.com/CycloneDX/cyclonedx-node-npm/blob/main/HISTORY.md)
- [Commits](CycloneDX/cyclonedx-node-npm@v4.2.1...v5.0.0)

Updates `vitest` from 1.6.1 to 3.2.6
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest)

Updates `esbuild` from 0.21.5 to 0.28.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2024.md)
- [Commits](evanw/esbuild@v0.21.5...v0.28.1)

Updates `vite` from 5.4.21 to 7.3.6
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.6/packages/vite)

Updates `dompurify` from 3.3.1 to 3.4.12
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](cure53/DOMPurify@3.3.1...3.4.12)

Updates `fast-uri` from 3.1.0 to 3.1.5
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](fastify/fast-uri@v3.1.0...v3.1.5)

Updates `follow-redirects` from 1.15.11 to 1.16.0
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](follow-redirects/follow-redirects@v1.15.11...v1.16.0)

Updates `postcss` from 8.5.6 to 8.5.25
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](postcss/postcss@8.5.6...8.5.25)

Updates `qs` from 6.14.2 to 6.15.3
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](ljharb/qs@v6.14.2...v6.15.3)

Updates `@babel/plugin-transform-modules-systemjs` from 7.29.0 to 7.29.8
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.29.8/packages/babel-plugin-transform-modules-systemjs)

Updates `body-parser` from 1.20.4 to 1.20.6
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](expressjs/body-parser@1.20.4...1.20.6)

Updates `dompurify` from 3.3.1 to 3.4.12
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](cure53/DOMPurify@3.3.1...3.4.12)

Updates `fast-uri` from 3.1.0 to 3.1.5
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](fastify/fast-uri@v3.1.0...v3.1.5)

Updates `follow-redirects` from 1.15.11 to 1.16.0
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](follow-redirects/follow-redirects@v1.15.11...v1.16.0)

Updates `mermaid` from 11.12.3 to 11.16.0
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Commits](https://github.com/mermaid-js/mermaid/compare/mermaid@11.12.3...mermaid@11.16.0)

Updates `postcss` from 8.5.6 to 8.5.25
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](postcss/postcss@8.5.6...8.5.25)

Updates `qs` from 6.14.2 to 6.15.3
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](ljharb/qs@v6.14.2...v6.15.3)

Updates `shell-quote` from 1.8.3 to 1.10.0
- [Changelog](https://github.com/ljharb/shell-quote/blob/main/CHANGELOG.md)
- [Commits](ljharb/shell-quote@v1.8.3...v1.10.0)

Updates `svgo` from 3.3.2 to 3.3.4
- [Commits](https://github.com/svg/svgo/commits)

Updates `ws` from 7.5.10 to 7.5.13
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](websockets/ws@7.5.10...7.5.13)

Updates `websocket-driver` from 0.7.4 to 0.7.5
- [Changelog](https://github.com/faye/websocket-driver-node/blob/main/CHANGELOG.md)
- [Commits](faye/websocket-driver-node@0.7.4...0.7.5)

---
updated-dependencies:
- dependency-name: "@cyclonedx/cyclonedx-npm"
  dependency-version: 5.0.0
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: vitest
  dependency-version: 3.2.6
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-version: 7.3.6
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: dompurify
  dependency-version: 3.4.12
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: fast-uri
  dependency-version: 3.1.5
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: follow-redirects
  dependency-version: 1.16.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: postcss
  dependency-version: 8.5.25
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: qs
  dependency-version: 6.15.3
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: "@babel/plugin-transform-modules-systemjs"
  dependency-version: 7.29.8
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: body-parser
  dependency-version: 1.20.6
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: dompurify
  dependency-version: 3.4.12
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: fast-uri
  dependency-version: 3.1.5
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: follow-redirects
  dependency-version: 1.16.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: mermaid
  dependency-version: 11.16.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: postcss
  dependency-version: 8.5.25
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: qs
  dependency-version: 6.15.3
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: shell-quote
  dependency-version: 1.10.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: svgo
  dependency-version: 3.3.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: ws
  dependency-version: 7.5.13
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: websocket-driver
  dependency-version: 0.7.5
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): exercise the vitest 1 -> 3 bump and add the coverage peer it needs

Dependabot's bump crosses two majors on vitest (^1.6.1 -> ^3.2.6) and one
on @cyclonedx/cyclonedx-npm (^4.2.1 -> ^5.0.0). A bump no build exercises
is not verified, so this branch was updated onto development and every
consumer of those tools was actually run:

  npm ci                    1178 packages, exit 0
  npm run build             webpack 5.109.2, exit 0 (3 pre-existing warnings)
  npm run test:unit         18 files, 283/283 passed on vitest 3.2.6
  npx eslint src            0 errors, 194 warnings
  npm run test:coverage     see below
  coverage ratchet          11.67% vs baseline 1.62% — passes

Versions are read back from node_modules on disk, not from the lockfile:
npm 11 prunes lockfile peers, so a lockfile entry is not evidence that a
package is installed.

**Added @vitest/coverage-v8.** `npm run test:coverage` and therefore
`test:coverage-ratchet` could not run at all — "Cannot find dependency
'@vitest/coverage-v8'". MEASURED on both sides: this is NOT a regression
from the bump, it fails identically on development with vitest 1.6.1,
where the package is equally undeclared. It is added here because this
change moves vitest across two majors, and shipping that without the
coverage plugin the ratchet depends on leaves a CI gate resting on a
command that cannot execute locally. With it, coverage runs and the
ratchet reports a real number.

It is pinned EXACT to 3.2.6 rather than ^3.2.6 deliberately: the caret
resolved to 3.2.7, whose peer requirement is `vitest 3.2.7` exactly,
which made the lockfile's vitest 3.2.6 report `invalid` in `npm ls`. The
exact pin keeps the two in lockstep.

**cyclonedx v5 was checked and is not newly broken.** `npx cyclonedx-npm`
exits with "npm-ls exited with errors: 1", because `npm ls` itself exits
1 on this tree. That is pre-existing: development reports 9 tree problems
(missing babel-loader, ts-loader, webpack-dev-server,
eslint-import-resolver-typescript peers — the npm 11 pruning — plus
several `invalid` entries) and this branch reports 8, i.e. one FEWER
after the coverage-v8 pin. The bump neither introduces nor worsens it.

@conduction/nextcloud-vue is untouched by this bump and stays pinned to
the exact 2.1.0-vue3.16 on the vue3 line. Checked its dist-tags first:
`latest` is 1.0.0-beta.3, an OLD VUE 2 build, so a bare `npm i` would
install Vue 2 — the exact pin is what keeps this repo off that path.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
* chore(deps): @conduction/nextcloud-vue 2.2.0-vue3.3 -> 2.2.0-vue3.6

Moves decidesk to the head of the `vue3` dist-tag. Verified rather than
assumed, and the lockfile diff was separated from npm's own churn with a
control run first.

**Control run — the diff is entirely the bump.** Regenerating the
lockfile with package.json COMPLETELY UNCHANGED produced ZERO changed
lines here. openregister (#2409) saw 53 packages flip dev -> production
on that same control, but decidesk's lockfile was already rewritten by
npm 11 during #369, so there was no npm 10 -> 11 rewrite left to absorb.
With that established, the post-bump diff is 8 lines — version, resolved
and integrity for this one package, in both places — and npm reported
"changed 1 package". No collateral, no dev/optional flips.

**Positive controls (the check can fail).**

  npm view @conduction/nextcloud-vue@2.2.0-vue3.6  -> 2.2.0-vue3.6
  npm view @conduction/nextcloud-vue@2.2.0-vue3.3  -> 2.2.0-vue3.3
  npm view @conduction/nextcloud-vue@3.0.0-vue3.4  -> E404
  npm view @conduction/nextcloud-vue@3.0.0-vue3.1  -> E404

The whole 3.x line is depublished. That is worth recording because it is
the mechanism behind the stale-checkout report that prompted this work: a
tree pinning 3.0.0-vue3.4 cannot fetch it, so whatever was already in
node_modules survives — in that checkout, 1.0.0-beta.220, a Vue 2 build.
`origin/development` was never affected; it pinned 2.2.0-vue3.3 and had
2.2.0-vue3.3 installed.

**Read off disk after a clean npm ci, not out of the lockfile:**

  version        2.2.0-vue3.6
  copies in tree 1
  vue peer       ^3.5.0   (installed vue 3.5.40)
  3.x / beta     none anywhere under node_modules/@conduction

**Exercised.** `npm run build` exits 0 — webpack 5.109.2 compiled with 3
warnings, the same count as before the bump. All 26 symbols decidesk
imports from the package are among the 499 it exports, so no import
breaks. vitest is 283/283 before and after.

⚠️ Those 283 tests are NOT evidence for this bump: zero of the 18 vitest
suites import @conduction/nextcloud-vue. What genuinely exercises it is
the webpack build (50 source files import it) and the e2e job, which
renders the real components in a browser. Recorded so the passing unit
count is not mistaken for UI verification.

**gate-55 does not move: 28 -> 28, findings byte-identical.** Checked
because CnWidgetGrid/widgetIcons.js — the registry those findings are
measured against — ships inside the package being bumped, and three
patches could have moved it. It did not: DASHBOARD_ICONS is 56 keys in
both 2.2.0-vue3.3 and 2.2.0-vue3.6, DEFAULT_ICON is still ViewDashboard,
and every flagged icon is still absent. Run the way the runner invokes
it (`<logfile> <files...>`, 24 manifests in scope, verdict read from the
LOG — the helper exits 0 either way).

The first build attempt was SIGKILL'd (137). That was not the bump:
dmesg recorded `Out of memory: Killed process (webpack)` at 9.9 GB RSS
while two unrelated webpack processes held 10.3 GB. Rebuilt once memory
freed; exit 0.

* chore(deps): advance the pin to 2.2.0-vue3.7

2.2.0-vue3.7 is the new vue3 dist-tag head and supersedes vue3.6.

Lockfile control (pin unchanged) is a 0-line diff, so the +5/-5 here is
entirely the version move.

unit 283 passed / 18 files -> 283 passed / 18 files
test:l10n exit 0, output identical; check:manifest exit 0
gate-55 detail-page-discipline 28 -> 28, findings byte-identical
build 3 warnings -> 3 warnings
bundle 151,328,450 -> 151,403,299 bytes

* chore(deps): advance the pin to 2.2.0-vue3.9

The vue3 dist-tag moved 3.7 -> 3.8 -> 3.9 while this was in flight.

unit 283 passed / 18 files (unchanged)
test:l10n exit 0; check:manifest exit 0
gate-55 detail-page-discipline 28 at vue3.6, 28 at vue3.9, byte-identical
build 3 warnings -> 3 warnings
bundle 151,328,450 (vue3.6) -> 151,449,798 (vue3.9)

---------

Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…cts (#447)

* fix(newman): decidesk#443 was a cookie jar, and it hid five real defects

All 13 Newman collections now run 310 assertions with 0 failures, measured on
a disposable NC 32.0.12 + openregister instance that first reproduced CI's 22
failures exactly (same collections, same counts).

decidesk#443 — NOT an authorization hole
  The four "non-admin gets 200 on an AuthorizedAdminSetting endpoint"
  assertions were true observations of a false premise. Newman keeps ONE cookie
  jar per host; Nextcloud resolves a logged-in session from that jar BEFORE it
  reads the Basic auth header, so a request carrying both runs as whoever
  authenticated last. Measured, not inferred: an inserted GET /ocs/v2.php/cloud/user
  on the same request chain answered id="admin", groups=["admin"]. Clearing the
  jar first turns the same request into 403 and the identity read into
  "decidesk-admset-nonadmin". The attributes were always enforced.

  Fixed by a collection-level jar clear in the four collections that assert on
  WHO is calling, plus an "Identity guard" request in each that asserts the
  probe really is the non-admin. Removing the clear turns the guard red first,
  so this cannot silently come back.

  The same jar also disarmed the IDOR guard decidesk-security-flow-e2e exists to
  prove: "register on behalf of an unrelated grantor" and "revoke by an
  unrelated participant" were running as the GRANTOR. They now genuinely 403.

Five product defects the false green was sitting on
  1. board-proxy schema never existed. ProxyVoteService wrote proxy rows into
     the `vote` schema, whose required value/castAt are not part of a
     delegation, so EVERY POST /api/proxies returned 422 "Failed to register
     proxy." Added the BoardProxy schema the service and both collections were
     already written against.
  2. governance-report schema never existed. persistReport() swallowed
     "Schema not found", so generate() returned 201 with no id and
     /api/governance-reports/{id}/export could never be addressed. Added
     GovernanceReport.
  3. The per-holder proxy cap never counted anything. ObjectService reads its
     register/schema context from filters.register / filters.schema; a TOP-LEVEL
     'register'/'schema' key is silently ignored and findAll() returns []
     without throwing. forMeeting() used the top-level shape, so a third proxy
     was accepted at a cap of 2. Live: now 422 with the maximum-reached message,
     and a different holder still registers.
  4. Anonymous ORI show returned 500, not 404. OpenRegister's published-predicate
     RBAC hides a future-dated payload by making find() THROW, so the not-live
     404 branch was unreachable and the blanket Throwable catch produced a 500 —
     itself a disclosure, since it separates "exists but hidden" from "unknown".
  5. Meeting close could never complete. openedAt/closedAt/meetingCost were
     declared readOnly, which OpenRegister enforces as immutable-after-create
     for every writer including MeetingService, so opened -> closed failed with
     "Cannot modify readOnly property: closedAt" (422). A meeting is created
     before it is opened, so a readOnly stamp can never be written.

Seed data: 8 objects were silently skipped on every install
  Five seed slugs exceeded 36 characters and were referenced by other objects;
  the relation column is varchar(36), so the write truncated and ImportHandler
  skipped the object. Shortened the five slugs and their references (and the
  openspec design/tasks tables that quote them). Locally: 8 skipped -> 0.

Test-side changes
  - decidesk-motion-amendment now seeds a real governance body + chair
    participant + meeting. VotingController::open() authorizes before it
    validates (correct order), so the placeholder meetingId 'meeting-x' meant
    the subjectType contract was never reached — the test only ever saw the 403.
  - ProxyVoteServiceTest's ObjectService mock treated every filter key as a row
    field, so a caller using the broken top-level shape still got rows: the mock
    could not tell a working call from one that returns [] in production. It now
    models the real contract and returns [] without register/schema context.
    Reverting forMeeting() to the old shape turns 3 tests red.
  - RegisterJsonTest schema-count ratchet 37 -> 39 with the reason.

Both directions proven per fix, one mutation at a time: swapping
AuthorizedAdminSetting for NoAdminRequired on MemberImportController::groups and
on ProcessTemplateController::index each turned exactly one assertion red;
removing the security-flow jar clear turned the identity guard red; reverting
forMeeting() turned the unit suite red. (Note: opcache.revalidate_freq=60 made
one mutation run look green — the probe instance now runs revalidate_freq=0.)

Left red and not touched here: 17 Playwright specs (95 pass) and the systemic
findAll config shape — ~50 call sites across lib/ still pass register/schema at
the top level and therefore read nothing. Filed separately; only the call site
covered by a red assertion is fixed here.

* fix(gates): close gate-54 and the coverage ratchet this branch opened

Two jobs went red on #447 that the first commit caused, both real:

quality / PHPUnit — coverage ratchet
  The new `catch (DoesNotExistException)` branch in OriController::show() added
  4 statements with no test, so coverage fell against the merge base
  (8866/15168 -> 8868/15172). Added the missing unit test: find() throwing
  DoesNotExistException must produce 404, not 500. It is the case the existing
  testShowFutureDatedPayloadIs404 never covered — that one hands the controller
  a row and exercises the not-live branch, while on a real instance the
  anonymous caller never gets the row at all. Swapping the catch to
  \LogicException turns the new test red with "500 is identical to 404".

quality / Hydra Gates — gate-54 relation-dialect, 4 findings
  Renaming the over-36-character seed slugs pulled two register fragments into
  the gate's diff scope and exposed four properties that declare
  `format: uuid` + `$ref` at a schema that does not exist in this register:
  Fractie and SchriftelijkeVraag are owned by the unlanded
  fractievoorzitter-fractie-koppeling change, and ArchivalDossier by
  records-management-archiving.

  The pair is unsatisfiable while the target is absent — keeping the $ref is a
  dangling target, dropping it while keeping format:uuid is a relation-shaped
  property with no $ref. Until the owning change lands these are plain string
  placeholders (the descriptions already say they are seeded as nil-UUID
  placeholders), so they are now declared as plain strings, each carrying the
  instruction to restore format:uuid + $ref in the same commit that introduces
  the schema. No waiver, no baseline: gate-54 goes from 4 findings to 0 in the
  files this branch touches.

  (One finding remains repo-wide: ConsultationRequest.achterbanraadpleging in
  47-works-council-consultation.json, a file this branch does not touch and
  which the gate's ADR-020 diff scoping therefore excludes.)

Re-verified after the register edits: all 13 Newman collections, 310 assertions,
0 failures; unit suite 831 tests green.

---------

Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…alendar (#450)

E2E: development baseline 17 failed / 95 passed / 59 skipped -> this branch 12 failed / 101 passed / 59 skipped. Skip count identical, so nothing was skipped away; 5 previously-failing specs now pass and one new spec was added (tie-break split into default vs revote). PHPUnit 8.3+8.4, phpstan/psalm/phpcs/phpmd, Newman and Hydra Gates all pass. Remaining 12 E2E failures are pre-existing and tracked.
#451)

Full-scope Hydra Gates on post-merge development confirms both findings are real: gate-51 FAIL 1 and gate-60 FAIL 1 (run 31459783989, gate package b8c7eade). Local full-scope run with the same package reproduces CI gate-for-gate and count-for-count, and shows both flipping to PASS with this change and no other gate moving.

E2E on this PR: 12 failed / 101 passed / 59 skipped — byte-identical to post-merge development, and the failing spec set is the same 12 specs. No regression. All 27 jobs reported; the only failures are E2E (identical to base) and Quality Report (pure aggregator).
…ic dashboard glyph (#452)

gate-55 (detail-page-discipline) FAIL 28 was one idiom repeated 28 times:
detail-page widget `icon` fields were written as vue-material-design-icons
FILE names (`GiftOutline`, `OfficeBuildingOutline`, `CalendarClockOutline`)
rather than keys of the shared widget registry. CnWidgetIcon resolves a widget
icon as `DASHBOARD_ICONS[name] || DASHBOARD_ICONS['ViewDashboard']` — the app's
own registerIcons() registry is NOT consulted on that path — so all 28 widgets
were rendering the generic dashboard glyph, with no visible breakage to notice.

The repo's own convention already agrees: 124 of the 152 widget icons were
registry keys. These 28 were drift. Each is mapped to the closest registry key
(OfficeBuildingOutline → OfficeBuilding, FileDocumentOutline → FileDocument,
CommentQuestionOutline → MessageTextOutline, CalendarClockOutline → Calendar,
GiftOutline → Package, …). No page ends up with two widgets sharing an icon.

Two of the chosen keys (Briefcase, Package) were not yet imported in
src/icons.js, which gate-60 correctly caught on the next run — an unregistered
name renders NO icon in the nav. Both added; both verified to exist in
vue-material-design-icons.

gate-46 (spec-anchor-existence) FAIL 1: RegisterFragmentMergeTest pointed at
openspec/changes/modular-register-manifest-fragments/… which does not exist in
this tree and never did (`ls` and `git log --diff-filter=D` both empty). The
fragment deep-merge it exercises is specified canonically by
openspec/specs/register-seed-data/spec.md (REQ-SEED-002, "Seeds declared in a
register fragment reach the importer" — the merge in
SettingsService::loadConfiguration()). Retargeted there, per the rule that
@SPEC anchors point at openspec/specs/, never at a change dir.

Measured with the gate package CI uses (ConductionNL/.github @ b8c7eade), full
scope, against post-merge development:

  before  19=984 25=25 26=5 46=1 54=2 55=28 63=2   51 PASS, 6 skip
  after   19=984 25=25 26=5        54=2       63=2 52 PASS, 6 skip

The local baseline reproduces CI run 31459783989 gate-for-gate and
count-for-count. Skip set is unchanged (24/29/33/47/48/61), so no failure was
converted into a skip.

Both directions proven: reverting ONE of the 28 icons (geschenk-data back to
GiftOutline) reproduces `[gate-55] FAIL — 1` on the same runner.

gate-54 and gate-63 are deliberately untouched — both are filed human
decisions, not defects.
…had a default order (#453)

`crud-persistence` has been failing three ways on development — Meeting create,
Decision create, Decision edit — and all three are the same defect, in the app
rather than in the tests: create an object, return to the list, and it is not
there.

Measured against the live API, not inferred:

  GET /apps/openregister/api/objects/decidesk/meeting?_limit=20&_page=1
    -> total=25 returned=20, oldest first (2026-05-13, 05-13, 05-16, ...)

CnIndexPage's self-fetch mode asks for `_limit=20&_page=1` and sends no
`_order`, so OpenRegister answers in insertion order. The 21st object a user
creates is therefore invisible on the page they are returned to. The test was
right; the list was wrong.

`_order[@self.created]=desc` is the ordering that works, verified against the
same endpoint — `asc` and `desc` return genuinely inverted windows:

  desc -> 2026-07-26T15:09:22, 07-26T14:41:31, 07-17T20:34:31, ...
  asc  -> 2026-05-13T07:32:12, 05-13T07:32:14, 05-16T10:50:58, ...

Note `_order[created]` (without the `@self.` prefix) is NOT that ordering:
`asc` and `desc` returned byte-identical windows, neither of them sorted. It
names a JSON data property that does not exist, and OpenRegister sorts by it
silently. Only the `@self.`-nested metadata form works — the same nesting rule
that applies to metadata filters.

So all 45 `type:"index"` pages now declare `sortKey: "@self.created"` +
`sortOrder: "desc"`. Declared in the manifest rather than injected at merge
time in main.js, so the ordering is greppable, reviewable and visible to the
manifest gates. CnPageRenderer spreads `config` onto the dispatched component
and CnIndexPage declares both as typed props (verified in the exact published
@conduction/nextcloud-vue@2.2.0-vue3.9 tarball CI installs, not in the working
copy of the library): `sortKey` -> `useSelfFetchList`'s `defaultSort` ->
`useListView.buildParams` -> `params._order = { '@self.created': 'desc' }`.

A user's own column sort still wins: `useSelfFetchList` restores
`$route.query._order` ahead of the declared default.

Scope check, machine-verified rather than eyeballed: 45 of 45 index pages carry
the default, 0 missing, and 0 leaked into a non-index page. `npm run
check:manifest` reports `Ajv validation: PASS (0 errors)` against
app-manifest-v2 schema 2.13.0 — real schema validation, not the structural
fallback.

The remaining upstream candidate is left deliberately unfiled here: newest-first
is arguably the right DEFAULT for CnIndexPage self-fetch when a host declares no
sort, which would fix this for every app instead of one. That is a
nextcloud-vue change plus a publish, and it is not this repo's to make.
…e machine (#454)

`development` tracks `node_modules` as a mode-120000 symlink to
`/home/rubenlinde/nextcloud-docker-dev/workspace/server/apps-extra/decidesk/node_modules`
— one developer's absolute path, on one machine. Anywhere else that is a
dangling link sitting exactly where npm expects a directory.

I put it there. It rode in on `git add -A` in #452 (`3511f0f6`, `A node_modules`),
and it was the only unintended entry in that commit.

WHY .GITIGNORE DID NOT CATCH IT

`.gitignore` said `/node_modules/`. A pattern ending in `/` matches a DIRECTORY
only. The thing added was a SYMLINK, so the rule did not apply to it and it went
straight into the index. Proven both directions on this tree:

  with the old pattern:  git check-ignore -v node_modules  -> no match
  with the new pattern:  git check-ignore -v node_modules  -> .gitignore:19:/node_modules

So the fix is not only to untrack the link but to close the hole: `/node_modules`
and `/website/node_modules` lose their trailing slashes, which makes them match a
directory AND a symlink. The comment above them says why, because the next person
to "tidy up" that pattern will want to put the slash back.

This is a foreseeable hazard rather than bad luck: pointing a git worktree at a
sibling checkout's install is the normal way to avoid a second 534 MB
`node_modules`, and it produces exactly this symlink in a tree where `git add -A`
will take it.

Untracking it does not delete anyone's local install — the link stays on disk and
is now ignored.

Scope: `.gitignore` plus removing the tracked entry. No source file is touched, so
no gate and no test can move.
gate-25 (contract-coverage) FAIL 25 — twenty-five public endpoints reachable
through appinfo/routes.php with nothing asserting their contract. Ten new
PHPUnit controller tests close twenty-one of them.

WHAT WAS ACTUALLY MISSING

Not obscure corners. `transcription#transcribe`, `#realign`, `#attach`,
`#sources`; the whole `motionCoauthor` surface (add, remove, updateText,
history); `boardEvaluation#respond`; `liveMeeting#recordLiveDecision`;
`integration#getOutcome` and `#subscribe`;
`participation#submitAnonymousReaction`; plus the framework endpoints
(`api#preflight`, `ori#preflight*`, `dashboard#page`/`#catchAll`,
`health#statusOptions`, `actionItem#destroy`).

The tests assert the CONTRACT, not that a method exists: status codes on the
happy path, the shape of the JSON body, and the refusal paths — 422 without
consent, 503 without a provider, 404 for an unknown id, and the authorisation
denial for a non-staff caller. Roughly half the assertions are refusals, which is
the half an endpoint written without tests usually gets wrong.

NO WAIVERS

Zero `@contract exclude` in this change. Every one of the twenty-one is closed by
a test that runs. That includes the endpoints where an exclude would have been
easy to argue — the CORS preflights get real tests asserting their headers and
204, because "it is only a preflight" is a claim about behaviour and behaviour is
testable.

THE TESTS RUN, WHICH IS THE ONLY THING THAT MAKES THEM COVERAGE

  ./vendor/bin/phpunit tests/Unit/Controller/<the ten new files>
  OK (72 tests, 176 assertions)          <- 0 skipped

Whole directory, to show nothing else moved:

  Tests: 250, Assertions: 662, Skipped: 1

The single skip is pre-existing and unrelated —
`DecisionControllerTest::testPublishSucceedsReturns200`, already parked against
decidesk#90. No test in this change is skipped, incomplete, or risky.

MEASURED (gate package ConductionNL/.github @ b8c7eade, full scope, rebased onto
012f96b):

  before  19=984 25=25 26=5 54=2 63=2   53 PASS, skips 24/29/33/47/48/61
  after   19=984 25=4  26=5 54=2 63=2   53 PASS, skips 24/29/33/47/48/61

Skip set byte-identical; no other gate moved. PASS is unchanged because gate-25
is still red — which is the honest outcome, not a rounding-up.

WHAT REMAINS, DELIBERATELY UNCLOSED

Four endpoints, named rather than waved at:

  participationBudget#castAdvisoryVote     /api/participation/proposals/{id}/vote
  participationBudget#publishBudgetResults  /api/participation/budgets/{id}/publish
  settings#getPublicationConfig             /api/settings/publication-config
  settings#setPublicationConfig             /api/settings/publication-config

They are not excluded and not annotated. gate-25 stays red until they have real
tests, which is the correct signal.
…he eleven E2E failures (#458)

Closes gate-63 by DELETION, never a rename: gate-63 compares a settings page's id and title against a reserved word list, so a rename ships the duplicate while turning the gate green. Positive control, --full on the same tree: pristine development FAIL — 2 settings-placement violation(s), this branch PASS.

Full-scope dispatch (run 31488483644) established the honest baseline: COVERAGE 58 of 64 declared, 58 of 58 applicable ran, 5 real failures — not the 25-gate push-scoped green CI advertised.

Rehomes organisatie_modus onto the Nextcloud admin page. ADR-079 assumes the lib/Settings section already covers the deleted page's keys; measured, it covered two of three, and that key drives mode-specific labels app-wide. Backed by an e2e test that selects, saves, reloads and requires the value to survive.

Retracts the premise I was given: DecisionIntegrations, AgendaItemIntegrations and MotionIntegrations are all LIVE — registered in registry.js, named as a manifest page component, targets of action handlers. Only MeetingIntegrations.vue is dead and only it is deleted.

E2E 11 failures to 7, a strict subset with zero new: the two decision-management tab locators, motion-amendment's heading and field labels, and publication-workflow's textarea descendant. No timeouts widened, no tests skipped or deleted, no serial mode.

Remaining 7 are pre-existing and filed rather than forced: nextcloud-vue#630 (kvk/opencorporates JS registration), decidesk#460 (the board-evaluation scoped-responder fix shipped and the symptom is byte-identical — hypothesis disproven), decidesk#459 (app-navigation spec still requires the deleted entry; reconciling it needs real @e2e work on 23 legacy scenarios, not blanket tags). Gate defect filed at source: .github#349.
…ects the contract test found (#465)

gate-54 2 to 0, gate-25 4 to 0, gate-26 5 to 0, gate-19 984 to 983, all at full-tree scope against the empty-tree base.

Three product defects found by writing the gate-25 contract test, all previously answering HTTP 200: unlimited duplicate advisory voting (now 409), a tally writing votesFor=0 over real votes, and publishBudgetResults publishing an empty proposals array. Root cause was a relation filter keyed on the schema slug, which matches nothing in what OpenRegister actually stores.

Controls: committed gate plants for gate-54 and gate-25, each observed FAIL naming the plant, then reverted byte-identical. Newman on a live rig fails exactly the 6 of 35 assertions describing the three defects when only the two service files are reverted. Playwright mutation matrix, with exact: true, where each test fails when and only when its own target breaks.

E2E is red with the same 7 failures as the merge base 461952a, byte-identical set, and 112 passed against its 108. Nothing skipped, no timeout widened, no exclusion added.
… and three locators (#469)

E2E 7 failed -> 3 failed, 112 -> 116 passed, skipped UNCHANGED at 58. Same-tree comparison: `git merge-base origin/development HEAD` == `443a77ed` == development's tip, and `git log origin/development ^HEAD` is empty, so the 7-failure baseline (run 31521531981) was produced from this exact tree.

58 tests skip in this suite, so absence from the failure list proves nothing. All four fixed tests confirmed POSITIVELY by their own check lines and durations:

  137 board-evaluation-workflow.spec.ts:84   (13.5s)
  138 board-evaluation-workflow.spec.ts:133  (12.2s)
  139 board-evaluation-workflow.spec.ts:168  (13.3s)
  157 meeting-transcription-workflow.spec.ts:220 (7.8s)

Two of those three product defects meant a whole capability was dead: no likert board-evaluation response could EVER be stored (every save rejected on an explicit null against a non-nullable typed property, returned as a 422 the UI rendered outside the card), and publishing persisted nothing while answering HTTP 200.

The three remaining reds, none worsened:

- integration-registry.spec.ts:342 — left red DELIBERATELY. OCS advertises `kvk` and `opencorporates`; the JS registry declares neither. Positive-controlled that @conduction/nextcloud-vue ships no leaf for either. Real cross-app drift, filed as nextcloud-vue#630. Its fix does not live in decidesk and weakening the assertion would delete a true finding.
- crud-persistence.spec.ts:236 / :352 — the SAME two tests, and the strict-mode fix WORKED: the failure moved from the Close click in `deleteRowViaUi` (line 192) to the post-delete verification at line 310, i.e. the delete-and-close now succeeds and the test gets four steps further before exhausting its 20s budget on three full SPA boots. Not fixed, because every available remedy is a weakening: widening the budget is forbidden, and the three page loads each carry distinct coverage (create-appears-in-list, detail-renders-values, edit-surfaces-in-list-then-delete). Recorded rather than papered over.

Hydra Gates is red and is NOT caused by this change: `[gate-24] integration-parity: SKIPPED (structural)` — `.github#370` has propagated, so gate-24 now recognises decidesk's direct `integrations.register(` form and moves from NOT APPLICABLE to a counted structural skip. `37 GATE(S) GREEN`, zero FAIL lines, `1 of 38 APPLICABLE gates DID NOT RUN`. This diff touches none of gate-24's inputs — no `scripts/`, no `src/integrations/`, no `lib/` LeafDescriptor — so it reproduces identically on development. Predicted by decidesk#466, which now also carries the measurement that copying the sibling checker as-is exits 1 on the same one-spelling blind spot.

Everything else green: phpcs, phpmd, psalm, phpstan, PHPUnit on both cells (the coverage ratchet is satisfied — the new statements are covered), Newman, Frontend Build, Frontend Tests, CodeQL, Coverage Baseline Protection.

Controls: three committed mutations, each reverted, each failing ONLY its own target — the `scoreSummary` array branch (`Failed asserting that null is identical to 4`, the production symptom exactly), `depublicatiedatum` restored to null, and `sanitiseAnswers` restored to writing nulls (2 failures naming both keys). Two green-for-the-wrong-reason tests corrected: one PINNED the defect by asserting `depublicatiedatum` was present AND null, and one used a fixture label (`E2E-Published`) that made its own assertion unfailable.

Weakening checks all negative: no test.skip / test.fixme / .only / mode:'serial' / testIgnore / networkidle / force:true added, zero `@e2e exclude` added, and no removed line contains `timeout`.
…472)

Two independent defects, and repairing either one alone leaves the listener
dead. A four-arm control over one production-shaped entity, printed:

  A  origin/development (method_exists) ......... ''         no match
  B  probe swapped to is_callable() ............. THREW      no match
  C  probe fixed to property_exists() only ...... '93'       no match
  D  this change ................................ 'meeting'  match

1. The probe. ObjectEntity declares getSchema() only as an @method docblock
   tag; Entity::__call serves it, so method_exists() is false for it and the
   getter tier was skipped for every entity that actually has a schema.
   is_callable() is not the remedy: arm B shows it is true for any name and
   the call then raises BadFunctionCallException. Entity::__call routes get*
   to Entity::getter(), which decides on property_exists(), so that is the
   instrument used here, with the call additionally made exception-safe.

2. The value. MagicMapper and SaveObject stamp the schema's numeric database
   id onto every entity they materialise, so arm C reads '93' where the guard
   compares against the slug 'meeting'. The id is now resolved back to its
   slug through OpenRegister's SchemaMapper, memoised per request because the
   unfiltered-registration fallback invokes listeners on every object write.

Why the suite was green over a listener that could not fire: the stub is
faithful — tests/Stubs/Db/ObjectEntity.php honours the decidesk#399 parity
contract and does not declare the magic accessors — but every fixture fed a
_schemaSlug key that OpenRegister never emits. The parity contract constrains
the double; it does not constrain the payload. The new tests build the entity
the way MagicMapper does and assert through the real resolver.

Both halves are proven able to fail. Removing the id-to-slug resolution fails
exactly 5 tests; restoring method_exists as the only probe fails exactly 6.
Both predictions were written before the reverts and matched.

Behaviour change: meeting creates now get their Files folder tree, which they
never have. The call is fail-soft and bounded (one folder tree per meeting
create), and an unresolvable schema fails closed - no tree is created for an
object that might not be a meeting.

Not touched, same defect, filed separately: SubmissionDeadlineListener and
GovernanceRoleProjectionListener resolve their slug the same dead way. Waking
those two changes what the write path permits rather than what it creates, so
they are sequenced behind their own verification.

Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
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.

3 participants