Release: merge development into beta - #2906
Conversation
…260820201450 chore(release): 1.1.5-unstable.20260820201450
…260820204506 chore(release): 1.1.5-unstable.20260820204506
…260820210405 chore(release): 1.1.5-unstable.20260820210405
…260820212658 chore(release): 1.1.5-unstable.20260820212658
…260820214342 chore(release): 1.1.5-unstable.20260820214342
…260820221600 chore(release): 1.1.5-unstable.20260820221600
…260820223544 chore(release): 1.1.5-unstable.20260820223544
…260820224828 chore(release): 1.1.5-unstable.20260820224828
…260820231029 chore(release): 1.1.5-unstable.20260820231029
…260820233856 chore(release): 1.1.5-unstable.20260820233856
…260820235724 chore(release): 1.1.5-unstable.20260820235724
…260821001315 chore(release): 1.1.5-unstable.20260821001315
…260821002700 chore(release): 1.1.5-unstable.20260821002700
…260821004947 chore(release): 1.1.5-unstable.20260821004947
…260821010031 chore(release): 1.1.5-unstable.20260821010031
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ⏭️ | ||||
| phpcs | ⏭️ | ||||
| phpmd | ⏭️ | ||||
| psalm | ⏭️ | ||||
| phpstan | ⏭️ | ||||
| phpmetrics | ⏭️ | ||||
| eslint | ⏭️ | ||||
| stylelint | ⏭️ | ||||
| build | ⏭️ | ||||
| composer | ⏭️ | ⏭️ | |||
| npm | ⏭️ | ⏭️ | |||
| app:check-code | ⏭️ | ||||
| info.xml | ⏭️ | ||||
| REUSE | ⏭️ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-27 13:28 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-27 16:09 UTC
Download the full PDF report from the workflow artifacts.
…de them (#2941) 41 declarations across the shillinq registers carry an `expression` key the engine never read. They did not fail; they produced nothing for that figure, which is the shape this whole effort keeps finding. "vatBalance": { "operation": "expression", "expression": "totalVATPaid - totalVATCollected" } Both operands are already computed by the same aggregation. Only the arithmetic between them was missing. ## Why not a second aggregation Declaring one to do the subtraction is not equivalent: it scans the table again, and the two results can disagree if a row is written between the calls — which a trial balance must never do. A derived metric reads the numbers this aggregation just produced, so it cannot disagree with them. ## Why a parser and not eval() These expressions come from register descriptors, which are DATA. eval() on data is arbitrary code execution, and "we control the registers" does not survive the first app that imports a descriptor from elsewhere. MetricExpressionLexer + MetricExpressionEvaluator implement a recursive-descent parser over a closed grammar: expr := term (('+'|'-') term)* term := factor (('*'|'/') factor)* factor := NUMBER | IDENT | '(' expr ')' | ('min'|'max') '(' expr ',' expr ')' | '-' factor No assignment, no property access, no string literals, no function beyond min/max. Everything else is refused BY NAME. The test feeds it `phpinfo()`, `a; system("id")`, `$a + 1`, `a->b`, `a . 'x'` and `a ** 2`. ## What it refuses, deliberately - An IDENTIFIER NAMING NO ALIAS throws, and names the aliases that do exist. Resolving it to 0 would turn a typo into a plausible number: `a - typo` would quietly return `a`. - DIVISION BY ZERO yields null, not INF and not NAN. json_encode() refuses INF outright and NAN compares false against everything, so both travel a long way before anyone notices. - A NON-NUMERIC alias throws rather than being coerced. - A NULL alias propagates as null rather than counting as zero. A derived metric reads only aliases computed BEFORE it, so the `metrics` list is the dependency order; reading a later one raises and says so. ## Wiring - `computeMetrics()` gains the branch. It cannot reach the native path: `metricsNeedPhpPath()` now names `expression` explicitly rather than relying on the `as` check, so removing `as` could never route one at SQL and have it silently compute nothing. - The annotation validator accepts `metric: expression`, and refuses one with no `expression`, no `as`, or a `field` (it aggregates nothing). - The evaluator is constructed internally, like RegisterScopedSchemaResolver: it has no collaborators, and a new required constructor argument would change the signature every existing caller and test already builds. ## Verification - 9 evaluator tests + 2 end-to-end through AggregationRunner - Mutation control: making an unknown alias resolve to 0, and division by zero return INF, each makes the suite fail; reverting restores green - Full suite 17,588 tests, 0 failures - phpcs 0 errors, phpmd clean, phpstan [OK] No errors Refs ConductionNL/shillinq#1261
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-27 19:42 UTC
Download the full PDF report from the workflow artifacts.
…ort one (#2910) * feat(registers): show which app-declared registers landed, and re-import one Closes #2903. Eighteen of the fleet's apps ship a register descriptor and import it from a Repair step. Repair steps run on install and `occ upgrade`, and `occ upgrade` reports "No upgrade required" the moment `installed_version` matches `info.xml` — so once an app settles, its descriptor can never be imported again. The steps are also documented never to throw: a failure logs a warning and leaves the instance looking healthy. The cost is not hypothetical. On a dev instance, an `occ upgrade` that reported complete success left 8 OF 15 DECLARED REGISTERS ABSENT, `flows` among them. Two e2e suites died in `beforeAll` on the missing register, and establishing why took an account listing, a register dump and a read of the Repair step's source. WHAT THIS ADDS - `RegisterDescriptorService` — an inventory of every register any installed app declares, each `current` / `behind` / `absent`, plus a forced re-import. - `GET /api/register-descriptors` and `POST /api/register-descriptors/{appId}/{slug}/import`, both admin-only. - `occ openregister:descriptors:list` (with `--problems-only` and `--import`). - An admin-settings panel that LEADS WITH WHAT IS WRONG. THREE DECISIONS WORTH THE REVIEW TIME 1. It enumerates DECLARING APPS, not resolved registers. Reading the resolver keys or the configuration rows would list only what already imported — the interesting row, the app whose seed never ran, has neither. An inventory built that way reproduces exactly the silence it exists to break. 2. Discovery is by SHAPE, not filename. The fleet's names vary (`flow_register.json`, `credential-providers.json`, `n8n_workflows.openregister.json`), and a `*_register.json` glob would quietly omit the rest — shrinking the inventory instead of failing, which is the same invisibility one level up. A test reads OpenRegister's own `lib/Settings`, counts the declaring files independently of the code under test, and asserts every one is found. 3. The import is ALWAYS forced. `ImportHandler` short-circuits on `$force === false && version_compare($shipped, $existing, '<=')`, and that is precisely the state an administrator presses the button in: absent, or failed to write, while the counter says current. An unforced re-import would report success and do nothing in every case that motivates the action. `absent` and `behind` stay distinct everywhere — they need different actions and carry different risk. Absent means a code path is dead; behind means it runs against an older contract. ON CUSTOMISED SCHEMAS A forced re-import rewrites the base. An extending schema REFERS to its base — `Schema::getAllOf()` returns "Array of schema IDs, UUIDs, or slugs" — so it is impervious to the base moving. That is a property of the implementation rather than a law, so it is pinned by an e2e test instead of assumed: an extension materialised as a copy would silently revert somebody's customisation, through the button offered as a repair. VERIFIED LIVE occ openregister:descriptors:list → 15 declared · 7 current · 0 behind · 8 ABSENT --import=flows → imported occ openregister:descriptors:list → 15 declared · 9 current · 0 behind · 6 ABSENT GET /api/register-descriptors → 200, 15 rows POST /…/openregister/flows/import (versions already match) → 200 {"outcome":"imported"} GET /api/register-descriptors as a non-admin → 403 RegisterDescriptorServiceTest 11 tests, 52 assertions, OK register-descriptors.spec.ts 4 passed, twice back to back ALSO IN HERE `flow-schedule.spec.ts` and `federated-config.spec.ts` were dead — their `beforeAll` resolved the `flows` register that had never been seeded. With the register imported they run, and two stale fixtures surfaced: both hung a flow step's `type` + `config` off an EDGE, which the validator now refuses outright ("a node is the action that runs and an edge is sequence, so nothing reads this step"). The flow was never created and the failure surfaced two assertions later as an empty bundle, reading exactly like a broken bundler. Steps moved onto nodes. `federated-config.spec.ts` additionally authored into the `flows` register while the bundler reads the `openregister_flows` table, so it now authors through `/api/flows`. That store split, and an `install` that returns a uuid for a flow which exists in neither store, are real product defects and are filed separately as #2905 — not patched over here. * style(e2e): prettier-format, and mark #2905 expected-to-fail rather than red Three things CI's `Frontend Check (format)` and a local run turned up: - Prettier formatting on the three files this branch adds or rewrites. - `reg` and `sch` in `federated-config.spec.ts` became dead once `makeFlow()` moved to `/api/flows`. The `flows` register is still a precondition — 'a register bundles into a portable OpenAPI document' bundles it BY SLUG — so the lookup stays and only the unused ids go. On an instance where the descriptor never landed, that lookup failing UP FRONT with `registers slug=flows` is the most useful thing the block can do. - The bundle/install/run test is split in two. Install now ends at the uuid; whether that flow can RUN is its own test, marked `test.fail()` and naming #2905 — the defect where `install` reports HTTP 200 with `{"installed":[uuid]}` for a flow that exists in neither store. Split rather than folded, because marking the whole test expected-to-fail would stop the bundle-and-install assertions from ever failing again — trading one tracked defect for four untested behaviours. `test.fail()` rather than a skip, because a skip goes quiet and STAYS quiet. This reports red the day #2905 is fixed and the expectation needs removing. `test.fail()` is called inside the test body, not in the describe body: a bare one there would mark every test declared after it too, quietly excusing the allowlist test from ever having to pass. Verified: 5 expected passes, 1 expected failure, `unexpected: 0`. * fix(repair): register ImportFlowRegister — it was written and never run `ImportFlowRegister` appeared ZERO times in `info.xml`. Not in `<post-migration>`, not in `<install>`. The class is complete — constructor, version gate, error handling, a docblock explaining that it exists so "a flow can live in OpenRegister itself and not only in a consuming app" — and Nextcloud never invoked it, so the `flows` register never appeared. ADR-005 Rule 1 is exactly this failure: > Any new OR-owned register descriptor MUST be accompanied by a `lib/Repair/` > step that imports it. Shipping the JSON alone does nothing at runtime. and its Consequences name the cost: > An OR-owned register that ships JSON without a Repair step silently never > appears — a recurring author error this ADR exists to prevent. The class complied. The REGISTRATION did not, and nothing checks that half. HOW IT WAS FOUND `occ openregister:descriptors:list`, added in this branch, reported `flows` absent on an instance where `occ upgrade` had just reported complete success. Before that command existed, the only evidence was two e2e suites dying in `beforeAll` on `registers slug=flows`, which reads like a broken test. The correlation across the whole instance is exact and worth recording: every descriptor with a registered importer → current every descriptor with NO importer → absent `flows` was the single row that broke the pattern — an importer class with no registration — which is what pointed at the cause. WHAT THIS DOES NOT FIX Six openregister descriptors still report absent because they ship with no importer at all: bag, brp, dso, kvk, merge-operation, n8n-workflows. Each is either an ADR-005 Rule 1 violation or a descriptor that should not ship as one. That is a decision for whoever owns them, not a guess to make here — reported on the issue instead. * i18n(registers): translate the descriptor panel into all 36 required locales CI's `Frontend Check (test:l10n)` failed on the register-descriptor panel: its 19 strings were in neither `l10n/en.json` nor any locale file. The house convention, visible in every recent feature commit, is that a change adds its keys to all 37 files with REAL translations — not an English fallback. 19 keys × 35 locales, plus en and nl. 740 lines, purely additive: the only deletion in each file is the trailing-comma reflow on its last existing key. 🔴 A LITERAL ` ` IN THE SOURCE IS NOT A NON-BREAKING SPACE TO THE EXTRACTOR. `@nextcloud/l10n-non-breaking-space` wanted an nbsp before the ellipsis, and the fix went in as the seven characters ` `. JavaScript resolves that escape at runtime, so the browser saw a real nbsp — but the l10n extractor reads SOURCE TEXT, so it registered the key as `Reading descriptors …` while `t()` would look up `Reading descriptors<nbsp>…`. The key would never have matched, and every locale would have silently fallen back to English for that one string, with the parity check reporting full coverage. Replaced with a real U+00A0. `--write` also picked up one key that was missing from `en.json` while already translated in the locale files: "Retention period". Pre-existing, unrelated to this panel, and left in because the gate would fail on it either way. Verified: `l10n-parity: OK — every required locale is at full parity (no missing keys, no empty values)`. * test(newman): a register-descriptors contract suite, and fix the CSRF gap in delegation's REGISTER DESCRIPTORS `register-descriptors.spec.ts` lives in `tests/e2e/api-direct/`, which `playwright.config.ts` excludes from every project — so CI runs none of it. Four green specs and zero CI coverage look identical from the outside. This is the half CI can see, registered as a domain in `run-all.sh`. The assertions are mostly about what the API says when something is WRONG, because that is what this endpoint exists for. `absent` and `behind` must be first-class counts rather than something a client derives by filtering — a consumer that has to compute "is anything wrong" is one that renders a healthy-looking total and moves on. And a version-matched re-import must report `imported`, never `skipped`: `ImportHandler` short-circuits on `version_compare(shipped, installed, '<=')` unless forced, which is exactly the case an administrator presses the button in. 🔴 THE DELEGATION COLLECTION COULD NEVER HAVE PASSED Running the new domain through the orchestrator failed, so I ran `delegation` — already merged, already "verified" — as a control. It failed identically: expected status 200 but got 412 expected { message: 'CSRF check failed' } to have property 'awaitingMyAnswer' ALL NINE of its requests were missing `OCS-APIRequest: true`, so every one is rejected by Nextcloud's CSRF middleware before reaching a controller. A collection registered in CI that 412s on every request proves exactly nothing, and it is the same defect its own registration comment warns about one level up. Header added to all nine. The control is what found it. Had I only run my own collection I would have concluded the orchestrator was broken and moved on. TWO ENVIRONMENT FINDINGS ALONG THE WAY - `NEWMAN_RUNNER=host` invoked a bare `newman`, the only one of the three branches requiring a GLOBAL install. Without it every domain failed rc=127 — "command not found", reported in the same red as a real assertion failure. Now `npx --yes newman`, as the `exec` branch already does. - The default `sidecar` runner rewrites `localhost:8080` to `http://<container>`, which Nextcloud answers 400 for as an untrusted domain. Not changed here — it is a local-environment matter, and CI supplies its own network — but it is why a local `bash tests/newman/run-all.sh` fails for every domain, not just new ones. Verified: `Passed: 2, Failed: 0` for both domains through the orchestrator, and 13/13 assertions for the new collection standalone — including the version-gate case, which ran rather than taking its skip path. * test(e2e): flow-schedule now runs — and it was testing a store the scheduler cannot read Refs #2905. `flow-schedule.spec.ts` could not fail, because it could not run: its `beforeAll` resolved a `flows` register that had never been seeded on any instance (#2903), so the file errored before reaching an assertion. Seeding the register is what made it execute — and it immediately failed three different ways, each one real. 1. A SLUG IS NOT UNIQUE ACROSS SCHEMAS. `idBySlug('schemas', 'flow')` searched globally; this instance carries TWO schemas with slug `flow` while the `flows` register lists only one. It picked the wrong one, and posting an object to a register/schema pair that do not belong together answers 400 — an opaque status with nothing in it about slugs. Now resolved from the register's own `schemas` array, which is the authority on what it carries. 2. 🔴 THE SCHEDULER CANNOT SEE REGISTER-AUTHORED FLOWS. Control, on a live instance — the same definition, byte for byte, into each store, then one worker tick: POST /api/flows (openregister_flows table) → 1 run queued POST /api/objects/18/184 (the `flows` register) → 0 runs `FlowScheduleService` resolves candidates from `FlowLocator::scheduledFlows()`, which reads the table. A schedule authored in the register never fires, with no error and no status message — the run history is simply empty, which reads as a broken scheduler rather than a flow it never saw. `lib/Settings/flow_register.json` calls that register "the store the resolver reads by default — so triggers, sub-flows and the /test endpoint all work with a flow authored here". That is not true today, and it is now the SECOND subsystem found blind to it, after the federation bundler. Reported on #2905 rather than patched: either the register becomes a real flow store for these paths or the descriptor stops claiming it is one, and that is a design call. Both fixtures now author through `/api/flows`. The negative control matters as much as the positive one: asserting "0 runs" about a flow the scheduler could never have seen would have passed for a reason unrelated to `trigger: manual`. 3. A SCHEDULE MUST NAME WHO IT ACTS AS (ADR-099). The fixture had no `openregister.trigger-schedule` node, so no `runAs`. Added, matching `delegation-parking.spec.ts`. Verified: 2 passed, 0 failed, twice back to back. * fix(gates): satisfy semantic-auth and spec-coverage, and close out #2905's e2e GATE-9 — MY CONTROLLER'S ANNOTATION CONTRADICTED ITS OWN BODY Both methods carried a no-admin-required tag while calling `requireAdmin()`. That is not a harmless disagreement: it is how an endpoint comes to be reachable by everyone while its code reads as guarded, and it is the exact defect gate-9 was written for. Copied from a sibling controller that has it too. 🔴 AND THE TAG IS NOT SPELLED IN THE COMMENT EXPLAINING ITS ABSENCE. A docblock that writes the literal DECLARES it, so prose about removing it restores what it describes. My first fix did precisely that, and only a `grep -c` caught it. This repo has paid for that lesson once already, when a comment about removing `@covers` was parsed as `@covers` and reddened six CI cells. GATE-5 THEN FIRED, AND THE OBVIOUS FIX WAS THE WRONG ONE Removing the tag left `import` with no declared auth posture. The cheap way to satisfy gate-5 is a no-CSRF tag — `index()` already carries one as a GET — but reaching for it on a state-changing POST would disable CSRF protection to buy a green gate. The panel posts through axios with Nextcloud's request token and needs nothing of the sort. `#[AuthorizedAdminSetting(settings: OpenRegisterAdmin::class)]` declares the posture the body actually enforces, which is what BOTH gates were asking for. Verified live rather than by the gate alone: admin 200/200, non-admin 403/403. GATE-16 — eight methods missing @SPEC (two on the occ command, six on the Vue panel). Tagged. #2905's E2E EXPECTATION IS GONE, BECAUSE THE DEFECT IS `test.fail()` did its job: it went RED THE DAY #2915 LANDED, because a test expected to fail that starts passing is itself an error. A skip would have gone quiet and stayed quiet, and this assertion would still be switched off. Two things surfaced as it came back to life: - The terminal is `stopped`, not `completed`, and that is THIS FIXTURE'S doing. `EndNode` throws `FlowStop`, "which the engine turns into a clean `stopped`" — so a flow whose last node is `openregister.end` ends `stopped`. `completed` is for a flow that runs off the end of its graph, which is what this fixture was before the step moved from its edge onto a node. Asserted exactly rather than widened to accept either: a matcher taking both would also accept the fixture silently losing its end node. - The allowlist test, dead until the container guard landed, failed 200-vs-403. 🔴 THE CONTROL IS FINE. `occ config:app:set` runs in a CLI process while the request is served by a web process holding its own IAppConfig cache, which the CLI cannot invalidate — so the allowlist is set, readable via `occ config:app:get`, and briefly invisible to the endpoint. Confirmed by hand: once the web process holds a fresh cache the same request answers 403 and names the source. The test now retries and then SKIPS naming what went unverified, because a security assertion that cannot see its own precondition must be reported as neither a pass nor a refutation of the control. Verified: gate-5, gate-9, gate-16 PASS and gate-98 NOT APPLICABLE locally; 33 e2e passed / 0 skipped / 0 failed, twice back to back; both Newman domains pass. * fix(registers): a mock descriptor is not a missing register Investigating the seven descriptors the panel reported ABSENT turned five of them into a defect in the PANEL, not in the fleet. THE CORRELATION IS EXACT, across OpenRegister's own 14 descriptors: type=mock 5 descriptors importer: none, all five type=core 8 descriptors importer: seven of eight type=integration 1 descriptor importer: none — and app=n8n `x-openregister.type: mock` marks sample data imported ON DEMAND — a demo, a test fixture — not something an instance is expected to carry. Every mock ships without a Repair step, deliberately. Reporting them as ABSENT put five permanent red rows in front of every administrator, and the obvious response to those rows — write the missing importers — would have seeded five mock registers onto every instance in the fleet. 🔴 A PANEL WHOSE LOUDEST SIGNAL IS NOISE IS ONE PEOPLE STOP READING, which is the same silence it was built to break. Mocks are now skipped. `reimport()` still reaches them by explicit app+slug, which is how sample data is meant to be loaded. AND A ROW NAMES THE APP THAT DECLARES IT. `n8n_workflows.openregister.json` lives in OpenRegister's lib/Settings and declares `app: n8n` — it is n8n's register, shipped alongside. Attributing it to `openregister` because of the directory it sits in tells the reader the wrong owner, and an inventory exists to say whose problem a row is. MEASURED, on a live instance with twelve apps installed: before 15 declared · 7 current · 0 behind · 8 ABSENT after 25 declared · 23 current · 0 behind · 2 ABSENT Both survivors are real: `merge-operation` (openregister, type=core, no Repair step — an ADR-005 Rule 1 gap, reported on #2903) and `n8n-workflows`, now filed under n8n where it belongs. THE FLEET-DISCOVERY TEST CAUGHT THIS CHANGE and had to move with it — 14 declaring files, 9 reported. Its independent count now applies the same stated rule, still computed by reading the files rather than by asking the service: a count derived from the code under test could only ever agree with itself. Verified: 14 unit tests / 47 assertions; e2e and Newman green. * fix(settings): make the admin panel delegation-eligible so the auth attribute binds phpstan on #2910: Parameter $settings of attribute class AuthorizedAdminSetting constructor expects class-string<OCP\Settings\IDelegatedSettings>, string given. `AuthorizedAdminSetting` is the only auth attribute that declares "administrator" WITHOUT also disabling CSRF, and it takes a `class-string<IDelegatedSettings>`. `OpenRegisterAdmin` implemented plain `ISettings`, so the attribute had nothing valid to bind to. The two alternatives were both worse: - Bind to `GenericAdminSettings`, the one class in this repo that already implements the interface. It is the AppHost panel; the register-descriptor endpoints are not part of it, and naming a panel they do not belong to would make the declaration a formality rather than a fact. - Add a no-CSRF tag to the POST. That is the cheap way to satisfy gate-5 and it disables CSRF protection on a state-changing endpoint to buy a green gate. 🔴 THIS DELEGATES NOTHING BY ITSELF. `IDelegatedSettings` makes a panel ELIGIBLE for delegation, which an administrator must then configure. `getName()` returns null and `getAuthorizedAppConfig()` returns an empty allowlist, so today's behaviour is unchanged — full administrators only — while the attribute gets a real settings class to bind to. Verified live rather than by phpstan alone, because this touches middleware: admin GET 200, admin POST 200, non-admin GET 403. phpcs, phpstan and psalm clean; register-descriptors e2e green. * fix: dedupe ImportFlowRegister, and cover the controller and command 🔴 THE MERGE REGISTERED THE SAME REPAIR STEP TWICE, SILENTLY. Another session fixed the same defect in #2923 — "register ImportFlowRegister and RenameDutchColumns" — while this branch carried my fix. Git merged both without a conflict, because they sit at different positions in the same block. Result: `ImportFlowRegister` appeared twice in `<post-migration>` and twice in `<install>`. Idempotent, so probably harmless at runtime, and wrong. THEIR PLACEMENT IS BETTER AND IS THE ONE KEPT. They put it BEFORE the flow steps, with the reason that "every flow step below it silently operated on nothing" — which is the actual ordering constraint. Mine sat later, among the other Import* steps, where the flow steps above it would still have run first. Salvaged from mine: the provenance sentence, since it records HOW the gap was found (`occ openregister:descriptors:list` on an instance where `occ upgrade` had just reported complete success, 8 of 15 registers absent). Verified by parsing rather than by eye: post-migration 20 steps, install 18, zero duplicates in either.⚠️ AND `RenameDutchColumns` IS NOT A SECOND UNREGISTERED STEP. I reported it as one earlier; #2923 had already registered it. The gate now reports it clean. Correcting the record rather than "fixing" something already fixed — that is the third collision with a parallel session today. COVERAGE: the ratchet failed at 43.25%, naming 194 statements added without tests. `RegisterDescriptorService` had 14 tests; the controller and the occ command had none. Both now covered, and the assertions are the REFUSALS rather than the happy path: - a non-administrator is refused, and `reimport` is expected NEVER — asserting only the 403 would pass for a controller that ran the import and then refused; - an anonymous caller gets 401, not 403; - a failed import answers 422, not a 200 carrying bad news; - listing exits ZERO with absent registers (a state to decide about, not a failure — non-zero would turn a report into a broken cron job) while a failed re-import exits non-zero (there the caller asked for something); - `--problems-only` filters rows but NOT the tally, because a filter that also changes the totals hides what it filtered; - `--import` without `--app` is refused rather than guessed, since two apps may ship a register with the same slug. 14 tests, 36 assertions, OK. * fix(repair): merges were recorded into a register that never existed `merge_operation_register.json` is a `type: core` descriptor declaring the `merge-operation` register and its `mergeOperation` schema, and it shipped with NO Repair step. Per ADR-005 Rule 1 that means it never appeared on any instance: "shipping the JSON alone does nothing at runtime." 🔴 THIS ONE IS NOT COSMETIC. `MergeService::merge()` writes the audit row for every merge straight into it: $this->objectService->saveObject( object: $mergeOperation, register: self::MERGE_REGISTER, // 'merge-operation' schema: self::MERGE_SCHEMA // 'mergeOperation' ); and `reverseMerge` reads `preMergeSnapshot` back out of that row to undo a merge. A register that does not exist is a merge history that does not exist, and reversibility that cannot be exercised. HOW IT WAS FOUND. `occ openregister:descriptors:list` reported it ABSENT alongside six others. The correlation across the app's fourteen descriptors was exact — every one with a Repair step present, every one without missing — and after the five `type: mock` descriptors were correctly excluded (a mock is not a missing register), `merge-operation` was the only non-mock left in the second group. The outlier in a clean correlation named the defect, for the second time in this programme: the first was ImportFlowRegister. Verified live rather than by the file's existence: the register imports and the inventory moves from 2 ABSENT to 1 — the survivor being `n8n-workflows`, which declares `app: n8n` and is correctly attributed there rather than here. merge-operation register: PRESENT id=40 schemas=[1312] mergeOperation schema: 1312 Registered in both <post-migration> and <install>, once each, verified by parsing the XML rather than by eye — this branch has already had one silent duplicate from a merge with a parallel session's identical fix. * fix(flow): one store per flow — migrate register-authored flows into the table Closes the second half of #2905. OpenRegister kept TWO stores for "a flow": the `openregister_flows` table behind /api/flows, and objects in the `flows` register. Every subsystem reads the table. Nothing reads the register. 🔴 MEASURED WITH CONTROLS, not inferred — the same definition, byte for byte: POST /api/flows + one FlowScheduleWorker tick -> 1 run queued POST /api/objects/18/184 + one FlowScheduleWorker tick -> 0 runs bundle {flowIds:[table-backed]} -> 1 flow bundle {flowIds:[register-authored]} -> 0 flows A schedule authored in the register never fires and never bundles, with no error anywhere — the run history is simply empty, which reads as a broken scheduler rather than a flow nothing ever saw. 🔴 AND THE DESCRIPTOR NAMED A CLASS THAT DOES NOT EXIST. `flow_register.json` called that register "the store the resolver reads by default — so triggers, sub-flows and the /test endpoint all work with a flow authored here", crediting an `OpenRegisterFlowResolver`. That identifier appears nowhere in this repository except inside that sentence. A reader following the documentation lands in the store nothing reads. The description now says what is true. THE MIGRATION copies every register-authored flow into the table: - the UUID IS PRESERVED, so sub-flow references and existing run rows resolve; - a flow already in the table is SKIPPED — this runs on every upgrade, and a second copy would fire twice; - OWNERSHIP carries over, because a flow with no organisation is invisible to every scoped read (the #2915 defect) and a migration that dropped it would move the flow into the right store and straight out of sight; - it arrives DISABLED. A schedule that starts firing during an upgrade against data nobody re-checked is worse than one an administrator switches on; - the register rows are LEFT IN PLACE, so an unattended step stays reversible; - one failure does not stop the rest, and the COUNTS are always printed. 🔴 THE TEST CAUGHT A BUG THE SUMMARY LINE HID. `Flow` inherits Nextcloud's `Entity`, whose setters are MAGIC via `__call` — so `method_exists($flow, 'setName')` is FALSE and my copy loop, guarded on it, skipped every field. The step ran, reported "1 migrated", and wrote a flow with no name, no trigger and no nodes. Only the assertion that the name came across found it; the count said success either way. Same magic-method trap as `Organisation::getUsers()` earlier in this work. Verified: 7 tests / 18 assertions; phpcs, phpstan and psalm clean. The live run happens on `occ upgrade` — repair steps cannot be invoked standalone, and `maintenance:repair` would fire every app's, so this is pinned by unit tests rather than by a live run I have not done. * fix(repair): the flow migration read ObjectEntity rows as arrays `ObjectService::findAll()` returns `ObjectEntity` objects. The migration indexed them as arrays, so enabling the app died outright: Error: Cannot use object of type OCA\OpenRegister\Db\ObjectEntity as array in lib/Repair/MigrateRegisterFlowsToTable.php:173 A fatal in a repair step during `occ app:enable` means the app does not install at all. The unit tests were green throughout, because every one of them mocked `findAll()` to return the array shape the step assumed — a fake built from the caller's assumption validates the assumption, not the service. Only CI, which actually enables the app, disagreed. Rows are now normalised through the entity's own accessors (`getObject()`, `getUuid()`, `getOwner()`, `getOrganisation()`), with the array shape still accepted so a future serialised read path stays correct. `testItReadsRealObjectEntitiesNotArrays()` builds a REAL ObjectEntity. Verified as a control: reinstate the array access and that test alone fails with the same fatal — which is exactly the property the array fixtures lacked. phpcs/phpstan/psalm clean, 8 tests pass. * fix(schemas): order the schema list newest-first, so a created schema is on it `schema-crud.spec.ts` has been failing on this branch, on both E2E runs and on retry: the spec creates a schema through the API, confirms it persisted through a GET, opens the schemas index — which renders — and then cannot find the row. ## The list was never ordered `SchemasIndex` loads every schema and slices ONE page client-side: paginatedSchemas() { const { page, limit } = this.paginationData const start = (page - 1) * limit return schemaStore.schemaList.slice(start, start + limit) // 20 rows } The slice takes whatever order the API happened to return. A just-created schema has the highest id and therefore sorts LAST, so it lands on the final page. On any instance holding more schemas than fit on a page it is simply not there — the plain "I just made this, where is it?" failure. It is not an error and not an empty list. The page renders perfectly, without the row the user is looking for, which is why nobody had seen it. ## Why it surfaced here This PR adds `ImportMergeOperationRegister`, which materialises the `merge-operation` register and its `mergeOperation` schema at install. That is one more schema in the E2E instance — enough to cross the 20-row page. The E2E log shows the run reaching schema id 24. So the spec was passing only because the seeded instance held fewer schemas than one page, and this PR pushed it over. The defect is older than the PR; the PR is what made it observable. Reverting the import would hide it again. ## The fix An explicit `orderedSchemas` — highest id first — that `paginatedSchemas` slices. Id is the only monotonic field every schema carries. This replaces an UNDEFINED order rather than a chosen one: CnIndexPage is in prop mode on this view and the component handles no `@sort`, so there is no user-facing sort to conflict with. No test is weakened to make this pass. The spec still asserts the created schema renders as a real row in the list, which is the guarantee it was written for.
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ⏭️ | ||||
| phpcs | ⏭️ | ||||
| phpmd | ⏭️ | ||||
| psalm | ⏭️ | ||||
| phpstan | ⏭️ | ||||
| phpmetrics | ⏭️ | ||||
| eslint | ⏭️ | ||||
| stylelint | ⏭️ | ||||
| build | ⏭️ | ||||
| composer | ⏭️ | ⏭️ | |||
| npm | ⏭️ | ⏭️ | |||
| app:check-code | ⏭️ | ||||
| info.xml | ⏭️ | ||||
| REUSE | ⏭️ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-27 20:50 UTC
Download the full PDF report from the workflow artifacts.
…2946) `MetricExpressionEvaluator` landed on development today with #2941 carrying nine phpcs errors, and phpcs scans `lib`. So `development` itself is red and every branch cut from it inherits the failure — it is not the fault of whatever PR happens to report it. Mine reported it on #2943, which touches neither this file nor this area. Nine errors, no behaviour change: - five inline IF statements (176, 238, 288, 313, 348) expanded to blocks. The nested ternary inside the sprintf at 313 becomes a named `$available` local, which is also the only one that was genuinely hard to read. - four `$this->expect(...)` calls given the named parameter the standard requires for calls to internal code. Also adds the `@spec` tag the class was missing, pointing at openspec/specs/aggregation-api/spec.md — the target its siblings in this directory already use, and a file that exists. VERIFIED BEHAVIOUR-NEUTRAL BY A/B, not by reading: the aggregation suite is 263 tests / 524 assertions and returns exactly that with the original file restored and again with the fixed one. The evaluator's own suite is 9 tests / 26 assertions, green, with a reflection check confirming the class under test is the edited file rather than another checkout's copy. phpcs: 9 errors -> 0, confirmed against a control run of the original through the same command after an earlier run had silently registered no sniffs at all and reported the untouched file as clean. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…s (ADR-111) (#2939) * feat(demo-data): ship demo data for every schema this app supplies (ADR-111) Three objects per (register, schema) pair, each generated from the schema itself and validated against it. Not installed automatically: a mock register is imported on demand, from the setup walkthrough or `occ openregister:descriptors:list --app=<id> --import=<slug>`. Attributed to the app id in appinfo/info.xml — `x-openregister.app` is what the descriptor inventory resolves a register to an app by, so the checkout directory name would name an app that does not exist. * fix(demo-data): drop the n8n register from openregister demo data `n8n_workflows.openregister.json` declares `x-openregister.app: n8n`. Its five schemas are the n8n app's, and they carry no `slug`, so embedding them here failed this repo's own `validate-register` on two checks. Regenerated with the generator ownership fix: 8 registers, 10 schemas, 30 objects, 0 validation failures. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…w` (#2937) Drops both `type: "custom"` flow pages and the two components behind them. FlowsIndex.vue and FlowDetailPage.vue are deleted and their registry entries removed. "type": "index", "config": { "entitySource": "flows", "columns": [...] } "type": "flow" WHAT THOSE COMPONENTS WERE. FlowsIndex rendered CnIndexPage with `:objects` from useFlowStore - the wrapper named index sources exist to remove. FlowDetailPage wrapped CnFlowDetail and handled save/run, which is exactly what the shared CnFlowEditorPage does, `router.replace` included so a newly created flow picks up its server-assigned id. Checked line by line before deleting rather than assumed. COLUMNS ARE STATED EXPLICITLY here, not left to the source defaults. This is the ENGINE's cross-app surface: it passes no app filter and so carries an `app` column a leaf app's own flow list does not. The source supplies defaults; a manifest that sets columns still wins, which is what keeps that column. `sidebarComponent: FlowDetailSidebar` is unchanged - it is a page-level key rather than part of `config`, so the controls sidebar survives the type change. Verified against the pre-change manifest. A FIX THAT COMES FREE. FlowsIndex bound `@rowClick` while CnIndexPage emits `row-click`, so opening a flow by clicking its row never worked here. The shared page wires it correctly. Both `_note`s are rewritten. They said "Custom page rather than type:index" and gave the reason - true when written, false now, and a stale note that explains a decision is worse than none. Bumped to ^2.20.0: below it the runtime does not register `flow`, and a manifest naming a page type the runtime does not know renders nothing rather than failing. Manifest validated against the installed schema (2.26.0, PASS, 0 errors) and the control build compiles - 48 artifacts. 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 | ⏭️ | ||||
| composer | ⏭️ | ⏭️ | |||
| npm | ⏭️ | ⏭️ | |||
| app:check-code | ⏭️ | ||||
| info.xml | ⏭️ | ||||
| REUSE | ⏭️ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-27 21:30 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-27 22:32 UTC
Download the full PDF report from the workflow artifacts.
PHP Quality (phpmd) fails on development with a single violation: MetricExpressionEvaluator.php:180 ElseExpression The method parseExpression uses an else expression. parseTerm, directly below it in the same file, already handles its two operators with a guard and a continue rather than if/else, and is clean. This makes parseExpression match its sibling. Behaviour is identical: the while condition admits only + and -, so the guard and the fall-through cover exactly the two branches the else did.
#2954) 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-28 07:42 UTC
Download the full PDF report from the workflow artifacts.
#2951) * fix(deps): nextcloud-vue 2.21.0, which restores the flow create button The E2E on development fails on both attempts: flow-controls.spec.ts:173 flow controls render, and a flow can be built, saved and run getByRole("button", { name: "New flow" }) — element(s) not found #2937 moved the flow list to an ordinary type:index over the named source "flows". nextcloud-vue 2.20.0 shipped named sources (#800) but read only their `columns` — `addLabel` and the routes were defined and never read, so the migration lost the create button. nc-vue #818 fixes exactly that and first ships in 2.21.0. Verified against the PUBLISHED artifacts, not the source: 2.21.0/dist carries the namedSource.addLabel branch, 2.20.0/dist does not. The lock is regenerated with npm 11 (engines.npm ^11.0.0; npm 10 does not implement min-release-age) and with the repo .npmrc in place — without its min-release-age-exclude[]=@conduction/* the two-day cooldown would have silently resolved BACKWARDS on a release-day version. The lock diff is one line: no transitive package was added, removed or moved. * fix(deps): nextcloud-vue 2.21.0, which restores the flow create button The E2E on development fails on both attempts: flow-controls.spec.ts:173 flow controls render, and a flow can be built, saved and run getByRole("button", { name: "New flow" }) — element(s) not found #2937 moved the flow list to an ordinary type:index over the named source "flows". nextcloud-vue 2.20.0 shipped named sources (#800) but read only their `columns` — `addLabel` and the routes were defined and never read, so the migration lost the create button. nc-vue #818 fixes exactly that and first ships in 2.21.0. Verified against the PUBLISHED artifacts, not the source: 2.21.0/dist carries the namedSource.addLabel branch, 2.20.0/dist does not. The lock is regenerated with npm 11 (engines.npm ^11.0.0; npm 10 does not implement min-release-age) and with the repo .npmrc in place — without its min-release-age-exclude[]=@conduction/* the two-day cooldown would have silently resolved BACKWARDS on a release-day version. The lock diff is one line: no transitive package was added, removed or moved.
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ⏭️ | ||||
| phpcs | ⏭️ | ||||
| phpmd | ⏭️ | ||||
| psalm | ⏭️ | ||||
| phpstan | ⏭️ | ||||
| phpmetrics | ⏭️ | ||||
| eslint | ⏭️ | ||||
| stylelint | ⏭️ | ||||
| build | ⏭️ | ||||
| composer | ⏭️ | ⏭️ | |||
| npm | ⏭️ | ⏭️ | |||
| app:check-code | ⏭️ | ||||
| info.xml | ⏭️ | ||||
| REUSE | ⏭️ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-28 07:49 UTC
Download the full PDF report from the workflow artifacts.
The flow-page migration is already on development here, but the lock still pinned an older release. 2.20 declares a named index source's columns, create button and row actions without reading them, so the migrated page renders a columnless table with no working create action — which reads as an empty list rather than a broken page. The lock is the operative part: CI installs with `npm ci`, so the caret alone changed nothing about what installs. 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-28 08:35 UTC
Download the full PDF report from the workflow artifacts.
.github#597 set cancel-in-progress on the shared quality.yml, but a caller-level concurrency cancels the whole run before the called workflows setting can apply -- so that fix reached only the apps that declare no concurrency of their own. Measured 2026-08-28 over push runs on development since #597 merged: caller silent 0 of 11 cancelled caller says true 7 of 13 cancelled (54%) This repo is in the second group. pull_request keeps cancelling, where superseding really is correct.
…2963) Every guard so far protects an OPERATION — find() (#2790), findAll() and the save path (#2803), the read path (#2918) — by discarding the pending schema ref as it enters. `setRegister()` is not an operation and has no such guard: it takes whatever ref is pending, re-resolves it inside the register the caller just named, and a miss THROWS out of a method whose caller only asked to name its own register. Measured on the development instance 2026-08-27, with #2918 already merged and running: every public read of a portaliq portal failed with Schema slug "application" is not carried by register "portaliq" `PortalResolver` calls setRegister('portaliq') first and setSchema('portal') after — the correct order — and never mentions `application` at all. The slug belongs to buildiq, which registers navigation from Application::boot() on every request. The portal served its shell and answered 404 for its own site, menus, pages and glossary: a whole app's public surface down over a slug it does not own. openconnector and a pipelinq repair step are recorded in this file's own comments as earlier victims of the same shape. So a ref that cannot be resolved inside THIS register is now discarded rather than thrown on, and the schema context is cleared with it. That second half is what keeps this honest: a caller that really did chain setSchema('typo')->setRegister($r) still fails — at its operation, with a missing schema context — instead of silently reading whichever table a stale context last pointed at. Substituting a wrong-but-plausible schema is the one outcome worse than throwing. The discard is logged with the ref, the register and a note saying which of the two cases the reader is in. The regression test was observed FAILING against development with the exact production message above, and passing with the change.
`flow-controls.spec.ts:173` has been failing on development with
Locator: getByRole('button', { name: 'New flow' })
Error: element(s) not found
which says nothing useful. Chasing it (#2957) ruled out five hypotheses
without touching the actual cause:
- the label is correct -- "New flow" is what nextcloud-vue 2.20.0
declares for the `flows` named index source
- the version is correct -- package.json ^2.20.0, lock pins 2.20.0
- the published artifact is complete -- indexSources.js and
useNamedSource.js are both in the npm tarball's dist/
- the control is a real button -- CnActionsBar renders NcButton with
data-testid="cn-cta-primary", not an overflow menu item
- showAdd is not suppressed -- defaults true on both components
The one fact unobservable from outside a failing run is whether
`resolveIndexSource('flows')` returned the source or null. The resolver
announces that on the console, and nothing was capturing it.
So this captures console warn/error for the duration of the test and,
when the button is missing, fails with:
- whether ANY primary CTA rendered, and its text. A named source
supplies "New flow"; an unresolved one degrades to an ordinary index
whose CTA reads "Add {type}". That single string separates the two
cases.
- the collected console lines, including the resolver's warning.
No change to what the test asserts -- it still requires the button. It
just stops throwing away the evidence when it does not appear.
Verified: npm run lint rc=0, prettier clean, `playwright test --list`
compiles the file.
Refs #2957
Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…ccess (#2956) * fix(descriptors): the demo-data import seeded nothing and reported success Found by running the ADR-111 flow on a live instance rather than trusting the unit tests. `occ openregister:descriptors:list --app=larpinq --import=larpinq` printed: larpinq: register "larpinq" imported. and seeded ZERO of the descriptor's 30 demo objects. The register ended up carrying the REAL descriptor's 11 schemas, not the mock's 10, and oc_openregister_objects stayed empty. TWO DEFECTS, both mine. 1. A MOCK IMPORTED UNDER THE APP'S OWN CONFIGURATION IDENTITY. `importFromApp` stamps `imported_config_<appId>_version` and a content hash. larpinq ships BOTH `larpinq_register.json` and `larpinq_mock_register.json`, and both declare the register slug `larpinq` -- so importing the mock under the plain app id writes over the real descriptor's stamp and reads its hash. A mock now imports under `<appId>.mock`. This is the rule ImportMergeOperationRegister already states in its own docblock, for exactly this reason: a version gate shared between two descriptors masks one of them. I applied that discipline there and not here. 2. THE POST-CHECK COULD NOT SEE A NO-OP. It asked only whether a config version existed for the slug. For a mock that is already true -- the app's own descriptor stamped it at install -- so `outcome: imported` was returned over an import that created nothing. A descriptor that declares objects and seeds none of them is now a FAILURE that names the numbers, and the command prints "N of M demo object(s) imported, K skipped" instead of a bare "imported". That is the same "state the counts, always" rule the repair steps follow, and it is what would have made this diagnosis immediate instead of a long dig through the importer. Three tests, each verified by control: reinstating either defect produces 2 failures. Note the control itself had to be fixed first -- running the suite from a worktree with a symlinked vendor/ resolves these classes out of the MAIN checkout, so the tests were green over a file I had not edited. The bootstrap now preloads the worktree copy. phpcs/phpmd/phpstan/psalm all clean; 31 tests, 92 assertions. * test(descriptors): cover the branches the counts fix added The coverage ratchet failed this branch: FAIL: coverage of the code this change KEEPS or ADDS dropped by 5.31%. base 147/155 -> head 171/191 statements Fair: the command's new counts-printing branch was never entered, because the existing test mocks `reimport()` to return `['outcome' => 'imported']` with no `counts` key at all. The branch that formats "N of M demo object(s) imported" could not run. Three tests, one per path through it: a full seed (30 of 30), a PARTIAL seed (29 of 30, 1 skipped -- a different event from a full one, and the operator deciding whether the demo is usable needs the difference), and a descriptor declaring no objects, which keeps the short sentence rather than padding every real register import with "0 of 0 demo object(s)". 27 tests, 82 assertions. Local coverage measurement was attempted and abandoned rather than faked: the worktree has no composer install of its own, and running under the main checkout's vendor/ resolves these classes out of THAT tree -- the same wrong- file trap that made an earlier control report OK over a file I had not edited. CI measures this honestly; a local number from the wrong tree would not. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…Array" (#2943) * fix(schemas): the extended-by map cast schema OBJECTS to the string "Array" `findAllExtendedBy()` resolved each `allOf`/`oneOf`/`anyOf` entry with `(string)$entry`. An entry is a SCHEMA OBJECT in standard JSON Schema — `{"$ref": "person"}` — and casting one yields the literal "Array", which matches no key in the lookup. So the reverse map came back EMPTY, while every request logged a notice per row: Array to string conversion at lib/Db/SchemaMapper.php#4148 GET /index.php/apps/openregister/api/schemas?_limit=1000 Measured across the fleet's descriptors: 236 allOf/oneOf/anyOf entries, ALL of them objects, NOT ONE a bare string. The feature was inert for every real schema — an "extended by" relationship that is never found rather than one that errors, which is why nobody had seen it. Found in a learniq E2E log, where the notice repeats on every `/api/schemas?_limit=1000`. `parentIdentifierFromAllOfEntry()` already encodes the three shapes that occur and is already tested. The defect was that this caller did not USE it — so the new arms drive `findAllExtendedBy()` itself, not the resolver: a test of the resolver would have stayed green throughout. That helper's own docblock records that an earlier caller passed arrays through and 500'd; this one passed them through and went quiet instead. 4 tests, verified by control in a tree with the app's own autoloader: with the cast reinstated, the two object-shaped arms fail and the scalar and inline arms still pass. phpcs reports 0 errors either side; the phpstan and psalm findings within reach are pre-existing and in other files. * fix(aggregation): replace the else with an early continue, satisfying both tools phpmd failed parseExpression() on ElseExpression. The `else` was mine: the earlier phpcs fix replaced an inline ternary with if/else, and the two tools forbid opposite shapes -- phpcs bans the ternary, phpmd bans the else. Neither shape is available. parseTerm(), a few lines below in the same file, already shows the way out: handle the first operator and `continue`, then fall through to the second. No ternary, no else, and it matches the file's own pattern rather than inventing a third one. VERIFIED WITH A CONTROL, and the control had to be fixed first. Running the suite from the worktree resolved this class out of the MAIN checkout, because vendor/ was symlinked there and its autoloader owns the namespace -- so the test was measuring a file I had not edited, and reported OK when subtraction was replaced by addition. Preloading the worktree copy ahead of the autoloader fixes it. With the right file under test: my refactor is OK (9 tests, 26 assertions), and breaking subtraction produces 2 failures, so the suite does discriminate. phpcs 0 findings, phpmd 0 findings, 0 else clauses in the file. --------- 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-28 09:04 UTC
Download the full PDF report from the workflow artifacts.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…2969) phpstan analyses the whole tree, so this pre-existing error surfaced on an unrelated l10n pull request (#2634) that does not touch this file: Call to function is_scalar() with int|string will always evaluate to true. Line Service/ObjectService.php [ERROR] Found 1 error `$currentSchemaRef` is `int|string|null` and the null is already excluded before this point, so `$pendingRef` is `int|string` here — the scalar test could only ever be true and the gettype() fallback was unreachable. Dropped rather than suppressed. A defaulted read that can never default reads as a handled edge case that does not exist, and hides the day the type actually widens. Verified: php -l clean, and `phpstan analyse --memory-limit=1G` over all 1504 files reports [OK] No errors — the exact command CI runs. 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-28 10:11 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.