Release: merge development into beta - #2976
Conversation
… one openregister ships a flows surface (the `flows` index at /flows, in the main menu) and its tour never mentioned it, so the automation was discoverable only to someone who already knew it was there. Found by widening gate 70's flows-page predicate, which matched only `type: "flows"` — a type exactly one fleet app declares. openregister ships its surface as an ordinary `type: "index"` page, so the gate reported NOT APPLICABLE: it read as covered while covering nothing. The step is `optional` with `allowManualNext`: showing where flows are edited must not become "author an automation before you may finish the tour". It targets `flows` — the ROUTE, lowercase, as the menu entry declares it. `data-cn-route` carries `item.route`, and that is what CnWalkthrough.resolveTarget() looks up. The menu entry's ID here is `Flows` with a capital F; targeting that would look right in review and resolve to nothing at runtime, and an optional step whose target is absent is SKIPPED silently. (The neighbouring `open-tables` step has exactly that shape — it targets the menu id `Tables` while advancing on route `tables` — which is worth a separate look.) All 36 required locales are translated, not just en/nl: this repo enforces full parity, and `test:l10n` fails on any missing or empty value. Verified: gate-70 0 findings, gate-96 rc=0, l10n parity OK across 36 locales, check:l10n-js rc=0.
feat(walkthrough): show where flows live, without making anyone build one
… page) (#2978) * docs: add a local demo environment Adds `openregister-compose.yaml` and a setup page describing it. The compose brings up Postgres and Nextcloud, installs openregister (required), thematiq and integriq (optional) and openregister from release tarballs, and enables them in dependency order. Nothing is bind-mounted: Nextcloud installs an app by deleting its directory and extracting an archive over it, so pointing that at a checkout deletes the working tree — measured on a development machine on 2026-08-27, where an app-store update fired on a container restart and removed every top-level file including .git. Release tarballs rather than a clone for a second reason: a tarball is a complete app carrying vendor/ and the built js/, and an app with no vendor/ does not fail loudly — it warns once and keeps loading, so it looks installed while every service needing a dependency is absent. The openregister dependency is not declared in appinfo/info.xml — no app in the fleet declares an <app> dependency — so the compose encodes what the manifest does not. Verified: docker compose config parses and interpolates; the same generated file was booted end to end for portaliq, which produced 17 registers, 86 schemas and 13 magic tables for its own register, with the portal content API returning a real site rather than an empty shell. * docs: make the demo verification command actually pass The verification curl was unauthenticated, and a Nextcloud app page requires a login, so it printed 401 on a healthy demo while the page described it as a pass. Measured on two booted demos: 401 without credentials, 200 with them. The command now carries them and says a bare 401 is expected. * test(e2e): check a demo environment the way its docs say to Validates a booted demo against the steps its own documentation tells the reader to run. Lives outside tests/e2e/ because the root config sets testDir: './tests/e2e' and this suite needs an already-booted demo that CI does not have -- collected there it would fail, and gated with a skip it would look identical in CI to a suite that ran and found nothing. None of the assertions is a status code, because two measurements taken while writing it show why: - the Nextcloud LOGIN page is served with HTTP 200, and basic auth does not authenticate a browser navigation (only API routes), so a status-only test passes while sitting on the login screen; - an unauthenticated app URL answers 401 on a healthy demo -- the request the demo documentation used to describe as a pass. Shown to fail, not just to pass: pointed at an app that is not installed, the two reachability tests fail; portal assertions forced on against a demo with no portal fail both. Green against two independently booted demos, portaliq (6 passed) and shillinq (4 passed, 2 correctly skipped). * fix(test): keep the demo e2e suite out of jest, and format it Adding tests/demo-e2e/ broke two frontend jobs, both my doing. jest collected the Playwright spec. Its ignore list named <rootDir>/tests/e2e/ specifically -- the Playwright suite -- and the demo suite deliberately sits OUTSIDE that directory, because playwright.config.ts sets testDir: './tests/e2e' and a spec placed there is collected by CI, which has no booted demo to point at. Solving the one collector walked straight into the other: two runners with opposite exclusions, so the file has to be named in both. Verified by listing tests rather than by reading the pattern: with the ignore jest collects 0 files under tests/demo-e2e/, without it 1. The two .ts files were also not in the repo's prettier style, which `prettier --check "**/*.{js,ts,vue,css,scss}"` covers. Formatted with the repo's own config; prettier --check is now clean on both. * style: format the demo e2e files with the repo's own prettier config The first attempt formatted copies in /tmp, which resolves a different prettier config and a different .prettierignore than the repo does -- so it reported the files clean while `prettier --check` at the repo root, which is what CI runs, still rejected them. It also made the repo's own jest.config.js look unclean, which it is not. Formatted in place. `prettier --check "**/*.{js,ts,vue,css,scss}"` -- CI's exact command, run from the repository root -- is now clean across the whole tree. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
… step (#2979) * feat(flow): attribute every write in a run to the run, node and step A flow run recorded exactly one object — `FlowRun.subjectUuid`, the thing that TRIGGERED it. Everything the run went on to touch was attributable to nothing, so neither "which node last touched this case" nor "what did this run change" could be answered. `FlowRunStep` already said what happened; it did not say what it happened to. The engine now establishes an ambient frame around each hop and the audit builder stamps it onto every row. Ambient rather than a parameter because the point is to catch writes made by code that has never heard of flows — a leaf app a node calls into, a cascade, a lifecycle hook. A node cannot report what it did not know it did. The pop is in a `finally`, and that is the whole safety property: every other exit from a hop is a `return` inside a catch — a stop, a suspension, a terminally-failed step. A frame left standing attributes LATER writes to a finished run, across runs, since one worker advances several. It produces no error and no wrong-looking row. FlowEngineAttributionTest asserts the leak direction; deleting the `finally` turns 5 of its 8 tests red. Step numbers are `base + index-in-log`, not a dispatch counter: a PINNED step logs an entry without ever reaching the dispatcher, so a counter would silently desynchronise from the FlowRunStep rows it has to line up with. ADR-003 Rule 4 — the three fields join the canonical JSON, so re-pointing a row at a different run breaks verification instead of going unnoticed. That makes it a seed migration (v1 → v2) rather than a column addition. The repair step verifies the OLD chain against a FROZEN v1 canonicaliser and records that verdict before re-sealing: verifying with the current canonicaliser would include the new keys, report every pre-existing row broken whether tampered with or not, and the re-chain would bless it either way — a check that cannot tell an intact chain from a compromised one is not a check. It refuses to start if it cannot store the verdict, because a re-chain with no account of what it replaced has no remedy. Also closes a pre-existing gap found on the way: `FlowRunController::show()` was unscoped while `index()` has been scoped since shared-credentials-and-flows D7 — and a run's serialisation carries its log, which records the subject data the flow touched. Both now resolve through one predicate in the mapper rather than two copies that drift. Refs: openspec/changes/flow-object-attribution * refactor(flow): one assignee rule, reachable from outside the controller `refuseUnlessAssignee()` guarded the HTTP resume endpoint, which was the whole story while HTTP was the only way to answer a step. It is not: a leaf app whose own object completes a task resumes the run IN-PROCESS through `FlowRunService::signal()`, which never passes the controller. Left where it was, every such caller re-implements the rule. Re-implementing it is the failure mode. Two copies of one access rule do not stay identical, and a divergence here does not throw — it lets the wrong person answer somebody else's question, correctly formatted, HTTP 200. The GROUP branch is the half a hand-written copy forgets, and forgetting it refuses the step's own intended audience while still reading as "the guard works". So the rule moves to FlowRunAssignee and the controller delegates. Its 24 existing tests pass unchanged, which is the point — behaviour is identical, only its reachability changed. The old private copy is deleted rather than left beside the new one. The new tests cover the three directions that pass while broken: the group branch, the deliberately-OPEN unassigned case (webhook and child-run signals record no assignee and must keep working), and the fail-closed anonymous case. Mutating `mayAnswer()` to return true kills 8 tests across both suites. Also adds `FlowNodeResumeState::nodeId()`. A node is handed its own slot but was never told its own name, which is fine until it must hand its identity to something outside the run — a task record that has to resume this exact node. A run holds one awaiting slot per node, so "resume this run" is not an answer. * refactor(audit): flow attribution gets its own home, and the gates pass phpmd caught a real regression rather than a style nit: adding the stamp and the query took AuditTrailMapper from clean to 27 non-accessor methods against a threshold of 25. Checked against origin/development rather than assumed — the base was clean, so this was mine. Both halves now live on AuditFlowAttribution. They belong together because they are one fact read from two ends: the stamp decides what a row claims, the query trusts that claim, and keeping them apart is how the column set they agree on drifts. The mapper is back under its ceiling and no longer has to know what a flow is. Other gate findings, each fixed at the cause: - FlowEngine::run() NPath 226/200. The 26 came from the `finally` that makes the attribution pop unconditional, and that is a correctness guarantee, not a convenience: every other exit from a hop is a `return` inside a catch, so a pop on the success path leaks the frame into a LATER run advanced by the same worker. Suppressed with that reasoning written down, rather than restructuring a walk to win a number. - AuditCanonicalV1's static access is suppressed with its reason: it is deliberately frozen, and presenting it as an injectable collaborator would imply it can be swapped or updated — the one thing it must never be. - `array_values()` after `usort()` was a no-op that read as a safeguard. - `@template-extends QBMapper<AuditTrail>`, matching FlowRunMapper's convention, rather than casting the return type. - Three phpcs errors of mine (two missing @PARAM, one comment), and the @SPEC tags I had put on member variables, which that standard does not allow there. Gates now clean on every changed file: phpcs, phpmd, psalm, phpstan. 725 flow, controller and audit tests pass. * fix(ci): cover the new code, and stop tipping FlowRunService over its ceilings CI caught three things. **phpmd, twice, and both were mine.** An inline FQN in Application.php that wanted a `use`; and FlowRunService at 1047 lines / complexity 51 against thresholds of 1000 / 50. The base was at ~998 lines, so any addition trips it — my eight lines were simply the ones that did. Rather than trim a comment until the number passed, the step-history concern moved out whole. `FlowStepHistory` now owns both the NUMBERING and the RECORDING of a run's steps, and they belong together for a reason: attribution has to PREDICT a step's number before the walk, while the step row is written after it, and the two must arrive at the same value or an attributed audit row and its step row describe different steps. One class, one arithmetic — and `testRecordedSequencesContinueFromTheSameBaseAttributionUsed` asserts both ends in a single test, because checking either alone passes while they disagree. **The coverage ratchet was right too** (-2.49%). FlowStepHistory (10 tests) and AuditFlowAttribution (6) are now covered. The stamper's tests are all about the abnormal paths, because the normal one is a single line: a row written outside any run must carry NO attribution, an unresolvable context must still let the row be written, and something that is not a run context must not be trusted to be one. While writing them, `TestCase::run()` is final — the same trap the existing FlowEngineTest documents in a comment. Helper renamed. Gates: phpcs, phpmd, psalm, phpstan clean on every changed file. * test(repair): cover the one step in this change that cannot be undone The v1 → v2 migration had no tests, and it is the riskiest thing here: a re-chain recomputes every hash from current content, so afterwards an intact chain and a tampered one look identical, and the v1 hashes that could have told them apart are gone. So these test the ORDER and the REFUSALS rather than the happy path: - the verdict is stored BEFORE anything is re-sealed, and names the seed it moved from and to; - a verdict that cannot be stored means NO re-chain at all — the one refusal worth blocking an upgrade over, since a re-chain with no account of what it replaced has no remedy; - a second run is a no-op, because a v2 chain checked against the v1 form would report a false break and overwrite the real verdict with a meaningless one; - a re-seal that throws does not leave the step marked done, or the next `occ maintenance:repair` skips a half-sealed table. Worth noting how the first run failed: my IAppConfig double used an arrow function, which captures by VALUE, so the read-back always saw an empty store — and the step refused, exactly as designed. The double was wrong; the refusal it tripped was right, which is a reasonable way to learn the guard works.
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 175/175 | |||
| npm | ✅ | ✅ 545/545 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-28 19:07 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 175/175 | |||
| npm | ✅ | ✅ 545/545 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-28 19:22 UTC
Download the full PDF report from the workflow artifacts.
… invisibility (#3005) * feat(flow): attribute every write in a run to the run, node and step A flow run recorded exactly one object — `FlowRun.subjectUuid`, the thing that TRIGGERED it. Everything the run went on to touch was attributable to nothing, so neither "which node last touched this case" nor "what did this run change" could be answered. `FlowRunStep` already said what happened; it did not say what it happened to. The engine now establishes an ambient frame around each hop and the audit builder stamps it onto every row. Ambient rather than a parameter because the point is to catch writes made by code that has never heard of flows — a leaf app a node calls into, a cascade, a lifecycle hook. A node cannot report what it did not know it did. The pop is in a `finally`, and that is the whole safety property: every other exit from a hop is a `return` inside a catch — a stop, a suspension, a terminally-failed step. A frame left standing attributes LATER writes to a finished run, across runs, since one worker advances several. It produces no error and no wrong-looking row. FlowEngineAttributionTest asserts the leak direction; deleting the `finally` turns 5 of its 8 tests red. Step numbers are `base + index-in-log`, not a dispatch counter: a PINNED step logs an entry without ever reaching the dispatcher, so a counter would silently desynchronise from the FlowRunStep rows it has to line up with. ADR-003 Rule 4 — the three fields join the canonical JSON, so re-pointing a row at a different run breaks verification instead of going unnoticed. That makes it a seed migration (v1 → v2) rather than a column addition. The repair step verifies the OLD chain against a FROZEN v1 canonicaliser and records that verdict before re-sealing: verifying with the current canonicaliser would include the new keys, report every pre-existing row broken whether tampered with or not, and the re-chain would bless it either way — a check that cannot tell an intact chain from a compromised one is not a check. It refuses to start if it cannot store the verdict, because a re-chain with no account of what it replaced has no remedy. Also closes a pre-existing gap found on the way: `FlowRunController::show()` was unscoped while `index()` has been scoped since shared-credentials-and-flows D7 — and a run's serialisation carries its log, which records the subject data the flow touched. Both now resolve through one predicate in the mapper rather than two copies that drift. Refs: openspec/changes/flow-object-attribution * refactor(flow): one assignee rule, reachable from outside the controller `refuseUnlessAssignee()` guarded the HTTP resume endpoint, which was the whole story while HTTP was the only way to answer a step. It is not: a leaf app whose own object completes a task resumes the run IN-PROCESS through `FlowRunService::signal()`, which never passes the controller. Left where it was, every such caller re-implements the rule. Re-implementing it is the failure mode. Two copies of one access rule do not stay identical, and a divergence here does not throw — it lets the wrong person answer somebody else's question, correctly formatted, HTTP 200. The GROUP branch is the half a hand-written copy forgets, and forgetting it refuses the step's own intended audience while still reading as "the guard works". So the rule moves to FlowRunAssignee and the controller delegates. Its 24 existing tests pass unchanged, which is the point — behaviour is identical, only its reachability changed. The old private copy is deleted rather than left beside the new one. The new tests cover the three directions that pass while broken: the group branch, the deliberately-OPEN unassigned case (webhook and child-run signals record no assignee and must keep working), and the fail-closed anonymous case. Mutating `mayAnswer()` to return true kills 8 tests across both suites. Also adds `FlowNodeResumeState::nodeId()`. A node is handed its own slot but was never told its own name, which is fine until it must hand its identity to something outside the run — a task record that has to resume this exact node. A run holds one awaiting slot per node, so "resume this run" is not an answer. * refactor(audit): flow attribution gets its own home, and the gates pass phpmd caught a real regression rather than a style nit: adding the stamp and the query took AuditTrailMapper from clean to 27 non-accessor methods against a threshold of 25. Checked against origin/development rather than assumed — the base was clean, so this was mine. Both halves now live on AuditFlowAttribution. They belong together because they are one fact read from two ends: the stamp decides what a row claims, the query trusts that claim, and keeping them apart is how the column set they agree on drifts. The mapper is back under its ceiling and no longer has to know what a flow is. Other gate findings, each fixed at the cause: - FlowEngine::run() NPath 226/200. The 26 came from the `finally` that makes the attribution pop unconditional, and that is a correctness guarantee, not a convenience: every other exit from a hop is a `return` inside a catch, so a pop on the success path leaks the frame into a LATER run advanced by the same worker. Suppressed with that reasoning written down, rather than restructuring a walk to win a number. - AuditCanonicalV1's static access is suppressed with its reason: it is deliberately frozen, and presenting it as an injectable collaborator would imply it can be swapped or updated — the one thing it must never be. - `array_values()` after `usort()` was a no-op that read as a safeguard. - `@template-extends QBMapper<AuditTrail>`, matching FlowRunMapper's convention, rather than casting the return type. - Three phpcs errors of mine (two missing @PARAM, one comment), and the @SPEC tags I had put on member variables, which that standard does not allow there. Gates now clean on every changed file: phpcs, phpmd, psalm, phpstan. 725 flow, controller and audit tests pass. * fix(ci): cover the new code, and stop tipping FlowRunService over its ceilings CI caught three things. **phpmd, twice, and both were mine.** An inline FQN in Application.php that wanted a `use`; and FlowRunService at 1047 lines / complexity 51 against thresholds of 1000 / 50. The base was at ~998 lines, so any addition trips it — my eight lines were simply the ones that did. Rather than trim a comment until the number passed, the step-history concern moved out whole. `FlowStepHistory` now owns both the NUMBERING and the RECORDING of a run's steps, and they belong together for a reason: attribution has to PREDICT a step's number before the walk, while the step row is written after it, and the two must arrive at the same value or an attributed audit row and its step row describe different steps. One class, one arithmetic — and `testRecordedSequencesContinueFromTheSameBaseAttributionUsed` asserts both ends in a single test, because checking either alone passes while they disagree. **The coverage ratchet was right too** (-2.49%). FlowStepHistory (10 tests) and AuditFlowAttribution (6) are now covered. The stamper's tests are all about the abnormal paths, because the normal one is a single line: a row written outside any run must carry NO attribution, an unresolvable context must still let the row be written, and something that is not a run context must not be trusted to be one. While writing them, `TestCase::run()` is final — the same trap the existing FlowEngineTest documents in a comment. Helper renamed. Gates: phpcs, phpmd, psalm, phpstan clean on every changed file. * test(repair): cover the one step in this change that cannot be undone The v1 → v2 migration had no tests, and it is the riskiest thing here: a re-chain recomputes every hash from current content, so afterwards an intact chain and a tampered one look identical, and the v1 hashes that could have told them apart are gone. So these test the ORDER and the REFUSALS rather than the happy path: - the verdict is stored BEFORE anything is re-sealed, and names the seed it moved from and to; - a verdict that cannot be stored means NO re-chain at all — the one refusal worth blocking an upgrade over, since a re-chain with no account of what it replaced has no remedy; - a second run is a no-op, because a v2 chain checked against the v1 form would report a false break and overwrite the real verdict with a meaningless one; - a re-seal that throws does not leave the step marked done, or the next `occ maintenance:repair` skips a half-sealed table. Worth noting how the first run failed: my IAppConfig double used an arrow function, which captures by VALUE, so the read-back always saw an empty store — and the step refused, exactly as designed. The double was wrong; the refusal it tripped was right, which is a reasonable way to learn the guard works. * fix(flow): a shipped flow must belong to a tenant, or it imports into invisibility Found by running the e2e for the first time, against a real instance. Every flow READ is organisation-scoped (`FlowService::findAll()` refuses outright when no tenant resolves). `SchemaFlowImportListener` never set one, so a flow declared through `x-openregister-flows` was stored with `organisation` NULL and returned by NOTHING: absent from the flows list, therefore impossible to open, therefore impossible to ADOPT — while sitting perfectly intact in the table. Measured on a clean instance: the declared `Case behandeling` flow was present in `oc_openregister_flows` with 18 nodes and invisible to `/api/flows`, next to two seeded flows that carried an organisation and listed fine. Backfilling the column made it appear immediately, with `enabled=false` and `owner=NULL` intact. Pre-existing: the importer has always done this. Nothing had noticed because this is the first shipped flow declaration in the fleet — the two example flows are seeded through a different path that sets the tenant. The resolver is container-based and returns null rather than throwing, so a declared flow still imports where OrganisationService is unavailable; it just stays unlisted, and now says so in a warning instead of silently. * fix(phpcs): document the container parameter I added phpcs caught what my local run had not: I ran the file through phpmd, psalm and phpstan but not phpcs before pushing. One missing @PARAM. * test(flow): cover the organisation stamp on an imported flow The coverage ratchet was right to fail #3005: the fix added fifteen statements and no test, so the code that decides whether a shipped flow is ever VISIBLE was the only part of the change nothing exercised. Six tests, one per branch of activeOrganisation() plus the re-import case. The last one is the one worth keeping: the stamp sits in the CREATE branch beside enabled/owner, so an upgrade running as a different tenant cannot move an already-adopted flow out from under the organisation using it. Mutation-checked: removing the setOrganisation() call turns testAnImportedFlowIsStampedWithTheActiveOrganisation red.
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 175/175 | |||
| npm | ✅ | ✅ 545/545 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-28 21:26 UTC
Download the full PDF report from the workflow artifacts.
…sation (#3007) My own fix for the invisible-flow defect was half a fix, and its e2e proved it — in CI, where the previous run could not. A schema import runs during install and during `occ maintenance:repair`. There is no user session there, so `getActiveOrganisation()` returns null, the flow is stored with organisation NULL, and every flow read being organisation-scoped it is invisible in /api/flows all over again. The dev stack I verified on happened to HAVE an active organisation, so it went green; the CI runner does not, and dossiq's case-flow e2e failed on exactly the assertion written to catch this. `getOrganisationForNewEntity()` is the call every ordinary object save already makes, and it exists precisely to fall back to the default organisation for callers with no session. The test double now answers NULL from `getActiveOrganisation()`, so the regression cannot pass: reverting the call turns two tests red.
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 175/175 | |||
| npm | ✅ | ✅ 545/545 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-28 22:06 UTC
Download the full PDF report from the workflow artifacts.
#2982) * feat(setup): a wizard that offers the demo data this app already ships This app ships lib/Settings/*_mock_register.json - a dataset generated from its own schemas, conformant by construction, validated by the generator's --check - and had no way for an operator to reach it. There was no setup wizard at all. welcome -> demo-data -> done. Nothing app-specific is invented: the only action is the demo-data import the descriptor already supports. A wizard that asked questions the app does not act on would be worse than none, which is why there are no configuration steps here yet. completed is TRUE and the demo-data step is optional, so setup never gates the app. skip-demo-data records its outcome just as installing does: since nextcloud-vue 2.21 an OUTSTANDING OPTIONAL step opens the wizard over every page (nextcloud-vue#806), so a step that can never be marked done is a dialog that never closes - the defect buildiq was failing 37 E2E specs on. Verified: manifest validates against schema 2.26.0, gate-100 PASS, routes.php and both PHP files parse. The template was checked on launchpad against phpcs, phpstan, psalm and phpmd - all clean. * fix(setup): declare the endpoints' auth, and translate the wizard's strings Two gate findings on the previous push. gate-5 route-auth — status() and runAction() carried no auth attribute. The docblock said 'admin-only by Nextcloud's default for an un-attributed method', which is true and is not a declaration: the gate exists because a missing attribute silently makes an endpoint unreachable, and a comment cannot be checked by middleware. Both now carry #[AuthorizedAdminSetting(Application::APP_ID)], placed DIRECTLY above the declaration - gate-5 walks upward from the method and a long docblock between attribute and declaration costs the attribute its visibility, which the gate documents as a false FAIL it had to repair. gate-102 manifest-l10n-coverage — the wizard's title and body strings had no l10n/nl.json key, so a Dutch user would read them in English. Added, and the browser catalogue rebuilt where the app ships one: nl.json alone is not enough, because the browser reads nl.js. The catalogue edit is insertions only, proven against the same change applied structurally - an earlier attempt on another app re-serialised the whole file (410 lines) before being reverted. * fix(setup): authorize against the admin settings class, and test what it guards `AuthorizedAdminSetting` takes a `class-string<IDelegatedSettings>`, not an app id. Passing `Application::APP_ID` is a plain string, so phpstan rejected it — and the apps where this shipped green (larpinq, shillinq) already pass their admin settings class. Match them: `OpenRegisterAdmin::class`, which implements `IDelegatedSettings`. gate-47 was right to fail this too. The change adds an admin-authorized endpoint pair with no test alongside it, and a mocked unit test cannot show that middleware admitting a real session. The e2e spec issues both calls from inside the logged-in admin page, and asserts the install response NAMES how much landed — the one assertion that separates a real import from one that wrote nothing, which is the defect this programme already shipped once. * test(setup): cover the demo-data controller and service The coverage ratchet was right: this change adds ~364 lines of PHP with no unit test behind them, so the coverage of the files it touches fell 46.48%. The two assertions worth naming: - a FAILED install must leave the step UNDECIDED. Recording the decision in the catch block would close the step for an operator who asked for demo data and received none — the wizard would never offer it again and nothing would have been imported. - the object count comes from the FILE, not the importer's reply, so the number reported is the number ASKED FOR. An object whose schema does not resolve is skipped rather than errored, and that discrepancy must stay visible. Both were verified by mutation: reversing each behaviour fails exactly the test that claims to guard it. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 175/175 | |||
| npm | ✅ | ✅ 545/545 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-29 01:56 UTC
Download the full PDF report from the workflow artifacts.
…licks (#3011) The ADR-111 setup step is OPTIONAL, and CnAppRoot opens the non-gating wizard as a full modal mask while any optional non-info step is reported not-done — in every fresh browser context, so once per spec. Merging the setup wizard therefore turned this app's whole E2E suite red without touching a single spec: the call log reads "locator resolved to <button ...> - attempting click action" with <ol class="cn-wizard-dialog__progress"> named as the interceptor. The element was found; the click never landed. SKIPPED rather than installed, because recording the DECISION is what closes the wizard. Installing would push the app's demo dataset into every list the suite asserts on, which changes what the other specs measure. `demo-data-setup-step.spec.ts` exercises the install deliberately, in isolation. Uses the workflow's own exported credentials rather than this script's internals, and is tolerant of a non-200: an app whose wizard has no demo-data step answers 400, and that is not a seeding failure. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 175/175 | |||
| npm | ✅ | ✅ 545/545 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-29 02:33 UTC
Download the full PDF report from the workflow artifacts.
) * perf(ci): pilot the changed-path filter and the reduced PR matrix * style(phpmd): drop the else in parseExpression `PHP Quality (phpmd)` was red on one violation: lib/Service/Aggregation/MetricExpressionEvaluator.php:180 ElseExpression The method parseExpression uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. The if/else assigned $value on both arms, so a ternary says the same thing with no branch to object to. Arithmetic is unchanged — the two arms are the same expressions in the same order. php -l clean; no other else clause remains in the file. --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 175/175 | |||
| npm | ✅ | ✅ 545/545 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-29 06:21 UTC
Download the full PDF report from the workflow artifacts.
…ts panel (#3014) The panel that shows which objects a run touched has existed in nc-vue since #834 and reached nobody: openregister was locked to 2.21.0, and the LOCKFILE is what decides, not the range. The range moves to ^2.22.0 as well, deliberately. Leaving the floor at ^2.21.0 would keep resolving a version WITHOUT the panel perfectly happily — the app would build, boot and render an empty sidebar, which is the failure mode this whole change exists to remove. The floor should encode what the code actually needs. Resolved with npm 11, which the repo requires for a reason: .npmrc sets min-release-age=2 and npm 10 does not implement it. 2.22.0 published today, so the cooldown would be in scope — it resolved forward anyway because min-release-age-exclude[] covers @conduction/*, which is exactly what that exclusion is for. Verified rather than assumed: npm ci -> exit 0, so lock and manifest agree node_modules/.../package.json -> 2.22.0 on disk npm run build -> exit 0 grep in js/ -> the built vendor bundle contains loadRunObjects, runObjects, AND the path flow-runs/${f}/objects, which is the endpoint #2979 added on the server That last one is the point: the two halves now name the same route. Exactly one package changed in the lockfile.
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 175/175 | |||
| npm | ✅ | ✅ 545/545 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-29 06:38 UTC
Download the full PDF report from the workflow artifacts.
$data['schemas'] is built purely from schemasMap, which holds only the
schemas the current run processed — the schema pass does
`if ($schema === null) { continue; }` before populating it, so an existing
schema that was skipped or rejected never enters the map. The id list then
arriving at updateFromArray() is missing it, and the update REPLACES the
register's list, unlinking a schema the register still owns.
Nothing fails. The schema keeps its table and rows; the register stops
listing it. Register-scoped reads then return empty collections for data
that is present — indistinguishable from a working-but-empty install.
Measured on a live instance (#2935): 60 of dossiq's schemas were unlinked,
11 of them holding rows. `relink-schemas` recovered them with no
re-import and no data change, which is what identifies the linkage rather
than the objects as the defect.
Union rather than replace. A link this run can prove stays; a link it
merely cannot see is left alone. The failure direction becomes a stale
link, which relink-schemas already reports and repairs; the opposite
direction loses reachable data silently.
…ot-unlink-schemas fix(import): a register import must not unlink schemas it cannot see
This spec shipped to development with the ADR-111 setup wizard and has never executed once. The CI job runs `tests/e2e/ci/playwright.config.ts`, an explicit allow-list where nothing runs unless it is named — and this file was not. Measured on the merge run: `Running 65 tests`, none of them these three. It could not simply be added, because it WRITES: the install is a real import that creates this app's eight demo registers. Left behind they would land in the lists the already-admitted specs assert on — which is exactly why the CI seed settles the demo-data decision as *skipped* rather than installing it. So it now takes criterion 2's second branch: an `afterAll` deletes those registers, resolved by SLUG rather than an id captured mid-run, so an aborted run still tears its fixtures down. `_limit` and not `limit`, because a bare control param is read as a property filter by this API and returns zero rows with HTTP 200 — a teardown that would silently delete nothing. Verified by discovery rather than by assumption: the CI config lists 65 tests before this change and 68 after, with the three arms named. 65 is the same number the failing run reported, so this is the config CI actually uses. Also fixes the lint this file carried in unmeasured (its import order and a useless initialiser), and keeps the SPDX header first — `eslint --fix` hoists the type import above it, which would break REUSE compliance. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 175/175 | |||
| npm | ✅ | ✅ 545/545 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ✅ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-29 09:29 UTC
Download the full PDF report from the workflow artifacts.
2.22.1 carries the theme app-id fix (nextcloud-vue#840). CnAppRoot calls useScopedTheme() with no slug, so this app resolved theme tokens, the token-set catalogue and the contrast check through a hardcoded 'nldesign' app id. thematiq is renaming to 'thematiq', and every path in that composable degrades to default styling by design — so once a renamed build is installed this app would render unthemed with nothing in any log. The LOCK is what moves here. A caret range alone changes nothing, because npm ci installs what package-lock.json pins. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 175/175 | |||
| npm | ✅ | ✅ 545/545 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-29 09:46 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 175/175 | |||
| npm | ✅ | ✅ 545/545 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-29 10:36 UTC
Download the full PDF report from the workflow artifacts.
The release workflow could only be triggered by a push. The shared workflow already expects a dispatch -- its skip guard is written `github.event_name != 'push' || ...` precisely so that a dispatch is "deliberately never skipped ... including of a commit this guard would refuse" -- and 18 of the 21 fleet apps already declare it. This repo was one of the three that did not. The per-branch job conditions are unchanged: `github.ref` is the branch the dispatch runs on, so dispatching on development still selects the unstable job, on beta the beta job, on main the stable one.
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 175/175 | |||
| npm | ✅ | ✅ 545/545 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-29 11:22 UTC
Download the full PDF report from the workflow artifacts.
…s own (#3020) `scripts/l10n/` — 21 register detectors, the gated writer, the status/worklist reader, the 16-assertion selfcheck, the harvest and audit aids — plus `docs/l10n-workflow.md` and `docs/l10n-ui-translation.md`. WHY IT IS SPLIT OUT. This tooling was written inside #2634 alongside a large translation effort, and it cannot merge until that effort finishes. But the tooling is what makes the effort possible, and right now it exists on NO branch of NO app — measured 2026-08-29 across the fleet, `scripts/l10n/apply.js` is present on zero of decidiq's 458 remote heads and on no other app at all. Four agents were sent to translate with it and all four correctly stopped: the method was unsatisfiable because the instruments were not there. So the instruments land first, and the data follows. Additive only: 60 new files, no bundle is touched, no CI leg is added or changed, nothing behaves differently until someone runs a script by hand. WHAT IT MEASURES, on evidence from using it this week: register detectors found 229 values in Estonian and 62 in Romanian addressing the user in the wrong register — pre-existing, invisible to every other check selfcheck caught 276 Serbian translations written in Cyrillic into a bundle that is 96% Latin apply.js's gates refused patches whose "translations" equalled their key, and refused a plural array with three forms where the locale declares two — which renders BLANK at the counts that select the missing index None of those are findable by reading a diff. One limitation worth recording for whoever uses it next: `apply.js`'s model is "key absent -> translate". It has no path for a key that is PRESENT and holds another language's text, because gate 7 refuses to overwrite a non-identical value unless every key is named in `--allow-replace`. De-contaminating a copied bundle needs a reset mode this tool does not have. Verified on `development`'s own bundles: batch.js, selfcheck.js (15 assertions), apply.js and all 21 detectors execute; 22 locale configs load. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
Bumps [phpstan/phpstan](https://github.com/phpstan/phpstan-phar-composer-source) from 2.2.8 to 2.2.9. - [Commits](https://github.com/phpstan/phpstan-phar-composer-source/commits) --- updated-dependencies: - dependency-name: phpstan/phpstan dependency-version: 2.2.9 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [nextcloud/ocp](https://github.com/nextcloud-deps/ocp) from 34.0.2 to 34.0.3. - [Commits](nextcloud-deps/ocp@v34.0.2...v34.0.3) --- updated-dependencies: - dependency-name: nextcloud/ocp dependency-version: 34.0.3 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
seed.sh creates e2e-owner and e2e-other fresh on every run, and a fresh Nextcloud account meets the firstrunwizard modal on its first page load -- the "A collaboration platform that puts you in control" panel. Its mask covers the app, so every click the specs make is swallowed. It reads as two unrelated defects. object-shares-tab fails with "a modal is still covering the page after three Escapes"; flow-controls fails with "the connection did not reach the canvas", because the drag never lands. Neither message mentions a wizard, and the spec's own Escape-based dismissal does not close this one -- it is not the app's setup wizard, which the seed already handles via skip-demo-data. Measured on a local instance against development, same build, same seeded accounts, only this variable changed: firstrunwizard enabled flow-controls FAILED firstrunwizard disabled flow-controls PASSED Disabled instance-wide rather than per-user: occ exposes no per-user "mark seen", and a test instance has no use for the wizard. The block is idempotent and tolerates the app being absent, matching the style of the user and group seeding above it. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…3051) Supersedes #2987, which bumped the dependency alone and left the frontend unit suite red. Migration: - `SafeParseReturnType<T, unknown>` is gone in zod 4; the replacement is `ZodSafeParseResult<T>`. All ten entity `validate()` signatures now return `ZodSafeParseResult<unknown>`, which keeps `data` typed `unknown` exactly as the zod 3 declaration did. - `z.record()` no longer accepts a single argument: the key schema is now mandatory. `schema.ts` and `view.ts` pass `z.string()` explicitly, which is what zod 3 inferred. - `object.spec.ts` asserted zod 3's wording for the `too_small` issue. zod 4 reworded it; the assertion now pins `code: 'too_small'` as well so it is anchored on the rule rather than on the message text. No schema was loosened. A differential run of all ten validators over 4325 mutated inputs shows zero accept/reject disagreements between zod 3.25.76 and zod 4.4.3; the only deltas are zod 4's renamed issue codes (invalid_string -> invalid_format, invalid_enum_value -> invalid_value) and an extra companion too_small issue where zod 3 short-circuited after invalid_type. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…egister (#3016) * feat(organisation): consolidate the leaf apps organisation onto OpenRegister ADR-002 makes the organisation UUID the only tenant key, but OR never owned the rest of what an organisation IS, so leaf apps grew their own copies -- OpenCatalogi a publisher record (oin/tooi/rsin/pki/image), Stackiq a vendor record. ADR-022 section 3 makes those copies the defect; this gives OR the columns that make reuse possible. Repairs two silent live defects along the way: - `groups` has no column on any instance. Version1Date20250102000000 adds it guarded by hasTable(openregister_organisations), but that class sorts five months BEFORE the one that creates the table, so the guard is false and the step succeeds having done nothing. TenantLifecycleService ::provision() calls setGroups(), so provisioning could never succeed. - storage_quota / bandwidth_quota / request_quota have no column anywhere; the only migration creating them targets openregister_applications. They were copy-pasted from Db/Application. QBMapper::update() builds SET from getUpdatedFields(), so a marked field with no column is an SQL error at write time, not a no-op -- every PUT /api/organisations/{uuid} touching those fields was a guaranteed 500. * fix(organisation): make the consolidation PR's own checks pass Three failures, all this change's own. phpmd: changeSchema() was a 20-branch, NPath-524288 method built from 17 identically shaped `if (hasColumn)` blocks. The list is pure description, so it is now data — split by the three facets the class docblock already reasons in (tenancy repairs, identity, relationship) — and the method is a guard plus two loops. Every column name, type and option is unchanged; verified by diffing the extracted name and Types:: constant per column. phpunit: OrganisationTest asserted the field-type keys under COLUMN names (`storage_quota`), which is what this branch deliberately changed. The entity is right and the test was pinning a latent bug: Entity::__call() resolves a setter to lcfirst(substr($method, 3)) and Entity::fromRow() maps a column to its property BEFORE looking the type up, so a snake_case key matches nothing and the declared cast never runs. For the ?DateTime properties that was a live 500 — fromRow() assigned the raw string onto a typed property and raised "Cannot assign string to property $provisionedAt of type ?DateTime". The test is updated to the intended behaviour and gains testFromRowCastsDatetimeColumnsToDateTimeObjects, which A/B-fails with exactly that TypeError against the old registration. hydra gate-46: the @SPEC tags pointed at openspec/changes/consolidate-organisation-on-or/, which was never written. Adds the change (proposal, delta spec, tasks) rather than dropping the tags, per the gate's "fix the TARGET" guidance. Also corrects two docblock claims that referenced things that do not exist: Organisation::$mergedInto said the tenant-resolution path calls resolveMergeTarget() (it has no caller), and the migration pointed {@see} at a Repair\ConsolidateLeafOrganisations class that was never written. Both gaps are now recorded as open tasks 3.3 and 5.1.
* feat(flow): pin every run to the flow version it started on
A run resumed against the LIVE definition: the advancer re-resolved the flow
by id on every pass, so a case parked for a week on a human step came back to
whatever the graph had become. ADR-098 Decision 6 calls that the programme's
highest-risk defect and forbids shipping human task nodes without versioning
first — dossiq already ships two.
Storage is two layers. openregister_flow_defs is a content store addressed by
the sha256 of the canonical graph, so an unedited flow stores ONE row no
matter how many runs it backs, and immutability is structural rather than
promised. openregister_flow_versions NAMES those graphs: (flow, version)
unique, with a draft/published/deprecated lifecycle and at most one published
version per flow.
A run records flow_version at queue time, and the graph it walks is a function
of that number alone. FlowRunService::execute() enforces that for all four
call sites that hand it a document — three of them resolved it LIVE, so a
pinned run reached the engine and then walked the current graph anyway. When a
pinned version cannot be resolved the run FAILS naming the version; it is
never re-pointed at a newer one, because its marking, its taken decisions and
its log all belong to the version it started on.
Authorization is deliberately NOT pinned. owner and organisation keep coming
from the live document, so a revoked grant stops the next hop of a run queued
while it was still valid. Pin the shape of the work, never the right to do it.
The trigger set is derived from the published version inside FlowTriggerIndex,
so a draft's trigger nodes match nothing by construction rather than by a
filter on the read path somebody has to remember. The dead-end preflight now
judges the graph being pinned rather than the editable head, so a broken draft
cannot refuse runs of a sound published version and a repaired draft cannot
mask a dead end in the version actually going live.
BackfillFlowVersions publishes version 1 of every existing flow and pins every
still-movable run to it. Without that step the upgrade is a fleet outage:
queue() refuses any flow with no published version and the advancer fails any
run with no pin, and nothing that exists today has either. It is idempotent by
query — already-versioned flows are skipped, runs are pinned only where
flow_version IS NULL — and terminal runs keep their null, because telling a
run that finished last year that it executed version 1 would invent history.
Tests are mutation-checked: reverting the pin, falling back to live on a
missing version, substituting a newer version, and sorting list members during
canonicalisation each turn the naming test red.
* feat(flow): the version lifecycle API — publish, draft, deprecate, read
Five routes and a refusal shape. `PUT /api/flows/{id}` now answers 409 when the
definition changes against a published head, and only then: renaming a flow,
editing its description or switching it off are not changes to the process, and
refusing those would make a published flow unmanageable rather than merely
uneditable. FlowService compares a signature of the four graph keys taken
BEFORE applyEditableFields() mutates the entity, because reading it afterwards
compares the incoming graph with itself and the refusal never fires.
409, not 400: the request is well-formed and it is the flow's STATE that
refuses it — a state the author can change, after which the identical request
succeeds. The body carries `reason`, `lifecycleStatus` and `flowId` as fields
rather than prose, because "this version is published, create a draft" and
"this flow has no published version, publish one" want opposite buttons from
the editor, and picking between them by parsing an English sentence is how a UI
offers the wrong one.
The version number in the route is \d+, not [^/]+. Without that,
/versions/publish would match the version route with the literal string
"publish" and return 404 for a route that exists.
A shipped flow now publishes version 1 on import. An app declaring a flow in
x-openregister-flows ships a finished process, and since versioning a flow with
no published version backs no run — so leaving it a draft would mean adopting
it still ran nothing, with no error to explain why. Published but still
disabled and unowned: publishing answers which graph would run, adoption
answers whether it may run at all.
The controller takes ONE new collaborator rather than four; the version reads
moved onto FlowVersionService and it resolves the publishing user from the
session itself, so an install-time import with no session and a request with
one both work without either caller remembering.
* test(e2e): the flow version lifecycle, end to end
Nine specs over the API a browser actually talks to. The headline one is the
defect: queue a run against version 1, publish a rewritten version 2, and
assert the queued run still names version 1. Asserting only that "the run
completed" would pass against an engine that silently adopted version 2, which
is exactly the failure being removed.
`createFlow()` now publishes by default, because a DRAFT BACKS NO RUN — every
fixture that runs a flow needs a published version, and getting that wrong
surfaces as a 409 on the run rather than as anything about the graph.
The refusal specs assert the stored graph is UNTOUCHED as well as the status
code: a refusal that still wrote would be the worst of both, an error the
author acts on over a change that happened anyway. And renaming a published
flow is asserted to SUCCEED, because metadata is not the definition and
refusing it would make a live flow unmanageable.
One spec exists only to catch a routing trap: `/versions/publish` must 404
rather than match the single-version route with the literal string "publish"
as a version number.
* docs(openspec): record what flow-definition-versioning actually shipped
3.3 was solved by a different mechanism than the task assumed: the pinned graph
is laid over the live document per run inside execute(), so the resolver memo
never needed a version dimension and two runs on different versions in one
worker batch still each get their own graph.
3.5 is explicitly NOT done. The hook exists — an unpinned run passes its own
document straight through — but nothing queues a draft test run or marks one in
the run list, so a draft cannot yet be tried from the editor.
* fix(flow): let an author still test-run a draft
Versioning made `queue()` refuse any flow with no published version, and the
editor's "test this flow" goes through `queue()` — so I had made every NEW flow
untestable until it was published. That is backwards: publishing is what you do
AFTER testing, and it broke a capability that worked before this change.
`trigger: 'test'` is now the one dispatch exempt from "a draft backs no run",
which is what the spec always said. Such a run is left UNPINNED, and
`overlayOnto()` passes an unpinned run's document through untouched — so a test
run is pinned in the sense that matters: it walks the exact graph it was started
with and nothing can substitute another one mid-run. It is distinguishable in
the run list by `trigger: test` and a null `flowVersion`.
The exemption is narrow in both directions, and both are tested: every other
trigger of an unpublished flow is still refused with `no-published-version`,
and a test run of a PUBLISHED flow still pins to it — this is about drafts, not
about test runs skipping versioning. Widening the condition to all triggers
turns the naming test red.
* test(flow): the trigger index must follow the published version
The published-only rule had no unit test at all — nothing constructs
FlowTriggerIndex by hand, so no existing suite reached it, and the rule could
have regressed to deriving from the head without a single cell going red.
Both directions of that failure are silent. Deriving from the head while a
draft is open subscribes the draft's trigger nodes — a half-authored flow
firing on real object writes — AND unsubscribes the published version's, so the
process that IS live quietly stops running. Neither raises anything.
Four tests: the head's draft trigger must not be subscribed while the published
one must be, a flow with no published version subscribes to nothing, `enabled`
still comes from the LIVE flow (switching a flow off must take effect at once,
whatever is published), and the rows stay keyed on the flow's own uuid rather
than the detached carrier's. Reverting the resolver to the head turns two of
them red.
* refactor(flow): keep the versioning work inside the quality thresholds
Running phpmd on the CHANGED FILES reported clean while running it over `lib`
— which is what CI does — reported three violations. A per-file invocation is
not the gate; the directory one is.
- `changeSchema()` had grown to 189 lines. Split into definitionStore(),
versionStore() and pinColumns(). The three calls are assigned to separate
variables before being OR'd, deliberately: written `$changed || $this->x()`
the short-circuit would SKIP later steps as soon as one reported a change —
creating the definition store and silently never adding the columns that
point at it.
- `FlowVersionService` had a coupling of 13. It no longer takes IUserSession;
the controller names the publisher from the FlowAccess it already holds,
which avoids a second session dependency there for one string.
- `FlowRunService` passed 1000 lines again. `buildRun()` and `newUuid()` — both
added by this change — move to `FlowRunRow`, which performs no checks at all:
every guard runs in `queue()` before it is reached, so a check there would be
a second copy of a rule that already exists, and the two would drift.
phpcs, phpmd (both passes), psalm and phpstan now all exit 0 in their CI form.
730 flow unit tests green.
* test(e2e): the version badge and the Publish button
Two UI specs, gated behind OR_UI_E2E like the rest of the page describe. They
need @conduction/nextcloud-vue 2.23.0, which carries the sidebar half.
The draft spec asserts the BADGE after clicking Publish, not the click. A
button that posts and silently fails looks exactly like one that worked; the
badge only reads 'Published' once the store has re-read the flow from the
server. It then re-reads the flow over the API too, so the test cannot pass on
an optimistic client-side state the server never accepted.
The published spec asserts Publish is ABSENT as well as Create draft being
present — offering to publish something already published is how an author
ends up meeting a 409 that the UI should have prevented.
* test(e2e): match the data-testid convention for the lifecycle hooks
* test(newman): publish each flow the engine collection then runs
All 14 cases in this collection create a flow and immediately run it, and both
setup sub-flows are invoked by SubFlowNode. Under versioning every one of those
would now answer 409 no-published-version, because a flow is created as a DRAFT
and a draft backs no run.
The collection is not in api-test-coverage's default set, so CI would not have
caught it — it would simply have rotted until somebody ran it by hand and found
sixteen red cases with no obvious cause.
Publishing in the create step is what an author actually does in the editor, so
each case still describes the same journey. The sub-flows need it for their own
reason: SubFlowNode QUEUES the child, and a queue resolves the CHILD's published
version at call time, so an unpublished child fails the step rather than the
parent's graph.
Written as a surgical text patch rather than by re-serialising the JSON: a
round-trip through json.dumps reformatted all 7,700 lines and buried the actual
change. Request count is unchanged at 65 and the file still carries zero \u
escapes, matching the 96 lines of raw UTF-8 it already had.
* style(e2e): run prettier over the flow-engine spec
quality / Frontend Check (format) runs prettier over **/*.{js,ts,vue,css,scss},
and tests/e2e/flow-engine.spec.ts was the only file it rejected on this branch.
* fix(e2e): the flow-engine fixtures were in the pre-inversion shape
Two specs in this suite have been failing on development, and not for any
reason to do with versioning: their fixtures hang the step type off the EDGE.
The engine inverted that — "an edge is sequence and a NODE is the action" — and
now refuses such a flow outright:
Flow edge "first" carries "type", which the engine no longer reads ...
This flow is in the pre-inversion shape and has not been migrated.
So the run failed before any node executed, and every assertion after it never
ran. This suite exists to be the POSITIVE CONTROL against an engine that
reports success while executing nothing — and it was not controlling anything.
Both fixtures now put the action on the node and leave the edge as pure
sequence, verified against a live instance before being written down:
A two set-fields nodes, last marked exit:true
-> status completed, 2 steps, types [openregister.set-fields, ...]
B a BARE `set-fields` node type, terminated by openregister.end
-> 1 step, failed, "No app provides the flow node type \"set-fields\""
`exit: true` rather than a terminal end node on A, deliberately: an end node
stops the run and lands it in `stopped`, while that test asserts the
`completed` path.
Found by running this suite against development BEFORE merging versioning, to
establish which reds were already there. Two were.
---------
Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…3057) The curl sweep that drove the docs rollout asserts status codes and redirect targets. Necessary, not sufficient: a Docusaurus site answers 200 for a 404 route, a blank shell and a build-error page alike -- the exact shape this repository's own demo suite exists to catch. So this renders every one of the 21 sites and asserts the page carries its own heading and its own compose filename, and follows all 13 retired hostnames in a real browser to assert where they LAND rather than trusting the 301. The deep-link case is checked through the request context, where the Location header can be compared exactly, query string included. 47 tests, all passing against production. One caution for whoever runs this next: a first attempt reported 5 failures and a second reported 13, and none of them were real. The machine had 21 orphaned Chrome processes and was answering ERR_INSUFFICIENT_RESOURCES before the request left the box, while curl returned a correct 301 for the same URL throughout. Check the local machine before believing a browser test about someone else's infrastructure. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
) `flow-controls.spec.ts` went red with "Run now was refused — the server answered 500", and the server log says: This flow has no published version, so it cannot back a run. Versioning (#3047) made every dispatch require a published version, which is the point of it — but it carves out one case, in FlowPublishedGraph: An unpinned run is the interactive draft test run — the one documented exception to "a draft cannot back a run". `FlowRunVersionPin::requirePublishedAndSound()` implements that exemption for `trigger === 'test'`. The editor's Run button posts to `flow#run`, which went through `FlowService::run()` and queued as MANUAL — so the exception could not be reached from the only screen that needs it. `FlowService::run()` now takes the trigger, and the editor endpoint names itself as the interactive run. Second defect, same path: `FlowLifecycleRefused` was uncaught in `FlowController::run()`, so a refusal that carries `reason` and `lifecycleStatus` — exactly so the editor can offer the right button — escaped as an HTML 500 and threw all of that away. It now returns the 409 the exception was designed to produce, via the existing `refusal()` helper the other three call sites already use. Verified: both new tests ERROR with the source change reverted. Flow suite 1218 tests green; phpcs and phpstan clean on the changed files. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…ngeSchema (#3063) Every fresh install ends up with a table that cannot be written to. The symptom, 238 times in one dossiq CI run: ERROR: null value in column "naam" of relation "oc_openregister_verwerkingsactiviteiten" violates not-null constraint STATEMENT: INSERT INTO ... ("code", "name", "purpose", "legal_basis", ...) Version1Date20260818230000 renames the 13 Dutch columns by adding the English ones in changeSchema() and copying + dropping the old ones in postSchemaChange(). postSchemaChange NEVER RUNS ON A FIRST-TIME INSTALL: Installer::installApp() calls $ms->migrate('latest', $previousVersion === ''), whose second argument is $schemaOnly, and MigrationService::migrateSchemaOnly() invokes changeSchema() on every step and nothing else -- then marks every step EXECUTED, so it never retries. naam, doelbinding and rechtsgrond are notnull => true from Version1Date20260430160000, while every current write names only the English columns. So the table keeps 33 columns and rejects every insert, forever. Drops the leftovers from changeSchema(), the one hook both paths run. A dropColumn() through the schema object is honoured there because the diff is applied by migrateToSchema() -- unlike in postSchemaChange, whose schema is never applied, an asymmetry 20260818230000 already documents. Data-safe in both paths: on an upgrade the copy+drop has already happened and this finds nothing; on a schema-only install there is by definition no prior data, since $schemaOnly is only true when the app had no version. Verified on a live install: 13 Dutch columns -> 0, the 13 English columns untouched, and the exact failing INSERT now returns INSERT 0 1.
…ue 2.24.0 (#3056) * fix(flow): version EVERY flow, not the first hundred `FlowMapper::findAllFlows()` defaults to `limit: 100`. Both flow repair steps called it bare, so each silently processed one page and reported success. Measured on a dev instance immediately after upgrading: **219 flows, 100 versioned, 119 left with no published version** — and a flow with no published version backs no run. The repair step that exists to stop versioning being a fleet outage was itself producing one, on 54% of the flows, while printing a cheerful summary. The trigger back-fill had the identical bug and it is worse there, because the symptom is pure silence: a flow past the first page is simply never subscribed and never fires. Both now page to exhaustion. Paged rather than passed a large limit on purpose: a limit big enough to look obviously safe today truncates silently once an instance grows past it, and the failure would look exactly the same. Verified by re-running `occ maintenance:repair` against the half-versioned instance the bug produced: Flow versions: published version 1 for 119 flow(s), 100 already versioned flows 219 · flow_versions 219 · still unversioned 0 which also demonstrates the idempotency guard doing its job on the 100 that were already done, and the content store deduplicating 219 flows down to 24 distinct graphs. * chore(deps): take @conduction/nextcloud-vue 2.24.0 for the flow version lifecycle 2.24.0 carries the editor half of flow definition versioning: the read-only canvas on a published version, the version badge, and Publish / Create draft / Deprecate. Without it an author has no way to publish a flow — and since a draft backs no run, a newly created flow could not be run from the interface at all. Exactly one package changed: 2.22.1 -> 2.24.0. Resolved with npm 11 deliberately: local npm 10.8.2 does not implement min-release-age, so a lockfile written by it does not reflect the cooldown policy this repo's .npmrc configures. * fix(flow): a refusal must reach the client as 409, not 500 Running a flow with no published version escaped `FlowController::run()` as an unhandled exception and reached the caller as an HTML error page. A 500 reads as "the server is broken" for what is actually "publish this flow first" — it sends the author to exactly the wrong place. `FlowRunController::test()` leaked the same way, and additionally leaked `FlowDeadEnd`, so pressing Run in the editor on a half-wired graph produced a stack trace instead of the sentence the engine had carefully written. **Every unit test passed through both defects**, because they all assert on the EXCEPTION rather than on the response a caller receives. The e2e caught them. There is now a controller test for the 409 path, and removing the handler turns it red. `FlowService::run()` also gained the `@throws FlowLifecycleRefused` it was missing — without it PHPStan called the new catch dead, having no way to know what the method actually throws. Test fixtures: several single-node flows had no outgoing edge, which is a dead end, so publishing them was correctly refused. They now mark `exit: true` — stopping there is deliberate in those fixtures, which is exactly what the flag is for. * fix(e2e): publish before running — a draft backs no run The spec that turned development red asserts the author journey build → save → run, and asserts 201 on the run. Versioning changed that journey: a flow is CREATED as a draft, and `flow-definition-versioning` says plainly that "a run SHALL be queued against the flow's published version. A draft or deprecated version SHALL NOT back a newly queued run." So from #3047 onward the journey is build → save → PUBLISH → run, and this spec was asserting a contract the app deliberately no longer offers. It surfaced as a 500 because `FlowLifecycleRefused` escaped `FlowController::run()` unhandled. That half is a real defect and is fixed in 7ed9ab7 — the refusal is now a 409 naming `no-published-version`. But 409 is not 201 either, so the status-code fix alone leaves this spec red, and nothing on the PR would have said so, because PRs into development SKIP the E2E job. The opposite shortcut was considered and rejected. Letting `POST /api/flows/{id}/run` quietly fall back to an unpinned draft test run makes this spec pass, and `tests/e2e/flow-engine.spec.ts` then asserts the opposite in two places: that this exact endpoint refuses a draft, and that it refuses a DEPRECATED flow, both with `no-published-version`. Two checks over one endpoint that cannot both hold. Silently running a retired process is a worse defect than the one being fixed, so the endpoint keeps its rule and the spec gains the step. Nothing is weakened: every existing assertion stands, including 201 on the run. Publishing goes through the editor's own control, so the new lifecycle UI is on the CI path too, and it asserts the BADGE rather than the click — a button that posts and silently fails looks exactly like one that worked. Also annotates the header's 1-node/0-node table as HISTORY. It records the dead-end 500 that step 2 fixed, and has already been read once as if it explained this later, unrelated 500 — sending the reader hunting for something the run path does per NODE. Node count had nothing to do with it. * test(flow): cover the paging fix and the refusals it now returns Nine unit tests over the code this PR changed, each with a positive control verified by reverting the fix and watching the named test go red. `BackfillFlowPagingTest` is the one that matters. The mapper double is a REAL pager — it honours `limit` and `offset` exactly as `FlowMapper::findAllFlows()` does — because a double that ignored them would let the unpaged implementation pass too, and the test would be asserting the double rather than the step. Over a 1,003-flow instance the unpaged version versions 100 and reports success, which is the defect as measured. Both steps are asserted on their OUTCOME (the version rows inserted; the flows handed to `rebuild()`), not on the call count: "it paged" is not the claim, "no flow was left unrunnable" is. A second test pins the mechanism, so a later implementation that reached the same total with one enormous page — which truncates silently once an instance outgrows it — is still visible. Also covered: the per-flow catch, because an aborted walk turns one broken flow into a broken instance; the trigger back-fill's refusal to throw, because an exception there aborts the upgrade carrying the fix; and idempotency, because `occ maintenance:repair` is expected to be re-runnable. `FlowRunControllerTest` gains the two refusals `test()` now catches. Both were escaping as HTML 500s, and every existing unit test passed through that, because they all assert on the EXCEPTION rather than on the response a caller receives — the same blind spot the 409 commit describes for `FlowController::run()`. These assert the response. Controls: reverting `everyFlow()` to a bare `findAllFlows()` fails 4 of the 7 paging tests at "100 of 1003"; removing the per-flow catch errors the resilience test; removing either catch in `test()` errors both controller tests with the exception escaping. * fix(flow): realign with development's test-run route, and drop the duplicate catch development independently fixed the 500-on-refusal and went further: the manual run endpoint now queues as TRIGGER_TEST, so pressing Run in the editor works on a flow that has never been published. That is the right call — requiring publication before you may TRY a flow makes publishing a precondition of testing, which is backwards. Git merged my catch alongside theirs and produced a duplicate; PHPStan caught it as an already-caught catch. Theirs stays, because it also carries the trigger change. Two e2e specs asserted the OLD behaviour and merged cleanly while becoming untrue — a textual merge over a semantic conflict: - 'a draft cannot be run' now asserts the opposite, that a draft CAN be run from the editor and that the run is UNPINNED, which is the property that actually matters: it walked the draft it was started with. - 'a deprecated flow backs no new run' asserted a 409 that no longer happens on that endpoint. It now asserts the invariant that does still hold — deprecating leaves the flow with NO published version, which is what stops a TRIGGERED run — rather than asserting something the product does not do. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…rejection (#3068) `handleValidationException()` formats `$exception->getErrors()` unconditionally. That getter is typed `?ValidationError` and IS null for every ValidationException thrown with a message alone — `enforceReadOnlyOnUpdate()` is one such path and says so in a comment where it throws. `ErrorFormatter::format(null)` is a TypeError, so the 400 this method exists to return became a 500 whose body carried none of the reason. Observed on dossiq's development, in the server log for every case edit: Opis\JsonSchema\Errors\ErrorFormatter::format(): Argument #1 ($error) must be of type Opis\JsonSchema\Errors\ValidationError, null given, called in .../ValidateObject.php on line 2233 The caller saw "server error" where the server had a precise sentence for it — "Cannot modify readOnly property: X" — which is exactly what an operator, and an e2e failure, needs to see. Guarded: format only a real ValidationError, and let the message carry the rest. Mutation-verified — removing the guard reproduces that TypeError verbatim in the new test, which otherwise asserts a 400 whose message still names the violated property. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Each run creates about fifteen flows and deleted none of them, so a shared instance accumulated them run after run. Measured today: **300 flows, 104 of them abandoned e2e fixtures.** That is not merely untidy. The flows index degrades with the row count until the list-page specs time out — a single spec exceeded 400 seconds — so the suite was slowly breaking itself, and the failures MOVED between runs rather than staying put. Left alone it reads as a product bug in the index rather than as litter, which is the expensive kind of wrong. `test.afterAll` now deletes what the run created, scoped to this run's own `RUN_ID` prefix — never a blanket sweep of test-looking names, because a parallel run's fixtures are not ours to remove. It never fails the suite: a cleanup error is worth logging but is not a verdict on the code under test. Proven by measurement rather than assumed: flows 196 before, 196 after, `cleaned up 19 fixture flow(s)`, zero fixtures left. Also: the published-badge spec gave its 15s timeout only to the FIRST assertion, so the sidebar's later renders fell back to the default 5s. Fine in a 3.3-minute describe, marginal in a 7-minute full run — it failed once there and passed in isolation, which is exactly that shape. Every assertion in it now carries the timeout, and the sibling spec waits for the button before clicking.
* fix(calc): a sequence-bearing identifier must survive an update A materialised calculation containing a `sequence` node was recomputed on every save. Off the create path there is deliberately no SequenceContext, so the evaluator resolves the node to null and `concat` renders it as an empty string — turning a stored "2026-0013" into "2026-" and silently destroying the assigned running number. Where that number is also `readOnly: true` (dossiq's `case.identifier`, a statutory zaaknummer), the corruption is user-visible: the edit form round-trips the value it was rendered with, readOnly enforcement compares it against the now-truncated stored value, and every PUT is rejected with "Cannot modify readOnly property: identifier". No case could be edited at all — dossiq e2e "editing a case persists the change" fails on exactly this. Skip re-materialising a sequence-bearing calculation whenever no SequenceContext is active, leaving the stored value untouched. The same guard covers the two other paths that persist a recomputation with no context: the temporal sweep (which also reported a spurious change on every pass) and `occ openregister:rematerialise-calculations` — the command schemas actually tell admins to run, which would have rewritten every identifier in a register. The two read-time paths (RenderObject, ManifestService) already skip materialised calculations and are unaffected. * docs(calc): point the inline references at the actual PR
* feat(openspec): rbac-disable-public-inheritance Add an opt-out for the "logged-in users inherit public group rights" semantics in OR's RBAC. Schemas and registers gain an optional inheritFromPublic boolean (default true, backwards-compatible). When false, authenticated users do NOT qualify for public rules — they qualify only via their own group memberships. Anonymous users see no behaviour change. Cascade: schema → register → IAppConfig openregister.rbac.inherit_from_public_default → hard-coded true. Implementation touches both RBAC layers identically: - PHP-side PermissionHandler::hasPermission inheritance fallback (line 229-241) - SQL-side MagicRbacHandler::processConditionalRule + processSimpleRule (and their UNION-mode siblings buildRbacConditionsSql + processConditionalRuleSql) Modified capability: rbac-scopes. Tracks GitHub issue #1439. * feat(rbac): implement inheritFromPublic flag (#1439) Adds an opt-out for the implicit "logged-in users inherit public group rights" semantics. Schemas (and registers, via cascade) gain an optional inheritFromPublic boolean, default true (preserves pre-change behaviour). Cascade: schema.authorization.inheritFromPublic → register.authorization.inheritFromPublic → IAppConfig openregister.rbac.inherit_from_public_default → hard-coded true null is treated as "unset" — cascade falls through. PermissionHandler: - new constructor dep IAppConfig - new public resolveInheritFromPublic(Schema): bool with per-request cache - hasPermission line 229-241 inheritance fallback now gated on the flag MagicRbacHandler: - resolveInheritFromPublic(Schema) helper delegating to PermissionHandler via existing container DI - applyRbacFilters resolves the flag once at the top, plumbs through processAuthorizationRule → processConditionalRule + processSimpleRule - same plumbing in the UNION-mode path: buildRbacConditionsSql → processAuthorizationRuleSql → processConditionalRuleSql, and the shared processSimpleRule - the per-object hasPermission method (separate from PermissionHandler's) also gated identically Behaviour: - inheritFromPublic = true (default): unchanged from pre-change. - inheritFromPublic = false + anonymous user: still qualifies for public. - inheritFromPublic = false + authenticated user: does NOT qualify for public rules; only own-group / owner / admin grants apply. Tests: - new PermissionHandlerInheritFromPublicTest covers cascade resolution (4 levels + null=unset semantics) and the four-state matrix on hasPermission, plus owner/admin shortcut invariance. - existing PermissionHandlerRbacTest updated for new constructor sig. Quality: - PHPCS clean on touched files (auto-fix + manual passes). - PHPStan clean. - Psalm clean. - openspec validate clean. Deferred (tracked in tasks.md as not-yet-checked): - 3.6, 3.7: SQL-side unit tests (need fixture DB). - 6.x: cross-app smoke tests against running stacks. - 7.3-7.5: integration tests against running services. - 8.1, 8.2: docs extension + worked example. - 9.1, 9.4: full unit suite (PHPUnit needs the NC docker bootstrap) + manual live-stack smoke. Closes (partially) #1439. * feat(rbac): expose inheritFromPublicDefault in settings UI (#1439) Surfaces the tenant-wide `rbac.inherit_from_public_default` IAppConfig key through the existing settings payload as `rbac.inheritFromPublicDefault`, and renders a toggle for it in the RBAC configuration section. Also updates the rbacOptions store default so the switch hydrates correctly on first load. Pairs with the schema-level checkbox in @conduction/nextcloud-vue (CnSchemaSecurityTab). Backend cascade was added in 3c05b62. * fix(rbac): wire inheritFromPublic through dedicated endpoint and validator (#1439) Two gaps surfaced during /opsx:verify against the live stack: 1. Schema::validateAuthorizationRules rejected `inheritFromPublic` because it only allowed CRUD action keys. The validator now treats it as an optional sibling of the action keys and verifies it is a boolean (or null = unset). 2. ConfigurationSettingsHandler::getRbacSettingsOnly / updateRbacSettingsOnly handle the dedicated `/api/settings/rbac` endpoint that the frontend store actually calls. They now read and write the `rbac.inherit_from_public_default` IAppConfig key, matching the unified `getSettings` / `updateSettings` paths added earlier. Verified end-to-end via the four-state matrix on /api/objects: with inheritFromPublic=true (default) anon and authenticated users both see public-conditional rows; with false set per-schema, anon still sees them but authenticated users without explicit group membership do not. * docs(rbac): mark live-stack smoke checks done in tasks.md (#1439) Verified manually against the Docker NC stack: - /api/settings/rbac round-trip (read + write) for inheritFromPublicDefault - schema-level inheritFromPublic accepted by validator and round-trips through /api/schemas/{id} - four-state matrix on /api/objects: (anon|auth) × (true|false) yields counts that match spec — only (auth, false) is denied; the other three states see the public-match objects Tasks 6.1, 6.2, 9.4 set to done. Remaining open items are unit-test extensions (3.6, 3.7, 7.x), additional docs (5.2, 8.1, 8.2), the broader suite run (9.1), and the Softwarecatalog smoke (6.3). * test(rbac): add SQL-side inheritFromPublic matrix tests (#1439) Adds tests/Unit/Db/MagicMapper/MagicRbacHandlerInheritFromPublicTest.php covering buildRbacConditionsSql and applyRbacFilters under the four-state matrix (anon|auth × inheritFromPublic true|false), plus parity checks for the simple-string `'public'` rule, the `'authenticated'` rule, and admin bypass. 10 new tests, all green via the in-container PHPUnit runner. Also fixes a regression in the existing PermissionHandlerRbacTest where buildHandlerWithRealMatcher() didn't pass the new IAppConfig dependency into PermissionHandler, causing 10 errors during the full suite run. Knocks out tasks 3.6, 3.7, 7.1, 7.2, 7.3, 7.4, 7.5, 9.1 in the change's tasks.md — the SQL-side matrix is now unit-tested and the integration scenarios are covered by either the cascade unit tests (cascade fall- through paths) or the live-stack matrix run during /opsx:verify. * docs(rbac): document inheritFromPublic flag and tenant default (#1439) Extends docs/Features/access-control.md with: - The optional `inheritFromPublic` boolean on the schema authorization JSON example - A new section "Disabling public-group inheritance for authenticated users" covering the cascade (schema → register → IAppConfig → true), the four-state matrix, a worked publication-style example, and the `'authenticated'` simple-rule alternative - The new `inheritFromPublicDefault` field in the RBAC Configuration block, including the IAppConfig key and the occ command Also cross-references the new flag from the `"public"` row of the rule table in docs/Features/property-authorization.md, since the previous phrasing ("matches any authenticated user") was unconditional. Closes tasks 5.2, 8.1, 8.2 in the rbac-disable-public-inheritance change. * Fix composer vulnerability * Updated package * fix(rbac): address PR #1440 review — strict bools, no fail-open, CSRF (#1439) Addresses the blocker + 3 concerns + 1 minor flagged in WilcoLouwerse's strict review of PR #1440. 🔴 Blocker — drop the silent fail-open in MagicRbacHandler::resolveInheritFromPublic. The previous try/catch returned `true` on any Throwable from the cascade walk, which silently undid the gate the tenant explicitly opted out of and let this SQL path diverge from the PHP per-object check (which propagates). Spec invariant "per-object checks and listing membership cannot drift" now holds even under failure: the request fails (5xx) instead of leaking rows. 🟡 Concern 1 — strict-boolean check at schema/register cascade levels. PHP's `(bool) "false"` is `true`, so a register persisted via direct mapper write / migration / seed JSON could store a string and silently invert the gate. Both schema (line 716) and register (line 728) now require literal `true` or `false`; anything else (string, int, etc.) is treated as "unset" and logged as a warning. Three new cascade tests pin the strict-equality contract. 🟡 Concern 2 — strict normalization on the API write paths. `updateRbacSettingsOnly` and `updateSettings` now use `filter_var` with `FILTER_VALIDATE_BOOLEAN | FILTER_NULL_ON_FAILURE` (matching the docs' boolean-tolerance claim) and throw on garbage rather than silently coercing `(bool) "false" === true`. Three new tests pin the normalize-and-persist contract (real bool, "false" string, garbage rejection). 🟡 Concern 3 — drop @NoCSRFRequired from updateRbacSettings. This endpoint is now security-load-bearing (it flips a tenant-wide RBAC default); CSRF protection on state-mutating admin endpoints is required by ADR-005. Frontend uses @nextcloud/axios which sends the request token automatically; no UI change needed. 🟢 Minor — docblock note on resolveInheritFromPublic about transient schemas (no-cache path) so future readers don't expect cache hits on in-memory drafts. Tasks 6.1 (DocuDesk smoke) re-opened — the previous justification was a settings-endpoint round-trip, not a behavioural exercise of DocuDesk's consent-fetch endpoint. Honest accounting per reviewer's note. Tests: 71 RBAC + 7 settings (was 68 + 4) — all green via the in-container PHPUnit runner. * chore(rbac): phpcs auto-fix in PermissionHandler — docblock spacing (#1439) Five auto-fixable violations the local cached run missed: - 2× "Expected 1 blank line after function; 2 found" between the `coerceStrictBoolOrLog` helper and its neighbours - 3× parameter-type alignment in the helper's @PARAM block Picked up by composer phpcs (CI scope: lib/) on PR #1441. * fix(beta): remove the orphaned inheritFromPublicDefault RBAC control Defect introduced by the development->beta sync (#2636), found by checking CONTENT rather than files. Old beta carried `inheritFromPublicDefault` in four places: two backend (ConfigurationSettingsController, ConfigurationSettingsHandler) and two frontend (store/settings.js, RbacConfiguration.vue). The sync resolved the two BACKEND files to development's version — development grafted `inheritFromPublic` but deliberately never took the `Default` setting (0 references) — while the two FRONTEND files merged cleanly and kept beta's. The result was a settings UI bound to a backend that no longer exists: a checkbox on `rbacOptions.inheritFromPublicDefault`, defaulted true in the store, in the RBAC settings panel — a silent no-op control in a SECURITY surface, where an admin would believe they had changed an access-control default and nothing would persist. This aligns the two frontend files with development, which is what the rest of the sync did. Beta is now internally consistent: zero references to `inheritFromPublicDefault` in lib/ or src/, matching development. Note for the record: the file-level check I ran after the sync reported "0 files lost" and was true but insufficient — no file disappeared, content inside resolved files did. Content-level verification is what caught this. * chore(release): 1.1.6-beta.20260820205738 [skip ci] (#2641) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --------- Co-authored-by: Robert Zondervan <robert@conduction.nl> Co-authored-by: Remko <remko@conduction.nl> Co-authored-by: Conduction Release Bot <release-bot@conduction.nl> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
#3074 carried beta's CONTENT into development but was squashed, so git never recorded beta as an ancestor. The merge base did not move, both branches still counted as having changed the version independently, and the promotion PR (#2976) stayed 'dirty' with development 21 commits behind. This is the same merge as a real merge commit, so the ancestry is recorded and development -> beta becomes a clean promotion. Content is already in place from #3074, so this changes almost nothing on disk; the version conflict resolves to development's 1.1.8-unstable, the higher of the two, as before. A sync or promotion between long-lived branches must be merged, never squashed: a squash copies the files and throws away the relationship that makes the NEXT merge clean.
…red (#3072) * fix(edepot): the transfer flow had no way to approve anything `TransferController::create()` dispatches only a list whose status is `approved`, and nothing in the fleet could set that status. `TransferListService::approveTransferList()` was implemented, carried a @SPEC tag for edepot-transfer, and had zero callers — in this repo or in any sibling app. `rejectTransferList()` was in the same state. So a transfer list could be built and then never moved, and the capability the spec requires was unreachable in practice. The service was ALREADY imported by the controller. The wiring had been started and left unfinished, which is why nothing looked obviously missing. Both decisions now have a route and a controller method. They share one private `decide()` because they share their entire shape — load, refuse if absent, refuse if the status forbids it, stamp who decided — and two copies of that would drift. A rejection REQUIRES a reason. An archivist refusing a transfer is a records-management decision somebody has to justify later, so an empty reason is a 400 rather than a silently empty field. The service's `InvalidArgumentException` maps to 409, not 500: "this list is not in review" is a state the caller can see and act on, not a server fault. Five tests. Stamping a fixed archivist instead of the acting user turns two of them red. * fix(audit): remove the two methods that could delete an audit trail `LogService::deleteLog()` and `deleteLogs()` call `auditTrailMapper->delete($log)`. The audit-trail specification contains a requirement titled, in as many words: ### Requirement: Audit trail entries MUST NOT be deletable or modifiable with cryptographic hash chaining, a ten-year minimum retention, and the note that 56% of analysed government tenders require exactly this. The controller enforces it: `auditTrail#destroy` and `auditTrail#destroyMultiple` are routed and return HTTP 405 "Audit trail entries cannot be deleted" unconditionally. So these were not a capability waiting to be wired. They were the opposite of one: an implementation of something the specification forbids, sitting unreferenced next to a hash chain they would silently break. gate-57 read them as "orphaned write capability" — accurate, but the remedy is removal, not a route. Their only @SPEC pointed at a retrofit CHANGE rather than a spec (`changes/retrofit-2026-05-25-bw2-svc-flat-2`), which is the shape of a legacy annotation rather than a live requirement. `clearAll` is untouched: it purges EXPIRED entries via `AuditTrailMapper::clearAllLogs()`, which is retention, not deletion of arbitrary history. Five tests went with them. Tests for a capability the spec forbids were asserting that the forbidden thing works. * fix(files,skos): give two specified capabilities a way in **File description and category had no surface at all.** `FileMapper::setDescriptionForFile()` and `setCategoryForFile()` are reached only from `UpdateFileHandler::updateFileMetadata()`, and nothing called that. Labels DID have a route and worked, which is exactly why the gap was easy to miss: the feature looked present because a third of it was. `PUT .../files/{fileId}/metadata` now reaches all three, behind the same object-level RBAC `updateLabels` uses — descriptive metadata is no less a mutation for being descriptive. Each field is skipped when absent and cleared when empty, so a caller can change one without wiping the others; that is the handler's contract, and `array_key_exists` is what preserves it where `??` would make an empty description impossible to send. The controller reaches the handler through FileService, the way it already does for `updateFile()`, rather than being handed the handler itself. **SKOS CSV import had no caller.** `VocabularyImportService::importCsvValueList()` is specified by `skos-concept-registers` and was reachable from nothing. It now has `occ openregister:vocabulary:import-csv`. A COMMAND rather than a route, deliberately: the service takes a filesystem path, which is meaningless across an HTTP boundary — the file is on the server, not in the caller's request. Exposing it as an endpoint would mean either an upload surface nobody asked for, or accepting a server-side path from a client, which is a traversal invitation. The command prints created/updated/unchanged/deprecated rather than "done", because an idempotent import's whole point is that the second run changes nothing, and that is only visible if the numbers are shown. * fix: catch the exception this controller actually raises, and cover the new endpoint Two CI failures on #3072, both mine. **psalm**: `updateMetadata()` caught `NotPermittedException`, which is not imported in this controller and does not exist in its namespace. I mirrored `updateLabels()`'s structure without mirroring what it CATCHES — it uses `\OCA\OpenRegister\Exception\NotAuthorizedException`. phpstan passed it because the class name resolved to a plausible-looking FQN; psalm asked whether that class exists. It does not. **gate-25 contract-coverage**: `files#updateMetadata` was a new public endpoint with no contract test. `transfer#approve` and `transfer#reject` passed the same gate because they already had tests — which is the gate working exactly as intended. Four tests now cover it, and two of them pin the contract that is easiest to break: absent means "leave this field alone", empty string means "clear it". Collapsing those with `??` would make an empty description impossible to send and would silently wipe fields a caller never mentioned. Verified against CI's real scoping (`--scope-to-diff`, which is how the workflow invokes it, and not what a bare local run does): ALL 37 APPLICABLE GATES PASS.
chore(release): merge beta history into development
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 172/172 | |||
| npm | ✅ | ✅ 547/547 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-29 21:21 UTC
Download the full PDF report from the workflow artifacts.
* fix(flow): delete a flow's version rows with the flow `FlowService::delete()` already cascades to trigger rows, runs, steps and state, each with a comment explaining why an orphan there is landfill. I added `openregister_flow_versions` and did not extend that cascade, so every deleted flow left its version rows behind. Measured on the dev instance: **38 orphans**, unreachable by any read path the app has, since every version read is by flow. Safe precisely BECAUSE the runs are deleted first. A version row exists so an in-flight run can resolve the graph it was pinned to — and this method has just removed every run of this flow, so nothing can still be pinned. Confirmed on the instance: zero active runs pinned to a version whose flow is already gone. `openregister_flow_defs` is deliberately NOT touched. It is content-addressed and SHARED — two flows holding the same graph share one row, and 219 flows deduplicated to 24 definitions here — so deleting by flow would pull a definition out from under an unrelated flow's version. Verified against a live instance rather than reasoned about: create a flow, publish it (1 version row), delete it, 0 version rows. * test(flow): cover the version cascade the ratchet caught The coverage guard failed #3080: 60.10% head against 61.11% base, "this change adds 5 statements to those files". It was right — I added `deleteByFlow()` and its call site with no test for either. Two mapper tests, and one that makes the CASCADE test actually assert the new cascade member. That last one needed a fix to the harness: its container double returned the organisation stub for EVERY `get()`, so the version cascade ran, threw "no such method", and was swallowed by `delete()`'s own try/catch — the line executed and asserted nothing. The container now answers by class. Both mutation-checked: - dropping the `WHERE` from `deleteByFlow()` — which would delete every flow's versions and strand every in-flight run on the instance — turns the mapper test red; - removing the cascade line from `delete()` turns the cascade test red. 733 flow tests green.
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 172/172 | |||
| npm | ✅ | ✅ 547/547 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-29 21:46 UTC
Download the full PDF report from the workflow artifacts.
…#3082) Supersedes #2992 and #2984. Both dependabot PRs failed 15/14 checks for one reason that had nothing to do with CSS: `npm ci` could not resolve, so "Install dependencies" failed and every frontend job — stylelint, eslint, build, unit tests, check:specs, the npm license and security legs — died before running. The stylelint step itself was SKIPPED, not red. @nextcloud/stylelint-config@3.2.2 peer-requires stylelint ^17.9.1 and stylelint-config-recommended-scss ^17.0.1, but both were pinned as root devDependencies at ^15.11.0 / ^13.1.0, and stylelint was additionally held at ^15.11.0 by an `overrides` entry. Bumping one package alone can never resolve; the stack has to move together: @nextcloud/stylelint-config ^2.4.0 -> ^3.2.2 stylelint ^15.11.0 -> ^17.9.1 (devDep + overrides) stylelint-config-recommended-scss ^13.1.0 -> ^17.0.1 stylelint-webpack-plugin ^4.1.1 -> ^5.1.0 stylelint-webpack-plugin@4 peer-caps stylelint at ^15. It is declared but never imported (no reference in webpack.config.js or anywhere else), so this is a peer-range fix only, with no build behaviour attached. With install fixed, stylelint actually ran and reported 233 errors. 213 were a single dead config entry: `indentation: null` in stylelint.config.js. Stylelint REMOVED that rule in v16, and from v17 a removed rule name is an "Unknown rule" error even when set to null. The comment already sitting above it predicted this ("removed in 16, so it is on its way out regardless"). The entry is deleted, not the enforcement — prettier owns indentation via the `format` script, whose glob is wider than stylelint's. The remaining 20 errors were genuinely deprecated CSS, fixed rather than disabled, and all rendering-neutral: 14x word-break: break-word -> overflow-wrap: break-word 2x word-wrap: break-word -> overflow-wrap: break-word 1x word-wrap: normal -> overflow-wrap: normal 1x grid-gap -> gap 1x clip: rect(0 0 0 0) -> clip-path: inset(50%) 1x -webkit-box-align: end -> deleted `word-break: break-word` is defined by spec as equivalent to `overflow-wrap: break-word`, `word-wrap` is the legacy alias of `overflow-wrap`, and `grid-gap` is an exact alias of `gap`. No site had a competing overflow-wrap declaration. `clip: rect(0 0 0 0)` is the classic visually-hidden idiom and `clip-path: inset(50%)` is its modern equivalent, so the screen-reader-only class keeps behaving identically. The -webkit-box-align case is the one where stylelint's suggestion is wrong for this code. It advises rewriting to `align-items`, but the rule is `display: flex` and already carries `align-items: center` on the line above; `-webkit-box-align` belongs to the 2009 flexbox draft and only applies under `display: -webkit-box`, so it is inert here. Following the suggestion would have set alignment to `end` and visibly moved the content. Deleting the dead prefixed property is the rendering-neutral fix. Not adopted: stylelint-config-recommended-vue stays at ^1.6.1, so #2984 is NOT included. @nextcloud/stylelint-config@3.2.2 peer-requires recommended-vue ^1.6.1, and recommended-vue@2.0.0 requires postcss-html ^2.0.0. Bumping recommended-vue alone is a hard ERESOLVE; bumping postcss-html alongside it only "resolves" because npm overrides the root's ^2.0.0 back down to 1.6.1 to satisfy the @nextcloud peer, which would leave package.json claiming a version the tree does not use. #2984 is blocked upstream until @nextcloud/stylelint-config widens that peer range. Left alone deliberately: 184 csstools/use-logical WARNINGS, a new rule in the v3 config flagging physical properties (border-left, margin-left, text-align: left) that have logical equivalents. They are warnings, CI runs `npm run stylelint` with no --max-warnings, and the run exits 0. Converting them is a real RTL behaviour change, not a lint fix, so it belongs in its own change. Verified locally: npm ci 0, npx stylelint 0 (0 errors, 184 warnings), npm test 0 (35 suites, 304 tests), npm run build 0, npm run lint 0, npm run format 0.
…de existed (#3083) #3080 made `FlowService::delete()` cascade to `openregister_flow_versions`, so no NEW orphan can appear. It did nothing about the rows already stranded: any instance that deleted a flow between the version table landing (#3047) and that cascade landing kept them, unreachable through any read path because every version read is keyed by flow. Measured on the dev instance: 38, and the count grows with every flow anyone removes. The existing `BackfillFlowVersions` repair step already runs on install and upgrade, so the sweep goes there rather than into a new step nobody would think to invoke. Safe on the same terms as the cascade: `delete()` removes a flow's RUNS too, so a version row whose flow is gone has no run left that could be pinned to it. Verified on the instance rather than reasoned about: before: 38 orphans occ maintenance:repair - Flow versions: removed 38 orphaned version row(s). after: 0 orphans · 196 flows · 196 version rows · 0 unversioned non-drafts Dropping the `notIn` predicate — which would empty the table and strand every in-flight run on the instance — turns the new test red.
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ❌ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ❌ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 172/172 | |||
| npm | ✅ | ✅ 547/547 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-29 21:50 UTC
Download the full PDF report from the workflow artifacts.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 172/172 | |||
| npm | ✅ | ✅ 547/547 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-29 22:01 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 172/172 | |||
| npm | ✅ | ✅ 547/547 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-29 22:16 UTC
Download the full PDF report from the workflow artifacts.
Automated PR to sync development changes to beta for beta release.
Merging this PR will trigger the beta release workflow.